From 2bfced9bbcfd1a94c9cbbadc695667bf3a882346 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 1 Dec 2022 08:34:14 +0000 Subject: [PATCH] Add support for most setters in SimpleBasePlayer This adds the forwarding logic for most setters in SimpleExoPlayer in the same style as the existing logic for setPlayWhenReady. This change doesn't implement the setters for modifying media items, seeking and releasing yet as they require additional handling that goes beyond the repeated implementation pattern in this change. PiperOrigin-RevId: 492124399 (cherry picked from commit e598a17b3628e1179fa4219ca3212407fb3fdeb1) --- .../android/exoplayer2/SimpleBasePlayer.java | 509 +++++- .../google/android/exoplayer2/util/Size.java | 3 + .../exoplayer2/SimpleBasePlayerTest.java | 1511 ++++++++++++++++- 3 files changed, 1955 insertions(+), 68 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 7a6e231a30..c5c9a10dc5 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 @@ -21,6 +21,7 @@ import static com.google.android.exoplayer2.util.Util.castNonNull; import static com.google.android.exoplayer2.util.Util.usToMs; import static java.lang.Math.max; +import android.graphics.Rect; import android.os.Looper; import android.os.SystemClock; import android.util.Pair; @@ -2039,6 +2040,7 @@ public abstract class SimpleBasePlayer extends BasePlayer { @Override public final void setPlayWhenReady(boolean playWhenReady) { verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. State state = this.state; if (!state.availableCommands.contains(Player.COMMAND_PLAY_PAUSE)) { return; @@ -2091,8 +2093,20 @@ public abstract class SimpleBasePlayer extends BasePlayer { @Override public final void prepare() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_PREPARE)) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handlePrepare(), + /* placeholderStateSupplier= */ () -> + state + .buildUpon() + .setPlayerError(null) + .setPlaybackState(state.timeline.isEmpty() ? STATE_ENDED : STATE_BUFFERING) + .build()); } @Override @@ -2117,8 +2131,15 @@ public abstract class SimpleBasePlayer extends BasePlayer { @Override public final void setRepeatMode(@Player.RepeatMode int repeatMode) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_SET_REPEAT_MODE)) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSetRepeatMode(repeatMode), + /* placeholderStateSupplier= */ () -> state.buildUpon().setRepeatMode(repeatMode).build()); } @Override @@ -2130,8 +2151,16 @@ public abstract class SimpleBasePlayer extends BasePlayer { @Override public final void setShuffleModeEnabled(boolean shuffleModeEnabled) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_SET_SHUFFLE_MODE)) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSetShuffleModeEnabled(shuffleModeEnabled), + /* placeholderStateSupplier= */ () -> + state.buildUpon().setShuffleModeEnabled(shuffleModeEnabled).build()); } @Override @@ -2152,6 +2181,12 @@ public abstract class SimpleBasePlayer extends BasePlayer { throw new IllegalStateException(); } + @Override + protected final void repeatCurrentMediaItem() { + // TODO: implement. + throw new IllegalStateException(); + } + @Override public final long getSeekBackIncrement() { verifyApplicationThreadAndInitState(); @@ -2172,8 +2207,16 @@ public abstract class SimpleBasePlayer extends BasePlayer { @Override public final void setPlaybackParameters(PlaybackParameters playbackParameters) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_SET_SPEED_AND_PITCH)) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSetPlaybackParameters(playbackParameters), + /* placeholderStateSupplier= */ () -> + state.buildUpon().setPlaybackParameters(playbackParameters).build()); } @Override @@ -2184,14 +2227,30 @@ public abstract class SimpleBasePlayer extends BasePlayer { @Override public final void stop() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_STOP)) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleStop(), + /* placeholderStateSupplier= */ () -> + state + .buildUpon() + .setPlaybackState(Player.STATE_IDLE) + .setTotalBufferedDurationMs(PositionSupplier.ZERO) + .setContentBufferedPositionMs(state.contentPositionMsSupplier) + .setAdBufferedPositionMs(state.adPositionMsSupplier) + .build()); } @Override public final void stop(boolean reset) { - // TODO: implement. - throw new IllegalStateException(); + stop(); + if (reset) { + clearMediaItems(); + } } @Override @@ -2214,8 +2273,16 @@ public abstract class SimpleBasePlayer extends BasePlayer { @Override public final void setTrackSelectionParameters(TrackSelectionParameters parameters) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS)) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSetTrackSelectionParameters(parameters), + /* placeholderStateSupplier= */ () -> + state.buildUpon().setTrackSelectionParameters(parameters).build()); } @Override @@ -2232,8 +2299,16 @@ public abstract class SimpleBasePlayer extends BasePlayer { @Override public final void setPlaylistMetadata(MediaMetadata mediaMetadata) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_SET_MEDIA_ITEMS_METADATA)) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSetPlaylistMetadata(mediaMetadata), + /* placeholderStateSupplier= */ () -> + state.buildUpon().setPlaylistMetadata(mediaMetadata).build()); } @Override @@ -2325,8 +2400,15 @@ public abstract class SimpleBasePlayer extends BasePlayer { @Override public final void setVolume(float volume) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_SET_VOLUME)) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSetVolume(volume), + /* placeholderStateSupplier= */ () -> state.buildUpon().setVolume(volume).build()); } @Override @@ -2335,58 +2417,122 @@ public abstract class SimpleBasePlayer extends BasePlayer { return state.volume; } - @Override - public final void clearVideoSurface() { - // TODO: implement. - throw new IllegalStateException(); - } - - @Override - public final void clearVideoSurface(@Nullable Surface surface) { - // TODO: implement. - throw new IllegalStateException(); - } - @Override public final void setVideoSurface(@Nullable Surface surface) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_SET_VIDEO_SURFACE)) { + return; + } + if (surface == null) { + clearVideoSurface(); + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSetVideoOutput(surface), + /* placeholderStateSupplier= */ () -> + state.buildUpon().setSurfaceSize(Size.UNKNOWN).build()); } @Override public final void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) { - // TODO: implement. - throw new IllegalStateException(); - } - - @Override - public final void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_SET_VIDEO_SURFACE)) { + return; + } + if (surfaceHolder == null) { + clearVideoSurface(); + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSetVideoOutput(surfaceHolder), + /* placeholderStateSupplier= */ () -> + state.buildUpon().setSurfaceSize(getSurfaceHolderSize(surfaceHolder)).build()); } @Override public final void setVideoSurfaceView(@Nullable SurfaceView surfaceView) { - // TODO: implement. - throw new IllegalStateException(); - } - - @Override - public final void clearVideoSurfaceView(@Nullable SurfaceView surfaceView) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_SET_VIDEO_SURFACE)) { + return; + } + if (surfaceView == null) { + clearVideoSurface(); + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSetVideoOutput(surfaceView), + /* placeholderStateSupplier= */ () -> + state + .buildUpon() + .setSurfaceSize(getSurfaceHolderSize(surfaceView.getHolder())) + .build()); } @Override public final void setVideoTextureView(@Nullable TextureView textureView) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_SET_VIDEO_SURFACE)) { + return; + } + if (textureView == null) { + clearVideoSurface(); + return; + } + Size surfaceSize; + if (textureView.isAvailable()) { + surfaceSize = new Size(textureView.getWidth(), textureView.getHeight()); + } else { + surfaceSize = Size.ZERO; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSetVideoOutput(textureView), + /* placeholderStateSupplier= */ () -> + state.buildUpon().setSurfaceSize(surfaceSize).build()); + } + + @Override + public final void clearVideoSurface() { + clearVideoOutput(/* videoOutput= */ null); + } + + @Override + public final void clearVideoSurface(@Nullable Surface surface) { + clearVideoOutput(surface); + } + + @Override + public final void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) { + clearVideoOutput(surfaceHolder); + } + + @Override + public final void clearVideoSurfaceView(@Nullable SurfaceView surfaceView) { + clearVideoOutput(surfaceView); } @Override public final void clearVideoTextureView(@Nullable TextureView textureView) { - // TODO: implement. - throw new IllegalStateException(); + clearVideoOutput(textureView); + } + + private void clearVideoOutput(@Nullable Object videoOutput) { + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_SET_VIDEO_SURFACE)) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleClearVideoOutput(videoOutput), + /* placeholderStateSupplier= */ () -> state.buildUpon().setSurfaceSize(Size.ZERO).build()); } @Override @@ -2427,26 +2573,56 @@ public abstract class SimpleBasePlayer extends BasePlayer { @Override public final void setDeviceVolume(int volume) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_SET_DEVICE_VOLUME)) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSetDeviceVolume(volume), + /* placeholderStateSupplier= */ () -> state.buildUpon().setDeviceVolume(volume).build()); } @Override public final void increaseDeviceVolume() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_ADJUST_DEVICE_VOLUME)) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleIncreaseDeviceVolume(), + /* placeholderStateSupplier= */ () -> + state.buildUpon().setDeviceVolume(state.deviceVolume + 1).build()); } @Override public final void decreaseDeviceVolume() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_ADJUST_DEVICE_VOLUME)) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleDecreaseDeviceVolume(), + /* placeholderStateSupplier= */ () -> + state.buildUpon().setDeviceVolume(max(0, state.deviceVolume - 1)).build()); } @Override public final void setDeviceMuted(boolean muted) { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + // Use a local copy to ensure the lambda below uses the current state value. + State state = this.state; + if (!state.availableCommands.contains(Player.COMMAND_ADJUST_DEVICE_VOLUME)) { + return; + } + updateStateForPendingOperation( + /* pendingOperation= */ handleSetDeviceMuted(muted), + /* placeholderStateSupplier= */ () -> state.buildUpon().setIsDeviceMuted(muted).build()); } /** @@ -2500,22 +2676,217 @@ public abstract class SimpleBasePlayer extends BasePlayer { } /** - * Handles calls to set {@link State#playWhenReady}. + * Handles calls to {@link Player#setPlayWhenReady}, {@link Player#play} and {@link Player#pause}. * - *

