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

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

  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

  • 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
  • TrackingService contains 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 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.