# GPS2Audio v2.0.7 — Atmo Service-Player Handoff **Version**: 2.0.7 · **versionCode**: 35 · **Build type**: Release (signed) **Baseline**: v2.0.6 (`GPS2Audio_v2_0_6_atmo_background_fix_source.zip`) **Signing**: `GPS2Audio_release_keystore.jks` (alias `gps2audio_release`) **Signature scheme**: APK Signature Scheme v2 (verified) **Package**: `de.waypointaudio` · **applicationId** unchanged. ## Artifacts | Path | Purpose | | --- | --- | | `/home/user/workspace/GPS2Audio_v2_0_7_atmo_service_player_signed.apk` | Signed release APK (≈15 MB) | | `/home/user/workspace/GPS2Audio_v2_0_7_atmo_service_player_source.zip` | Full source tree (no build dir, no keystore) | | `/home/user/workspace/GPS2Audio_v2_0_7_atmo_service_player_handoff.md` | This document | --- ## Root cause (why v2.0.6 still failed) Even after the wake-mode fix in v2.0.6, **the ExoPlayer instance was still owned by an application-process singleton** (`BackgroundMusicPlayer`, held by `BackgroundMusicManager`). The `AtmoPlaybackService` was a *shell foreground service*: it only posted a notification and held the `mediaPlayback` foreground type — it did **not** own the player. This split ownership is the architecture that Android 12+ specifically penalises. The OS heuristic that allows background audio to survive screen-off and process-cached transitions is: > A `FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK` service must be the visible owner > of the audio playback. Detached playback (player in app process, FGS > elsewhere) is increasingly treated as a candidate for pause/throttle. In addition, the service in v2.0.6 was `START_NOT_STICKY` and self-stopped on any `intent.action == null` re-delivery. Combined with split ownership, any brief process-state transition (e.g. Activity → background while another FGS context is busy) could cause the ExoPlayer to lose its execution priority and the audio pipeline to pause — `setWakeMode()` alone is not enough on heavily-Doze'd OEMs. ## Architectural change in v2.0.7 The fix is to give the foreground service **direct ownership of the ExoPlayer**. The Activity / ViewModel / Manager no longer touch the player directly. ``` ┌───────────────────────────────────────┐ │ AtmoPlaybackService │ │ (foreground · mediaPlayback) │ │ │ │ ExoPlayer ◀── owns & holds │ │ • setWakeMode │ │ • AudioAttributes (USAGE_MEDIA) │ │ • setHandleAudioBecomingNoisy │ │ │ │ StateFlow │ │ ── companion-singleton, lives even │ │ when service is down │ └───────────────┬───────────────────────┘ ▲ Intent actions │ StateFlow.collect (START / PAUSE / RESUME / │ STOP / NEXT / PREV / │ SET_VOLUME / UPDATE_SETTINGS)│ │ ┌─────────────────────────────┴────────────────────────┐ │ │ BackgroundMusicPlayer UI / ViewModel (thin facade — same (no API change) public API as v2.0.6) ▲ │ BackgroundMusicManager (app singleton, unchanged) ``` ### What changed in code 1. **`AtmoPlaybackService` (rewritten)** - Owns the `ExoPlayer` instance from `applySettingsFromIntent()` until `ACTION_STOP` / `onDestroy`. - Exposes a `companion object` `state: StateFlow` that survives between service lifecycles (always reflects current truth). - Handles `ACTION_START`, `ACTION_UPDATE_SETTINGS`, `ACTION_PAUSE`, `ACTION_RESUME`, `ACTION_NEXT`, `ACTION_PREVIOUS`, `ACTION_SET_VOLUME`, `ACTION_STOP`. - **Returns `START_STICKY`** (was `START_NOT_STICKY`): a system-induced restart no longer terminates playback. - Null/unknown intent actions on re-delivery **leave the player intact** (v2.0.6 self-stopped — that was a bug that compounded background kills). - Adds a `Stop` action button to the notification so the user can stop Atmo from outside the app. - `startForeground()` is still called as the very first action in `onCreate()` (preserves the v2.0.5 fix for `ForegroundServiceDidNotStartInTimeException`). - Wraps `startForeground` in try/catch so a denied `POST_NOTIFICATIONS` permission cannot crash the service. 2. **`BackgroundMusicPlayer` (rewritten as facade)** - Same public API as before — drop-in for `BackgroundMusicManager` and `WaypointViewModel`. No call sites changed. - `play()` / `pause()` / `stop()` / `next()` / `previous()` / `setVolume()` all route into `AtmoPlaybackService` companion intents. - `isPlaying` / `hasContent` read from `AtmoPlaybackService.state`. - `playbackState: StateFlow` is now a direct alias of the service's companion flow, so UI observers see the canonical truth. - `beforeWaypointAudio()` / `afterWaypointAudio()` preserve pause/resume, fade-out/in and duck behaviour. Fades are still implemented locally — they just call `AtmoPlaybackService.setVolume()` repeatedly instead of mutating an owned player. 3. **No changes** in `BackgroundMusicManager`, `AtmoResumeManager`, `WaypointViewModel`, `BegleitmusikDialog`, `WaypointListScreen`, `LivePttManager`, `WaypointAudioInterruptionCoordinator` — the facade keeps their interfaces stable. 4. **`AndroidManifest.xml`** unchanged. The `FOREGROUND_SERVICE_MEDIA_PLAYBACK` permission and `android:foregroundServiceType="mediaPlayback"` on `AtmoPlaybackService` were already present in v2.0.6. 5. **`build.gradle.kts`**: `versionCode 34 → 35`, `versionName 2.0.6 → 2.0.7`. Everything else unchanged. ## Lifecycle / service ownership behaviour (exact) | Event | Effect on service | Effect on ExoPlayer | | --- | --- | --- | | User taps "Play" / `play()` first time | `startForegroundService` → `ACTION_START` | Created inside service; `play()` | | User taps "Pause" | `ACTION_PAUSE` | `player.pause()` — service stays | | User taps "Stop" (UI or notification) | `ACTION_STOP` | `stop()` + `release()`, then `stopSelf()` | | User changes Atmo source (Begleitmusik-Dialog Save) | `ACTION_UPDATE_SETTINGS` (or `ACTION_START` if not running) | Source compared; rebuilt only if different | | Begleitmusik checkbox disabled | Manager calls `player.stop()` → `ACTION_STOP` | Service ends | | Tour switched (Atmo disabled in new tour) | Manager calls `player.stop()` | Service ends | | App minimised (Home / Recents) | No effect | Continues | | Screen off / Doze | No effect (FGS + wakeMode) | Continues | | App swiped from Recents | `onTaskRemoved` — service intentionally **keeps running** | Continues; user can stop from notification | | PTT begins | `AtmoResumeManager` → `pauseMusic()` → `ACTION_PAUSE` | `pause()`; service stays | | PTT ends | `AtmoResumeManager` → `player.play()` → `ACTION_RESUME` | `play()` | | Waypoint audio begins (PAUSE_RESUME) | `beforeWaypointAudio` → `pause` | `pause()` | | Waypoint audio begins (DUCK_UNDERLAY) | fade `ACTION_SET_VOLUME` to duckVolume | volume ramps; play continues | | Waypoint audio ends | `afterWaypointAudio` → resume / fade-up | as configured | | Manual player begins | same as waypoint (via `musicManager.beforeWaypointAudio`) | as configured | | Manual player ends | resume / fade-up | as configured | | System OOM-kill of process (very rare with FGS) | `START_STICKY` → service restarted, no stale player | Player will be empty until user re-starts | ## Manual test plan Build and install the new APK (uninstall older v2.0.x first or use a different applicationId variant to keep installed in parallel). Then test: 1. **Stream — backgrounded with screen off (≥ 5 min)** - Configure a HTTP stream URL in Begleitmusik. - Start playback. Press Home. Lock the screen. - Wait 5 minutes. Audio must continue without interruption. - Unlock — playback should still be active; "Stopp" button in Atmo-Notification works. 2. **Local playlist — backgrounded with screen off (≥ 5 min)** - Add 3+ local audio files to the playlist (with Shuffle on or off). - Start playback. Press Home. Lock the screen. - Wait 5 minutes spanning at least one track transition. - Track must auto-advance, audio must not stop. 3. **App swipe from Recents** - Start Atmo. Open Recents. Swipe the app away. - Audio must continue. - Tap the Atmo notification → app reopens cleanly. - Tap "Stopp" → audio stops; notification disappears. 4. **PTT interruption** - Start Atmo. Tap-and-hold PTT — Atmo must pause. - Release PTT — Atmo must resume from same position. - Repeat 3× — no leak; no stale pauses. 5. **Waypoint audio interruption (each behaviour)** - For each `WaypointMusicBehavior` (PAUSE_RESUME, FADE_OUT_IN, DUCK_UNDERLAY, CONTINUE_UNDERLAY): trigger a waypoint with audio. Behaviour must match v2.0.6 semantics. - After waypoint completes, Atmo resumes (if it was playing prior). - `autoStartAfterWaypoint=true`: if Atmo was paused/stopped, it starts fresh after the first waypoint. 6. **Manual single-file / playlist player vs. Atmo** - Tap any waypoint card's play icon — manual file plays, Atmo ducks/pauses per behaviour. - Tap stop / pause on manual — Atmo resumes. - Use playlist mode play/pause/next/prev — Atmo behaviour identical. 7. **Begleitmusik-Dialog Cancel/Save** - Open dialog while Atmo plays. Cancel — Atmo stops (intentional, kept v2.0.6 behaviour; user cancelled the config session). - Open dialog. Change source. Save — service updates / rebuilds player. 8. **Tour switch** - Switch to a tour with Atmo disabled — Atmo stops, service ends. - Switch to a tour with Atmo enabled — Atmo state remains stopped until user presses Play. 9. **Notification permission denied** - Revoke POST_NOTIFICATIONS in system settings. - Start Atmo — no crash; service runs (silent notification absent). 10. **Doze / Battery saver** - On a device with aggressive OEM Doze (Xiaomi, Huawei, OnePlus): with Atmo running and screen off for 10 minutes, audio must persist. ## Regression checklist (preserved from v2.0.6) - [x] PTT pauses waypoint audio (`WaypointAudioInterruptionCoordinator`) - [x] PTT pauses & resumes Atmo (`AtmoResumeManager`) - [x] Drag-and-drop reorder of waypoints and tour tabs - [x] Wake Lock settings dialog and controller - [x] Map providers (OSM / fallback) - [x] Track-Editor and Drafts archive - [x] Backup import/export (ZIP) - [x] Map search (Nominatim) - [x] Audio library cross-tour - [x] OSM attribution - [x] ScrollableTabRow fix - [x] About dialog with PayPal link and version notice on first open ## Known non-issues - `MainActivity.launchWhenStarted` deprecation warning — pre-existing in v2.0.6, not a runtime concern. Replaced only when Lifecycle APIs are next refactored. - `Modifier.menuAnchor()` deprecation — Compose Material3 1.3+; cosmetic. - `Polyline.fillColor/strokeColor/strokeWidth` deprecation — osmdroid 9.0.2; cosmetic. ## Build details ``` package: de.waypointaudio versionCode=35 versionName=2.0.7 minSdk=26 targetSdk=35 compileSdk=35 SHA-256 cert: 7B:0E:A4:D5:52:86:05:17:3D:29:9D:7B:26:8E:7A:B8:76:8D:D5:04:EC:15:B6:6A:52:D4:7B:42:57:B3:BB:46 Built with: Gradle 8.9, OpenJDK 21.0.11, Android SDK build-tools 35.0.0 APK size: 15.13 MB ```