mediaItems) {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final void moveMediaItems(int fromIndex, int toIndex, int newIndex) {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final void removeMediaItems(int fromIndex, int toIndex) {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final void prepare() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final int getPlaybackState() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final int getPlaybackSuppressionReason() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Nullable
+ @Override
+ public final PlaybackException getPlayerError() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final void setRepeatMode(int repeatMode) {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final int getRepeatMode() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final void setShuffleModeEnabled(boolean shuffleModeEnabled) {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final boolean getShuffleModeEnabled() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final boolean isLoading() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final void seekTo(int mediaItemIndex, long positionMs) {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final long getSeekBackIncrement() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final long getSeekForwardIncrement() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final long getMaxSeekToPreviousPosition() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final void setPlaybackParameters(PlaybackParameters playbackParameters) {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final PlaybackParameters getPlaybackParameters() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final void stop() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final void stop(boolean reset) {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final void release() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final Tracks getCurrentTracks() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final TrackSelectionParameters getTrackSelectionParameters() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final void setTrackSelectionParameters(TrackSelectionParameters parameters) {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final MediaMetadata getMediaMetadata() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final MediaMetadata getPlaylistMetadata() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final void setPlaylistMetadata(MediaMetadata mediaMetadata) {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final Timeline getCurrentTimeline() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final int getCurrentPeriodIndex() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final int getCurrentMediaItemIndex() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final long getDuration() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final long getCurrentPosition() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final long getBufferedPosition() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final long getTotalBufferedDuration() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final boolean isPlayingAd() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final int getCurrentAdGroupIndex() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final int getCurrentAdIndexInAdGroup() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final long getContentPosition() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final long getContentBufferedPosition() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final AudioAttributes getAudioAttributes() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final void setVolume(float volume) {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final float getVolume() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @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();
+ }
+
+ @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();
+ }
+
+ @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();
+ }
+
+ @Override
+ public final void setVideoTextureView(@Nullable TextureView textureView) {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final void clearVideoTextureView(@Nullable TextureView textureView) {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final VideoSize getVideoSize() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final CueGroup getCurrentCues() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final DeviceInfo getDeviceInfo() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final int getDeviceVolume() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final boolean isDeviceMuted() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final void setDeviceVolume(int volume) {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final void increaseDeviceVolume() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final void decreaseDeviceVolume() {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final void setDeviceMuted(boolean muted) {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ /**
+ * Invalidates the current state.
+ *
+ * Triggers a call to {@link #getState()} and informs listeners if the state changed.
+ *
+ *
Note that this may not have an immediate effect while there are still player methods being
+ * handled asynchronously. The state will be invalidated automatically once these pending
+ * synchronous operations are finished and there is no need to call this method again.
+ */
+ protected final void invalidateState() {
+ verifyApplicationThreadAndInitState();
+ if (!pendingOperations.isEmpty()) {
+ return;
+ }
+ updateStateAndInformListeners(getState());
+ }
+
+ /**
+ * Returns the current {@link State} of the player.
+ *
+ *
The {@link State} should include all {@linkplain
+ * State.Builder#setAvailableCommands(Commands) available commands} indicating which player
+ * methods are allowed to be called.
+ *
+ *
Note that this method won't be called while asynchronous handling of player methods is in
+ * progress. This means that the implementation doesn't need to handle state changes caused by
+ * these asynchronous operations until they are done and can return the currently known state
+ * directly. The placeholder state used while these asynchronous operations are in progress can be
+ * customized by overriding {@link #getPlaceholderState(State)} if required.
+ */
+ @ForOverride
+ protected abstract State getState();
+
+ /**
+ * Returns the placeholder state used while a player method is handled asynchronously.
+ *
+ *
The {@code suggestedPlaceholderState} already contains the most likely state update, for
+ * example setting {@link State#playWhenReady} to true if {@code player.setPlayWhenReady(true)} is
+ * called, and an implementations only needs to override this method if it can determine a more
+ * accurate placeholder state.
+ *
+ * @param suggestedPlaceholderState The suggested placeholder {@link State}, including the most
+ * likely outcome of handling all pending asynchronous operations.
+ * @return The placeholder {@link State} to use while asynchronous operations are pending.
+ */
+ @ForOverride
+ protected State getPlaceholderState(State suggestedPlaceholderState) {
+ return suggestedPlaceholderState;
+ }
+
+ /**
+ * Handles calls to set {@link State#playWhenReady}.
+ *
+ *
Will only be called if {@link Player.Command#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();
+ }
+
+ @SuppressWarnings("deprecation") // Calling deprecated listener methods.
+ @RequiresNonNull("state")
+ private void updateStateAndInformListeners(State newState) {
+ State previousState = state;
+ // Assign new state immediately such that all getters return the right values, but use a
+ // snapshot of the previous and new state so that listener invocations are triggered correctly.
+ this.state = newState;
+
+ boolean playWhenReadyChanged = previousState.playWhenReady != newState.playWhenReady;
+ if (playWhenReadyChanged /* TODO: || playbackStateChanged */) {
+ listeners.queueEvent(
+ /* eventFlag= */ C.INDEX_UNSET,
+ listener ->
+ listener.onPlayerStateChanged(newState.playWhenReady, /* TODO */ Player.STATE_IDLE));
+ }
+ if (playWhenReadyChanged
+ || previousState.playWhenReadyChangeReason != newState.playWhenReadyChangeReason) {
+ listeners.queueEvent(
+ Player.EVENT_PLAY_WHEN_READY_CHANGED,
+ listener ->
+ listener.onPlayWhenReadyChanged(
+ newState.playWhenReady, newState.playWhenReadyChangeReason));
+ }
+ if (isPlaying(previousState) != isPlaying(newState)) {
+ listeners.queueEvent(
+ Player.EVENT_IS_PLAYING_CHANGED,
+ listener -> listener.onIsPlayingChanged(isPlaying(newState)));
+ }
+ if (!previousState.availableCommands.equals(newState.availableCommands)) {
+ listeners.queueEvent(
+ Player.EVENT_AVAILABLE_COMMANDS_CHANGED,
+ listener -> listener.onAvailableCommandsChanged(newState.availableCommands));
+ }
+ listeners.flushEvents();
+ }
+
+ @EnsuresNonNull("state")
+ private void verifyApplicationThreadAndInitState() {
+ if (Thread.currentThread() != applicationLooper.getThread()) {
+ String message =
+ Util.formatInvariant(
+ "Player is accessed on the wrong thread.\n"
+ + "Current thread: '%s'\n"
+ + "Expected thread: '%s'\n"
+ + "See https://exoplayer.dev/issues/player-accessed-on-wrong-thread",
+ Thread.currentThread().getName(), applicationLooper.getThread().getName());
+ throw new IllegalStateException(message);
+ }
+ if (state == null) {
+ // First time accessing state.
+ state = getState();
+ }
+ }
+
+ @RequiresNonNull("state")
+ private void updateStateForPendingOperation(
+ ListenableFuture> pendingOperation, Supplier placeholderStateSupplier) {
+ if (pendingOperation.isDone() && pendingOperations.isEmpty()) {
+ updateStateAndInformListeners(getState());
+ } else {
+ pendingOperations.add(pendingOperation);
+ State suggestedPlaceholderState = placeholderStateSupplier.get();
+ updateStateAndInformListeners(getPlaceholderState(suggestedPlaceholderState));
+ pendingOperation.addListener(
+ () -> {
+ castNonNull(state); // Already check by method @RequiresNonNull pre-condition.
+ pendingOperations.remove(pendingOperation);
+ if (pendingOperations.isEmpty()) {
+ updateStateAndInformListeners(getState());
+ }
+ },
+ this::postOrRunOnApplicationHandler);
+ }
+ }
+
+ private void postOrRunOnApplicationHandler(Runnable runnable) {
+ if (applicationHandler.getLooper() == Looper.myLooper()) {
+ runnable.run();
+ } else {
+ applicationHandler.post(runnable);
+ }
+ }
+
+ private static boolean isPlaying(State state) {
+ return state.playWhenReady && false;
+ // TODO: && state.playbackState == Player.STATE_READY
+ // && state.playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE
+ }
+}
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
new file mode 100644
index 0000000000..7c9fcf1afe
--- /dev/null
+++ b/library/common/src/test/java/com/google/android/exoplayer2/SimpleBasePlayerTest.java
@@ -0,0 +1,406 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.os.Looper;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.android.exoplayer2.Player.Commands;
+import com.google.android.exoplayer2.Player.Listener;
+import com.google.android.exoplayer2.SimpleBasePlayer.State;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit test for {@link SimpleBasePlayer}. */
+@RunWith(AndroidJUnit4.class)
+public class SimpleBasePlayerTest {
+
+ @Test
+ public void allPlayerInterfaceMethods_declaredFinal() throws Exception {
+ for (Method method : Player.class.getDeclaredMethods()) {
+ assertThat(
+ SimpleBasePlayer.class
+ .getMethod(method.getName(), method.getParameterTypes())
+ .getModifiers()
+ & Modifier.FINAL)
+ .isNotEqualTo(0);
+ }
+ }
+
+ @Test
+ public void stateBuildUpon_build_isEqual() {
+ State state =
+ new State.Builder()
+ .setAvailableCommands(new Commands.Builder().addAllCommands().build())
+ .setPlayWhenReady(
+ /* playWhenReady= */ true,
+ /* playWhenReadyChangeReason= */ Player
+ .PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS)
+ .build();
+
+ State newState = state.buildUpon().build();
+
+ assertThat(newState).isEqualTo(state);
+ assertThat(newState.hashCode()).isEqualTo(state.hashCode());
+ }
+
+ @Test
+ public void stateBuilderSetAvailableCommands_setsAvailableCommands() {
+ Commands commands =
+ new Commands.Builder()
+ .addAll(Player.COMMAND_GET_DEVICE_VOLUME, Player.COMMAND_GET_TIMELINE)
+ .build();
+ State state = new State.Builder().setAvailableCommands(commands).build();
+
+ assertThat(state.availableCommands).isEqualTo(commands);
+ }
+
+ @Test
+ public void stateBuilderSetPlayWhenReady_setsStatePlayWhenReadyAndReason() {
+ State state =
+ new State.Builder()
+ .setPlayWhenReady(
+ /* playWhenReady= */ true,
+ /* playWhenReadyChangeReason= */ Player
+ .PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS)
+ .build();
+
+ assertThat(state.playWhenReady).isTrue();
+ assertThat(state.playWhenReadyChangeReason)
+ .isEqualTo(Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS);
+ }
+
+ @Test
+ public void getterMethods_noOtherMethodCalls_returnCurrentState() {
+ Commands commands =
+ new Commands.Builder()
+ .addAll(Player.COMMAND_GET_DEVICE_VOLUME, Player.COMMAND_GET_TIMELINE)
+ .build();
+ State state =
+ new State.Builder()
+ .setAvailableCommands(commands)
+ .setPlayWhenReady(
+ /* playWhenReady= */ true,
+ /* playWhenReadyChangeReason= */ Player
+ .PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS)
+ .build();
+ SimpleBasePlayer player =
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return state;
+ }
+ };
+
+ assertThat(player.getApplicationLooper()).isEqualTo(Looper.myLooper());
+ assertThat(player.getAvailableCommands()).isEqualTo(commands);
+ assertThat(player.getPlayWhenReady()).isTrue();
+ }
+
+ @SuppressWarnings("deprecation") // Verifying deprecated listener call.
+ @Test
+ public void invalidateState_updatesStateAndInformsListeners() {
+ State state1 =
+ new State.Builder()
+ .setAvailableCommands(new Commands.Builder().addAllCommands().build())
+ .setPlayWhenReady(
+ /* playWhenReady= */ true,
+ /* playWhenReadyChangeReason= */ Player
+ .PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS)
+ .build();
+ Commands commands = new Commands.Builder().add(Player.COMMAND_GET_TEXT).build();
+ State state2 =
+ new State.Builder()
+ .setAvailableCommands(commands)
+ .setPlayWhenReady(
+ /* playWhenReady= */ false,
+ /* playWhenReadyChangeReason= */ Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE)
+ .build();
+ AtomicBoolean returnState2 = new AtomicBoolean();
+ SimpleBasePlayer player =
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return returnState2.get() ? state2 : state1;
+ }
+ };
+ Listener listener = mock(Listener.class);
+ player.addListener(listener);
+ // Verify state1 is used.
+ assertThat(player.getPlayWhenReady()).isTrue();
+
+ returnState2.set(true);
+ player.invalidateState();
+
+ // Verify updated state.
+ assertThat(player.getAvailableCommands()).isEqualTo(commands);
+ assertThat(player.getPlayWhenReady()).isFalse();
+ // Verify listener calls.
+ verify(listener).onAvailableCommandsChanged(commands);
+ verify(listener)
+ .onPlayWhenReadyChanged(
+ /* playWhenReady= */ false, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE);
+ verify(listener)
+ .onPlayerStateChanged(/* playWhenReady= */ false, /* playbackState= */ Player.STATE_IDLE);
+ verifyNoMoreInteractions(listener);
+ }
+
+ @Test
+ public void invalidateState_duringAsyncMethodHandling_isIgnored() {
+ State state1 =
+ new State.Builder()
+ .setAvailableCommands(new Commands.Builder().addAllCommands().build())
+ .setPlayWhenReady(
+ /* playWhenReady= */ true,
+ /* playWhenReadyChangeReason= */ Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST)
+ .build();
+ State state2 =
+ state1
+ .buildUpon()
+ .setPlayWhenReady(
+ /* playWhenReady= */ false,
+ /* playWhenReadyChangeReason= */ Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE)
+ .build();
+ AtomicReference currentState = new AtomicReference<>(state1);
+ SettableFuture> asyncFuture = SettableFuture.create();
+ SimpleBasePlayer player =
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return currentState.get();
+ }
+
+ @Override
+ protected ListenableFuture> handleSetPlayWhenReady(boolean playWhenReady) {
+ return asyncFuture;
+ }
+ };
+ Listener listener = mock(Listener.class);
+ player.addListener(listener);
+ // Verify state1 is used trigger async method.
+ assertThat(player.getPlayWhenReady()).isTrue();
+ player.setPlayWhenReady(true);
+
+ currentState.set(state2);
+ player.invalidateState();
+
+ // Verify placeholder state is used (and not state2).
+ assertThat(player.getPlayWhenReady()).isTrue();
+
+ // Finish async operation and verify no listeners are informed.
+ currentState.set(state1);
+ asyncFuture.set(null);
+
+ assertThat(player.getPlayWhenReady()).isTrue();
+ verifyNoMoreInteractions(listener);
+ }
+
+ @Test
+ public void overlappingAsyncMethodHandling_onlyUpdatesStateAfterAllDone() {
+ State state1 =
+ new State.Builder()
+ .setAvailableCommands(new Commands.Builder().addAllCommands().build())
+ .setPlayWhenReady(
+ /* playWhenReady= */ true,
+ /* playWhenReadyChangeReason= */ Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST)
+ .build();
+ State state2 =
+ state1
+ .buildUpon()
+ .setPlayWhenReady(
+ /* playWhenReady= */ false,
+ /* playWhenReadyChangeReason= */ Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE)
+ .build();
+ AtomicReference currentState = new AtomicReference<>(state1);
+ ArrayList> asyncFutures = new ArrayList<>();
+ SimpleBasePlayer player =
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return currentState.get();
+ }
+
+ @Override
+ protected ListenableFuture> handleSetPlayWhenReady(boolean playWhenReady) {
+ SettableFuture> future = SettableFuture.create();
+ asyncFutures.add(future);
+ return future;
+ }
+ };
+ Listener listener = mock(Listener.class);
+ player.addListener(listener);
+ // Verify state1 is used.
+ assertThat(player.getPlayWhenReady()).isTrue();
+
+ // Trigger multiple parallel async calls and set state2 (which should never be used).
+ player.setPlayWhenReady(true);
+ currentState.set(state2);
+ assertThat(player.getPlayWhenReady()).isTrue();
+ player.setPlayWhenReady(true);
+ assertThat(player.getPlayWhenReady()).isTrue();
+ player.setPlayWhenReady(true);
+ assertThat(player.getPlayWhenReady()).isTrue();
+
+ // Finish async operation and verify state2 is not used while operations are pending.
+ asyncFutures.get(1).set(null);
+ assertThat(player.getPlayWhenReady()).isTrue();
+ asyncFutures.get(2).set(null);
+ assertThat(player.getPlayWhenReady()).isTrue();
+ verifyNoMoreInteractions(listener);
+
+ // Finish last async operation and verify updated state and listener calls.
+ asyncFutures.get(0).set(null);
+ assertThat(player.getPlayWhenReady()).isFalse();
+ verify(listener)
+ .onPlayWhenReadyChanged(
+ /* playWhenReady= */ false, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE);
+ }
+
+ @SuppressWarnings("deprecation") // Verifying deprecated listener call.
+ @Test
+ public void setPlayWhenReady_immediateHandling_updatesStateAndInformsListeners() {
+ State state =
+ new State.Builder()
+ .setAvailableCommands(new Commands.Builder().addAllCommands().build())
+ .setPlayWhenReady(
+ /* playWhenReady= */ false, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST)
+ .build();
+ State updatedState =
+ state
+ .buildUpon()
+ .setPlayWhenReady(
+ /* playWhenReady= */ true, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE)
+ .build();
+ AtomicBoolean stateUpdated = new AtomicBoolean();
+ SimpleBasePlayer player =
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return stateUpdated.get() ? updatedState : state;
+ }
+
+ @Override
+ protected ListenableFuture> handleSetPlayWhenReady(boolean playWhenReady) {
+ stateUpdated.set(true);
+ return Futures.immediateVoidFuture();
+ }
+ };
+ Listener listener = mock(Listener.class);
+ player.addListener(listener);
+
+ // Intentionally use parameter that doesn't match final result.
+ player.setPlayWhenReady(false);
+
+ assertThat(player.getPlayWhenReady()).isTrue();
+ verify(listener)
+ .onPlayWhenReadyChanged(
+ /* playWhenReady= */ true, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE);
+ verify(listener)
+ .onPlayerStateChanged(/* playWhenReady= */ true, /* playbackState= */ Player.STATE_IDLE);
+ verifyNoMoreInteractions(listener);
+ }
+
+ @SuppressWarnings("deprecation") // Verifying deprecated listener call.
+ @Test
+ public void setPlayWhenReady_asyncHandling_usesPlaceholderStateAndInformsListeners() {
+ State state =
+ new State.Builder()
+ .setAvailableCommands(new Commands.Builder().addAllCommands().build())
+ .setPlayWhenReady(
+ /* playWhenReady= */ false, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST)
+ .build();
+ State updatedState =
+ state
+ .buildUpon()
+ .setPlayWhenReady(
+ /* playWhenReady= */ true, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE)
+ .build();
+ SettableFuture> future = SettableFuture.create();
+ SimpleBasePlayer player =
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return future.isDone() ? updatedState : state;
+ }
+
+ @Override
+ protected ListenableFuture> handleSetPlayWhenReady(boolean playWhenReady) {
+ return future;
+ }
+ };
+ Listener listener = mock(Listener.class);
+ player.addListener(listener);
+
+ player.setPlayWhenReady(true);
+
+ // Verify placeholder state and listener calls.
+ assertThat(player.getPlayWhenReady()).isTrue();
+ verify(listener)
+ .onPlayWhenReadyChanged(
+ /* playWhenReady= */ true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
+ verify(listener)
+ .onPlayerStateChanged(/* playWhenReady= */ true, /* playbackState= */ Player.STATE_IDLE);
+ verifyNoMoreInteractions(listener);
+
+ future.set(null);
+
+ // Verify actual state update.
+ assertThat(player.getPlayWhenReady()).isTrue();
+ verify(listener)
+ .onPlayWhenReadyChanged(
+ /* playWhenReady= */ true, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE);
+ verifyNoMoreInteractions(listener);
+ }
+
+ @Test
+ public void setPlayWhenReady_withoutAvailableCommand_isNotForwarded() {
+ State state =
+ new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder().addAllCommands().remove(Player.COMMAND_PLAY_PAUSE).build())
+ .build();
+ AtomicBoolean callForwarded = new AtomicBoolean();
+ SimpleBasePlayer player =
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return state;
+ }
+
+ @Override
+ protected ListenableFuture> handleSetPlayWhenReady(boolean playWhenReady) {
+ callForwarded.set(true);
+ return Futures.immediateVoidFuture();
+ }
+ };
+
+ player.setPlayWhenReady(true);
+
+ assertThat(callForwarded.get()).isFalse();
+ }
+}