From 09d37641d1ef3b8cf26dc39cfcd317ebe3ef5c79 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 19 Dec 2022 12:16:35 +0000 Subject: [PATCH] Add playlist and seek operations to SimpleBasePlayer These are the remaining setter operations. They all share the same logic that handles playlist and/or position changes. The logic to create the placeholder state is mostly copied from ExoPlayerImpl's maskTimelineAndPosition and getPeriodPositonUsAfterTimelineChanged. PiperOrigin-RevId: 496364712 --- .../android/exoplayer2/SimpleBasePlayer.java | 435 +- .../exoplayer2/SimpleBasePlayerTest.java | 3546 ++++++++++++++++- 2 files changed, 3954 insertions(+), 27 deletions(-) diff --git a/library/common/src/main/java/com/google/android/exoplayer2/SimpleBasePlayer.java b/library/common/src/main/java/com/google/android/exoplayer2/SimpleBasePlayer.java index d501170103..27d02c9d31 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/SimpleBasePlayer.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/SimpleBasePlayer.java @@ -22,6 +22,7 @@ import static com.google.android.exoplayer2.util.Util.castNonNull; import static com.google.android.exoplayer2.util.Util.msToUs; import static com.google.android.exoplayer2.util.Util.usToMs; import static java.lang.Math.max; +import static java.lang.Math.min; import android.graphics.Rect; import android.os.Looper; @@ -52,6 +53,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.ForOverride; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -2021,33 +2023,118 @@ public abstract class SimpleBasePlayer extends BasePlayer { @Override public final void setMediaItems(List mediaItems, boolean resetPosition) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + int startIndex = resetPosition ? C.INDEX_UNSET : state.currentMediaItemIndex; + long startPositionMs = resetPosition ? C.TIME_UNSET : state.contentPositionMsSupplier.get(); + setMediaItemsInternal(mediaItems, startIndex, startPositionMs); } @Override public final void setMediaItems( List mediaItems, int startIndex, long startPositionMs) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + if (startIndex == C.INDEX_UNSET) { + startIndex = state.currentMediaItemIndex; + startPositionMs = state.contentPositionMsSupplier.get(); + } + setMediaItemsInternal(mediaItems, startIndex, startPositionMs); + } + + @RequiresNonNull("state") + private void setMediaItemsInternal( + List mediaItems, int startIndex, long startPositionMs) { + checkArgument(startIndex == C.INDEX_UNSET || startIndex >= 0); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!shouldHandleCommand(Player.COMMAND_CHANGE_MEDIA_ITEMS) + && (mediaItems.size() != 1 || !shouldHandleCommand(Player.COMMAND_SET_MEDIA_ITEM))) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSetMediaItems(mediaItems, startIndex, startPositionMs), + /* placeholderStateSupplier= */ () -> { + ArrayList placeholderPlaylist = new ArrayList<>(); + for (int i = 0; i < mediaItems.size(); i++) { + placeholderPlaylist.add(getPlaceholderMediaItemData(mediaItems.get(i))); + } + return getStateWithNewPlaylistAndPosition( + state, placeholderPlaylist, startIndex, startPositionMs); + }); } @Override public final void addMediaItems(int index, List mediaItems) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + checkArgument(index >= 0); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + int playlistSize = state.playlist.size(); + if (!shouldHandleCommand(Player.COMMAND_CHANGE_MEDIA_ITEMS) || mediaItems.isEmpty()) { + return; + } + int correctedIndex = min(index, playlistSize); + updateStateForPendingOperation( + /* pendingOperation= */ handleAddMediaItems(correctedIndex, mediaItems), + /* placeholderStateSupplier= */ () -> { + ArrayList placeholderPlaylist = new ArrayList<>(state.playlist); + for (int i = 0; i < mediaItems.size(); i++) { + placeholderPlaylist.add( + i + correctedIndex, getPlaceholderMediaItemData(mediaItems.get(i))); + } + return getStateWithNewPlaylist(state, placeholderPlaylist, period); + }); } @Override public final void moveMediaItems(int fromIndex, int toIndex, int newIndex) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + checkArgument(fromIndex >= 0 && toIndex >= fromIndex && newIndex >= 0); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + int playlistSize = state.playlist.size(); + if (!shouldHandleCommand(Player.COMMAND_CHANGE_MEDIA_ITEMS) + || playlistSize == 0 + || fromIndex >= playlistSize) { + return; + } + int correctedToIndex = min(toIndex, playlistSize); + int correctedNewIndex = min(newIndex, state.playlist.size() - (correctedToIndex - fromIndex)); + if (fromIndex == correctedToIndex || correctedNewIndex == fromIndex) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleMoveMediaItems( + fromIndex, correctedToIndex, correctedNewIndex), + /* placeholderStateSupplier= */ () -> { + ArrayList placeholderPlaylist = new ArrayList<>(state.playlist); + Util.moveItems(placeholderPlaylist, fromIndex, correctedToIndex, correctedNewIndex); + return getStateWithNewPlaylist(state, placeholderPlaylist, period); + }); } @Override public final void removeMediaItems(int fromIndex, int toIndex) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + checkArgument(fromIndex >= 0 && toIndex >= fromIndex); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + int playlistSize = state.playlist.size(); + if (!shouldHandleCommand(Player.COMMAND_CHANGE_MEDIA_ITEMS) + || playlistSize == 0 + || fromIndex >= playlistSize) { + return; + } + int correctedToIndex = min(toIndex, playlistSize); + if (fromIndex == correctedToIndex) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleRemoveMediaItems(fromIndex, correctedToIndex), + /* placeholderStateSupplier= */ () -> { + ArrayList placeholderPlaylist = new ArrayList<>(state.playlist); + Util.removeRange(placeholderPlaylist, fromIndex, correctedToIndex); + return getStateWithNewPlaylist(state, placeholderPlaylist, period); + }); } @Override @@ -2141,8 +2228,21 @@ public abstract class SimpleBasePlayer extends BasePlayer { long positionMs, @Player.Command int seekCommand, boolean isRepeatingCurrentItem) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + checkArgument(mediaItemIndex >= 0); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!shouldHandleCommand(seekCommand) + || isPlayingAd() + || (!state.playlist.isEmpty() && mediaItemIndex >= state.playlist.size())) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSeek(mediaItemIndex, positionMs, seekCommand), + /* placeholderStateSupplier= */ () -> + getStateWithNewPlaylistAndPosition(state, state.playlist, mediaItemIndex, positionMs), + /* seeked= */ true, + isRepeatingCurrentItem); } @Override @@ -2617,7 +2717,8 @@ public abstract class SimpleBasePlayer extends BasePlayer { if (!pendingOperations.isEmpty() || released) { return; } - updateStateAndInformListeners(getState()); + updateStateAndInformListeners( + getState(), /* seeked= */ false, /* isRepeatingCurrentItem= */ false); } /** @@ -2653,6 +2754,26 @@ public abstract class SimpleBasePlayer extends BasePlayer { return suggestedPlaceholderState; } + /** + * Returns the placeholder {@link MediaItemData} used for a new {@link MediaItem} added to the + * playlist. + * + *

