Foreground location service: permissions, notification, battery exemption (#15) #97

Merged
robert merged 1 commit from area/foreground-location-service into main 2026-09-04 17:32:59 +02:00
Owner

Closes #15.

What this does

  • RideService (companion/ride/src/main/kotlin/de/butzei/pedalpebble/ride/RideService.kt) — the foreground Service that keeps GPS updates alive for a ride: a persistent low-importance notification (mandatory for any foreground service), android:foregroundServiceType="location" plus the API 34+ FOREGROUND_SERVICE_LOCATION permission, a partial wake lock held only while a ride is running, and START_STICKY restart handling that resumes the same running state if the system kills the process. Declared in the app module's manifest, not :companion:ride's own, per that module's existing manifest note that a foreground service's notification/lifecycle is an app-composition concern.
  • AndroidLocationSource (:companion:location) — the GPS source behind it, on android.location.LocationManager's API 31+ LocationRequest.Builder at 1 Hz (NFR-B2). No Google Play services dependency: minSdk is already 31, which is exactly where that builder API starts.
  • RideSetupActivity (:companion, reached only from a new "Start ride" button — never at app launch) walks the rider through each permission one at a time, each with its own plain-language rationale screen (NFR-S6): ACCESS_FINE_LOCATION+ACCESS_COARSE_LOCATION together, then ACCESS_BACKGROUND_LOCATION as a separate later request, then POST_NOTIFICATIONS (skipped below API 33), then the battery-optimisation exemption.
  • RideSetupState (:companion:core) — the ordering above as a pure, android.*-free sealed-class decision function, unit tested on the JVM lane (RideSetupStateTest, 8 cases). RidePermissions (:companion:location) is the thin Android-side bridge that supplies its real inputs.
  • Battery exemption uses the direct ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS dialog, not a Settings redirect — Play's Battery Optimization policy accepts this for apps whose core function is continuous background location tracking, which is exactly what a ride's foreground GPS service is. Checked against developer.android.com and Play's policy centre (2026-09-04); see BatteryOptimizationIntents's kdoc for the citation.

Verified

  • ./gradlew :companion:assembleDebug — succeeds, clean.
  • ./gradlew :companion:core:test — passes, including the new RideSetupStateTest.
  • ./gradlew :companion:lintDebug — succeeds; the only new finding is an informational InlinedApi note on the POST_NOTIFICATIONS reference (expected and harmless — minSdk 31, compileSdk 37).
  • Inspected the merged manifest directly (companion/build/intermediates/packaged_manifests/debug/.../AndroidManifest.xml): all permissions present, RideService declared with foregroundServiceType="location".

Not in scope here (by design)

Actual speed/distance computation and BLE wheel-sensor arbitration are #17/#19. Full ride-session state restoration across a process death (elapsed distance, position, etc.) is #12 — this issue's restart handling only proves the platform-survival shell (notification, wake lock, GPS updates) resumes, not that a ride's numbers come back with it. A real multi-hour screen-off ride verification is a manual/field-test step this sandbox can't perform.

https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt

Closes #15. ## What this does - **`RideService`** (`companion/ride/src/main/kotlin/de/butzei/pedalpebble/ride/RideService.kt`) — the foreground `Service` that keeps GPS updates alive for a ride: a persistent low-importance notification (mandatory for any foreground service), `android:foregroundServiceType="location"` plus the API 34+ `FOREGROUND_SERVICE_LOCATION` permission, a partial wake lock held only while a ride is running, and `START_STICKY` restart handling that resumes the same running state if the system kills the process. Declared in the **app module's** manifest, not `:companion:ride`'s own, per that module's existing manifest note that a foreground service's notification/lifecycle is an app-composition concern. - **`AndroidLocationSource`** (`:companion:location`) — the GPS source behind it, on `android.location.LocationManager`'s API 31+ `LocationRequest.Builder` at 1 Hz (NFR-B2). No Google Play services dependency: minSdk is already 31, which is exactly where that builder API starts. - **`RideSetupActivity`** (`:companion`, reached only from a new "Start ride" button — never at app launch) walks the rider through each permission one at a time, each with its own plain-language rationale screen (NFR-S6): `ACCESS_FINE_LOCATION`+`ACCESS_COARSE_LOCATION` together, then `ACCESS_BACKGROUND_LOCATION` as a separate later request, then `POST_NOTIFICATIONS` (skipped below API 33), then the battery-optimisation exemption. - **`RideSetupState`** (`:companion:core`) — the ordering above as a pure, `android.*`-free sealed-class decision function, unit tested on the JVM lane (`RideSetupStateTest`, 8 cases). `RidePermissions` (`:companion:location`) is the thin Android-side bridge that supplies its real inputs. - Battery exemption uses the direct `ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` dialog, not a Settings redirect — Play's Battery Optimization policy accepts this for apps whose core function is continuous background location tracking, which is exactly what a ride's foreground GPS service is. Checked against developer.android.com and Play's policy centre (2026-09-04); see `BatteryOptimizationIntents`'s kdoc for the citation. ## Verified - `./gradlew :companion:assembleDebug` — succeeds, clean. - `./gradlew :companion:core:test` — passes, including the new `RideSetupStateTest`. - `./gradlew :companion:lintDebug` — succeeds; the only new finding is an informational `InlinedApi` note on the `POST_NOTIFICATIONS` reference (expected and harmless — minSdk 31, compileSdk 37). - Inspected the merged manifest directly (`companion/build/intermediates/packaged_manifests/debug/.../AndroidManifest.xml`): all permissions present, `RideService` declared with `foregroundServiceType="location"`. ## Not in scope here (by design) Actual speed/distance computation and BLE wheel-sensor arbitration are #17/#19. Full ride-session state restoration across a process death (elapsed distance, position, etc.) is #12 — this issue's restart handling only proves the platform-survival shell (notification, wake lock, GPS updates) resumes, not that a ride's numbers come back with it. A real multi-hour screen-off ride verification is a manual/field-test step this sandbox can't perform. https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt
Foreground location service: permissions, notification, battery exemption (#15)
Some checks failed
dev-artifact / build-pbw (push) Failing after 0s
dev-artifact / build-apk (push) Failing after 0s
dev-artifact / publish (push) Has been skipped
fast-lane / host-c-tests (push) Failing after 0s
fast-lane / jvm-tests (push) Failing after 0s
fast-lane / pebble-build (push) Failing after 0s
fast-lane / lint-and-secrets (push) Failing after 0s
fast-lane / meta-declares-required-jobs (push) Failing after 0s
fast-lane / host-c-tests (pull_request) Failing after 0s
fast-lane / jvm-tests (pull_request) Failing after 0s
fast-lane / pebble-build (pull_request) Failing after 0s
fast-lane / lint-and-secrets (pull_request) Failing after 0s
fast-lane / meta-declares-required-jobs (pull_request) Failing after 0s
aac5bc67c3
RideService (:companion:ride) is the foreground Service that keeps GPS updates
alive for a ride: a persistent low-importance notification (mandatory for any
foreground service), android:foregroundServiceType="location" plus the API 34+
FOREGROUND_SERVICE_LOCATION permission, a partial wake lock held only while a
ride is running, and START_STICKY restart handling that resumes the same
running state if the system kills the process (full ride-state restoration
stays #12's job — this only proves the platform-survival shell holds).

AndroidLocationSource (:companion:location) is the GPS source behind it, built
directly on android.location.LocationManager's API 31+ LocationRequest.Builder
at 1 Hz (NFR-B2) — no Play Services dependency needed since minSdk is already
31, the same "already past the workaround" shape as D19's BLE call.

RideSetupActivity (:companion, launched only from a "Start ride" action, never
at app launch) walks the rider through each permission one at a time, each
with its own plain-language rationale screen (NFR-S6):
ACCESS_FINE_LOCATION+ACCESS_COARSE_LOCATION, then ACCESS_BACKGROUND_LOCATION
(a separate, later request — Android denies it if requested alongside
foreground location), then POST_NOTIFICATIONS (skipped below API 33), then the
battery-optimisation exemption. The ordering is driven by RideSetupState, a
pure sealed-class decision function in :companion:core with no android.*
import (RideSetupStateTest, JVM/Kotest) — RidePermissions in
:companion:location supplies its Android-side inputs.

Battery exemption uses the direct ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
dialog rather than routing to Settings: Play's Battery Optimization policy
accepts this for apps whose core function is continuous background location
tracking, which a ride's foreground GPS service is (checked against
developer.android.com and Play's policy centre, 2026-09-04).

Verified with a real build: ./gradlew :companion:assembleDebug,
:companion:core:test (RideSetupStateTest), and :companion:lintDebug all pass;
the merged manifest carries the expected permissions and the RideService
<service> element with foregroundServiceType="location".

Claude-Session: https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt
robert merged commit 654a955d08 into main 2026-09-04 17:32:58 +02:00
Sign in to join this conversation.
No description provided.