fix(android): PlayerController.setQueue dispatches to controller thread
android / Build + lint + test (push) Successful in 4m15s
android / Build + lint + test (push) Successful in 4m15s
Crash on cold boot: ResumeController.restore is suspend, lands on Dispatchers.Default after awaitReady() unblocks (drift #562), and calls PlayerController.setQueue which calls MediaController.setMediaItems — MediaController enforces application-thread access and throws IllegalStateException 'method is called from a wrong thread'. Drift #562 added awaitReady() to fix the race where setQueue early-returned on null controller and silently dropped the persisted queue. That fix exposed the next bug down the stack: the threading violation that was previously masked by the early-return. setQueue now posts the MediaController calls to the controller's applicationLooper if we're not already on it. UI callers (already Main) run inline with no re-dispatch latency. ResumeController's cold-boot path lands on the right thread. Discovered on-device 2026-06-03 during like-button verification on the Pixel 6 Pro emulator — crash log at PlayerController.kt:190.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user