• v2.0.7 b2aa1c167c

    marcel released this 2026-05-15 14:32:13 +02:00 | 7 commits to main since this release

    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<MusicPlaybackState>        │
                                  │     ── 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<MusicPlaybackState> 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<MusicPlaybackState> 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 startForegroundServiceACTION_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 AtmoResumeManagerpauseMusic()ACTION_PAUSE pause(); service stays
    PTT ends AtmoResumeManagerplayer.play()ACTION_RESUME play()
    Waypoint audio begins (PAUSE_RESUME) beforeWaypointAudiopause 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)

    • PTT pauses waypoint audio (WaypointAudioInterruptionCoordinator)
    • PTT pauses & resumes Atmo (AtmoResumeManager)
    • Drag-and-drop reorder of waypoints and tour tabs
    • Wake Lock settings dialog and controller
    • Map providers (OSM / fallback)
    • Track-Editor and Drafts archive
    • Backup import/export (ZIP)
    • Map search (Nominatim)
    • Audio library cross-tour
    • OSM attribution
    • ScrollableTabRow fix
    • 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
    
    Downloads