Will only be called if {@link Player.Command#COMMAND_PLAY_PAUSE} is available. + *

Will only be called if {@link Player#COMMAND_PLAY_PAUSE} is available. * * @param playWhenReady The requested {@link State#playWhenReady} * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} * changes caused by this call. - * @see Player#setPlayWhenReady(boolean) - * @see Player#play() - * @see Player#pause() */ @ForOverride protected ListenableFuture handleSetPlayWhenReady(boolean playWhenReady) { throw new IllegalStateException(); } + /** + * Handles calls to {@link Player#prepare}. + * + *

Will only be called if {@link Player#COMMAND_PREPARE} is available. + * + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handlePrepare() { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#stop}. + * + *

Will only be called if {@link Player#COMMAND_STOP} is available. + * + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleStop() { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#setRepeatMode}. + * + *

Will only be called if {@link Player#COMMAND_SET_REPEAT_MODE} is available. + * + * @param repeatMode The requested {@link RepeatMode}. + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleSetRepeatMode(@RepeatMode int repeatMode) { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#setShuffleModeEnabled}. + * + *

Will only be called if {@link Player#COMMAND_SET_SHUFFLE_MODE} is available. + * + * @param shuffleModeEnabled Whether shuffle mode was requested to be enabled. + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleSetShuffleModeEnabled(boolean shuffleModeEnabled) { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#setPlaybackParameters} or {@link Player#setPlaybackSpeed}. + * + *

Will only be called if {@link Player#COMMAND_SET_SPEED_AND_PITCH} is available. + * + * @param playbackParameters The requested {@link PlaybackParameters}. + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleSetPlaybackParameters(PlaybackParameters playbackParameters) { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#setTrackSelectionParameters}. + * + *

Will only be called if {@link Player#COMMAND_SET_TRACK_SELECTION_PARAMETERS} is available. + * + * @param trackSelectionParameters The requested {@link TrackSelectionParameters}. + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleSetTrackSelectionParameters( + TrackSelectionParameters trackSelectionParameters) { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#setPlaylistMetadata}. + * + *

Will only be called if {@link Player#COMMAND_SET_MEDIA_ITEMS_METADATA} is available. + * + * @param playlistMetadata The requested {@linkplain MediaMetadata playlist metadata}. + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleSetPlaylistMetadata(MediaMetadata playlistMetadata) { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#setVolume}. + * + *

Will only be called if {@link Player#COMMAND_SET_VOLUME} is available. + * + * @param volume The requested audio volume, with 0 being silence and 1 being unity gain (signal + * unchanged). + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleSetVolume(@FloatRange(from = 0, to = 1.0) float volume) { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#setDeviceVolume}. + * + *

Will only be called if {@link Player#COMMAND_SET_DEVICE_VOLUME} is available. + * + * @param deviceVolume The requested device volume. + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleSetDeviceVolume(@IntRange(from = 0) int deviceVolume) { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#increaseDeviceVolume()}. + * + *

Will only be called if {@link Player#COMMAND_ADJUST_DEVICE_VOLUME} is available. + * + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleIncreaseDeviceVolume() { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#decreaseDeviceVolume()}. + * + *

Will only be called if {@link Player#COMMAND_ADJUST_DEVICE_VOLUME} is available. + * + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleDecreaseDeviceVolume() { + throw new IllegalStateException(); + } + + /** + * Handles calls to {@link Player#setDeviceMuted}. + * + *

Will only be called if {@link Player#COMMAND_ADJUST_DEVICE_VOLUME} is available. + * + * @param muted Whether the device was requested to be muted. + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleSetDeviceMuted(boolean muted) { + throw new IllegalStateException(); + } + + /** + * Handles calls to set the video output. + * + *

Will only be called if {@link Player#COMMAND_SET_VIDEO_SURFACE} is available. + * + * @param videoOutput The requested video output. This is either a {@link Surface}, {@link + * SurfaceHolder}, {@link TextureView} or {@link SurfaceView}. + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleSetVideoOutput(Object videoOutput) { + throw new IllegalStateException(); + } + + /** + * Handles calls to clear the video output. + * + *

Will only be called if {@link Player#COMMAND_SET_VIDEO_SURFACE} is available. + * + * @param videoOutput The video output to clear. If null any current output should be cleared. If + * non-null, the output should only be cleared if it matches the provided argument. This is + * either a {@link Surface}, {@link SurfaceHolder}, {@link TextureView} or {@link + * SurfaceView}. + * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State} + * changes caused by this call. + */ + @ForOverride + protected ListenableFuture handleClearVideoOutput(@Nullable Object videoOutput) { + throw new IllegalStateException(); + } + @SuppressWarnings("deprecation") // Calling deprecated listener methods. @RequiresNonNull("state") private void updateStateAndInformListeners(State newState) { @@ -2974,4 +3345,12 @@ public abstract class SimpleBasePlayer extends BasePlayer { } return C.INDEX_UNSET; } + + private static Size getSurfaceHolderSize(SurfaceHolder surfaceHolder) { + if (!surfaceHolder.getSurface().isValid()) { + return Size.ZERO; + } + Rect surfaceFrame = surfaceHolder.getSurfaceFrame(); + return new Size(surfaceFrame.width(), surfaceFrame.height()); + } } diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/Size.java b/library/common/src/main/java/com/google/android/exoplayer2/util/Size.java index c273fbf5ee..bbd8e25893 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/Size.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/Size.java @@ -28,6 +28,9 @@ public final class Size { public static final Size UNKNOWN = new Size(/* width= */ C.LENGTH_UNSET, /* height= */ C.LENGTH_UNSET); + /* A static instance to represent a size of zero height and width. */ + public static final Size ZERO = new Size(/* width= */ 0, /* height= */ 0); + private final int width; private final int height; 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 0995639a66..ee56a1705b 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 @@ -27,8 +27,12 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import android.graphics.SurfaceTexture; import android.os.Looper; import android.os.SystemClock; +import android.view.Surface; +import androidx.annotation.Nullable; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.Player.Commands; import com.google.android.exoplayer2.Player.Listener; @@ -1833,17 +1837,18 @@ public class SimpleBasePlayerTest { .setPlayWhenReady( /* playWhenReady= */ true, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE) .build(); - AtomicBoolean stateUpdated = new AtomicBoolean(); SimpleBasePlayer player = new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + @Override protected State getState() { - return stateUpdated.get() ? updatedState : state; + return playerState; } @Override protected ListenableFuture handleSetPlayWhenReady(boolean playWhenReady) { - stateUpdated.set(true); + playerState = updatedState; return Futures.immediateVoidFuture(); } }; @@ -1941,6 +1946,1506 @@ public class SimpleBasePlayerTest { assertThat(callForwarded.get()).isFalse(); } + @SuppressWarnings("deprecation") // Verifying deprecated listener call. + @Test + public void prepare_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaybackState(Player.STATE_IDLE) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build())) + .build(); + State updatedState = state.buildUpon().setPlaybackState(Player.STATE_READY).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handlePrepare() { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.prepare(); + + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_READY); + verify(listener).onPlaybackStateChanged(Player.STATE_READY); + verify(listener) + .onPlayerStateChanged(/* playWhenReady= */ false, /* playbackState= */ Player.STATE_READY); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener call. + @Test + public void prepare_asyncHandling_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaybackState(Player.STATE_IDLE) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build())) + .build(); + State updatedState = state.buildUpon().setPlaybackState(Player.STATE_READY).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handlePrepare() { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.prepare(); + + // Verify placeholder state and listener calls. + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_BUFFERING); + verify(listener).onPlaybackStateChanged(Player.STATE_BUFFERING); + verify(listener) + .onPlayerStateChanged( + /* playWhenReady= */ false, /* playbackState= */ Player.STATE_BUFFERING); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_READY); + verify(listener).onPlaybackStateChanged(Player.STATE_READY); + verify(listener) + .onPlayerStateChanged(/* playWhenReady= */ false, /* playbackState= */ Player.STATE_READY); + verifyNoMoreInteractions(listener); + } + + @Test + public void prepare_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder().addAllCommands().remove(Player.COMMAND_PREPARE).build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handlePrepare() { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.prepare(); + + assertThat(callForwarded.get()).isFalse(); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener call. + @Test + public void stop_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaybackState(Player.STATE_READY) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build())) + .build(); + State updatedState = state.buildUpon().setPlaybackState(Player.STATE_IDLE).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleStop() { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.stop(); + + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_IDLE); + verify(listener).onPlaybackStateChanged(Player.STATE_IDLE); + verify(listener) + .onPlayerStateChanged(/* playWhenReady= */ false, /* playbackState= */ Player.STATE_IDLE); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener call. + @Test + public void stop_asyncHandling_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaybackState(Player.STATE_READY) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build())) + .build(); + // Additionally set the repeat mode to see a difference between the placeholder and new state. + State updatedState = + state + .buildUpon() + .setPlaybackState(Player.STATE_IDLE) + .setRepeatMode(Player.REPEAT_MODE_ALL) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleStop() { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.stop(); + + // Verify placeholder state and listener calls. + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_IDLE); + assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_OFF); + verify(listener).onPlaybackStateChanged(Player.STATE_IDLE); + verify(listener) + .onPlayerStateChanged(/* playWhenReady= */ false, /* playbackState= */ Player.STATE_IDLE); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ALL); + verify(listener).onRepeatModeChanged(Player.REPEAT_MODE_ALL); + verifyNoMoreInteractions(listener); + } + + @Test + public void stop_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder().addAllCommands().remove(Player.COMMAND_STOP).build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleStop() { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.stop(); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void setRepeatMode_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + // Set a different one to the one requested to ensure the updated state is used. + State updatedState = state.buildUpon().setRepeatMode(Player.REPEAT_MODE_ALL).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleSetRepeatMode(@Player.RepeatMode int repeatMode) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setRepeatMode(Player.REPEAT_MODE_ONE); + + assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ALL); + verify(listener).onRepeatModeChanged(Player.REPEAT_MODE_ALL); + verifyNoMoreInteractions(listener); + } + + @Test + public void setRepeatMode_asyncHandling_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + // Set a new repeat mode to see a difference between the placeholder and new state. + State updatedState = state.buildUpon().setRepeatMode(Player.REPEAT_MODE_ALL).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetRepeatMode(@Player.RepeatMode int repeatMode) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setRepeatMode(Player.REPEAT_MODE_ONE); + + // Verify placeholder state and listener calls. + assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE); + verify(listener).onRepeatModeChanged(Player.REPEAT_MODE_ONE); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ALL); + verify(listener).onRepeatModeChanged(Player.REPEAT_MODE_ALL); + verifyNoMoreInteractions(listener); + } + + @Test + public void setRepeatMode_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SET_REPEAT_MODE) + .build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSetRepeatMode(@Player.RepeatMode int repeatMode) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.setRepeatMode(Player.REPEAT_MODE_ONE); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void setShuffleModeEnabled_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + // Also change the repeat mode to ensure the updated state is used. + State updatedState = + state.buildUpon().setShuffleModeEnabled(true).setRepeatMode(Player.REPEAT_MODE_ALL).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleSetShuffleModeEnabled(boolean shuffleModeEnabled) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setShuffleModeEnabled(true); + + assertThat(player.getShuffleModeEnabled()).isTrue(); + assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ALL); + verify(listener).onShuffleModeEnabledChanged(true); + verify(listener).onRepeatModeChanged(Player.REPEAT_MODE_ALL); + verifyNoMoreInteractions(listener); + } + + @Test + public void setShuffleModeEnabled_asyncHandling_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() { + // Always return the same state to revert the shuffle mode change. This allows to see a + // difference between the placeholder and new state. + return state; + } + + @Override + protected ListenableFuture handleSetShuffleModeEnabled(boolean shuffleModeEnabled) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setShuffleModeEnabled(true); + + // Verify placeholder state and listener calls. + assertThat(player.getShuffleModeEnabled()).isTrue(); + verify(listener).onShuffleModeEnabledChanged(true); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getShuffleModeEnabled()).isFalse(); + verify(listener).onShuffleModeEnabledChanged(false); + verifyNoMoreInteractions(listener); + } + + @Test + public void setShuffleModeEnabled_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SET_SHUFFLE_MODE) + .build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSetShuffleModeEnabled(boolean shuffleModeEnabled) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.setShuffleModeEnabled(true); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void setPlaybackParameters_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + // Set a different one to the one requested to ensure the updated state is used. + State updatedState = + state.buildUpon().setPlaybackParameters(new PlaybackParameters(/* speed= */ 3f)).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleSetPlaybackParameters( + PlaybackParameters playbackParameters) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setPlaybackParameters(new PlaybackParameters(/* speed= */ 2f)); + + assertThat(player.getPlaybackParameters()).isEqualTo(new PlaybackParameters(/* speed= */ 3f)); + verify(listener).onPlaybackParametersChanged(new PlaybackParameters(/* speed= */ 3f)); + verifyNoMoreInteractions(listener); + } + + @Test + public void setPlaybackParameters_asyncHandling_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + // Set a new repeat mode to see a difference between the placeholder and new state. + State updatedState = + state.buildUpon().setPlaybackParameters(new PlaybackParameters(/* speed= */ 3f)).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetPlaybackParameters( + PlaybackParameters playbackParameters) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setPlaybackParameters(new PlaybackParameters(/* speed= */ 2f)); + + // Verify placeholder state and listener calls. + assertThat(player.getPlaybackParameters()).isEqualTo(new PlaybackParameters(/* speed= */ 2f)); + verify(listener).onPlaybackParametersChanged(new PlaybackParameters(/* speed= */ 2f)); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getPlaybackParameters()).isEqualTo(new PlaybackParameters(/* speed= */ 3f)); + verify(listener).onPlaybackParametersChanged(new PlaybackParameters(/* speed= */ 3f)); + verifyNoMoreInteractions(listener); + } + + @Test + public void setPlaybackParameters_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SET_SPEED_AND_PITCH) + .build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSetPlaybackParameters( + PlaybackParameters playbackParameters) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.setPlaybackParameters(new PlaybackParameters(/* speed= */ 2f)); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void setTrackSelectionParameters_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + // Set a different one to the one requested to ensure the updated state is used. + TrackSelectionParameters updatedParameters = + new TrackSelectionParameters.Builder(ApplicationProvider.getApplicationContext()) + .setMaxVideoBitrate(3000) + .build(); + State updatedState = state.buildUpon().setTrackSelectionParameters(updatedParameters).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleSetTrackSelectionParameters( + TrackSelectionParameters trackSelectionParameters) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setTrackSelectionParameters( + new TrackSelectionParameters.Builder(ApplicationProvider.getApplicationContext()) + .setMaxVideoBitrate(1000) + .build()); + + assertThat(player.getTrackSelectionParameters()).isEqualTo(updatedParameters); + verify(listener).onTrackSelectionParametersChanged(updatedParameters); + verifyNoMoreInteractions(listener); + } + + @Test + public void setTrackSelectionParameters_asyncHandling_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + // Set new parameters to see a difference between the placeholder and new state. + TrackSelectionParameters updatedParameters = + new TrackSelectionParameters.Builder(ApplicationProvider.getApplicationContext()) + .setMaxVideoBitrate(3000) + .build(); + State updatedState = state.buildUpon().setTrackSelectionParameters(updatedParameters).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetTrackSelectionParameters( + TrackSelectionParameters trackSelectionParameters) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + TrackSelectionParameters requestedParameters = + new TrackSelectionParameters.Builder(ApplicationProvider.getApplicationContext()) + .setMaxVideoBitrate(3000) + .build(); + player.setTrackSelectionParameters(requestedParameters); + + // Verify placeholder state and listener calls. + assertThat(player.getTrackSelectionParameters()).isEqualTo(requestedParameters); + verify(listener).onTrackSelectionParametersChanged(requestedParameters); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getTrackSelectionParameters()).isEqualTo(updatedParameters); + verify(listener).onTrackSelectionParametersChanged(updatedParameters); + verifyNoMoreInteractions(listener); + } + + @Test + public void setTrackSelectionParameters_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS) + .build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSetTrackSelectionParameters( + TrackSelectionParameters trackSelectionParameters) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.setTrackSelectionParameters( + new TrackSelectionParameters.Builder(ApplicationProvider.getApplicationContext()) + .setMaxVideoBitrate(1000) + .build()); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void setPlaylistMetadata_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + // Set a different one to the one requested to ensure the updated state is used. + MediaMetadata updatedMetadata = new MediaMetadata.Builder().setArtist("artist").build(); + State updatedState = state.buildUpon().setPlaylistMetadata(updatedMetadata).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleSetPlaylistMetadata(MediaMetadata playlistMetadata) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setPlaylistMetadata(new MediaMetadata.Builder().setTitle("title").build()); + + assertThat(player.getPlaylistMetadata()).isEqualTo(updatedMetadata); + verify(listener).onPlaylistMetadataChanged(updatedMetadata); + verifyNoMoreInteractions(listener); + } + + @Test + public void setPlaylistMetadata_asyncHandling_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + // Set new metadata to see a difference between the placeholder and new state. + MediaMetadata updatedMetadata = new MediaMetadata.Builder().setArtist("artist").build(); + State updatedState = state.buildUpon().setPlaylistMetadata(updatedMetadata).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetPlaylistMetadata(MediaMetadata playlistMetadata) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + MediaMetadata requestedMetadata = new MediaMetadata.Builder().setTitle("title").build(); + player.setPlaylistMetadata(requestedMetadata); + + // Verify placeholder state and listener calls. + assertThat(player.getPlaylistMetadata()).isEqualTo(requestedMetadata); + verify(listener).onPlaylistMetadataChanged(requestedMetadata); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getPlaylistMetadata()).isEqualTo(updatedMetadata); + verify(listener).onPlaylistMetadataChanged(updatedMetadata); + verifyNoMoreInteractions(listener); + } + + @Test + public void setPlaylistMetadata_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SET_MEDIA_ITEMS_METADATA) + .build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSetPlaylistMetadata(MediaMetadata playlistMetadata) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.setPlaylistMetadata(new MediaMetadata.Builder().setTitle("title").build()); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void setVolume_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + // Set a different one to the one requested to ensure the updated state is used. + State updatedState = state.buildUpon().setVolume(.8f).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleSetVolume(float volume) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setVolume(.5f); + + assertThat(player.getVolume()).isEqualTo(.8f); + verify(listener).onVolumeChanged(.8f); + verifyNoMoreInteractions(listener); + } + + @Test + public void setVolume_asyncHandling_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + // Set a new volume to see a difference between the placeholder and new state. + State updatedState = state.buildUpon().setVolume(.8f).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetVolume(float volume) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setVolume(.5f); + + // Verify placeholder state and listener calls. + assertThat(player.getVolume()).isEqualTo(.5f); + verify(listener).onVolumeChanged(.5f); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getVolume()).isEqualTo(.8f); + verify(listener).onVolumeChanged(.8f); + verifyNoMoreInteractions(listener); + } + + @Test + public void setVolume_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder().addAllCommands().remove(Player.COMMAND_SET_VOLUME).build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSetVolume(float volume) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.setVolume(.5f); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void setDeviceVolume_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + // Set a different one to the one requested to ensure the updated state is used. + State updatedState = state.buildUpon().setDeviceVolume(6).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleSetDeviceVolume(int volume) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setDeviceVolume(3); + + assertThat(player.getDeviceVolume()).isEqualTo(6); + verify(listener).onDeviceVolumeChanged(6, /* muted= */ false); + verifyNoMoreInteractions(listener); + } + + @Test + public void setDeviceVolume_asyncHandling_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + // Set a new volume to see a difference between the placeholder and new state. + State updatedState = state.buildUpon().setDeviceVolume(6).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetDeviceVolume(int volume) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setDeviceVolume(3); + + // Verify placeholder state and listener calls. + assertThat(player.getDeviceVolume()).isEqualTo(3); + verify(listener).onDeviceVolumeChanged(3, /* muted= */ false); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getDeviceVolume()).isEqualTo(6); + verify(listener).onDeviceVolumeChanged(6, /* muted= */ false); + verifyNoMoreInteractions(listener); + } + + @Test + public void setDeviceVolume_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SET_DEVICE_VOLUME) + .build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSetDeviceVolume(int volume) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.setDeviceVolume(3); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void increaseDeviceVolume_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setDeviceVolume(3) + .build(); + // Set a different one to the one requested to ensure the updated state is used. + State updatedState = state.buildUpon().setDeviceVolume(6).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleIncreaseDeviceVolume() { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.increaseDeviceVolume(); + + assertThat(player.getDeviceVolume()).isEqualTo(6); + verify(listener).onDeviceVolumeChanged(6, /* muted= */ false); + verifyNoMoreInteractions(listener); + } + + @Test + public void increaseDeviceVolume_asyncHandling_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setDeviceVolume(3) + .build(); + // Set a new volume to see a difference between the placeholder and new state. + State updatedState = state.buildUpon().setDeviceVolume(6).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleIncreaseDeviceVolume() { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.increaseDeviceVolume(); + + // Verify placeholder state and listener calls. + assertThat(player.getDeviceVolume()).isEqualTo(4); + verify(listener).onDeviceVolumeChanged(4, /* muted= */ false); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getDeviceVolume()).isEqualTo(6); + verify(listener).onDeviceVolumeChanged(6, /* muted= */ false); + verifyNoMoreInteractions(listener); + } + + @Test + public void increaseDeviceVolume_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_ADJUST_DEVICE_VOLUME) + .build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleIncreaseDeviceVolume() { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.increaseDeviceVolume(); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void decreaseDeviceVolume_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setDeviceVolume(3) + .build(); + // Set a different one to the one requested to ensure the updated state is used. + State updatedState = state.buildUpon().setDeviceVolume(1).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleDecreaseDeviceVolume() { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.decreaseDeviceVolume(); + + assertThat(player.getDeviceVolume()).isEqualTo(1); + verify(listener).onDeviceVolumeChanged(1, /* muted= */ false); + verifyNoMoreInteractions(listener); + } + + @Test + public void decreaseDeviceVolume_asyncHandling_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setDeviceVolume(3) + .build(); + // Set a new volume to see a difference between the placeholder and new state. + State updatedState = state.buildUpon().setDeviceVolume(1).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleDecreaseDeviceVolume() { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.decreaseDeviceVolume(); + + // Verify placeholder state and listener calls. + assertThat(player.getDeviceVolume()).isEqualTo(2); + verify(listener).onDeviceVolumeChanged(2, /* muted= */ false); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getDeviceVolume()).isEqualTo(1); + verify(listener).onDeviceVolumeChanged(1, /* muted= */ false); + verifyNoMoreInteractions(listener); + } + + @Test + public void decreaseDeviceVolume_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_ADJUST_DEVICE_VOLUME) + .build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleDecreaseDeviceVolume() { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.decreaseDeviceVolume(); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void setDeviceMuted_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + // Also change the volume to ensure the updated state is used. + State updatedState = state.buildUpon().setIsDeviceMuted(true).setDeviceVolume(6).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleSetDeviceMuted(boolean muted) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setDeviceMuted(true); + + assertThat(player.isDeviceMuted()).isTrue(); + assertThat(player.getDeviceVolume()).isEqualTo(6); + verify(listener).onDeviceVolumeChanged(6, /* muted= */ true); + verifyNoMoreInteractions(listener); + } + + @Test + public void setDeviceMuted_asyncHandling_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() { + // Always return the same state to revert the muted change. This allows to see a + // difference between the placeholder and new state. + return state; + } + + @Override + protected ListenableFuture handleSetDeviceMuted(boolean muted) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setDeviceMuted(true); + + // Verify placeholder state and listener calls. + assertThat(player.isDeviceMuted()).isTrue(); + verify(listener).onDeviceVolumeChanged(0, /* muted= */ true); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.isDeviceMuted()).isFalse(); + verify(listener).onDeviceVolumeChanged(0, /* muted= */ false); + verifyNoMoreInteractions(listener); + } + + @Test + public void setDeviceMuted_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_ADJUST_DEVICE_VOLUME) + .build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSetDeviceMuted(boolean muted) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.setDeviceMuted(true); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void setVideoSurface_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setSurfaceSize(Size.ZERO) + .build(); + Size updatedSize = new Size(/* width= */ 300, /* height= */ 200); + State updatedState = state.buildUpon().setSurfaceSize(updatedSize).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleSetVideoOutput(Object videoOutput) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setVideoSurface(new Surface(new SurfaceTexture(0))); + + assertThat(player.getSurfaceSize()).isEqualTo(updatedSize); + verify(listener).onSurfaceSizeChanged(updatedSize.getWidth(), updatedSize.getHeight()); + verifyNoMoreInteractions(listener); + } + + @Test + public void setVideoSurface_asyncHandling_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setSurfaceSize(Size.ZERO) + .build(); + SettableFuture future = SettableFuture.create(); + Size updatedSize = new Size(/* width= */ 300, /* height= */ 200); + State updatedState = state.buildUpon().setSurfaceSize(updatedSize).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleSetVideoOutput(Object videoOutput) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.setVideoSurface(new Surface(new SurfaceTexture(0))); + + // Verify placeholder state and listener calls. + assertThat(player.getSurfaceSize()).isEqualTo(Size.UNKNOWN); + verify(listener) + .onSurfaceSizeChanged(/* width= */ C.LENGTH_UNSET, /* height= */ C.LENGTH_UNSET); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getSurfaceSize()).isEqualTo(updatedSize); + verify(listener).onSurfaceSizeChanged(updatedSize.getWidth(), updatedSize.getHeight()); + verifyNoMoreInteractions(listener); + } + + @Test + public void setVideoSurface_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SET_VIDEO_SURFACE) + .build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSetVideoOutput(Object videoOutput) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.setVideoSurface(new Surface(new SurfaceTexture(0))); + + assertThat(callForwarded.get()).isFalse(); + } + + @Test + public void clearVideoSurface_immediateHandling_updatesStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setSurfaceSize(new Size(/* width= */ 300, /* height= */ 200)) + .build(); + // Change something else in addition to ensure we actually use the updated state. + State updatedState = + state.buildUpon().setSurfaceSize(Size.ZERO).setRepeatMode(Player.REPEAT_MODE_ONE).build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + private State playerState = state; + + @Override + protected State getState() { + return playerState; + } + + @Override + protected ListenableFuture handleClearVideoOutput(@Nullable Object videoOutput) { + playerState = updatedState; + return Futures.immediateVoidFuture(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.clearVideoSurface(); + + assertThat(player.getSurfaceSize()).isEqualTo(Size.ZERO); + assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE); + verify(listener).onSurfaceSizeChanged(/* width= */ 0, /* height= */ 0); + verify(listener).onRepeatModeChanged(Player.REPEAT_MODE_ONE); + verifyNoMoreInteractions(listener); + } + + @Test + public void clearVideoSurface_asyncHandling_usesPlaceholderStateAndInformsListeners() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setSurfaceSize(new Size(/* width= */ 300, /* height= */ 200)) + .build(); + // Change something else in addition to ensure we actually use the updated state. + State updatedState = + state.buildUpon().setSurfaceSize(Size.ZERO).setRepeatMode(Player.REPEAT_MODE_ONE).build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return future.isDone() ? updatedState : state; + } + + @Override + protected ListenableFuture handleClearVideoOutput(@Nullable Object videoOutput) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.clearVideoSurface(); + + // Verify placeholder state and listener calls. + assertThat(player.getSurfaceSize()).isEqualTo(Size.ZERO); + verify(listener).onSurfaceSizeChanged(/* width= */ 0, /* height= */ 0); + verifyNoMoreInteractions(listener); + + future.set(null); + + // Verify actual state update. + assertThat(player.getSurfaceSize()).isEqualTo(Size.ZERO); + assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ONE); + verify(listener).onRepeatModeChanged(Player.REPEAT_MODE_ONE); + verifyNoMoreInteractions(listener); + } + + @Test + public void clearVideoSurface_withoutAvailableCommand_isNotForwarded() { + State state = + new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SET_VIDEO_SURFACE) + .build()) + .build(); + AtomicBoolean callForwarded = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleClearVideoOutput(@Nullable Object videoOutput) { + callForwarded.set(true); + return Futures.immediateVoidFuture(); + } + }; + + player.clearVideoSurface(); + + assertThat(callForwarded.get()).isFalse(); + } + private static Object[] getAnyArguments(Method method) { Object[] arguments = new Object[method.getParameterCount()]; Class[] argumentTypes = method.getParameterTypes();