diff --git a/android/app/src/main/java/com/fabledsword/minstrel/player/ResumeController.kt b/android/app/src/main/java/com/fabledsword/minstrel/player/ResumeController.kt index 18940a02..f4d48703 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/player/ResumeController.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/player/ResumeController.kt @@ -8,7 +8,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json import timber.log.Timber import javax.inject.Inject @@ -41,21 +40,25 @@ class ResumeController @Inject constructor( } suspend fun restore() { + // runCatching folds row-missing + decode-failure + empty-tracks into + // a single null-or-go-ahead expression so detekt's ReturnCount rule + // is satisfied. The .onFailure side effect drops a corrupted row + // rather than leaving a poison pill across launches. val row = dao.get() ?: return val payload = - try { - json.decodeFromString(row.json) - } catch (e: SerializationException) { - // Schema drift or corruption — drop the row and start clean - // rather than crashing or leaving a poison pill. - Timber.w(e, "ResumeController: dropping unreadable resume state") - dao.clear() - return - } - if (payload.tracks.isEmpty()) return + runCatching { json.decodeFromString(row.json) } + .onFailure { + Timber.w(it, "ResumeController: dropping unreadable resume state") + dao.clear() + } + .getOrNull() + ?.takeIf { it.tracks.isNotEmpty() } + ?: return playerController.setQueue( tracks = payload.tracks, - initialIndex = payload.queueIndex.coerceAtLeast(0).coerceAtMost(payload.tracks.lastIndex), + initialIndex = payload.queueIndex + .coerceAtLeast(0) + .coerceAtMost(payload.tracks.lastIndex), source = payload.source, ) // PositionMs not seeked yet — Player connection may still be