ForwardingPlayer only forwards Player operations

PiperOrigin-RevId: 374621615
This commit is contained in:
christosts 2021-05-19 12:56:54 +01:00 committed by Oliver Woodman
parent 41afb6ac4e
commit 65d8ff80db
2 changed files with 261 additions and 697 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 The Android Open Source Project
* Copyright 2021 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.
@ -15,74 +15,32 @@
*/
package com.google.android.exoplayer2;
import static com.google.android.exoplayer2.util.Assertions.checkState;
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 androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.device.DeviceInfo;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.ListenerSet;
import com.google.android.exoplayer2.video.VideoSize;
import java.util.List;
/**
* A {@link Player} that forwards operations to another {@link Player}. Applications can use this
* class to suppress or modify specific operations, by overriding the respective methods.
*
* <p>An application can {@link #setDisabledCommands disable available commands}. When the wrapped
* player advertises available commands, either with {@link Player#isCommandAvailable(int)} or with
* {@link Listener#onAvailableCommandsChanged}, the disabled commands will be filtered out.
*/
public class ForwardingPlayer implements Player {
private final Player player;
private final Clock clock;
@Nullable private ForwardingListener forwardingListener;
private Commands disabledCommands;
@Nullable private Commands unfilteredCommands;
@Nullable private Commands filteredCommands;
private final Player player;
/** Creates a new instance that forwards all operations to {@code player}. */
public ForwardingPlayer(Player player) {
this(player, Clock.DEFAULT);
}
@VisibleForTesting
/* package */ ForwardingPlayer(Player player, Clock clock) {
this.player = player;
this.clock = clock;
this.disabledCommands = Commands.EMPTY;
}
/**
* Sets the disabled {@link Commands}.
*
* <p>When querying for available commands with {@link #isCommandAvailable(int)}, or when the
* wrapped player advertises available commands with {@link Listener#isCommandAvailable}, disabled
* commands will be filtered out.
*/
public void setDisabledCommands(Commands commands) {
checkState(player.getApplicationLooper().equals(Looper.myLooper()));
disabledCommands = commands;
filteredCommands = null;
if (forwardingListener != null) {
forwardingListener.maybeAdvertiseAvailableCommands();
}
}
/** Returns the disabled commands. */
public Commands getDisabledCommands() {
return disabledCommands;
}
@Override
@ -91,35 +49,25 @@ public class ForwardingPlayer implements Player {
}
@Override
@SuppressWarnings("deprecation") // Implementing deprecated method.
public void addListener(EventListener listener) {
addListener(new EventListenerWrapper(listener));
player.addListener(new ForwardingEventListener(this, listener));
}
@Override
public void addListener(Listener listener) {
if (forwardingListener == null) {
forwardingListener = new ForwardingListener(this);
}
if (!forwardingListener.isRegistered()) {
forwardingListener.registerTo(player);
}
forwardingListener.addListener(listener);
player.addListener(new ForwardingListener(this, listener));
}
@Override
@SuppressWarnings("deprecation") // Implementing deprecated method.
public void removeListener(EventListener listener) {
removeListener(new EventListenerWrapper(listener));
player.removeListener(new ForwardingEventListener(this, listener));
}
@Override
public void removeListener(Listener listener) {
if (forwardingListener == null) {
return;
}
forwardingListener.removeListener(listener);
if (!forwardingListener.hasListeners()) {
forwardingListener.unregisterFrom(player);
}
player.removeListener(new ForwardingListener(this, listener));
}
@Override
@ -200,17 +148,12 @@ public class ForwardingPlayer implements Player {
@Override
public boolean isCommandAvailable(@Command int command) {
return !disabledCommands.contains(command) && player.isCommandAvailable(command);
return player.isCommandAvailable(command);
}
@Override
public Commands getAvailableCommands() {
Commands commands = player.getAvailableCommands();
if (filteredCommands == null || !commands.equals(unfilteredCommands)) {
filteredCommands = filterCommands(commands, disabledCommands);
unfilteredCommands = commands;
}
return filteredCommands;
return player.getAvailableCommands();
}
@Override
@ -602,424 +545,126 @@ public class ForwardingPlayer implements Player {
player.setDeviceMuted(muted);
}
/**
* Wraps a {@link Listener} and intercepts {@link Listener#onAvailableCommandsChanged} in order to
* filter disabled commands. All other operations are forwarded to the wrapped {@link Listener}.
*/
private static class ForwardingListener implements Listener {
private final ForwardingPlayer player;
private final ListenerSet<Listener> listeners;
private boolean registered;
private Commands lastReceivedCommands;
private Commands lastAdvertisedCommands;
@SuppressWarnings("deprecation") // Use of deprecated type for backwards compatibility.
private static class ForwardingEventListener implements EventListener {
public ForwardingListener(ForwardingPlayer forwardingPlayer) {
this.player = forwardingPlayer;
listeners =
new ListenerSet<>(
forwardingPlayer.player.getApplicationLooper(),
forwardingPlayer.clock,
(listener, flags) -> listener.onEvents(forwardingPlayer, new Events(flags)));
lastReceivedCommands = Commands.EMPTY;
lastAdvertisedCommands = Commands.EMPTY;
private final ForwardingPlayer forwardingPlayer;
private final EventListener eventListener;
private ForwardingEventListener(
ForwardingPlayer forwardingPlayer, EventListener eventListener) {
this.forwardingPlayer = forwardingPlayer;
this.eventListener = eventListener;
}
public void registerTo(Player player) {
checkState(!registered);
player.addListener(this);
lastReceivedCommands = player.getAvailableCommands();
lastAdvertisedCommands = lastReceivedCommands;
registered = true;
}
public void unregisterFrom(Player player) {
checkState(registered);
player.removeListener(this);
registered = false;
}
public boolean isRegistered() {
return registered;
}
public void addListener(Listener listener) {
listeners.add(listener);
}
public void removeListener(Listener listener) {
listeners.remove(listener);
}
public boolean hasListeners() {
return listeners.size() > 0;
}
// VideoListener callbacks
@Override
public void onVideoSizeChanged(VideoSize videoSize) {
listeners.sendEvent(C.INDEX_UNSET, listener -> listener.onVideoSizeChanged(videoSize));
}
@Override
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
public void onVideoSizeChanged(
int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
listeners.sendEvent(
C.INDEX_UNSET,
listener ->
listener.onVideoSizeChanged(
width, height, unappliedRotationDegrees, pixelWidthHeightRatio));
}
@Override
public void onSurfaceSizeChanged(int width, int height) {
listeners.sendEvent(C.INDEX_UNSET, listener -> listener.onSurfaceSizeChanged(width, height));
}
@Override
public void onRenderedFirstFrame() {
listeners.sendEvent(C.INDEX_UNSET, Listener::onRenderedFirstFrame);
}
// AudioListener callbacks
@Override
public void onAudioSessionIdChanged(int audioSessionId) {
listeners.sendEvent(
C.INDEX_UNSET, listener -> listener.onAudioSessionIdChanged(audioSessionId));
}
@Override
public void onAudioAttributesChanged(AudioAttributes audioAttributes) {
listeners.sendEvent(
C.INDEX_UNSET, listener -> listener.onAudioAttributesChanged(audioAttributes));
}
@Override
public void onVolumeChanged(float volume) {
listeners.sendEvent(C.INDEX_UNSET, listener -> listener.onVolumeChanged(volume));
}
@Override
public void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) {
listeners.sendEvent(
C.INDEX_UNSET, listener -> listener.onSkipSilenceEnabledChanged(skipSilenceEnabled));
}
// TextOutput callbacks
@Override
public void onCues(List<Cue> cues) {
listeners.sendEvent(C.INDEX_UNSET, listener -> listener.onCues(cues));
}
// MetadataOutput callbacks
@Override
public void onMetadata(Metadata metadata) {
listeners.sendEvent(C.INDEX_UNSET, listener -> listener.onMetadata(metadata));
}
// DeviceListener callbacks
@Override
public void onDeviceInfoChanged(DeviceInfo deviceInfo) {
listeners.sendEvent(C.INDEX_UNSET, listener -> listener.onDeviceInfoChanged(deviceInfo));
}
@Override
public void onDeviceVolumeChanged(int volume, boolean muted) {
listeners.sendEvent(C.INDEX_UNSET, listener -> listener.onDeviceVolumeChanged(volume, muted));
}
// EventListener callbacks
@Override
public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {
listeners.sendEvent(
EVENT_TIMELINE_CHANGED, listener -> listener.onTimelineChanged(timeline, reason));
eventListener.onTimelineChanged(timeline, reason);
}
@Override
public void onMediaItemTransition(
@Nullable MediaItem mediaItem, @MediaItemTransitionReason int reason) {
listeners.sendEvent(
EVENT_MEDIA_ITEM_TRANSITION,
listener -> listener.onMediaItemTransition(mediaItem, reason));
eventListener.onMediaItemTransition(mediaItem, reason);
}
@Override
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
listeners.sendEvent(
EVENT_TRACKS_CHANGED, listener -> listener.onTracksChanged(trackGroups, trackSelections));
eventListener.onTracksChanged(trackGroups, trackSelections);
}
@Override
public void onStaticMetadataChanged(List<Metadata> metadataList) {
listeners.sendEvent(
EVENT_STATIC_METADATA_CHANGED,
listener -> listener.onStaticMetadataChanged(metadataList));
eventListener.onStaticMetadataChanged(metadataList);
}
@Override
public void onMediaMetadataChanged(MediaMetadata mediaMetadata) {
listeners.sendEvent(
EVENT_MEDIA_METADATA_CHANGED, listener -> listener.onMediaMetadataChanged(mediaMetadata));
eventListener.onMediaMetadataChanged(mediaMetadata);
}
@Override
public void onIsLoadingChanged(boolean isLoading) {
listeners.sendEvent(
EVENT_IS_LOADING_CHANGED, listener -> listener.onIsLoadingChanged(isLoading));
eventListener.onIsLoadingChanged(isLoading);
}
@Override
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
public void onLoadingChanged(boolean isLoading) {
listeners.sendEvent(
EVENT_IS_LOADING_CHANGED, listener -> listener.onLoadingChanged(isLoading));
eventListener.onIsLoadingChanged(isLoading);
}
@Override
public void onAvailableCommandsChanged(Commands availableCommands) {
lastReceivedCommands = availableCommands;
maybeAdvertiseAvailableCommands();
eventListener.onAvailableCommandsChanged(availableCommands);
}
@Override
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
public void onPlayerStateChanged(boolean playWhenReady, @State int playbackState) {
listeners.sendEvent(
C.INDEX_UNSET, listener -> listener.onPlayerStateChanged(playWhenReady, playbackState));
eventListener.onPlayerStateChanged(playWhenReady, playbackState);
}
@Override
public void onPlaybackStateChanged(@State int state) {
listeners.sendEvent(
EVENT_PLAYBACK_STATE_CHANGED, listener -> listener.onPlaybackStateChanged(state));
}
@Override
public void onPlayWhenReadyChanged(boolean playWhenReady, @State int reason) {
listeners.sendEvent(
EVENT_PLAY_WHEN_READY_CHANGED,
listener -> listener.onPlayWhenReadyChanged(playWhenReady, reason));
}
@Override
public void onPlaybackSuppressionReasonChanged(
@PlaybackSuppressionReason int playbackSuppressionReason) {
listeners.sendEvent(
EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED,
listener -> listener.onPlaybackSuppressionReasonChanged(playbackSuppressionReason));
}
@Override
public void onIsPlayingChanged(boolean isPlaying) {
listeners.sendEvent(
EVENT_IS_PLAYING_CHANGED, listener -> listener.onIsPlayingChanged(isPlaying));
}
@Override
public void onRepeatModeChanged(@RepeatMode int repeatMode) {
listeners.sendEvent(
EVENT_REPEAT_MODE_CHANGED, listener -> listener.onRepeatModeChanged(repeatMode));
}
@Override
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
listeners.sendEvent(
EVENT_SHUFFLE_MODE_ENABLED_CHANGED,
listener -> listener.onShuffleModeEnabledChanged(shuffleModeEnabled));
}
@Override
public void onPlayerError(ExoPlaybackException error) {
listeners.sendEvent(EVENT_PLAYER_ERROR, listener -> listener.onPlayerError(error));
}
@Override
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
listeners.sendEvent(
EVENT_POSITION_DISCONTINUITY, listener -> listener.onPositionDiscontinuity(reason));
}
@Override
public void onPositionDiscontinuity(
PositionInfo oldPosition, PositionInfo newPosition, @DiscontinuityReason int reason) {
listeners.sendEvent(
EVENT_POSITION_DISCONTINUITY,
listener -> listener.onPositionDiscontinuity(oldPosition, newPosition, reason));
}
@Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
listeners.sendEvent(
EVENT_PLAYBACK_PARAMETERS_CHANGED,
listener -> listener.onPlaybackParametersChanged(playbackParameters));
}
@Override
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
public void onSeekProcessed() {
listeners.sendEvent(C.INDEX_UNSET, EventListener::onSeekProcessed);
}
@Override
public void onEvents(Player player, Events events) {
// Do nothing, individual callbacks will trigger this event on behalf of the forwarding
// player.
}
public void maybeAdvertiseAvailableCommands() {
Commands commandsToAdvertise = filterCommands(lastReceivedCommands, player.disabledCommands);
if (!commandsToAdvertise.equals(lastAdvertisedCommands)) {
lastAdvertisedCommands = commandsToAdvertise;
listeners.sendEvent(
EVENT_AVAILABLE_COMMANDS_CHANGED,
listener -> listener.onAvailableCommandsChanged(commandsToAdvertise));
}
}
}
/**
* Wraps an {@link EventListener} as a {@link Listener} so that it can be used by the {@link
* ForwardingListener}.
*/
private static class EventListenerWrapper implements Listener {
private final EventListener listener;
/** Wraps an {@link EventListener}. */
public EventListenerWrapper(EventListener listener) {
this.listener = listener;
}
// EventListener callbacks
@Override
public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {
listener.onTimelineChanged(timeline, reason);
}
@Override
public void onMediaItemTransition(
@Nullable MediaItem mediaItem, @MediaItemTransitionReason int reason) {
listener.onMediaItemTransition(mediaItem, reason);
}
@Override
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
listener.onTracksChanged(trackGroups, trackSelections);
}
@Override
public void onStaticMetadataChanged(List<Metadata> metadataList) {
listener.onStaticMetadataChanged(metadataList);
}
@Override
public void onMediaMetadataChanged(MediaMetadata mediaMetadata) {
listener.onMediaMetadataChanged(mediaMetadata);
}
@Override
public void onIsLoadingChanged(boolean isLoading) {
listener.onIsLoadingChanged(isLoading);
}
@Override
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
public void onLoadingChanged(boolean isLoading) {
listener.onLoadingChanged(isLoading);
}
@Override
public void onAvailableCommandsChanged(Commands availableCommands) {
listener.onAvailableCommandsChanged(availableCommands);
}
@Override
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
public void onPlayerStateChanged(boolean playWhenReady, @State int playbackState) {
listener.onPlayerStateChanged(playWhenReady, playbackState);
}
@Override
public void onPlaybackStateChanged(@State int state) {
listener.onPlaybackStateChanged(state);
eventListener.onPlaybackStateChanged(state);
}
@Override
public void onPlayWhenReadyChanged(
boolean playWhenReady, @PlayWhenReadyChangeReason int reason) {
listener.onPlayWhenReadyChanged(playWhenReady, reason);
eventListener.onPlayWhenReadyChanged(playWhenReady, reason);
}
@Override
public void onPlaybackSuppressionReasonChanged(
@PlaybackSuppressionReason int playbackSuppressionReason) {
listener.onPlaybackSuppressionReasonChanged(playbackSuppressionReason);
@PlayWhenReadyChangeReason int playbackSuppressionReason) {
eventListener.onPlaybackStateChanged(playbackSuppressionReason);
}
@Override
public void onIsPlayingChanged(boolean isPlaying) {
listener.onIsPlayingChanged(isPlaying);
eventListener.onIsPlayingChanged(isPlaying);
}
@Override
public void onRepeatModeChanged(@RepeatMode int repeatMode) {
listener.onRepeatModeChanged(repeatMode);
eventListener.onRepeatModeChanged(repeatMode);
}
@Override
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
listener.onShuffleModeEnabledChanged(shuffleModeEnabled);
eventListener.onShuffleModeEnabledChanged(shuffleModeEnabled);
}
@Override
public void onPlayerError(ExoPlaybackException error) {
listener.onPlayerError(error);
eventListener.onPlayerError(error);
}
@Override
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
listener.onPositionDiscontinuity(reason);
eventListener.onPositionDiscontinuity(reason);
}
@Override
public void onPositionDiscontinuity(
PositionInfo oldPosition, PositionInfo newPosition, @DiscontinuityReason int reason) {
listener.onPositionDiscontinuity(oldPosition, newPosition, reason);
eventListener.onPositionDiscontinuity(oldPosition, newPosition, reason);
}
@Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
listener.onPlaybackParametersChanged(playbackParameters);
eventListener.onPlaybackParametersChanged(playbackParameters);
}
@Override
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
public void onSeekProcessed() {
listener.onSeekProcessed();
eventListener.onSeekProcessed();
}
@Override
public void onEvents(Player player, Events events) {
listener.onEvents(player, events);
}
// Other Listener callbacks, they should never be invoked on this wrapper.
@Override
public void onMetadata(Metadata metadata) {
throw new IllegalStateException();
}
@Override
public void onCues(List<Cue> cues) {
throw new IllegalStateException();
// Replace player with forwarding player.
eventListener.onEvents(forwardingPlayer, events);
}
@Override
@ -1027,31 +672,106 @@ public class ForwardingPlayer implements Player {
if (this == o) {
return true;
}
if (!(o instanceof EventListenerWrapper)) {
if (!(o instanceof ForwardingEventListener)) {
return false;
}
EventListenerWrapper that = (EventListenerWrapper) o;
return listener.equals(that.listener);
ForwardingEventListener that = (ForwardingEventListener) o;
if (!forwardingPlayer.equals(that.forwardingPlayer)) {
return false;
}
return eventListener.equals(that.eventListener);
}
@Override
public int hashCode() {
return listener.hashCode();
int result = forwardingPlayer.hashCode();
result = 31 * result + eventListener.hashCode();
return result;
}
}
/** Returns the remaining available commands after removing disabled commands. */
private static Commands filterCommands(Commands availableCommands, Commands disabledCommands) {
if (disabledCommands.size() == 0) {
return availableCommands;
private static final class ForwardingListener extends ForwardingEventListener
implements Listener {
private final Listener listener;
public ForwardingListener(ForwardingPlayer forwardingPlayer, Listener listener) {
super(forwardingPlayer, listener);
this.listener = listener;
}
Commands.Builder builder = new Commands.Builder();
for (int i = 0; i < availableCommands.size(); i++) {
int command = availableCommands.get(i);
builder.addIf(command, !disabledCommands.contains(command));
// VideoListener methods.
@Override
public void onVideoSizeChanged(VideoSize videoSize) {
listener.onVideoSizeChanged(videoSize);
}
@Override
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
public void onVideoSizeChanged(
int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
listener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio);
}
@Override
public void onSurfaceSizeChanged(int width, int height) {
listener.onSurfaceSizeChanged(width, height);
}
@Override
public void onRenderedFirstFrame() {
listener.onRenderedFirstFrame();
}
// AudioListener methods
@Override
public void onAudioSessionIdChanged(int audioSessionId) {
listener.onAudioSessionIdChanged(audioSessionId);
}
@Override
public void onAudioAttributesChanged(AudioAttributes audioAttributes) {
listener.onAudioAttributesChanged(audioAttributes);
}
@Override
public void onVolumeChanged(float volume) {
listener.onVolumeChanged(volume);
}
@Override
public void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) {
listener.onSkipSilenceEnabledChanged(skipSilenceEnabled);
}
// TextOutput methods.
@Override
public void onCues(List<Cue> cues) {
listener.onCues(cues);
}
// MetadataOutput methods.
@Override
public void onMetadata(Metadata metadata) {
listener.onMetadata(metadata);
}
// DeviceListener callbacks
@Override
public void onDeviceInfoChanged(DeviceInfo deviceInfo) {
listener.onDeviceInfoChanged(deviceInfo);
}
@Override
public void onDeviceVolumeChanged(int volume, boolean muted) {
listener.onDeviceVolumeChanged(volume, muted);
}
return builder.build();
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 The Android Open Source Project
* Copyright 2021 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.
@ -15,26 +15,18 @@
*/
package com.google.android.exoplayer2;
import static com.google.android.exoplayer2.Player.COMMAND_PLAY_PAUSE;
import static com.google.android.exoplayer2.Player.COMMAND_PREPARE_STOP;
import static com.google.android.exoplayer2.Player.COMMAND_SEEK_TO_MEDIA_ITEM;
import static com.google.android.exoplayer2.Player.EVENT_AVAILABLE_COMMANDS_CHANGED;
import static com.google.android.exoplayer2.Player.EVENT_PLAYBACK_STATE_CHANGED;
import static com.google.android.exoplayer2.Player.STATE_READY;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static com.google.android.exoplayer2.Player.EVENT_IS_PLAYING_CHANGED;
import static com.google.android.exoplayer2.Player.EVENT_MEDIA_ITEM_TRANSITION;
import static com.google.android.exoplayer2.Player.EVENT_TIMELINE_CHANGED;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.os.Looper;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.StubExoPlayer;
import com.google.android.exoplayer2.util.ExoFlags;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayDeque;
@ -46,220 +38,112 @@ import java.util.Queue;
import java.util.Set;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatcher;
import org.mockito.InOrder;
import org.robolectric.shadows.ShadowLooper;
import org.mockito.ArgumentCaptor;
/** Unit test for {@link ForwardingPlayer}. */
/** Unit tests for {@link ForwardingPlayer}. */
@RunWith(AndroidJUnit4.class)
public class ForwardingPlayerTest {
@Test
public void getAvailableCommands_withDisabledCommands_filtersDisabledCommands() {
Player player = new FakePlayer(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP);
public void addListener_addsForwardingListener() {
FakePlayer player = new FakePlayer();
Player.Listener listener1 = mock(Player.Listener.class);
Player.Listener listener2 = mock(Player.Listener.class);
ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player);
forwardingPlayer.setDisabledCommands(buildCommands(COMMAND_PREPARE_STOP));
forwardingPlayer.addListener(listener1);
// Add listener1 again.
forwardingPlayer.addListener(listener1);
forwardingPlayer.addListener(listener2);
assertThat(forwardingPlayer.getAvailableCommands())
.isEqualTo(buildCommands(COMMAND_PLAY_PAUSE));
assertThat(player.listeners).hasSize(2);
}
@Test
public void getAvailableCommands_playerAvailableCommandsChanged_returnsFreshCommands() {
FakePlayer player = new FakePlayer(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP);
@SuppressWarnings("deprecation") // Testing backwards compatibility with deprecated method.
public void addEventListener_addsForwardingListener() {
FakePlayer player = new FakePlayer();
Player.EventListener listener1 = mock(Player.EventListener.class);
Player.EventListener listener2 = mock(Player.EventListener.class);
ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player);
forwardingPlayer.addListener(listener1);
// Add listener1 again.
forwardingPlayer.addListener(listener1);
forwardingPlayer.addListener(listener2);
forwardingPlayer.setDisabledCommands(buildCommands(COMMAND_PREPARE_STOP));
assertThat(forwardingPlayer.getAvailableCommands())
.isEqualTo(buildCommands(COMMAND_PLAY_PAUSE));
player.setAvailableCommands(
buildCommands(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP, COMMAND_SEEK_TO_MEDIA_ITEM));
assertThat(forwardingPlayer.getAvailableCommands())
.isEqualTo(buildCommands(COMMAND_PLAY_PAUSE, COMMAND_SEEK_TO_MEDIA_ITEM));
assertThat(player.eventListeners).hasSize(2);
}
@Test
public void isCommandAvailable_withDisabledCommands_filtersDisabledCommands() {
Player player = new FakePlayer(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP);
public void removeListener_removesForwardingListener() {
FakePlayer player = new FakePlayer();
Player.Listener listener1 = mock(Player.Listener.class);
Player.Listener listener2 = mock(Player.Listener.class);
ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player);
forwardingPlayer.setDisabledCommands(buildCommands(COMMAND_PREPARE_STOP));
forwardingPlayer.addListener(listener1);
forwardingPlayer.addListener(listener2);
assertThat(forwardingPlayer.isCommandAvailable(COMMAND_PLAY_PAUSE)).isTrue();
assertThat(forwardingPlayer.isCommandAvailable(COMMAND_PREPARE_STOP)).isFalse();
forwardingPlayer.removeListener(listener1);
assertThat(player.listeners).hasSize(1);
// Remove same listener again.
forwardingPlayer.removeListener(listener1);
assertThat(player.listeners).hasSize(1);
forwardingPlayer.removeListener(listener2);
assertThat(player.listeners).isEmpty();
}
@Test
public void setDisabledCommands_triggersOnCommandsAvailableChanged() {
Player player = new FakePlayer(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP);
Player.Listener listener = mock(Player.Listener.class);
@SuppressWarnings("deprecation") // Testing backwards compatibility with deprecated method.
public void removeEventListener_removesForwardingListener() {
FakePlayer player = new FakePlayer();
Player.EventListener listener1 = mock(Player.EventListener.class);
Player.EventListener listener2 = mock(Player.EventListener.class);
ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player);
forwardingPlayer.addListener(listener);
forwardingPlayer.addListener(listener1);
forwardingPlayer.addListener(listener2);
forwardingPlayer.setDisabledCommands(buildCommands(COMMAND_PREPARE_STOP));
ShadowLooper.idleMainLooper();
InOrder inOrder = inOrder(listener);
inOrder.verify(listener).onAvailableCommandsChanged(buildCommands(COMMAND_PLAY_PAUSE));
inOrder
.verify(listener)
.onEvents(
same(forwardingPlayer), argThat(new EventsMatcher(EVENT_AVAILABLE_COMMANDS_CHANGED)));
inOrder.verifyNoMoreInteractions();
forwardingPlayer.removeListener(listener1);
assertThat(player.eventListeners).hasSize(1);
// Remove same listener again.
forwardingPlayer.removeListener(listener1);
assertThat(player.eventListeners).hasSize(1);
forwardingPlayer.removeListener(listener2);
assertThat(player.eventListeners).isEmpty();
}
@Test
public void setDisabledCommands_withoutChangingAvailableCommands_noCallbackTriggered() {
Player player = new FakePlayer(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP);
Player.Listener listener = mock(Player.Listener.class);
ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player);
forwardingPlayer.addListener(listener);
forwardingPlayer.setDisabledCommands(buildCommands(COMMAND_SEEK_TO_MEDIA_ITEM));
ShadowLooper.idleMainLooper();
verifyNoMoreInteractions(listener);
}
@Test
public void setDisabledCommands_multipleTimes_availableCommandsUpdated() {
Player player = new FakePlayer(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP);
ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player);
forwardingPlayer.setDisabledCommands(buildCommands(COMMAND_SEEK_TO_MEDIA_ITEM));
assertThat(forwardingPlayer.getAvailableCommands())
.isEqualTo(buildCommands(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP));
forwardingPlayer.setDisabledCommands(
buildCommands(COMMAND_PREPARE_STOP, COMMAND_SEEK_TO_MEDIA_ITEM));
assertThat(forwardingPlayer.getAvailableCommands())
.isEqualTo(buildCommands(COMMAND_PLAY_PAUSE));
}
@Test
public void onCommandsAvailableChanged_listenerChangesCommandsRecursively_secondCallbackCalled() {
FakePlayer player = new FakePlayer(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP);
ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player);
Player.Listener listener =
spy(
new Player.Listener() {
@Override
public void onAvailableCommandsChanged(Player.Commands availableCommands) {
// The callback changes the forwarding player's disabled commands triggering
// exactly one more callback.
forwardingPlayer.setDisabledCommands(buildCommands(COMMAND_PREPARE_STOP));
}
});
forwardingPlayer.addListener(listener);
Player.Commands updatedCommands =
buildCommands(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP, COMMAND_SEEK_TO_MEDIA_ITEM);
player.setAvailableCommands(updatedCommands);
player.forwardingListener.onAvailableCommandsChanged(updatedCommands);
ShadowLooper.idleMainLooper();
InOrder inOrder = inOrder(listener);
inOrder
.verify(listener)
.onAvailableCommandsChanged(
buildCommands(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP, COMMAND_SEEK_TO_MEDIA_ITEM));
inOrder
.verify(listener)
.onAvailableCommandsChanged(buildCommands(COMMAND_PLAY_PAUSE, COMMAND_SEEK_TO_MEDIA_ITEM));
inOrder
.verify(listener)
.onEvents(
same(forwardingPlayer), argThat(new EventsMatcher(EVENT_AVAILABLE_COMMANDS_CHANGED)));
inOrder.verifyNoMoreInteractions();
}
@Test
public void
interceptingOnAvailableCommandsChanged_withDisabledCommands_filtersDisabledCommands() {
public void onEvents_passesForwardingPlayerAsArgument() {
FakePlayer player = new FakePlayer();
Player.Listener listener = mock(Player.Listener.class);
ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player);
forwardingPlayer.addListener(listener);
Player.Listener forwardingListener = player.listeners.iterator().next();
forwardingPlayer.setDisabledCommands(buildCommands(COMMAND_PREPARE_STOP));
ShadowLooper.idleMainLooper();
// Setting the disabled commands did not affect the available commands, hence no callback was
// triggered.
verifyNoMoreInteractions(listener);
forwardingListener.onEvents(
player,
new Player.Events(
new ExoFlags.Builder()
.addAll(
EVENT_TIMELINE_CHANGED, EVENT_MEDIA_ITEM_TRANSITION, EVENT_IS_PLAYING_CHANGED)
.build()));
// The wrapped player advertises new available commands.
Player.Commands updatedCommands = buildCommands(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP);
player.setAvailableCommands(updatedCommands);
player.forwardingListener.onAvailableCommandsChanged(updatedCommands);
ShadowLooper.idleMainLooper();
verify(listener).onAvailableCommandsChanged(buildCommands(COMMAND_PLAY_PAUSE));
verify(listener)
.onEvents(
same(forwardingPlayer), argThat(new EventsMatcher(EVENT_AVAILABLE_COMMANDS_CHANGED)));
verifyNoMoreInteractions(listener);
}
@Test
public void
interceptingOnAvailableCommandsChanged_withDisabledCommandsButAvailableCommandsNotChanged_doesNotForwardCallback() {
FakePlayer player = new FakePlayer(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP);
Player.Listener listener = mock(Player.Listener.class);
ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player);
forwardingPlayer.addListener(listener);
// Disable commands that do not affect the available commands.
forwardingPlayer.setDisabledCommands(buildCommands(COMMAND_SEEK_TO_MEDIA_ITEM));
ShadowLooper.idleMainLooper();
verifyNoMoreInteractions(listener);
// The wrapped player advertises new available commands which, after filtering the disabled
// commands, do not change the available commands.
Player.Commands updatedCommands =
buildCommands(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP, COMMAND_SEEK_TO_MEDIA_ITEM);
player.setAvailableCommands(updatedCommands);
player.forwardingListener.onAvailableCommandsChanged(updatedCommands);
ShadowLooper.idleMainLooper();
verifyNoMoreInteractions(listener);
}
@Test
public void removeListener_removesListenerFromPlayer() {
FakePlayer player = new FakePlayer(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP);
Player.Listener listener = mock(Player.Listener.class);
ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player);
forwardingPlayer.addListener(listener);
assertThat(player.forwardingListener).isNotNull();
forwardingPlayer.removeListener(listener);
assertThat(player.forwardingListener).isNull();
}
@Test
public void addEventListener_forwardsEventListenerEvents() {
FakePlayer player = new FakePlayer(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP);
Player.EventListener eventListener = mock(Player.EventListener.class);
ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player);
forwardingPlayer.addListener(eventListener);
player.forwardingListener.onPlaybackStateChanged(STATE_READY);
ShadowLooper.idleMainLooper();
InOrder inOrder = inOrder(eventListener);
inOrder.verify(eventListener).onPlaybackStateChanged(STATE_READY);
inOrder
.verify(eventListener)
.onEvents(same(forwardingPlayer), argThat(new EventsMatcher(EVENT_PLAYBACK_STATE_CHANGED)));
inOrder.verifyNoMoreInteractions();
ArgumentCaptor<Player.Events> eventsArgumentCaptor =
ArgumentCaptor.forClass(Player.Events.class);
verify(listener).onEvents(same(forwardingPlayer), eventsArgumentCaptor.capture());
Player.Events receivedEvents = eventsArgumentCaptor.getValue();
assertThat(receivedEvents.size()).isEqualTo(3);
assertThat(receivedEvents.contains(EVENT_TIMELINE_CHANGED)).isTrue();
assertThat(receivedEvents.contains(EVENT_MEDIA_ITEM_TRANSITION)).isTrue();
assertThat(receivedEvents.contains(EVENT_IS_PLAYING_CHANGED)).isTrue();
}
@Test
public void forwardingPlayer_overridesAllPlayerMethods() throws Exception {
// Check with reflection that ForwardingPlayer overrides all Player methods.
List<Method> playerMethods = getPublicMethods(Player.class);
for (int i = 0; i < playerMethods.size(); i++) {
Method method = playerMethods.get(i);
List<Method> methods = getPublicMethods(Player.class);
for (int i = 0; i < methods.size(); i++) {
Method method = methods.get(i);
assertThat(
ForwardingPlayer.class.getDeclaredMethod(
method.getName(), method.getParameterTypes()))
@ -268,13 +152,13 @@ public class ForwardingPlayerTest {
}
@Test
public void forwardingListener_overridesAllListenerMethods() throws Exception {
// Check with reflection that ForwardingListener in ForwardingPlayer overrides all Listener
// methods.
Class<?> forwardingListenerClass = getNestedClass("ForwardingListener");
List<Method> publicListenerMethods = getPublicMethods(Player.Listener.class);
for (int i = 0; i < publicListenerMethods.size(); i++) {
Method method = publicListenerMethods.get(i);
@SuppressWarnings("deprecation") // Testing backwards compatibility with deprecated type.
public void forwardingEventListener_overridesAllEventListenerMethods() throws Exception {
// Check with reflection that ForwardingListener overrides all Listener methods.
Class<?> forwardingListenerClass = getInnerClass("ForwardingEventListener");
List<Method> methods = getPublicMethods(Player.EventListener.class);
for (int i = 0; i < methods.size(); i++) {
Method method = methods.get(i);
assertThat(
forwardingListenerClass.getDeclaredMethod(
method.getName(), method.getParameterTypes()))
@ -283,99 +167,20 @@ public class ForwardingPlayerTest {
}
@Test
public void eventListenerWrapper_overridesAllEventListenerMethods() throws Exception {
// Check with reflection that EventListenerWrapper in ForwardingPlayer overrides all
// EventListener methods.
Class<?> listenerWrapperClass = getNestedClass("EventListenerWrapper");
List<Method> publicListenerMethods = getPublicMethods(Player.EventListener.class);
for (int i = 0; i < publicListenerMethods.size(); i++) {
Method method = publicListenerMethods.get(i);
assertThat(
listenerWrapperClass.getDeclaredMethod(method.getName(), method.getParameterTypes()))
public void forwardingListener_overridesAllListenerMethods() throws Exception {
// Check with reflection that ForwardingListener overrides all Listener methods.
Class<?> forwardingListenerClass = getInnerClass("ForwardingListener");
List<Method> methods = getPublicMethods(Player.Listener.class);
for (int i = 0; i < methods.size(); i++) {
Method method = methods.get(i);
assertThat(forwardingListenerClass.getMethod(method.getName(), method.getParameterTypes()))
.isNotNull();
}
}
private static class FakePlayer extends StubExoPlayer {
private Commands availableCommands;
/**
* Supports up to 1 registered listener, named deliberately forwardingListener to emphasize its
* purpose.
*/
@Nullable private Listener forwardingListener;
public FakePlayer() {
this.availableCommands = Commands.EMPTY;
}
public FakePlayer(@Command int... commands) {
this.availableCommands = new Commands.Builder().addAll(commands).build();
}
@Override
public void addListener(Listener listener) {
checkState(this.forwardingListener == null);
this.forwardingListener = listener;
}
@Override
public void removeListener(Listener listener) {
checkState(this.forwardingListener.equals(listener));
this.forwardingListener = null;
}
@Override
public Commands getAvailableCommands() {
return availableCommands;
}
@Override
public Looper getApplicationLooper() {
return Looper.getMainLooper();
}
public void setAvailableCommands(Commands availableCommands) {
this.availableCommands = availableCommands;
}
}
private static Player.Commands buildCommands(@Player.Command int... commands) {
return new Player.Commands.Builder().addAll(commands).build();
}
private Class<?> getNestedClass(String className) {
for (Class<?> declaredClass : ForwardingPlayer.class.getDeclaredClasses()) {
if (declaredClass.getSimpleName().equals(className)) {
return declaredClass;
}
}
throw new IllegalStateException();
}
private static class EventsMatcher implements ArgumentMatcher<Player.Events> {
private final int[] events;
private EventsMatcher(int... events) {
this.events = events;
}
@Override
public boolean matches(Player.Events argument) {
if (events.length != argument.size()) {
return false;
}
for (int event : events) {
if (!argument.contains(event)) {
return false;
}
}
return true;
}
}
/** Returns all the methods of Java interface. */
/** Returns all the public methods of a Java interface. */
private static List<Method> getPublicMethods(Class<?> anInterface) {
assertThat(anInterface.isInterface()).isTrue();
checkArgument(anInterface.isInterface());
// Run a BFS over all extended interfaces to inspect them all.
Queue<Class<?>> interfacesQueue = new ArrayDeque<>();
interfacesQueue.add(anInterface);
@ -398,4 +203,43 @@ public class ForwardingPlayerTest {
return list;
}
private static Class<?> getInnerClass(String className) {
for (Class<?> innerClass : ForwardingPlayer.class.getDeclaredClasses()) {
if (innerClass.getSimpleName().equals(className)) {
return innerClass;
}
}
throw new IllegalStateException();
}
private static class FakePlayer extends StubExoPlayer {
@SuppressWarnings("deprecation") // Use of deprecated type for backwards compatibility.
private final Set<EventListener> eventListeners = new HashSet<>();
private final Set<Listener> listeners = new HashSet<>();
@Override
@SuppressWarnings("deprecation") // Implementing deprecated method.
public void addListener(EventListener listener) {
eventListeners.add(listener);
}
@Override
public void addListener(Listener listener) {
listeners.add(listener);
}
@Override
@SuppressWarnings("deprecation") // Implementing deprecated method.
public void removeListener(EventListener listener) {
eventListeners.remove(listener);
}
@Override
public void removeListener(Listener listener) {
listeners.remove(listener);
}
}
}