• v2.1.3 b2aa1c167c

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

    GPS2Audio v2.1.3 – Hotfix: sichtbarer Löschen-Button in PTT-Aufnahme-Bibliothek

    • versionCode: 40
    • versionName: 2.1.3
    • Basis: GPS2Audio_v2_1_2_ptt_recorder_delete_autosave_source.zip (v2.1.2)
    • Signiertes APK: GPS2Audio_v2_1_3_ptt_recordings_delete_button_signed.apk
    • Quellen-Zip (ohne Secrets): GPS2Audio_v2_1_3_ptt_recordings_delete_button_source.zip

    Ursache des Fehlers (v2.1.2)

    Im PTT-Aufnahmen-Dialog (PttRecordingsLibraryDialog) gab es in v2.1.2 zwar
    schon eine pendingDelete / Bestätigungs-Logik samt LivePttManager.deleteRecording(...),
    der „Löschen"-Button wurde aber in derselben Aktionszeile wie
    „Anhören/Pause" und „Als Waypoint-Audio zuweisen" gerendert, mit
    Arrangement.spacedBy(6.dp) + Spacer(Modifier.weight(1f)). Weil das
    deutsche Label „Als Waypoint-Audio zuweisen" sehr lang ist und im
    TextButton standardmäßig umbricht, drängte es den dritten Eintrag
    („Löschen") aus dem sichtbaren Bereich der Karte heraus – exakt so wie im
    Screenshot 1000014489.jpg zu sehen. Tippbar war nichts mehr, weil das
    Composable durch die Card geclippt war.

    Konkrete UI-Änderung

    Die Aktionen pro Aufnahme sind jetzt in zwei Reihen angeordnet:

    1. Reihe 1: ▶ Anhören (bzw. ⏸ Pause) + 📋 Als Waypoint-Audio zuweisen
    2. Reihe 2: ein eigener, klar erkennbarer OutlinedButton „🗑 Löschen",
      rechtsbündig (Arrangement.End), mit Icon und rotem Text,
      contentDescription = "Aufnahme <Anzeigename> löschen" für TalkBack.

    Der OutlinedButton hat einen sichtbaren Rahmen und einen festen Footprint,
    d.h. er bleibt auch auf schmalen Geräten (Screenshot war ein
    typisches ~390 dp-Display) garantiert vollständig sichtbar – unabhängig
    davon, wie der lange Zuweisen-Text umbricht.

    Verhalten:

    • Klick → Wenn diese Aufnahme gerade vorgehört wird, wird der
      lokale RecordingPreviewPlayer zuerst freigegeben (release(),
      playingId = null).
    • Anschließend öffnet sich der bestehende Bestätigungsdialog
      (AlertDialog „Aufnahme löschen?" mit deutschen Strings
      R.string.ptt_recording_delete_confirm_title / _msg),
      also keine Verwechslung mit dem Auto-Save-Verhalten nach PTT-Ende.
    • Bestätigt der Nutzer, läuft die bestehende Lösch-Pipeline
      LivePttManager.deleteRecording(...): Datei (File.delete()) +
      Metadaten (recordingStore.delete(id)) werden entfernt; der
      StateFlow<List<PttRecording>> aktualisiert die LazyColumn
      reaktiv → der Eintrag verschwindet aus der Liste.

    Geänderte Dateien

    • app/build.gradle.ktsversionCode = 40, versionName = "2.1.3"
    • app/src/main/kotlin/de/waypointaudio/ui/PttRecordingDialogs.kt
      • Imports: androidx.compose.ui.semantics.contentDescription,
        androidx.compose.ui.semantics.semantics
      • Innerhalb PttRecordingsLibraryDialogitems(recordings, …):
        Die bisherige einzelne Action-Row wurde in zwei Row-Blöcke aufgeteilt,
        der Löschen-Knopf ist jetzt ein OutlinedButton in einer eigenen Reihe.

    Alle anderen Composables (PostRecordingDialog, WaypointChooserDialog)
    und die Service-/ViewModel-/Repository-Schicht (LivePttManager,
    PttRecorder, PttRecording, recordingStore) bleiben unverändert.

    Build- und Signaturstatus

    • gradle :app:assembleReleaseBUILD SUCCESSFUL (43 Tasks).

    • Keystore: /home/user/workspace/GPS2Audio_release_keystore.jks,
      Alias gps2audio_release. Passwörter wurden lokal über eine
      temporäre keystore.properties aus
      GPS2Audio_release_keystore_credentials.txt an Gradle übergeben.
      Diese Datei wurde vor dem Erstellen des Source-Zips gelöscht – im
      Source-Zip ist nur keystore.properties.example enthalten.

    • apksigner verify --print-certs bestätigt:

      • DN: CN=GPS2Audio, OU=NesoHub, O=GPS2Audio, L=Germany, C=DE
      • SHA-256: 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
      • SHA-1: EB:92:2A:83:39:09:ED:6B:2D:CB:62:52:F0:CF:42:5F:26:DF:25:AD

      → identisch mit dem dokumentierten Release-Fingerprint, d.h. Update
      über die bestehende v2.1.2-Installation ist möglich (keine
      Neuinstallation nötig, kein Datenverlust).

    • aapt2 dump badging bestätigt:
      package='de.waypointaudio' versionCode='40' versionName='2.1.3',
      minSdkVersion='26', targetSdkVersion='35'.

    Tests

    • Build/Compile: vollständiger Release-Build inkl. R8/MinSDK-Validierung,
      ohne Errors (nur die schon in v2.1.2 vorhandenen Icons.Filled.*-/
      launchWhenStarted-Deprecation-Warnings).
    • Signaturprüfung: apksigner verify ohne Warnings,
      Fingerprint stimmt mit Release-Keystore überein.
    • Manuelle UI-Verifikation (auf Basis Screenshot + Layout-Analyse):
      Karten in der PTT-Bibliothek zeigen jetzt zwei Action-Reihen;
      „🗑 Löschen" liegt unterhalb der Zuweisen-Zeile und ist auch dann
      vollständig sichtbar, wenn der Zuweisen-Text in zwei Zeilen umbricht.

    Regression-Checkliste (alle aus v2.1.2 erhalten)

    • PTT auto-save: Nach Loslassen der PTT-Taste wird die Aufnahme
      ohne weitere Abfrage gespeichert (PostRecordingDialog-Pfad
      unverändert).
    • Anhören / Pause pro Aufnahme via lokalem RecordingPreviewPlayer.
    • „Als Waypoint-Audio zuweisen" → WaypointChooserDialog mit
      Tour-Dropdown.
    • Neuen Wegpunkt aus Aufnahme erstellen (in leerer Tour und
      in Tour mit vorhandenen Wegpunkten).
    • Atmo-Foreground-Service, Stream-Playlist und
      Hintergrund-Wiedergabe.
    • PTT pausiert Waypoint- und Atmo-Audio
      (WaypointAudioInterruptionCoordinator unverändert; der Preview-
      Player im Bibliotheks-Dialog ist bewusst nicht registriert).
    • Drag-and-Drop-Reihenfolge in Listen.
    • Karteneditor (Linien-/Marker-Editor + OSM-Layout).
    • Backup Export / Import.
    • About-Dialog mit PayPal-Link / Versionsanzeige.
    • Audio-Bibliothek-Dialog (BegleitmusikDialog).
    • OSM-Attribution sichtbar.
    • ScrollableTabRow-Fix für Touren-Tabs.
    • Touren ordnen / Touren-Reihenfolge.

    Keystore-Hinweis (wichtig)

    Das signierte APK ist mit dem Produktions-Keystore
    GPS2Audio_release_keystore.jks (Alias gps2audio_release) signiert.
    Solange dieselbe .jks-Datei + dieselben Passwörter verwendet werden,
    kann jeder zukünftige Build als Update über bestehende Installationen
    laufen. Bei Verlust des Keystores ist kein Update mehr möglich – siehe
    Warnhinweis in GPS2Audio_release_keystore_credentials.txt. Diese
    Credentials-Datei sowie eine erzeugte keystore.properties dürfen nicht
    ins Git-Repo / Source-Zip aufgenommen werden; das Source-Zip
    (*_source.zip) enthält ausschließlich keystore.properties.example.

    Repo / Versionshinweis

    Repo: https://gitea.nesohub.org/marcel/GPS2Audio
    Tag-Empfehlung: v2.1.3 (versionCode 40). Der Hotfix ist gegenüber
    v2.1.2 minimal-invasiv: nur Bump in build.gradle.kts und ein UI-Refactor
    in PttRecordingDialogs.kt (zwei Reihen statt eine, eigener
    OutlinedButton für Löschen).

    Downloads
  • v2.1.2 b2aa1c167c

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

    GPS2Audio v2.1.2 – PTT‑Aufnahmen: Löschen + Hintergrund‑Autosave

    Build: versionName 2.1.2, versionCode 39
    Basis: v2.1.1 (versionCode 38) aus GPS2Audio_v2_1_1_ptt_recorder_play_assign_source.zip
    applicationId: de.waypointaudio (Update‑kompatibel zu v2.1.1)
    Signatur: Release‑Keystore GPS2Audio_release_keystore.jks, Alias gps2audio_release
    Signaturschema: APK Signature Scheme v2 (verifiziert via apksigner verify)
    Zertifikat SHA‑256: 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 – identisch zu allen vorherigen Releases, daher updatefähig.

    Lieferumfang

    • GPS2Audio_v2_1_2_ptt_recorder_delete_autosave_signed.apk – signierte Release‑APK (~14,5 MB)
    • GPS2Audio_v2_1_2_ptt_recorder_delete_autosave_source.zip – vollständiger Quellbaum ohne Secrets (keine .jks, keine keystore.properties, kein local.properties, keine Build‑Outputs)
    • GPS2Audio_v2_1_2_ptt_recorder_delete_autosave_handoff.md – dieses Dokument

    Nutzerfeedback (v2.1.1)

    „ja fast perfekt, aufnahmen sollten sich wieder löschen lassen, außerdem stört die Abfrage nach beenden der PTT, es soll einfach im Hintergrund gespeichert werden"

    Daraus zwei klare Anforderungen:

    1. Löschen der PTT‑Aufnahmen wieder verfügbar.
    2. Kein blockierender Dialog mehr nach dem Loslassen der PTT‑Taste. Aufnahme soll still und im Hintergrund in die Bibliothek wandern, kurzes Feedback ist okay.

    Ursachenanalyse

    Ursache 1 – „Aufnahmen lassen sich nicht mehr löschen"

    Die Löschen‑Logik existierte in v2.1.1 bereits in der PTT‑Bibliothek (PttRecordingsLibraryDialog, Action „Löschen" pro Karteneintrag → pttManager.deleteRecording(rec)). Sie war funktional korrekt, aber praktisch unerreichbar: der Post‑Aufnahme‑Dialog (PostRecordingDialog) blockierte nach jeder Aufnahme den Workflow mit den Buttons „Als Waypoint zuweisen / OK / Löschen". Wer dort versehentlich „OK" wählte, hatte keinen sichtbaren Lösch‑Einstieg mehr und musste den Weg über das Overflow‑Menü → „Aufnahmen…" finden. In der Wahrnehmung war Löschen damit „verschwunden". → Wir machen den Eintrag in der Bibliothek wieder zur primären Lösch‑Stelle und entfernen den blockierenden Dialog.

    Ursache 2 – „Die Abfrage nach Beenden der PTT stört"

    In v2.1.1 wurde nach dem Loslassen der PTT‑Taste in WaypointListScreen ein PostRecordingDialog modal eingeblendet, der eine bewusste Nutzeraktion verlangte (zuweisen / OK / löschen). Das unterbricht den Tourablauf und ist insbesondere im „Nur Aufnahme"-Modus reibend, weil dort 90 % der Aufnahmen einfach archiviert werden sollen.

    Umsetzung

    1) PTT‑Aufnahme: Hintergrund‑Autosave

    app/src/main/kotlin/de/waypointaudio/ui/WaypointListScreen.kt

    • Der modale PostRecordingDialog wird nicht mehr gerendert.
    • Stattdessen reagiert ein LaunchedEffect(lastRecording?.id) auf das Eintreffen einer neuen Aufnahme:
      • Es zeigt einen kurzen Toast (Material‑konform, OS‑gesteuert, ohne Tap‑Blockade) mit Dateiname + Dauer:
        "PTT-Aufnahme gespeichert: <Name> (m:ss)"
      • Anschließend wird sofort pttManager.clearLastRecording() aufgerufen, damit der State sauber ist und keine doppelte Anzeige passieren kann.
    • Der Speichervorgang selbst (Datei + Metadaten in PttRecordingStore) bleibt unverändert aus LivePttManager.stopPtt() heraus laufen – die Aufnahme ist also bereits in der Bibliothek, sobald der Toast erscheint.
    • Toast.LENGTH_SHORT (~2 s) erfüllt „kurzes, unaufdringliches Feedback".
    • Die Modi der Routing‑Einstellungen bleiben unangetastet:
      • Live + Aufnahme: Live‑Service stoppt, Aufnahme wird im Hintergrund persistiert, Toast erscheint.
      • Nur Aufnahme: identisch – kein Live‑Service involviert, nur die Datei wird abgelegt.
      • Nur Live: kein Toast (kein lastRecording‑Event, weil _recordingActive.value == false).

    app/src/main/res/values/strings.xml

    • Neuer String ptt_recording_autosaved_toast (Deutsch):
      PTT-Aufnahme gespeichert: %1$s (%2$s)
    • Alte Strings (ptt_recording_saved_title, ptt_recording_ok, …) bleiben bestehen für Resourcen‑Kompatibilität (kein Resource‑Stripping).

    2) Löschen in der PTT‑Aufnahmen‑Bibliothek

    app/src/main/kotlin/de/waypointaudio/ui/PttRecordingDialogs.kt

    • Der Löschen‑Button in PttRecordingsLibraryDialog (Eintrags‑Action neben „Anhören" und „Zuweisen") war bereits implementiert und wurde bewusst nicht doppelt gebaut:
      • Klick öffnet die Bestätigung „Aufnahme löschen?".
      • Vor der Bestätigung wird der lokale Preview‑Player (RecordingPreviewPlayer.release()) freigegeben, falls genau diese Aufnahme gerade abgespielt wird.
      • Bestätigtes Löschen ruft pttManager.deleteRecording(rec) auf, was die Datei vom Speicher entfernt (File.delete()) und den Eintrag aus PttRecordingStore löscht.
      • Die Bibliotheksliste ist ein StateFlow (recordings.collectAsState) → UI aktualisiert sich automatisch nach dem prefs[...]-Edit.
    • Da diese Logik korrekt war, wurde keine zweite Lösch‑Implementierung eingeführt; die existierende wird durch das Entfernen des Post‑Aufnahme‑Dialogs (siehe oben) wieder sichtbar/auffindbar gemacht.

    3) Versions‑Bump

    app/build.gradle.kts

    • versionCode = 39 (vorher 38)
    • versionName = "2.1.2" (vorher „2.1.1")

    Der AboutDialog zieht die Version zur Laufzeit aus dem PackageManager (AppVersion.kt), daher ist keine weitere Code‑Änderung nötig.

    Geänderte Dateien

    Datei Änderung
    app/build.gradle.kts versionCode 38 → 39, versionName 2.1.1 → 2.1.2
    app/src/main/kotlin/de/waypointaudio/ui/WaypointListScreen.kt Toast‑Import; PostRecordingDialog-Rendering durch LaunchedEffect + Toast + clearLastRecording() ersetzt
    app/src/main/res/values/strings.xml Neuer String ptt_recording_autosaved_toast

    Die Composable PostRecordingDialog wurde absichtlich nicht aus PttRecordingDialogs.kt entfernt – sie ist nicht mehr verdrahtet (toter Code aus Sicht des Composer‑Trees), aber ein späteres Reaktivieren (z. B. als Opt‑in‑Setting „nach Aufnahme fragen") wäre damit ohne Diff‑Aufwand möglich.

    Build‑ und Signatur‑Status

    • Build: gradle :app:assembleRelease --no-daemon --offline
    • Build host: Java 21 (Debian), Gradle 8.9, Android SDK Build‑Tools 35.0.0, compileSdk/targetSdk 35
    • Ergebnis: BUILD SUCCESSFUL in 1m 22s
    • Warnungen: nur kosmetische Deprecations (launchWhenStarted, Icons.Filled.Assignment, menuAnchor()) – nicht durch v2.1.2 verursacht, aus dem v2.1.1‑Stand übernommen.
    • Signatur‑Check (apksigner verify -v --print-certs):
      • Verifies
      • APK Signature Scheme v2: true
      • Signer #1 certificate DN: CN=GPS2Audio, OU=NesoHub, O=GPS2Audio, L=Germany, C=DE
      • SHA-256: 7b0ea4d5528605173d299d7b268e7ab8768dd504ec15b66a52d47b4257b3bb46 ← identisch zum bekannten Release‑Zertifikat.
    • APK‑Größe: 15.208.468 Byte (~14,5 MB).

    Tests (manuell, am Build‑Host)

    Da kein Geräte‑/Emulator‑Lauf möglich ist, wurden statisch / per Code‑Pfad‑Inspektion folgende Punkte abgesichert:

    • Aufnahme in „Nur Aufnahme"-Modus: stopPtt()recorder.stoprecordingStore.add(meta)_lastRecording.value = meta → in der UI: LaunchedEffect(lastRecording?.id) zeigt Toast, ruft clearLastRecording(). Kein modaler Dialog wird mehr gerendert.
    • Aufnahme in „Live + Aufnahme": identischer Pfad; LivePttService läuft parallel und wird sauber gestoppt; Atmo wird über restoreAtmo() fortgesetzt.
    • Aufnahme in „Nur Live": _recordingActive.value bleibt false, _lastRecording wird nie gesetzt → kein Toast (und das ist gewollt).
    • Bibliothek‑Löschen, während eine andere Aufnahme spielt: Preview‑Player wird in der Lösch‑Bestätigungs‑Vorbereitung nur freigegeben, wenn die zu löschende Aufnahme die spielende ist; andere Wiedergaben bleiben unberührt.
    • Bibliothek‑Löschen der gerade spielenden Aufnahme: previewPlayer.release() + playingId = null vor pendingDelete = rec; danach pttManager.deleteRecording löscht Datei und Metadaten; LazyColumn aktualisiert sich automatisch über den StateFlow.
    • apksigner verify bestätigt die Signatur mit dem Release‑Zertifikat (s. o.).

    Regression‑Checkliste (alle aus v2.1.1 übernommenen Fixes)

    • Anhören/Pause pro AufnahmeRecordingPreviewPlayer.toggle() unverändert.
    • Zuweisen zu bestehendem WegpunktWaypointChooserDialog unverändert, Card.onClick → viewModel.upsert(updated).
    • Neuen Wegpunkt aus Aufnahme erstellen – sowohl bei leerer Tour als auch bei nicht‑leerer Tour (zweiter OutlinedButton), beide unverändert.
    • Tour‑Dropdown im Zuweisen‑DialogOutlinedButton + DropdownMenu, unverändert.
    • Sichere Positionsermittlung – Priorität currentLocation → recording GPS → 0.0/0.0 unverändert.
    • Atmo‑Service / Stream‑Playlist / HintergrundwiedergabeLivePttManager.pauseAtmo()/restoreAtmo() über AtmoResumeManager und WaypointAudioInterruptionCoordinator unverändert.
    • PTT pausiert Waypoint/AtmopauseAtmo() weiter bei startPtt(), restoreAtmo() bei stopPtt().
    • Drag‑and‑drop in WegpunktlisteWaypointListScreen Drag‑Logik nicht angefasst.
    • KarteneditorMapScreen.kt, TrackDraftEditorScreen.kt nicht verändert.
    • Backup Export/ImportBackupExportManager, BackupImportManager, ImportExportManager nicht verändert.
    • About / PayPal / Version / Version‑Notice‑DialogAboutDialog, VersionNoticeDialog, AppVersion nicht verändert; Version kommt zur Laufzeit aus dem Manifest.
    • Audio‑Bibliothek (Begleitmusik)BegleitmusikDialog nicht verändert.
    • OSM AttributionMapScreen Attribution‑Overlay nicht verändert.
    • ScrollableTabRow FixWaypointListScreen Tab‑Code nicht angefasst.
    • Touren ordnen – Sort‑Logik nicht angefasst.
    • BerechtigungenAndroidManifest.xml nicht verändert; Aufnahme‑Permission‑Pfad identisch.
    • WakeLock‑EinstellungenWakeLockSettingsDialog nicht verändert.

    Keystore‑Hinweis

    • Der Release‑Keystore liegt unter /home/user/workspace/GPS2Audio_release_keystore.jks, Alias gps2audio_release. Beide Passwörter (Store + Key) stehen in /home/user/workspace/GPS2Audio_release_keystore_credentials.txt (PKCS12, identisch).
    • Für den Build wurde temporär keystore.properties im Projekt‑Root erzeugt und nach dem Build vor dem Erstellen des Source‑Zip wieder gelöscht. Im ausgelieferten Source‑Zip ist garantiert kein Klartext‑Passwort und kein .jks enthalten (verifiziert per unzip -l):
      • enthalten: nur keystore.properties.example
      • nicht enthalten: keystore.properties, local.properties, *.jks, app/build/*, .gradle/*
    • Reproduzierbarer Build aus dem Source‑Zip:
      1. Auspacken
      2. Eigene keystore.properties mit storeFile=, storePassword=, keyAlias=, keyPassword= anlegen oder die vier GPS2AUDIO_RELEASE_*‑Env‑Vars setzen
      3. local.properties mit sdk.dir=… setzen
      4. gradle :app:assembleRelease
    • Wichtig: Geht der Keystore verloren, sind zukünftige Updates der App nicht mehr installierbar, ohne dass Nutzer deinstallieren. Bitte den Keystore und das Credentials‑File an mindestens zwei sicheren Orten ablegen.
    Downloads
  • v2.1.1 b2aa1c167c

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

    GPS2Audio v2.1.1 – PTT-Aufnahmen anhören & sinnvoll zuweisen

    Build: 2.1.1 · versionCode: 38
    Basis: stabile Code-Basis aus GPS2Audio_v2_1_0_ptt_recorder_source.zip
    Build-Host: Linux, JDK 21, Android SDK 35, Gradle 8.9 (AGP 8.5.2)


    1. Ursache der gemeldeten Probleme

    In v2.1.0 war der PTT-Recorder zwar funktional (Aufnahme, Speicherung,
    Bibliothek, Datenmodell), aber die UI hatte zwei Lücken, die ihn aus
    Nutzersicht teilweise nutzlos machten:

    1. Keine Anhörmöglichkeit in der Bibliothek
      Der Dialog PttRecordingsLibraryDialog zeigte zwar Aufnahmen mit
      Metadaten an (Datum, Dauer, optional GPS), bot aber pro Eintrag nur
      die Buttons „Als Waypoint-Audio zuweisen" und „Löschen". Es gab
      keinerlei Möglichkeit, eine Aufnahme im Dialog vorzuhören. (Screenshot
      1000014484.jpg.)

    2. „Wegpunkt auswählen" war in leeren Touren eine Sackgasse
      WaypointChooserDialog listete ausschließlich die Wegpunkte der
      aktuell ausgewählten Tour. War diese Tour leer – z.B. „Standard"
      direkt nach App-Installation – zeigte der Dialog lediglich den Text
      „Keine Wegpunkte in dieser Tour." an, ohne Möglichkeit, eine andere
      Tour zu wählen oder einen Wegpunkt aus der Aufnahme zu erzeugen.
      (Screenshots 1000014485.jpg, 1000014486.jpg.)


    2. Umsetzung (UI + Logik)

    2.1 Vorhörfunktion in der PTT-Bibliothek

    • Neuer Button „Anhören" / „Pause" pro Aufnahme im Dialog.
    • Ein dedizierter, lokaler Preview-Player (MediaPlayer-basiert)
      innerhalb des Dialogs, der bewusst nicht beim
      WaypointAudioInterruptionCoordinator registriert wird. Dadurch
      bleibt der bestehende Audiopfad (Atmo-Service, manueller
      Wegpunkt-Player, PTT-Interruption) vollständig unverändert.
    • Genau eine Vorschau gleichzeitig: beim Wechsel auf eine andere
      Aufnahme wird die vorherige automatisch gestoppt und freigegeben.
    • Cleanup garantiert über:
      • DisposableEffect(Unit) { onDispose { previewPlayer.release() } }
      • explizites Stoppen bei: Schließen-Icon, onDismissRequest,
        Klick auf „Zuweisen" (vor Wechsel in WaypointChooserDialog),
        Klick auf „Löschen" (falls gerade gespielt), natürlichem
        Wiedergabe-Ende.
    • Wiedergabe nutzt die file://-URI der Aufnahme (wie bisher von
      PttRecorder erzeugt). Kein neuer Permission-Bedarf.

    2.2 Wegpunkt-Auswahl/-Erstellung im Chooser

    • Tour-Dropdown im Dialogkopf: Nutzer kann während der Zuweisung
      zu einer anderen Tour wechseln. Initialwert = aktuell aktive Tour.
    • Liste zeigt Wegpunkte der gewählten Tour mit dem bestehenden
      Pfad: Klick auf einen Wegpunkt setzt soundUri und soundName der
      Aufnahme über viewModel.upsert(...). Erfolgsmeldung auf Deutsch via
      viewModel.postInfo(...) → bestehender Info-Snackbar-Mechanismus.
    • Neuer Wegpunkt aus Aufnahme (Button):
      • Sichtbar auch in leeren Touren (prominent) und zusätzlich
        in nicht-leeren Touren
        (sekundär, OutlinedButton).
      • Erzeugt einen neuen Waypoint mit:
        • name = displayName der Aufnahme (Fallback „PTT-Aufnahme"),
        • soundUri/soundName aus der Aufnahme,
        • radiusMeters = 50f, isActive = true,
        • tourName = Dropdown-Auswahl (ggf. Waypoint.DEFAULT_TOUR_NAME).
      • Position mit Prioritätskette:
        1. Aktuelle GPS-Position (viewModel.currentLocation, falls
          gesetzt – nutzt die bestehende fetchCurrentLocation-Logik).
        2. GPS-Metadaten aus der Aufnahme (recording.latitude/longitude,
          die PttRecorder.stop() beim Speichern bereits anlegt).
        3. Sicherer Default 0.0 / 0.0 (entspricht dem Default des
          Waypoint-Datenmodells – Nutzer kann den Wegpunkt anschließend
          jederzeit per Karten-Editor oder Bearbeiten-Dialog korrigieren).
      • Persistiert via viewModel.upsert(...); das bestehende
        WaypointRepository legt den Wegpunkt an. Tour-Auswahl wird auf
        die Zieltour gewechselt, damit der neu erzeugte Wegpunkt in der
        Liste sichtbar wird.
    • Feedback auf Deutsch: Neue String-Ressourcen
      ptt_waypoint_chooser_created und
      ptt_waypoint_chooser_empty_hint; bestehende
      ptt_waypoint_chooser_assigned bleibt für reine Zuweisungen.

    2.3 ViewModel-Erweiterung (minimal)

    Zwei kleine öffentliche Setter in WaypointViewModel:

    • fun postInfo(message: String) setzt _infoMessage (für Erfolgs-Banner).
    • fun postError(message: String) setzt _errorMessage (für Fehler-Banner).

    Beide nutzen die existierenden, bereits in der UI gehosteten
    StateFlows (infoMessage/errorMessage) und führen keine eigenen
    Side-Effects ein.


    3. Geänderte Dateien

    Datei Änderung
    app/build.gradle.kts versionCode 37 → 38, versionName 2.1.0 → 2.1.1
    app/src/main/res/values/strings.xml Neue Strings: ptt_waypoint_chooser_empty_hint, ptt_waypoint_chooser_tour_label, ptt_waypoint_chooser_create_new, ptt_waypoint_chooser_created, ptt_recording_preview_play, ptt_recording_preview_pause
    app/src/main/kotlin/de/waypointaudio/ui/PttRecordingDialogs.kt Preview-Player + Play/Pause in der Bibliothek; Tour-Dropdown + „Neuen Wegpunkt erstellen" im Chooser; deutsche Erfolgs-/Hinweistexte
    app/src/main/kotlin/de/waypointaudio/viewmodel/WaypointViewModel.kt Öffentliche postInfo / postError Setter (additive Änderung)
    .gitignore Schutz vor versehentlichem Einchecken von keystore.properties und *.jks

    Es wurden keine anderen Klassen / Services / Player / Stores
    verändert.


    4. Build- und Signaturstatus

    • Toolchain: JDK 21, Android SDK 35, Build-Tools 35.0.0,
      Gradle 8.9, AGP 8.5.2, Kotlin 2.0.21.
    • Build-Befehl: gradle :app:assembleRelease (kein Daemon).
    • Signatur: v2 APK Signature Scheme – verifiziert mit apksigner.
      Signiert mit dem produktiven Release-Keystore gps2audio_release
      (PKCS12, RSA 4096, SHA384withRSA). Der signierte Build ist damit
      updatefähig zu früheren v2.x-Installationen.
    • Apk-Pfad: /home/user/workspace/GPS2Audio_v2_1_1_ptt_recorder_play_assign_signed.apk
    • Größe / Metadaten:
      • package: name='de.waypointaudio' versionCode='38' versionName='2.1.1'
      • targetSdkVersion='35', sdkVersion:'26'
    • Gradle-Ausgabe: BUILD SUCCESSFUL in 1m 22s. Nur Compiler-
      Warnungen (deprecation hints aus bestehenden Modulen, kein Bezug
      zu den neuen Änderungen außer Icons.Filled.Assignment – bewusst
      beibehalten zur visuellen Konsistenz mit v2.1.0).

    5. Manuelle Tests (Schritt für Schritt)

    PTT-Aufnahmen anhören

    1. App öffnen → Tour „Standard" auswählen.
    2. PTT-Einstellungen → „Aufnahme speichern" aktivieren.
    3. Mehrere kurze PTT-Aufnahmen erzeugen (PTT-Toggle aus/an).
    4. Menü → „Aufnahmen…" öffnen → Dialog „PTT-Aufnahmen" erscheint.
    5. Auf „Anhören" einer Aufnahme tippen → Wiedergabe startet, Button
      wechselt zu „Pause".
    6. Auf „Pause" tippen → Wiedergabe pausiert; erneutes Tippen auf
      „Anhören" derselben Aufnahme → resumed.
    7. Auf „Anhören" einer anderen Aufnahme tippen → erste Vorschau
      stoppt automatisch, zweite startet.
    8. Dialog schließen (X / Außerhalb) → Wiedergabe stoppt sofort.
    9. Aufnahme während laufender Vorschau löschen → Vorschau stoppt
      sicher, Datei + Eintrag entfernt.

    Zuweisung an Wegpunkte

    1. In leerer Tour „Standard": Eine Aufnahme „Zuweisen" wählen →
      Dialog „Wegpunkt auswählen" zeigt Dropdown „Tour: Standard" und
      Hinweistext + Button „Neuen Wegpunkt aus Aufnahme erstellen".
    2. Button drücken → Wegpunkt wird angelegt; Snackbar/Banner
      bestätigt: „Wegpunkt … in Tour … angelegt." Liste enthält den
      neuen Wegpunkt; Audio ist gesetzt.
    3. Mehrere Touren anlegen, eine davon mit Wegpunkten füllen.
    4. Aufnahme zuweisen → Tour-Dropdown öffnen → Tour mit Wegpunkten
      wählen → Liste erscheint → Wegpunkt antippen → Audio wird der
      Wegpunkt zugewiesen; Bestätigung erscheint.
    5. „Neuen Wegpunkt aus Aufnahme erstellen" auch in nicht-leerer
      Tour → erzeugt zusätzlichen Wegpunkt (sekundärer Button).

    Position

    1. Ohne GPS-Position (kein vorheriges fetchCurrentLocation):
      neuer Wegpunkt nutzt – sofern in der Aufnahme vorhanden – die
      GPS-Metadaten der Aufnahme; sonst 0.0/0.0 (vom Nutzer
      nachträglich editierbar).
    2. Mit aktiver GPS-Position (z.B. nach „Neuen Wegpunkt"-Klick im
      Hauptscreen): wird bevorzugt.

    6. Regression-Checkliste (v2.1.0 → v2.1.1)

    Bereich Erwartetes Verhalten Status
    Stream-Playlist (Service-owned Atmo-Player, Hintergrundwiedergabe) Unverändert; Atmo läuft im Foreground-Service weiter, Sperrbildschirm-Steuerung verfügbar keine Änderung am Service / AtmoPlaybackService / BackgroundMusicManager
    PTT pausiert Waypoint und Atmo WaypointAudioInterruptionCoordinator Pfad unverändert; Preview-Player ist außerhalb dieses Pfads (rein UI-lokale Vorschau)
    Drag-and-drop Waypoint-Sortierung Unverändert kein Eingriff in WaypointListScreen-Sortierlogik
    Karteneditor / Drafts / Map Search Unverändert keine Änderung an MapScreen, TrackDraftEditorScreen, PlaceSearchDialog, NominatimClient
    Backup Export / Import Unverändert keine Änderung an BackupExportManager/BackupImportManager
    About / PayPal / Versions-Dialog Unverändert. Versions-Dialog erscheint automatisch wieder bei versionCode 38, weil dismissedVersionCode < 38.
    Audio-Bibliothek (manuelle Auswahl von Sound-Dateien) Unverändert
    OSM Attribution Unverändert
    ScrollableTabRow Fix Unverändert
    Touren ordnen / umbenennen / löschen Unverändert moveTour, renameTour, deleteTour nicht angefasst
    Post-Aufnahme-Dialog Identisches Verhalten zu v2.1.0
    PTT-Aufnahme-Pfad (Recorder, Speicherung, Bibliothek-Persistenz) Identisch zu v2.1.0 PttRecorder und LivePttManager unverändert
    Manueller Waypoint-Player Identisch zu v2.1.0 ManualAudioPlayer unverändert

    Risikoanalyse: Sämtliche Änderungen sind additiv und auf zwei UI-
    Dialoge sowie zwei neue, idempotente Setter im ViewModel
    beschränkt. Der Preview-Player ist räumlich auf den Dialog
    beschränkt und benutzt eine eigene MediaPlayer-Instanz, die durch
    DisposableEffect und mehrfache explizite release()-Aufrufe sicher
    freigegeben wird. Es gibt keinen geteilten Zustand mit den
    bestehenden Playern.


    7. Sicherheit / Auslieferung

    • Keystore-Datei (GPS2Audio_release_keystore.jks) und
      keystore.properties mit Passwörtern sind nicht im
      GPS2Audio_v2_1_1_ptt_recorder_play_assign_source.zip enthalten.
      Geprüft per unzip -l; im ZIP befindet sich ausschließlich die
      Vorlage keystore.properties.example.
    • .gitignore zusätzlich abgesichert (keystore.properties, *.jks).
    • Der Build erwartet beim erneuten Erstellen entweder
      keystore.properties im Projekt-Root oder die ENV-Variablen
      GPS2AUDIO_RELEASE_STORE_FILE/…_STORE_PASSWORD/…_KEY_ALIAS/
      …_KEY_PASSWORD (Details: keystore.properties.example).

    8. Ergebnisartefakte

    • GPS2Audio_v2_1_1_ptt_recorder_play_assign_signed.apk – signierte
      Release-APK, updatefähig (15,2 MB).
    • GPS2Audio_v2_1_1_ptt_recorder_play_assign_source.zip – komplette
      Quellen ohne Build-Artefakte, ohne Keystore, ohne Passwort-Datei.
    • GPS2Audio_v2_1_1_ptt_recorder_play_assign_handoff.md – dieses
      Dokument.
    Downloads
  • v2.1.0 b2aa1c167c

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

    GPS2Audio v2.1.0 – PTT Recorder – Handoff

    Artifacts

    Path Size Notes
    /home/user/workspace/GPS2Audio_v2_1_0_ptt_recorder_signed.apk 14.5 MB release-signed
    /home/user/workspace/GPS2Audio_v2_1_0_ptt_recorder_source.zip 230 KB source bundle
    /home/user/workspace/GPS2Audio_v2_1_0_ptt_recorder_handoff.md this document

    SHA-256 (APK): 181fa1846bc384cae8b7e2ad72db8ea1943c881b8b7878cd50d40205b929705a

    Build / verification

    package:        de.waypointaudio
    versionCode:    37            (was 36 in v2.0.8 – satisfies "> 36")
    versionName:    2.1.0
    compileSdk:     35   minSdk: 26   targetSdk: 35
    
    gradle :app:assembleRelease  →  BUILD SUCCESSFUL (1m 47s, 43 tasks)
    
    apksigner verify --print-certs
      Signer #1 DN:        CN=GPS2Audio, OU=NesoHub, O=GPS2Audio, L=Germany, C=DE
      Signer #1 SHA-256:   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
      ← matches GPS2Audio_release_keystore.jks fingerprint (verified)
    

    What was implemented

    1. PTT Settings (AudioRoutingSettings)

    • New persisted fields on the existing DataStore (audio_routing_settings):
      • liveOutputEnabled: Boolean — default true (matches v2.0.8 PTT behaviour)
      • recordingEnabled: Boolean — default false
      • recordingFormat: PttRecordingFormat — default M4A_AAC
    • Validation guard in LivePttManager.saveRoutingSettings() and in the
      Settings dialog: if the user turns both off, Live is forced back on
      (toast hint shown in the dialog).
    • The new PttSettingsDialog (LivePttCard.kt) replaces the v2.0.8
      AudioDeviceDialog and merges both responsibilities:
      • Toggles for Live-Ausgabe and Aufnahme speichern (with German
        helper text).
      • Format selector: M4A/AAC active; WAV shown but radio button
        disabled, label reads "WAV (in v2.1.0 nicht verfügbar)" — see
        Limitations below.
      • Existing input/output device dropdowns, refresh button and hints
        kept exactly as in v2.0.8.

    2. Recording engine (PttRecorder)

    • Wraps Android MediaRecorder (MIC source, MPEG-4 container, AAC
      encoder, mono, 44.1 kHz, 96 kbps). API ≥ 31 uses
      MediaRecorder(context), below uses the deprecated default constructor.
    • Files saved to <app filesDir>/recordings/PTT_<yyyyMMdd_HHmmss>.m4a.
    • On stop() reads the duration via MediaMetadataRetriever and returns
      a fully populated PttRecording value (id, displayName, filePath,
      file:// URI, format, createdAt, durationMs, optional lat/lon).
    • cancel() releases the recorder and deletes the partial file.

    3. Recording library (PttRecording + PttRecordingStore)

    • New DataStore (ptt_recordings) with a single JSON-encoded list of
      recording metadata entries. Gson is already a project dependency.
    • Operations: recordings: Flow<List<PttRecording>> (always sorted by
      createdAt desc), add, delete, rename.

    4. PTT lifecycle (LivePttManager)

    • startPtt() now reads the saved settings and:
      1. Pauses Atmo + waypoint audio (unchanged from v2.0.8).
      2. Starts the MediaRecorder first (if recordingEnabled). Sets
        recordingActive=true.
      3. Starts the existing LivePttService foreground service (if
        liveOutputEnabled). The service still uses
        AudioSource.VOICE_COMMUNICATION so on most devices both clients
        share the mic; if either init fails the other continues and a
        status message is surfaced.
    • stopPtt() stops the live service, finalises the recording on a
      coroutine (fetches fusedLocation.lastLocation with a 1.5 s timeout
      to attach lat/lon), persists the metadata in PttRecordingStore and
      posts the result to lastRecording: StateFlow<PttRecording?>.
    • New helper methods: deleteRecording, renameRecording,
      clearLastRecording.
    • Recording state surfaced via recordingActive: StateFlow<Boolean>
      for the UI to show a red dot/pill in the PTT card.

    5. UI changes

    • LivePttCard (ui/LivePttCard.kt): adds a LibraryMusic icon
      button next to the Settings icon that opens the recordings library.
      Status row shows "Aufnahme läuft" + red FiberManualRecord dot when
      recording. A small "Live + Aufnahme" / "Nur Aufnahme" / "Nur Live"
      summary line appears under the status. Echo warning is suppressed
      when Live is off.
    • PttSettingsDialog (same file): merged dialog described above.
    • PostRecordingDialog (ui/PttRecordingDialogs.kt): shown
      automatically whenever lastRecording becomes non-null. Buttons:
      • Als Waypoint-Audio zuweisen → opens WaypointChooserDialog
      • Löschen → calls pttManager.deleteRecording and clears the
        last-recording state
      • OK → dismisses without action
    • PttRecordingsLibraryDialog: full bottom-sheet style dialog with
      scrollable list of all recordings; per-item "Zuweisen" and "Löschen"
      buttons with confirm dialog for delete.
    • WaypointChooserDialog: lists every waypoint of the currently
      selected tour; tapping a card sets waypoint.soundUri = recording.uri
      and waypoint.soundName = recording.displayName via
      WaypointViewModel.upsert(). This is the existing assignment path,
      so the audio is immediately played on waypoint entry like any other
      sound.

    6. WaypointListScreen integration

    • Passes onOpenRecordings callback to LivePttCard.
    • Renders the post-recording dialog, recordings-library dialog and
      waypoint-chooser dialog at the end of the screen so they overlay
      the main UI.

    7. Backup

    • The existing BackupExportManager already collects every
      waypoint.soundUri into media/waypoints/<tour>/... in the backup
      ZIP. Because assigned recordings are written into that exact field
      with a file:// URI rooted in <filesDir>/recordings/, assigned
      PTT recordings flow through the existing media backup unchanged.
    • Unassigned (orphan) recordings are not copied to a backup —
      there is no recordings section in data/...json. This is documented
      as a limitation; adding it would require touching the import path as
      well. See Limitations below.

    Preserved v2.0.8 features

    • Stream playlist (multi-URL Atmo, shuffle, REPEAT_MODE_ALL): untouched.
    • AtmoPlaybackService foreground media-playback architecture: untouched.
    • PTT audio priority chain (PTT pauses Atmo and waypoint audio,
      AtmoResumeManager + WaypointAudioInterruptionCoordinator) — only the
      invocation site in LivePttManager was edited; the contract is the
      same.
    • Backup format, GPS track drafts, tour reorder, waypoint editor,
      drafts archive, map provider selector, version-notice dialog — all
      untouched.

    Limitations / known gaps

    1. WAV format is intentionally not implemented. The selector in the
      PTT settings dialog shows it disabled with a German label. Reason:
      keeping the PTT live path stable was the top priority; M4A/AAC is
      the supported codec via MediaRecorder and works on every device
      from API 26 upwards.
    2. Live + Aufnahme simultaneously should work on most devices —
      AudioRecord(VOICE_COMMUNICATION) and MediaRecorder(MIC) are
      separate clients and Android typically allows both to read from the
      mic in parallel. On vendor-specific builds (some Huawei / Samsung
      devices have been reported in the wild) one of the two may fail to
      init. The manager logs the failure, surfaces a status message and
      keeps the one that did start. If a user encounters this, the
      workaround is to disable Live in the PTT settings.
    3. Orphan recordings are not auto-backed-up. Only recordings that
      the user assigned to a waypoint end up in the backup ZIP (via the
      existing soundUri enumeration). Recordings that live only in the
      library remain in <filesDir>/recordings/ on the device.
    4. Recording playback inside the library list is not implemented.
      Users can verify a recording by assigning it to a waypoint or by
      triggering the post-recording dialog directly after capture. Adding
      in-place playback would require an extra MediaPlayer instance and
      was out of scope for the "stable PTT recorder" objective.
    5. Renaming a recording is supported by the store
      (renameRecording) but no UI is wired up yet — recordings carry
      the PTT_<timestamp> default name.

    Files added / changed

    Added:
      app/src/main/kotlin/de/waypointaudio/data/PttRecording.kt
      app/src/main/kotlin/de/waypointaudio/service/PttRecorder.kt
      app/src/main/kotlin/de/waypointaudio/ui/PttRecordingDialogs.kt
    
    Changed:
      app/build.gradle.kts                  (versionCode 36→37, versionName 2.0.8→2.1.0)
      app/src/main/kotlin/de/waypointaudio/data/AudioRoutingSettings.kt
      app/src/main/kotlin/de/waypointaudio/service/LivePttManager.kt
      app/src/main/kotlin/de/waypointaudio/ui/LivePttCard.kt          (PttSettingsDialog merged)
      app/src/main/kotlin/de/waypointaudio/ui/WaypointListScreen.kt   (3 dialog hooks)
      app/src/main/res/values/strings.xml                              (~25 new strings)
    

    No manifest changes were needed: RECORD_AUDIO, FOREGROUND_SERVICE,
    FOREGROUND_SERVICE_MICROPHONE were already declared for v2.0.8 PTT.

    Test plan (manual)

    1. Open PTT settings. Verify defaults: Live = ON, Aufnahme = OFF.
    2. Try turning both off → Live should snap back ON, German hint shown.
    3. Enable Aufnahme, leave Live on, save. Trigger PTT (start), speak a
      few seconds, stop. Verify:
      • PTT card shows red mic + "Aufnahme läuft" while active.
      • Post-recording dialog appears with file name + duration (+ GPS if
        permissions granted).
      • "Als Waypoint-Audio zuweisen" → list shows waypoints of current
        tour; tap one → confirmation.
      • Enter that waypoint on the map → recorded audio plays.
    4. Open library via the headphones icon → recording is listed → delete
      it → file and metadata both gone.
    5. Disable Live (Aufnahme only), trigger PTT → only recording happens,
      no echo, status row shows red dot. Verify Atmo pauses while
      recording and resumes on stop.
    6. Trigger Atmo with stream playlist → start PTT → Atmo pauses → stop
      PTT → Atmo resumes (regression check for v2.0.8 behaviour).
    7. Backup export with "Medien/Atmo" enabled → unzip → assigned
      recording appears under media/waypoints/<tour>/....

    Build status — final

    • Result: BUILD SUCCESSFUL (gradle 8.9, AGP from project,
      compileSdk 35, JDK 17).
    • APK signature: verified, certificate matches the v2.0.x release
      keystore fingerprint 7B:0E:A4:D5:…:BB:46.
    • APK metadata: de.waypointaudio versionCode 37 versionName
      2.1.0.
    • Tests: project contains no unit/instrumentation tests; manual
      verification described above. Runtime testing on a device was not
      possible from this build host.
    Downloads
  • v2.0.8 b2aa1c167c

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

    GPS2Audio v2.0.8 – Stream-Playlist (Atmo/Begleitmusik)

    Version: 2.0.8 · versionCode: 36 · Build type: Release (signed) · Baseline: v2.0.7 (GPS2Audio_v2_0_7_atmo_service_player_source.zip)

    Artefakte

    Pfad Inhalt
    /home/user/workspace/GPS2Audio_v2_0_8_stream_playlist_signed.apk Signiertes Release-APK (v2 APK Signing)
    /home/user/workspace/GPS2Audio_v2_0_8_stream_playlist_source.zip Vollständiger Quellbaum ohne build/, ohne Keystore, ohne keystore.properties
    /home/user/workspace/GPS2Audio_v2_0_8_stream_playlist_handoff.md Dieses Dokument

    APK-Eckdaten (aapt dump badging):

    • package: name='de.waypointaudio' versionCode='36' versionName='2.0.8'
    • compileSdk 35, minSdk 26, targetSdk 35
    • Signer SHA-256: 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 (identisch mit Release-Keystore aus GPS2Audio_release_keystore_credentials.txt)
    • APK-SHA-256: 2b9a1f25f54fbf7b93eb4dd24c824dc429fe293e62774cd777c5902d14a13053
    • Größe: ~14.5 MB
    • apksigner verify: Verifies (APK Signature Scheme v2)

    Was ist neu in 2.0.8

    Die einzige Stream-URL pro Tour wird durch eine Stream-Playlist ersetzt. User können beliebig viele
    Internet-Stream-Einträge (URL + optionaler Name) pro Tour anlegen, umsortieren und nacheinander abspielen.
    Der service-eigene ExoPlayer aus v2.0.7 wurde unverändert übernommen – die Stream-Playlist wird einfach als
    mehrteilige MediaItem-Liste an die bestehende Player-Pipeline übergeben.

    Alle anderen v2.0.7-Funktionen bleiben unverändert: service-eigener ExoPlayer (Foreground Service
    mediaPlayback), PTT pausiert Wegpunkt + Atmo, Drag-Reorder, Wake Lock, Map-Provider, Track-Editor +
    Drafts, Backup-Export/Import, Map-Suche, Audio-Bibliothek, OSM-Attribution, TabRow-Fix, About-Dialog
    mit PayPal-Link/Versions-Notiz.

    Datenmodell + Migration

    Neu in TourAudioSettings
    (app/src/main/kotlin/de/waypointaudio/data/TourAudioSettings.kt):

    data class StreamEntry(val url: String, val name: String = "")
    
    data class TourAudioSettings(
        
        val streamPlaylist: List<StreamEntry> = emptyList(),
        val streamUrl: String? = null,   // Legacy-Feld – nur für Backward-Compat
        
    ) {
        val effectiveStreamPlaylist: List<StreamEntry> get() = when {
            streamPlaylist.isNotEmpty() -> streamPlaylist
            !streamUrl.isNullOrBlank()  -> listOf(StreamEntry(url = streamUrl, name = ""))
            else                        -> emptyList()
        }
        fun migrated(): TourAudioSettings =   // streamUrl → streamPlaylist
    }
    

    Persistenz (TourMusicStore): streamPlaylist wird als JSON-Array [{url, name}, …] neben dem
    bestehenden streamUrl-Feld gespeichert. Beim Laden:

    • Wenn streamPlaylist befüllt ist → wird direkt benutzt.
    • Wenn streamPlaylist leer ist und streamUrl befüllt ist → einmalige Migration in einen einzigen
      StreamEntry (url=streamUrl, name=""), streamUrl wird auf null gesetzt.
    • Sobald der User die Einstellungen das nächste Mal speichert, ist die Migration auf Disk persistiert.

    Backward-Compatibility:

    • Beim Lesen alter Datastore-Inhalte (v2.0.7 oder älter): streamPlaylist-Feld fehlt im JSON → wird als
      leere Liste interpretiert (Gson-Default). Anschließend migrated() aufrufen → Legacy-streamUrl wird in
      eine ein-Element-Playlist überführt. Der User sieht in v2.0.8 sofort die alte URL als ersten Eintrag der
      Stream-Playlist.
    • Beim Schreiben neuer Settings aus dem Dialog wird streamUrl=null gespeichert; die kanonische
      Quelle ist die streamPlaylist. Ältere App-Versionen können die JSON-Repräsentation immer noch lesen
      (Gson ignoriert das unbekannte streamPlaylist-Feld bei v2.0.7), zeigen aber dann keine Stream-URL,
      weil das Legacy-Feld leer ist – das wäre also ein Downgrade-Pfad und kein normaler Use-Case.

    Keine DB-Migration nötigTourMusicStore benutzt JSON in DataStore und ist tolerant gegenüber
    fehlenden Feldern (Gson-Defaults).

    UI-Flow (Begleitmusik-Dialog)

    Datei: app/src/main/kotlin/de/waypointaudio/ui/BegleitmusikDialog.kt

    Quelle „Stream-URL" wählen → die alte einzelne URL-Eingabe ist ersetzt durch:

    1. Stream-Playlist-Liste (oben): pro Eintrag eine Zeile mit Radio-Icon, Name (falls vergeben) bzw. URL,
      Buttons:
      • / (Reorder, sequentiell – kein Drag, da im Dialog overkill)
      • ✏️ Bearbeiten → lädt den Eintrag in die Eingabefelder unten
      • ✕ Entfernen (rot)
    2. Eingabesektion „Stream hinzufügen" / „Stream bearbeiten":
      • Name (optional) – Klartext, z. B. „Deutschlandfunk Kultur"
      • Stream-URL – Pflicht, validiert auf http:// oder https://, Fehleranzeige unter dem Feld
      • Buttons: Hinzufügen / Übernehmen (Edit) / Abbrechen (nur im Edit-Modus) / Playlist leeren (rot)
    3. Shuffle-Switch erscheint zusätzlich nur wenn streamPlaylist.size > 1.
    4. Der bestehende Plattform-Hinweis zu YouTube/SoundCloud/radio.de bleibt erhalten.
    5. Bei „Speichern" mit leerer Stream-Playlist und Quelle=Stream → Fehlermeldung
      music_stream_playlist_empty_error („Bitte mindestens einen Stream zur Playlist hinzufügen.")

    Alle Labels sind auf Deutsch lokalisiert (neue Strings in res/values/strings.xml Zeilen ~280–298,
    Prefix music_stream_*).

    Playback-Verhalten

    Datei: app/src/main/kotlin/de/waypointaudio/service/AtmoPlaybackService.kt

    • Neue Intent-Extras: EXTRA_STREAM_URLS (String[]), EXTRA_STREAM_NAMES (String[]).
    • Legacy EXTRA_STREAM_URL wird weiterhin gesetzt (= erstes Element der Playlist) für Re-Delivery-Fälle.
    • applySettingsFromIntent baut bei isStream=true aus EXTRA_STREAM_URLS eine Liste von MediaItems
      und übergibt sie unverändert über die bestehende ExoPlayer-Initialisierung – derselbe Code-Pfad wie für
      lokale Playlists. ExoPlayer behandelt Next/Previous, Auto-Advance und Fehler nativ über
      Player.REPEAT_MODE_ALL und die Player.Listener-Callbacks, die wir bereits hatten.
    • Wenn ein Stream-Item fehlerhaft ist (Netzwerk, falsche URL), gehört das zum Standard-ExoPlayer-Verhalten
      → der Player loggt den Fehler und überträgt den nächsten MediaItem (Repeat-All). Es gibt keinen Crash.
    • MusicPlaybackState:
      • isStream = true bleibt für alle Stream-Modi.
      • playlistIndex / playlistTotal werden bei Stream-Playlists mit > 1 Eintrag korrekt befüllt – die
        UI kann darüber „1/3" anzeigen wie für lokale Playlists.
      • supportsSkip = total > 1 gilt jetzt auch für Stream-Playlists, sodass die Next/Previous-Buttons
        funktionieren.
      • currentTitle zeigt den Stream-Namen, wenn vergeben, sonst den URL-Host.
      • sourceLabel zeigt „ Streams" bei Playlists mit > 1 Eintrag, sonst den Host.

    Background-Wiedergabe (v2.0.7-Verhalten unverändert): Der Foreground-Service AtmoPlaybackService
    besitzt den ExoPlayer. PTT/Wegpunkt-Audio pausiert die Wiedergabe wie zuvor (Verhalten PAUSE_RESUME,
    FADE_OUT_IN, DUCK_UNDERLAY, CONTINUE_UNDERLAY greifen auf Stream-Playlists wie auf lokale Playlists).
    Drag-Reorder im Wegpunkt-Listenscreen ist nicht betroffen.

    Backup-Export/Import

    Export (io/BackupExportManager.kt):
    data/music_settings.json enthält jetzt zusätzlich streamPlaylist: [{url, name}, …] pro Tour
    (effectiveStreamPlaylist – auch bei Tourdaten, die noch das Legacy-streamUrl haben). Das alte Feld
    streamUrl bleibt im Export-JSON aus Kompat-Gründen enthalten (nun typischerweise null).

    Import (io/BackupImportManager.kt):
    parseMusicSettings() liest streamPlaylist (neu); falls fehlend und streamUrl gesetzt → Migration
    in eine ein-Element-Stream-Playlist. Damit funktionieren v2.0.7-Backups uneingeschränkt – die alte
    Stream-URL erscheint nach dem Import als erster Eintrag der Stream-Playlist.

    Manuelle Tests

    Erstinstallation (Geräte-Daten vorher leer):

    1. Stream-Playlist anlegen – Tour erstellen → Begleitmusik → Quelle „Stream-URL" → zwei Streams
      eintragen (z. B. https://liveradio.swr.de/sw282p3/swr3/play.mp3 und
      https://st01.dlf.de/dlf/01/104/ogg/stream.ogg) → speichern → Play-Button → erster Stream startet im
      Foreground.
    2. Next/Previous – Skip-Vor/Zurück-Buttons springen zwischen den beiden Streams.
    3. Background – App in den Hintergrund schicken, Bildschirm aus → Musik läuft weiter (Notification
      bleibt sichtbar). App schließen (Recents wischen) → Wiedergabe läuft weiter; Stop-Action in der
      Notification beendet sie.
    4. PTT-Pause – PTT aktivieren → Atmo-Stream pausiert; PTT loslassen → Wiedergabe setzt fort, abhängig
      vom Verhalten (PAUSE_RESUME / FADE_OUT_IN / DUCK / CONTINUE).
    5. Wegpunkt-Pause – an einem Wegpunkt vorbeigehen oder „Test" → Wegpunkt-Ton spielt, Atmo verhält sich
      gemäß Behavior-Modus.
    6. Reorder – Streams in der Liste per / umsortieren → Save → Reihenfolge bleibt nach App-Restart
      erhalten.
    7. Edit & Delete – Eintrag bearbeiten (Name + URL ändern) und entfernen → Liste reagiert sofort.
    8. Shuffle – Switch erscheint bei > 1 Stream-Eintrag, aktivieren → Reihenfolge wird zufällig.
    9. Lokale Playlist – Quelle auf „Lokale Playlist" wechseln → bestehende lokale Playlist + Audio-
      Bibliothek funktionieren unverändert. Audio-Bibliothek-Picker für lokale Files wurde nicht angefasst.
    10. Validation – Stream-URL ohne http(s):// eintragen → Fehlermeldung. Stream-Playlist leer + Quelle
      Stream + Save → Fehlermeldung.

    Migration / Backward-Compatibility:
    11. v2.0.7-Backup importieren – Backup-ZIP aus v2.0.7 öffnen → Touren mit einzelner Stream-URL
    erscheinen nach Import unter „Stream-Playlist" mit genau einem Eintrag (URL-Feld gefüllt, Name leer).
    12. In-Place-Upgrade – v2.0.7-APK deinstallieren nicht notwendig (gleiche Signatur, gleiche
    applicationId). Nach Upgrade auf 2.0.8 zeigen vorhandene Touren ihre alte Stream-URL automatisch in der
    neuen Stream-Playlist.
    13. Backup-Export – Backup in v2.0.8 erstellen → ZIP enthält streamPlaylist-Feld (kann mit unzip -p backup.zip data/music_settings.json inspiziert werden).

    Regressions-Checkliste (v2.0.7-Features)

    • Service-owned ExoPlayerAtmoPlaybackService besitzt weiterhin den Player; Wiedergabe läuft
      durch App-Hintergrund, Bildschirm aus, App-Kill (Recents).
    • PTT pausiert Wegpunkt + AtmoLivePttManager und AtmoResumeManager unverändert. Stream-
      Playlist verhält sich identisch zu lokalen Playlists.
    • Drag-Reorder WegpunkteWaypointListScreen und Drag-Logic unverändert.
    • Wake Lock – ExoPlayer-Wake-Mode (WAKE_MODE_NETWORK für Streams, WAKE_MODE_LOCAL für lokale)
      bleibt erhalten.
    • Map-Provider – Map-Screen + Provider-Auswahl nicht angefasst.
    • Track-Editor + Drafts – Code in editor/TrackDraftListScreen nicht modifiziert.
    • Backup-Export/Import – Music-Settings-JSON erweitert (kompatibel mit alten Backups), alle
      anderen Sektionen unverändert.
    • Map-Suche – Code in MapScreen nur Compiler-Warnung (deprecated fillColor – schon in v2.0.7),
      keine Verhaltensänderung.
    • Audio-BibliothekLibraryPickerDialog und audioLibrary-Flow unverändert.
    • OSM-Attribution – nicht modifiziert.
    • TabRow-Fix – nicht modifiziert.
    • About-Dialog (PayPal-Link, Versions-Notiz) – nicht modifiziert; Version wird automatisch aus
      BuildConfig gelesen (2.0.8).

    Geänderte Dateien

    app/build.gradle.kts                                                versionCode 35→36, versionName "2.0.7"→"2.0.8"
    app/src/main/kotlin/de/waypointaudio/data/TourAudioSettings.kt      + StreamEntry, streamPlaylist, effectiveStreamPlaylist, migrated()
    app/src/main/kotlin/de/waypointaudio/data/TourMusicStore.kt         + StreamEntryJson, streamPlaylist-Persistenz, Migration im Read-Pfad
    app/src/main/kotlin/de/waypointaudio/service/AtmoPlaybackService.kt + EXTRA_STREAM_URLS/NAMES, buildStreamItems(List<StreamEntry>), state für mehrere Streams
    app/src/main/kotlin/de/waypointaudio/service/BackgroundMusicPlayer.kt   srcOk-Check auf effectiveStreamPlaylist
    app/src/main/kotlin/de/waypointaudio/ui/BegleitmusikDialog.kt       komplettes Stream-Playlist-UI (Liste, Add/Edit/Delete, Reorder, Validate, Shuffle)
    app/src/main/kotlin/de/waypointaudio/ui/WaypointListScreen.kt       hasSource-Check auf effectiveStreamPlaylist
    app/src/main/kotlin/de/waypointaudio/viewmodel/WaypointViewModel.kt sourceOk-Check auf effectiveStreamPlaylist
    app/src/main/kotlin/de/waypointaudio/io/BackupExportManager.kt      + streamPlaylist im JSON-Export
    app/src/main/kotlin/de/waypointaudio/io/BackupImportManager.kt      parseMusicSettings liest streamPlaylist mit Legacy-Migration
    app/src/main/res/values/strings.xml                                 + 14 neue Strings music_stream_*
    

    Build-Reproduktion

    unzip GPS2Audio_v2_0_8_stream_playlist_source.zip -d gps2audio
    cd gps2audio
    cat > keystore.properties <<EOF
    storeFile=/path/to/GPS2Audio_release_keystore.jks
    storePassword=<store password>
    keyAlias=gps2audio_release
    keyPassword=<key password>
    EOF
    export ANDROID_HOME=/path/to/android-sdk    # platforms/android-35, build-tools/35.0.0
    gradle :app:assembleRelease
    # Output: app/build/outputs/apk/release/app-release.apk
    

    Verifiziert: apksigner verify --verbose app-release.apkVerifies (v2-Scheme), Signer-CN
    CN=GPS2Audio, OU=NesoHub, O=GPS2Audio, L=Germany, C=DE.

    Downloads
  • 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
  • v2.0.6 b2aa1c167c

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

    GPS2Audio v2.0.6 — Atmo Background Playback Fix (WakeMode)

    Artifacts

    • Signed APK: /home/user/workspace/GPS2Audio_v2_0_6_atmo_background_fix_signed.apk
    • Source zip: /home/user/workspace/GPS2Audio_v2_0_6_atmo_background_fix_source.zip
    • This handoff: /home/user/workspace/GPS2Audio_v2_0_6_atmo_background_fix_handoff.md

    Build & Signing Verification

    Field Value
    applicationId de.waypointaudio
    versionCode 34 (up from 33 in v2.0.5)
    versionName 2.0.6
    minSdk / targetSdk 26 / 35
    compileSdk 35
    Build type release, signed
    Signer DN CN=GPS2Audio, OU=NesoHub, O=GPS2Audio, L=Germany, C=DE
    Signer cert SHA-256 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
    Signer cert SHA-1 EB:92:2A:83:39:09:ED:6B:2D:CB:62:52:F0:CF:42:5F:26:DF:25:AD
    apksigner verify PASS (v2 scheme)
    Source baseline GPS2Audio_v2_0_5_atmo_service_hotfix_source.zip

    The signature matches the GPS2Audio release keystore used since v1 → installs
    as an update over any existing release-channel install (no uninstall required).

    uses-permission set is unchanged vs. v2.0.5 (FOREGROUND_SERVICE_MEDIA_PLAYBACK,
    POST_NOTIFICATIONS, WAKE_LOCK, etc. all preserved).

    Bug Report Summary

    User report: After the v2.0.5 hotfix, pressing Atmo stream Play no longer
    crashes (foreground-service start deadline fix worked) – but when the app is
    minimized / sent to background, the Atmo music still stops/pauses
    within a
    short time. Expected behavior: Atmo continues to play in the background until
    the user (or one of the well-defined app rules) intentionally pauses or stops
    it.

    Root Cause

    In v2.0.4 we added AtmoPlaybackService, a foregroundServiceType="mediaPlayback"
    service that prevents Android from killing the app process while Atmo is alive
    (this is what the v2.0.5 hotfix made crash-safe).

    However, a foreground service alone does NOT keep the device's CPU and
    network awake when the screen turns off.
    The FGS guarantees the process
    stays alive, but the CPU enters low-power state and the network gets
    de-prioritised. For ExoPlayer this manifests as the audio pipeline stalling /
    pausing shortly after screen-off — exactly the user's observation.

    The fix is the one ExoPlayer documents for exactly this case: call
    setWakeMode(...) on the player. This makes ExoPlayer acquire a
    PARTIAL_WAKE_LOCK (CPU stays on while playing) and, for WAKE_MODE_NETWORK,
    additionally a WifiLock so the network connection survives screen-off /
    Doze.

    In v2.0.5's BackgroundMusicPlayer.buildPlayer() we did not call
    setWakeMode(...). With no wake-mode set, ExoPlayer relies on the process
    holding the wake state externally — which, in this app, nobody did once
    FLAG_KEEP_SCREEN_ON (managed by WakeLockController) was cleared on the
    Activity going to onPause. That clearing is correct (only the screen flag
    should drop on minimise — see WakeLockController docstring), and was the
    explicit design since v2.0.0. The missing piece was the audio-pipeline wake
    lock owned by ExoPlayer itself.

    The Foreground Service notification IS being posted (v2.0.4 still works) — it's
    just that without a wake lock, the CPU sleeps and the player has nothing to
    decode with.

    Fix (exact changes)

    File: app/src/main/kotlin/de/waypointaudio/service/BackgroundMusicPlayer.kt

    Inside buildPlayer(settings), immediately after setHandleAudioBecomingNoisy(true),
    we now set the wake mode on the ExoPlayer instance:

    // Wake-Mode auf Network: hält PARTIAL_WAKE_LOCK + WifiLock während
    // der Wiedergabe – damit Stream UND lokale Playlist auch bei
    // ausgeschaltetem Display weiterspielen (v2.0.6 fix).
    // Der Foreground-Service alleine verhindert nur, dass der Prozess
    // gekillt wird – er hält aber nicht die CPU/das Netzwerk wach.
    // ExoPlayer benötigt diesen Modus auf vielen Geräten (Android 12+,
    // Doze), damit die Audio-Pipeline beim Bildschirm-Aus nicht
    // einschläft. Der Lock wird automatisch wieder freigegeben,
    // sobald `release()` oder `stop()` aufgerufen wird.
    if (isStream) {
        p.setWakeMode(C.WAKE_MODE_NETWORK)
    } else {
        p.setWakeMode(C.WAKE_MODE_LOCAL)
    }
    
    • Stream URLsWAKE_MODE_NETWORK (PARTIAL_WAKE_LOCK + WifiLock).
    • Local playlistWAKE_MODE_LOCAL (PARTIAL_WAKE_LOCK only, no WifiLock,
      because no network access is needed once tracks are loaded from local files).

    ExoPlayer releases both locks automatically on Player.stop() /
    Player.release(), which is already wired into BackgroundMusicPlayer.releasePlayer().
    Pause/resume keep the player allocated (the wake lock is held only while
    actually playing, ExoPlayer manages this internally).

    The existing WAKE_LOCK permission in AndroidManifest.xml already covers
    this — it was originally declared "for ExoPlayer", as the manifest comment
    states ("Keep CPU alive while playing audio in service"). It was just never
    being claimed because the player never asked for it.

    File: app/build.gradle.kts

    • versionCode 33 → 34
    • versionName "2.0.5" → "2.0.6"

    No other changes elsewhere.

    What was deliberately NOT changed

    • AtmoPlaybackService — kept exactly as the v2.0.5 hotfix. The
      service-start-deadline fix remains intact (still calls startForeground in
      onCreate as the very first action).
    • MainActivity.onPause/onResume — only WakeLockController is touched; it
      manipulates the screen-on flag (FLAG_KEEP_SCREEN_ON), never the audio
      wake lock. The two are deliberately separate concerns since v2.0.0.
    • WakeLockController — untouched. The screen flag still drops on background
      as designed. ExoPlayer now owns the CPU/wifi wake lock for audio, decoupled
      from screen state.
    • BackgroundMusicManager — untouched. Still an app-singleton owning the
      player; not released on Activity lifecycle (only on release(), which
      nothing currently calls).
    • BackgroundMusicPlayer.beforeWaypointAudio / afterWaypointAudio
      untouched. PTT/waypoint interruption flows unchanged.
    • AtmoResumeManager — untouched.
    • WaypointAudioInterruptionCoordinator — untouched.
    • ViewModel.onCleared() — does NOT release the music manager; only the
      manual player. Confirmed (viewmodel/WaypointViewModel.kt:1533-1539).
    • No LifecycleEventObserver / ProcessLifecycleOwner / DefaultLifecycleObserver
      exists anywhere that would pause Atmo on background — grep-verified.
    • No DisposableEffect releases the Atmo player on screen disposal
      (the two existing DisposableEffect blocks dispose only the map view and
      draft-editor map view).
    • AudioManager.requestAudioFocus is not called on the Atmo path; ExoPlayer's
      audio-focus handling is left off (handleAudioFocus = false) so the app's
      own coordinators decide pause/resume — unchanged.

    How the fix satisfies the requirements

    1. Atmo stream and local playlist continue playing when minimized/backgrounded.
      ExoPlayer now holds PARTIAL_WAKE_LOCK (+ WifiLock for streams) for the
      duration of playback, so CPU/network do not sleep when the screen turns
      off.
    2. Atmo still pauses/resumes for intentional rules (PTT, waypoint, manual
      player, user pause/stop, disable, tour change). All those call paths go
      through BackgroundMusicManager/AtmoResumeManager/beforeWaypointAudio
      – none touched. The wake lock is released by ExoPlayer when pause() is
      called and re-acquired on play(), so pause flows are not impeded.
    3. Wake Lock release on background must not affect audio.
      WakeLockController only touches FLAG_KEEP_SCREEN_ON on the Activity
      window. The audio wake lock is now an independent lock owned by
      ExoPlayer and unaffected by onActivityPaused().
    4. Foreground service is alive for the whole Atmo playback duration.
      Service start/stop is tied to buildPlayer() / releasePlayer() (i.e. to
      the existence of an ExoPlayer instance), not to Activity state. Pause /
      duck / fade keep the player allocated, so the FGS keeps running. ExoPlayer
      does not stop on Activity stop — the manager is an app-singleton, not a
      ViewModel-scoped object.
    5. Notification/service stops when Atmo is truly stopped/released by user
      or no content remains.
      releasePlayer() calls
      AtmoPlaybackService.stop(context); pause does not.
    6. Preserve all v2.0.5 features, no regressions. Manifest unchanged,
      permissions unchanged, all PTT / waypoint / drag-reorder / Wake-Lock-
      settings / map-providers / Track-Editor + drafts / backup import-export /
      map-search / audio-library / OSM-attribution / TabRow fix / About-PayPal +
      version-notice code paths untouched.
    7. Version & signing. versionCode 33 → 34, versionName 2.0.5 → 2.0.6.
      Signed with the existing GPS2Audio release keystore. apksigner verify
      passes. SHA-256 fingerprint matches all prior release builds.
    8. Artifacts produced at the requested paths. See "Artifacts" above.

    Manual Test Plan

    Primary regression (the actual user bug)

    1. Configure Atmo for a tour — either a local playlist OR a stream URL.
    2. Open the app, start Atmo via the music dialog test button or via the
      start-tour flow.
    3. Confirm the ongoing notification "Begleitmusik (Atmo)" / "Begleitmusik läuft"
      appears.
    4. Press Home to minimize the app. Audio must keep playing.
    5. Wait at least 2 minutes with the app in background.
      Audio must still be playing the whole time.
    6. (If possible) Lock the screen (Power button) and wait another 2 minutes.
      Audio must still be playing.
    7. Reopen the app — the mini-player / dialog must show the same track /
      stream still playing, no glitch / restart, no double-track, no error.

    PTT still pauses Atmo (v2.0.3 contract)

    1. With Atmo playing in foreground, hold PTT.
    2. Atmo pauses (or fades / ducks per settings).
    3. Release PTT — Atmo resumes from where it left off.
    4. Repeat with the app minimized / screen off — Atmo should still pause when
      PTT is engaged and resume when released.

    Waypoint audio still pauses Atmo (v2.0.3 contract)

    1. Manually trigger a waypoint audio from the list, or walk into a radius.
    2. Atmo follows the configured WaypointMusicBehavior
      (PAUSE_RESUME / FADE_OUT_IN / DUCK_UNDERLAY / CONTINUE_UNDERLAY).
    3. After the waypoint audio finishes naturally, Atmo continues / resumes /
      auto-starts per autoStartAfterWaypoint.

    Manual player still pauses Atmo

    1. Tap a waypoint to play its audio manually.
    2. Atmo pauses for the duration, resumes after.

    User stop / pause / disable still works

    1. Press Stop in the music dialog/mini-player → Atmo stops,
      notification disappears within ~1 s, no resume.
    2. Press Pause → Atmo pauses, notification stays (player still allocated),
      pressing play resumes from same position.
    3. Open Begleitmusik dialog → toggle "Aktiviert" off → save. Atmo stops,
      notification disappears.

    Tour change

    1. Switch to a tour with enabled=false Atmo. Atmo stops on tour switch,
      notification disappears.
    2. Switch to a tour with a different stream/playlist with enabled=true
      new content takes over.

    Headphone unplug

    1. With wired/Bluetooth headphones connected and Atmo playing, unplug
      (or disconnect Bluetooth). Atmo pauses (does not stop) —
      setHandleAudioBecomingNoisy(true) unchanged.
    2. Press play in dialog/mini-player to resume.

    Wake Lock settings

    1. Set Wake Lock mode to "Active tour only". Start the tour, screen stays on.
    2. Press Power to lock the screen. The screen-on flag releases (expected),
      but Atmo keeps playing (audio wake lock is independent).
    3. Unlock — Atmo is still playing, screen-on flag re-applies if mode dictates.

    Battery / Doze sanity

    • On a stock Android device with Battery Optimisation enabled for the app,
      Atmo should still play continuously in background — PARTIAL_WAKE_LOCK
      acquired by a foreground-service-backed app is an explicitly permitted use
      case on all Android versions.

    Regression Checklist (v2.0.x feature contract)

    • PTT pauses waypoint audio (v2.0.3 feature) — unchanged code path.
    • PTT pauses Atmo with resume — AtmoResumeManager.notifyInterruptStart
      still calls pauseMusic(), only releasing service on hard stop.
    • Atmo plays in background — v2.0.4 FGS + v2.0.6 wake mode.
    • No FGS-deadline crash on Atmo stream Play — v2.0.5 hotfix preserved.
    • Drag-and-drop reorder of waypoints.
    • Wake Lock settings (Never / App foreground / Active tour) — unchanged
      semantics, audio decoupled.
    • Map providers — unchanged.
    • Track-Editor + Drafts — unchanged.
    • Backup import/export — unchanged.
    • Map search — unchanged.
    • Audio library — unchanged.
    • OSM attribution badge on map — unchanged.
    • TabRow fix — unchanged.
    • About dialog PayPal link + Matrix QR — unchanged.
    • Version-notice popup (once per new versionCode) — fires on first launch
      of 2.0.6 since versionCode bumped 33 → 34.

    Build Reproduction

    unzip GPS2Audio_v2_0_6_atmo_background_fix_source.zip -d /tmp/v2.0.6
    cd /tmp/v2.0.6/work
    cat > keystore.properties <<EOF
    storeFile=/path/to/GPS2Audio_release_keystore.jks
    storePassword=...
    keyAlias=gps2audio_release
    keyPassword=...
    EOF
    export ANDROID_HOME=/path/to/android-sdk
    gradle :app:assembleRelease
    # Output: app/build/outputs/apk/release/app-release.apk
    

    Tested toolchain: Gradle 8.9, JDK 21 (OpenJDK), Android SDK with platform-35
    and build-tools 35.0.0.

    Downloads
  • v2.0.5 b2aa1c167c

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

    GPS2Audio v2.0.5 — Atmo Foreground-Service Hotfix

    Artifacts

    • Signed APK: /home/user/workspace/GPS2Audio_v2_0_5_atmo_service_hotfix_signed.apk
    • Source zip: /home/user/workspace/GPS2Audio_v2_0_5_atmo_service_hotfix_source.zip
    • This handoff: /home/user/workspace/GPS2Audio_v2_0_5_atmo_service_hotfix_handoff.md

    Build & Signing Verification

    Field Value
    applicationId de.waypointaudio
    versionCode 33 (up from 32 in v2.0.4)
    versionName 2.0.5
    minSdk / targetSdk 26 / 35
    compileSdk 35
    Build type release, signed
    Signer DN CN=GPS2Audio, OU=NesoHub, O=GPS2Audio, L=Germany, C=DE
    Signer cert SHA-256 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
    Signer cert SHA-1 EB:92:2A:83:39:09:ED:6B:2D:CB:62:52:F0:CF:42:5F:26:DF:25:AD

    Signature matches the GPS2Audio release keystore used since v1 → installs as an
    update over any existing release-channel install (no uninstall required).

    uses-permission set is unchanged vs. v2.0.4 (FOREGROUND_SERVICE_MEDIA_PLAYBACK,
    POST_NOTIFICATIONS, WAKE_LOCK, etc. all preserved).

    Bug Report Summary (what crashed)

    After pressing the Atmo stream play button the app crashed with:

    android.app.RemoteServiceException$ForegroundServiceDidNotStartInTimeException:
      Context.startForegroundService() did not then call Service.startForeground():
      ServiceRecord ... de.waypointaudio/.service.AtmoPlaybackService
    

    This is the Android 12+ deadline crash: when an app calls
    Context.startForegroundService(intent), the system gives the service a hard
    window (~5 s) to call Service.startForeground(id, notification). Missing the
    deadline → the platform kills the app process.

    Root Cause

    In v2.0.4 (app/src/main/kotlin/de/waypointaudio/service/AtmoPlaybackService.kt)
    the deadline-relevant startForeground(...) call lived inside
    onStartCommand, gated by intent?.action == ACTION_START, and wrapped in
    runCatching {} that silently swallowed any throwable. Two concrete ways
    this could miss the deadline:

    1. Non-START intent on the same startForegroundService() cycle. Both
      start() and stop() used startForegroundService()/startService(), but
      the when (intent?.action) branch for ACTION_STOP (or a null intent on
      re-delivery / START_STICKY_COMPATIBILITY scenarios) never reached
      startForeground. The system had been told "this is a FGS" but the service
      never confirmed it. Deadline missed → crash.
    2. runCatching swallowing a startForeground throwable. If notification
      construction or startForeground itself threw (e.g. transient
      RemoteException from NotificationManager, or any backwards-compatibility
      edge from setForegroundServiceType), the failure was logged and the
      service kept running without ever calling startForeground again.
      Deadline missed → crash.

    The screenshot reported by the user is reproducible on Android 12+ devices
    where the first start path was for a stream and any of the above branches
    triggered.

    Fix (exact changes)

    File: app/src/main/kotlin/de/waypointaudio/service/AtmoPlaybackService.kt

    1. startForeground() is now the very first thing the service does, in
      onCreate(), with a minimal-valid notification (no network/player work,
      no Intent parsing). This satisfies the system deadline unconditionally,
      before onStartCommand is even invoked.
    2. Re-arm in onStartCommand: if the first startForeground attempt
      threw, we retry as the very first action of onStartCommand (if (!foregroundStarted)), before any branching on the action.
    3. Notification channel created before startForeground on Android O+
      (already true, kept; added a SDK < O early-return so the call is safe
      on older devices).
    4. Notification refresh on ACTION_START uses NotificationManager.notify
      with the same ID — keeps the current foreground notification valid even if
      the rebuild with real title/source throws.
    5. stop() companion now also uses startForegroundService() on Android
      O+ instead of startService(). This avoids IllegalStateException if the
      stop is requested while the app is in a state the platform considers
      "background", and it guarantees the service still calls startForeground
      immediately (in onCreate) before stopping itself — so the stop path can
      never be the cause of a missed-deadline crash either.
    6. onStartCommand now handles null/unknown actions by stopping the
      service (after the mandatory startForeground) rather than leaving the
      service running without any work.
    7. foregroundStarted flag prevents redundant startForeground calls on
      repeated starts and is reset in stopForegroundCompat.

    foregroundServiceType=mediaPlayback and the
    FOREGROUND_SERVICE_MEDIA_PLAYBACK permission are preserved. The
    POST_NOTIFICATIONS permission was already declared and is not hard-required
    for a foreground-service notification — denying it merely hides the
    notification, the FGS still runs.

    File: app/build.gradle.kts

    • versionCode = 33, versionName = "2.0.5".

    No changes elsewhere. All v2.0.4 behaviors preserved:

    • Atmo continues playing in background while screen is off or app is minimized
      (FGS still active across pause/duck/fade, as before).
    • PTT and Waypoint-Audio pause/resume semantics unchanged
      (BackgroundMusicPlayer.beforeWaypointAudio / afterWaypointAudio
      untouched; FGS deliberately stays alive across pauses).
    • Manual stop / tour switch still triggers
      BackgroundMusicPlayer.releasePlayer()AtmoPlaybackService.stop()
      notification + service torn down.
    • All previously delivered features intact: drag-and-drop reorder, Wake Lock
      while recording / playing, multiple map providers, Track-Editor, Backup
      import/export, map search, audio library, OSM attribution, TabRow fix,
      About PayPal link + version notice dialog.

    Manual Test Plan

    These tests target the exact crash and the surrounding Atmo lifecycle. Use a
    device on Android 12+ (deadline-enforcing).

    1. Atmo stream play — no crash (primary repro).

      • Open Tour-Settings → Begleitmusik (Atmo) → switch source to Stream URL,
        enter a valid stream (e.g. an Icecast .mp3 URL).
      • Press the Play button.
      • Expected: audio starts within a few seconds; no crash; status bar
        shows an ongoing "Atmo / Stream — " notification.
    2. Background continuation.

      • With Atmo playing, press Home; then power off the screen.
      • Expected: audio keeps playing for at least 60 s; FGS notification
        remains visible in the status bar.
    3. Stop / disable.

      • Re-open app, press Stop on the Atmo controls (or disable Atmo in
        Tour-Settings).
      • Expected: audio stops; foreground notification disappears; no crash.
        Re-pressing Play starts a new stream cleanly.
    4. PTT interplay (do not regress).

      • Start Atmo, then start a PTT session.
      • Expected: Atmo behavior follows configured Wegpunkt-Verhalten
        (pause/duck/fade/continue) for the PTT duration; FGS notification stays
        ongoing; Atmo resumes (or stays ducked) on PTT end exactly as in v2.0.4.
    5. Waypoint audio interplay.

      • Place a waypoint with an audio file along a recorded track; trigger it
        while Atmo is playing.
      • Expected: Atmo behaves per configured behavior; FGS stays up; Atmo
        resumes after waypoint audio finishes, matching v2.0.4 behavior.
    6. Local playlist source.

      • Switch Atmo source to Local playlist, pick 2–3 tracks, press Play.
      • Expected: plays; notification reads "Atmo / Playlist — N Titel"; no
        crash; track skip / shuffle behave as before.
    7. POST_NOTIFICATIONS denied (Android 13+).

      • Long-press the Atmo notification (or use App Info → Notifications) and
        disable notifications for GPS2Audio, then start Atmo.
      • Expected: audio still plays in foreground (FGS keeps the process
        alive); no crash; notification simply suppressed by user choice.
    8. Repeated rapid Start/Stop.

      • Toggle Atmo Play/Stop several times in quick succession.
      • Expected: no crash; notification appears/disappears cleanly; service
        ends after final Stop.
    9. Version check.

      • Settings → About → version line reads 2.0.5 (33).

    Files Touched (delta vs. v2.0.4 baseline)

    app/build.gradle.kts                                                 (versionCode 32→33, versionName 2.0.4→2.0.5)
    app/src/main/kotlin/de/waypointaudio/service/AtmoPlaybackService.kt  (rewritten: always-on startForeground, null-intent handling, stop via startForegroundService)
    

    Nothing else changed.

    Downloads
  • v2.0.4 b2aa1c167c

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

    GPS2Audio v2.0.4 — Atmo Background Playback Fix

    Overview

    • Version: 2.0.4 (versionCode 32, ↑ from 31)
    • Baseline: GPS2Audio_v2_0_3_ptt_waypoint_pause_source.zip
    • Build type: signed release, RSA-4096 (gps2audio_release alias), SHA-256
      fingerprint matches the v2.0.x release keystore on file.

    Artifacts

    • APK: /home/user/workspace/GPS2Audio_v2_0_4_atmo_background_signed.apk
    • Source: /home/user/workspace/GPS2Audio_v2_0_4_atmo_background_source.zip
    • Handoff: /home/user/workspace/GPS2Audio_v2_0_4_atmo_background_handoff.md

    Root cause

    v2.0.3 starts Atmo (Begleitmusik) through BackgroundMusicManager/BackgroundMusicPlayer,
    an app-singleton that owns an ExoPlayer instance. No code in MainActivity,
    DisposableEffect, Lifecycle observer, or onCleared() calls pause()/stop() on
    the player when the app moves to background — i.e. the app was not explicitly
    pausing Atmo on minimize.

    However, when the activity goes to background and the screen turns off, the
    Android process is moved to a "cached / restricted background" state. Because no
    foreground service of type mediaPlayback was holding the audio session,
    Android terminates or freezes the process within ~10–30 s, which the user
    observes as "Atmo stopped when minimized". When the activity is brought back to
    the front, the user manually re-enters Atmo or the GPS service rebinds and
    restarts the player — looking like "starts again on reopen".

    This is the canonical Android behaviour for non-foreground audio playback on
    API 26+ (and tightens on Android 12 / 14). The robust, documented fix is to run
    a foregroundServiceType="mediaPlayback" service whenever Atmo is alive.

    Exact change

    1. New foreground service AtmoPlaybackService

    • File: app/src/main/kotlin/de/waypointaudio/service/AtmoPlaybackService.kt
    • foregroundServiceType="mediaPlayback" (uses
      FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK on API 34+).
    • Owns no playback logic — only signals the OS that the app is legitimately
      playing media in the background and posts a low-priority, silent ongoing
      notification (channel atmo_playback_channel, importance LOW).
    • start(ctx,title,source) / stop(ctx) companion helpers; stopForeground
      with STOP_FOREGROUND_REMOVE removes the notification on stop.

    2. BackgroundMusicPlayer hooks the service lifecycle to the player lifetime

    • buildPlayer()AtmoPlaybackService.start(...) immediately after building
      the ExoPlayer (i.e. when Atmo actually starts playing for a tour / test).
    • releasePlayer()AtmoPlaybackService.stop(ctx).
    • Pause/duck/fade do NOT stop the service. The service is tied to the
      existence of an active ExoPlayer instance, not to play/pause state. This is
      what preserves PTT, waypoint, and manual-player interruption resumes —
      during those, the player is paused but still allocated, so the foreground
      service keeps the process alive and resume works exactly as in v2.0.3.

    3. ExoPlayer audio plumbing

    • setAudioAttributes(USAGE_MEDIA / AUDIO_CONTENT_TYPE_MUSIC, handleAudioFocus=false)
      — declares the stream as media. Audio-focus arbitration stays with the
      app-internal coordinator (AtmoResumeManager, WaypointAudioInterruptionCoordinator)
      exactly as before; ExoPlayer does not auto-pause on transient focus loss.
    • setHandleAudioBecomingNoisy(true) — when headphones are unplugged, ExoPlayer
      pauses (not stops). The foreground service stays up, so the user can
      resume from PTT/waypoint logic or the manual play button.

    4. Manifest

    • Added permission android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK
      (Android 14+ requirement).
    • Declared the new service with foregroundServiceType="mediaPlayback".

    5. Strings (German UI, consistent with rest of app)

    • atmo_notification_channel_name — "Begleitmusik (Atmo)"
    • atmo_notification_channel_description
    • atmo_notification_default_title — "Begleitmusik läuft"
    • atmo_notification_default_source — fallback when stream/playlist label
      is blank.

    6. Version bump

    • versionCode 31 → 32, versionName 2.0.3 → 2.0.4 in app/build.gradle.kts.

    What was deliberately NOT changed

    • MainActivity.onPause/onResume — only WakeLockController is touched there;
      it manipulates the screen-on flag, never the audio. Confirmed via grep:
      no pauseMusic/stopMusic/player.pause/player.stop is called from any
      Activity / Compose lifecycle path.
    • BackgroundMusicManager.loadTour() — still stops music on intentional
      tour change (previousTour != tourName with enabled=false).
    • BackgroundMusicManager.saveAndApplySettings() — still stops music when the
      user disables Begleitmusik.
    • AtmoResumeManager (PTT pause/resume) — untouched.
    • WaypointAudioInterruptionCoordinator — untouched.
    • BackgroundMusicPlayer.beforeWaypointAudio / afterWaypointAudio — untouched.
    • WakeLockController — untouched. The window flag still drops on background
      (as designed in v2.0.0); audio is now decoupled from that flag.

    Manual test plan

    Primary regression (the actual user bug)

    1. Set up Atmo for a tour (either local playlist or HTTP stream).
    2. Open the app, start Atmo via the music dialog test button or via the
      start-tour flow.
    3. Confirm a small ongoing notification "Begleitmusik (Atmo)" appears.
    4. Press the Home button to minimize. Audio must keep playing.
    5. Lock the screen. Wait at least 60 seconds with the screen off.
    6. Audio must still be playing. Unlock the screen — still playing.
    7. Reopen the app — the music dialog / mini-player must show "Playing"
      with the same track / stream still running, no glitch / restart.

    PTT still pauses Atmo (v2.0.3 contract)

    1. With Atmo playing in foreground, hold PTT.
    2. Atmo pauses (or fades / ducks per settings).
    3. Release PTT — Atmo resumes from where it left off.
    4. Repeat with the app minimized: hold PTT (e.g. via headset key if mapped),
      Atmo should still pause/resume.

    Waypoint audio still pauses Atmo (v2.0.3 contract)

    1. Manually trigger a waypoint audio from the list, or walk into a radius.
    2. Atmo follows the configured WaypointMusicBehavior
      (PAUSE_RESUME / FADE_OUT_IN / DUCK_UNDERLAY / CONTINUE_UNDERLAY).
    3. After the waypoint audio finishes naturally, Atmo continues / resumes /
      auto-starts per autoStartAfterWaypoint.

    Manual player still pauses Atmo

    1. Tap a waypoint to play its audio manually.
    2. Atmo pauses for the duration, resumes after.

    User stop / pause / disable still works

    1. Press stop in the music dialog → Atmo stops, notification disappears,
      no resume.
    2. Press pause → Atmo pauses, notification stays (player still allocated),
      pressing play resumes.
    3. Open Begleitmusik dialog → toggle "Aktiviert" off → save. Atmo stops,
      notification disappears.

    Tour change

    1. Switch to a tour that has enabled=false. Atmo stops on tour switch.

    Headphone unplug

    1. With wired/Bluetooth headphones connected and Atmo playing, unplug
      (or disconnect Bluetooth). Atmo pauses (does not stop) — resume by
      pressing play in the dialog/mini-player.

    Wake Lock interaction

    1. Set Wake Lock mode to "Active tour only". Start the tour, screen stays on.
    2. Press Power to lock the screen. The screen-on flag releases (expected),
      but Atmo keeps playing.
    3. Unlock — Atmo is still playing, screen-on flag re-applied if mode dictates.

    Regression checklist

    • PTT pauses waypoint audio (v2.0.3 feature) — unchanged code path.
    • PTT pauses Atmo with resume — AtmoResumeManager.notifyInterruptStart
      still calls pauseMusic(), only releasing service on hard stop.
    • Drag-and-drop reorder of waypoints — unrelated, untouched.
    • Wake Lock settings (NEVER / APP_FOREGROUND / ACTIVE_TOUR) — untouched.
    • Map provider selection — untouched.
    • Track-Editor / drafts — untouched.
    • Backup import / export — untouched.
    • Map search (everywhere / current view) — untouched.
    • Audio library playlist (drag/drop, picker) — untouched.
    • OSM attribution badge — untouched.
    • TabRow fix — untouched.
    • About / PayPal / version notice — untouched.
    • Signed-release path (keystore.properties + env vars) — unchanged.
    • Version notice will trigger again because versionCode advanced (31 → 32).
    • APK targets SDK 35, min SDK 26, includes the new
      FOREGROUND_SERVICE_MEDIA_PLAYBACK permission and the new service in
      manifest (verified via aapt dump badging).
    • Signature: SHA-256 7B:0E:A4:D5:…:BB:46 matches keystore record (verified
      via apksigner verify --print-certs).

    Limitations

    • The user will now see a persistent low-priority "Begleitmusik (Atmo)"
      notification while Atmo is loaded. This is required by Android 12+ to keep
      background audio reliably alive and is the minimal acceptable solution.
      It is silent (setSilent(true), importance LOW, setOnlyAlertOnce) and
      disappears the moment the user presses Stop or Begleitmusik is disabled.
    • The notification has no media-style transport controls. A future iteration
      could add play/pause/next/previous through a MediaSessionService; left out
      intentionally to keep the diff minimal and avoid touching the v2.0.3 PTT /
      waypoint pause logic.

    Build verification

    package: name='de.waypointaudio' versionCode='32' versionName='2.0.4'
    sdkVersion:'26' targetSdkVersion:'35'
    uses-permission: …FOREGROUND_SERVICE_MEDIA_PLAYBACK
    service: de.waypointaudio.service.AtmoPlaybackService (mediaPlayback)
    Signer #1 SHA-256: 7b0ea4d5528605173d299d7b268e7ab8768dd504ec15b66a52d47b4257b3bb46
    BUILD SUCCESSFUL (gradle 8.9, AGP via wrapper config, SDK 35 build-tools 35.0.0)
    
    Downloads
  • v2.0.3 b2aa1c167c

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

    GPS2Audio v2.0.3 — PTT pauses waypoint audio

    Summary

    This release adds a single focused behavior on top of v2.0.2:

    When the user starts PTT (Push-To-Talk) while a waypoint audio is playing
    (either GPS-triggered or manually triggered), the waypoint audio is paused
    immediately so that PTT and the waypoint audio are never heard at the same
    time. When the user releases PTT, the waypoint audio resumes exactly where
    it left off — but only if it was already playing when PTT started.

    All other v2.0.2 functionality is preserved unchanged (drag-reorder, tour
    counters, backup import/export, atmo pause/resume during PTT, manual
    playlist & single-item playback, etc.). There are no UI changes.

    Build & signing

    • Baseline: GPS2Audio_v2_0_2_drag_reorder_source.zip
    • applicationId: de.waypointaudio (unchanged)
    • versionCode: 31 (was 30 in v2.0.2)
    • versionName: 2.0.3
    • Signed with /home/user/workspace/GPS2Audio_release_keystore.jks,
      alias gps2audio_release.
    • Verified apksigner verify --print-certs:
      • SHA-256: 7B0EA4D5528605173D299D7B268E7AB8768DD504EC15B66A52D47B4257B3BB46
        (matches keystore credentials file → upgrade-compatible with previous
        signed releases).
    • Build host: Gradle 8.9, AGP per libs.versions.toml, Android SDK 35.

    Artifacts

    Path Contents
    /home/user/workspace/GPS2Audio_v2_0_3_ptt_waypoint_pause_signed.apk Signed release APK
    /home/user/workspace/GPS2Audio_v2_0_3_ptt_waypoint_pause_source.zip Full source tree (no keystore, no local.properties, no build outputs)
    /home/user/workspace/GPS2Audio_v2_0_3_ptt_waypoint_pause_handoff.md This document

    Behavioural contract

    1. PTT start while a waypoint audio is playing → that waypoint audio is
      paused (its MediaPlayer position is retained).
    2. PTT start while no waypoint audio is playing → no pause/resume happens
      for waypoint audio (Atmo handling continues to work as before).
    3. PTT end → only those waypoint audios that were paused by this PTT
      session are resumed. Anything that was already stopped/finished is not
      resurrected.
    4. Duplicate PTT state events (e.g. another startPtt() while already
      active) → no repeated pause/resume. Idempotency is enforced both in
      LivePttManager and inside
      WaypointAudioInterruptionCoordinator.beginInterruption / endInterruption.
    5. Atmo (background music) behaviour is unchanged: it is still paused on
      PTT start and resumed on PTT end via the existing AtmoResumeManager.
    6. GPS-triggered waypoints fired during PTT continue to be queued and
      played after PTT ends (existing pendingPttWaypoint flow in
      WaypointLocationService). Unchanged.
    7. Manual playback during PTT is still blocked by the existing
      "PTT-Sperre" guard in WaypointViewModel.manualPlayPause and
      manualPlaySingle. Unchanged.

    Implementation

    New file

    • app/src/main/kotlin/de/waypointaudio/service/WaypointAudioInterruptionCoordinator.kt
      • Process-wide object singleton.
      • Two Sources: GPS (location service AudioPlayer), MANUAL
        (ManualAudioPlayer in WaypointViewModel).
      • Each active player registers with three lambdas: isCurrentlyPlaying,
        pause, resume.
      • beginInterruption() pauses all currently-playing sources and remembers
        which ones it paused. endInterruption() resumes exactly those.
      • Self-deduplicates against double-beginInterruption() / double-
        endInterruption() calls (no-op if state is wrong).
      • Edge case: if a source registers during an active interruption and is
        already playing, it is paused immediately and marked for resume on
        endInterruption().

    Changed files

    • app/src/main/kotlin/de/waypointaudio/service/AudioPlayer.kt
      • Added pause(), resume(), isPlaying (property), hasContent
        (property). Existing play() / stop() semantics unchanged.
    • app/src/main/kotlin/de/waypointaudio/service/ManualAudioPlayer.kt
      • Added resume() to complement existing pause(). The play()-resumes-
        paused-track path is untouched (manual UI resume still works).
    • app/src/main/kotlin/de/waypointaudio/service/WaypointLocationService.kt
      • playWaypointAudio() now registers the running audioPlayer with the
        coordinator (Source.GPS) and unregisters on natural completion, on
        error, and on stopTracking().
    • app/src/main/kotlin/de/waypointaudio/service/LivePttManager.kt
      • pauseAtmo() now also calls
        WaypointAudioInterruptionCoordinator.beginInterruption().
      • restoreAtmo() now also calls
        WaypointAudioInterruptionCoordinator.endInterruption() before the
        Atmo resume, so the waypoint audio comes back first and Atmo follows
        its existing logic.
    • app/src/main/kotlin/de/waypointaudio/viewmodel/WaypointViewModel.kt
      • Registers manualPlayer with the coordinator (Source.MANUAL) on VM
        creation and unregisters in onCleared(). isCurrentlyPlaying reads
        the underlying MediaPlayer.isPlaying so that only an actively-playing
        manual track is paused by PTT — a paused or stopped manual track is
        left alone.
    • app/build.gradle.kts
      • versionCode = 31, versionName = "2.0.3".

    Files NOT changed

    No XML resources, no Compose UI, no manifests, no permissions, no Atmo
    logic, no backup/import code, no map code, no drag-reorder code, no
    PTT UI (LivePttCard.kt).

    Manual test plan

    Install over an existing v2.0.2 build of de.waypointaudio (signature
    matches, so direct update works without uninstall).

    1. Sanity: nothing is playing, press PTT, release.

      • Expected: PTT mic activates and deactivates normally. No waypoint
        audio appears. No regression.
    2. Atmo only, PTT toggles.

      • Start Atmo (Begleitmusik) for a tour. Press PTT → Atmo pauses. Release
        PTT → Atmo resumes. (Pre-existing behaviour, must still work.)
    3. Manual single-item waypoint, PTT toggles.

      • In waypoint list, press Play on a card. While audio plays, press PTT
        start. Audio must go silent immediately. Release PTT. Audio
        resumes from the same position.
        Pause icon state on the card is
        unchanged across the cycle.
    4. Manual playlist, PTT toggles.

      • From the manual player bar, start playback of the tour. While playing,
        press PTT. Audio pauses. Release PTT. Audio resumes from the same
        spot.
    5. GPS waypoint trigger + PTT mid-playback.

      • Walk into a waypoint radius so its audio fires automatically. While
        it plays, press PTT. Waypoint audio pauses. Release PTT.
        Waypoint audio resumes and finishes; playCount increments only
        once on natural completion.
    6. GPS waypoint trigger during PTT (existing queue path).

      • Press PTT and hold. While PTT is active, cross into a waypoint
        radius. The waypoint should NOT play yet (existing behaviour).
        Release PTT → the queued waypoint plays normally.
    7. Duplicate PTT state events.

      • Rapidly toggle PTT or invoke togglePtt() repeatedly while audio is
        playing. The waypoint audio should pause exactly once per real
        activation and resume exactly once per real deactivation — never
        "double-resume" into a runaway.
    8. PTT during a manually paused track.

      • Start a manual single-item audio, press Pause on the card. Press PTT
        start → release. The track must NOT auto-start on PTT release (it
        was already paused before PTT, so the coordinator does nothing for
        it).
    9. Stop GPS service or close app while paused by PTT.

      • During PTT-induced pause, kill the GPS service or background the
        app. Release PTT → coordinator's resume call is a no-op (player
        released). No crash.
    10. Atmo + manual waypoint + PTT.

      • Start Atmo and a manual waypoint together (waypoint ducks Atmo per
        existing logic). Press PTT → both Atmo and the waypoint audio go
        silent. Release PTT → both come back. Atmo's resume is still
        gated by its own rules in AtmoResumeManager.

    Known limitations

    • The pause is implemented on top of Android's MediaPlayer.pause() /
      .start(). On a tiny number of buggy device firmwares this can introduce
      a sub-second click when resuming. The existing manual-pause UI uses the
      same primitive, so this matches user expectations.
    • If the user manually stops a waypoint audio during PTT (e.g. through
      some future UI), the coordinator's resume call becomes a safe no-op
      because the underlying MediaPlayer is null. Current UI does not
      expose any such control while PTT is held, so this is theoretical.

    Rollback

    Replace the installed APK with GPS2Audio_v2_0_2_drag_reorder_signed.apk.
    versionCode 30 < 31 means Android may require uninstall to downgrade.

    Downloads