14 KiB
CLAUDE.md
This file provides guidance to Claude Code when working with code in this repository.
Project Overview
This is an Android navigation app built with Jetpack Compose that supports multiple routing providers (OSRM, Valhalla, TomTom) and includes Android Auto/Automotive OS integration. The app uses MapLibre for rendering, Androidx DataStore for local persistence, and Koin for dependency injection.
Build Commands
# Build the app (from repository root)
./gradlew :app:assembleDebug
# Build a specific flavor
./gradlew :app:assemblePlayDebug
./gradlew :app:assembleDemoDebug
./gradlew :app:assembleFullDebug
# Run unit tests
./gradlew test
# Run tests for a specific module
./gradlew :common:data:test
./gradlew :common:car:test
# Install on device
./gradlew :app:installDebug
# Clean build
./gradlew clean
Module Structure
The project uses a multi-module architecture (see settings.gradle.kts):
- app/ - Main Android app with Jetpack Compose UI for phone (
com.kouros.navigation) - common/data/ - Core data layer with routing logic, repositories, view models, persistence (
com.kouros.data) - common/car/ - Android Auto/Automotive OS UI implementation
- automotive/ - Placeholder for future native Automotive OS app (no Kotlin sources yet)
Dependencies flow: app → common:car → common:data
Architecture
Routing Providers (Pluggable System)
The app supports three routing engines that extend the NavigationRepository abstract class (common/data/.../data/NavigationRepository.kt):
- ValhallaRepository - Valhalla routing engine (ordinal 0)
- OsrmRepository - OSRM routing engine (ordinal 1)
- TomTomRepository - TomTom routing engine (ordinal 2, default)
Selection is driven by RouteEngine enum order (see Data.kt). NavigationRepository also exposes shared HTTP helpers (fetchUrl, searchPlaces, reverseAddress) that use Nominatim for geocoding and ApplicationConfig-backed HTTP basic auth for protected endpoints.
Each provider has a corresponding mapper (OsrmRoute, ValhallaRoute, TomTomRoute) that converts provider-specific JSON into the universal Route model. The universal model is decomposed into a dedicated data/route/ package: Routes, Leg, Step, Maneuver, Intersection, Lane, Summary.
Adding a new routing provider:
- Create
NewProviderRepositoryextendingNavigationRepositoryundercommon/data/src/main/java/com/kouros/navigation/data/<provider>/ - Implement
getRoute()andgetTraffic() - Create
NewProviderRoute.ktwith amapToRoute(response, builder)function that populates aRoute.Builder - Add a
RouteEngineenum entry and provider branch inRoute.Builder.route()(data/Route.kt) - Update
NavigationUtils.getViewModel()(utils/NavigationUtils.kt) to return aNavigationViewModelwired to the new repository
Data Flow
User action (search / select destination)
↓
NavigationViewModel.loadRoute() [LiveData / Flow]
↓
NavigationRepository.getRoute() [selected provider]
↓
*Route.mapToRoute() [convert to universal Route]
↓
RouteModel.startNavigation()
↓
RouteCalculator.findStep() [on each location update]
↓
NavigationState updated → UI observes and renders current step
Key Classes
Navigation logic (common/data/.../model/):
RouteModel.kt- Core navigation engine; tracks position, manages step progression, ownsNavigationStateRouteCalculator.kt- Step-finding algorithm: snaps current location to the nearest waypoint, computes leftover distance, handles snap correction and reroute thresholdsRouteCarModel.kt(common/car/.../navigation/) - Extends RouteModel with Android Auto-specific formattingNavigationViewModel.kt- androidx ViewModel exposing route, traffic, places (Nominatim), amenities (Overpass), and fuel prices (Tankerkönig) as LiveDataSettingsViewModel.kt- State holder for DataStore-backed settings (dark mode, 3D, routing engine, avoid preferences, etc.)BaseStyleModel.kt- Map style state
Data models (common/data/.../data/):
Route.kt- Universal route wrapper withRoute.Builderand provider dispatchdata/route/*- Decomposed route components (Routes,Leg,Step,Maneuver,Intersection,Lane,Summary)NavigationState.kt- Immutable navigation state (route, flags, location, bearing, maneuver, destination)Data.kt- Shared types (Place,StepData,SearchFilter,Locations,ValhallaLocation) plusobject Constantsand theRouteEngine,DarkMode,EngineType,ViewStyle,NavigationThemeColorenumsApplicationConfig.kt- LoadsUSER/PASSWORDfromBuildConfigfor HTTP basic auth on protected endpoints
Persistence (common/data/.../data/datastore/ and common/data/.../repository/):
DataStoreManager.kt- Single source of truth for preference keys; exposesFlow<T>reads andsuspendwrites for each settingSettingsRepository.kt- Higher-level wrapper overDataStoreManager
Repositories (common/data/.../data/):
NavigationRepository.kt- Abstract base class for routing providers; also handles Nominatim geocoding (search and reverse)osrm/,valhalla/,tomtom/- Provider implementations and JSON DTOsfuel/FuelPrices.kt- Tankerkönig fuel-price client (returnsList<Station>)overpass/- Overpass API client for POIs and speed limits
Android Auto / Automotive (common/car/):
NavigationCarAppService.kt- CarAppService entry pointCarSession.kt(abstract) and concreteNavigationSession.kt,ClusterSession.ktscreen/NavigationScreen.kt- Main navigation template;NavigationTypeenum drives template selection (VIEW, NAVIGATION, REROUTE, RECENT, ARRIVAL)screen/SearchScreen.kt,RoutePreviewScreen.kt,PlaceListScreen.kt,CategoriesScreen.kt,CategoryScreen.kt,StopOverScreen.kt,RequestPermissionScreen.ktscreen/settings/*- Per-setting screens (RoutingSettings, NavigationSettings, DisplaySettings, DarkModeSettings, DistanceSettings, AudioSettings, CarSettings, PasswordSettings)screen/observers/*- Observer pattern that bridges LiveData to car screens.NavigationObserverManagerorchestratesRouteObserver,TrafficObserver,TrafficMessageObserver,PlaceSearchObserver,CategoryObserver,MaxSpeedObserver,SpeedCameraObserver- Supporting managers:
SurfaceRenderer.kt(virtual display + map),DeviceLocationManager.kt,CarSensorManager.kt,NavigationNotificationManager.kt+NavigationNotificationService.kt,TextToSpeechManager.kt,CustomLifecycleOwner.kt map/MapView.kt,map/LocationPuck.kt- MapLibre rendering for the car surface
External APIs
| Service | Purpose | URL |
|---|---|---|
| OSRM | Routing | https://router.project-osrm.org/route/v1/driving/ |
| Valhalla | Routing | https://kouros-online.de/valhalla/route?json= (HTTP basic auth) |
| TomTom | Routing | https://api.tomtom.com/routing/1/calculateRoute/ |
| TomTom | Traffic incidents | https://api.tomtom.com/traffic/services/5/incidentDetails |
| Nominatim | Geocoding (search/reverse) | https://nominatim.openstreetmap.org/ |
| Overpass | POIs & speed limits | OpenStreetMap Overpass API (DEBUG builds use https://kouros-online.de/api/interpreter) |
| Tankerkönig | Fuel prices | https://creativecommons.tankerkoenig.de/json/list.php? (API key in fuel/FuelPrices.kt) |
In DEBUG builds, TomTomRepository and FuelPrices can be pointed at a local fixture (see useLocal flags) — TomTom can fall back to R.raw.tomom_routing, fuel falls back to a local JSON URL.
Important Constants
Defined in object Constants inside common/data/.../data/Data.kt:
NEXT_STEP_THRESHOLD = 500.0 // Distance (m) to show next maneuver
DESTINATION_ARRIVAL_DISTANCE = 10.0 // Distance (m) to trigger arrival
MAXIMAL_SNAP_CORRECTION = 50.0 // Max distance (m) to snap to route
MAXIMAL_ROUTE_DEVIATION = 100.0 // Max deviation (m) before reroute
NEAREST_LOCATION_DISTANCE = 10F
SPEED_UPDATE_DISTANCE = 600F
INSTRUCTION_DISTANCE = 50
SPEED_BEARING_DEVIATION = 60
TILT = 60.0 // Map tilt in degrees during navigation
TANKER_KOENIG_DELAY = 300_000 // ms between fuel price refreshes
DataStore keys (see DataStoreManager.PreferencesKeys):
RoutingEngine(Int) —0=Valhalla,1=OSRM,2=TomTom(default 2)DarkMode(Int),Show3D(Bool),CarLocation(Bool)AvoidMotorway,AvoidTollway,AvoidFerry(Bool)LastRoute,RecentPlaces,FuelPrices,LastFuelPricesTomTomApiKey,DistanceMode,GuidanceAudio,Traffic,TripSuggestion,EngineType,AlternativeRoutes
Navigation Flow
- Route loading — User searches via Nominatim → selects place →
NavigationViewModel.loadRoute()calls the selected repository - Route parsing — Provider JSON →
*Route.mapToRoute()populatesRoute.Builder→ universalRoute→RouteModel.startNavigation() - Location tracking —
FusedLocationProviderClientupdates →RouteModel.updateLocation() - Step calculation —
RouteCalculator.findStep()snaps location to the nearest waypoint, returnsStepMatch, and updates the current step index - UI updates —
NavigationStateflows out via LiveData/Compose state; phone and car UIs render the current/next step (instruction, distance, icon, lanes) - Arrival — When distance <
DESTINATION_ARRIVAL_DISTANCE, navigation ends
Testing Navigation
The phone app supports mock locations for testing:
- Set
useMock = trueinMainActivity - Enable "Mock location app" in Android Developer Options
- Choose mode in
model/Simulation.kt/model/MockLocation.kt:type = 1— Simulate movement along the entire routetype = 2— Test a specific step rangetype = 3— Replay a GPX track file
The car module has its own navigation/Simulation.kt for car-side simulation.
Unit tests
common/data/src/test/.../model/RouteCalculatorTest.ktcommon/data/src/test/.../model/RouteModelTest.ktcommon/data/src/test/.../model/IconMapperTest.ktcommon/data/src/test/.../model/OverpassTest.ktcommon/data/src/test/.../utils/GeoUtilsTest.ktcommon/car/src/test/.../screen/NavigationScreenTest.ktcommon/car/src/test/.../screen/observers/CategoryObserverTest.kt,ObserversTest.kt
Persistence
All app preferences are stored via Androidx DataStore Preferences (navigation_settings data store). There is no Room/ObjectBox/SQL database — the only persisted entities are settings and serialized lists (recent places, last route, last fuel prices) kept as strings.
DataStoreManager exposes a Flow<T> per setting plus a matching suspend set* writer. SettingsRepository wraps it for higher-level access. SettingsViewModel and AppViewModel consume those flows for UI state.
Compose UI Structure
Phone app (app/src/main/java/com/kouros/navigation/):
ui/MainActivity.kt- Entry point with permission handling and Navigation Compose hostui/MapView.kt- MapLibre rendering with camera state managementui/SheetLayout.kt- Bottom sheet scaffoldui/PermissionScreen.ktui/navigation/AppNavGraph.kt- Compose navigation graphui/navigation/NavigationScreen.kt,NavigationSheet.kt- Turn-by-turn UIui/search/SearchScreen.kt,SearchSheet.ktui/settings/SettingsScreen.kt,SettingsRoute.kt,DisplayScreen.kt,NavigationScreen.kt,CarScreen.kt,Settings.ktui/components/SettingItem.kt,SettingSwitch.kt,RadioButtonSingleSelection.kt,SectionTitle.ktui/app/AppViewModel.kt,AppViewModelProvider.ktui/theme/{Color,Type,Shapes,Theme}.ktmodel/Simulation.kt,model/MockLocation.kt- Mock location for testingdi/appModule.kt- Koin moduleMainApplication.kt- Application class
Android Auto uses CarAppService templates (NavigationTemplate, MessageTemplate, MapWithContentTemplate). NavigationType (in screen/NavigationScreen.kt) controls which template to render. UI state is synchronized with NavigationViewModel through the observers in screen/observers/.
Build Flavors
app/build.gradle.kts defines three product flavors under the store dimension:
- play - applicationIdSuffix
.play - demo - applicationIdSuffix
.demo - full - applicationIdSuffix
.full
Base namespace is com.kouros.navigation; current versionName is 0.3.0.109. Java/Kotlin target is 21 for the app module, 11 for common:data. compileSdk/targetSdk = 37, minSdk = 33.
signing.properties (root, gitignored) provides the keystore credentials for both debug and release builds. local.properties provides USER / PASSWORD build config fields consumed by ApplicationConfig.
Common Patterns
Dependency injection (Koin):
single { OsrmRepository() }
viewModel { NavigationViewModel(get()) }
LiveData observation:
viewModel.route.observe(this) { routeJson ->
routeModel.startNavigation(routeJson, context)
}
Step-finding algorithm:
RouteCalculator iterates the current step's waypoints, calculates distance from the current location to each, and snaps to the nearest waypoint. Snap is rejected if the closest point exceeds MAXIMAL_SNAP_CORRECTION; deviation greater than MAXIMAL_ROUTE_DEVIATION triggers a reroute.
Settings flow:
val darkMode by viewModel.darkMode.collectAsStateWithLifecycle()
Known Limitations
- Valhalla route mapping is incomplete in places (search for TODO comments in
data/valhalla/ValhallaRoute.kt) - Rerouting logic exists but needs more testing
- Speed-limit queries via Overpass API could be optimized for performance
- TomTom and Tankerkönig clients have DEBUG-only
useLocalshortcuts that hit fixture URLs (http://192.168.1.37/...andR.raw.tomom_routing) — these need to be off for any real-network testing - The
automotivemodule is wired into Gradle but has no Kotlin source yet