fix(android): Warteschlangen-Wiederherstellung robuster und nachvollziehbar (1.5)
Das Wiederherstellen schlug still fehl. Mehrere runCatching verschluckten jeden Grund, sodass von aussen nur "passiert nichts" zu sehen war. Zwei wahrscheinliche Ursachen adressiert: - connect() laeuft in Application.onCreate(), wo der DataStore mit den Navidrome-Zugangsdaten unter Umstaenden noch nicht gelesen ist. Dann scheitert das Aufloesen ALLER Song-Ids und die Warteschlange bleibt leer. Es wird jetzt bis zu fuenfmal mit wachsendem Abstand versucht. - Wischt man die App aus den letzten Apps, wurde nichts gesichert: onIsPlayingChanged feuert dabei nicht zuverlaessig. MusicService.onTaskRemoved sichert jetzt vor dem Beenden. Dazu Protokollausgaben unter dem Tag NsQueue an jeder Stufe (gesichert, gefunden, Id nicht aufloesbar, wiederhergestellt, aufgegeben), damit ein weiterer Fehlschlag per logcat zu klaeren ist statt per Vermutung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8da67e0975
commit
0b23a07039
3 files changed
+64
-17
No files matched your search
@@ -14,8 +14,8 @@ android {
|
||||
applicationId = "com.novasonic.app"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 7
|
||||
versionName = "1.4"
|
||||
versionCode = 8
|
||||
versionName = "1.5"
|
||||
// Host der eigenen NovaSonic-Web-App — Ziel der geteilten Links. Einmal in
|
||||
// gradle.properties setzen (novasonicHost=music.example.com); ohne Angabe bleibt der
|
||||
// Platzhalter stehen und der App-Link greift schlicht nicht.
|
||||
|
||||
@@ -238,7 +238,14 @@ class MusicService : MediaLibraryService() {
|
||||
|
||||
override fun onGetSession(info: MediaSession.ControllerInfo) = session
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) { player.stop(); stopSelf() }
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
// Wischt jemand die App aus den letzten Apps, wird der Prozess gleich beendet — hier ist die
|
||||
// letzte Gelegenheit, die Warteschlange festzuhalten. Ohne das ginge sie genau in dem Fall
|
||||
// verloren, in dem man sie beim naechsten Oeffnen am ehesten erwartet.
|
||||
runCatching { (application as? com.novasonic.app.NovaSonicApp)?.playerController?.saveQueueNow() }
|
||||
player.stop()
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
scope.cancel()
|
||||
|
||||
@@ -37,6 +37,8 @@ class PlayerController(private val context: Context, private val repo: MusicRepo
|
||||
private val _state = MutableStateFlow(PlayerState())
|
||||
val state: StateFlow<PlayerState> = _state.asStateFlow()
|
||||
|
||||
private val TAG_QUEUE = "NsQueue"
|
||||
|
||||
private val scope = CoroutineScope(
|
||||
Dispatchers.Main + SupervisorJob() +
|
||||
CoroutineExceptionHandler { _, t -> android.util.Log.e("PlayerController", "Uncaught coroutine exception", t) }
|
||||
@@ -71,27 +73,65 @@ class PlayerController(private val context: Context, private val repo: MusicRepo
|
||||
val ids = queue.map { it.id }
|
||||
if (ids.isEmpty() || ids.any { it.startsWith("abs:") }) return
|
||||
val index = ctrl.currentMediaItemIndex
|
||||
scope.launch { runCatching { repo.saveQueueState(ids, index) } }
|
||||
scope.launch {
|
||||
runCatching { repo.saveQueueState(ids, index) }
|
||||
.onSuccess { android.util.Log.i(TAG_QUEUE, "gesichert: ${ids.size} Titel, Index $index") }
|
||||
.onFailure { android.util.Log.w(TAG_QUEUE, "Sichern fehlgeschlagen", it) }
|
||||
}
|
||||
}
|
||||
|
||||
/** Von aussen aufrufbar (Dienst-Ende beim Wegwischen der App). */
|
||||
fun saveQueueNow() = persistQueueState()
|
||||
|
||||
/**
|
||||
* Stellt die zuletzt gehoerte Warteschlange wieder her — vorbereitet, aber NICHT abgespielt.
|
||||
* Wer die App oeffnet, will nicht ueberfallen werden; ein Druck auf Play macht dort weiter,
|
||||
* wo es aufgehoert hat. Laeuft schon etwas (etwa ueber Android Auto gestartet), passiert nichts.
|
||||
* Wer die App oeffnet, will nicht ueberfallen werden; ein Druck auf Play macht dort weiter.
|
||||
* Laeuft schon etwas (etwa ueber Android Auto gestartet), passiert nichts.
|
||||
*
|
||||
* Die Aufloesung der Song-Ids braucht die Navidrome-Zugangsdaten. connect() laeuft aber schon in
|
||||
* Application.onCreate(), wo der DataStore unter Umstaenden noch nicht gelesen ist — dann
|
||||
* scheitern alle Abfragen und die Warteschlange bliebe still leer. Deshalb wird es mehrfach mit
|
||||
* wachsendem Abstand versucht, statt beim ersten Fehlschlag aufzugeben.
|
||||
*/
|
||||
fun restoreQueue() {
|
||||
val ctrl = controller ?: return
|
||||
if (ctrl.mediaItemCount > 0) return
|
||||
scope.launch {
|
||||
val zustand = runCatching { repo.readQueueState() }.getOrNull() ?: return@launch
|
||||
// Songs einzeln nachladen; verschwundene (geloescht, umbenannt) werden uebersprungen.
|
||||
val songs = zustand.ids.mapNotNull { runCatching { repo.getSong(it) }.getOrNull() }
|
||||
if (songs.isEmpty()) return@launch
|
||||
val index = zustand.index.coerceIn(0, songs.lastIndex)
|
||||
queue = songs
|
||||
ctrl.setMediaItems(songs.map { it.toMediaItem(repo) }, index, 0L)
|
||||
ctrl.prepare()
|
||||
syncState()
|
||||
val zustand = runCatching { repo.readQueueState() }.getOrNull()
|
||||
if (zustand == null) {
|
||||
android.util.Log.i(TAG_QUEUE, "nichts gesichert — nichts wiederherzustellen")
|
||||
return@launch
|
||||
}
|
||||
android.util.Log.i(TAG_QUEUE, "gesichert gefunden: ${zustand.ids.size} Ids, Index ${zustand.index}")
|
||||
|
||||
repeat(5) { versuch ->
|
||||
val ctrl = controller
|
||||
if (ctrl == null) {
|
||||
delay(500L * (versuch + 1)); return@repeat
|
||||
}
|
||||
if (ctrl.mediaItemCount > 0) {
|
||||
android.util.Log.i(TAG_QUEUE, "Player hat bereits Titel — nicht angefasst")
|
||||
return@launch
|
||||
}
|
||||
val songs = zustand.ids.mapNotNull { id ->
|
||||
runCatching { repo.getSong(id) }
|
||||
.onFailure { android.util.Log.w(TAG_QUEUE, "Id $id nicht aufloesbar: ${it.message}") }
|
||||
.getOrNull()
|
||||
}
|
||||
if (songs.isNotEmpty()) {
|
||||
val index = zustand.index.coerceIn(0, songs.lastIndex)
|
||||
queue = songs
|
||||
originalQueue = songs
|
||||
runCatching {
|
||||
ctrl.setMediaItems(songs.map { it.toMediaItem(repo) }, index, 0L)
|
||||
ctrl.prepare()
|
||||
}.onFailure { android.util.Log.w(TAG_QUEUE, "setMediaItems fehlgeschlagen", it) }
|
||||
syncState()
|
||||
android.util.Log.i(TAG_QUEUE, "wiederhergestellt: ${songs.size} Titel, Index $index")
|
||||
return@launch
|
||||
}
|
||||
android.util.Log.i(TAG_QUEUE, "Versuch ${versuch + 1}: kein Titel aufloesbar, warte…")
|
||||
delay(800L * (versuch + 1))
|
||||
}
|
||||
android.util.Log.w(TAG_QUEUE, "aufgegeben — keine der ${zustand.ids.size} Ids war aufloesbar")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user