Implement Player.replaceMediaItem(s)

This change moves the default logic into the actual Player
implementations, but does not introduce any behavior changes compared
to addMediaItems+removeMediaItems except to make the updates "atomic"
in ExoPlayerImpl, SimpleBasePlayer and MediaController. It also
provides backwards compatbility for cases where Players don't support
the operation.

Issue: google/ExoPlayer#8046

#minor-release

PiperOrigin-RevId: 534945089
This commit is contained in:
tonihei 2023-05-24 20:41:47 +01:00
parent cf0334d793
commit 2c07468908
26 changed files with 1974 additions and 58 deletions

View File

@ -751,8 +751,8 @@ package androidx.media3.common {
method public void removeListener(androidx.media3.common.Player.Listener);
method public void removeMediaItem(int);
method public void removeMediaItems(int, int);
method public default void replaceMediaItem(int, androidx.media3.common.MediaItem);
method public default void replaceMediaItems(int, int, java.util.List<androidx.media3.common.MediaItem>);
method public void replaceMediaItem(int, androidx.media3.common.MediaItem);
method public void replaceMediaItems(int, int, java.util.List<androidx.media3.common.MediaItem>);
method public void seekBack();
method public void seekForward();
method public void seekTo(long);
@ -1607,6 +1607,8 @@ package androidx.media3.session {
method public final void removeListener(androidx.media3.common.Player.Listener);
method public final void removeMediaItem(int);
method public final void removeMediaItems(int, int);
method public final void replaceMediaItem(int, androidx.media3.common.MediaItem);
method public final void replaceMediaItems(int, int, java.util.List<androidx.media3.common.MediaItem>);
method public final void seekBack();
method public final void seekForward();
method public final void seekTo(long);

View File

@ -321,6 +321,18 @@ public final class CastPlayer extends BasePlayer {
moveMediaItemsInternal(uids, fromIndex, newIndex);
}
@Override
public void replaceMediaItems(int fromIndex, int toIndex, List<MediaItem> mediaItems) {
checkArgument(fromIndex >= 0 && fromIndex <= toIndex);
int playlistSize = currentTimeline.getWindowCount();
if (fromIndex > playlistSize) {
return;
}
toIndex = min(toIndex, playlistSize);
addMediaItems(toIndex, mediaItems);
removeMediaItems(fromIndex, toIndex);
}
@Override
public void removeMediaItems(int fromIndex, int toIndex) {
checkArgument(fromIndex >= 0 && toIndex >= fromIndex);

View File

@ -699,6 +699,38 @@ public class CastPlayerTest {
.queueRemoveItems(new int[] {1, 2, 3, 4, 5}, /* customData= */ null);
}
@Test
public void replaceMediaItems_callsRemoteMediaClient() {
int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 2);
List<MediaItem> mediaItems = createMediaItems(mediaQueueItemIds);
// Add two items.
addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds);
String uri = "http://www.google.com/video3";
MediaItem anotherMediaItem =
new MediaItem.Builder().setUri(uri).setMimeType(MimeTypes.APPLICATION_MPD).build();
ImmutableList<MediaItem> newPlaylist = ImmutableList.of(mediaItems.get(0), anotherMediaItem);
// Replace item at position 1.
castPlayer.replaceMediaItems(
/* fromIndex= */ 1, /* toIndex= */ 2, ImmutableList.of(anotherMediaItem));
updateTimeLine(
newPlaylist,
/* mediaQueueItemIds= */ new int[] {mediaQueueItemIds[0], 123},
/* currentItemId= */ 123);
verify(mockRemoteMediaClient, times(2))
.queueInsertItems(queueItemsArgumentCaptor.capture(), anyInt(), any());
verify(mockRemoteMediaClient).queueRemoveItems(new int[] {2}, /* customData= */ null);
assertThat(queueItemsArgumentCaptor.getAllValues().get(1)[0])
.isEqualTo(mediaItemConverter.toMediaQueueItem(anotherMediaItem));
Timeline.Window currentWindow =
castPlayer
.getCurrentTimeline()
.getWindow(castPlayer.getCurrentMediaItemIndex(), new Timeline.Window());
assertThat(currentWindow.uid).isEqualTo(123);
assertThat(currentWindow.mediaItem).isEqualTo(anotherMediaItem);
}
@SuppressWarnings("ConstantConditions")
@Test
public void addMediaItems_fillsTimeline() {

View File

@ -78,6 +78,12 @@ public abstract class BasePlayer implements Player {
}
}
@Override
public final void replaceMediaItem(int index, MediaItem mediaItem) {
replaceMediaItems(
/* fromIndex= */ index, /* toIndex= */ index + 1, ImmutableList.of(mediaItem));
}
@Override
public final void removeMediaItem(int index) {
removeMediaItems(/* fromIndex= */ index, /* toIndex= */ index + 1);

View File

@ -37,7 +37,6 @@ import androidx.media3.common.util.Size;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
@ -2158,10 +2157,7 @@ public interface Player {
* of the playlist, the request is ignored.
* @param mediaItem The new {@link MediaItem}.
*/
default void replaceMediaItem(int index, MediaItem mediaItem) {
replaceMediaItems(
/* fromIndex= */ index, /* toIndex= */ index + 1, ImmutableList.of(mediaItem));
}
void replaceMediaItem(int index, MediaItem mediaItem);
/**
* Replaces the media items at the given range of the playlist.
@ -2180,10 +2176,7 @@ public interface Player {
* larger than the size of the playlist, items up to the end of the playlist are replaced.
* @param mediaItems The {@linkplain MediaItem media items} to replace the range with.
*/
default void replaceMediaItems(int fromIndex, int toIndex, List<MediaItem> mediaItems) {
addMediaItems(toIndex, mediaItems);
removeMediaItems(fromIndex, toIndex);
}
void replaceMediaItems(int fromIndex, int toIndex, List<MediaItem> mediaItems);
/**
* Removes the media item at the given index of the playlist.

View File

@ -2141,16 +2141,43 @@ public abstract class SimpleBasePlayer extends BasePlayer {
});
}
@Override
public final void replaceMediaItem(int index, MediaItem mediaItem) {
replaceMediaItems(
/* fromIndex= */ index, /* toIndex= */ index + 1, ImmutableList.of(mediaItem));
}
@Override
public final void replaceMediaItems(int fromIndex, int toIndex, List<MediaItem> mediaItems) {
addMediaItems(toIndex, mediaItems);
removeMediaItems(fromIndex, toIndex);
verifyApplicationThreadAndInitState();
checkArgument(fromIndex >= 0 && fromIndex <= toIndex);
State state = this.state;
int playlistSize = state.playlist.size();
if (!shouldHandleCommand(Player.COMMAND_CHANGE_MEDIA_ITEMS) || fromIndex > playlistSize) {
return;
}
int correctedToIndex = min(toIndex, playlistSize);
updateStateForPendingOperation(
/* pendingOperation= */ handleReplaceMediaItems(fromIndex, correctedToIndex, mediaItems),
/* placeholderStateSupplier= */ () -> {
ArrayList<MediaItemData> placeholderPlaylist = new ArrayList<>(state.playlist);
for (int i = 0; i < mediaItems.size(); i++) {
placeholderPlaylist.add(
i + correctedToIndex, getPlaceholderMediaItemData(mediaItems.get(i)));
}
State updatedState;
if (!state.playlist.isEmpty()) {
updatedState = getStateWithNewPlaylist(state, placeholderPlaylist, period);
} else {
// Handle initial position update when these are the first items added to the playlist.
updatedState =
getStateWithNewPlaylistAndPosition(
state,
placeholderPlaylist,
state.currentMediaItemIndex,
state.contentPositionMsSupplier.get());
}
if (fromIndex < correctedToIndex) {
Util.removeRange(placeholderPlaylist, fromIndex, correctedToIndex);
return getStateWithNewPlaylist(updatedState, placeholderPlaylist, period);
} else {
return updatedState;
}
});
}
@Override
@ -3182,6 +3209,27 @@ public abstract class SimpleBasePlayer extends BasePlayer {
throw new IllegalStateException("Missing implementation to handle COMMAND_CHANGE_MEDIA_ITEMS");
}
/**
* Handles calls to {@link Player#replaceMediaItem} and {@link Player#replaceMediaItems}.
*
* <p>Will only be called if {@link Player#COMMAND_CHANGE_MEDIA_ITEMS} is available.
*
* @param fromIndex The start index of the items to replace. The index is in the range 0 &lt;=
* {@code fromIndex} &lt; {@link #getMediaItemCount()}.
* @param toIndex The index of the first item not to be replaced (exclusive). The index is in the
* range {@code fromIndex} &lt; {@code toIndex} &lt;= {@link #getMediaItemCount()}.
* @param mediaItems The media items to replace the specified range with.
* @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
* changes caused by this call.
*/
@ForOverride
protected ListenableFuture<?> handleReplaceMediaItems(
int fromIndex, int toIndex, List<MediaItem> mediaItems) {
ListenableFuture<?> addFuture = handleAddMediaItems(toIndex, mediaItems);
ListenableFuture<?> removeFuture = handleRemoveMediaItems(fromIndex, toIndex);
return Util.transformFutureAsync(addFuture, unused -> removeFuture);
}
/**
* Handles calls to {@link Player#removeMediaItem} and {@link Player#removeMediaItems}.
*

View File

@ -6868,6 +6868,559 @@ public class SimpleBasePlayerTest {
assertThat(callForwarded.get()).isFalse();
}
@Test
public void replaceMediaItems_immediateHandling_updatesStateAndInformsListeners() {
State state =
new State.Builder()
.setAvailableCommands(new Commands.Builder().addAllCommands().build())
.setPlaylist(
ImmutableList.of(
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(),
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build()))
.build();
State updatedState =
state
.buildUpon()
.setPlaylist(
ImmutableList.of(
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(),
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(),
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 4).build(),
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build()))
.build();
SimpleBasePlayer player =
new SimpleBasePlayer(Looper.myLooper()) {
private State playerState = state;
@Override
protected State getState() {
return playerState;
}
@Override
protected ListenableFuture<?> handleReplaceMediaItems(
int fromIndex, int toIndex, List<MediaItem> mediaItems) {
playerState = updatedState;
return Futures.immediateVoidFuture();
}
};
Listener listener = mock(Listener.class);
player.addListener(listener);
player.replaceMediaItems(
/* fromIndex= */ 1,
/* toIndex= */ 2,
ImmutableList.of(
new MediaItem.Builder().setMediaId("3").build(),
new MediaItem.Builder().setMediaId("4").build(),
new MediaItem.Builder().setMediaId("2").build()));
assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline);
verify(listener)
.onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
verifyNoMoreInteractions(listener);
}
@Test
public void
replaceMediaItems_asyncHandlingNotReplacingCurrentItem_usesPlaceholderStateAndInformsListeners() {
State state =
new State.Builder()
.setAvailableCommands(new Commands.Builder().addAllCommands().build())
.setPlaylist(
ImmutableList.of(
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(),
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build(),
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build()))
.setCurrentMediaItemIndex(2)
.setPlaybackState(Player.STATE_READY)
.build();
State updatedState =
state
.buildUpon()
.setPlaylist(
ImmutableList.of(
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(),
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 4).build(),
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 5).build(),
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build()))
.setCurrentMediaItemIndex(3)
.build();
SettableFuture<?> future = SettableFuture.create();
SimpleBasePlayer player =
new SimpleBasePlayer(Looper.myLooper()) {
@Override
protected State getState() {
return future.isDone() ? updatedState : state;
}
@Override
protected ListenableFuture<?> handleReplaceMediaItems(
int fromIndex, int toIndex, List<MediaItem> mediaItems) {
return future;
}
};
Listener listener = mock(Listener.class);
player.addListener(listener);
player.replaceMediaItems(
/* fromIndex= */ 1,
/* toIndex= */ 2,
ImmutableList.of(
new MediaItem.Builder().setMediaId("4").build(),
new MediaItem.Builder().setMediaId("5").build()));
// Verify placeholder state and listener calls.
assertThat(player.getCurrentMediaItemIndex()).isEqualTo(3);
assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(4);
Timeline.Window window = new Timeline.Window();
player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window);
assertThat(window.uid).isEqualTo(1);
assertThat(window.isPlaceholder).isFalse();
player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window);
assertThat(window.mediaItem.mediaId).isEqualTo("4");
assertThat(window.isPlaceholder).isTrue();
player.getCurrentTimeline().getWindow(/* windowIndex= */ 2, window);
assertThat(window.mediaItem.mediaId).isEqualTo("5");
assertThat(window.isPlaceholder).isTrue();
player.getCurrentTimeline().getWindow(/* windowIndex= */ 3, window);
assertThat(window.uid).isEqualTo(3);
assertThat(window.isPlaceholder).isFalse();
verify(listener)
.onTimelineChanged(
player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
verifyNoMoreInteractions(listener);
future.set(null);
// Verify actual state update.
assertThat(player.getCurrentMediaItemIndex()).isEqualTo(3);
assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline);
verify(listener)
.onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
verifyNoMoreInteractions(listener);
}
@SuppressWarnings("deprecation") // Testing deprecated listener call.
@Test
public void
replaceMediaItem_asyncHandlingReplacingCurrentItem_usesPlaceholderStateAndInformsListeners() {
State state =
new State.Builder()
.setAvailableCommands(new Commands.Builder().addAllCommands().build())
.setPlaylist(
ImmutableList.of(
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(),
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build(),
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build()))
.setCurrentMediaItemIndex(1)
.setPlaybackState(Player.STATE_READY)
.build();
State updatedState =
state
.buildUpon()
.setPlaylist(
ImmutableList.of(
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(),
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 4).build(),
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build()))
.setCurrentMediaItemIndex(2)
.build();
SettableFuture<?> future = SettableFuture.create();
SimpleBasePlayer player =
new SimpleBasePlayer(Looper.myLooper()) {
@Override
protected State getState() {
return future.isDone() ? updatedState : state;
}
@Override
protected ListenableFuture<?> handleReplaceMediaItems(
int fromIndex, int toIndex, List<MediaItem> mediaItems) {
return future;
}
};
Listener listener = mock(Listener.class);
player.addListener(listener);
player.replaceMediaItem(/* index= */ 1, new MediaItem.Builder().setMediaId("4").build());
// Verify placeholder state and listener calls.
assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1);
assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(3);
Timeline.Window window = new Timeline.Window();
player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window);
assertThat(window.uid).isEqualTo(1);
assertThat(window.isPlaceholder).isFalse();
player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window);
assertThat(window.mediaItem.mediaId).isEqualTo("4");
assertThat(window.isPlaceholder).isTrue();
player.getCurrentTimeline().getWindow(/* windowIndex= */ 2, window);
assertThat(window.uid).isEqualTo(3);
assertThat(window.isPlaceholder).isFalse();
verify(listener)
.onTimelineChanged(
player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE));
verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_REMOVE);
verify(listener)
.onMediaItemTransition(
new MediaItem.Builder().setMediaId("4").build(),
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED);
verifyNoMoreInteractions(listener);
future.set(null);
// Verify actual state update.
assertThat(player.getCurrentMediaItemIndex()).isEqualTo(2);
assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline);
verify(listener)
.onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
verifyNoMoreInteractions(listener);
}
@SuppressWarnings("deprecation") // Testing deprecated listener call.
@Test
public void
replaceMediaItems_asyncHandlingReplacingCurrentItemWithEmptyListAndSubsequentItem_usesPlaceholderStateAndInformsListeners() {
MediaItem testMediaItem = new MediaItem.Builder().setMediaId("3").build();
State state =
new State.Builder()
.setAvailableCommands(new Commands.Builder().addAllCommands().build())
.setPlaylist(
ImmutableList.of(
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(),
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build(),
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3)
.setMediaItem(testMediaItem)
.build()))
.setCurrentMediaItemIndex(1)
.setPlaybackState(Player.STATE_READY)
.build();
State updatedState =
state
.buildUpon()
.setPlaylist(
ImmutableList.of(
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(),
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build()))
.setCurrentMediaItemIndex(1)
.build();
SettableFuture<?> future = SettableFuture.create();
SimpleBasePlayer player =
new SimpleBasePlayer(Looper.myLooper()) {
@Override
protected State getState() {
return future.isDone() ? updatedState : state;
}
@Override
protected ListenableFuture<?> handleReplaceMediaItems(
int fromIndex, int toIndex, List<MediaItem> mediaItems) {
return future;
}
};
Listener listener = mock(Listener.class);
player.addListener(listener);
player.replaceMediaItems(/* fromIndex= */ 1, /* toIndex= */ 2, ImmutableList.of());
// Verify placeholder state and listener calls.
assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1);
assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2);
Timeline.Window window = new Timeline.Window();
player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window);
assertThat(window.uid).isEqualTo(1);
assertThat(window.isPlaceholder).isFalse();
player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window);
assertThat(window.uid).isEqualTo(3);
assertThat(window.isPlaceholder).isFalse();
verify(listener)
.onTimelineChanged(
player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE));
verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_REMOVE);
verify(listener)
.onMediaItemTransition(testMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED);
verifyNoMoreInteractions(listener);
future.set(null);
// Verify actual state update.
assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1);
assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline);
verify(listener)
.onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
verifyNoMoreInteractions(listener);
}
@SuppressWarnings("deprecation") // Testing deprecated listener call.
@Test
public void
replaceMediaItems_asyncHandlingReplacingCurrentItemWithEmptyListAndNoSubsequentItem_usesPlaceholderStateAndInformsListeners() {
MediaItem testMediaItem = new MediaItem.Builder().setMediaId("1").build();
State state =
new State.Builder()
.setAvailableCommands(new Commands.Builder().addAllCommands().build())
.setPlaylist(
ImmutableList.of(
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1)
.setMediaItem(testMediaItem)
.build(),
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build()))
.setCurrentMediaItemIndex(1)
.setPlaybackState(Player.STATE_READY)
.build();
State updatedState =
state
.buildUpon()
.setPlaylist(
ImmutableList.of(new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build()))
.setCurrentMediaItemIndex(0)
.setPlaybackState(Player.STATE_ENDED)
.build();
SettableFuture<?> future = SettableFuture.create();
SimpleBasePlayer player =
new SimpleBasePlayer(Looper.myLooper()) {
@Override
protected State getState() {
return future.isDone() ? updatedState : state;
}
@Override
protected ListenableFuture<?> handleReplaceMediaItems(
int fromIndex, int toIndex, List<MediaItem> mediaItems) {
return future;
}
};
Listener listener = mock(Listener.class);
player.addListener(listener);
player.replaceMediaItems(/* fromIndex= */ 1, /* toIndex= */ 2, ImmutableList.of());
// Verify placeholder state and listener calls.
assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0);
assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(1);
Timeline.Window window = new Timeline.Window();
player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window);
assertThat(window.uid).isEqualTo(1);
assertThat(window.isPlaceholder).isFalse();
verify(listener)
.onTimelineChanged(
player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE));
verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_REMOVE);
verify(listener)
.onMediaItemTransition(testMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED);
verify(listener).onPlaybackStateChanged(Player.STATE_ENDED);
verify(listener).onPlayerStateChanged(/* playWhenReady= */ false, Player.STATE_ENDED);
verifyNoMoreInteractions(listener);
future.set(null);
// Verify actual state update.
assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0);
assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline);
verify(listener)
.onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
verifyNoMoreInteractions(listener);
}
@SuppressWarnings("deprecation") // Testing deprecated listener call.
@Test
public void
replaceMediaItems_asyncHandlingFromPreparedEmpty_usesPlaceholderStateAndInformsListeners() {
State state =
new State.Builder()
.setAvailableCommands(new Commands.Builder().addAllCommands().build())
.setPlaylist(ImmutableList.of())
.setCurrentMediaItemIndex(1)
.setPlaybackState(Player.STATE_ENDED)
.build();
State updatedState =
state
.buildUpon()
.setPlaylist(
ImmutableList.of(
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(),
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build()))
.setPlaybackState(Player.STATE_BUFFERING)
.build();
SettableFuture<?> future = SettableFuture.create();
SimpleBasePlayer player =
new SimpleBasePlayer(Looper.myLooper()) {
@Override
protected State getState() {
return future.isDone() ? updatedState : state;
}
@Override
protected ListenableFuture<?> handleReplaceMediaItems(
int fromIndex, int toIndex, List<MediaItem> mediaItems) {
return future;
}
};
Listener listener = mock(Listener.class);
player.addListener(listener);
player.replaceMediaItems(
/* fromIndex= */ 0,
/* toIndex= */ 0,
ImmutableList.of(
new MediaItem.Builder().setMediaId("1").build(),
new MediaItem.Builder().setMediaId("2").build()));
// Verify placeholder state and listener calls.
assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1);
assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2);
Timeline.Window window = new Timeline.Window();
player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window);
assertThat(window.mediaItem.mediaId).isEqualTo("1");
assertThat(window.isPlaceholder).isTrue();
player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window);
assertThat(window.mediaItem.mediaId).isEqualTo("2");
assertThat(window.isPlaceholder).isTrue();
verify(listener)
.onTimelineChanged(
player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
verify(listener)
.onMediaItemTransition(
new MediaItem.Builder().setMediaId("2").build(),
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED);
verify(listener).onPlaybackStateChanged(Player.STATE_BUFFERING);
verify(listener).onPlayerStateChanged(/* playWhenReady= */ false, Player.STATE_BUFFERING);
verifyNoMoreInteractions(listener);
future.set(null);
// Verify actual state update.
assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1);
assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline);
verify(listener)
.onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
verifyNoMoreInteractions(listener);
}
@SuppressWarnings("deprecation") // Testing deprecated listener call.
@Test
public void
replaceMediaItems_asyncHandlingFromEmptyToEmpty_usesPlaceholderStateAndInformsListeners() {
State state =
new State.Builder()
.setAvailableCommands(new Commands.Builder().addAllCommands().build())
.setPlaylist(ImmutableList.of())
.setCurrentMediaItemIndex(1)
.setPlaybackState(Player.STATE_ENDED)
.build();
SettableFuture<?> future = SettableFuture.create();
SimpleBasePlayer player =
new SimpleBasePlayer(Looper.myLooper()) {
@Override
protected State getState() {
return state;
}
@Override
protected ListenableFuture<?> handleReplaceMediaItems(
int fromIndex, int toIndex, List<MediaItem> mediaItems) {
return future;
}
};
Listener listener = mock(Listener.class);
player.addListener(listener);
player.replaceMediaItems(/* fromIndex= */ 0, /* toIndex= */ 0, ImmutableList.of());
// Verify placeholder state is a no-op and no listeners are called.
assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1);
assertThat(player.getCurrentTimeline().isEmpty()).isTrue();
verifyNoMoreInteractions(listener);
future.set(null);
// Verify actual state update is equally a no-op.
assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1);
verifyNoMoreInteractions(listener);
}
@Test
public void replaceMediaItem_withoutAvailableCommand_isNotForwarded() {
State state =
new State.Builder()
.setAvailableCommands(
new Commands.Builder()
.addAllCommands()
.remove(Player.COMMAND_CHANGE_MEDIA_ITEMS)
.build())
.setPlaylist(
ImmutableList.of(new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build()))
.build();
AtomicBoolean callForwarded = new AtomicBoolean();
SimpleBasePlayer player =
new SimpleBasePlayer(Looper.myLooper()) {
@Override
protected State getState() {
return state;
}
@Override
protected ListenableFuture<?> handleReplaceMediaItems(
int fromIndex, int toIndex, List<MediaItem> mediaItems) {
callForwarded.set(true);
return Futures.immediateVoidFuture();
}
};
player.replaceMediaItem(/* index= */ 0, new MediaItem.Builder().setMediaId("id").build());
assertThat(callForwarded.get()).isFalse();
}
@Test
public void replaceMediaItems_withInvalidToIndex_replacesToEndOfPlaylist() {
State state =
new State.Builder()
.setAvailableCommands(new Commands.Builder().addAllCommands().build())
.setPlaylist(
ImmutableList.of(
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(),
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build()))
.build();
AtomicInteger fromIndexInHandleMethod = new AtomicInteger(C.INDEX_UNSET);
AtomicInteger toIndexInHandleMethod = new AtomicInteger(C.INDEX_UNSET);
SimpleBasePlayer player =
new SimpleBasePlayer(Looper.myLooper()) {
@Override
protected State getState() {
return state;
}
@Override
protected ListenableFuture<?> handleReplaceMediaItems(
int fromIndex, int toIndex, List<MediaItem> mediaItems) {
fromIndexInHandleMethod.set(fromIndex);
toIndexInHandleMethod.set(toIndex);
return SettableFuture.create();
}
};
player.replaceMediaItems(
/* fromIndex= */ 1,
/* toIndex= */ 5000,
ImmutableList.of(new MediaItem.Builder().setMediaId("id").build()));
assertThat(fromIndexInHandleMethod.get()).isEqualTo(1);
assertThat(toIndexInHandleMethod.get()).isEqualTo(2);
assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2);
Timeline.Window window = new Timeline.Window();
player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window);
assertThat(window.uid).isEqualTo(1);
assertThat(window.isPlaceholder).isFalse();
player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window);
assertThat(window.mediaItem.mediaId).isEqualTo("id");
assertThat(window.isPlaceholder).isTrue();
}
@SuppressWarnings("deprecation") // Verifying deprecated listener calls.
@Test
public void seekTo_immediateHandling_updatesStateAndInformsListeners() {

View File

@ -732,6 +732,38 @@ import java.util.concurrent.TimeoutException;
/* repeatCurrentMediaItem= */ false);
}
@Override
public void replaceMediaItems(int fromIndex, int toIndex, List<MediaItem> mediaItems) {
verifyApplicationThread();
checkArgument(fromIndex >= 0 && toIndex >= fromIndex);
int playlistSize = mediaSourceHolderSnapshots.size();
if (fromIndex > playlistSize) {
// Do nothing.
return;
}
toIndex = min(toIndex, playlistSize);
List<MediaSource> mediaSources = createMediaSources(mediaItems);
if (mediaSourceHolderSnapshots.isEmpty()) {
// Handle initial items in a playlist as a set operation to ensure state changes and initial
// position are updated correctly.
setMediaSources(mediaSources, /* resetPosition= */ maskingWindowIndex == C.INDEX_UNSET);
return;
}
PlaybackInfo newPlaybackInfo = addMediaSourcesInternal(playbackInfo, toIndex, mediaSources);
newPlaybackInfo = removeMediaItemsInternal(newPlaybackInfo, fromIndex, toIndex);
boolean positionDiscontinuity =
!newPlaybackInfo.periodId.periodUid.equals(playbackInfo.periodId.periodUid);
updatePlaybackInfo(
newPlaybackInfo,
/* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
positionDiscontinuity,
DISCONTINUITY_REASON_REMOVE,
/* discontinuityWindowStartPositionUs= */ getCurrentPositionUsInternal(newPlaybackInfo),
/* ignored */ C.INDEX_UNSET,
/* repeatCurrentMediaItem= */ false);
}
@Override
public void setShuffleOrder(ShuffleOrder shuffleOrder) {
verifyApplicationThread();

View File

@ -944,6 +944,12 @@ public class SimpleExoPlayer extends BasePlayer
player.moveMediaItems(fromIndex, toIndex, newIndex);
}
@Override
public void replaceMediaItems(int fromIndex, int toIndex, List<MediaItem> mediaItems) {
blockUntilConstructorFinished();
player.replaceMediaItems(fromIndex, toIndex, mediaItems);
}
@Override
public void removeMediaItems(int fromIndex, int toIndex) {
blockUntilConstructorFinished();

View File

@ -85,6 +85,7 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.robolectric.Shadows.shadowOf;
import android.content.Context;
@ -12651,6 +12652,232 @@ public final class ExoPlayerTest {
eventsInOrder.verify(mockListener).onPlayerError(any(), any());
}
@Test
public void replaceMediaItems_notReplacingCurrentItem_correctMasking() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Listener listener = mock(Listener.class);
player.addMediaSources(
ImmutableList.of(new FakeMediaSource(), new FakeMediaSource(), new FakeMediaSource()));
player.seekToDefaultPosition(/* mediaItemIndex= */ 2);
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
player.addListener(listener);
player.replaceMediaItems(
/* fromIndex= */ 1,
/* toIndex= */ 2,
ImmutableList.of(
MediaItem.fromUri("test://test.uri"), MediaItem.fromUri("test://test2.uri")));
assertThat(player.getCurrentMediaItemIndex()).isEqualTo(3);
assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(4);
Timeline.Window window = new Timeline.Window();
player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window);
assertThat(window.mediaItem.localConfiguration.uri).isEqualTo(Uri.parse("test://test.uri"));
assertThat(window.isPlaceholder).isTrue();
player.getCurrentTimeline().getWindow(/* windowIndex= */ 2, window);
assertThat(window.mediaItem.localConfiguration.uri).isEqualTo(Uri.parse("test://test2.uri"));
assertThat(window.isPlaceholder).isTrue();
verify(listener)
.onTimelineChanged(
player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
verifyNoMoreInteractions(listener);
player.release();
}
@SuppressWarnings("deprecation") // Testing deprecated listener call.
@Test
public void replaceMediaItems_replacingCurrentItem_correctMasking() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Listener listener = mock(Listener.class);
player.addMediaSources(
ImmutableList.of(
createFakeMediaSource("1"), createFakeMediaSource("2"), createFakeMediaSource("3")));
player.seekToDefaultPosition(/* mediaItemIndex= */ 1);
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
player.addListener(listener);
player.replaceMediaItems(
/* fromIndex= */ 1,
/* toIndex= */ 2,
ImmutableList.of(
MediaItem.fromUri("test://test.uri"), MediaItem.fromUri("test://test2.uri")));
assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1);
assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(4);
Timeline.Window window = new Timeline.Window();
player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window);
assertThat(window.mediaItem.localConfiguration.uri).isEqualTo(Uri.parse("test://test.uri"));
assertThat(window.isPlaceholder).isTrue();
player.getCurrentTimeline().getWindow(/* windowIndex= */ 2, window);
assertThat(window.mediaItem.localConfiguration.uri).isEqualTo(Uri.parse("test://test2.uri"));
assertThat(window.isPlaceholder).isTrue();
verify(listener)
.onTimelineChanged(
player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE));
verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_REMOVE);
verify(listener)
.onMediaItemTransition(
MediaItem.fromUri("test://test.uri"),
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED);
verify(listener).onTracksChanged(Tracks.EMPTY);
verify(listener).onAvailableCommandsChanged(any());
verifyNoMoreInteractions(listener);
player.release();
}
@SuppressWarnings("deprecation") // Testing deprecated listener call.
@Test
public void replaceMediaItems_replacingCurrentItemWithEmptyListAndSubsequentItem_correctMasking()
throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Listener listener = mock(Listener.class);
player.addMediaSources(
ImmutableList.of(
createFakeMediaSource("1"), createFakeMediaSource("2"), createFakeMediaSource("3")));
player.seekToDefaultPosition(/* mediaItemIndex= */ 1);
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
player.addListener(listener);
player.replaceMediaItems(/* fromIndex= */ 1, /* toIndex= */ 2, ImmutableList.of());
assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1);
assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2);
verify(listener)
.onTimelineChanged(
player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE));
verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_REMOVE);
verify(listener)
.onMediaItemTransition(
createFakeMediaSource("3").getMediaItem(),
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED);
verify(listener).onTracksChanged(Tracks.EMPTY);
verify(listener).onAvailableCommandsChanged(any());
verifyNoMoreInteractions(listener);
player.release();
}
@SuppressWarnings("deprecation") // Testing deprecated listener call.
@Test
public void
replaceMediaItems_replacingCurrentItemWithEmptyListAndNoSubsequentItem_correctMasking()
throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Listener listener = mock(Listener.class);
player.addMediaSources(
ImmutableList.of(createFakeMediaSource("1"), createFakeMediaSource("2")));
player.seekToDefaultPosition(/* mediaItemIndex= */ 1);
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
player.addListener(listener);
player.replaceMediaItems(/* fromIndex= */ 1, /* toIndex= */ 2, ImmutableList.of());
assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0);
assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(1);
verify(listener)
.onTimelineChanged(
player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE));
verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_REMOVE);
verify(listener)
.onMediaItemTransition(
createFakeMediaSource("1").getMediaItem(),
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED);
verify(listener).onTracksChanged(Tracks.EMPTY);
verify(listener).onAvailableCommandsChanged(any());
verify(listener).onPlaybackStateChanged(Player.STATE_ENDED);
verify(listener).onPlayerStateChanged(/* playWhenReady= */ false, Player.STATE_ENDED);
verifyNoMoreInteractions(listener);
player.release();
}
@SuppressWarnings("deprecation") // Testing deprecated listener call.
@Test
public void replaceMediaItems_fromPreparedEmpty_correctMasking() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Listener listener = mock(Listener.class);
player.prepare();
player.seekToDefaultPosition(/* mediaItemIndex= */ 1);
runUntilPlaybackState(player, Player.STATE_ENDED);
player.addListener(listener);
player.replaceMediaItems(
/* fromIndex= */ 0,
/* toIndex= */ 0,
ImmutableList.of(
MediaItem.fromUri("test://test.uri"), MediaItem.fromUri("test://test2.uri")));
assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1);
assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2);
Timeline.Window window = new Timeline.Window();
player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window);
assertThat(window.mediaItem.localConfiguration.uri).isEqualTo(Uri.parse("test://test.uri"));
assertThat(window.isPlaceholder).isTrue();
player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window);
assertThat(window.mediaItem.localConfiguration.uri).isEqualTo(Uri.parse("test://test2.uri"));
assertThat(window.isPlaceholder).isTrue();
verify(listener)
.onTimelineChanged(
player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
verify(listener)
.onMediaItemTransition(
MediaItem.fromUri("test://test2.uri"),
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED);
verify(listener).onAvailableCommandsChanged(any());
verify(listener).onPlaybackStateChanged(Player.STATE_BUFFERING);
verify(listener).onPlayerStateChanged(/* playWhenReady= */ false, Player.STATE_BUFFERING);
verifyNoMoreInteractions(listener);
player.release();
}
@Test
public void replaceMediaItems_fromEmptyToEmpty_doesNothing() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Listener listener = mock(Listener.class);
player.prepare();
player.seekToDefaultPosition(/* mediaItemIndex= */ 1);
runUntilPlaybackState(player, Player.STATE_ENDED);
player.addListener(listener);
player.replaceMediaItems(/* fromIndex= */ 0, /* toIndex= */ 0, ImmutableList.of());
assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1);
assertThat(player.getCurrentTimeline().isEmpty()).isTrue();
verifyNoMoreInteractions(listener);
player.release();
}
@Test
public void replaceMediaItems_withInvalidToIndex_correctMasking() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Listener listener = mock(Listener.class);
player.addMediaSources(ImmutableList.of(new FakeMediaSource(), new FakeMediaSource()));
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
player.addListener(listener);
player.replaceMediaItems(
/* fromIndex= */ 1,
/* toIndex= */ 5000,
ImmutableList.of(MediaItem.fromUri("test://test.uri")));
assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2);
Timeline.Window window = new Timeline.Window();
player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window);
assertThat(window.mediaItem.localConfiguration.uri).isEqualTo(Uri.parse("test://test.uri"));
assertThat(window.isPlaceholder).isTrue();
verify(listener)
.onTimelineChanged(
player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
verifyNoMoreInteractions(listener);
player.release();
}
// Internal methods.
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {

View File

@ -83,6 +83,8 @@ oneway interface IMediaSession {
IMediaController caller, int seq, int currentIndex, int newIndex) = 3021;
void moveMediaItems(
IMediaController caller, int seq, int fromIndex, int toIndex, int newIndex) = 3022;
void replaceMediaItem(IMediaController caller, int seq, int index, in Bundle mediaItemBundle) = 3054;
void replaceMediaItems(IMediaController caller, int seq, int fromIndex, int toIndex, IBinder mediaItems) = 3055;
void play(IMediaController caller, int seq) = 3023;
void pause(IMediaController caller, int seq) = 3024;
void prepare(IMediaController caller, int seq) = 3025;
@ -118,7 +120,7 @@ oneway interface IMediaSession {
void setRatingWithMediaId(
IMediaController caller, int seq, String mediaId, in Bundle rating) = 3048;
void setRating(IMediaController caller, int seq, in Bundle rating) = 3049;
// Next Id for MediaSession: 3054
// Next Id for MediaSession: 3056
void getLibraryRoot(IMediaController caller, int seq, in Bundle libraryParams) = 4000;
void getItem(IMediaController caller, int seq, String mediaId) = 4001;

View File

@ -1071,12 +1071,6 @@ public class MediaController implements Player {
impl.addMediaItem(index, mediaItem);
}
/**
* {@inheritDoc}
*
* <p>Interoperability: When connected to {@link
* android.support.v4.media.session.MediaSessionCompat}, this doesn't atomically add items.
*/
@Override
public final void addMediaItems(List<MediaItem> mediaItems) {
verifyApplicationThread();
@ -1087,12 +1081,6 @@ public class MediaController implements Player {
impl.addMediaItems(mediaItems);
}
/**
* {@inheritDoc}
*
* <p>Interoperability: When connected to {@link
* android.support.v4.media.session.MediaSessionCompat}, this doesn't atomically add items.
*/
@Override
public final void addMediaItems(int index, List<MediaItem> mediaItems) {
verifyApplicationThread();
@ -1113,12 +1101,6 @@ public class MediaController implements Player {
impl.removeMediaItem(index);
}
/**
* {@inheritDoc}
*
* <p>Interoperability: When connected to {@link
* android.support.v4.media.session.MediaSessionCompat}, this doesn't atomically remove items.
*/
@Override
public final void removeMediaItems(int fromIndex, int toIndex) {
verifyApplicationThread();
@ -1129,12 +1111,6 @@ public class MediaController implements Player {
impl.removeMediaItems(fromIndex, toIndex);
}
/**
* {@inheritDoc}
*
* <p>Interoperability: When connected to {@link
* android.support.v4.media.session.MediaSessionCompat}, this doesn't atomically clear items.
*/
@Override
public final void clearMediaItems() {
verifyApplicationThread();
@ -1145,12 +1121,6 @@ public class MediaController implements Player {
impl.clearMediaItems();
}
/**
* {@inheritDoc}
*
* <p>Interoperability: When connected to {@link
* android.support.v4.media.session.MediaSessionCompat}, this doesn't atomically move items.
*/
@Override
public final void moveMediaItem(int currentIndex, int newIndex) {
verifyApplicationThread();
@ -1161,12 +1131,6 @@ public class MediaController implements Player {
impl.moveMediaItem(currentIndex, newIndex);
}
/**
* {@inheritDoc}
*
* <p>Interoperability: When connected to {@link
* android.support.v4.media.session.MediaSessionCompat}, this doesn't atomically move items.
*/
@Override
public final void moveMediaItems(int fromIndex, int toIndex, int newIndex) {
verifyApplicationThread();
@ -1177,6 +1141,26 @@ public class MediaController implements Player {
impl.moveMediaItems(fromIndex, toIndex, newIndex);
}
@Override
public final void replaceMediaItem(int index, MediaItem mediaItem) {
verifyApplicationThread();
if (!isConnected()) {
Log.w(TAG, "The controller is not connected. Ignoring replaceMediaItem().");
return;
}
impl.replaceMediaItem(index, mediaItem);
}
@Override
public final void replaceMediaItems(int fromIndex, int toIndex, List<MediaItem> mediaItems) {
verifyApplicationThread();
if (!isConnected()) {
Log.w(TAG, "The controller is not connected. Ignoring replaceMediaItems().");
return;
}
impl.replaceMediaItems(fromIndex, toIndex, mediaItems);
}
/**
* @deprecated Use {@link #isCurrentMediaItemDynamic()} instead.
*/
@ -2034,6 +2018,10 @@ public class MediaController implements Player {
void moveMediaItems(int fromIndex, int toIndex, int newIndex);
void replaceMediaItem(int index, MediaItem mediaItem);
void replaceMediaItems(int fromIndex, int toIndex, List<MediaItem> mediaItems);
int getCurrentPeriodIndex();
int getCurrentMediaItemIndex();

View File

@ -1183,6 +1183,79 @@ import org.checkerframework.checker.nullness.qual.NonNull;
moveMediaItemsInternal(fromIndex, toIndex, newIndex);
}
@Override
public void replaceMediaItem(int index, MediaItem mediaItem) {
if (!isPlayerCommandAvailable(Player.COMMAND_CHANGE_MEDIA_ITEMS)) {
return;
}
checkArgument(index >= 0);
dispatchRemoteSessionTaskWithPlayerCommand(
(iSession, seq) -> {
if (checkNotNull(connectedToken).getInterfaceVersion() >= 2) {
iSession.replaceMediaItem(controllerStub, seq, index, mediaItem.toBundle());
} else {
iSession.addMediaItemWithIndex(controllerStub, seq, index + 1, mediaItem.toBundle());
iSession.removeMediaItem(controllerStub, seq, index);
}
});
replaceMediaItemsInternal(
/* fromIndex= */ index, /* toIndex= */ index + 1, ImmutableList.of(mediaItem));
}
@Override
public void replaceMediaItems(int fromIndex, int toIndex, List<MediaItem> mediaItems) {
if (!isPlayerCommandAvailable(Player.COMMAND_CHANGE_MEDIA_ITEMS)) {
return;
}
checkArgument(fromIndex >= 0 && fromIndex <= toIndex);
dispatchRemoteSessionTaskWithPlayerCommand(
(iSession, seq) -> {
IBinder mediaItemsBundleBinder =
new BundleListRetriever(BundleableUtil.toBundleList(mediaItems));
if (checkNotNull(connectedToken).getInterfaceVersion() >= 2) {
iSession.replaceMediaItems(
controllerStub, seq, fromIndex, toIndex, mediaItemsBundleBinder);
} else {
iSession.addMediaItemsWithIndex(controllerStub, seq, toIndex, mediaItemsBundleBinder);
iSession.removeMediaItems(controllerStub, seq, fromIndex, toIndex);
}
});
replaceMediaItemsInternal(fromIndex, toIndex, mediaItems);
}
private void replaceMediaItemsInternal(int fromIndex, int toIndex, List<MediaItem> mediaItems) {
int playlistSize = playerInfo.timeline.getWindowCount();
if (fromIndex > playlistSize) {
return;
}
if (playerInfo.timeline.isEmpty()) {
// Handle initial items in a playlist as a set operation to ensure state changes and initial
// position are updated correctly.
setMediaItemsInternal(
mediaItems,
/* startIndex= */ C.INDEX_UNSET,
/* startPositionMs= */ C.TIME_UNSET,
/* resetToDefaultPosition= */ false);
return;
}
toIndex = min(toIndex, playlistSize);
PlayerInfo newPlayerInfo = maskPlaybackInfoForAddedItems(playerInfo, toIndex, mediaItems);
newPlayerInfo = maskPlayerInfoForRemovedItems(newPlayerInfo, fromIndex, toIndex);
boolean replacedCurrentItem =
playerInfo.sessionPositionInfo.positionInfo.mediaItemIndex >= fromIndex
&& playerInfo.sessionPositionInfo.positionInfo.mediaItemIndex < toIndex;
updatePlayerInfo(
newPlayerInfo,
/* timelineChangeReason= */ Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* positionDiscontinuity= */ replacedCurrentItem,
Player.DISCONTINUITY_REASON_REMOVE,
/* mediaItemTransition= */ replacedCurrentItem,
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED);
}
@Override
public int getCurrentPeriodIndex() {
return playerInfo.sessionPositionInfo.positionInfo.periodIndex;

View File

@ -849,6 +849,25 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
}
@Override
public void replaceMediaItem(int index, MediaItem mediaItem) {
replaceMediaItems(
/* fromIndex= */ index, /* toIndex= */ index + 1, ImmutableList.of(mediaItem));
}
@Override
public void replaceMediaItems(int fromIndex, int toIndex, List<MediaItem> mediaItems) {
checkArgument(fromIndex >= 0 && fromIndex <= toIndex);
QueueTimeline queueTimeline = (QueueTimeline) controllerInfo.playerInfo.timeline;
int size = queueTimeline.getWindowCount();
if (fromIndex > size) {
return;
}
toIndex = min(toIndex, size);
addMediaItems(toIndex, mediaItems);
removeMediaItems(fromIndex, toIndex);
}
@Override
public int getCurrentPeriodIndex() {
return getCurrentMediaItemIndex();

View File

@ -113,7 +113,7 @@ import java.util.concurrent.ExecutionException;
private static final String TAG = "MediaSessionStub";
/** The version of the IMediaSession interface. */
public static final int VERSION_INT = 1;
public static final int VERSION_INT = 2;
private final WeakReference<MediaSessionImpl> sessionImpl;
private final MediaSessionManager sessionManager;
@ -1270,6 +1270,74 @@ import java.util.concurrent.ExecutionException;
sendSessionResultSuccess(player -> player.moveMediaItems(fromIndex, toIndex, newIndex)));
}
@Override
public void replaceMediaItem(
IMediaController caller, int sequenceNumber, int index, Bundle mediaItemBundle) {
if (caller == null || mediaItemBundle == null) {
return;
}
MediaItem mediaItem;
try {
mediaItem = MediaItem.CREATOR.fromBundle(mediaItemBundle);
} catch (RuntimeException e) {
Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e);
return;
}
queueSessionTaskWithPlayerCommand(
caller,
sequenceNumber,
COMMAND_CHANGE_MEDIA_ITEMS,
sendSessionResultWhenReady(
handleMediaItemsWhenReady(
(sessionImpl, controller, sequenceNum) ->
sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)),
(player, controller, mediaItems) -> {
if (mediaItems.size() == 1) {
player.replaceMediaItem(
maybeCorrectMediaItemIndex(controller, player, index), mediaItems.get(0));
} else {
player.replaceMediaItems(
maybeCorrectMediaItemIndex(controller, player, index),
maybeCorrectMediaItemIndex(controller, player, index + 1),
mediaItems);
}
})));
}
@Override
public void replaceMediaItems(
IMediaController caller,
int sequenceNumber,
int fromIndex,
int toIndex,
IBinder mediaItemsRetriever) {
if (caller == null || mediaItemsRetriever == null) {
return;
}
ImmutableList<MediaItem> mediaItems;
try {
mediaItems =
BundleableUtil.fromBundleList(
MediaItem.CREATOR, BundleListRetriever.getList(mediaItemsRetriever));
} catch (RuntimeException e) {
Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e);
return;
}
queueSessionTaskWithPlayerCommand(
caller,
sequenceNumber,
COMMAND_CHANGE_MEDIA_ITEMS,
sendSessionResultWhenReady(
handleMediaItemsWhenReady(
(sessionImpl, controller, sequenceNum) ->
sessionImpl.onAddMediaItemsOnHandler(controller, mediaItems),
(player, controller, items) ->
player.replaceMediaItems(
maybeCorrectMediaItemIndex(controller, player, fromIndex),
maybeCorrectMediaItemIndex(controller, player, toIndex),
items))));
}
@Override
public void seekToPreviousMediaItem(@Nullable IMediaController caller, int sequenceNumber) {
if (caller == null) {

View File

@ -484,6 +484,18 @@ import java.util.List;
super.moveMediaItems(fromIndex, toIndex, newIndex);
}
@Override
public void replaceMediaItem(int index, MediaItem mediaItem) {
verifyApplicationThread();
super.replaceMediaItem(index, mediaItem);
}
@Override
public void replaceMediaItems(int fromIndex, int toIndex, List<MediaItem> mediaItems) {
verifyApplicationThread();
super.replaceMediaItems(fromIndex, toIndex, mediaItems);
}
@Deprecated
@Override
public boolean hasPrevious() {

View File

@ -62,6 +62,8 @@ interface IRemoteMediaController {
void clearMediaItems(String controllerId);
void moveMediaItem(String controllerId, int currentIndex, int newIndex);
void moveMediaItems(String controllerId, int fromIndex, int toIndex, int newIndex);
void replaceMediaItem(String controllerId, int index, in Bundle mediaItem);
void replaceMediaItems(String controllerId, int fromIndex, int toIndex, in List<Bundle> mediaItems);
void seekToPreviousMediaItem(String controllerId);
void seekToNextMediaItem(String controllerId);
void seekToPrevious(String controllerId);

View File

@ -3240,6 +3240,334 @@ public class MediaControllerStateMaskingTest {
assertThat(itemsAfterMove).containsExactly(items.get(1), items.get(0)).inOrder();
}
@Test
public void replaceMediaItems_notReplacingCurrentItem_correctMasking() throws Exception {
Bundle playerConfig =
new RemoteMediaSession.MockPlayerConfigBuilder()
.setTimeline(MediaTestUtils.createTimeline(3))
.setCurrentMediaItemIndex(2)
.build();
remoteSession.setPlayer(playerConfig);
MediaController controller = controllerTestRule.createController(remoteSession.getToken());
CountDownLatch latch = new CountDownLatch(2);
AtomicReference<Timeline> newTimelineRef = new AtomicReference<>();
AtomicReference<Player.Events> onEventsRef = new AtomicReference<>();
Player.Listener listener =
new Player.Listener() {
@Override
public void onTimelineChanged(Timeline timeline, int reason) {
newTimelineRef.set(timeline);
latch.countDown();
}
@Override
public void onEvents(Player player, Player.Events events) {
onEventsRef.set(events);
latch.countDown();
}
};
threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener));
AtomicInteger currentMediaItemIndexRef = new AtomicInteger();
threadTestRule
.getHandler()
.postAndSync(
() -> {
controller.replaceMediaItems(
/* fromIndex= */ 1, /* toIndex= */ 2, createMediaItems(2));
currentMediaItemIndexRef.set(controller.getCurrentMediaItemIndex());
});
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(newTimelineRef.get().getWindowCount()).isEqualTo(4);
assertThat(currentMediaItemIndexRef.get()).isEqualTo(3);
assertThat(getEventsAsList(onEventsRef.get())).containsExactly(Player.EVENT_TIMELINE_CHANGED);
}
@Test
public void replaceMediaItems_replacingCurrentItem_correctMasking() throws Exception {
Bundle playerConfig =
new RemoteMediaSession.MockPlayerConfigBuilder()
.setTimeline(MediaTestUtils.createTimeline(3))
.setCurrentMediaItemIndex(1)
.build();
remoteSession.setPlayer(playerConfig);
MediaController controller = controllerTestRule.createController(remoteSession.getToken());
CountDownLatch latch = new CountDownLatch(2);
AtomicReference<Timeline> newTimelineRef = new AtomicReference<>();
AtomicReference<Player.Events> onEventsRef = new AtomicReference<>();
Player.Listener listener =
new Player.Listener() {
@Override
public void onTimelineChanged(Timeline timeline, int reason) {
newTimelineRef.set(timeline);
latch.countDown();
}
@Override
public void onEvents(Player player, Player.Events events) {
onEventsRef.set(events);
latch.countDown();
}
};
threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener));
AtomicInteger currentMediaItemIndexRef = new AtomicInteger();
threadTestRule
.getHandler()
.postAndSync(
() -> {
controller.replaceMediaItem(/* index= */ 1, createMediaItems(1).get(0));
currentMediaItemIndexRef.set(controller.getCurrentMediaItemIndex());
});
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(newTimelineRef.get().getWindowCount()).isEqualTo(3);
assertThat(currentMediaItemIndexRef.get()).isEqualTo(1);
assertThat(getEventsAsList(onEventsRef.get()))
.containsExactly(
Player.EVENT_TIMELINE_CHANGED,
Player.EVENT_POSITION_DISCONTINUITY,
Player.EVENT_MEDIA_ITEM_TRANSITION);
}
@Test
public void replaceMediaItems_replacingCurrentItemWithEmptyListAndSubsequentItem_correctMasking()
throws Exception {
Bundle playerConfig =
new RemoteMediaSession.MockPlayerConfigBuilder()
.setTimeline(MediaTestUtils.createTimeline(3))
.setCurrentMediaItemIndex(1)
.build();
remoteSession.setPlayer(playerConfig);
MediaController controller = controllerTestRule.createController(remoteSession.getToken());
CountDownLatch latch = new CountDownLatch(2);
AtomicReference<Timeline> newTimelineRef = new AtomicReference<>();
AtomicReference<Player.Events> onEventsRef = new AtomicReference<>();
Player.Listener listener =
new Player.Listener() {
@Override
public void onTimelineChanged(Timeline timeline, int reason) {
newTimelineRef.set(timeline);
latch.countDown();
}
@Override
public void onEvents(Player player, Player.Events events) {
onEventsRef.set(events);
latch.countDown();
}
};
threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener));
AtomicInteger currentMediaItemIndexRef = new AtomicInteger();
threadTestRule
.getHandler()
.postAndSync(
() -> {
controller.replaceMediaItems(
/* fromIndex= */ 1, /* toIndex= */ 2, ImmutableList.of());
currentMediaItemIndexRef.set(controller.getCurrentMediaItemIndex());
});
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(newTimelineRef.get().getWindowCount()).isEqualTo(2);
assertThat(currentMediaItemIndexRef.get()).isEqualTo(1);
assertThat(getEventsAsList(onEventsRef.get()))
.containsExactly(
Player.EVENT_TIMELINE_CHANGED,
Player.EVENT_POSITION_DISCONTINUITY,
Player.EVENT_MEDIA_ITEM_TRANSITION);
}
@Test
public void
replaceMediaItems_replacingCurrentItemWithEmptyListAndNoSubsequentItem_correctMasking()
throws Exception {
Bundle playerConfig =
new RemoteMediaSession.MockPlayerConfigBuilder()
.setTimeline(MediaTestUtils.createTimeline(2))
.setCurrentMediaItemIndex(1)
.setPlaybackState(Player.STATE_BUFFERING)
.build();
remoteSession.setPlayer(playerConfig);
MediaController controller = controllerTestRule.createController(remoteSession.getToken());
CountDownLatch latch = new CountDownLatch(2);
AtomicReference<Timeline> newTimelineRef = new AtomicReference<>();
AtomicReference<Player.Events> onEventsRef = new AtomicReference<>();
Player.Listener listener =
new Player.Listener() {
@Override
public void onTimelineChanged(Timeline timeline, int reason) {
newTimelineRef.set(timeline);
latch.countDown();
}
@Override
public void onEvents(Player player, Player.Events events) {
onEventsRef.set(events);
latch.countDown();
}
};
threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener));
AtomicInteger currentMediaItemIndexRef = new AtomicInteger();
AtomicInteger playbackStateRef = new AtomicInteger();
threadTestRule
.getHandler()
.postAndSync(
() -> {
controller.replaceMediaItems(
/* fromIndex= */ 1, /* toIndex= */ 2, ImmutableList.of());
currentMediaItemIndexRef.set(controller.getCurrentMediaItemIndex());
playbackStateRef.set(controller.getPlaybackState());
});
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(newTimelineRef.get().getWindowCount()).isEqualTo(1);
assertThat(currentMediaItemIndexRef.get()).isEqualTo(0);
assertThat(playbackStateRef.get()).isEqualTo(Player.STATE_ENDED);
assertThat(getEventsAsList(onEventsRef.get()))
.containsExactly(
Player.EVENT_TIMELINE_CHANGED,
Player.EVENT_POSITION_DISCONTINUITY,
Player.EVENT_MEDIA_ITEM_TRANSITION,
Player.EVENT_PLAYBACK_STATE_CHANGED);
}
@Test
public void replaceMediaItems_fromPreparedEmpty_correctMasking() throws Exception {
Bundle playerConfig =
new RemoteMediaSession.MockPlayerConfigBuilder()
.setTimeline(Timeline.EMPTY)
.setCurrentMediaItemIndex(1)
.setPlaybackState(Player.STATE_ENDED)
.build();
remoteSession.setPlayer(playerConfig);
MediaController controller = controllerTestRule.createController(remoteSession.getToken());
CountDownLatch latch = new CountDownLatch(2);
AtomicReference<Timeline> newTimelineRef = new AtomicReference<>();
AtomicReference<Player.Events> onEventsRef = new AtomicReference<>();
Player.Listener listener =
new Player.Listener() {
@Override
public void onTimelineChanged(Timeline timeline, int reason) {
newTimelineRef.set(timeline);
latch.countDown();
}
@Override
public void onEvents(Player player, Player.Events events) {
onEventsRef.set(events);
latch.countDown();
}
};
threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener));
AtomicInteger currentMediaItemIndexRef = new AtomicInteger();
AtomicInteger playbackStateRef = new AtomicInteger();
threadTestRule
.getHandler()
.postAndSync(
() -> {
controller.replaceMediaItems(
/* fromIndex= */ 0, /* toIndex= */ 0, createMediaItems(2));
currentMediaItemIndexRef.set(controller.getCurrentMediaItemIndex());
playbackStateRef.set(controller.getPlaybackState());
});
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(newTimelineRef.get().getWindowCount()).isEqualTo(2);
assertThat(currentMediaItemIndexRef.get()).isEqualTo(1);
assertThat(playbackStateRef.get()).isEqualTo(Player.STATE_BUFFERING);
assertThat(getEventsAsList(onEventsRef.get()))
.containsExactly(
Player.EVENT_TIMELINE_CHANGED,
Player.EVENT_MEDIA_ITEM_TRANSITION,
Player.EVENT_PLAYBACK_STATE_CHANGED);
}
@Test
public void replaceMediaItems_fromEmptyToEmpty_correctMasking() throws Exception {
Bundle playerConfig =
new RemoteMediaSession.MockPlayerConfigBuilder()
.setTimeline(Timeline.EMPTY)
.setCurrentMediaItemIndex(1)
.setPlaybackState(Player.STATE_ENDED)
.build();
remoteSession.setPlayer(playerConfig);
MediaController controller = controllerTestRule.createController(remoteSession.getToken());
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<Timeline> newTimelineRef = new AtomicReference<>();
AtomicInteger currentMediaItemIndexRef = new AtomicInteger();
AtomicInteger playbackStateRef = new AtomicInteger();
threadTestRule
.getHandler()
.postAndSync(
() -> {
controller.replaceMediaItems(
/* fromIndex= */ 0, /* toIndex= */ 0, ImmutableList.of());
newTimelineRef.set(controller.getCurrentTimeline());
currentMediaItemIndexRef.set(controller.getCurrentMediaItemIndex());
playbackStateRef.set(controller.getPlaybackState());
latch.countDown();
});
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(newTimelineRef.get().isEmpty()).isTrue();
assertThat(currentMediaItemIndexRef.get()).isEqualTo(1);
assertThat(playbackStateRef.get()).isEqualTo(Player.STATE_ENDED);
}
@Test
public void replaceMediaItems_withInvalidToIndex_correctMasking() throws Exception {
Bundle playerConfig =
new RemoteMediaSession.MockPlayerConfigBuilder()
.setTimeline(MediaTestUtils.createTimeline(3))
.setCurrentMediaItemIndex(2)
.build();
remoteSession.setPlayer(playerConfig);
MediaController controller = controllerTestRule.createController(remoteSession.getToken());
CountDownLatch latch = new CountDownLatch(2);
AtomicReference<Timeline> newTimelineRef = new AtomicReference<>();
AtomicReference<Player.Events> onEventsRef = new AtomicReference<>();
Player.Listener listener =
new Player.Listener() {
@Override
public void onTimelineChanged(Timeline timeline, int reason) {
newTimelineRef.set(timeline);
latch.countDown();
}
@Override
public void onEvents(Player player, Player.Events events) {
onEventsRef.set(events);
latch.countDown();
}
};
threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener));
AtomicInteger currentMediaItemIndexRef = new AtomicInteger();
threadTestRule
.getHandler()
.postAndSync(
() -> {
controller.replaceMediaItems(
/* fromIndex= */ 1, /* toIndex= */ 5000, createMediaItems(2));
currentMediaItemIndexRef.set(controller.getCurrentMediaItemIndex());
});
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(newTimelineRef.get().getWindowCount()).isEqualTo(3);
assertThat(currentMediaItemIndexRef.get()).isEqualTo(1);
assertThat(getEventsAsList(onEventsRef.get()))
.containsExactly(
Player.EVENT_TIMELINE_CHANGED,
Player.EVENT_MEDIA_ITEM_TRANSITION,
Player.EVENT_POSITION_DISCONTINUITY);
}
private void assertMoveMediaItems(
int initialMediaItemCount,
int initialMediaItemIndex,

View File

@ -1307,4 +1307,380 @@ public class MediaControllerStateMaskingWithMediaSessionCompatTest {
assertThat(itemsAfterMove).containsExactly(items.get(1), items.get(0)).inOrder();
}
@Test
public void replaceMediaItems_notReplacingCurrentItem_correctMasking() throws Exception {
List<MediaItem> mediaItems = MediaTestUtils.createMediaItems("a", "b", "c");
List<QueueItem> queue = MediaTestUtils.convertToQueueItemsWithoutBitmap(mediaItems);
long testPosition = 200L;
int initialMediaItemIndex = 2;
MediaItem testCurrentMediaItem = mediaItems.get(initialMediaItemIndex);
session.setPlaybackState(
new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_PAUSED, testPosition, /* playbackSpeed= */ 1.0f)
.setActiveQueueItemId(queue.get(initialMediaItemIndex).getQueueId())
.build());
session.setQueue(queue);
List<MediaItem> newMediaItems = MediaTestUtils.createMediaItems("A", "B");
List<MediaItem> expectedMediaItems = new ArrayList<>();
expectedMediaItems.add(mediaItems.get(0));
expectedMediaItems.addAll(newMediaItems);
expectedMediaItems.add(mediaItems.get(2));
Events expectedEvents =
new Events(new FlagSet.Builder().addAll(EVENT_TIMELINE_CHANGED).build());
MediaController controller = controllerTestRule.createController(session.getSessionToken());
CountDownLatch latch = new CountDownLatch(3); // 2x onTimelineChanged + onEvents
AtomicReference<Timeline> timelineFromParamRef = new AtomicReference<>();
AtomicInteger timelineChangedReasonRef = new AtomicInteger();
AtomicReference<Player.Events> onEventsRef = new AtomicReference<>();
Player.Listener listener =
new Player.Listener() {
@Override
public void onTimelineChanged(
Timeline timeline, @Player.TimelineChangeReason int reason) {
timelineFromParamRef.set(timeline);
timelineChangedReasonRef.set(reason);
latch.countDown();
}
@Override
public void onEvents(Player player, Player.Events events) {
onEventsRef.set(events);
latch.countDown();
}
};
threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener));
AtomicInteger currentMediaItemIndexRef = new AtomicInteger();
AtomicReference<MediaItem> currentMediaItemRef = new AtomicReference<>();
AtomicReference<Timeline> timelineFromGetterRef = new AtomicReference<>();
threadTestRule
.getHandler()
.postAndSync(
() -> {
controller.replaceMediaItems(/* fromIndex= */ 1, /* toIndex= */ 2, newMediaItems);
currentMediaItemIndexRef.set(controller.getCurrentMediaItemIndex());
currentMediaItemRef.set(controller.getCurrentMediaItem());
timelineFromGetterRef.set(controller.getCurrentTimeline());
});
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
MediaTestUtils.assertTimelineContains(timelineFromParamRef.get(), expectedMediaItems);
assertThat(timelineChangedReasonRef.get()).isEqualTo(TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
assertThat(onEventsRef.get()).isEqualTo(expectedEvents);
assertThat(currentMediaItemIndexRef.get()).isEqualTo(3);
assertThat(currentMediaItemRef.get()).isEqualTo(testCurrentMediaItem);
MediaTestUtils.assertTimelineContains(timelineFromGetterRef.get(), expectedMediaItems);
}
@Test
public void replaceMediaItems_replacingCurrentItem_correctMasking() throws Exception {
List<MediaItem> mediaItems = MediaTestUtils.createMediaItems("a", "b", "c");
List<QueueItem> queue = MediaTestUtils.convertToQueueItemsWithoutBitmap(mediaItems);
long testPosition = 200L;
int initialMediaItemIndex = 1;
session.setPlaybackState(
new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_PAUSED, testPosition, /* playbackSpeed= */ 1.0f)
.setActiveQueueItemId(queue.get(initialMediaItemIndex).getQueueId())
.build());
session.setQueue(queue);
List<MediaItem> newMediaItems = MediaTestUtils.createMediaItems("A", "B");
List<MediaItem> expectedMediaItems = new ArrayList<>();
expectedMediaItems.add(mediaItems.get(0));
expectedMediaItems.addAll(newMediaItems);
expectedMediaItems.add(mediaItems.get(2));
Events expectedEvents =
new Events(new FlagSet.Builder().addAll(EVENT_TIMELINE_CHANGED).build());
MediaController controller = controllerTestRule.createController(session.getSessionToken());
CountDownLatch latch = new CountDownLatch(3); // 2x onTimelineChanged + onEvents
AtomicReference<Timeline> timelineFromParamRef = new AtomicReference<>();
AtomicInteger timelineChangedReasonRef = new AtomicInteger();
AtomicReference<Player.Events> onEventsRef = new AtomicReference<>();
Player.Listener listener =
new Player.Listener() {
@Override
public void onTimelineChanged(
Timeline timeline, @Player.TimelineChangeReason int reason) {
timelineFromParamRef.set(timeline);
timelineChangedReasonRef.set(reason);
latch.countDown();
}
@Override
public void onEvents(Player player, Player.Events events) {
onEventsRef.set(events);
latch.countDown();
}
};
threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener));
AtomicInteger currentMediaItemIndexRef = new AtomicInteger();
AtomicReference<MediaItem> currentMediaItemRef = new AtomicReference<>();
AtomicReference<Timeline> timelineFromGetterRef = new AtomicReference<>();
threadTestRule
.getHandler()
.postAndSync(
() -> {
controller.replaceMediaItems(/* fromIndex= */ 1, /* toIndex= */ 2, newMediaItems);
currentMediaItemIndexRef.set(controller.getCurrentMediaItemIndex());
currentMediaItemRef.set(controller.getCurrentMediaItem());
timelineFromGetterRef.set(controller.getCurrentTimeline());
});
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
MediaTestUtils.assertTimelineContains(timelineFromParamRef.get(), expectedMediaItems);
assertThat(timelineChangedReasonRef.get()).isEqualTo(TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
assertThat(onEventsRef.get()).isEqualTo(expectedEvents);
assertThat(currentMediaItemIndexRef.get()).isEqualTo(1);
assertThat(currentMediaItemRef.get()).isEqualTo(newMediaItems.get(0));
MediaTestUtils.assertTimelineContains(timelineFromGetterRef.get(), expectedMediaItems);
}
@Test
public void replaceMediaItems_replacingCurrentItemWithEmptyListAndSubsequentItem_correctMasking()
throws Exception {
List<MediaItem> mediaItems = MediaTestUtils.createMediaItems("a", "b", "c");
List<QueueItem> queue = MediaTestUtils.convertToQueueItemsWithoutBitmap(mediaItems);
long testPosition = 200L;
int initialMediaItemIndex = 1;
session.setPlaybackState(
new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_PAUSED, testPosition, /* playbackSpeed= */ 1.0f)
.setActiveQueueItemId(queue.get(initialMediaItemIndex).getQueueId())
.build());
session.setQueue(queue);
List<MediaItem> expectedMediaItems = new ArrayList<>();
expectedMediaItems.add(mediaItems.get(0));
expectedMediaItems.add(mediaItems.get(2));
Events expectedEvents =
new Events(new FlagSet.Builder().addAll(EVENT_TIMELINE_CHANGED).build());
MediaController controller = controllerTestRule.createController(session.getSessionToken());
CountDownLatch latch = new CountDownLatch(2);
AtomicReference<Timeline> timelineFromParamRef = new AtomicReference<>();
AtomicInteger timelineChangedReasonRef = new AtomicInteger();
AtomicReference<Player.Events> onEventsRef = new AtomicReference<>();
Player.Listener listener =
new Player.Listener() {
@Override
public void onTimelineChanged(
Timeline timeline, @Player.TimelineChangeReason int reason) {
timelineFromParamRef.set(timeline);
timelineChangedReasonRef.set(reason);
latch.countDown();
}
@Override
public void onEvents(Player player, Player.Events events) {
onEventsRef.set(events);
latch.countDown();
}
};
threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener));
AtomicInteger currentMediaItemIndexRef = new AtomicInteger();
AtomicReference<MediaItem> currentMediaItemRef = new AtomicReference<>();
AtomicReference<Timeline> timelineFromGetterRef = new AtomicReference<>();
threadTestRule
.getHandler()
.postAndSync(
() -> {
controller.replaceMediaItems(
/* fromIndex= */ 1, /* toIndex= */ 2, ImmutableList.of());
currentMediaItemIndexRef.set(controller.getCurrentMediaItemIndex());
currentMediaItemRef.set(controller.getCurrentMediaItem());
timelineFromGetterRef.set(controller.getCurrentTimeline());
});
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
MediaTestUtils.assertTimelineContains(timelineFromParamRef.get(), expectedMediaItems);
assertThat(timelineChangedReasonRef.get()).isEqualTo(TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
assertThat(onEventsRef.get()).isEqualTo(expectedEvents);
assertThat(currentMediaItemIndexRef.get()).isEqualTo(1);
assertThat(currentMediaItemRef.get()).isEqualTo(expectedMediaItems.get(1));
MediaTestUtils.assertTimelineContains(timelineFromGetterRef.get(), expectedMediaItems);
}
@Test
public void
replaceMediaItems_replacingCurrentItemWithEmptyListAndNoSubsequentItem_correctMasking()
throws Exception {
List<MediaItem> mediaItems = MediaTestUtils.createMediaItems("a", "b");
List<QueueItem> queue = MediaTestUtils.convertToQueueItemsWithoutBitmap(mediaItems);
long testPosition = 200L;
int initialMediaItemIndex = 1;
session.setPlaybackState(
new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_PAUSED, testPosition, /* playbackSpeed= */ 1.0f)
.setActiveQueueItemId(queue.get(initialMediaItemIndex).getQueueId())
.build());
session.setQueue(queue);
List<MediaItem> expectedMediaItems = new ArrayList<>();
expectedMediaItems.add(mediaItems.get(0));
Events expectedEvents =
new Events(new FlagSet.Builder().addAll(EVENT_TIMELINE_CHANGED).build());
MediaController controller = controllerTestRule.createController(session.getSessionToken());
CountDownLatch latch = new CountDownLatch(2);
AtomicReference<Timeline> timelineFromParamRef = new AtomicReference<>();
AtomicInteger timelineChangedReasonRef = new AtomicInteger();
AtomicReference<Player.Events> onEventsRef = new AtomicReference<>();
Player.Listener listener =
new Player.Listener() {
@Override
public void onTimelineChanged(
Timeline timeline, @Player.TimelineChangeReason int reason) {
timelineFromParamRef.set(timeline);
timelineChangedReasonRef.set(reason);
latch.countDown();
}
@Override
public void onEvents(Player player, Player.Events events) {
onEventsRef.set(events);
latch.countDown();
}
};
threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener));
AtomicInteger currentMediaItemIndexRef = new AtomicInteger();
AtomicReference<MediaItem> currentMediaItemRef = new AtomicReference<>();
AtomicReference<Timeline> timelineFromGetterRef = new AtomicReference<>();
threadTestRule
.getHandler()
.postAndSync(
() -> {
controller.replaceMediaItems(
/* fromIndex= */ 1, /* toIndex= */ 2, ImmutableList.of());
currentMediaItemIndexRef.set(controller.getCurrentMediaItemIndex());
currentMediaItemRef.set(controller.getCurrentMediaItem());
timelineFromGetterRef.set(controller.getCurrentTimeline());
});
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
MediaTestUtils.assertTimelineContains(timelineFromParamRef.get(), expectedMediaItems);
assertThat(timelineChangedReasonRef.get()).isEqualTo(TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
assertThat(onEventsRef.get()).isEqualTo(expectedEvents);
assertThat(currentMediaItemIndexRef.get()).isEqualTo(0);
assertThat(currentMediaItemRef.get()).isEqualTo(expectedMediaItems.get(0));
MediaTestUtils.assertTimelineContains(timelineFromGetterRef.get(), expectedMediaItems);
}
@Test
public void replaceMediaItems_fromEmpty_correctMasking() throws Exception {
session.setPlaybackState(
new PlaybackStateCompat.Builder()
.setState(
PlaybackStateCompat.STATE_STOPPED, /* position= */ 0, /* playbackSpeed= */ 1.0f)
.build());
session.setQueue(ImmutableList.of());
List<MediaItem> newMediaItems = MediaTestUtils.createMediaItems("A", "B");
Events expectedEvents =
new Events(new FlagSet.Builder().addAll(EVENT_TIMELINE_CHANGED).build());
MediaController controller = controllerTestRule.createController(session.getSessionToken());
CountDownLatch latch = new CountDownLatch(2);
AtomicReference<Timeline> timelineFromParamRef = new AtomicReference<>();
AtomicInteger timelineChangedReasonRef = new AtomicInteger();
AtomicReference<Player.Events> onEventsRef = new AtomicReference<>();
Player.Listener listener =
new Player.Listener() {
@Override
public void onTimelineChanged(
Timeline timeline, @Player.TimelineChangeReason int reason) {
timelineFromParamRef.set(timeline);
timelineChangedReasonRef.set(reason);
latch.countDown();
}
@Override
public void onEvents(Player player, Player.Events events) {
onEventsRef.set(events);
latch.countDown();
}
};
threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener));
AtomicInteger currentMediaItemIndexRef = new AtomicInteger();
AtomicReference<MediaItem> currentMediaItemRef = new AtomicReference<>();
AtomicReference<Timeline> timelineFromGetterRef = new AtomicReference<>();
threadTestRule
.getHandler()
.postAndSync(
() -> {
controller.replaceMediaItems(/* fromIndex= */ 0, /* toIndex= */ 0, newMediaItems);
currentMediaItemIndexRef.set(controller.getCurrentMediaItemIndex());
currentMediaItemRef.set(controller.getCurrentMediaItem());
timelineFromGetterRef.set(controller.getCurrentTimeline());
});
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
MediaTestUtils.assertTimelineContains(timelineFromParamRef.get(), newMediaItems);
assertThat(timelineChangedReasonRef.get()).isEqualTo(TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
assertThat(onEventsRef.get()).isEqualTo(expectedEvents);
assertThat(currentMediaItemIndexRef.get()).isEqualTo(0);
assertThat(currentMediaItemRef.get()).isEqualTo(newMediaItems.get(0));
MediaTestUtils.assertTimelineContains(timelineFromGetterRef.get(), newMediaItems);
}
@Test
public void replaceMediaItems_withInvalidToIndex_correctMasking() throws Exception {
List<MediaItem> mediaItems = MediaTestUtils.createMediaItems("a", "b", "c");
List<QueueItem> queue = MediaTestUtils.convertToQueueItemsWithoutBitmap(mediaItems);
long testPosition = 200L;
int initialMediaItemIndex = 1;
session.setPlaybackState(
new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_PAUSED, testPosition, /* playbackSpeed= */ 1.0f)
.setActiveQueueItemId(queue.get(initialMediaItemIndex).getQueueId())
.build());
session.setQueue(queue);
List<MediaItem> newMediaItems = MediaTestUtils.createMediaItems("A", "B");
List<MediaItem> expectedMediaItems = new ArrayList<>();
expectedMediaItems.add(mediaItems.get(0));
expectedMediaItems.addAll(newMediaItems);
Events expectedEvents =
new Events(new FlagSet.Builder().addAll(EVENT_TIMELINE_CHANGED).build());
MediaController controller = controllerTestRule.createController(session.getSessionToken());
CountDownLatch latch = new CountDownLatch(3);
AtomicReference<Timeline> timelineFromParamRef = new AtomicReference<>();
AtomicInteger timelineChangedReasonRef = new AtomicInteger();
AtomicReference<Player.Events> onEventsRef = new AtomicReference<>();
Player.Listener listener =
new Player.Listener() {
@Override
public void onTimelineChanged(
Timeline timeline, @Player.TimelineChangeReason int reason) {
timelineFromParamRef.set(timeline);
timelineChangedReasonRef.set(reason);
latch.countDown();
}
@Override
public void onEvents(Player player, Player.Events events) {
onEventsRef.set(events);
latch.countDown();
}
};
threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener));
AtomicInteger currentMediaItemIndexRef = new AtomicInteger();
AtomicReference<MediaItem> currentMediaItemRef = new AtomicReference<>();
AtomicReference<Timeline> timelineFromGetterRef = new AtomicReference<>();
threadTestRule
.getHandler()
.postAndSync(
() -> {
controller.replaceMediaItems(/* fromIndex= */ 1, /* toIndex= */ 5000, newMediaItems);
currentMediaItemIndexRef.set(controller.getCurrentMediaItemIndex());
currentMediaItemRef.set(controller.getCurrentMediaItem());
timelineFromGetterRef.set(controller.getCurrentTimeline());
});
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
MediaTestUtils.assertTimelineContains(timelineFromParamRef.get(), expectedMediaItems);
assertThat(timelineChangedReasonRef.get()).isEqualTo(TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
assertThat(onEventsRef.get()).isEqualTo(expectedEvents);
assertThat(currentMediaItemIndexRef.get()).isEqualTo(1);
assertThat(currentMediaItemRef.get()).isEqualTo(newMediaItems.get(0));
MediaTestUtils.assertTimelineContains(timelineFromGetterRef.get(), expectedMediaItems);
}
}

