diff --git a/RELEASENOTES.md b/RELEASENOTES.md index cc4fc72c3e9..a882485c0bd 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -185,6 +185,9 @@ `MediaSession.Callback.onSetMediaUri`. The same functionality can be achieved by using `MediaController.setMediaItem` and `MediaSession.Callback.onAddMediaItems`. + * Fix `IndexOutOfBoundsException` when setting less media items than in + the current playlist + ([#86](https://github.com/androidx/media/issues/86)). * Data sources: * Rename `DummyDataSource` to `PlaceholderDataSource`. * Workaround OkHttp interrupt handling. diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java index c80e832a97f..d9d804c1e65 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java @@ -828,7 +828,7 @@ public void setMediaItem(MediaItem mediaItem) { Collections.singletonList(mediaItem), /* startIndex= */ C.INDEX_UNSET, /* startPositionMs= */ C.TIME_UNSET, - /* resetToDefaultPosition= */ false); + /* resetToDefaultPosition= */ true); } @Override @@ -887,7 +887,7 @@ public void setMediaItems(List mediaItems) { mediaItems, /* startIndex= */ C.INDEX_UNSET, /* startPositionMs= */ C.TIME_UNSET, - /* resetToDefaultPosition= */ false); + /* resetToDefaultPosition= */ true); } @Override @@ -1832,12 +1832,18 @@ private void setMediaItemsInternal( throw new IllegalSeekPositionException(newTimeline, startIndex, startPositionMs); } + boolean correctedStartIndex = false; if (resetToDefaultPosition) { startIndex = newTimeline.getFirstWindowIndex(playerInfo.shuffleModeEnabled); startPositionMs = C.TIME_UNSET; } else if (startIndex == C.INDEX_UNSET) { startIndex = playerInfo.sessionPositionInfo.positionInfo.mediaItemIndex; startPositionMs = playerInfo.sessionPositionInfo.positionInfo.positionMs; + if (startIndex >= newTimeline.getWindowCount()) { + correctedStartIndex = true; + startIndex = newTimeline.getFirstWindowIndex(playerInfo.shuffleModeEnabled); + startPositionMs = C.TIME_UNSET; + } } PositionInfo newPositionInfo; SessionPositionInfo newSessionPositionInfo; @@ -1905,7 +1911,7 @@ private void setMediaItemsInternal( // Mask the playback state. int maskingPlaybackState = newPlayerInfo.playbackState; if (startIndex != C.INDEX_UNSET && newPlayerInfo.playbackState != STATE_IDLE) { - if (newTimeline.isEmpty() || startIndex >= newTimeline.getWindowCount()) { + if (newTimeline.isEmpty() || correctedStartIndex) { // Setting an empty timeline or invalid seek transitions to ended. maskingPlaybackState = STATE_ENDED; } else { diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java index 80b893e9d35..63ea9ad1c9b 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java @@ -36,6 +36,7 @@ import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; import androidx.media3.common.HeartRating; +import androidx.media3.common.IllegalSeekPositionException; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaLibraryInfo; import androidx.media3.common.MediaMetadata; @@ -56,6 +57,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; +import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -64,6 +66,7 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.ClassRule; import org.junit.Rule; @@ -1055,4 +1058,95 @@ public void getMediaMetadata() throws Exception { assertThat(mediaMetadata).isEqualTo(testMediaMetadata); } + + @Test + public void + setMediaItems_setLessMediaItemsThanCurrentMediaItemIndex_masksCurrentMediaItemIndexAndStateCorrectly() + throws Exception { + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + List threeItemsList = + ImmutableList.of( + MediaItem.fromUri("http://www.google.com/1"), + MediaItem.fromUri("http://www.google.com/2"), + MediaItem.fromUri("http://www.google.com/3")); + List twoItemsList = + ImmutableList.of( + MediaItem.fromUri("http://www.google.com/1"), + MediaItem.fromUri("http://www.google.com/2")); + + int[] currentMediaIndexAndState = + threadTestRule + .getHandler() + .postAndSync( + () -> { + controller.setMediaItems(threeItemsList); + controller.prepare(); + controller.seekTo(/* mediaItemIndex= */ 2, /* positionMs= */ C.TIME_UNSET); + controller.setMediaItems(twoItemsList); + return new int[] { + controller.getCurrentMediaItemIndex(), controller.getPlaybackState() + }; + }); + + assertThat(currentMediaIndexAndState[0]).isEqualTo(0); + assertThat(currentMediaIndexAndState[1]).isEqualTo(Player.STATE_BUFFERING); + } + + @Test + public void + setMediaItems_setLessMediaItemsThanCurrentMediaItemIndexResetPositionFalse_masksCurrentMediaItemIndexAndStateCorrectly() + throws Exception { + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + List threeItemsList = + ImmutableList.of( + MediaItem.fromUri("http://www.google.com/1"), + MediaItem.fromUri("http://www.google.com/2"), + MediaItem.fromUri("http://www.google.com/3")); + List twoItemsList = + ImmutableList.of( + MediaItem.fromUri("http://www.google.com/1"), + MediaItem.fromUri("http://www.google.com/2")); + + int[] currentMediaItemIndexAndState = + threadTestRule + .getHandler() + .postAndSync( + () -> { + controller.setMediaItems(threeItemsList); + controller.prepare(); + controller.seekTo(/* mediaItemIndex= */ 2, /* positionMs= */ C.TIME_UNSET); + controller.setMediaItems(twoItemsList, /* resetPosition= */ false); + return new int[] { + controller.getCurrentMediaItemIndex(), controller.getPlaybackState() + }; + }); + + assertThat(currentMediaItemIndexAndState[0]).isEqualTo(0); + assertThat(currentMediaItemIndexAndState[1]).isEqualTo(Player.STATE_ENDED); + } + + @Test + public void setMediaItems_startIndexTooLarge_throwIllegalSeekPositionException() + throws Exception { + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + List threeItemsList = + ImmutableList.of( + MediaItem.fromUri("http://www.google.com/1"), + MediaItem.fromUri("http://www.google.com/2"), + MediaItem.fromUri("http://www.google.com/3")); + + Assert.assertThrows( + IllegalSeekPositionException.class, + () -> + threadTestRule + .getHandler() + .postAndSync( + () -> { + controller.setMediaItems( + threeItemsList, + /* startIndex= */ 99, + /* startPositionMs= */ C.TIME_UNSET); + return controller.getCurrentMediaItemIndex(); + })); + } }