An implementation only needs to override this method if it can determine a more accurate + * placeholder state than the default. + * + * @param mediaItem The {@link MediaItem} added to the playlist. + * @return The {@link MediaItemData} used as placeholder while adding the item to the playlist is + * in progress. + */ + @ForOverride + protected MediaItemData getPlaceholderMediaItemData(MediaItem mediaItem) { + return new MediaItemData.Builder(new PlaceholderUid()) + .setMediaItem(mediaItem) + .setIsDynamic(true) + .setIsPlaceholder(true) + .build(); + } + /** * Handles calls to {@link Player#setPlayWhenReady}, {@link Player#play} and {@link Player#pause}. * @@ -2877,6 +2998,101 @@ public abstract class SimpleBasePlayer extends BasePlayer { throw new IllegalStateException(); } + /** + * Handles calls to {@link Player#setMediaItem} and {@link Player#setMediaItems}. + * + *

Will only be called if {@link Player#COMMAND_SET_MEDIA_ITEM} or {@link + * Player#COMMAND_CHANGE_MEDIA_ITEMS} is available. If only {@link Player#COMMAND_SET_MEDIA_ITEM} + * is available, the list of media items will always contain exactly one item. + * + * @param mediaItems The media items to add. + * @param startIndex The index at which to start playback from, or {@link C#INDEX_UNSET} to start + * at the default item. + * @param startPositionMs The position in milliseconds to start playback from, or {@link + * C#TIME_UNSET} to start at the default position in the media item. + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#addMediaItem} and {@link Player#addMediaItems}. + * + *

Will only be called if {@link Player#COMMAND_CHANGE_MEDIA_ITEMS} is available. + * + * @param index The index at which to add the items. The index is in the range 0 <= {@code + * index} <= {@link #getMediaItemCount()}. + * @param mediaItems The media items to add. + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleAddMediaItems(int index, List mediaItems) { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#moveMediaItem} and {@link Player#moveMediaItems}. + * + *

Will only be called if {@link Player#COMMAND_CHANGE_MEDIA_ITEMS} is available. + * + * @param fromIndex The start index of the items to move. The index is in the range 0 <= {@code + * fromIndex} < {@link #getMediaItemCount()}. + * @param toIndex The index of the first item not to be included in the move (exclusive). The + * index is in the range {@code fromIndex} < {@code toIndex} <= {@link + * #getMediaItemCount()}. + * @param newIndex The new index of the first moved item. The index is in the range {@code 0} + * <= {@code newIndex} < {@link #getMediaItemCount() - (toIndex - fromIndex)}. + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleMoveMediaItems(int fromIndex, int toIndex, int newIndex) { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#removeMediaItem} and {@link Player#removeMediaItems}. + * + *

Will only be called if {@link Player#COMMAND_CHANGE_MEDIA_ITEMS} is available. + * + * @param fromIndex The index at which to start removing media items. The index is in the range 0 + * <= {@code fromIndex} < {@link #getMediaItemCount()}. + * @param toIndex The index of the first item to be kept (exclusive). The index is in the range + * {@code fromIndex} < {@code toIndex} <= {@link #getMediaItemCount()}. + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleRemoveMediaItems(int fromIndex, int toIndex) { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#seekTo} and other seek operations (for example, {@link + * Player#seekToNext}). + * + *

Will only be called if the appropriate {@link Player.Command}, for example {@link + * Player#COMMAND_SEEK_TO_MEDIA_ITEM} or {@link Player#COMMAND_SEEK_TO_NEXT}, is available. + * + * @param mediaItemIndex The media item index to seek to. The index is in the range 0 <= {@code + * mediaItemIndex} < {@code mediaItems.size()}. + * @param positionMs The position in milliseconds to start playback from, or {@link C#TIME_UNSET} + * to start at the default position in the media item. + * @param seekCommand The {@link Player.Command} used to trigger the seek. + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + throw new IllegalStateException(); + } + @RequiresNonNull("state") private boolean shouldHandleCommand(@Player.Command int commandCode) { return !released && state.availableCommands.contains(commandCode); @@ -2884,7 +3100,8 @@ public abstract class SimpleBasePlayer extends BasePlayer { @SuppressWarnings("deprecation") // Calling deprecated listener methods. @RequiresNonNull("state") - private void updateStateAndInformListeners(State newState) { + private void updateStateAndInformListeners( + State newState, boolean seeked, boolean isRepeatingCurrentItem) { State previousState = state; // Assign new state immediately such that all getters return the right values, but use a // snapshot of the previous and new state so that listener invocations are triggered correctly. @@ -2906,10 +3123,11 @@ public abstract class SimpleBasePlayer extends BasePlayer { MediaMetadata previousMediaMetadata = getMediaMetadataInternal(previousState); MediaMetadata newMediaMetadata = getMediaMetadataInternal(newState); int positionDiscontinuityReason = - getPositionDiscontinuityReason(previousState, newState, window, period); + getPositionDiscontinuityReason(previousState, newState, seeked, window, period); boolean timelineChanged = !previousState.timeline.equals(newState.timeline); int mediaItemTransitionReason = - getMediaItemTransitionReason(previousState, newState, positionDiscontinuityReason, window); + getMediaItemTransitionReason( + previousState, newState, positionDiscontinuityReason, isRepeatingCurrentItem, window); if (timelineChanged) { @Player.TimelineChangeReason @@ -3093,7 +3311,7 @@ public abstract class SimpleBasePlayer extends BasePlayer { listeners.queueEvent( Player.EVENT_METADATA, listener -> listener.onMetadata(newState.timedMetadata)); } - if (false /* TODO: add flag to know when a seek request has been resolved */) { + if (positionDiscontinuityReason == Player.DISCONTINUITY_REASON_SEEK) { listeners.queueEvent(/* eventFlag= */ C.INDEX_UNSET, Listener::onSeekProcessed); } if (!previousState.availableCommands.equals(newState.availableCommands)) { @@ -3125,18 +3343,33 @@ public abstract class SimpleBasePlayer extends BasePlayer { @RequiresNonNull("state") private void updateStateForPendingOperation( ListenableFuture pendingOperation, Supplier placeholderStateSupplier) { + updateStateForPendingOperation( + pendingOperation, + placeholderStateSupplier, + /* seeked= */ false, + /* isRepeatingCurrentItem= */ false); + } + + @RequiresNonNull("state") + private void updateStateForPendingOperation( + ListenableFuture pendingOperation, + Supplier placeholderStateSupplier, + boolean seeked, + boolean isRepeatingCurrentItem) { if (pendingOperation.isDone() && pendingOperations.isEmpty()) { - updateStateAndInformListeners(getState()); + updateStateAndInformListeners(getState(), seeked, isRepeatingCurrentItem); } else { pendingOperations.add(pendingOperation); State suggestedPlaceholderState = placeholderStateSupplier.get(); - updateStateAndInformListeners(getPlaceholderState(suggestedPlaceholderState)); + updateStateAndInformListeners( + getPlaceholderState(suggestedPlaceholderState), seeked, isRepeatingCurrentItem); pendingOperation.addListener( () -> { castNonNull(state); // Already checked by method @RequiresNonNull pre-condition. pendingOperations.remove(pendingOperation); if (pendingOperations.isEmpty() && !released) { - updateStateAndInformListeners(getState()); + updateStateAndInformListeners( + getState(), /* seeked= */ false, /* isRepeatingCurrentItem= */ false); } }, this::postOrRunOnApplicationHandler); @@ -3221,7 +3454,11 @@ public abstract class SimpleBasePlayer extends BasePlayer { return Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED; } for (int i = 0; i < previousPlaylist.size(); i++) { - if (!previousPlaylist.get(i).uid.equals(newPlaylist.get(i).uid)) { + Object previousUid = previousPlaylist.get(i).uid; + Object newUid = newPlaylist.get(i).uid; + boolean resolvedAutoGeneratedPlaceholder = + previousUid instanceof PlaceholderUid && !(newUid instanceof PlaceholderUid); + if (!previousUid.equals(newUid) && !resolvedAutoGeneratedPlaceholder) { return Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED; } } @@ -3229,11 +3466,18 @@ public abstract class SimpleBasePlayer extends BasePlayer { } private static int getPositionDiscontinuityReason( - State previousState, State newState, Timeline.Window window, Timeline.Period period) { + State previousState, + State newState, + boolean seeked, + Timeline.Window window, + Timeline.Period period) { if (newState.hasPositionDiscontinuity) { // We were asked to report a discontinuity. return newState.positionDiscontinuityReason; } + if (seeked) { + return Player.DISCONTINUITY_REASON_SEEK; + } if (previousState.playlist.isEmpty()) { // First change from an empty playlist is not reported as a discontinuity. return C.INDEX_UNSET; @@ -3247,6 +3491,10 @@ public abstract class SimpleBasePlayer extends BasePlayer { getCurrentPeriodIndexInternal(previousState, window, period)); Object newPeriodUid = newState.timeline.getUidOfPeriod(getCurrentPeriodIndexInternal(newState, window, period)); + if (previousPeriodUid instanceof PlaceholderUid && !(newPeriodUid instanceof PlaceholderUid)) { + // An auto-generated placeholder was resolved to a real item. + return C.INDEX_UNSET; + } if (!newPeriodUid.equals(previousPeriodUid) || previousState.currentAdGroupIndex != newState.currentAdGroupIndex || previousState.currentAdIndexInAdGroup != newState.currentAdIndexInAdGroup) { @@ -3343,6 +3591,7 @@ public abstract class SimpleBasePlayer extends BasePlayer { State previousState, State newState, int positionDiscontinuityReason, + boolean isRepeatingCurrentItem, Timeline.Window window) { Timeline previousTimeline = previousState.timeline; Timeline newTimeline = newState.timeline; @@ -3356,6 +3605,10 @@ public abstract class SimpleBasePlayer extends BasePlayer { .uid; Object newWindowUid = newState.timeline.getWindow(getCurrentMediaItemIndexInternal(newState), window).uid; + if (previousWindowUid instanceof PlaceholderUid && !(newWindowUid instanceof PlaceholderUid)) { + // An auto-generated placeholder was resolved to a real item. + return C.INDEX_UNSET; + } if (!previousWindowUid.equals(newWindowUid)) { if (positionDiscontinuityReason == DISCONTINUITY_REASON_AUTO_TRANSITION) { return MEDIA_ITEM_TRANSITION_REASON_AUTO; @@ -3371,8 +3624,7 @@ public abstract class SimpleBasePlayer extends BasePlayer { && getContentPositionMsInternal(previousState) > getContentPositionMsInternal(newState)) { return MEDIA_ITEM_TRANSITION_REASON_REPEAT; } - if (positionDiscontinuityReason == DISCONTINUITY_REASON_SEEK - && /* TODO: mark repetition seeks to detect this case */ false) { + if (positionDiscontinuityReason == DISCONTINUITY_REASON_SEEK && isRepeatingCurrentItem) { return MEDIA_ITEM_TRANSITION_REASON_SEEK; } return C.INDEX_UNSET; @@ -3385,4 +3637,139 @@ public abstract class SimpleBasePlayer extends BasePlayer { Rect surfaceFrame = surfaceHolder.getSurfaceFrame(); return new Size(surfaceFrame.width(), surfaceFrame.height()); } + + private static int getMediaItemIndexInNewPlaylist( + List oldPlaylist, + Timeline newPlaylistTimeline, + int oldMediaItemIndex, + Timeline.Period period) { + if (oldPlaylist.isEmpty()) { + return oldMediaItemIndex < newPlaylistTimeline.getWindowCount() + ? oldMediaItemIndex + : C.INDEX_UNSET; + } + Object oldFirstPeriodUid = + oldPlaylist.get(oldMediaItemIndex).getPeriodUid(/* periodIndexInMediaItem= */ 0); + if (newPlaylistTimeline.getIndexOfPeriod(oldFirstPeriodUid) == C.INDEX_UNSET) { + return C.INDEX_UNSET; + } + return newPlaylistTimeline.getPeriodByUid(oldFirstPeriodUid, period).windowIndex; + } + + private static State getStateWithNewPlaylist( + State oldState, List newPlaylist, Timeline.Period period) { + State.Builder stateBuilder = oldState.buildUpon(); + stateBuilder.setPlaylist(newPlaylist); + Timeline newTimeline = stateBuilder.timeline; + long oldPositionMs = oldState.contentPositionMsSupplier.get(); + int oldIndex = getCurrentMediaItemIndexInternal(oldState); + int newIndex = getMediaItemIndexInNewPlaylist(oldState.playlist, newTimeline, oldIndex, period); + long newPositionMs = newIndex == C.INDEX_UNSET ? C.TIME_UNSET : oldPositionMs; + // If the current item no longer exists, try to find a matching subsequent item. + for (int i = oldIndex + 1; newIndex == C.INDEX_UNSET && i < oldState.playlist.size(); i++) { + // TODO: Use shuffle order to iterate. + newIndex = + getMediaItemIndexInNewPlaylist( + oldState.playlist, newTimeline, /* oldMediaItemIndex= */ i, period); + } + // If this fails, transition to ENDED state. + if (oldState.playbackState != Player.STATE_IDLE && newIndex == C.INDEX_UNSET) { + stateBuilder.setPlaybackState(Player.STATE_ENDED).setIsLoading(false); + } + return buildStateForNewPosition( + stateBuilder, + oldState, + oldPositionMs, + newPlaylist, + newIndex, + newPositionMs, + /* keepAds= */ true); + } + + private static State getStateWithNewPlaylistAndPosition( + State oldState, List newPlaylist, int newIndex, long newPositionMs) { + State.Builder stateBuilder = oldState.buildUpon(); + stateBuilder.setPlaylist(newPlaylist); + if (oldState.playbackState != Player.STATE_IDLE) { + if (newPlaylist.isEmpty()) { + stateBuilder.setPlaybackState(Player.STATE_ENDED).setIsLoading(false); + } else { + stateBuilder.setPlaybackState(Player.STATE_BUFFERING); + } + } + long oldPositionMs = oldState.contentPositionMsSupplier.get(); + return buildStateForNewPosition( + stateBuilder, + oldState, + oldPositionMs, + newPlaylist, + newIndex, + newPositionMs, + /* keepAds= */ false); + } + + private static State buildStateForNewPosition( + State.Builder stateBuilder, + State oldState, + long oldPositionMs, + List newPlaylist, + int newIndex, + long newPositionMs, + boolean keepAds) { + // Resolve unset or invalid index and position. + oldPositionMs = getPositionOrDefaultInMediaItem(oldPositionMs, oldState); + if (!newPlaylist.isEmpty() && (newIndex == C.INDEX_UNSET || newIndex >= newPlaylist.size())) { + newIndex = 0; // TODO: Use shuffle order to get first index. + newPositionMs = C.TIME_UNSET; + } + if (!newPlaylist.isEmpty() && newPositionMs == C.TIME_UNSET) { + newPositionMs = usToMs(newPlaylist.get(newIndex).defaultPositionUs); + } + boolean oldOrNewPlaylistEmpty = oldState.playlist.isEmpty() || newPlaylist.isEmpty(); + boolean mediaItemChanged = + !oldOrNewPlaylistEmpty + && !oldState + .playlist + .get(getCurrentMediaItemIndexInternal(oldState)) + .uid + .equals(newPlaylist.get(newIndex).uid); + if (oldOrNewPlaylistEmpty || mediaItemChanged || newPositionMs < oldPositionMs) { + // New item or seeking back. Assume no buffer and no ad playback persists. + stateBuilder + .setCurrentMediaItemIndex(newIndex) + .setCurrentAd(C.INDEX_UNSET, C.INDEX_UNSET) + .setContentPositionMs(newPositionMs) + .setContentBufferedPositionMs(PositionSupplier.getConstant(newPositionMs)) + .setTotalBufferedDurationMs(PositionSupplier.ZERO); + } else if (newPositionMs == oldPositionMs) { + // Unchanged position. Assume ad playback and buffer in current item persists. + stateBuilder.setCurrentMediaItemIndex(newIndex); + if (oldState.currentAdGroupIndex != C.INDEX_UNSET && keepAds) { + stateBuilder.setTotalBufferedDurationMs( + PositionSupplier.getConstant( + oldState.adBufferedPositionMsSupplier.get() - oldState.adPositionMsSupplier.get())); + } else { + stateBuilder + .setCurrentAd(C.INDEX_UNSET, C.INDEX_UNSET) + .setTotalBufferedDurationMs( + PositionSupplier.getConstant( + getContentBufferedPositionMsInternal(oldState) - oldPositionMs)); + } + } else { + // Seeking forward. Assume remaining buffer in current item persist, but no ad playback. + long contentBufferedDurationMs = + max(getContentBufferedPositionMsInternal(oldState), newPositionMs); + long totalBufferedDurationMs = + max(0, oldState.totalBufferedDurationMsSupplier.get() - (newPositionMs - oldPositionMs)); + stateBuilder + .setCurrentMediaItemIndex(newIndex) + .setCurrentAd(C.INDEX_UNSET, C.INDEX_UNSET) + .setContentPositionMs(newPositionMs) + .setContentBufferedPositionMs(PositionSupplier.getConstant(contentBufferedDurationMs)) + .setTotalBufferedDurationMs(PositionSupplier.getConstant(totalBufferedDurationMs)); + } + return stateBuilder.build(); + } + + private static final class PlaceholderUid {} } diff --git a/library/common/src/test/java/com/google/android/exoplayer2/SimpleBasePlayerTest.java b/library/common/src/test/java/com/google/android/exoplayer2/SimpleBasePlayerTest.java index 3cb6f57de4..4019f9668d 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/SimpleBasePlayerTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/SimpleBasePlayerTest.java @@ -25,6 +25,7 @@ import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -56,7 +57,9 @@ import com.google.common.util.concurrent.SettableFuture; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import org.junit.Ignore; import org.junit.Test; @@ -1400,6 +1403,7 @@ public class SimpleBasePlayerTest { /* adIndexInAdGroup= */ C.INDEX_UNSET), Player.DISCONTINUITY_REASON_SEEK); verify(listener).onMediaItemTransition(mediaItem1, Player.MEDIA_ITEM_TRANSITION_REASON_SEEK); + verify(listener).onSeekProcessed(); verify(listener) .onEvents( player, @@ -1439,9 +1443,6 @@ public class SimpleBasePlayerTest { verifyNoMoreInteractions(listener); // Assert that we actually called all listeners. for (Method method : Player.Listener.class.getDeclaredMethods()) { - if (method.getName().equals("onSeekProcessed")) { - continue; - } if (method.getName().equals("onAudioSessionIdChanged") || method.getName().equals("onSkipSilenceEnabledChanged")) { // Skip listeners for ExoPlayer-specific states @@ -3817,6 +3818,3545 @@ public class SimpleBasePlayerTest { assertThat(callForwarded.get()).isFalse(); } + @Test + public void addMediaItems_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 handleAddMediaItems(int index, List mediaItems) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.addMediaItems( + /* index= */ 1, + ImmutableList.of( + new MediaItem.Builder().setMediaId("3").build(), + new MediaItem.Builder().setMediaId("4").build())); + + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + } + + @Test + public void addMediaItems_asyncHandling_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())) + .setCurrentMediaItemIndex(1) + .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())) + .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 handleAddMediaItems(int index, List mediaItems) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.addMediaItems( + /* index= */ 1, + ImmutableList.of( + new MediaItem.Builder().setMediaId("3").build(), + new MediaItem.Builder().setMediaId("4").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("3"); + assertThat(window.isPlaceholder).isTrue(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 2, window); + assertThat(window.mediaItem.mediaId).isEqualTo("4"); + assertThat(window.isPlaceholder).isTrue(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 3, window); + assertThat(window.uid).isEqualTo(2); + 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); + } + + @Test + public void + addMediaItems_asyncHandlingWhileAdIsPlaying_usesPlaceholderStateAndInformsListeners() { + SimpleBasePlayer.PeriodData adPeriodData = + new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()) + .setAdPlaybackState( + new AdPlaybackState(/* adsId= */ new Object(), /* adGroupTimesUs...= */ 123)) + .build(); + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1) + .setPeriods(ImmutableList.of(adPeriodData)) + .build())) + .setCurrentAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1) + .setPeriods(ImmutableList.of(adPeriodData)) + .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 handleAddMediaItems(int index, List mediaItems) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.addMediaItem(/* index= */ 0, new MediaItem.Builder().setMediaId("id").build()); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentAdGroupIndex()).isEqualTo(0); + assertThat(player.getCurrentAdIndexInAdGroup()).isEqualTo(0); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.mediaItem.mediaId).isEqualTo("id"); + assertThat(window.isPlaceholder).isTrue(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.uid).isEqualTo(1); + 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(1); + assertThat(player.getCurrentAdGroupIndex()).isEqualTo(0); + assertThat(player.getCurrentAdIndexInAdGroup()).isEqualTo(0); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @Test + public void addMediaItems_asyncHandlingFromEmpty_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setContentPositionMs(5000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleAddMediaItems(int index, List mediaItems) { + return future; + } + + @Override + protected MediaItemData getPlaceholderMediaItemData(MediaItem mediaItem) { + return super.getPlaceholderMediaItemData(mediaItem) + .buildUpon() + .setDefaultPositionUs(5_000_000) + .build(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("3").build(); + + player.addMediaItems( + ImmutableList.of(newMediaItem, new MediaItem.Builder().setMediaId("2").build())); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(5000); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.mediaItem.mediaId).isEqualTo("3"); + 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(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(5000); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @Test + public void + addMediaItems_asyncHandlingFromEmptyWithPreviouslySetPosition_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleAddMediaItems(int index, List mediaItems) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("2").build(); + + player.addMediaItems( + ImmutableList.of(new MediaItem.Builder().setMediaId("3").build(), newMediaItem)); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.mediaItem.mediaId).isEqualTo("3"); + 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(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @Test + public void + addMediaItems_asyncHandlingFromEmptyWithPreviouslySetPositionExceedingNewPlaylistSize_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setCurrentMediaItemIndex(5000) + .setContentPositionMs(3000) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(0) + .setContentPositionMs(1000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleAddMediaItems(int index, List mediaItems) { + return future; + } + + @Override + protected MediaItemData getPlaceholderMediaItemData(MediaItem mediaItem) { + return super.getPlaceholderMediaItemData(mediaItem) + .buildUpon() + .setDefaultPositionUs(1_000_000) + .build(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("3").build(); + + player.addMediaItems( + ImmutableList.of(newMediaItem, new MediaItem.Builder().setMediaId("2").build())); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(1000); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.mediaItem.mediaId).isEqualTo("3"); + 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(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(1000); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @Test + public void + addMediaItems_asyncHandlingFromEmptyWithPreviouslySetIndexAndDefaultPosition_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setCurrentMediaItemIndex(1) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setContentPositionMs(5000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleAddMediaItems(int index, List mediaItems) { + return future; + } + + @Override + protected MediaItemData getPlaceholderMediaItemData(MediaItem mediaItem) { + return super.getPlaceholderMediaItemData(mediaItem) + .buildUpon() + .setDefaultPositionUs(5_000_000) + .build(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("2").build(); + + player.addMediaItems( + ImmutableList.of(new MediaItem.Builder().setMediaId("3").build(), newMediaItem)); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(5000); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.mediaItem.mediaId).isEqualTo("3"); + 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(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(5000); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @Test + public void addMediaItems_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_CHANGE_MEDIA_ITEMS) + .build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleAddMediaItems(int index, List mediaItems) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.addMediaItem(new MediaItem.Builder().setMediaId("id").build()); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void addMediaItems_withInvalidIndex_addsToEndOfPlaylist() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of(new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build())) + .build(); + AtomicInteger indexInHandleMethod = new AtomicInteger(C.INDEX_UNSET); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleAddMediaItems(int index, List mediaItems) { + indexInHandleMethod.set(index); + return SettableFuture.create(); + } + }; + + player.addMediaItem(/* index= */ 5000, new MediaItem.Builder().setMediaId("new").build()); + + assertThat(indexInHandleMethod.get()).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); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.mediaItem.mediaId).isEqualTo("new"); + } + + @Test + public void moveMediaItems_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(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build())) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build())) + .setCurrentMediaItemIndex(2) + .build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleMoveMediaItems( + int fromIndex, int toIndex, int newIndex) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3, /* newIndex= */ 0); + + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + } + + @Test + public void moveMediaItems_asyncHandling_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())) + .build(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 4).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).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 handleMoveMediaItems( + int fromIndex, int toIndex, int newIndex) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3, /* newIndex= */ 0); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(2); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(3); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.uid).isEqualTo(2); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.uid).isEqualTo(3); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 2, window); + assertThat(window.uid).isEqualTo(1); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_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_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + } + + @Test + public void + moveMediaItems_asyncHandlingWhileAdIsPlaying_usesPlaceholderStateAndInformsListeners() { + SimpleBasePlayer.PeriodData adPeriodData = + new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()) + .setAdPlaybackState( + new AdPlaybackState(/* adsId= */ new Object(), /* adGroupTimesUs...= */ 123)) + .build(); + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1) + .setPeriods(ImmutableList.of(adPeriodData)) + .build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build())) + .setCurrentAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0) + .build(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 4).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1) + .setPeriods(ImmutableList.of(adPeriodData)) + .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 handleMoveMediaItems( + int fromIndex, int toIndex, int newIndex) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3, /* newIndex= */ 0); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentAdGroupIndex()).isEqualTo(0); + assertThat(player.getCurrentAdIndexInAdGroup()).isEqualTo(0); + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(2); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(3); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.uid).isEqualTo(2); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.uid).isEqualTo(3); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 2, window); + assertThat(window.uid).isEqualTo(1); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentAdGroupIndex()).isEqualTo(0); + assertThat(player.getCurrentAdIndexInAdGroup()).isEqualTo(0); + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(2); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + } + + @Test + public void moveMediaItems_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(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleMoveMediaItems( + int fromIndex, int toIndex, int newIndex) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 2, /* newIndex= */ 0); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void moveMediaItems_withInvalidIndices_usesValidIndexRange() { + 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())) + .build(); + AtomicInteger fromIndexInHandleMethod = new AtomicInteger(C.INDEX_UNSET); + AtomicInteger toIndexInHandleMethod = new AtomicInteger(C.INDEX_UNSET); + AtomicInteger newIndexInHandleMethod = new AtomicInteger(C.INDEX_UNSET); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleMoveMediaItems( + int fromIndex, int toIndex, int newIndex) { + fromIndexInHandleMethod.set(fromIndex); + toIndexInHandleMethod.set(toIndex); + newIndexInHandleMethod.set(newIndex); + return SettableFuture.create(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 2500, /* newIndex= */ 0); + assertThat(fromIndexInHandleMethod.get()).isEqualTo(1); + assertThat(toIndexInHandleMethod.get()).isEqualTo(3); + assertThat(newIndexInHandleMethod.get()).isEqualTo(0); + + player.moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 2, /* newIndex= */ 6000); + assertThat(fromIndexInHandleMethod.get()).isEqualTo(0); + assertThat(toIndexInHandleMethod.get()).isEqualTo(2); + assertThat(newIndexInHandleMethod.get()).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); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.uid).isEqualTo(2); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 2, window); + assertThat(window.uid).isEqualTo(3); + verify(listener, times(2)) + .onTimelineChanged(any(), eq(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)); + verifyNoMoreInteractions(listener); + } + + @Test + public void removeMediaItems_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(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 4).build())) + .setCurrentMediaItemIndex(3) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 4).build())) + .setCurrentMediaItemIndex(1) + .build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleRemoveMediaItems(int fromIndex, int toIndex) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.removeMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3); + + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + } + + @Test + public void removeMediaItems_asyncHandling_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(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 4).build())) + .setCurrentMediaItemIndex(3) + .build(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 5).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 4).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 handleRemoveMediaItems(int fromIndex, int toIndex) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.removeMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3); + + // 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); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.uid).isEqualTo(4); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_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_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + } + + @Test + public void + removeMediaItems_asyncHandlingWhileAdIsPlaying_usesPlaceholderStateAndInformsListeners() { + SimpleBasePlayer.PeriodData adPeriodData = + new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()) + .setAdPlaybackState( + new AdPlaybackState(/* adsId= */ new Object(), /* adGroupTimesUs...= */ 123)) + .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).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 4) + .setPeriods(ImmutableList.of(adPeriodData)) + .build())) + .setCurrentAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0) + .setCurrentMediaItemIndex(3) + .build(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 5).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 4) + .setPeriods(ImmutableList.of(adPeriodData)) + .build())) + .setCurrentMediaItemIndex(1) + .setCurrentAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleRemoveMediaItems(int fromIndex, int toIndex) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.removeMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentAdGroupIndex()).isEqualTo(0); + assertThat(player.getCurrentAdIndexInAdGroup()).isEqualTo(0); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.uid).isEqualTo(1); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.uid).isEqualTo(4); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentAdGroupIndex()).isEqualTo(0); + assertThat(player.getCurrentAdIndexInAdGroup()).isEqualTo(0); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Testing deprecated listener call. + @Test + public void + removeMediaItems_asyncHandlingRemovingCurrentItemWithSubsequentMatch_usesPlaceholderStateAndInformsListeners() { + MediaItem lastMediaItem = new MediaItem.Builder().setMediaId("id").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).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 4) + .setMediaItem(lastMediaItem) + .build())) + .setCurrentMediaItemIndex(1) + .build(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 5).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 4) + .setMediaItem(lastMediaItem) + .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 handleRemoveMediaItems(int fromIndex, int toIndex) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.removeMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3); + + // 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); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 1, window); + assertThat(window.uid).isEqualTo(4); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE)); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_REMOVE); + verify(listener) + .onMediaItemTransition(lastMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_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_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Testing deprecated listener call. + @Test + public void + removeMediaItems_asyncHandlingRemovingCurrentItemWithoutSubsequentMatch_usesPlaceholderStateAndInformsListeners() { + MediaItem firstMediaItem = new MediaItem.Builder().setMediaId("id").build(); + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1) + .setMediaItem(firstMediaItem) + .build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build())) + .setCurrentMediaItemIndex(1) + .setPlaybackState(Player.STATE_READY) + .build(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1) + .setMediaItem(firstMediaItem) + .build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 5).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 handleRemoveMediaItems(int fromIndex, int toIndex) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.removeMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3); + + // 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); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE)); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_REMOVE); + verify(listener) + .onMediaItemTransition( + firstMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_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_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Testing deprecated listener call. + @Test + public void + removeMediaItems_asyncHandlingRemovingEntirePlaylist_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(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = + state + .buildUpon() + .setPlaylist(ImmutableList.of()) + .setCurrentMediaItemIndex(C.INDEX_UNSET) + .setRepeatMode(Player.REPEAT_MODE_ALL) + .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 handleRemoveMediaItems(int fromIndex, int toIndex) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.clearMediaItems(); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE)); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_REMOVE); + verify(listener) + .onMediaItemTransition( + /* mediaItem= */ null, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verify(listener) + .onTimelineChanged( + player.getCurrentTimeline(), Player.TIMELINE_CHANGE_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).onRepeatModeChanged(Player.REPEAT_MODE_ALL); + verifyNoMoreInteractions(listener); + } + + @Test + public void removeMediaItems_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 handleRemoveMediaItems(int fromIndex, int toIndex) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.removeMediaItem(/* index= */ 0); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void removeMediaItems_withInvalidIndex_removesToEndOfPlaylist() { + 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 handleRemoveMediaItems(int fromIndex, int toIndex) { + fromIndexInHandleMethod.set(fromIndex); + toIndexInHandleMethod.set(toIndex); + return SettableFuture.create(); + } + }; + + player.removeMediaItems(/* fromIndex= */ 1, /* toIndex= */ 5000); + + assertThat(fromIndexInHandleMethod.get()).isEqualTo(1); + assertThat(toIndexInHandleMethod.get()).isEqualTo(2); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(1); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.uid).isEqualTo(1); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void setMediaItems_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(); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("new").build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3) + .setMediaItem(newMediaItem) + .build())) + .setCurrentMediaItemIndex(1) + .build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setMediaItems( + ImmutableList.of( + new MediaItem.Builder().setMediaId("2").build(), + new MediaItem.Builder().setMediaId("3").build())); + + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_REMOVE); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE)); + verify(listener) + .onMediaItemTransition(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void + setMediaItems_asyncHandlingWithIndexAndPosition_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())) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("2").build(); + + player.setMediaItems( + ImmutableList.of(new MediaItem.Builder().setMediaId("3").build(), newMediaItem), + /* startIndex= */ 1, + /* startPositionMs= */ 3000); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.mediaItem.mediaId).isEqualTo("3"); + 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(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_REMOVE); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE)); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @Test + public void + setMediaItems_asyncHandlingWithIndexAndPositionFromEmpty_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("2").build(); + + player.setMediaItems( + ImmutableList.of(new MediaItem.Builder().setMediaId("1").build(), newMediaItem), + /* startIndex= */ 1, + /* startPositionMs= */ 3000); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + 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(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @Test + public void + setMediaItems_asyncHandlingWithIndexAndDefaultPosition_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + + @Override + protected MediaItemData getPlaceholderMediaItemData(MediaItem mediaItem) { + return super.getPlaceholderMediaItemData(mediaItem) + .buildUpon() + .setDefaultPositionUs(3_000_000) + .build(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("2").build(); + + player.setMediaItems( + ImmutableList.of(new MediaItem.Builder().setMediaId("1").build(), newMediaItem), + /* startIndex= */ 1, + /* startPositionMs= */ C.TIME_UNSET); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + 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(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void + setMediaItems_asyncHandlingWithEmptyPlaylistAndIndexAndPosition_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())) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist(ImmutableList.of()) + .setCurrentMediaItemIndex(20) + .setContentPositionMs(3000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setMediaItems(ImmutableList.of(), /* startIndex= */ 20, /* startPositionMs= */ 3000); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(20); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verify(listener) + .onTimelineChanged(Timeline.EMPTY, 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= */ null, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(20); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void + setMediaItems_asyncHandlingWithEmptyPlaylistAndIndexAndDefaultPosition_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())) + .build(); + State updatedState = + state.buildUpon().setPlaylist(ImmutableList.of()).setCurrentMediaItemIndex(20).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setMediaItems( + ImmutableList.of(), /* startIndex= */ 20, /* startPositionMs= */ C.TIME_UNSET); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(20); + assertThat(player.getCurrentPosition()).isEqualTo(0); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verify(listener) + .onTimelineChanged(Timeline.EMPTY, 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= */ null, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(20); + assertThat(player.getCurrentPosition()).isEqualTo(0); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void setMediaItems_asyncHandlingWithResetTrue_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())) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(0) + .setContentPositionMs(5000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + + @Override + protected MediaItemData getPlaceholderMediaItemData(MediaItem mediaItem) { + return super.getPlaceholderMediaItemData(mediaItem) + .buildUpon() + .setDefaultPositionUs(5_000_000) + .build(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("3").build(); + + player.setMediaItems( + ImmutableList.of(newMediaItem, new MediaItem.Builder().setMediaId("2").build()), + /* resetPosition= */ true); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(5000); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.mediaItem.mediaId).isEqualTo("3"); + 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(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_REMOVE); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE)); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(5000); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @Test + public void + setMediaItems_asyncHandlingWithResetTrueFromEmpty_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(0) + .setContentPositionMs(5000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + + @Override + protected MediaItemData getPlaceholderMediaItemData(MediaItem mediaItem) { + return super.getPlaceholderMediaItemData(mediaItem) + .buildUpon() + .setDefaultPositionUs(5_000_000) + .build(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("3").build(); + + player.setMediaItems( + ImmutableList.of(newMediaItem, new MediaItem.Builder().setMediaId("2").build()), + /* resetPosition= */ true); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(5000); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.mediaItem.mediaId).isEqualTo("3"); + 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(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(5000); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void + setMediaItems_asyncHandlingWithResetTrueToEmpty_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())) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist(ImmutableList.of()) + .setCurrentMediaItemIndex(C.INDEX_UNSET) + .setContentPositionMs(C.TIME_UNSET) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setMediaItems(ImmutableList.of(), /* resetPosition= */ true); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(0); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verify(listener) + .onTimelineChanged(Timeline.EMPTY, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verify(listener) + .onMediaItemTransition( + /* mediaItem= */ null, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_REMOVE); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE)); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(0); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verifyNoMoreInteractions(listener); + } + + @Test + public void + setMediaItems_asyncHandlingWithResetTrueFromEmptyToEmpty_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + State updatedState = + state + .buildUpon() + .setCurrentMediaItemIndex(C.INDEX_UNSET) + .setContentPositionMs(C.TIME_UNSET) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setMediaItems(ImmutableList.of(), /* resetPosition= */ true); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(0); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(0); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void setMediaItems_asyncHandlingWithResetFalse_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())) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("2").build(); + + player.setMediaItems( + ImmutableList.of(new MediaItem.Builder().setMediaId("3").build(), newMediaItem), + /* resetPosition= */ false); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.mediaItem.mediaId).isEqualTo("3"); + 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(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_REMOVE); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE)); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @Test + public void + setMediaItems_asyncHandlingWithResetFalseFromEmptyWithSetPosition_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("2").build(); + + player.setMediaItems( + ImmutableList.of(new MediaItem.Builder().setMediaId("3").build(), newMediaItem), + /* resetPosition= */ false); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.mediaItem.mediaId).isEqualTo("3"); + 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(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @Test + public void + setMediaItems_asyncHandlingWithResetFalseFromEmptyWithSetPositionExceedingPlaylistSize_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setCurrentMediaItemIndex(5000) + .setContentPositionMs(3000) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(0) + .setContentPositionMs(1000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + + @Override + protected MediaItemData getPlaceholderMediaItemData(MediaItem mediaItem) { + return super.getPlaceholderMediaItemData(mediaItem) + .buildUpon() + .setDefaultPositionUs(1_000_000) + .build(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("3").build(); + + player.setMediaItems( + ImmutableList.of(newMediaItem, new MediaItem.Builder().setMediaId("2").build()), + /* resetPosition= */ false); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(1000); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.mediaItem.mediaId).isEqualTo("3"); + 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(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(1000); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @Test + public void + setMediaItems_asyncHandlingWithResetFalseFromEmptyWithIndexAndDefaultPosition_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setCurrentMediaItemIndex(1) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setContentPositionMs(5000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + + @Override + protected MediaItemData getPlaceholderMediaItemData(MediaItem mediaItem) { + return super.getPlaceholderMediaItemData(mediaItem) + .buildUpon() + .setDefaultPositionUs(5_000_000) + .build(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("2").build(); + + player.setMediaItems( + ImmutableList.of(new MediaItem.Builder().setMediaId("3").build(), newMediaItem), + /* resetPosition= */ false); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(5000); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.mediaItem.mediaId).isEqualTo("3"); + 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(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(5000); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @Test + public void + setMediaItems_asyncHandlingWithResetFalseFromEmptyWithDefaultIndexAndPosition_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 3).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setContentPositionMs(5000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + + @Override + protected MediaItemData getPlaceholderMediaItemData(MediaItem mediaItem) { + return super.getPlaceholderMediaItemData(mediaItem) + .buildUpon() + .setDefaultPositionUs(5_000_000) + .build(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("3").build(); + + player.setMediaItems( + ImmutableList.of(newMediaItem, new MediaItem.Builder().setMediaId("2").build()), + /* resetPosition= */ false); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(5000); + assertThat(player.getCurrentTimeline().getWindowCount()).isEqualTo(2); + Timeline.Window window = new Timeline.Window(); + player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, window); + assertThat(window.mediaItem.mediaId).isEqualTo("3"); + 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(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(5000); + assertThat(player.getCurrentTimeline()).isEqualTo(updatedState.timeline); + verify(listener) + .onTimelineChanged(updatedState.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void + setMediaItems_asyncHandlingWithResetFalseToEmpty_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())) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + State updatedState = + state + .buildUpon() + .setPlaylist(ImmutableList.of()) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setMediaItems(ImmutableList.of(), /* resetPosition= */ false); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verify(listener) + .onTimelineChanged(Timeline.EMPTY, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verify(listener) + .onMediaItemTransition( + /* mediaItem= */ null, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_REMOVE); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE)); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verifyNoMoreInteractions(listener); + } + + @Test + public void + setMediaItems_asyncHandlingWithResetFalseFromEmptyToEmptyWithSetPosition_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(3000) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setMediaItems(ImmutableList.of(), /* resetPosition= */ false); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verifyNoMoreInteractions(listener); + } + + @Test + public void + setMediaItems_asyncHandlingWithResetFalseFromEmptyToEmptyWithIndexAndDefaultPosition_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setCurrentMediaItemIndex(1) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setMediaItems(ImmutableList.of(), /* resetPosition= */ false); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(0); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(0); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verifyNoMoreInteractions(listener); + } + + @Test + public void + setMediaItems_asyncHandlingWithResetFalseFromEmptyToEmptyWithDefaultIndexAndPosition_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setMediaItems(ImmutableList.of(), /* resetPosition= */ false); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(0); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(0); + assertThat(player.getCurrentTimeline().isEmpty()).isTrue(); + verifyNoMoreInteractions(listener); + } + + @Test + public void setMediaItems_withoutAvailableCommandForEmptyPlaylist_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 handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.setMediaItems(ImmutableList.of()); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void setMediaItems_withoutAvailableCommandForSingleItemPlaylist_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .removeAll(Player.COMMAND_CHANGE_MEDIA_ITEMS, Player.COMMAND_SET_MEDIA_ITEM) + .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 handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.setMediaItems(ImmutableList.of(new MediaItem.Builder().setMediaId("new").build())); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void setMediaItems_withJustSetMediaItemCommandForSingleItemPlaylist_isForwarded() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().add(Player.COMMAND_SET_MEDIA_ITEM).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 handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.setMediaItems(ImmutableList.of(new MediaItem.Builder().setMediaId("new").build())); + + assertThat(callForwarded.get()).isTrue(); + } + + @Test + public void setMediaItems_withJustChangeMediaItemsCommandForSingleItemPlaylist_isForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder().add(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 handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.setMediaItems(ImmutableList.of(new MediaItem.Builder().setMediaId("new").build())); + + assertThat(callForwarded.get()).isTrue(); + } + + @Test + public void setMediaItems_withoutAvailableCommandForMultiItemPlaylist_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 handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.setMediaItems( + ImmutableList.of( + new MediaItem.Builder().setMediaId("1").build(), + new MediaItem.Builder().setMediaId("2").build())); + + assertThat(callForwarded.get()).isFalse(); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void seekTo_immediateHandling_updatesStateAndInformsListeners() { + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("2").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) + .setMediaItem(newMediaItem) + .build())) + .build(); + State updatedState = + state.buildUpon().setCurrentMediaItemIndex(1).setContentPositionMs(3000).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 3000); + + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK)); + verify(listener).onMediaItemTransition(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_SEEK); + verify(listener).onSeekProcessed(); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void seekTo_asyncHandlingWithIndexAndPosition_usesPlaceholderStateAndInformsListeners() { + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("2").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) + .setMediaItem(newMediaItem) + .build())) + .setContentPositionMs(8000) + .setContentBufferedPositionMs(SimpleBasePlayer.PositionSupplier.getConstant(10000)) + .setTotalBufferedDurationMs(SimpleBasePlayer.PositionSupplier.getConstant(2000)) + .build(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = + state.buildUpon().setCurrentMediaItemIndex(1).setContentPositionMs(3005).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 3000); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getBufferedPosition()).isEqualTo(3000); + assertThat(player.getTotalBufferedDuration()).isEqualTo(0); + verify(listener).onMediaItemTransition(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_SEEK); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK)); + verify(listener).onSeekProcessed(); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3005); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void + seekTo_asyncHandlingWithIndexAndDefaultPosition_usesPlaceholderStateAndInformsListeners() { + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("2").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) + .setMediaItem(newMediaItem) + .setDefaultPositionUs(3_000_000) + .build())) + .setContentPositionMs(8000) + .setContentBufferedPositionMs(SimpleBasePlayer.PositionSupplier.getConstant(10000)) + .setTotalBufferedDurationMs(SimpleBasePlayer.PositionSupplier.getConstant(2000)) + .build(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = + state.buildUpon().setCurrentMediaItemIndex(1).setContentPositionMs(3005).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ C.TIME_UNSET); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getBufferedPosition()).isEqualTo(3000); + assertThat(player.getTotalBufferedDuration()).isEqualTo(0); + verify(listener).onMediaItemTransition(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_SEEK); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK)); + verify(listener).onSeekProcessed(); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3005); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void + seekTo_asyncHandlingWithIndexAndPositionAndEmptyPlaylist_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist(ImmutableList.of()) + .setContentPositionMs(8000) + .setContentBufferedPositionMs(SimpleBasePlayer.PositionSupplier.getConstant(10000)) + .setTotalBufferedDurationMs(SimpleBasePlayer.PositionSupplier.getConstant(2000)) + .build(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = + state.buildUpon().setCurrentMediaItemIndex(1).setContentPositionMs(3005).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 3000); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getBufferedPosition()).isEqualTo(3000); + assertThat(player.getTotalBufferedDuration()).isEqualTo(0); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK)); + verify(listener).onSeekProcessed(); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(3005); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void + seekTo_asyncHandlingWithIndexAndDefaultPositionAndEmptyPlaylist_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist(ImmutableList.of()) + .setContentPositionMs(8000) + .setContentBufferedPositionMs(SimpleBasePlayer.PositionSupplier.getConstant(10000)) + .setTotalBufferedDurationMs(SimpleBasePlayer.PositionSupplier.getConstant(2000)) + .build(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = + state.buildUpon().setCurrentMediaItemIndex(1).setContentPositionMs(100).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ C.TIME_UNSET); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(0); + assertThat(player.getBufferedPosition()).isEqualTo(0); + assertThat(player.getTotalBufferedDuration()).isEqualTo(0); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK)); + verify(listener).onSeekProcessed(); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPosition()).isEqualTo(100); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void + seekTo_asyncHandlingWithSeekBackInCurrentItem_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of(new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build())) + .setContentPositionMs(8000) + .setContentBufferedPositionMs(SimpleBasePlayer.PositionSupplier.getConstant(10000)) + .setTotalBufferedDurationMs(SimpleBasePlayer.PositionSupplier.getConstant(2000)) + .build(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = state.buildUpon().setContentPositionMs(3005).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.seekTo(/* positionMs= */ 3000); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getBufferedPosition()).isEqualTo(3000); + assertThat(player.getTotalBufferedDuration()).isEqualTo(0); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK)); + verify(listener).onSeekProcessed(); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(3005); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void + seekTo_asyncHandlingWithSeekToCurrentPosition_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of(new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build())) + .setContentPositionMs(3000) + .setContentBufferedPositionMs(SimpleBasePlayer.PositionSupplier.getConstant(10000)) + .setTotalBufferedDurationMs(SimpleBasePlayer.PositionSupplier.getConstant(7000)) + .build(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = state.buildUpon().setContentPositionMs(3005).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.seekTo(/* positionMs= */ 3000); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(3000); + assertThat(player.getBufferedPosition()).isEqualTo(10000); + assertThat(player.getTotalBufferedDuration()).isEqualTo(7000); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK)); + verify(listener).onSeekProcessed(); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(3005); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void + seekTo_asyncHandlingWithSeekForwardInCurrentItem_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of(new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build())) + .setContentPositionMs(3000) + .setContentBufferedPositionMs(SimpleBasePlayer.PositionSupplier.getConstant(10000)) + .setTotalBufferedDurationMs(SimpleBasePlayer.PositionSupplier.getConstant(7000)) + .build(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = state.buildUpon().setContentPositionMs(7005).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.seekTo(/* positionMs= */ 7000); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(7000); + assertThat(player.getBufferedPosition()).isEqualTo(10000); + assertThat(player.getTotalBufferedDuration()).isEqualTo(3000); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK)); + verify(listener).onSeekProcessed(); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(7005); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void + seekTo_asyncHandlingWithRepeatOfCurrentItem_usesPlaceholderStateAndInformsListeners() { + MediaItem mediaItem = new MediaItem.Builder().setMediaId("id").build(); + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1) + .setMediaItem(mediaItem) + .build())) + .setContentPositionMs(8000) + .setContentBufferedPositionMs(SimpleBasePlayer.PositionSupplier.getConstant(10000)) + .setTotalBufferedDurationMs(SimpleBasePlayer.PositionSupplier.getConstant(2000)) + .setRepeatMode(Player.REPEAT_MODE_ALL) + .build(); + // Change updated state slightly to see a difference to the placeholder state. + State updatedState = state.buildUpon().setContentPositionMs(5).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.seekToNext(); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(0); + assertThat(player.getBufferedPosition()).isEqualTo(0); + assertThat(player.getTotalBufferedDuration()).isEqualTo(0); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK)); + verify(listener).onMediaItemTransition(mediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_SEEK); + verify(listener).onSeekProcessed(); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0); + assertThat(player.getCurrentPosition()).isEqualTo(5); + verifyNoMoreInteractions(listener); + } + + @Test + public void seekTo_withoutAvailableCommandForSeekToMediaItem_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SEEK_TO_MEDIA_ITEM) + .build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 4000); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void seekTo_withoutAvailableCommandForSeekInCurrentMediaItem_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM) + .build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.seekTo(/* positionMs= */ 4000); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void seekToDefaultPosition_withoutAvailableCommandForSeekToMediaItem_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SEEK_TO_MEDIA_ITEM) + .build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.seekToDefaultPosition(/* mediaItemIndex= */ 1); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void + seekToDefaultPosition_withoutAvailableCommandForSeekToDefaultPosition_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SEEK_TO_DEFAULT_POSITION) + .build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.seekToDefaultPosition(); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void seekBack_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder().addAllCommands().remove(Player.COMMAND_SEEK_BACK).build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(1) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.seekBack(); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void seekToPrevious_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SEEK_TO_PREVIOUS) + .build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(1) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.seekToPrevious(); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void seekToPreviousMediaItem_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) + .build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .setCurrentMediaItemIndex(1) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.seekToPreviousMediaItem(); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void seekForward_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder().addAllCommands().remove(Player.COMMAND_SEEK_FORWARD).build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.seekForward(); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void seekToNext_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder().addAllCommands().remove(Player.COMMAND_SEEK_TO_NEXT).build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.seekToNext(); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void seekToNextMediaItem_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) + .build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2).build())) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.seekToNextMediaItem(); + + assertThat(callForwarded.get()).isFalse(); + } + private static Object[] getAnyArguments(Method method) { Object[] arguments = new Object[method.getParameterCount()]; Class[] argumentTypes = method.getParameterTypes();