Override available commands in ForwardingPlayer

This change adds an API in the ForwardingPlayer to disable commands.
This is affecting what Player.isCommandAvailable() returns as
well as what is being advertised from the
EventListener.onAvailableCommandsChanged() callback.

For the callback case, the ForwardingPlayer needs to intercept the
callback. It does so by wrapping registered EventListener and Listener
instances, which resulted in some boiler-plate code. In addition, there
is logic on the wrapped listeners to avoid triggering a queued callback
if all listeners have been removed in the meantime. This includes the
case where new listeners are added while callbacks scheduled for the
removed listeners are still pending.

PiperOrigin-RevId: 371139703
This commit is contained in:
christosts 2021-04-29 17:44:21 +01:00 committed by bachinger
parent cdff456621
commit 7d2a2aa283
4 changed files with 927 additions and 5 deletions

View file

@ -37,6 +37,7 @@ dependencies {
testImplementation 'com.google.truth:truth:' + truthVersion
testImplementation 'com.squareup.okhttp3:mockwebserver:' + mockWebServerVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
testImplementation project(modulePrefix + 'library-core')
testImplementation project(modulePrefix + 'testutils')
}

View file

@ -15,31 +15,74 @@
*/
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 extends BasePlayer {
private final Player player;
private final Clock clock;
@Nullable private ForwardingListener forwardingListener;
private Commands disabledCommands;
@Nullable private Commands unfilteredCommands;
@Nullable private Commands filteredCommands;
/** 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
@ -49,22 +92,34 @@ public class ForwardingPlayer extends BasePlayer {
@Override
public void addListener(EventListener listener) {
player.addListener(listener);
addListener(new EventListenerWrapper(listener));
}
@Override
public void addListener(Listener listener) {
player.addListener(listener);
if (forwardingListener == null) {
forwardingListener = new ForwardingListener(this);
}
if (!forwardingListener.isRegistered()) {
forwardingListener.registerTo(player);
}
forwardingListener.addListener(listener);
}
@Override
public void removeListener(EventListener listener) {
player.removeListener(listener);
removeListener(new EventListenerWrapper(listener));
}
@Override
public void removeListener(Listener listener) {
player.removeListener(listener);
if (forwardingListener == null) {
return;
}
forwardingListener.removeListener(listener);
if (!forwardingListener.hasListeners()) {
forwardingListener.unregisterFrom(player);
}
}
@Override
@ -95,7 +150,12 @@ public class ForwardingPlayer extends BasePlayer {
@Override
public Commands getAvailableCommands() {
return player.getAvailableCommands();
Commands commands = player.getAvailableCommands();
if (filteredCommands == null || !commands.equals(unfilteredCommands)) {
filteredCommands = filterCommands(commands, disabledCommands);
unfilteredCommands = commands;
}
return filteredCommands;
}
@Override
@ -364,4 +424,474 @@ public class ForwardingPlayer extends BasePlayer {
public void setDeviceMuted(boolean muted) {
player.setDeviceMuted(muted);
}
/**
* Wraps a {@link Listener} and intercepts {@link EventListener#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;
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;
}
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));
}
@Override
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
public void onTimelineChanged(
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
listeners.sendEvent(
EVENT_TIMELINE_CHANGED,
listener -> listener.onTimelineChanged(timeline, manifest, reason));
}
@Override
public void onMediaItemTransition(
@Nullable MediaItem mediaItem, @MediaItemTransitionReason int reason) {
listeners.sendEvent(
EVENT_MEDIA_ITEM_TRANSITION,
listener -> listener.onMediaItemTransition(mediaItem, reason));
}
@Override
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
listeners.sendEvent(
EVENT_TRACKS_CHANGED, listener -> listener.onTracksChanged(trackGroups, trackSelections));
}
@Override
public void onStaticMetadataChanged(List<Metadata> metadataList) {
listeners.sendEvent(
EVENT_STATIC_METADATA_CHANGED,
listener -> listener.onStaticMetadataChanged(metadataList));
}
@Override
public void onMediaMetadataChanged(MediaMetadata mediaMetadata) {
listeners.sendEvent(
EVENT_MEDIA_METADATA_CHANGED, listener -> listener.onMediaMetadataChanged(mediaMetadata));
}
@Override
public void onIsLoadingChanged(boolean isLoading) {
listeners.sendEvent(
EVENT_IS_LOADING_CHANGED, listener -> listener.onIsLoadingChanged(isLoading));
}
@Override
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
public void onLoadingChanged(boolean isLoading) {
listeners.sendEvent(
EVENT_IS_LOADING_CHANGED, listener -> listener.onLoadingChanged(isLoading));
}
@Override
public void onAvailableCommandsChanged(Commands availableCommands) {
lastReceivedCommands = availableCommands;
maybeAdvertiseAvailableCommands();
}
@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));
}
@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
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
public void onTimelineChanged(
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
listener.onTimelineChanged(timeline, manifest, reason);
}
@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);
}
@Override
public void onPlayWhenReadyChanged(
boolean playWhenReady, @PlayWhenReadyChangeReason int reason) {
listener.onPlayWhenReadyChanged(playWhenReady, reason);
}
@Override
public void onPlaybackSuppressionReasonChanged(
@PlaybackSuppressionReason int playbackSuppressionReason) {
listener.onPlaybackSuppressionReasonChanged(playbackSuppressionReason);
}
@Override
public void onIsPlayingChanged(boolean isPlaying) {
listener.onIsPlayingChanged(isPlaying);
}
@Override
public void onRepeatModeChanged(@RepeatMode int repeatMode) {
listener.onRepeatModeChanged(repeatMode);
}
@Override
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
listener.onShuffleModeEnabledChanged(shuffleModeEnabled);
}
@Override
public void onPlayerError(ExoPlaybackException error) {
listener.onPlayerError(error);
}
@Override
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
listener.onPositionDiscontinuity(reason);
}
@Override
public void onPositionDiscontinuity(
PositionInfo oldPosition, PositionInfo newPosition, @DiscontinuityReason int reason) {
listener.onPositionDiscontinuity(oldPosition, newPosition, reason);
}
@Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
listener.onPlaybackParametersChanged(playbackParameters);
}
@Override
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
public void onSeekProcessed() {
listener.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();
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (!(o instanceof EventListenerWrapper)) {
return false;
}
EventListenerWrapper that = (EventListenerWrapper) o;
return listener.equals(that.listener);
}
@Override
public int hashCode() {
return listener.hashCode();
}
}
/** Returns the remaining available commands after removing disabled commands. */
private static Commands filterCommands(Commands availableCommands, Commands disabledCommands) {
if (disabledCommands.size() == 0) {
return availableCommands;
}
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));
}
return builder.build();
}
}

