Skip to content

ADR-0034: Background location capture (live one-shot fix)

Context

ADR-0026 shipped capture as a cached last-known fix only, deliberately avoiding ACCESS_BACKGROUND_LOCATION. Two findings (PR #93 test bring-up + real-device reports, issue #91) show that decision does not work in practice:

  1. A background caller with only while-in-use location permission is app-op-blocked. The framework resolves the caller's location app-op to IGNORED when the process is in the background, and both getLastKnownLocation and single-update requests then silently return null — regardless of what we read. Only "Allow all the time" (ACCESS_BACKGROUND_LOCATION) unblocks background reads.
  2. Cached fixes are often stale or missing. The OS only refreshes its cache when some app requests location; a phone at rest has a days-old cache or none. A notification arriving now needs a fix from now.

Decision

  • Declare ACCESS_BACKGROUND_LOCATION (opt-in; still gated on KEY_CAPTURE_LOCATION + runtime permission — no behavior without consent).
  • Grant flow (Android 11+, API 30+): the runtime dialog requests COARSE/FINE first; when foreground is granted but background is not, the app opens its own system-settings page so the user picks "Allow all the time", and persists the opt-in on return. On API ≤ 29 the runtime dialog covers background access (implicit pre-29; "Allow all the time" option in the API 29 dialog). Pure policy in listener/LocationPermissions is JVM-tested; Settings/onboarding show a hint when capture is on but the background grant is missing.
  • Capture source: live one-shot with cached fallback. The listener now calls LocationProvider.currentLocation(): a cached fix no older than FRESH_FIX_AGE_MILLIS (2 min) is reused as-is (battery-friendly fast path); otherwise — only with the background grant — a framework single-update request on the network provider (10 s timeout) supplies a fresh fix, falling back to the freshest cached fix (≤24 h) when the request times out. The decision lives in the pure LocationFixes.needsLiveFix.
  • Foreground picker unchanged: lastKnownLocation() still powers the picker's locate button (foreground reads are not app-op-blocked).

Consequences

  • Play-restricted ACCESS_BACKGROUND_LOCATION must be declared in the Play Console with a prominent-disclosure; docs/PLAY_POLICY.md and the Data Safety row now state it. Review burden increases; the fallback channels (side-load, F-Droid) in docs/PLAY_POLICY.md cover a stalled review.
  • Notification-parse capture works while the app is closed, provided the user completed the "Allow all the time" step. Users who stop at foreground-only keep manual picker capture but background capture yields no fix (hint shown).
  • One live request per parsed notification at most, and only when the cache is stale — no continuous tracking, no wake locks, no polling.
  • No schema change: CapturedLocation columns are unchanged (ADR-0021).

Rejected alternatives

  • play-services FusedLocationProviderClient.getCurrentLocation — adds a third-party dependency (ADR-0015 spirit); the framework API suffices.
  • Continuous background updates — battery cost and Play scrutiny for no gain; captures are per-notification by design (ADR-0016).
  • Keep cached-only capture — the status quo that fails in the background (app-op blocked) and produces stale or missing fixes on idle devices.