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>
This commit is contained in:
119
rippr-src/docs/v2/13-map-integration.md
Normal file
119
rippr-src/docs/v2/13-map-integration.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user