From c245b1ef0b9fc1790c54552068322d8461b7cb02 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 4 Jun 2026 00:37:34 -0400 Subject: [PATCH] fix(android): hold UI patch until cursor sync lands; reanchor on track flip --- .../minstrel/player/PlayerController.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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 e0760987..3f7ae5a8 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 @@ -411,6 +411,11 @@ class PlayerController @Inject constructor( // Reset the per-item evaluation guard so the new // item's STATE_READY transition gets a fresh check. lastEvaluatedItemIndex = -1 + // The track flipped -- re-anchor the position interpolator + // so the next polling tick treats the new track's + // remoteState.positionMs as fresh rather than carrying the + // old anchor + elapsed forward. + lastSeenRemotePositionMs = -1L } override fun onPlaybackStateChanged(playbackState: Int) { @@ -493,6 +498,14 @@ class PlayerController @Inject constructor( while (isActive) { delay(POSITION_POLL_INTERVAL_MS) val upnpActive = activeUpnpHolder.active.value != null + // Track-alignment gate: if Sonos's reported Track is past the + // wrapped player's currentMediaItemIndex, a forward cursor sync + // is in flight (pollOnce wrote new remoteState, handler.post'd + // delegate.seekTo; the seek hasn't run yet). Skipping the tick + // here lets onMediaItemTransition rebuild the full uiState + // atomically rather than patching position+duration while + // currentTrack/queueIndex still point at the old track. + if (upnpActive && isForwardCursorSyncPending(controller)) continue val effectiveIsPlaying = if (upnpActive) remoteState.isPlaying else controller.isPlaying val effectivePosition = if (upnpActive) { @@ -533,6 +546,12 @@ class PlayerController @Inject constructor( @Volatile private var positionAnchorMs: Long = 0L @Volatile private var positionAnchorAtRealtimeMs: Long = 0L + private fun isForwardCursorSyncPending(controller: MediaController): Boolean { + val sonosTrack = remoteState.trackNumber + if (sonosTrack <= 0) return false + return (sonosTrack - 1) > controller.currentMediaItemIndex + } + private fun interpolatedRemotePosition(isPlaying: Boolean): Long { val raw = remoteState.positionMs val now = SystemClock.elapsedRealtime()