View file

@ -155,6 +155,11 @@ public final class ListenerSet<T> {
}
}
/** Returns the number of added listeners. */
public int size() {
return listeners.size();
}
/**
* Adds an event that is sent to the listeners when {@link #flushEvents} is called.
*

View file

@ -0,0 +1,386 @@
/*
* Copyright (C) 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.
* 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.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.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 java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
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;
/** Unit test 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);
ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player);
forwardingPlayer.setDisabledCommands(buildCommands(COMMAND_PREPARE_STOP));
assertThat(forwardingPlayer.getAvailableCommands())
.isEqualTo(buildCommands(COMMAND_PLAY_PAUSE));
}
@Test
public void getAvailableCommands_playerAvailableCommandsChanged_returnsFreshCommands() {
FakePlayer player = new FakePlayer(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP);
ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player);
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));
}
@Test
public void isCommandAvailable_withDisabledCommands_filtersDisabledCommands() {
Player player = new FakePlayer(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP);
ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player);
forwardingPlayer.setDisabledCommands(buildCommands(COMMAND_PREPARE_STOP));
assertThat(forwardingPlayer.isCommandAvailable(COMMAND_PLAY_PAUSE)).isTrue();
assertThat(forwardingPlayer.isCommandAvailable(COMMAND_PREPARE_STOP)).isFalse();
}
@Test
public void setDisabledCommands_triggersOnCommandsAvailableChanged() {
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_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();
}
@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() {
FakePlayer player = new FakePlayer();
Player.Listener listener = mock(Player.Listener.class);
ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player);
forwardingPlayer.addListener(listener);
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);
// 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();
}
@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 (Method method : publicListenerMethods) {
assertThat(
forwardingListenerClass.getDeclaredMethod(
method.getName(), method.getParameterTypes()))
.isNotNull();
}
}
@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 (Method method : publicListenerMethods) {
assertThat(
listenerWrapperClass.getDeclaredMethod(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. */
private static List<Method> getPublicMethods(Class<?> anInterface) {
assertThat(anInterface.isInterface()).isTrue();
// Run a BFS over all extended interfaces to inspect them all.
Queue<Class<?>> interfacesQueue = new ArrayDeque<>();
interfacesQueue.add(anInterface);
Set<Class<?>> interfaces = new HashSet<>();
while (!interfacesQueue.isEmpty()) {
Class<?> currentInterface = interfacesQueue.remove();
if (interfaces.add(currentInterface)) {
Collections.addAll(interfacesQueue, currentInterface.getInterfaces());
}
}
List<Method> list = new ArrayList<>();
for (Class<?> currentInterface : interfaces) {
for (Method method : currentInterface.getDeclaredMethods()) {
if (Modifier.isPublic(method.getModifiers())) {
list.add(method);
}
}
}
return list;
}
}