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>
120 lines
4.9 KiB
Markdown
120 lines
4.9 KiB
Markdown
# 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.
|