Two copies for two jobs. rippr-src/ is a browsable git archive export of the tracked tree at 2f76983 - no build outputs, no local.properties, no nested .git - which is convenient to read in gitea but carries no history and will drift. rippr-full-history.bundle is the real backup: all 18 commits, verified as "records a complete history" and test-cloned before committing. This matters because ~/dojo/rippr has no git remote and otherwise exists only on one machine. rippr-src/SNAPSHOT.md explains the difference and how to restore. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4.9 KiB
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:
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:
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
- Application-level init of
Configurationbefore first map use. Config.mapEnabledgetter/setter following the existing pattern.ui/components/OsmMap.kt— theAndroidViewwrapper with full lifecycle wiring.- Replace the T11 map placeholder with
OsmMap, gated on the toggle. - Toggle switch in the detail screen.
- Verify no permission was added to the merged manifest.
Acceptance criteria
- Tiles render on trip detail
- 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
- No new permission in the merged manifest
TrackingServicecontains no reference to any map type- 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
MapViewis 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.