diff --git a/android/app/src/main/java/com/fabledsword/minstrel/player/RemotePlayerState.kt b/android/app/src/main/java/com/fabledsword/minstrel/player/RemotePlayerState.kt index 0ffde318..bbaab6db 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/player/RemotePlayerState.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/player/RemotePlayerState.kt @@ -27,6 +27,13 @@ class RemotePlayerState @Inject constructor() { @Volatile var lastError: Throwable? = null; private set @Volatile var trackNumber: Int = 0; private set + // The operator's last explicit transport intent (play=true, pause=false), + // distinct from the observed remote isPlaying. handleRemoteDrop resumes + // local playback based on THIS, not isPlaying -- so a play() that fails + // its SOAP (stale/dead renderer) still resumes on the phone instead of + // swallowing the tap. Deliberately NOT cleared by applyError. + @Volatile var lastPlayIntent: Boolean = false; private set + // Pending-transport deadline (SystemClock.elapsedRealtime() at which we // give up waiting). When > 0, a user transport action (next/prev/seekTo // idx) is in flight: ForwardingPlayer has already moved the wrapped @@ -63,6 +70,8 @@ class RemotePlayerState @Inject constructor() { positionMs = 0L } + fun setPlayIntent(intended: Boolean) { lastPlayIntent = intended } + fun applyError(t: Throwable) { isPlaying = false lastError = t @@ -80,6 +89,7 @@ class RemotePlayerState @Inject constructor() { positionMs = 0L durationMs = 0L isPlaying = false + lastPlayIntent = false currentTrackUri = "" lastError = null consecutivePollFailures = 0 diff --git a/android/app/src/test/java/com/fabledsword/minstrel/player/RemotePlayerStateTest.kt b/android/app/src/test/java/com/fabledsword/minstrel/player/RemotePlayerStateTest.kt index bb0c0224..b597187a 100644 --- a/android/app/src/test/java/com/fabledsword/minstrel/player/RemotePlayerStateTest.kt +++ b/android/app/src/test/java/com/fabledsword/minstrel/player/RemotePlayerStateTest.kt @@ -69,6 +69,32 @@ class RemotePlayerStateTest { assertFalse(state.recordPollFailure()) } + @Test + fun `play intent survives a SOAP error so a failed play can resume locally`() { + val state = RemotePlayerState() + state.setPlayIntent(true) + state.applyError(RuntimeException("connect timeout")) + // isPlaying is cleared by the error, but the operator's intent persists. + assertFalse(state.isPlaying) + assertTrue(state.lastPlayIntent) + } + + @Test + fun `pausing clears play intent`() { + val state = RemotePlayerState() + state.setPlayIntent(true) + state.setPlayIntent(false) + assertFalse(state.lastPlayIntent) + } + + @Test + fun `reset clears play intent`() { + val state = RemotePlayerState() + state.setPlayIntent(true) + state.reset() + assertFalse(state.lastPlayIntent) + } + private companion object { const val DROP_THRESHOLD = 30 }