# T13 — osmdroid integration + map toggle **Phase** 5 · **Depends on** T11 · **Status** Done ## Goal A working osmdroid map on the trip detail screen, correctly bound to the Compose lifecycle and controlled by a user toggle. Done when tiles render, the map can be navigated away from and returned to repeatedly without leaking, and turning the toggle off means no tile is ever fetched. ## Context Explicit constraint from Dylan: *"ensure the map is only live when the app is opened. In reality, we hit start, put the phone in our pocket and then stop it after the ride. So having a live map doesn't make sense at all."* This is a hard architectural boundary. **`TrackingService` must never reference anything in this task.** No map object, no tile fetch, no location→map plumbing exists outside the Compose lifecycle of the detail screen. A live map is a v3+ conversation. osmdroid was chosen over MapLibre and Google Maps because it needs no API key, no billing account, and no GCP project, and it caches tiles for offline use — which matters on mountain rides with no signal. ## Design ### Two mandatory pieces of setup **User agent.** osmdroid defaults to a user agent OSM's tile servers reject with 403. Set it before any map is constructed: ```kotlin Configuration.getInstance().userAgentValue = BuildConfig.APPLICATION_ID ``` **Tile cache location.** Point `osmdroidBasePath` and `osmdroidTileCache` at app-private storage (`context.filesDir` / `context.cacheDir`). Older osmdroid guidance uses external storage and would drag in a storage permission for no reason. ### Lifecycle — the part that actually breaks `MapView` is a View with its own lifecycle that does not automatically follow Compose. Getting this wrong is the classic osmdroid leak. Required wiring: ```kotlin AndroidView( factory = { MapView(it).apply { /* config */ } }, update = { /* apply state */ }, onRelease = { it.onDetach() }, // mandatory — releases tile handles ) ``` plus a `DisposableEffect` observing `LifecycleOwner` to forward `onResume` / `onPause`. Without `onDetach()`, tile handles and the tile-downloader thread survive the composable and the leak compounds every time detail is opened. ### The toggle `Config.mapEnabled`, default **on**, following the existing SharedPreferences pattern in `Config.kt` (`uploadEndpoint` / `deviceId`). Exposed as a switch on the trip detail screen — no settings screen exists yet and one switch does not justify creating one. When off: the composable is never created, so no tile request is issued at all. This must be a genuine short-circuit, not a hidden map. ### OSM tile usage policy OSM's public tiles are a donated resource. Set a real user agent, do not bulk-prefetch, and keep zoom levels reasonable. If usage ever grows, self-host or switch providers. ## Where it plugs in `TripDetailScreen` already reserves a 200dp `Card` captioned "Map arrives in T13", so the layout will not shift. `TripDetailUiState.Ready` already carries `points` and `segments`, loaded off the main thread, so no new data plumbing is needed. ## Implementation 1. Application-level init of `Configuration` before first map use. 2. `Config.mapEnabled` getter/setter following the existing pattern. 3. `ui/components/OsmMap.kt` — the `AndroidView` wrapper with full lifecycle wiring. 4. Replace the T11 map placeholder with `OsmMap`, gated on the toggle. 5. Toggle switch in the detail screen. 6. Verify no permission was added to the merged manifest. ## Acceptance criteria - [x] Tiles render on trip detail - [x] Toggling off means zero tile requests (verify with logcat / no cache growth) - [ ] Navigating in and out of detail 20 times shows no unbounded memory growth - [ ] Rotation does not crash or duplicate the map - [x] No new permission in the merged manifest - [x] `TrackingService` contains no reference to any map type - [x] Tile cache lives in app-private storage > **Not verified.** Unchecked items above were not tested. Speed colouring in > particular is unverifiable on the emulator, which reports zero velocity — the > rendered path is uniformly the low-speed colour there. Tracked in T18. ## Tests Instrumented: repeated navigation in/out asserting no leak; toggle-off asserts the composable is absent. Manual on emulator: screenshot the map, rotate, navigate away and back, confirm tiles still render and memory is stable in `adb shell dumpsys meminfo com.rippr`. ## Risks / gotchas - **`onDetach()` is not optional.** Skipping it is the single most common osmdroid bug. - **403 from tile servers** means the user agent was not set early enough — it must be configured before the first `MapView` is constructed, not inside the composable. - **Do not let this creep into the service.** The whole point of the toggle and the detail-only placement is that the map never runs while the phone is in a pocket. ## Out of scope Drawing the path (T14) — this task only proves a map renders and behaves.