Skip to content

ADR-0026: Location capture (opt-in) and offline coordinate picker

Context

ADR-0021 landed the storage (schema v6 location columns on transactions and unmatched) and the pipeline seam (CapturePipeline.onNotification(..., location?)), but capture never shipped — the listener passed null and no permission was requested. This ADR ships capture: a device fix at notification-parse time (opt-in) and a manual editor for correcting/adding location.

Two constraints shape it:

  1. The app is offline-first with no INTERNET permission (ADR-0015), so an online tile map or geocoder is unavailable.
  2. The notification listener runs in the background. Actively requesting a live GPS fix from a background service requires ACCESS_BACKGROUND_LOCATION, a Play-restricted permission.

Decision

  • Capture source: cached last-known location. AndroidLocationProvider reads LocationManager.getLastKnownLocation (GPS, network, passive — freshest wins, ≤24 h old). This needs only ACCESS_COARSE_LOCATION/ACCESS_FINE_LOCATION and avoids ACCESS_BACKGROUND_LOCATION entirely. The trade-off — a possibly stale fix — is acceptable for an approximate "where did I spend" signal.
  • Gate: capture happens only when KEY_CAPTURE_LOCATION is "true" and a location permission is held; otherwise the listener passes null (the no-location case the issue allows).
  • Opt-in: onboarding gains a fourth step (toggle + permission request); Settings exposes the same toggle under Automation (KEY_CAPTURE_LOCATION).
  • Manual edit: TransactionDetailScreen gains a location row (coords or "No location data") opening a full-screen offline coordinate picker (TransactionLocationScreen): a Canvas grid, tap/drag pin, "locate current" button, Save/Cancel. No map tiles, no search, no network — honors ADR-0015. The picker projects a pixel offset from an anchor point into lat/lng via the pure common/Geo helpers.
  • Retention: keep the raw fix; placeName/placeId remain null until reverse geocoding ships.

Consequences

  • Play-restricted ACCESS_BACKGROUND_LOCATION is not requested, simplifying Play review (see docs/PLAY_POLICY.md).
  • Location is approximate (last-known); no continuous tracking, no live fix in the background.
  • Data Safety now discloses location collection (on-device, opt-in, not shared); docs/user-guide/security.md and docs/PLAY_POLICY.md drop the "never reads location" claim.
  • The coordinate picker is deliberately coarse (no street context); real map tiles or search require revisiting ADR-0015.

Rejected alternatives

  • FusedLocationProviderClient.getCurrentLocation — adds play-services-location and, from a background listener, requires ACCESS_BACKGROUND_LOCATION.
  • OSMDroid/MapLibre with bundled offline tiles — larger APK and tile-set maintenance for marginal gain; a coordinate picker covers pin-a-point needs.
  • Google Maps Compose — requires an API key + network tiles, conflicts with ADR-0015.
  • Online geocoding search in the picker — requires INTERNET, rejected.