diff --git a/android/app/src/main/java/com/fabledsword/minstrel/player/PlayerController.kt b/android/app/src/main/java/com/fabledsword/minstrel/player/PlayerController.kt index 102bcba1..0604cd03 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/player/PlayerController.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/player/PlayerController.kt @@ -3,6 +3,8 @@ package com.fabledsword.minstrel.player import android.content.ComponentName import android.content.Context import android.os.Bundle +import android.os.Handler +import android.os.Looper import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata import androidx.media3.common.Player @@ -187,9 +189,26 @@ class PlayerController @Inject constructor( val controller = mediaController ?: return queueRefs = tracks val items = tracks.map { it.toMediaItem(source) } - controller.setMediaItems(items, initialIndex, /* startPositionMs = */ 0L) - controller.prepare() - if (autoplay) controller.play() + // Drift #562 cold-boot resume calls this from a non-Main suspend + // context after awaitReady() unblocks (ResumeController launches + // on Dispatchers.Default by the time it reaches us). MediaController + // enforces application-thread access and throws + // IllegalStateException otherwise — post to its applicationLooper + // if we're already there, run directly to avoid the re-dispatch + // latency UI callers depend on. + runOnControllerThread(controller) { + controller.setMediaItems(items, initialIndex, /* startPositionMs = */ 0L) + controller.prepare() + if (autoplay) controller.play() + } + } + + private fun runOnControllerThread(controller: MediaController, block: () -> Unit) { + if (Looper.myLooper() == controller.applicationLooper) { + block() + } else { + Handler(controller.applicationLooper).post(block) + } } /**