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();