mirror of
https://github.com/androidx/media.git
synced 2025-05-04 22:20:47 +08:00
Add initial version of SimpleBasePlayer
This base class will simplify the implementation of custom Player classes. The current version only supports available commands and playWhenReady handling. PiperOrigin-RevId: 467618021 (cherry picked from commit 9a7fde8fde6e194b92ec67095b7b889ee23a3d77)
This commit is contained in:
parent
9b6d997703
commit
d2000fd25f
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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 android.media.MediaPlayer;
|
||||
import android.os.Looper;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
/** A {@link Player} wrapper for the legacy Android platform {@link MediaPlayer}. */
|
||||
public final class LegacyMediaPlayerWrapper extends SimpleBasePlayer {
|
||||
|
||||
private final MediaPlayer player;
|
||||
|
||||
private boolean playWhenReady;
|
||||
|
||||
/**
|
||||
* Creates the {@link MediaPlayer} wrapper.
|
||||
*
|
||||
* @param looper The {@link Looper} used to call all methods on.
|
||||
*/
|
||||
public LegacyMediaPlayerWrapper(Looper looper) {
|
||||
super(looper);
|
||||
this.player = new MediaPlayer();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected State getState() {
|
||||
return new State.Builder()
|
||||
.setAvailableCommands(new Commands.Builder().addAll(Player.COMMAND_PLAY_PAUSE).build())
|
||||
.setPlayWhenReady(playWhenReady, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ListenableFuture<?> handleSetPlayWhenReady(boolean playWhenReady) {
|
||||
this.playWhenReady = playWhenReady;
|
||||
// TODO: Only call these methods if the player is in Started or Paused state.
|
||||
if (playWhenReady) {
|
||||
player.start();
|
||||
} else {
|
||||
player.pause();
|
||||
}
|
||||
return Futures.immediateVoidFuture();
|
||||
}
|
||||
}
|
@ -0,0 +1,793 @@
|
||||
/*
|
||||
* 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.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
import static com.google.android.exoplayer2.util.Util.castNonNull;
|
||||
|
||||
import android.os.Looper;
|
||||
import android.view.Surface;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.TextureView;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.text.CueGroup;
|
||||
import com.google.android.exoplayer2.util.Clock;
|
||||
import com.google.android.exoplayer2.util.HandlerWrapper;
|
||||
import com.google.android.exoplayer2.util.ListenerSet;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.common.base.Supplier;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import com.google.errorprone.annotations.ForOverride;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
|
||||
/**
|
||||
* A base implementation for {@link Player} that reduces the number of methods to implement to a
|
||||
* minimum.
|
||||
*
|
||||
* <p>Implementation notes:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Subclasses must override {@link #getState()} to populate the current player state on
|
||||
* request.
|
||||
* <li>The {@link State} should set the {@linkplain State.Builder#setAvailableCommands available
|
||||
* commands} to indicate which {@link Player} methods are supported.
|
||||
* <li>All setter-like player methods (for example, {@link #setPlayWhenReady}) forward to
|
||||
* overridable methods (for example, {@link #handleSetPlayWhenReady}) that can be used to
|
||||
* handle these requests. These methods return a {@link ListenableFuture} to indicate when the
|
||||
* request has been handled and is fully reflected in the values returned from {@link
|
||||
* #getState}. This class will automatically request a state update once the request is done.
|
||||
* If the state changes can be handled synchronously, these methods can return Guava's {@link
|
||||
* Futures#immediateVoidFuture()}.
|
||||
* <li>Subclasses can manually trigger state updates with {@link #invalidateState}, for example if
|
||||
* something changes independent of {@link Player} method calls.
|
||||
* </ul>
|
||||
*
|
||||
* This base class handles various aspects of the player implementation to simplify the subclass:
|
||||
*
|
||||
* <ul>
|
||||
* <li>The {@link State} can only be created with allowed combinations of state values, avoiding
|
||||
* any invalid player states.
|
||||
* <li>Only functionality that is declared as {@linkplain Player.Command available} needs to be
|
||||
* implemented. Other methods are automatically ignored.
|
||||
* <li>Listener handling and informing listeners of state changes is handled automatically.
|
||||
* <li>The base class provides a framework for asynchronous handling of method calls. It changes
|
||||
* the visible playback state immediately to the most likely outcome to ensure the
|
||||
* user-visible state changes look like synchronous operations. The state is then updated
|
||||
* again once the asynchronous method calls have been fully handled.
|
||||
* </ul>
|
||||
*/
|
||||
public abstract class SimpleBasePlayer extends BasePlayer {
|
||||
|
||||
/** An immutable state description of the player. */
|
||||
protected static final class State {
|
||||
|
||||
/** A builder for {@link State} objects. */
|
||||
public static final class Builder {
|
||||
|
||||
private Commands availableCommands;
|
||||
private boolean playWhenReady;
|
||||
private @PlayWhenReadyChangeReason int playWhenReadyChangeReason;
|
||||
|
||||
/** Creates the builder. */
|
||||
public Builder() {
|
||||
availableCommands = Commands.EMPTY;
|
||||
playWhenReady = false;
|
||||
playWhenReadyChangeReason = Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST;
|
||||
}
|
||||
|
||||
private Builder(State state) {
|
||||
this.availableCommands = state.availableCommands;
|
||||
this.playWhenReady = state.playWhenReady;
|
||||
this.playWhenReadyChangeReason = state.playWhenReadyChangeReason;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the available {@link Commands}.
|
||||
*
|
||||
* @param availableCommands The available {@link Commands}.
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setAvailableCommands(Commands availableCommands) {
|
||||
this.availableCommands = availableCommands;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether playback should proceed when ready and not suppressed.
|
||||
*
|
||||
* @param playWhenReady Whether playback should proceed when ready and not suppressed.
|
||||
* @param playWhenReadyChangeReason The {@linkplain PlayWhenReadyChangeReason reason} for
|
||||
* changing the value.
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setPlayWhenReady(
|
||||
boolean playWhenReady, @PlayWhenReadyChangeReason int playWhenReadyChangeReason) {
|
||||
this.playWhenReady = playWhenReady;
|
||||
this.playWhenReadyChangeReason = playWhenReadyChangeReason;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Builds the {@link State}. */
|
||||
public State build() {
|
||||
return new State(this);
|
||||
}
|
||||
}
|
||||
|
||||
/** The available {@link Commands}. */
|
||||
public final Commands availableCommands;
|
||||
/** Whether playback should proceed when ready and not suppressed. */
|
||||
public final boolean playWhenReady;
|
||||
/** The last reason for changing {@link #playWhenReady}. */
|
||||
public final @PlayWhenReadyChangeReason int playWhenReadyChangeReason;
|
||||
|
||||
private State(Builder builder) {
|
||||
this.availableCommands = builder.availableCommands;
|
||||
this.playWhenReady = builder.playWhenReady;
|
||||
this.playWhenReadyChangeReason = builder.playWhenReadyChangeReason;
|
||||
}
|
||||
|
||||
/** Returns a {@link Builder} pre-populated with the current state values. */
|
||||
public Builder buildUpon() {
|
||||
return new Builder(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof State)) {
|
||||
return false;
|
||||
}
|
||||
State state = (State) o;
|
||||
return playWhenReady == state.playWhenReady
|
||||
&& playWhenReadyChangeReason == state.playWhenReadyChangeReason
|
||||
&& availableCommands.equals(state.availableCommands);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = 7;
|
||||
result = 31 * result + availableCommands.hashCode();
|
||||
result = 31 * result + (playWhenReady ? 1 : 0);
|
||||
result = 31 * result + playWhenReadyChangeReason;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private final ListenerSet<Listener> listeners;
|
||||
private final Looper applicationLooper;
|
||||
private final HandlerWrapper applicationHandler;
|
||||
private final HashSet<ListenableFuture<?>> pendingOperations;
|
||||
|
||||
private @MonotonicNonNull State state;
|
||||
|
||||
/**
|
||||
* Creates the base class.
|
||||
*
|
||||
* @param applicationLooper The {@link Looper} that must be used for all calls to the player and
|
||||
* that is used to call listeners on.
|
||||
*/
|
||||
protected SimpleBasePlayer(Looper applicationLooper) {
|
||||
this(applicationLooper, Clock.DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the base class.
|
||||
*
|
||||
* @param applicationLooper The {@link Looper} that must be used for all calls to the player and
|
||||
* that is used to call listeners on.
|
||||
* @param clock The {@link Clock} that will be used by the player.
|
||||
*/
|
||||
protected SimpleBasePlayer(Looper applicationLooper, Clock clock) {
|
||||
this.applicationLooper = applicationLooper;
|
||||
applicationHandler = clock.createHandler(applicationLooper, /* callback= */ null);
|
||||
pendingOperations = new HashSet<>();
|
||||
@SuppressWarnings("nullness:argument.type.incompatible") // Using this in constructor.
|
||||
ListenerSet<Player.Listener> listenerSet =
|
||||
new ListenerSet<>(
|
||||
applicationLooper,
|
||||
clock,
|
||||
(listener, flags) -> listener.onEvents(/* player= */ this, new Events(flags)));
|
||||
listeners = listenerSet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void addListener(Listener listener) {
|
||||
// Don't verify application thread. We allow calls to this method from any thread.
|
||||
listeners.add(checkNotNull(listener));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void removeListener(Listener listener) {
|
||||
// Don't verify application thread. We allow calls to this method from any thread.
|
||||
checkNotNull(listener);
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Looper getApplicationLooper() {
|
||||
// Don't verify application thread. We allow calls to this method from any thread.
|
||||
return applicationLooper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Commands getAvailableCommands() {
|
||||
verifyApplicationThreadAndInitState();
|
||||
return state.availableCommands;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void setPlayWhenReady(boolean playWhenReady) {
|
||||
verifyApplicationThreadAndInitState();
|
||||
State state = this.state;
|
||||
if (!state.availableCommands.contains(Player.COMMAND_PLAY_PAUSE)) {
|
||||
return;
|
||||
}
|
||||
updateStateForPendingOperation(
|
||||
/* pendingOperation= */ handleSetPlayWhenReady(playWhenReady),
|
||||
/* placeholderStateSupplier= */ () ->
|
||||
state
|
||||
.buildUpon()
|
||||
.setPlayWhenReady(playWhenReady, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean getPlayWhenReady() {
|
||||
verifyApplicationThreadAndInitState();
|
||||
return state.playWhenReady;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void setMediaItems(List<MediaItem> mediaItems, boolean resetPosition) {
|
||||
// TODO: implement.
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void setMediaItems(
|
||||
List<MediaItem> mediaItems, int startIndex, long startPositionMs) {
|
||||
// TODO: implement.
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void addMediaItems(int index, List<MediaItem> 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.
|
||||
*
|
||||
* <p>Triggers a call to {@link #getState()} and informs listeners if the state changed.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>The {@link State} should include all {@linkplain
|
||||
* State.Builder#setAvailableCommands(Commands) available commands} indicating which player
|
||||
* methods are allowed to be called.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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}.
|
||||
*
|
||||
* <p>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<State> 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
|
||||
}
|
||||
}
|
@ -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<State> 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<State> currentState = new AtomicReference<>(state1);
|
||||
ArrayList<SettableFuture<?>> 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();
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user