From 3d8c52f28d5d3ef04c14868e15036563a9fc662d Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 23 Nov 2022 14:10:08 +0000 Subject: [PATCH] Exclude tracks from `PlayerInfo` if not changed This change includes a change in the `IMediaController.aidl` file and needs to provide backwards compatibility for when a client connects that is of an older or newer version of the current service implementation. This CL proposes to create a new AIDL method `onPlayerInfoChangedWithExtensions` that is easier to extend in the future because it does use an `Bundle` rather than primitives. A `Bundle` can be changed in a backward/forwards compatible way in case we need further changes. The compatibility handling is provided in `MediaSessionStub` and `MediaControllerStub`. The approach is not based on specific AIDL/Binder features but implemented fully in application code. Issue: androidx/media#102 #minor-release PiperOrigin-RevId: 490483068 --- .../java/androidx/media3/common/Player.java | 3 +- .../media3/session/IMediaController.aidl | 9 +- .../session/MediaControllerImplBase.java | 47 ++++-- .../media3/session/MediaControllerStub.java | 27 +++- .../androidx/media3/session/MediaSession.java | 3 +- .../media3/session/MediaSessionImpl.java | 130 ++++++++++------ .../media3/session/MediaSessionStub.java | 36 +++-- .../androidx/media3/session/MediaUtils.java | 42 ++++++ .../androidx/media3/session/PlayerInfo.java | 4 + .../session/MediaControllerListenerTest.java | 86 +++++------ .../media3/session/MediaUtilsTest.java | 140 ++++++++++++++++++ 11 files changed, 396 insertions(+), 131 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/Player.java b/libraries/common/src/main/java/androidx/media3/common/Player.java index 5ad4cf90ad..de3005b2bb 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Player.java +++ b/libraries/common/src/main/java/androidx/media3/common/Player.java @@ -677,7 +677,8 @@ public interface Player { * to the current {@link #getRepeatMode() repeat mode}. * *

