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