From 65d8ff80db383b51c402563ff54d5faa68cb16e0 Mon Sep 17 00:00:00 2001 From: christosts Date: Wed, 19 May 2021 12:56:54 +0100 Subject: [PATCH] ForwardingPlayer only forwards Player operations PiperOrigin-RevId: 374621615 --- .../android/exoplayer2/ForwardingPlayer.java | 540 +++++------------- .../exoplayer2/ForwardingPlayerTest.java | 418 +++++--------- 2 files changed, 261 insertions(+), 697 deletions(-) diff --git a/library/common/src/main/java/com/google/android/exoplayer2/ForwardingPlayer.java b/library/common/src/main/java/com/google/android/exoplayer2/ForwardingPlayer.java index e9202190f8..2925987807 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/ForwardingPlayer.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/ForwardingPlayer.java @@ -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. - * - *

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}. - * - *

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 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 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 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 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 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 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(); } } diff --git a/library/common/src/test/java/com/google/android/exoplayer2/ForwardingPlayerTest.java b/library/common/src/test/java/com/google/android/exoplayer2/ForwardingPlayerTest.java index 944db43330..edfe2693d5 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/ForwardingPlayerTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/ForwardingPlayerTest.java @@ -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 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 playerMethods = getPublicMethods(Player.class); - for (int i = 0; i < playerMethods.size(); i++) { - Method method = playerMethods.get(i); + List 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 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 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 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 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 { - 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 getPublicMethods(Class anInterface) { - assertThat(anInterface.isInterface()).isTrue(); + checkArgument(anInterface.isInterface()); // Run a BFS over all extended interfaces to inspect them all. Queue> 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 eventListeners = new HashSet<>(); + + private final Set 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); + } + } }