Note that this callback is also called when the playlist becomes non-empty or empty as a - * consequence of a playlist change. + * consequence of a playlist change or {@linkplain #onAvailableCommandsChanged(Commands) a + * change in available commands}. * *

{@link #onEvents(Player, Events)} will also be called to report this event along with * other events that happen in the same {@link Looper} message queue iteration. diff --git a/libraries/session/src/main/aidl/androidx/media3/session/IMediaController.aidl b/libraries/session/src/main/aidl/androidx/media3/session/IMediaController.aidl index d1a348cd3a..7c1eb001d2 100644 --- a/libraries/session/src/main/aidl/androidx/media3/session/IMediaController.aidl +++ b/libraries/session/src/main/aidl/androidx/media3/session/IMediaController.aidl @@ -35,14 +35,19 @@ oneway interface IMediaController { void onSetCustomLayout(int seq, in List commandButtonList) = 3003; void onCustomCommand(int seq, in Bundle command, in Bundle args) = 3004; void onDisconnected(int seq) = 3005; - void onPlayerInfoChanged(int seq, in Bundle playerInfoBundle, boolean isTimelineExcluded) = 3006; + /** Deprecated: Use onPlayerInfoChangedWithExclusions from MediaControllerStub#VERSION_INT=2. */ + void onPlayerInfoChanged( + int seq, in Bundle playerInfoBundle, boolean isTimelineExcluded) = 3006; + /** Introduced to deprecate onPlayerInfoChanged (from MediaControllerStub#VERSION_INT=2). */ + void onPlayerInfoChangedWithExclusions( + int seq, in Bundle playerInfoBundle, in Bundle playerInfoExclusions) = 3012; void onPeriodicSessionPositionInfoChanged(int seq, in Bundle sessionPositionInfo) = 3007; void onAvailableCommandsChangedFromPlayer(int seq, in Bundle commandsBundle) = 3008; void onAvailableCommandsChangedFromSession( int seq, in Bundle sessionCommandsBundle, in Bundle playerCommandsBundle) = 3009; void onRenderedFirstFrame(int seq) = 3010; void onExtrasChanged(int seq, in Bundle extras) = 3011; - // Next Id for MediaController: 3012 + // Next Id for MediaController: 3013 void onChildrenChanged( int seq, String parentId, int itemCount, in @nullable Bundle libraryParams) = 4000; diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java index 4f7940b30a..6560cea856 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java @@ -23,6 +23,7 @@ import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Util.usToMs; import static androidx.media3.session.MediaUtils.calculateBufferedPercentage; import static androidx.media3.session.MediaUtils.intersect; +import static androidx.media3.session.MediaUtils.mergePlayerInfo; import static java.lang.Math.max; import static java.lang.Math.min; @@ -42,6 +43,7 @@ import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; import android.support.v4.media.MediaBrowserCompat; +import android.util.Pair; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; @@ -79,6 +81,7 @@ import androidx.media3.common.util.Log; import androidx.media3.common.util.Size; import androidx.media3.common.util.Util; import androidx.media3.session.MediaController.MediaControllerImpl; +import androidx.media3.session.PlayerInfo.BundlingExclusions; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -129,7 +132,8 @@ import org.checkerframework.checker.nullness.qual.NonNull; @Nullable private IMediaSession iSession; private long lastReturnedCurrentPositionMs; private long lastSetPlayWhenReadyCalledTimeMs; - @Nullable private Timeline pendingPlayerInfoUpdateTimeline; + @Nullable private PlayerInfo pendingPlayerInfo; + @Nullable private BundlingExclusions pendingBundlingExclusions; public MediaControllerImplBase( Context context, @@ -2329,30 +2333,41 @@ import org.checkerframework.checker.nullness.qual.NonNull; } @SuppressWarnings("deprecation") // Implementing and calling deprecated listener method. - void onPlayerInfoChanged(PlayerInfo newPlayerInfo, boolean isTimelineExcluded) { + void onPlayerInfoChanged(PlayerInfo newPlayerInfo, BundlingExclusions bundlingExclusions) { if (!isConnected()) { return; } + if (pendingPlayerInfo != null && pendingBundlingExclusions != null) { + Pair mergedPlayerInfoUpdate = + mergePlayerInfo( + pendingPlayerInfo, + pendingBundlingExclusions, + newPlayerInfo, + bundlingExclusions, + intersectedPlayerCommands); + newPlayerInfo = mergedPlayerInfoUpdate.first; + bundlingExclusions = mergedPlayerInfoUpdate.second; + } + pendingPlayerInfo = null; + pendingBundlingExclusions = null; if (!pendingMaskingSequencedFutureNumbers.isEmpty()) { // We are still waiting for all pending masking operations to be handled. - if (!isTimelineExcluded) { - pendingPlayerInfoUpdateTimeline = newPlayerInfo.timeline; - } + pendingPlayerInfo = newPlayerInfo; + pendingBundlingExclusions = bundlingExclusions; return; } PlayerInfo oldPlayerInfo = playerInfo; - if (isTimelineExcluded) { - newPlayerInfo = - newPlayerInfo.copyWithTimeline( - pendingPlayerInfoUpdateTimeline != null - ? pendingPlayerInfoUpdateTimeline - : oldPlayerInfo.timeline); - } // Assigning class variable now so that all getters called from listeners see the updated value. // But we need to use a local final variable to ensure listeners get consistent parameters. - playerInfo = newPlayerInfo; - PlayerInfo finalPlayerInfo = newPlayerInfo; - pendingPlayerInfoUpdateTimeline = null; + playerInfo = + mergePlayerInfo( + oldPlayerInfo, + /* oldBundlingExclusions= */ BundlingExclusions.NONE, + newPlayerInfo, + /* newBundlingExclusions= */ bundlingExclusions, + intersectedPlayerCommands) + .first; + PlayerInfo finalPlayerInfo = playerInfo; PlaybackException oldPlayerError = oldPlayerInfo.playerError; PlaybackException playerError = finalPlayerInfo.playerError; boolean errorsMatch = @@ -2397,7 +2412,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; /* eventFlag= */ Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED, listener -> listener.onShuffleModeEnabledChanged(finalPlayerInfo.shuffleModeEnabled)); } - if (!isTimelineExcluded && !Util.areEqual(oldPlayerInfo.timeline, finalPlayerInfo.timeline)) { + if (!Util.areEqual(oldPlayerInfo.timeline, finalPlayerInfo.timeline)) { listeners.queueEvent( /* eventFlag= */ Player.EVENT_TIMELINE_CHANGED, listener -> diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerStub.java index 59b49bdda5..f9673ccf05 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerStub.java @@ -26,6 +26,7 @@ import androidx.media3.common.Player.Commands; import androidx.media3.common.util.BundleableUtil; import androidx.media3.common.util.Log; import androidx.media3.session.MediaLibraryService.LibraryParams; +import androidx.media3.session.PlayerInfo.BundlingExclusions; import java.lang.ref.WeakReference; import java.util.List; import org.checkerframework.checker.nullness.qual.NonNull; @@ -35,7 +36,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; private static final String TAG = "MediaControllerStub"; /** The version of the IMediaController interface. */ - public static final int VERSION_INT = 1; + public static final int VERSION_INT = 2; private final WeakReference controller; @@ -169,8 +170,23 @@ import org.checkerframework.checker.nullness.qual.NonNull; controller -> controller.notifyPeriodicSessionPositionInfoChanged(sessionPositionInfo)); } + /** + * @deprecated Use {@link #onPlayerInfoChangedWithExclusions} from {@link #VERSION_INT} 2. + */ @Override + @Deprecated public void onPlayerInfoChanged(int seq, Bundle playerInfoBundle, boolean isTimelineExcluded) { + onPlayerInfoChangedWithExclusions( + seq, + playerInfoBundle, + new BundlingExclusions(isTimelineExcluded, /* areCurrentTracksExcluded= */ true) + .toBundle()); + } + + /** Added in {@link #VERSION_INT} 2. */ + @Override + public void onPlayerInfoChangedWithExclusions( + int seq, Bundle playerInfoBundle, Bundle playerInfoExclusions) { PlayerInfo playerInfo; try { playerInfo = PlayerInfo.CREATOR.fromBundle(playerInfoBundle); @@ -178,8 +194,15 @@ import org.checkerframework.checker.nullness.qual.NonNull; Log.w(TAG, "Ignoring malformed Bundle for PlayerInfo", e); return; } + BundlingExclusions bundlingExclusions; + try { + bundlingExclusions = BundlingExclusions.CREATOR.fromBundle(playerInfoExclusions); + } catch (RuntimeException e) { + Log.w(TAG, "Ignoring malformed Bundle for BundlingExclusions", e); + return; + } dispatchControllerTaskOnHandler( - controller -> controller.onPlayerInfoChanged(playerInfo, isTimelineExcluded)); + controller -> controller.onPlayerInfoChanged(playerInfo, bundlingExclusions)); } @Override diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java index 9da5c30fe5..6b25c8d56c 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -1136,7 +1136,8 @@ public class MediaSession { boolean excludeMediaItemsMetadata, boolean excludeCues, boolean excludeTimeline, - boolean excludeTracks) + boolean excludeTracks, + int controllerInterfaceVersion) throws RemoteException {} default void onPeriodicSessionPositionInfoChanged( diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java index 705115745f..d01fb6eee3 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -15,6 +15,10 @@ */ package androidx.media3.session; +import static androidx.media3.common.Player.COMMAND_GET_MEDIA_ITEMS_METADATA; +import static androidx.media3.common.Player.COMMAND_GET_TEXT; +import static androidx.media3.common.Player.COMMAND_GET_TIMELINE; +import static androidx.media3.common.Player.COMMAND_GET_TRACKS; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Util.castNonNull; @@ -274,7 +278,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; } playerInfo = newPlayerWrapper.createPlayerInfoForBundling(); - onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ false); + onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ false, /* excludeTracks= */ false); } public void release() { @@ -374,7 +379,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; controller, (callback, seq) -> callback.onAvailableCommandsChangedFromSession(seq, sessionCommands, playerCommands)); - onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ false); + onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ false, /* excludeTracks= */ false); } else { sessionLegacyStub .getConnectedControllersManager() @@ -387,7 +393,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; (controller, seq) -> controller.sendCustomCommand(seq, command, args)); } - private void dispatchOnPlayerInfoChanged(PlayerInfo playerInfo, boolean excludeTimeline) { + private void dispatchOnPlayerInfoChanged( + PlayerInfo playerInfo, boolean excludeTimeline, boolean excludeTracks) { List controllers = sessionStub.getConnectedControllersManager().getConnectedControllers(); @@ -395,8 +402,9 @@ import org.checkerframework.checker.initialization.qual.Initialized; ControllerInfo controller = controllers.get(i); try { int seq; - SequencedFutureManager manager = - sessionStub.getConnectedControllersManager().getSequencedFutureManager(controller); + ConnectedControllersManager controllersManager = + sessionStub.getConnectedControllersManager(); + SequencedFutureManager manager = controllersManager.getSequencedFutureManager(controller); if (manager != null) { seq = manager.obtainNextSequenceNumber(); } else { @@ -410,19 +418,18 @@ import org.checkerframework.checker.initialization.qual.Initialized; .onPlayerInfoChanged( seq, playerInfo, - /* excludeMediaItems= */ !sessionStub - .getConnectedControllersManager() - .isPlayerCommandAvailable(controller, Player.COMMAND_GET_TIMELINE), - /* excludeMediaItemsMetadata= */ !sessionStub - .getConnectedControllersManager() - .isPlayerCommandAvailable(controller, Player.COMMAND_GET_MEDIA_ITEMS_METADATA), - /* excludeCues= */ !sessionStub - .getConnectedControllersManager() - .isPlayerCommandAvailable(controller, Player.COMMAND_GET_TEXT), - excludeTimeline, - /* excludeTracks= */ !sessionStub - .getConnectedControllersManager() - .isPlayerCommandAvailable(controller, Player.COMMAND_GET_TRACKS)); + /* excludeMediaItems= */ !controllersManager.isPlayerCommandAvailable( + controller, COMMAND_GET_TIMELINE), + /* excludeMediaItemsMetadata= */ !controllersManager.isPlayerCommandAvailable( + controller, COMMAND_GET_MEDIA_ITEMS_METADATA), + /* excludeCues= */ !controllersManager.isPlayerCommandAvailable( + controller, COMMAND_GET_TEXT), + excludeTimeline + || !controllersManager.isPlayerCommandAvailable( + controller, COMMAND_GET_TIMELINE), + excludeTracks + || !controllersManager.isPlayerCommandAvailable(controller, COMMAND_GET_TRACKS), + controller.getInterfaceVersion()); } catch (DeadObjectException e) { onDeadObjectException(controller); } catch (RemoteException e) { @@ -745,7 +752,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; return; } session.playerInfo = session.playerInfo.copyWithPlayerError(error); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onPlayerError(seq, error)); } @@ -765,7 +773,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; // Note: OK to omit mediaItem here, because PlayerInfo changed message will copy playerInfo // with sessionPositionInfo, which includes current window index. session.playerInfo = session.playerInfo.copyWithMediaItemTransitionReason(reason); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onMediaItemTransition(seq, mediaItem, reason)); } @@ -785,7 +794,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; session.playerInfo = session.playerInfo.copyWithPlayWhenReady( playWhenReady, reason, session.playerInfo.playbackSuppressionReason); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onPlayWhenReadyChanged(seq, playWhenReady, reason)); } @@ -806,7 +816,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; session.playerInfo.playWhenReady, session.playerInfo.playWhenReadyChangedReason, reason); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onPlaybackSuppressionReasonChanged(seq, reason)); } @@ -824,7 +835,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; } session.playerInfo = session.playerInfo.copyWithPlaybackState(playbackState, player.getPlayerError()); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> { callback.onPlaybackStateChanged(seq, playbackState, player.getPlayerError()); @@ -843,7 +855,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; return; } session.playerInfo = session.playerInfo.copyWithIsPlaying(isPlaying); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onIsPlayingChanged(seq, isPlaying)); } @@ -860,7 +873,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; return; } session.playerInfo = session.playerInfo.copyWithIsLoading(isLoading); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onIsLoadingChanged(seq, isLoading)); } @@ -880,7 +894,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; session.playerInfo = session.playerInfo.copyWithPositionInfos(oldPosition, newPosition, reason); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onPositionDiscontinuity(seq, oldPosition, newPosition, reason)); @@ -898,7 +913,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; return; } session.playerInfo = session.playerInfo.copyWithPlaybackParameters(playbackParameters); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onPlaybackParametersChanged(seq, playbackParameters)); } @@ -915,7 +931,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; return; } session.playerInfo = session.playerInfo.copyWithSeekBackIncrement(seekBackIncrementMs); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onSeekBackIncrementChanged(seq, seekBackIncrementMs)); } @@ -932,7 +949,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; return; } session.playerInfo = session.playerInfo.copyWithSeekForwardIncrement(seekForwardIncrementMs); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onSeekForwardIncrementChanged(seq, seekForwardIncrementMs)); } @@ -951,7 +969,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; session.playerInfo = session.playerInfo.copyWithTimelineAndSessionPositionInfo( timeline, player.createSessionPositionInfoForBundling()); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ false); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ false, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onTimelineChanged(seq, timeline, reason)); } @@ -964,7 +983,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; } session.verifyApplicationThread(); session.playerInfo = session.playerInfo.copyWithPlaylistMetadata(playlistMetadata); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onPlaylistMetadataChanged(seq, playlistMetadata)); } @@ -981,7 +1001,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; return; } session.playerInfo = session.playerInfo.copyWithRepeatMode(repeatMode); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onRepeatModeChanged(seq, repeatMode)); } @@ -998,7 +1019,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; return; } session.playerInfo = session.playerInfo.copyWithShuffleModeEnabled(shuffleModeEnabled); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onShuffleModeEnabledChanged(seq, shuffleModeEnabled)); } @@ -1015,7 +1037,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; return; } session.playerInfo = session.playerInfo.copyWithAudioAttributes(attributes); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (controller, seq) -> controller.onAudioAttributesChanged(seq, attributes)); } @@ -1028,7 +1051,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; } session.verifyApplicationThread(); session.playerInfo = session.playerInfo.copyWithVideoSize(size); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onVideoSizeChanged(seq, size)); } @@ -1041,7 +1065,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; } session.verifyApplicationThread(); session.playerInfo = session.playerInfo.copyWithVolume(volume); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onVolumeChanged(seq, volume)); } @@ -1058,7 +1083,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; return; } session.playerInfo = new PlayerInfo.Builder(session.playerInfo).setCues(cueGroup).build(); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); } @Override @@ -1073,7 +1099,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; return; } session.playerInfo = session.playerInfo.copyWithDeviceInfo(deviceInfo); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onDeviceInfoChanged(seq, deviceInfo)); } @@ -1090,7 +1117,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; return; } session.playerInfo = session.playerInfo.copyWithDeviceVolume(volume, muted); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onDeviceVolumeChanged(seq, volume, muted)); } @@ -1106,7 +1134,9 @@ import org.checkerframework.checker.initialization.qual.Initialized; if (player == null) { return; } - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ false); + boolean excludeTracks = !availableCommands.contains(COMMAND_GET_TRACKS); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ false, excludeTracks); session.dispatchRemoteControllerTaskWithoutReturn( (callback, seq) -> callback.onAvailableCommandsChangedFromPlayer(seq, availableCommands)); @@ -1128,7 +1158,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; return; } session.playerInfo = session.playerInfo.copyWithCurrentTracks(tracks); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ false); session.dispatchRemoteControllerTaskWithoutReturn( (callback, seq) -> callback.onTracksChanged(seq, tracks)); } @@ -1145,7 +1176,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; return; } session.playerInfo = session.playerInfo.copyWithTrackSelectionParameters(parameters); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskWithoutReturn( (callback, seq) -> callback.onTrackSelectionParametersChanged(seq, parameters)); } @@ -1162,7 +1194,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; return; } session.playerInfo = session.playerInfo.copyWithMediaMetadata(mediaMetadata); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); session.dispatchRemoteControllerTaskToLegacyStub( (callback, seq) -> callback.onMediaMetadataChanged(seq, mediaMetadata)); } @@ -1190,7 +1223,8 @@ import org.checkerframework.checker.initialization.qual.Initialized; } session.playerInfo = session.playerInfo.copyWithMaxSeekToPreviousPositionMs(maxSeekToPreviousPositionMs); - session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ true); + session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage( + /* excludeTimeline= */ true, /* excludeTracks= */ true); } @Nullable @@ -1224,10 +1258,12 @@ import org.checkerframework.checker.initialization.qual.Initialized; private static final int MSG_PLAYER_INFO_CHANGED = 1; private boolean excludeTimeline; + private boolean excludeTracks; public PlayerInfoChangedHandler(Looper looper) { super(looper); excludeTimeline = true; + excludeTracks = true; } @Override @@ -1237,15 +1273,17 @@ import org.checkerframework.checker.initialization.qual.Initialized; playerInfo.copyWithTimelineAndSessionPositionInfo( getPlayerWrapper().getCurrentTimeline(), getPlayerWrapper().createSessionPositionInfoForBundling()); - dispatchOnPlayerInfoChanged(playerInfo, excludeTimeline); + dispatchOnPlayerInfoChanged(playerInfo, excludeTimeline, excludeTracks); excludeTimeline = true; + excludeTracks = true; } else { throw new IllegalStateException("Invalid message what=" + msg.what); } } - public void sendPlayerInfoChangedMessage(boolean excludeTimeline) { + public void sendPlayerInfoChangedMessage(boolean excludeTimeline, boolean excludeTracks) { this.excludeTimeline = this.excludeTimeline && excludeTimeline; + this.excludeTracks = this.excludeTracks && excludeTracks; if (!onPlayerInfoChangedHandler.hasMessages(MSG_PLAYER_INFO_CHANGED)) { onPlayerInfoChangedHandler.sendEmptyMessage(MSG_PLAYER_INFO_CHANGED); } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java index 7160d1e176..b13b4d61fb 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java @@ -70,6 +70,7 @@ import androidx.media3.common.PlaybackParameters; import androidx.media3.common.Player; import androidx.media3.common.Rating; import androidx.media3.common.TrackSelectionParameters; +import androidx.media3.common.util.Assertions; import androidx.media3.common.util.BundleableUtil; import androidx.media3.common.util.Consumer; import androidx.media3.common.util.Log; @@ -1596,17 +1597,32 @@ import java.util.concurrent.ExecutionException; boolean excludeMediaItemsMetadata, boolean excludeCues, boolean excludeTimeline, - boolean excludeTracks) + boolean excludeTracks, + int controllerInterfaceVersion) throws RemoteException { - iController.onPlayerInfoChanged( - sequenceNumber, - playerInfo.toBundle( - excludeMediaItems, - excludeMediaItemsMetadata, - excludeCues, - excludeTimeline, - excludeTracks), - /* isTimelineExcluded= */ excludeTimeline); + Assertions.checkState(controllerInterfaceVersion != 0); + if (controllerInterfaceVersion >= 2) { + iController.onPlayerInfoChangedWithExclusions( + sequenceNumber, + playerInfo.toBundle( + excludeMediaItems, + excludeMediaItemsMetadata, + excludeCues, + excludeTimeline, + excludeTracks), + new PlayerInfo.BundlingExclusions(excludeTimeline, excludeTracks).toBundle()); + } else { + //noinspection deprecation + iController.onPlayerInfoChanged( + sequenceNumber, + playerInfo.toBundle( + excludeMediaItems, + excludeMediaItemsMetadata, + excludeCues, + excludeTimeline, + /* excludeTracks= */ true), + excludeTimeline); + } } @Override diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java index 64a25e0eb9..f58882351d 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java @@ -62,6 +62,7 @@ import android.support.v4.media.session.MediaSessionCompat.QueueItem; import android.support.v4.media.session.PlaybackStateCompat; import android.support.v4.media.session.PlaybackStateCompat.CustomAction; import android.text.TextUtils; +import android.util.Pair; import androidx.annotation.Nullable; import androidx.media.AudioAttributesCompat; import androidx.media.MediaBrowserServiceCompat.BrowserRoot; @@ -87,6 +88,7 @@ import androidx.media3.common.Timeline.Window; import androidx.media3.common.util.Log; import androidx.media3.common.util.Util; import androidx.media3.session.MediaLibraryService.LibraryParams; +import androidx.media3.session.PlayerInfo.BundlingExclusions; import com.google.common.collect.ImmutableList; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -1288,6 +1290,46 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return intersectCommandsBuilder.build(); } + /** + * Merges the excluded fields into the {@code newPlayerInfo} by taking the values of the {@code + * previousPlayerInfo} and taking into account the passed available commands. + * + * @param oldPlayerInfo The old {@link PlayerInfo}. + * @param oldBundlingExclusions The bundling exlusions in the old {@link PlayerInfo}. + * @param newPlayerInfo The new {@link PlayerInfo}. + * @param newBundlingExclusions The bundling exlusions in the new {@link PlayerInfo}. + * @param availablePlayerCommands The available commands to take into account when merging. + * @return A pair with the resulting {@link PlayerInfo} and {@link BundlingExclusions}. + */ + public static Pair mergePlayerInfo( + PlayerInfo oldPlayerInfo, + BundlingExclusions oldBundlingExclusions, + PlayerInfo newPlayerInfo, + BundlingExclusions newBundlingExclusions, + Commands availablePlayerCommands) { + PlayerInfo mergedPlayerInfo = newPlayerInfo; + BundlingExclusions mergedBundlingExclusions = newBundlingExclusions; + if (newBundlingExclusions.isTimelineExcluded + && availablePlayerCommands.contains(Player.COMMAND_GET_TIMELINE) + && !oldBundlingExclusions.isTimelineExcluded) { + // Use the previous timeline if it is excluded in the most recent update. + mergedPlayerInfo = mergedPlayerInfo.copyWithTimeline(oldPlayerInfo.timeline); + mergedBundlingExclusions = + new BundlingExclusions( + /* isTimelineExcluded= */ false, mergedBundlingExclusions.areCurrentTracksExcluded); + } + if (newBundlingExclusions.areCurrentTracksExcluded + && availablePlayerCommands.contains(Player.COMMAND_GET_TRACKS) + && !oldBundlingExclusions.areCurrentTracksExcluded) { + // Use the previous tracks if it is excluded in the most recent update. + mergedPlayerInfo = mergedPlayerInfo.copyWithCurrentTracks(oldPlayerInfo.currentTracks); + mergedBundlingExclusions = + new BundlingExclusions( + mergedBundlingExclusions.isTimelineExcluded, /* areCurrentTracksExcluded= */ false); + } + return new Pair<>(mergedPlayerInfo, mergedBundlingExclusions); + } + private static byte[] convertToByteArray(Bitmap bitmap) throws IOException { try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) { bitmap.compress(Bitmap.CompressFormat.PNG, /* ignored */ 0, stream); diff --git a/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java b/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java index 0ea0a266b4..a4a94969d9 100644 --- a/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java +++ b/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java @@ -66,6 +66,10 @@ import java.lang.annotation.Target; */ public static class BundlingExclusions implements Bundleable { + /** Bundling exclusions with no exclusions. */ + public static final BundlingExclusions NONE = + new BundlingExclusions( + /* isTimelineExcluded= */ false, /* areCurrentTracksExcluded= */ false); /** Whether the {@linkplain PlayerInfo#timeline timeline} is excluded. */ public final boolean isTimelineExcluded; /** Whether the {@linkplain PlayerInfo#currentTracks current tracks} are excluded. */ diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java index 8cb138e0f9..13f7d64d4e 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java @@ -1052,8 +1052,8 @@ public class MediaControllerListenerTest { MediaController controller = controllerTestRule.createController(remoteSession.getToken()); AtomicReference changedCurrentTracksFromParamRef = new AtomicReference<>(); AtomicReference changedCurrentTracksFromGetterRef = new AtomicReference<>(); - AtomicReference changedCurrentTracksFromOnEventsRef = new AtomicReference<>(); - AtomicReference eventsRef = new AtomicReference<>(); + List changedCurrentTracksFromOnEvents = new ArrayList<>(); + List capturedEvents = new ArrayList<>(); CountDownLatch latch = new CountDownLatch(2); Player.Listener listener = new Player.Listener() { @@ -1061,13 +1061,12 @@ public class MediaControllerListenerTest { public void onTracksChanged(Tracks currentTracks) { changedCurrentTracksFromParamRef.set(currentTracks); changedCurrentTracksFromGetterRef.set(controller.getCurrentTracks()); - latch.countDown(); } @Override public void onEvents(Player player, Player.Events events) { - eventsRef.set(events); - changedCurrentTracksFromOnEventsRef.set(player.getCurrentTracks()); + capturedEvents.add(events); + changedCurrentTracksFromOnEvents.add(player.getCurrentTracks()); latch.countDown(); } }; @@ -1081,13 +1080,22 @@ public class MediaControllerListenerTest { }); player.notifyTracksChanged(currentTracks); + player.notifyIsLoadingChanged(true); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(initialCurrentTracksRef.get()).isEqualTo(Tracks.EMPTY); assertThat(changedCurrentTracksFromParamRef.get()).isEqualTo(currentTracks); assertThat(changedCurrentTracksFromGetterRef.get()).isEqualTo(currentTracks); - assertThat(changedCurrentTracksFromOnEventsRef.get()).isEqualTo(currentTracks); - assertThat(getEventsAsList(eventsRef.get())).containsExactly(Player.EVENT_TRACKS_CHANGED); + assertThat(capturedEvents).hasSize(2); + assertThat(getEventsAsList(capturedEvents.get(0))).containsExactly(Player.EVENT_TRACKS_CHANGED); + assertThat(getEventsAsList(capturedEvents.get(1))) + .containsExactly(Player.EVENT_IS_LOADING_CHANGED); + assertThat(changedCurrentTracksFromOnEvents).hasSize(2); + assertThat(changedCurrentTracksFromOnEvents.get(0)).isEqualTo(currentTracks); + assertThat(changedCurrentTracksFromOnEvents.get(1)).isEqualTo(currentTracks); + // Assert that an equal instance is not re-sent over the binder. + assertThat(changedCurrentTracksFromOnEvents.get(0)) + .isSameInstanceAs(changedCurrentTracksFromOnEvents.get(1)); } @Test @@ -1142,6 +1150,9 @@ public class MediaControllerListenerTest { assertThat(capturedCurrentTracks).containsExactly(Tracks.EMPTY); assertThat(initialCurrentTracksWithCommandAvailable.get().getGroups()).hasSize(1); assertThat(capturedCurrentTracksWithCommandAvailable.get().getGroups()).hasSize(1); + // Assert that an equal instance is not re-sent over the binder. + assertThat(initialCurrentTracksWithCommandAvailable.get()) + .isSameInstanceAs(capturedCurrentTracksWithCommandAvailable.get()); } @Test @@ -1181,6 +1192,7 @@ public class MediaControllerListenerTest { availableCommands.get().buildUpon().remove(Player.COMMAND_GET_TRACKS).build()); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(capturedCurrentTracks).hasSize(2); assertThat(capturedCurrentTracks.get(0).getGroups()).hasSize(1); assertThat(capturedCurrentTracks.get(1)).isEqualTo(Tracks.EMPTY); } @@ -2203,7 +2215,7 @@ public class MediaControllerListenerTest { } @Test - public void onTimelineChanged_emptyMediaItemAndMediaMetadata_whenCommandUnavailableFromPlayer() + public void onTimelineChanged_playerCommandUnavailable_emptyTimelineMediaItemAndMetadata() throws Exception { int testMediaItemsSize = 2; List testMediaItemList = MediaTestUtils.createMediaItems(testMediaItemsSize); @@ -2217,7 +2229,7 @@ public class MediaControllerListenerTest { AtomicReference timelineFromGetterRef = new AtomicReference<>(); List onEventsTimelines = new ArrayList<>(); AtomicReference metadataFromGetterRef = new AtomicReference<>(); - AtomicReference currentMediaItemGetterRef = new AtomicReference<>(); + AtomicReference isCurrentMediaItemNullRef = new AtomicReference<>(); List eventsList = new ArrayList<>(); Player.Listener listener = new Player.Listener() { @@ -2226,7 +2238,7 @@ public class MediaControllerListenerTest { timelineFromParamRef.set(timeline); timelineFromGetterRef.set(controller.getCurrentTimeline()); metadataFromGetterRef.set(controller.getMediaMetadata()); - currentMediaItemGetterRef.set(controller.getCurrentMediaItem()); + isCurrentMediaItemNullRef.set(controller.getCurrentMediaItem() == null); latch.countDown(); } @@ -2244,24 +2256,7 @@ public class MediaControllerListenerTest { remoteSession.getMockPlayer().notifyAvailableCommandsChanged(commandsWithoutGetTimeline); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(timelineFromParamRef.get().getWindowCount()).isEqualTo(testMediaItemsSize); - for (int i = 0; i < timelineFromParamRef.get().getWindowCount(); i++) { - assertThat( - timelineFromParamRef - .get() - .getWindow(/* windowIndex= */ i, new Timeline.Window()) - .mediaItem) - .isEqualTo(MediaItem.EMPTY); - } - assertThat(timelineFromGetterRef.get().getWindowCount()).isEqualTo(testMediaItemsSize); - for (int i = 0; i < timelineFromGetterRef.get().getWindowCount(); i++) { - assertThat( - timelineFromGetterRef - .get() - .getWindow(/* windowIndex= */ i, new Timeline.Window()) - .mediaItem) - .isEqualTo(MediaItem.EMPTY); - } + assertThat(timelineFromParamRef.get()).isEqualTo(Timeline.EMPTY); assertThat(onEventsTimelines).hasSize(2); for (int i = 0; i < onEventsTimelines.get(1).getWindowCount(); i++) { assertThat( @@ -2272,15 +2267,16 @@ public class MediaControllerListenerTest { .isEqualTo(MediaItem.EMPTY); } assertThat(metadataFromGetterRef.get()).isEqualTo(MediaMetadata.EMPTY); - assertThat(currentMediaItemGetterRef.get()).isEqualTo(MediaItem.EMPTY); + assertThat(isCurrentMediaItemNullRef.get()).isTrue(); assertThat(eventsList).hasSize(2); assertThat(getEventsAsList(eventsList.get(0))) .containsExactly(Player.EVENT_AVAILABLE_COMMANDS_CHANGED); - assertThat(getEventsAsList(eventsList.get(1))).contains(Player.EVENT_TIMELINE_CHANGED); + assertThat(getEventsAsList(eventsList.get(1))) + .containsExactly(Player.EVENT_TIMELINE_CHANGED, Player.EVENT_MEDIA_ITEM_TRANSITION); } @Test - public void onTimelineChanged_emptyMediaItemAndMediaMetadata_whenCommandUnavailableFromSession() + public void onTimelineChanged_sessionCommandUnavailable_emptyTimelineMediaItemAndMetadata() throws Exception { int testMediaItemsSize = 2; List testMediaItemList = MediaTestUtils.createMediaItems(testMediaItemsSize); @@ -2293,7 +2289,7 @@ public class MediaControllerListenerTest { AtomicReference timelineFromParamRef = new AtomicReference<>(); AtomicReference timelineFromGetterRef = new AtomicReference<>(); AtomicReference metadataFromGetterRef = new AtomicReference<>(); - AtomicReference currentMediaItemGetterRef = new AtomicReference<>(); + AtomicReference isCurrentMediaItemNullRef = new AtomicReference<>(); List eventsList = new ArrayList<>(); Player.Listener listener = new Player.Listener() { @@ -2302,7 +2298,7 @@ public class MediaControllerListenerTest { timelineFromParamRef.set(timeline); timelineFromGetterRef.set(controller.getCurrentTimeline()); metadataFromGetterRef.set(controller.getMediaMetadata()); - currentMediaItemGetterRef.set(controller.getCurrentMediaItem()); + isCurrentMediaItemNullRef.set(controller.getCurrentMediaItem() == null); latch.countDown(); } @@ -2319,30 +2315,14 @@ public class MediaControllerListenerTest { remoteSession.setAvailableCommands(SessionCommands.EMPTY, commandsWithoutGetTimeline); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(timelineFromParamRef.get().getWindowCount()).isEqualTo(testMediaItemsSize); - for (int i = 0; i < timelineFromParamRef.get().getWindowCount(); i++) { - assertThat( - timelineFromParamRef - .get() - .getWindow(/* windowIndex= */ i, new Timeline.Window()) - .mediaItem) - .isEqualTo(MediaItem.EMPTY); - } - assertThat(timelineFromGetterRef.get().getWindowCount()).isEqualTo(testMediaItemsSize); - for (int i = 0; i < timelineFromGetterRef.get().getWindowCount(); i++) { - assertThat( - timelineFromGetterRef - .get() - .getWindow(/* windowIndex= */ i, new Timeline.Window()) - .mediaItem) - .isEqualTo(MediaItem.EMPTY); - } + assertThat(timelineFromParamRef.get()).isEqualTo(Timeline.EMPTY); assertThat(metadataFromGetterRef.get()).isEqualTo(MediaMetadata.EMPTY); - assertThat(currentMediaItemGetterRef.get()).isEqualTo(MediaItem.EMPTY); + assertThat(isCurrentMediaItemNullRef.get()).isTrue(); assertThat(eventsList).hasSize(2); assertThat(getEventsAsList(eventsList.get(0))) .containsExactly(Player.EVENT_AVAILABLE_COMMANDS_CHANGED); - assertThat(getEventsAsList(eventsList.get(1))).contains(Player.EVENT_TIMELINE_CHANGED); + assertThat(getEventsAsList(eventsList.get(1))) + .containsExactly(Player.EVENT_TIMELINE_CHANGED, Player.EVENT_MEDIA_ITEM_TRANSITION); } /** This also tests {@link MediaController#getAvailableCommands()}. */ diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java index 80dd8073b4..83c5a4e3f8 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaUtilsTest.java @@ -20,6 +20,9 @@ import static android.support.v4.media.MediaBrowserCompat.MediaItem.FLAG_PLAYABL import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_DURATION; import static android.support.v4.media.session.MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS; import static androidx.media.utils.MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS; +import static androidx.media3.common.MimeTypes.AUDIO_AAC; +import static androidx.media3.common.MimeTypes.VIDEO_H264; +import static androidx.media3.common.MimeTypes.VIDEO_H265; import static androidx.media3.session.MediaConstants.EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY; import static com.google.common.truth.Truth.assertThat; import static java.util.concurrent.TimeUnit.SECONDS; @@ -36,11 +39,13 @@ import android.support.v4.media.RatingCompat; import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; +import android.util.Pair; import androidx.annotation.Nullable; import androidx.media.AudioAttributesCompat; import androidx.media.utils.MediaConstants; import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; +import androidx.media3.common.Format; import androidx.media3.common.HeartRating; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaMetadata; @@ -49,10 +54,15 @@ import androidx.media3.common.Player; import androidx.media3.common.Rating; import androidx.media3.common.StarRating; import androidx.media3.common.ThumbRating; +import androidx.media3.common.Timeline; +import androidx.media3.common.TrackGroup; +import androidx.media3.common.Tracks; +import androidx.media3.session.PlayerInfo.BundlingExclusions; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SdkSuppress; import androidx.test.filters.SmallTest; +import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ListenableFuture; import java.util.ArrayList; import java.util.Collections; @@ -623,4 +633,134 @@ public final class MediaUtilsTest { state, /* metadataCompat= */ null, /* timeDiffMs= */ C.INDEX_UNSET); assertThat(totalBufferedDurationMs).isEqualTo(testTotalBufferedDurationMs); } + + @Test + public void mergePlayerInfo_timelineAndTracksExcluded_correctMerge() { + Timeline timeline = + new Timeline.RemotableTimeline( + ImmutableList.of(new Timeline.Window()), + ImmutableList.of(new Timeline.Period()), + /* shuffledWindowIndices= */ new int[] {0}); + Tracks tracks = + new Tracks( + ImmutableList.of( + new Tracks.Group( + new TrackGroup(new Format.Builder().setSampleMimeType(AUDIO_AAC).build()), + /* adaptiveSupported= */ false, + new int[] {C.FORMAT_EXCEEDS_CAPABILITIES}, + /* trackSelected= */ new boolean[] {true}), + new Tracks.Group( + new TrackGroup( + new Format.Builder().setSampleMimeType(VIDEO_H264).build(), + new Format.Builder().setSampleMimeType(VIDEO_H265).build()), + /* adaptiveSupported= */ true, + new int[] {C.FORMAT_HANDLED, C.FORMAT_UNSUPPORTED_TYPE}, + /* trackSelected= */ new boolean[] {false, true}))); + PlayerInfo oldPlayerInfo = + PlayerInfo.DEFAULT.copyWithCurrentTracks(tracks).copyWithTimeline(timeline); + PlayerInfo newPlayerInfo = PlayerInfo.DEFAULT; + Player.Commands availableCommands = + Player.Commands.EMPTY + .buildUpon() + .add(Player.COMMAND_GET_TIMELINE) + .add(Player.COMMAND_GET_TRACKS) + .build(); + + Pair mergeResult = + MediaUtils.mergePlayerInfo( + oldPlayerInfo, + BundlingExclusions.NONE, + newPlayerInfo, + new BundlingExclusions(/* isTimelineExcluded= */ true, /* areTracksExcluded= */ true), + availableCommands); + + assertThat(mergeResult.first.timeline).isSameInstanceAs(oldPlayerInfo.timeline); + assertThat(mergeResult.first.currentTracks).isSameInstanceAs(oldPlayerInfo.currentTracks); + assertThat(mergeResult.second.isTimelineExcluded).isFalse(); + assertThat(mergeResult.second.areCurrentTracksExcluded).isFalse(); + } + + @Test + public void mergePlayerInfo_getTimelineCommandNotAvailable_emptyTimeline() { + Timeline timeline = + new Timeline.RemotableTimeline( + ImmutableList.of(new Timeline.Window()), + ImmutableList.of(new Timeline.Period()), + /* shuffledWindowIndices= */ new int[] {0}); + Tracks tracks = + new Tracks( + ImmutableList.of( + new Tracks.Group( + new TrackGroup(new Format.Builder().setSampleMimeType(AUDIO_AAC).build()), + /* adaptiveSupported= */ false, + new int[] {C.FORMAT_EXCEEDS_CAPABILITIES}, + /* trackSelected= */ new boolean[] {true}), + new Tracks.Group( + new TrackGroup( + new Format.Builder().setSampleMimeType(VIDEO_H264).build(), + new Format.Builder().setSampleMimeType(VIDEO_H265).build()), + /* adaptiveSupported= */ true, + new int[] {C.FORMAT_HANDLED, C.FORMAT_UNSUPPORTED_TYPE}, + /* trackSelected= */ new boolean[] {false, true}))); + PlayerInfo oldPlayerInfo = + PlayerInfo.DEFAULT.copyWithCurrentTracks(tracks).copyWithTimeline(timeline); + PlayerInfo newPlayerInfo = PlayerInfo.DEFAULT; + Player.Commands availableCommands = + Player.Commands.EMPTY.buildUpon().add(Player.COMMAND_GET_TRACKS).build(); + + Pair mergeResult = + MediaUtils.mergePlayerInfo( + oldPlayerInfo, + BundlingExclusions.NONE, + newPlayerInfo, + new BundlingExclusions(/* isTimelineExcluded= */ true, /* areTracksExcluded= */ true), + availableCommands); + + assertThat(mergeResult.first.timeline).isSameInstanceAs(Timeline.EMPTY); + assertThat(mergeResult.first.currentTracks).isSameInstanceAs(oldPlayerInfo.currentTracks); + assertThat(mergeResult.second.isTimelineExcluded).isTrue(); + assertThat(mergeResult.second.areCurrentTracksExcluded).isFalse(); + } + + @Test + public void mergePlayerInfo_getTracksCommandNotAvailable_emptyTracks() { + Timeline timeline = + new Timeline.RemotableTimeline( + ImmutableList.of(new Timeline.Window()), + ImmutableList.of(new Timeline.Period()), + /* shuffledWindowIndices= */ new int[] {0}); + Tracks tracks = + new Tracks( + ImmutableList.of( + new Tracks.Group( + new TrackGroup(new Format.Builder().setSampleMimeType(AUDIO_AAC).build()), + /* adaptiveSupported= */ false, + new int[] {C.FORMAT_EXCEEDS_CAPABILITIES}, + /* trackSelected= */ new boolean[] {true}), + new Tracks.Group( + new TrackGroup( + new Format.Builder().setSampleMimeType(VIDEO_H264).build(), + new Format.Builder().setSampleMimeType(VIDEO_H265).build()), + /* adaptiveSupported= */ true, + new int[] {C.FORMAT_HANDLED, C.FORMAT_UNSUPPORTED_TYPE}, + /* trackSelected= */ new boolean[] {false, true}))); + PlayerInfo oldPlayerInfo = + PlayerInfo.DEFAULT.copyWithCurrentTracks(tracks).copyWithTimeline(timeline); + PlayerInfo newPlayerInfo = PlayerInfo.DEFAULT; + Player.Commands availableCommands = + Player.Commands.EMPTY.buildUpon().add(Player.COMMAND_GET_TIMELINE).build(); + + Pair mergeResult = + MediaUtils.mergePlayerInfo( + oldPlayerInfo, + BundlingExclusions.NONE, + newPlayerInfo, + new BundlingExclusions(/* isTimelineExcluded= */ true, /* areTracksExcluded= */ true), + availableCommands); + + assertThat(mergeResult.first.timeline).isSameInstanceAs(oldPlayerInfo.timeline); + assertThat(mergeResult.first.currentTracks).isSameInstanceAs(Tracks.EMPTY); + assertThat(mergeResult.second.isTimelineExcluded).isFalse(); + assertThat(mergeResult.second.areCurrentTracksExcluded).isTrue(); + } }