From f55c09cfe26a52018f53e969ef8eacc7beac9123 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 8 Jul 2024 01:12:07 -0700 Subject: [PATCH] Add ForwardingSimpleBasePlayer This utility helps apps to forward to another Player while overriding selected behavior or state values. The advantage to a ForwardingPlayer is that the SimpleBasePlayer base class keeps ensuring correctness, listener handling etc. The default forwarding logic tries to stay as close as possible to the original method calls, even if not strictly required by the Player interface (e.g. calling single item addMediaItem instead of addMediaItems if only one item is added). Issue: androidx/media#1183 PiperOrigin-RevId: 650155924 --- RELEASENOTES.md | 4 + .../common/ForwardingSimpleBasePlayer.java | 499 +++++ .../ForwardingSimpleBasePlayerTest.java | 1611 +++++++++++++++++ 3 files changed, 2114 insertions(+) create mode 100644 libraries/common/src/main/java/androidx/media3/common/ForwardingSimpleBasePlayer.java create mode 100644 libraries/common/src/test/java/androidx/media3/common/ForwardingSimpleBasePlayerTest.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ab48c5581d..84af1d467c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -3,6 +3,10 @@ ### Unreleased changes * Common Library: + * Add `ForwardingSimpleBasePlayer` that allows forwarding to another + player with small adjustments while ensuring full consistency and + listener handling + ([#1183](https://github.com/androidx/media/issues/1183)). * Replace `SimpleBasePlayer.State.playlist` by `getPlaylist()` method. * Add override for `SimpleBasePlayer.State.Builder.setPlaylist()` to directly specify a `Timeline` and current `Tracks` and `Metadata` diff --git a/libraries/common/src/main/java/androidx/media3/common/ForwardingSimpleBasePlayer.java b/libraries/common/src/main/java/androidx/media3/common/ForwardingSimpleBasePlayer.java new file mode 100644 index 0000000000..821f8687de --- /dev/null +++ b/libraries/common/src/main/java/androidx/media3/common/ForwardingSimpleBasePlayer.java @@ -0,0 +1,499 @@ +/* + * Copyright 2024 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 androidx.media3.common; + +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.TextureView; +import androidx.annotation.Nullable; +import androidx.media3.common.util.UnstableApi; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import java.util.List; + +// LINT.IfChange(javadoc) +/** + * A {@link SimpleBasePlayer} that forwards all calls to another {@link Player} instance. + * + *

