# 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](app/src/main/kotlin/de/waypointaudio/data/TourAudioSettings.kt)): ```kotlin data class StreamEntry(val url: String, val name: String = "") data class TourAudioSettings( … val streamPlaylist: List = emptyList(), val streamUrl: String? = null, // Legacy-Feld – nur für Backward-Compat … ) { val effectiveStreamPlaylist: List 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ötig** – `TourMusicStore` 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](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](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 `MediaItem`s 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](app/src/main/kotlin/de/waypointaudio/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](app/src/main/kotlin/de/waypointaudio/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) - [x] **Service-owned ExoPlayer** – `AtmoPlaybackService` besitzt weiterhin den Player; Wiedergabe läuft durch App-Hintergrund, Bildschirm aus, App-Kill (Recents). - [x] **PTT pausiert Wegpunkt + Atmo** – `LivePttManager` und `AtmoResumeManager` unverändert. Stream- Playlist verhält sich identisch zu lokalen Playlists. - [x] **Drag-Reorder Wegpunkte** – `WaypointListScreen` und Drag-Logic unverändert. - [x] **Wake Lock** – ExoPlayer-Wake-Mode (`WAKE_MODE_NETWORK` für Streams, `WAKE_MODE_LOCAL` für lokale) bleibt erhalten. - [x] **Map-Provider** – Map-Screen + Provider-Auswahl nicht angefasst. - [x] **Track-Editor + Drafts** – Code in `editor`/`TrackDraftListScreen` nicht modifiziert. - [x] **Backup-Export/Import** – Music-Settings-JSON erweitert (kompatibel mit alten Backups), alle anderen Sektionen unverändert. - [x] **Map-Suche** – Code in `MapScreen` nur Compiler-Warnung (deprecated `fillColor` – schon in v2.0.7), keine Verhaltensänderung. - [x] **Audio-Bibliothek** – `LibraryPickerDialog` und `audioLibrary`-Flow unverändert. - [x] **OSM-Attribution** – nicht modifiziert. - [x] **TabRow-Fix** – nicht modifiziert. - [x] **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), 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 ```bash unzip GPS2Audio_v2_0_8_stream_playlist_source.zip -d gps2audio cd gps2audio cat > keystore.properties < keyAlias=gps2audio_release keyPassword= 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.apk` → `Verifies` (v2-Scheme), Signer-CN `CN=GPS2Audio, OU=NesoHub, O=GPS2Audio, L=Germany, C=DE`.