ADR-0034: Background location capture (live one-shot fix)
- Status: Accepted
- Date: 2026-09-05
- Deciders: maintainers
- Related: ADR-0026 (partially superseded), ADR-0021, ADR-0015
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:
- 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
getLastKnownLocationand single-update requests then silently return null — regardless of what we read. Only "Allow all the time" (ACCESS_BACKGROUND_LOCATION) unblocks background reads. - 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 onKEY_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/LocationPermissionsis 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 thanFRESH_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 pureLocationFixes.needsLiveFix. - Foreground picker unchanged:
lastKnownLocation()still powers the picker's locate button (foreground reads are not app-op-blocked).
Consequences
- Play-restricted
ACCESS_BACKGROUND_LOCATIONmust be declared in the Play Console with a prominent-disclosure;docs/PLAY_POLICY.mdand the Data Safety row now state it. Review burden increases; the fallback channels (side-load, F-Droid) indocs/PLAY_POLICY.mdcover 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:
CapturedLocationcolumns 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.