View File

@ -168,6 +168,21 @@ public class MediaSessionPermissionTest {
controller -> controller.removeMediaItems(/* fromIndex= */ 0, /* toIndex= */ 1));
}
@Test
public void replaceMediaItem() throws Exception {
testOnCommandRequest(
COMMAND_CHANGE_MEDIA_ITEMS,
controller -> controller.replaceMediaItem(/* index= */ 0, MediaItem.EMPTY));
}
@Test
public void replaceMediaItems() throws Exception {
testOnCommandRequest(
COMMAND_CHANGE_MEDIA_ITEMS,
controller ->
controller.replaceMediaItems(/* fromIndex= */ 0, /* toIndex= */ 1, ImmutableList.of()));
}
@Test
public void setDeviceVolume() throws Exception {
testOnCommandRequest(COMMAND_SET_DEVICE_VOLUME, controller -> controller.setDeviceVolume(0));

View File

@ -703,6 +703,34 @@ public class MediaSessionPlayerTest {
assertThat(player.newIndex).isEqualTo(newIndex);
}
@Test
public void replaceMediaItem() throws Exception {
player.setMediaItems(MediaTestUtils.createMediaItems(4));
MediaItem mediaItem = MediaTestUtils.createMediaItem("replaceMediaItem");
controller.replaceMediaItem(/* index= */ 2, mediaItem);
player.awaitMethodCalled(MockPlayer.METHOD_REPLACE_MEDIA_ITEM, TIMEOUT_MS);
assertThat(player.index).isEqualTo(2);
assertThat(player.mediaItems).hasSize(4);
assertThat(player.mediaItems.get(2)).isEqualTo(mediaItem);
}
@Test
public void replaceMediaItems() throws Exception {
player.setMediaItems(MediaTestUtils.createMediaItems(4));
List<MediaItem> mediaItems = MediaTestUtils.createMediaItems(2);
controller.replaceMediaItems(/* fromIndex= */ 1, /* toIndex= */ 2, mediaItems);
player.awaitMethodCalled(MockPlayer.METHOD_REPLACE_MEDIA_ITEMS, TIMEOUT_MS);
assertThat(player.fromIndex).isEqualTo(1);
assertThat(player.toIndex).isEqualTo(2);
assertThat(player.mediaItems).hasSize(5);
assertThat(player.mediaItems.get(1)).isEqualTo(mediaItems.get(0));
assertThat(player.mediaItems.get(2)).isEqualTo(mediaItems.get(1));
}
@Test
public void seekToPreviousMediaItem() throws Exception {
controller.seekToPreviousMediaItem();

View File

@ -370,6 +370,39 @@ public class MockPlayerTest {
.inOrder();
}
@Test
public void replaceMediaItem() {
List<MediaItem> mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3);
player.addMediaItems(mediaItems);
MediaItem mediaItem = MediaTestUtils.createMediaItem("item");
player.replaceMediaItem(/* index= */ 1, mediaItem);
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_REPLACE_MEDIA_ITEM)).isTrue();
assertThat(player.index).isEqualTo(1);
assertThat(player.mediaItems).containsExactly(mediaItems.get(0), mediaItem, mediaItems.get(2));
}
@Test
public void replaceMediaItems() {
List<MediaItem> mediaItems = MediaTestUtils.createMediaItems(/* size= */ 4);
player.addMediaItems(mediaItems);
List<MediaItem> newMediaItems = MediaTestUtils.createMediaItems(/* size= */ 3);
player.replaceMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3, newMediaItems);
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_REPLACE_MEDIA_ITEMS)).isTrue();
assertThat(player.fromIndex).isEqualTo(1);
assertThat(player.toIndex).isEqualTo(3);
assertThat(player.mediaItems)
.containsExactly(
mediaItems.get(0),
newMediaItems.get(0),
newMediaItems.get(1),
newMediaItems.get(2),
mediaItems.get(3));
}
@Test
public void seekToPreviousMediaItem() {
player.seekToPreviousMediaItem();

View File

@ -463,6 +463,28 @@ public class MediaControllerProviderService extends Service {
});
}
@Override
public void replaceMediaItem(String controllerId, int index, Bundle mediaItem)
throws RemoteException {
runOnHandler(
() -> {
MediaController controller = mediaControllerMap.get(controllerId);
controller.replaceMediaItem(index, MediaItem.CREATOR.fromBundle(mediaItem));
});
}
@Override
public void replaceMediaItems(
String controllerId, int fromIndex, int toIndex, List<Bundle> mediaItems)
throws RemoteException {
runOnHandler(
() -> {
MediaController controller = mediaControllerMap.get(controllerId);
controller.replaceMediaItems(
fromIndex, toIndex, BundleableUtil.fromBundleList(MediaItem.CREATOR, mediaItems));
});
}
@Override
public void seekToPreviousMediaItem(String controllerId) throws RemoteException {
runOnHandler(

View File

@ -107,7 +107,9 @@ public class MockPlayer implements Player {
METHOD_SET_SHUFFLE_MODE,
METHOD_SET_TRACK_SELECTION_PARAMETERS,
METHOD_SET_VOLUME,
METHOD_STOP
METHOD_STOP,
METHOD_REPLACE_MEDIA_ITEM,
METHOD_REPLACE_MEDIA_ITEMS
})
public @interface Method {}
@ -203,6 +205,10 @@ public class MockPlayer implements Player {
public static final int METHOD_SET_DEVICE_MUTED_WITH_FLAGS = 44;
/** Maps to {@link Player#setDeviceVolume(int, int)}. */
public static final int METHOD_SET_DEVICE_VOLUME_WITH_FLAGS = 45;
/** Maps to {@link Player#replaceMediaItem(int, MediaItem)}. */
public static final int METHOD_REPLACE_MEDIA_ITEM = 46;
/** Maps to {@link Player#replaceMediaItems(int, int, List)} . */
public static final int METHOD_REPLACE_MEDIA_ITEMS = 47;
private final boolean changePlayerStateWithTransportControl;
private final Looper applicationLooper;
@ -990,6 +996,22 @@ public class MockPlayer implements Player {
checkNotNull(conditionVariables.get(METHOD_MOVE_MEDIA_ITEMS)).open();
}
@Override
public void replaceMediaItem(int index, MediaItem mediaItem) {
this.index = index;
this.mediaItems.set(index, mediaItem);
checkNotNull(conditionVariables.get(METHOD_REPLACE_MEDIA_ITEM)).open();
}
@Override
public void replaceMediaItems(int fromIndex, int toIndex, List<MediaItem> mediaItems) {
this.fromIndex = fromIndex;
this.toIndex = toIndex;
this.mediaItems.addAll(toIndex, mediaItems);
Util.removeRange(this.mediaItems, fromIndex, toIndex);
checkNotNull(conditionVariables.get(METHOD_REPLACE_MEDIA_ITEMS)).open();
}
/**
* @deprecated Use {@link #hasPreviousMediaItem()} instead.
*/
@ -1410,6 +1432,8 @@ public class MockPlayer implements Player {
.put(METHOD_SET_TRACK_SELECTION_PARAMETERS, new ConditionVariable())
.put(METHOD_SET_VOLUME, new ConditionVariable())
.put(METHOD_STOP, new ConditionVariable())
.put(METHOD_REPLACE_MEDIA_ITEM, new ConditionVariable())
.put(METHOD_REPLACE_MEDIA_ITEMS, new ConditionVariable())
.buildOrThrow();
}

View File

@ -218,6 +218,16 @@ public class RemoteMediaController {
binder.moveMediaItems(controllerId, fromIndex, toIndex, newIndex);
}
public void replaceMediaItem(int index, MediaItem mediaItem) throws RemoteException {
binder.replaceMediaItem(controllerId, index, mediaItem.toBundle());
}
public void replaceMediaItems(int fromIndex, int toIndex, List<MediaItem> mediaItems)
throws RemoteException {
binder.replaceMediaItems(
controllerId, fromIndex, toIndex, BundleableUtil.toBundleList(mediaItems));
}
public void seekToPreviousMediaItem() throws RemoteException {
binder.seekToPreviousMediaItem(controllerId);
}

View File

@ -102,6 +102,11 @@ public class StubPlayer extends BasePlayer {
throw new UnsupportedOperationException();
}
@Override
public void replaceMediaItems(int fromIndex, int toIndex, List<MediaItem> mediaItems) {
throw new UnsupportedOperationException();
}
@Override
public void removeMediaItems(int fromIndex, int toIndex) {
throw new UnsupportedOperationException();