Files
samplez/rippr-src/docs/v2/13-map-integration.md
uhryniuk 280fd7f988 Add Rippr source snapshot and full-history bundle
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>
2026-08-11 08:30:25 -05:00

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.