The class can be used to selectively override {@link #getState()} or {@code handle{Action}} + * methods: + * + *

{@code
+ * new ForwardingSimpleBasePlayer(player) {
+ *   @Override
+ *   protected State getState() {
+ *     State state = super.getState();
+ *     // Modify current state as required:
+ *     return state.buildUpon().setAvailableCommands(filteredCommands).build();
+ *   }
+ *
+ *   @Override
+ *   protected ListenableFuture handleSetRepeatMode(int repeatMode) {
+ *     // Modify actions by directly calling the underlying player as needed:
+ *     getPlayer().setShuffleModeEnabled(true);
+ *     // ..or forward to the default handling with modified parameters:
+ *     return super.handleSetRepeatMode(Player.REPEAT_MODE_ALL);
+ *   }
+ * }
+ * }
+ * + * This base class handles many aspect of the player implementation to simplify the subclass, for + * example listener handling. See the documentation of {@link SimpleBasePlayer} for a more detailed + * description. + */ +@UnstableApi +public class ForwardingSimpleBasePlayer extends SimpleBasePlayer { + + private final Player player; + + private ForwardingPositionSupplier currentPositionSupplier; + private Metadata lastTimedMetadata; + private @Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason; + private @Player.DiscontinuityReason int pendingDiscontinuityReason; + private long pendingPositionDiscontinuityNewPositionMs; + private boolean pendingFirstFrameRendered; + + /** + * Creates the forwarding player. + * + * @param player The {@link Player} to forward to. + */ + public ForwardingSimpleBasePlayer(Player player) { + super(player.getApplicationLooper()); + this.player = player; + this.lastTimedMetadata = new Metadata(/* presentationTimeUs= */ C.TIME_UNSET); + this.playWhenReadyChangeReason = Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST; + this.pendingDiscontinuityReason = Player.DISCONTINUITY_REASON_INTERNAL; + this.currentPositionSupplier = new ForwardingPositionSupplier(player); + player.addListener( + new Listener() { + @Override + public void onMetadata(Metadata metadata) { + lastTimedMetadata = metadata; + } + + @Override + public void onPlayWhenReadyChanged( + boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) { + playWhenReadyChangeReason = reason; + } + + @Override + public void onPositionDiscontinuity( + PositionInfo oldPosition, + PositionInfo newPosition, + @Player.DiscontinuityReason int reason) { + pendingDiscontinuityReason = reason; + pendingPositionDiscontinuityNewPositionMs = newPosition.positionMs; + // Any previously created State will directly call through to player.getCurrentPosition + // via the existing position supplier. From this point onwards, this is wrong as the + // player had a discontinuity and will now return a new position unrelated to the old + // State. We can disconnect these old State objects from the underlying Player by fixing + // the position to the one before the discontinuity and using a new (live) position + // supplier for future State objects. + currentPositionSupplier.setConstant( + oldPosition.positionMs, oldPosition.contentPositionMs); + currentPositionSupplier = new ForwardingPositionSupplier(player); + } + + @Override + public void onRenderedFirstFrame() { + pendingFirstFrameRendered = true; + } + + @SuppressWarnings("method.invocation.invalid") // Calling method from constructor. + @Override + public void onEvents(Player player, Events events) { + invalidateState(); + } + }); + } + + /** Returns the wrapped player. */ + protected final Player getPlayer() { + return player; + } + + @Override + protected State getState() { + // Ordered alphabetically by State.Builder setters. + State.Builder state = new State.Builder(); + ForwardingPositionSupplier positionSupplier = currentPositionSupplier; + if (player.isCommandAvailable(Player.COMMAND_GET_CURRENT_MEDIA_ITEM)) { + state.setAdBufferedPositionMs(positionSupplier::getBufferedPositionMs); + state.setAdPositionMs(positionSupplier::getCurrentPositionMs); + } + if (player.isCommandAvailable(Player.COMMAND_GET_AUDIO_ATTRIBUTES)) { + state.setAudioAttributes(player.getAudioAttributes()); + } + state.setAvailableCommands(player.getAvailableCommands()); + if (player.isCommandAvailable(Player.COMMAND_GET_CURRENT_MEDIA_ITEM)) { + state.setContentBufferedPositionMs(positionSupplier::getContentBufferedPositionMs); + state.setContentPositionMs(positionSupplier::getContentPositionMs); + if (player.isCommandAvailable(Player.COMMAND_GET_TIMELINE)) { + state.setCurrentAd(player.getCurrentAdGroupIndex(), player.getCurrentAdIndexInAdGroup()); + } + } + if (player.isCommandAvailable(Player.COMMAND_GET_TEXT)) { + state.setCurrentCues(player.getCurrentCues()); + } + if (player.isCommandAvailable(Player.COMMAND_GET_TIMELINE)) { + state.setCurrentMediaItemIndex(player.getCurrentMediaItemIndex()); + } + state.setDeviceInfo(player.getDeviceInfo()); + if (player.isCommandAvailable(Player.COMMAND_GET_DEVICE_VOLUME)) { + state.setDeviceVolume(player.getDeviceVolume()); + state.setIsDeviceMuted(player.isDeviceMuted()); + } + state.setIsLoading(player.isLoading()); + state.setMaxSeekToPreviousPositionMs(player.getMaxSeekToPreviousPosition()); + if (pendingFirstFrameRendered) { + state.setNewlyRenderedFirstFrame(true); + pendingFirstFrameRendered = false; + } + state.setPlaybackParameters(player.getPlaybackParameters()); + state.setPlaybackState(player.getPlaybackState()); + state.setPlaybackSuppressionReason(player.getPlaybackSuppressionReason()); + state.setPlayerError(player.getPlayerError()); + if (player.isCommandAvailable(Player.COMMAND_GET_TIMELINE)) { + Tracks tracks = + player.isCommandAvailable(Player.COMMAND_GET_TRACKS) + ? player.getCurrentTracks() + : Tracks.EMPTY; + MediaMetadata mediaMetadata = + player.isCommandAvailable(Player.COMMAND_GET_METADATA) ? player.getMediaMetadata() : null; + state.setPlaylist(player.getCurrentTimeline(), tracks, mediaMetadata); + } + if (player.isCommandAvailable(Player.COMMAND_GET_METADATA)) { + state.setPlaylistMetadata(player.getPlaylistMetadata()); + } + state.setPlayWhenReady(player.getPlayWhenReady(), playWhenReadyChangeReason); + if (pendingPositionDiscontinuityNewPositionMs != C.TIME_UNSET) { + state.setPositionDiscontinuity( + pendingDiscontinuityReason, pendingPositionDiscontinuityNewPositionMs); + pendingPositionDiscontinuityNewPositionMs = C.TIME_UNSET; + } + state.setRepeatMode(player.getRepeatMode()); + state.setSeekBackIncrementMs(player.getSeekBackIncrement()); + state.setSeekForwardIncrementMs(player.getSeekForwardIncrement()); + state.setShuffleModeEnabled(player.getShuffleModeEnabled()); + state.setSurfaceSize(player.getSurfaceSize()); + state.setTimedMetadata(lastTimedMetadata); + if (player.isCommandAvailable(Player.COMMAND_GET_CURRENT_MEDIA_ITEM)) { + state.setTotalBufferedDurationMs(positionSupplier::getTotalBufferedDurationMs); + } + state.setTrackSelectionParameters(player.getTrackSelectionParameters()); + state.setVideoSize(player.getVideoSize()); + if (player.isCommandAvailable(Player.COMMAND_GET_VOLUME)) { + state.setVolume(player.getVolume()); + } + return state.build(); + } + + @Override + protected ListenableFuture handleSetPlayWhenReady(boolean playWhenReady) { + player.setPlayWhenReady(playWhenReady); + return Futures.immediateVoidFuture(); + } + + @Override + protected ListenableFuture handlePrepare() { + player.prepare(); + return Futures.immediateVoidFuture(); + } + + @Override + protected ListenableFuture handleStop() { + player.stop(); + return Futures.immediateVoidFuture(); + } + + @Override + protected ListenableFuture handleRelease() { + player.release(); + return Futures.immediateVoidFuture(); + } + + @Override + protected ListenableFuture handleSetRepeatMode(@Player.RepeatMode int repeatMode) { + player.setRepeatMode(repeatMode); + return Futures.immediateVoidFuture(); + } + + @Override + protected ListenableFuture handleSetShuffleModeEnabled(boolean shuffleModeEnabled) { + player.setShuffleModeEnabled(shuffleModeEnabled); + return Futures.immediateVoidFuture(); + } + + @Override + protected ListenableFuture handleSetPlaybackParameters(PlaybackParameters playbackParameters) { + player.setPlaybackParameters(playbackParameters); + return Futures.immediateVoidFuture(); + } + + @Override + protected ListenableFuture handleSetTrackSelectionParameters( + TrackSelectionParameters trackSelectionParameters) { + player.setTrackSelectionParameters(trackSelectionParameters); + return Futures.immediateVoidFuture(); + } + + @Override + protected ListenableFuture handleSetPlaylistMetadata(MediaMetadata playlistMetadata) { + player.setPlaylistMetadata(playlistMetadata); + return Futures.immediateVoidFuture(); + } + + @Override + protected ListenableFuture handleSetVolume(float volume) { + player.setVolume(volume); + return Futures.immediateVoidFuture(); + } + + @SuppressWarnings("deprecation") // Calling deprecated method if updated command not available. + @Override + protected ListenableFuture handleSetDeviceVolume(int deviceVolume, int flags) { + if (player.isCommandAvailable(Player.COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS)) { + player.setDeviceVolume(deviceVolume, flags); + } else { + player.setDeviceVolume(deviceVolume); + } + return Futures.immediateVoidFuture(); + } + + @SuppressWarnings("deprecation") // Calling deprecated method if updated command not available. + @Override + protected ListenableFuture handleIncreaseDeviceVolume(@C.VolumeFlags int flags) { + if (player.isCommandAvailable(Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS)) { + player.increaseDeviceVolume(flags); + } else { + player.increaseDeviceVolume(); + } + return Futures.immediateVoidFuture(); + } + + @SuppressWarnings("deprecation") // Calling deprecated method if updated command not available. + @Override + protected ListenableFuture handleDecreaseDeviceVolume(@C.VolumeFlags int flags) { + if (player.isCommandAvailable(Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS)) { + player.decreaseDeviceVolume(flags); + } else { + player.decreaseDeviceVolume(); + } + return Futures.immediateVoidFuture(); + } + + @SuppressWarnings("deprecation") // Calling deprecated method if updated command not available. + @Override + protected ListenableFuture handleSetDeviceMuted(boolean muted, @C.VolumeFlags int flags) { + if (player.isCommandAvailable(Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS)) { + player.setDeviceMuted(muted, flags); + } else { + player.setDeviceMuted(muted); + } + + return Futures.immediateVoidFuture(); + } + + @Override + protected ListenableFuture handleSetAudioAttributes( + AudioAttributes audioAttributes, boolean handleAudioFocus) { + player.setAudioAttributes(audioAttributes, handleAudioFocus); + return Futures.immediateVoidFuture(); + } + + @Override + protected ListenableFuture handleSetVideoOutput(Object videoOutput) { + if (videoOutput instanceof SurfaceView) { + player.setVideoSurfaceView((SurfaceView) videoOutput); + } else if (videoOutput instanceof TextureView) { + player.setVideoTextureView((TextureView) videoOutput); + } else if (videoOutput instanceof SurfaceHolder) { + player.setVideoSurfaceHolder((SurfaceHolder) videoOutput); + } else if (videoOutput instanceof Surface) { + player.setVideoSurface((Surface) videoOutput); + } else { + throw new IllegalStateException(); + } + return Futures.immediateVoidFuture(); + } + + @Override + protected ListenableFuture handleClearVideoOutput(@Nullable Object videoOutput) { + if (videoOutput instanceof SurfaceView) { + player.clearVideoSurfaceView((SurfaceView) videoOutput); + } else if (videoOutput instanceof TextureView) { + player.clearVideoTextureView((TextureView) videoOutput); + } else if (videoOutput instanceof SurfaceHolder) { + player.clearVideoSurfaceHolder((SurfaceHolder) videoOutput); + } else if (videoOutput instanceof Surface) { + player.clearVideoSurface((Surface) videoOutput); + } else if (videoOutput == null) { + player.clearVideoSurface(); + } else { + throw new IllegalStateException(); + } + return Futures.immediateVoidFuture(); + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + boolean useSingleItemCall = + mediaItems.size() == 1 && player.isCommandAvailable(Player.COMMAND_SET_MEDIA_ITEM); + if (startIndex == C.INDEX_UNSET) { + if (useSingleItemCall) { + player.setMediaItem(mediaItems.get(0)); + } else { + player.setMediaItems(mediaItems); + } + } else { + if (useSingleItemCall) { + player.setMediaItem(mediaItems.get(0), startPositionMs); + } else { + player.setMediaItems(mediaItems, startIndex, startPositionMs); + } + } + return Futures.immediateVoidFuture(); + } + + @Override + protected ListenableFuture handleAddMediaItems(int index, List mediaItems) { + if (mediaItems.size() == 1) { + player.addMediaItem(index, mediaItems.get(0)); + } else { + player.addMediaItems(index, mediaItems); + } + return Futures.immediateVoidFuture(); + } + + @Override + protected ListenableFuture handleMoveMediaItems(int fromIndex, int toIndex, int newIndex) { + if (toIndex == fromIndex + 1) { + player.moveMediaItem(fromIndex, newIndex); + } else { + player.moveMediaItems(fromIndex, toIndex, newIndex); + } + return Futures.immediateVoidFuture(); + } + + @Override + protected ListenableFuture handleReplaceMediaItems( + int fromIndex, int toIndex, List mediaItems) { + if (toIndex == fromIndex + 1 && mediaItems.size() == 1) { + player.replaceMediaItem(fromIndex, mediaItems.get(0)); + } else { + player.replaceMediaItems(fromIndex, toIndex, mediaItems); + } + return Futures.immediateVoidFuture(); + } + + @Override + protected ListenableFuture handleRemoveMediaItems(int fromIndex, int toIndex) { + if (toIndex == fromIndex + 1) { + player.removeMediaItem(fromIndex); + } else { + player.removeMediaItems(fromIndex, toIndex); + } + return Futures.immediateVoidFuture(); + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Command int seekCommand) { + switch (seekCommand) { + case Player.COMMAND_SEEK_BACK: + player.seekBack(); + break; + case Player.COMMAND_SEEK_FORWARD: + player.seekForward(); + break; + case Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM: + player.seekTo(positionMs); + break; + case Player.COMMAND_SEEK_TO_DEFAULT_POSITION: + player.seekToDefaultPosition(); + break; + case Player.COMMAND_SEEK_TO_MEDIA_ITEM: + if (mediaItemIndex != C.INDEX_UNSET) { + player.seekTo(mediaItemIndex, positionMs); + } + break; + case Player.COMMAND_SEEK_TO_NEXT: + player.seekToNext(); + break; + case Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM: + player.seekToNextMediaItem(); + break; + case Player.COMMAND_SEEK_TO_PREVIOUS: + player.seekToPrevious(); + break; + case Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM: + player.seekToPreviousMediaItem(); + break; + default: + throw new IllegalStateException(); + } + return Futures.immediateVoidFuture(); + } + + /** + * Forwards to the changing position values of the wrapped player until the forwarding is + * deactivated with constant values. + */ + private static final class ForwardingPositionSupplier { + + private final Player player; + + private long positionsMs; + private long contentPositionMs; + + public ForwardingPositionSupplier(Player player) { + this.player = player; + this.positionsMs = C.TIME_UNSET; + this.contentPositionMs = C.TIME_UNSET; + } + + public void setConstant(long positionMs, long contentPositionMs) { + this.positionsMs = positionMs; + this.contentPositionMs = contentPositionMs; + } + + public long getCurrentPositionMs() { + return positionsMs == C.TIME_UNSET ? player.getCurrentPosition() : positionsMs; + } + + public long getBufferedPositionMs() { + return positionsMs == C.TIME_UNSET ? player.getBufferedPosition() : positionsMs; + } + + public long getContentPositionMs() { + return contentPositionMs == C.TIME_UNSET ? player.getContentPosition() : contentPositionMs; + } + + public long getContentBufferedPositionMs() { + return contentPositionMs == C.TIME_UNSET + ? player.getContentBufferedPosition() + : contentPositionMs; + } + + public long getTotalBufferedDurationMs() { + return positionsMs == C.TIME_UNSET ? player.getTotalBufferedDuration() : 0; + } + } +} diff --git a/libraries/common/src/test/java/androidx/media3/common/ForwardingSimpleBasePlayerTest.java b/libraries/common/src/test/java/androidx/media3/common/ForwardingSimpleBasePlayerTest.java new file mode 100644 index 0000000000..8def2784d5 --- /dev/null +++ b/libraries/common/src/test/java/androidx/media3/common/ForwardingSimpleBasePlayerTest.java @@ -0,0 +1,1611 @@ +/* + * Copyright 2024 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 androidx.media3.common; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import android.graphics.SurfaceTexture; +import android.os.Looper; +import android.view.Surface; +import android.view.SurfaceView; +import android.view.TextureView; +import androidx.annotation.Nullable; +import androidx.media3.common.text.Cue; +import androidx.media3.common.text.CueGroup; +import androidx.media3.common.util.Size; +import androidx.media3.test.utils.FakeMetadataEntry; +import androidx.media3.test.utils.TestUtil; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.shadows.ShadowLooper; +import org.robolectric.shadows.ShadowSurfaceView; + +/** Unit test for {@link ForwardingSimpleBasePlayer}. */ +@RunWith(AndroidJUnit4.class) +public final class ForwardingSimpleBasePlayerTest { + + @Test + public void forwardingSimpleBasePlayer_overridesAllSimpleBasePlayerMethods() throws Exception { + // Check with reflection that ForwardingSimpleBasePlayer overrides all overridable + // SimpleBasePlayer methods. This guards against accidentally forgetting to forward a method. + for (Method method : SimpleBasePlayer.class.getDeclaredMethods()) { + int modifiers = method.getModifiers(); + if (!Modifier.isPrivate(modifiers) + && !Modifier.isFinal(modifiers) + && !method.isSynthetic() + && !method.getName().equals("getPlaceholderState") + && !method.getName().equals("getPlaceholderMediaItemData")) { + assertThat( + ForwardingSimpleBasePlayer.class + .getDeclaredMethod(method.getName(), method.getParameterTypes()) + .getDeclaringClass()) + .isEqualTo(ForwardingSimpleBasePlayer.class); + } + } + } + + @Test + public void getterMethods_noOtherMethodCalls_returnCurrentStateFromWrappedPlayer() { + Player.Commands commands = + new Player.Commands.Builder() + .addAll(Player.COMMAND_GET_DEVICE_VOLUME, Player.COMMAND_GET_TIMELINE) + .build(); + PlaybackException error = + new PlaybackException( + /* message= */ null, /* cause= */ null, PlaybackException.ERROR_CODE_DECODING_FAILED); + PlaybackParameters playbackParameters = new PlaybackParameters(/* speed= */ 2f); + TrackSelectionParameters trackSelectionParameters = + TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT + .buildUpon() + .setMaxVideoBitrate(1000) + .build(); + AudioAttributes audioAttributes = + new AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MOVIE).build(); + VideoSize videoSize = new VideoSize(/* width= */ 200, /* height= */ 400); + CueGroup cueGroup = + new CueGroup( + ImmutableList.of(new Cue.Builder().setText("text").build()), + /* presentationTimeUs= */ 123); + DeviceInfo deviceInfo = + new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_LOCAL).setMaxVolume(7).build(); + MediaMetadata playlistMetadata = new MediaMetadata.Builder().setArtist("artist").build(); + SimpleBasePlayer.PositionSupplier contentPositionSupplier = () -> 456; + SimpleBasePlayer.PositionSupplier contentBufferedPositionSupplier = () -> 499; + SimpleBasePlayer.PositionSupplier totalBufferedPositionSupplier = () -> 567; + Object mediaItemUid = new Object(); + Object periodUid = new Object(); + Tracks tracks = + new Tracks( + ImmutableList.of( + new Tracks.Group( + new TrackGroup(new Format.Builder().build()), + /* adaptiveSupported= */ true, + /* trackSupport= */ new int[] {C.FORMAT_HANDLED}, + /* trackSelected= */ new boolean[] {true}))); + MediaItem mediaItem = new MediaItem.Builder().setMediaId("id").build(); + MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("title").build(); + Object manifest = new Object(); + Size surfaceSize = new Size(480, 360); + MediaItem.LiveConfiguration liveConfiguration = + new MediaItem.LiveConfiguration.Builder().setTargetOffsetMs(2000).build(); + ImmutableList playlist = + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build(), + new SimpleBasePlayer.MediaItemData.Builder(mediaItemUid) + .setTracks(tracks) + .setMediaItem(mediaItem) + .setMediaMetadata(mediaMetadata) + .setManifest(manifest) + .setLiveConfiguration(liveConfiguration) + .setPresentationStartTimeMs(12) + .setWindowStartTimeMs(23) + .setElapsedRealtimeEpochOffsetMs(10234) + .setIsSeekable(true) + .setIsDynamic(true) + .setDefaultPositionUs(456_789) + .setDurationUs(500_000) + .setPositionInFirstPeriodUs(100_000) + .setIsPlaceholder(true) + .setPeriods( + ImmutableList.of( + new SimpleBasePlayer.PeriodData.Builder(periodUid) + .setIsPlaceholder(true) + .setDurationUs(600_000) + .setAdPlaybackState( + new AdPlaybackState( + /* adsId= */ new Object(), /* adGroupTimesUs...= */ 555, 666)) + .build())) + .build()); + SimpleBasePlayer.State state = + new SimpleBasePlayer.State.Builder() + .setAvailableCommands(commands) + .setPlayWhenReady( + /* playWhenReady= */ true, + /* playWhenReadyChangeReason= */ Player + .PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS) + .setPlaybackState(Player.STATE_IDLE) + .setPlaybackSuppressionReason( + Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS) + .setPlayerError(error) + .setRepeatMode(Player.REPEAT_MODE_ALL) + .setShuffleModeEnabled(true) + .setIsLoading(false) + .setSeekBackIncrementMs(5000) + .setSeekForwardIncrementMs(4000) + .setMaxSeekToPreviousPositionMs(3000) + .setPlaybackParameters(playbackParameters) + .setTrackSelectionParameters(trackSelectionParameters) + .setAudioAttributes(audioAttributes) + .setVolume(0.5f) + .setVideoSize(videoSize) + .setCurrentCues(cueGroup) + .setDeviceInfo(deviceInfo) + .setDeviceVolume(5) + .setIsDeviceMuted(true) + .setSurfaceSize(surfaceSize) + .setPlaylist(playlist) + .setPlaylistMetadata(playlistMetadata) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(contentPositionSupplier) + .setContentBufferedPositionMs(contentBufferedPositionSupplier) + .setTotalBufferedDurationMs(totalBufferedPositionSupplier) + .build(); + Player wrappedPlayer = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + }; + + Player forwardingPlayer = new ForwardingPlayer(wrappedPlayer); + + assertThat(forwardingPlayer.getApplicationLooper()).isEqualTo(Looper.myLooper()); + assertThat(forwardingPlayer.getAvailableCommands()).isEqualTo(commands); + assertThat(forwardingPlayer.getPlayWhenReady()).isTrue(); + assertThat(forwardingPlayer.getPlaybackState()).isEqualTo(Player.STATE_IDLE); + assertThat(forwardingPlayer.getPlaybackSuppressionReason()) + .isEqualTo(Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS); + assertThat(forwardingPlayer.getPlayerError()).isEqualTo(error); + assertThat(forwardingPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ALL); + assertThat(forwardingPlayer.getShuffleModeEnabled()).isTrue(); + assertThat(forwardingPlayer.isLoading()).isFalse(); + assertThat(forwardingPlayer.getSeekBackIncrement()).isEqualTo(5000); + assertThat(forwardingPlayer.getSeekForwardIncrement()).isEqualTo(4000); + assertThat(forwardingPlayer.getMaxSeekToPreviousPosition()).isEqualTo(3000); + assertThat(forwardingPlayer.getPlaybackParameters()).isEqualTo(playbackParameters); + assertThat(forwardingPlayer.getCurrentTracks()).isEqualTo(tracks); + assertThat(forwardingPlayer.getTrackSelectionParameters()).isEqualTo(trackSelectionParameters); + assertThat(forwardingPlayer.getMediaMetadata()).isEqualTo(mediaMetadata); + assertThat(forwardingPlayer.getPlaylistMetadata()).isEqualTo(playlistMetadata); + assertThat(forwardingPlayer.getCurrentPeriodIndex()).isEqualTo(1); + assertThat(forwardingPlayer.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(forwardingPlayer.getDuration()).isEqualTo(500); + assertThat(forwardingPlayer.getCurrentPosition()).isEqualTo(456); + assertThat(forwardingPlayer.getBufferedPosition()).isEqualTo(499); + assertThat(forwardingPlayer.getTotalBufferedDuration()).isEqualTo(567); + assertThat(forwardingPlayer.isPlayingAd()).isFalse(); + assertThat(forwardingPlayer.getCurrentAdGroupIndex()).isEqualTo(C.INDEX_UNSET); + assertThat(forwardingPlayer.getCurrentAdIndexInAdGroup()).isEqualTo(C.INDEX_UNSET); + assertThat(forwardingPlayer.getContentPosition()).isEqualTo(456); + assertThat(forwardingPlayer.getContentBufferedPosition()).isEqualTo(499); + assertThat(forwardingPlayer.getAudioAttributes()).isEqualTo(audioAttributes); + assertThat(forwardingPlayer.getVolume()).isEqualTo(0.5f); + assertThat(forwardingPlayer.getVideoSize()).isEqualTo(videoSize); + assertThat(forwardingPlayer.getCurrentCues()).isEqualTo(cueGroup); + assertThat(forwardingPlayer.getDeviceInfo()).isEqualTo(deviceInfo); + assertThat(forwardingPlayer.getDeviceVolume()).isEqualTo(5); + assertThat(forwardingPlayer.isDeviceMuted()).isTrue(); + assertThat(forwardingPlayer.getSurfaceSize()).isEqualTo(surfaceSize); + Timeline timeline = forwardingPlayer.getCurrentTimeline(); + assertThat(timeline.getPeriodCount()).isEqualTo(2); + assertThat(timeline.getWindowCount()).isEqualTo(2); + Timeline.Window window = timeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()); + assertThat(window.defaultPositionUs).isEqualTo(0); + assertThat(window.durationUs).isEqualTo(C.TIME_UNSET); + assertThat(window.elapsedRealtimeEpochOffsetMs).isEqualTo(C.TIME_UNSET); + assertThat(window.firstPeriodIndex).isEqualTo(0); + assertThat(window.isDynamic).isFalse(); + assertThat(window.isPlaceholder).isFalse(); + assertThat(window.isSeekable).isFalse(); + assertThat(window.lastPeriodIndex).isEqualTo(0); + assertThat(window.positionInFirstPeriodUs).isEqualTo(0); + assertThat(window.presentationStartTimeMs).isEqualTo(C.TIME_UNSET); + assertThat(window.windowStartTimeMs).isEqualTo(C.TIME_UNSET); + assertThat(window.liveConfiguration).isNull(); + assertThat(window.manifest).isNull(); + assertThat(window.mediaItem).isEqualTo(MediaItem.EMPTY); + window = timeline.getWindow(/* windowIndex= */ 1, new Timeline.Window()); + assertThat(window.defaultPositionUs).isEqualTo(456_789); + assertThat(window.durationUs).isEqualTo(500_000); + assertThat(window.elapsedRealtimeEpochOffsetMs).isEqualTo(10234); + assertThat(window.firstPeriodIndex).isEqualTo(1); + assertThat(window.isDynamic).isTrue(); + assertThat(window.isPlaceholder).isTrue(); + assertThat(window.isSeekable).isTrue(); + assertThat(window.lastPeriodIndex).isEqualTo(1); + assertThat(window.positionInFirstPeriodUs).isEqualTo(100_000); + assertThat(window.presentationStartTimeMs).isEqualTo(12); + assertThat(window.windowStartTimeMs).isEqualTo(23); + assertThat(window.liveConfiguration).isEqualTo(liveConfiguration); + assertThat(window.manifest).isEqualTo(manifest); + assertThat(window.mediaItem).isEqualTo(mediaItem); + assertThat(window.uid).isEqualTo(mediaItemUid); + Timeline.Period period = + timeline.getPeriod(/* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true); + assertThat(period.durationUs).isEqualTo(C.TIME_UNSET); + assertThat(period.isPlaceholder).isFalse(); + assertThat(period.positionInWindowUs).isEqualTo(0); + assertThat(period.windowIndex).isEqualTo(0); + assertThat(period.getAdGroupCount()).isEqualTo(0); + period = timeline.getPeriod(/* periodIndex= */ 1, new Timeline.Period(), /* setIds= */ true); + assertThat(period.durationUs).isEqualTo(600_000); + assertThat(period.isPlaceholder).isTrue(); + assertThat(period.positionInWindowUs).isEqualTo(-100_000); + assertThat(period.windowIndex).isEqualTo(1); + assertThat(period.id).isEqualTo(periodUid); + assertThat(period.getAdGroupCount()).isEqualTo(2); + assertThat(period.getAdGroupTimeUs(/* adGroupIndex= */ 0)).isEqualTo(555); + assertThat(period.getAdGroupTimeUs(/* adGroupIndex= */ 1)).isEqualTo(666); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener call. + @Test + public void stateChangesInWrappedPlayer_areForwardedToListeners() throws Exception { + Object mediaItemUid0 = new Object(); + MediaItem mediaItem0 = new MediaItem.Builder().setMediaId("0").build(); + SimpleBasePlayer.MediaItemData mediaItemData0 = + new SimpleBasePlayer.MediaItemData.Builder(mediaItemUid0).setMediaItem(mediaItem0).build(); + SimpleBasePlayer.State state1 = + new SimpleBasePlayer.State.Builder() + .setAvailableCommands( + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SET_MEDIA_ITEM) + .build()) + .setPlayWhenReady( + /* playWhenReady= */ true, + /* playWhenReadyChangeReason= */ Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST) + .setPlaybackState(Player.STATE_READY) + .setPlaybackSuppressionReason(Player.PLAYBACK_SUPPRESSION_REASON_NONE) + .setPlayerError(null) + .setRepeatMode(Player.REPEAT_MODE_ONE) + .setShuffleModeEnabled(false) + .setIsLoading(true) + .setSeekBackIncrementMs(7000) + .setSeekForwardIncrementMs(2000) + .setMaxSeekToPreviousPositionMs(8000) + .setPlaybackParameters(PlaybackParameters.DEFAULT) + .setTrackSelectionParameters(TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT) + .setAudioAttributes(AudioAttributes.DEFAULT) + .setVolume(1f) + .setVideoSize(VideoSize.UNKNOWN) + .setCurrentCues(CueGroup.EMPTY_TIME_ZERO) + .setDeviceInfo(DeviceInfo.UNKNOWN) + .setDeviceVolume(0) + .setIsDeviceMuted(false) + .setPlaylist(ImmutableList.of(mediaItemData0)) + .setPlaylistMetadata(MediaMetadata.EMPTY) + .setCurrentMediaItemIndex(0) + .setContentPositionMs(8_000) + .build(); + Object mediaItemUid1 = new Object(); + MediaItem mediaItem1 = new MediaItem.Builder().setMediaId("1").build(); + MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("title").build(); + Player.Commands commands = + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS) + .build(); + Tracks tracks = + new Tracks( + ImmutableList.of( + new Tracks.Group( + new TrackGroup(new Format.Builder().build()), + /* adaptiveSupported= */ true, + /* trackSupport= */ new int[] {C.FORMAT_HANDLED}, + /* trackSelected= */ new boolean[] {true}))); + SimpleBasePlayer.MediaItemData mediaItemData1 = + new SimpleBasePlayer.MediaItemData.Builder(mediaItemUid1) + .setMediaItem(mediaItem1) + .setMediaMetadata(mediaMetadata) + .setTracks(tracks) + .build(); + PlaybackException error = + new PlaybackException( + /* message= */ null, /* cause= */ null, PlaybackException.ERROR_CODE_DECODING_FAILED); + PlaybackParameters playbackParameters = new PlaybackParameters(/* speed= */ 2f); + TrackSelectionParameters trackSelectionParameters = + TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT + .buildUpon() + .setMaxVideoBitrate(1000) + .build(); + AudioAttributes audioAttributes = + new AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MOVIE).build(); + VideoSize videoSize = new VideoSize(/* width= */ 200, /* height= */ 400); + CueGroup cueGroup = + new CueGroup( + ImmutableList.of(new Cue.Builder().setText("text").build()), + /* presentationTimeUs= */ 123); + Metadata timedMetadata = + new Metadata(/* presentationTimeUs= */ 42, new FakeMetadataEntry("data")); + Size surfaceSize = new Size(480, 360); + DeviceInfo deviceInfo = + new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_LOCAL).setMaxVolume(7).build(); + MediaMetadata playlistMetadata = new MediaMetadata.Builder().setArtist("artist").build(); + SimpleBasePlayer.State state2 = + new SimpleBasePlayer.State.Builder() + .setAvailableCommands(commands) + .setPlayWhenReady( + /* playWhenReady= */ false, + /* playWhenReadyChangeReason= */ Player + .PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS) + .setPlaybackState(Player.STATE_IDLE) + .setPlaybackSuppressionReason( + Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS) + .setPlayerError(error) + .setRepeatMode(Player.REPEAT_MODE_ALL) + .setShuffleModeEnabled(true) + .setIsLoading(false) + .setSeekBackIncrementMs(5000) + .setSeekForwardIncrementMs(4000) + .setMaxSeekToPreviousPositionMs(3000) + .setPlaybackParameters(playbackParameters) + .setTrackSelectionParameters(trackSelectionParameters) + .setAudioAttributes(audioAttributes) + .setVolume(0.5f) + .setVideoSize(videoSize) + .setCurrentCues(cueGroup) + .setDeviceInfo(deviceInfo) + .setDeviceVolume(5) + .setIsDeviceMuted(true) + .setSurfaceSize(surfaceSize) + .setNewlyRenderedFirstFrame(true) + .setTimedMetadata(timedMetadata) + .setPlaylist(ImmutableList.of(mediaItemData0, mediaItemData1)) + .setPlaylistMetadata(playlistMetadata) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(12_000) + .setPositionDiscontinuity( + Player.DISCONTINUITY_REASON_SEEK, /* discontinuityPositionMs= */ 11_500) + .build(); + AtomicBoolean returnState2 = new AtomicBoolean(); + SimpleBasePlayer innerPlayer = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return returnState2.get() ? state2 : state1; + } + }; + ForwardingSimpleBasePlayer forwardingPlayer = new ForwardingSimpleBasePlayer(innerPlayer); + // Ensure state1 is used. + assertThat(forwardingPlayer.getPlayWhenReady()).isTrue(); + Player.Listener listener = mock(Player.Listener.class); + forwardingPlayer.addListener(listener); + + returnState2.set(true); + innerPlayer.invalidateState(); + // Idle Looper to ensure all callbacks (including onEvents) are delivered. + ShadowLooper.idleMainLooper(); + + verify(listener).onAvailableCommandsChanged(commands); + verify(listener) + .onPlayWhenReadyChanged( + /* playWhenReady= */ false, Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS); + verify(listener) + .onPlayerStateChanged(/* playWhenReady= */ false, /* playbackState= */ Player.STATE_IDLE); + verify(listener).onPlaybackStateChanged(Player.STATE_IDLE); + verify(listener) + .onPlaybackSuppressionReasonChanged( + Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS); + verify(listener).onIsPlayingChanged(false); + verify(listener).onPlayerError(error); + verify(listener).onPlayerErrorChanged(error); + verify(listener).onRepeatModeChanged(Player.REPEAT_MODE_ALL); + verify(listener).onShuffleModeEnabledChanged(true); + verify(listener).onLoadingChanged(false); + verify(listener).onIsLoadingChanged(false); + verify(listener).onSeekBackIncrementChanged(5000); + verify(listener).onSeekForwardIncrementChanged(4000); + verify(listener).onMaxSeekToPreviousPositionChanged(3000); + verify(listener).onPlaybackParametersChanged(playbackParameters); + verify(listener).onTrackSelectionParametersChanged(trackSelectionParameters); + verify(listener).onAudioAttributesChanged(audioAttributes); + verify(listener).onVolumeChanged(0.5f); + verify(listener).onVideoSizeChanged(videoSize); + verify(listener).onCues(cueGroup.cues); + verify(listener).onCues(cueGroup); + verify(listener).onDeviceInfoChanged(deviceInfo); + verify(listener).onDeviceVolumeChanged(/* volume= */ 5, /* muted= */ true); + verify(listener) + .onTimelineChanged(state2.timeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verify(listener).onMediaMetadataChanged(mediaMetadata); + verify(listener).onTracksChanged(tracks); + verify(listener).onPlaylistMetadataChanged(playlistMetadata); + verify(listener).onRenderedFirstFrame(); + verify(listener).onMetadata(timedMetadata); + verify(listener).onSurfaceSizeChanged(surfaceSize.getWidth(), surfaceSize.getHeight()); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + verify(listener) + .onPositionDiscontinuity( + /* oldPosition= */ new Player.PositionInfo( + mediaItemUid0, + /* mediaItemIndex= */ 0, + mediaItem0, + /* periodUid= */ mediaItemUid0, + /* periodIndex= */ 0, + /* positionMs= */ 8_000, + /* contentPositionMs= */ 8_000, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET), + /* newPosition= */ new Player.PositionInfo( + mediaItemUid1, + /* mediaItemIndex= */ 1, + mediaItem1, + /* periodUid= */ mediaItemUid1, + /* periodIndex= */ 1, + /* positionMs= */ 11_500, + /* contentPositionMs= */ 11_500, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET), + Player.DISCONTINUITY_REASON_SEEK); + verify(listener).onMediaItemTransition(mediaItem1, Player.MEDIA_ITEM_TRANSITION_REASON_SEEK); + verify(listener) + .onEvents( + forwardingPlayer, + new Player.Events( + new FlagSet.Builder() + .addAll( + Player.EVENT_TIMELINE_CHANGED, + Player.EVENT_MEDIA_ITEM_TRANSITION, + Player.EVENT_TRACKS_CHANGED, + Player.EVENT_IS_LOADING_CHANGED, + Player.EVENT_PLAYBACK_STATE_CHANGED, + Player.EVENT_PLAY_WHEN_READY_CHANGED, + Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED, + Player.EVENT_IS_PLAYING_CHANGED, + Player.EVENT_REPEAT_MODE_CHANGED, + Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED, + Player.EVENT_PLAYER_ERROR, + Player.EVENT_POSITION_DISCONTINUITY, + Player.EVENT_PLAYBACK_PARAMETERS_CHANGED, + Player.EVENT_AVAILABLE_COMMANDS_CHANGED, + Player.EVENT_MEDIA_METADATA_CHANGED, + Player.EVENT_PLAYLIST_METADATA_CHANGED, + Player.EVENT_SEEK_BACK_INCREMENT_CHANGED, + Player.EVENT_SEEK_FORWARD_INCREMENT_CHANGED, + Player.EVENT_MAX_SEEK_TO_PREVIOUS_POSITION_CHANGED, + Player.EVENT_TRACK_SELECTION_PARAMETERS_CHANGED, + Player.EVENT_AUDIO_ATTRIBUTES_CHANGED, + Player.EVENT_VOLUME_CHANGED, + Player.EVENT_SURFACE_SIZE_CHANGED, + Player.EVENT_VIDEO_SIZE_CHANGED, + Player.EVENT_RENDERED_FIRST_FRAME, + Player.EVENT_CUES, + Player.EVENT_METADATA, + Player.EVENT_DEVICE_INFO_CHANGED, + Player.EVENT_DEVICE_VOLUME_CHANGED) + .build())); + verifyNoMoreInteractions(listener); + // Assert that we actually called all listeners. This guards against forgetting a State setter + // when forwarding state in ForwardingSimpleBasePlayer.getState(). + for (Method method : TestUtil.getPublicMethods(Player.Listener.class)) { + if (method.getName().equals("onAudioSessionIdChanged") + || method.getName().equals("onSkipSilenceEnabledChanged")) { + // Skip listeners for ExoPlayer-specific states + continue; + } + method.invoke(verify(listener), getAnyArguments(method)); + } + } + + @Test + public void setPlayWhenReady_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder().add(Player.COMMAND_PLAY_PAUSE).build()) + .build(); + } + + @Override + protected ListenableFuture handleSetPlayWhenReady(boolean playWhenReady) { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + + forwardingPlayer.setPlayWhenReady(true); + forwardingPlayer.play(); + forwardingPlayer.pause(); + + verify(wrappedPlayer, times(2)).setPlayWhenReady(true); + verify(wrappedPlayer).setPlayWhenReady(false); + } + + @Test + public void prepare_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder().add(Player.COMMAND_PREPARE).build()) + .build(); + } + + @Override + protected ListenableFuture handlePrepare() { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + + forwardingPlayer.prepare(); + + verify(wrappedPlayer).prepare(); + } + + @Test + public void stop_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder().add(Player.COMMAND_STOP).build()) + .build(); + } + + @Override + protected ListenableFuture handleStop() { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + + forwardingPlayer.stop(); + + verify(wrappedPlayer).stop(); + } + + @Test + public void release_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder().add(Player.COMMAND_RELEASE).build()) + .build(); + } + + @Override + protected ListenableFuture handleRelease() { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + + forwardingPlayer.release(); + + verify(wrappedPlayer).release(); + } + + @Test + public void setRepeatMode_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder().add(Player.COMMAND_SET_REPEAT_MODE).build()) + .build(); + } + + @Override + protected ListenableFuture handleSetRepeatMode( + @Player.RepeatMode int repeatMode) { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + + forwardingPlayer.setRepeatMode(Player.REPEAT_MODE_ONE); + + verify(wrappedPlayer).setRepeatMode(Player.REPEAT_MODE_ONE); + } + + @Test + public void setShuffleModeEnabled_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder().add(Player.COMMAND_SET_SHUFFLE_MODE).build()) + .build(); + } + + @Override + protected ListenableFuture handleSetShuffleModeEnabled( + boolean shuffleModeEnabled) { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + + forwardingPlayer.setShuffleModeEnabled(true); + + verify(wrappedPlayer).setShuffleModeEnabled(true); + } + + @Test + public void setPlaybackParameters_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder().add(Player.COMMAND_SET_SPEED_AND_PITCH).build()) + .build(); + } + + @Override + protected ListenableFuture handleSetPlaybackParameters( + PlaybackParameters playbackParameters) { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + + forwardingPlayer.setPlaybackParameters( + new PlaybackParameters(/* speed= */ 2f, /* pitch= */ 3f)); + forwardingPlayer.setPlaybackSpeed(5f); + + verify(wrappedPlayer) + .setPlaybackParameters(new PlaybackParameters(/* speed= */ 2f, /* pitch= */ 3f)); + verify(wrappedPlayer).setPlaybackParameters(new PlaybackParameters(/* speed= */ 5f)); + } + + @Test + public void setTrackSelectionParameters_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .add(Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS) + .build()) + .build(); + } + + @Override + protected ListenableFuture handleSetTrackSelectionParameters( + TrackSelectionParameters trackSelectionParameters) { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + TrackSelectionParameters parameters = + new TrackSelectionParameters.Builder(ApplicationProvider.getApplicationContext()) + .setMaxVideoBitrate(1000) + .build(); + + forwardingPlayer.setTrackSelectionParameters(parameters); + + verify(wrappedPlayer).setTrackSelectionParameters(parameters); + } + + @Test + public void setPlaylistMetadata_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .add(Player.COMMAND_SET_PLAYLIST_METADATA) + .build()) + .build(); + } + + @Override + protected ListenableFuture handleSetPlaylistMetadata( + MediaMetadata playlistMetadata) { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + MediaMetadata metadata = new MediaMetadata.Builder().setTitle("title").build(); + + forwardingPlayer.setPlaylistMetadata(metadata); + + verify(wrappedPlayer).setPlaylistMetadata(metadata); + } + + @Test + public void setVolume_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder().add(Player.COMMAND_SET_VOLUME).build()) + .build(); + } + + @Override + protected ListenableFuture handleSetVolume(float volume) { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + + forwardingPlayer.setVolume(0.5f); + + verify(wrappedPlayer).setVolume(0.5f); + } + + @Test + public void setDeviceVolume_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .add(Player.COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS) + .build()) + .build(); + } + + @Override + protected ListenableFuture handleSetDeviceVolume(int deviceVolume, int flags) { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + + forwardingPlayer.setDeviceVolume(50, C.VOLUME_FLAG_SHOW_UI); + + verify(wrappedPlayer).setDeviceVolume(50, C.VOLUME_FLAG_SHOW_UI); + } + + @Test + public void increaseDeviceVolume_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .add(Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS) + .build()) + .build(); + } + + @Override + protected ListenableFuture handleIncreaseDeviceVolume( + @C.VolumeFlags int flags) { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + + forwardingPlayer.increaseDeviceVolume(C.VOLUME_FLAG_PLAY_SOUND); + + verify(wrappedPlayer).increaseDeviceVolume(C.VOLUME_FLAG_PLAY_SOUND); + } + + @Test + public void decreaseDeviceVolume_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .add(Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS) + .build()) + .build(); + } + + @Override + protected ListenableFuture handleDecreaseDeviceVolume( + @C.VolumeFlags int flags) { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + + forwardingPlayer.decreaseDeviceVolume(C.VOLUME_FLAG_PLAY_SOUND); + + verify(wrappedPlayer).decreaseDeviceVolume(C.VOLUME_FLAG_PLAY_SOUND); + } + + @Test + public void setDeviceMuted_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .add(Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS) + .build()) + .build(); + } + + @Override + protected ListenableFuture handleSetDeviceMuted( + boolean muted, @C.VolumeFlags int flags) { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + + forwardingPlayer.setDeviceMuted(true, C.VOLUME_FLAG_PLAY_SOUND); + + verify(wrappedPlayer).setDeviceMuted(true, C.VOLUME_FLAG_PLAY_SOUND); + } + + @Test + public void setAudioAttributes_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder().add(Player.COMMAND_SET_AUDIO_ATTRIBUTES).build()) + .build(); + } + + @Override + protected ListenableFuture handleSetAudioAttributes( + AudioAttributes audioAttributes, boolean handleAudioFocus) { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + AudioAttributes audioAttributes = + new AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MUSIC).build(); + + forwardingPlayer.setAudioAttributes(audioAttributes, /* handleAudioFocus= */ true); + + verify(wrappedPlayer).setAudioAttributes(audioAttributes, /* handleAudioFocus= */ true); + } + + @Test + public void setVideoSurface_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder().add(Player.COMMAND_SET_VIDEO_SURFACE).build()) + .build(); + } + + @Override + protected ListenableFuture handleSetVideoOutput(Object videoOutput) { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + Surface surface = new Surface(new SurfaceTexture(/* texName= */ 0)); + TextureView textureView = new TextureView(ApplicationProvider.getApplicationContext()); + SurfaceView surfaceView = new SurfaceView(ApplicationProvider.getApplicationContext()); + ShadowSurfaceView.FakeSurfaceHolder surfaceHolder = new ShadowSurfaceView.FakeSurfaceHolder(); + + forwardingPlayer.setVideoSurface(surface); + forwardingPlayer.setVideoTextureView(textureView); + forwardingPlayer.setVideoSurfaceView(surfaceView); + forwardingPlayer.setVideoSurfaceHolder(surfaceHolder); + + verify(wrappedPlayer).setVideoSurface(surface); + verify(wrappedPlayer).setVideoTextureView(textureView); + verify(wrappedPlayer).setVideoSurfaceView(surfaceView); + verify(wrappedPlayer).setVideoSurfaceHolder(surfaceHolder); + } + + @Test + public void clearVideoSurface_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder().add(Player.COMMAND_SET_VIDEO_SURFACE).build()) + .build(); + } + + @Override + protected ListenableFuture handleClearVideoOutput( + @Nullable Object videoOutput) { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + Surface surface = new Surface(new SurfaceTexture(/* texName= */ 0)); + TextureView textureView = new TextureView(ApplicationProvider.getApplicationContext()); + SurfaceView surfaceView = new SurfaceView(ApplicationProvider.getApplicationContext()); + ShadowSurfaceView.FakeSurfaceHolder surfaceHolder = new ShadowSurfaceView.FakeSurfaceHolder(); + + forwardingPlayer.clearVideoSurface(); + forwardingPlayer.clearVideoSurface(surface); + forwardingPlayer.clearVideoTextureView(textureView); + forwardingPlayer.clearVideoSurfaceView(surfaceView); + forwardingPlayer.clearVideoSurfaceHolder(surfaceHolder); + + verify(wrappedPlayer).clearVideoSurface(); + verify(wrappedPlayer).clearVideoSurface(surface); + verify(wrappedPlayer).clearVideoTextureView(textureView); + verify(wrappedPlayer).clearVideoSurfaceView(surfaceView); + verify(wrappedPlayer).clearVideoSurfaceHolder(surfaceHolder); + } + + @Test + public void setMediaItems_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAll( + Player.COMMAND_SET_MEDIA_ITEM, + Player.COMMAND_CHANGE_MEDIA_ITEMS) + .build()) + .build(); + } + + @Override + protected ListenableFuture handleSetMediaItems( + List mediaItems, int startIndex, long startPositionMs) { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + MediaItem mediaItem1 = new MediaItem.Builder().setMediaId("1").build(); + MediaItem mediaItem2 = new MediaItem.Builder().setMediaId("2").build(); + + forwardingPlayer.setMediaItem(mediaItem1); + forwardingPlayer.setMediaItem(mediaItem1, /* startPositionMs= */ 500); + forwardingPlayer.setMediaItems(ImmutableList.of(mediaItem1, mediaItem2)); + forwardingPlayer.setMediaItems( + ImmutableList.of(mediaItem1, mediaItem2), /* startIndex= */ 1, /* startPositionMs= */ 600); + + verify(wrappedPlayer).setMediaItem(mediaItem1); + verify(wrappedPlayer).setMediaItem(mediaItem1, /* startPositionMs= */ 500); + verify(wrappedPlayer).setMediaItems(ImmutableList.of(mediaItem1, mediaItem2)); + verify(wrappedPlayer) + .setMediaItems( + ImmutableList.of(mediaItem1, mediaItem2), + /* startIndex= */ 1, + /* startPositionMs= */ 600); + } + + @Test + public void addMediaItems_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAll( + Player.COMMAND_CHANGE_MEDIA_ITEMS, Player.COMMAND_GET_TIMELINE) + .build()) + .setPlaylist( + ImmutableList.of( + new MediaItemData.Builder(new Object()).build(), + new MediaItemData.Builder(new Object()).build())) + .build(); + } + + @Override + protected ListenableFuture handleAddMediaItems( + int index, List mediaItems) { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + MediaItem mediaItem1 = new MediaItem.Builder().setMediaId("1").build(); + MediaItem mediaItem2 = new MediaItem.Builder().setMediaId("2").build(); + + forwardingPlayer.addMediaItem(/* index= */ 1, mediaItem1); + forwardingPlayer.addMediaItems(/* index= */ 2, ImmutableList.of(mediaItem1, mediaItem2)); + + verify(wrappedPlayer).addMediaItem(/* index= */ 1, mediaItem1); + verify(wrappedPlayer).addMediaItems(/* index= */ 2, ImmutableList.of(mediaItem1, mediaItem2)); + } + + @Test + public void moveMediaItems_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAll( + Player.COMMAND_CHANGE_MEDIA_ITEMS, Player.COMMAND_GET_TIMELINE) + .build()) + .setPlaylist( + ImmutableList.of( + new MediaItemData.Builder(new Object()).build(), + new MediaItemData.Builder(new Object()).build(), + new MediaItemData.Builder(new Object()).build())) + .build(); + } + + @Override + protected ListenableFuture handleMoveMediaItems( + int fromIndex, int toIndex, int newIndex) { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + + forwardingPlayer.moveMediaItem(/* currentIndex= */ 1, /* newIndex= */ 0); + forwardingPlayer.moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 2, /* newIndex= */ 1); + + verify(wrappedPlayer).moveMediaItem(/* currentIndex= */ 1, /* newIndex= */ 0); + verify(wrappedPlayer).moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 2, /* newIndex= */ 1); + } + + @Test + public void replaceMediaItems_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAll( + Player.COMMAND_CHANGE_MEDIA_ITEMS, Player.COMMAND_GET_TIMELINE) + .build()) + .setPlaylist( + ImmutableList.of( + new MediaItemData.Builder(new Object()).build(), + new MediaItemData.Builder(new Object()).build(), + new MediaItemData.Builder(new Object()).build())) + .build(); + } + + @Override + protected ListenableFuture handleReplaceMediaItems( + int fromIndex, int toIndex, List mediaItems) { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + MediaItem mediaItem1 = new MediaItem.Builder().setMediaId("1").build(); + MediaItem mediaItem2 = new MediaItem.Builder().setMediaId("2").build(); + + forwardingPlayer.replaceMediaItem(/* index= */ 1, mediaItem1); + forwardingPlayer.replaceMediaItems( + /* fromIndex= */ 0, /* toIndex= */ 2, ImmutableList.of(mediaItem1, mediaItem2)); + + verify(wrappedPlayer).replaceMediaItem(/* index= */ 1, mediaItem1); + verify(wrappedPlayer) + .replaceMediaItems( + /* fromIndex= */ 0, /* toIndex= */ 2, ImmutableList.of(mediaItem1, mediaItem2)); + } + + @Test + public void removeMediaItems_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .addAll( + Player.COMMAND_CHANGE_MEDIA_ITEMS, Player.COMMAND_GET_TIMELINE) + .build()) + .setPlaylist( + ImmutableList.of( + new MediaItemData.Builder(new Object()).build(), + new MediaItemData.Builder(new Object()).build(), + new MediaItemData.Builder(new Object()).build())) + .build(); + } + + @Override + protected ListenableFuture handleRemoveMediaItems(int fromIndex, int toIndex) { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + + forwardingPlayer.removeMediaItem(/* index= */ 1); + forwardingPlayer.removeMediaItems(/* fromIndex= */ 0, /* toIndex= */ 2); + + verify(wrappedPlayer).removeMediaItem(/* index= */ 1); + verify(wrappedPlayer).removeMediaItems(/* fromIndex= */ 0, /* toIndex= */ 2); + } + + @Test + public void seekBack_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder().add(Player.COMMAND_SEEK_BACK).build()) + .build(); + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Command int seekCommand) { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + + forwardingPlayer.seekBack(); + + verify(wrappedPlayer).seekBack(); + } + + @Test + public void seekForward_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder().add(Player.COMMAND_SEEK_FORWARD).build()) + .build(); + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Command int seekCommand) { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + + forwardingPlayer.seekForward(); + + verify(wrappedPlayer).seekForward(); + } + + @Test + public void seekInCurrentItem_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .add(Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM) + .build()) + .build(); + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Command int seekCommand) { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + + forwardingPlayer.seekTo(/* positionMs= */ 5000); + + verify(wrappedPlayer).seekTo(/* positionMs= */ 5000); + } + + @Test + public void seekToDefaultPosition_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .add(Player.COMMAND_SEEK_TO_DEFAULT_POSITION) + .build()) + .build(); + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Command int seekCommand) { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + + forwardingPlayer.seekToDefaultPosition(); + + verify(wrappedPlayer).seekToDefaultPosition(); + } + + @Test + public void seekToMediaItem_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder().add(Player.COMMAND_SEEK_TO_MEDIA_ITEM).build()) + .build(); + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Command int seekCommand) { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + + forwardingPlayer.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 3000); + + verify(wrappedPlayer).seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 3000); + } + + @Test + public void seekToNext_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder().add(Player.COMMAND_SEEK_TO_NEXT).build()) + .build(); + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Command int seekCommand) { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + + forwardingPlayer.seekToNext(); + + verify(wrappedPlayer).seekToNext(); + } + + @Test + public void seekToPrevious_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder().add(Player.COMMAND_SEEK_TO_PREVIOUS).build()) + .build(); + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Command int seekCommand) { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + + forwardingPlayer.seekToPrevious(); + + verify(wrappedPlayer).seekToPrevious(); + } + + @Test + public void seekToNextMediaItem_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .add(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) + .build()) + .build(); + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Command int seekCommand) { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + + forwardingPlayer.seekToNextMediaItem(); + + verify(wrappedPlayer).seekToNextMediaItem(); + } + + @Test + public void seekToPreviousMediaItem_isForwardedToWrappedPlayer() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands( + new Commands.Builder() + .add(Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) + .build()) + .build(); + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Command int seekCommand) { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer); + + forwardingPlayer.seekToPreviousMediaItem(); + + verify(wrappedPlayer).seekToPreviousMediaItem(); + } + + @Test + public void overrideSetters_forwardsOverriddenCallsOnly() { + Player wrappedPlayer = + spy( + new ForwardingPlayer( + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .build(); + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Command int seekCommand) { + return Futures.immediateVoidFuture(); + } + + @Override + protected ListenableFuture handleSetShuffleModeEnabled( + boolean shuffleModeEnabled) { + return Futures.immediateVoidFuture(); + } + })); + Player forwardingPlayer = + new ForwardingSimpleBasePlayer(wrappedPlayer) { + @Override + protected ListenableFuture handleSetRepeatMode(int repeatMode) { + // Usage variant 1: Directly call player. + getPlayer().setShuffleModeEnabled(true); + return Futures.immediateVoidFuture(); + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, int seekCommand) { + // Usage variant 2: Call super method with different parameters. + return super.handleSeek( + /* mediaItemIndex= */ 1, /* positionMs= */ 2000, Player.COMMAND_SEEK_TO_MEDIA_ITEM); + } + }; + + forwardingPlayer.seekToNext(); + forwardingPlayer.setRepeatMode(Player.REPEAT_MODE_ONE); + + verify(wrappedPlayer).seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 2000); + verify(wrappedPlayer).setShuffleModeEnabled(true); + verify(wrappedPlayer, never()).seekToNext(); + verify(wrappedPlayer, never()).setRepeatMode(anyInt()); + } + + @Test + public void overrideState_triggersListenersAccordingToOverridenState() { + SimpleBasePlayer.State wrappedState1 = + new SimpleBasePlayer.State.Builder() + .setAvailableCommands( + new Player.Commands.Builder().add(Player.COMMAND_GET_TIMELINE).build()) + .setPlaybackState(Player.STATE_READY) + .setPlayWhenReady(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST) + .setPlaylist( + ImmutableList.of(new SimpleBasePlayer.MediaItemData.Builder(new Object()).build())) + .build(); + SimpleBasePlayer.State wrappedState2 = + wrappedState1.buildUpon().setPlaybackState(Player.STATE_BUFFERING).build(); + AtomicBoolean returnWrappedState2 = new AtomicBoolean(); + SimpleBasePlayer wrappedPlayer = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return returnWrappedState2.get() ? wrappedState2 : wrappedState1; + } + }; + Player forwardingPlayer = + new ForwardingSimpleBasePlayer(wrappedPlayer) { + @Override + protected State getState() { + State state = super.getState(); + if (state.playbackState == Player.STATE_BUFFERING) { + // Suppress changes in the wrapped player and add new changes. + state = + state + .buildUpon() + .setPlayWhenReady(false, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE) + .setPlaybackState(Player.STATE_READY) + .build(); + } + return state; + } + }; + Player.Listener listener = mock(Player.Listener.class); + forwardingPlayer.addListener(listener); + + @Player.State int playbackState1 = forwardingPlayer.getPlaybackState(); + boolean playWhenReady1 = forwardingPlayer.getPlayWhenReady(); + returnWrappedState2.set(true); + wrappedPlayer.invalidateState(); + // Idle Looper to ensure all callbacks (including onEvents) are delivered. + ShadowLooper.idleMainLooper(); + @Player.State int playbackState2 = forwardingPlayer.getPlaybackState(); + boolean playWhenReady2 = forwardingPlayer.getPlayWhenReady(); + + assertThat(playbackState1).isEqualTo(Player.STATE_READY); + assertThat(playbackState2).isEqualTo(Player.STATE_READY); + assertThat(playWhenReady1).isTrue(); + assertThat(playWhenReady2).isFalse(); + verify(listener, never()).onPlaybackStateChanged(anyInt()); + verify(listener).onPlayWhenReadyChanged(false, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE); + } + + private static Object[] getAnyArguments(Method method) { + Object[] arguments = new Object[method.getParameterCount()]; + Class[] argumentTypes = method.getParameterTypes(); + for (int i = 0; i < arguments.length; i++) { + if (argumentTypes[i].equals(Integer.TYPE)) { + arguments[i] = anyInt(); + } else if (argumentTypes[i].equals(Long.TYPE)) { + arguments[i] = anyLong(); + } else if (argumentTypes[i].equals(Float.TYPE)) { + arguments[i] = anyFloat(); + } else if (argumentTypes[i].equals(Boolean.TYPE)) { + arguments[i] = anyBoolean(); + } else { + arguments[i] = any(); + } + } + return arguments; + } +}