diff --git a/docs/playback-functional-requirements.md b/docs/playback-functional-requirements.md index d01456a2..1afdb08d 100644 --- a/docs/playback-functional-requirements.md +++ b/docs/playback-functional-requirements.md @@ -29,6 +29,9 @@ This is a list of requirements that the internal playback is built around. play order and starts at its first item while shuffle is enabled. - Adding "next" during shuffle puts all inserted items immediately after the current item in their supplied order; Media3's randomly assigned insertion positions must not leak into the UI order. + Implementation: `QueueAwarePlayer.addMediaItems` detects `shuffleModeEnabled && index == + currentMediaItemIndex + 1` and rewrites the shuffle order via `ShuffleOrderCoordinator.insertNext` + to splice the new items at `currentShufflePos + 1`. - Reordering a shuffled queue changes play order without accidentally treating a play-order row as a canonical Media3 index. - Queue, current item/index/position, repeat mode, shuffle flag, and exact shuffle order survive diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/service/PlaybackQueueActionsTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/service/PlaybackQueueActionsTest.kt index 1287406f..305c96f4 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/service/PlaybackQueueActionsTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/service/PlaybackQueueActionsTest.kt @@ -1,6 +1,7 @@ package org.moire.ultrasonic.service import androidx.media3.common.C +import androidx.media3.common.MediaItem import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.Test @@ -103,10 +104,52 @@ class PlaybackQueueActionsTest { verify(playback, never()).play() } - private fun playback(shuffleEnabled: Boolean): PlaybackRepository { + @Test + fun `play next without shuffle inserts immediately after current item`() = runTest { + val playback = playback(shuffleEnabled = false, currentIndex = 2, queueSize = 5) + + PlaybackQueueActions(playback).add( + tracks = emptyList(), + insertionMode = QueueInsertionMode.AFTER_CURRENT, + autoPlay = false + ) + + // insertAt = currentMediaItemIndex + 1 = 3 + verify(playback).addMediaItems(3, emptyList()) + } + + @Test + fun `play next with shuffle inserts at correct index for QueueAwarePlayer to reorder`() = + runTest { + val playback = playback(shuffleEnabled = true, currentIndex = 1, queueSize = 4) + + PlaybackQueueActions(playback).add( + tracks = emptyList(), + insertionMode = QueueInsertionMode.AFTER_CURRENT, + autoPlay = false + ) + + // insertAt = currentMediaItemIndex + 1 = 2; QueueAwarePlayer handles shuffle order + verify(playback).addMediaItems(2, emptyList()) + } + + private fun playback( + shuffleEnabled: Boolean, + currentIndex: Int = C.INDEX_UNSET, + queueSize: Int = 0 + ): PlaybackRepository { val playback = mock() + val entries = (0 until queueSize).map { i -> + PlaybackQueueEntry(mediaItemIndex = i, mediaItem = MediaItem.EMPTY) + } whenever(playback.snapshot).thenReturn( - MutableStateFlow(PlaybackSnapshot(shuffleModeEnabled = shuffleEnabled)) + MutableStateFlow( + PlaybackSnapshot( + shuffleModeEnabled = shuffleEnabled, + currentMediaItemIndex = currentIndex, + queueInPlayOrder = entries + ) + ) ) return playback }