From 627b7a3e56e3c8242a1f702509c7484373f9cdce Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 17 Oct 2024 09:51:58 -0700 Subject: [PATCH] Add media button preferences This adds the API surface for media button preferences in MediaSession and MediaController. It closely mimics the existing custom layout infrastructure (which it will replace eventually). Compat logic: - Session: - When converting to platform custom actions, prefer to use media button preferences if both are set. - When connecting to an older Media3 controller, send the media button preferences as custom layout instead. - Controller: - Maintain a single resolved media button preferences field. - For Media3 controller receiving both values, prefer media button preferences over custom layouts. Missing functionality: - The conversion from/to custom layout and platform custom actions does not take the slot preferences into account yet. PiperOrigin-RevId: 686950100 --- .../media3/session/IMediaController.aidl | 3 +- .../media3/session/ConnectionState.java | 38 +- .../DefaultMediaNotificationProvider.java | 34 +- .../media3/session/LegacyConversions.java | 16 +- .../media3/session/MediaController.java | 43 +- .../session/MediaControllerImplBase.java | 138 +++++-- .../session/MediaControllerImplLegacy.java | 83 ++-- .../media3/session/MediaControllerStub.java | 27 +- .../media3/session/MediaLibraryService.java | 40 ++ .../session/MediaLibrarySessionImpl.java | 2 + .../media3/session/MediaNotification.java | 7 +- .../session/MediaNotificationManager.java | 9 +- .../androidx/media3/session/MediaSession.java | 226 +++++++++-- .../media3/session/MediaSessionImpl.java | 51 ++- .../session/MediaSessionLegacyStub.java | 5 + .../media3/session/MediaSessionService.java | 3 +- .../media3/session/MediaSessionStub.java | 30 +- .../media3/session/PlayerWrapper.java | 18 +- .../media3/session/ConnectionStateTest.java | 140 +++++++ .../media3/session/LegacyConversionsTest.java | 12 +- .../session/MediaSessionServiceTest.java | 108 ++++- .../media3/session/PlayerWrapperTest.java | 3 +- .../common/IRemoteMediaController.aidl | 1 + .../session/common/IRemoteMediaSession.aidl | 1 + ...tateCompatActionsWithMediaSessionTest.java | 220 +++++++++- ...lerListenerWithMediaSessionCompatTest.java | 68 ++++ .../media3/session/MediaControllerTest.java | 383 ++++++++++++++++++ .../session/MediaSessionCallbackTest.java | 64 ++- .../session/MediaSessionServiceTest.java | 119 +++++- .../MediaControllerProviderService.java | 14 + .../session/MediaSessionProviderService.java | 18 + .../media3/session/RemoteMediaController.java | 11 + .../media3/session/RemoteMediaSession.java | 9 + .../session/TestMediaBrowserListener.java | 6 + 34 files changed, 1769 insertions(+), 181 deletions(-) create mode 100644 libraries/session/src/test/java/androidx/media3/session/ConnectionStateTest.java 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 21914d90c7..f5895f1c85 100644 --- a/libraries/session/src/main/aidl/androidx/media3/session/IMediaController.aidl +++ b/libraries/session/src/main/aidl/androidx/media3/session/IMediaController.aidl @@ -49,7 +49,8 @@ oneway interface IMediaController { void onExtrasChanged(int seq, in Bundle extras) = 3011; void onSessionActivityChanged(int seq, in PendingIntent pendingIntent) = 3013; void onError(int seq, in Bundle sessionError) = 3014; - // Next Id for MediaController: 3015 + void onSetMediaButtonPreferences(int seq, in List commandButtonList) = 3015; + // Next Id for MediaController: 3016 void onChildrenChanged( int seq, String parentId, int itemCount, in @nullable Bundle libraryParams) = 4000; diff --git a/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java b/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java index 5c2eb43ad7..cd0bf10fd8 100644 --- a/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java +++ b/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java @@ -58,6 +58,8 @@ import java.util.List; public final ImmutableList customLayout; + public final ImmutableList mediaButtonPreferences; + @Nullable public final Token platformToken; public final ImmutableList commandButtonsForMediaItems; @@ -68,6 +70,7 @@ import java.util.List; IMediaSession sessionBinder, @Nullable PendingIntent sessionActivity, ImmutableList customLayout, + ImmutableList mediaButtonPreferences, ImmutableList commandButtonsForMediaItems, SessionCommands sessionCommands, Player.Commands playerCommandsFromSession, @@ -81,6 +84,7 @@ import java.util.List; this.sessionBinder = sessionBinder; this.sessionActivity = sessionActivity; this.customLayout = customLayout; + this.mediaButtonPreferences = mediaButtonPreferences; this.commandButtonsForMediaItems = commandButtonsForMediaItems; this.sessionCommands = sessionCommands; this.playerCommandsFromSession = playerCommandsFromSession; @@ -95,6 +99,7 @@ import java.util.List; private static final String FIELD_SESSION_BINDER = Util.intToStringMaxRadix(1); private static final String FIELD_SESSION_ACTIVITY = Util.intToStringMaxRadix(2); private static final String FIELD_CUSTOM_LAYOUT = Util.intToStringMaxRadix(9); + private static final String FIELD_MEDIA_BUTTON_PREFERENCES = Util.intToStringMaxRadix(14); private static final String FIELD_COMMAND_BUTTONS_FOR_MEDIA_ITEMS = Util.intToStringMaxRadix(13); private static final String FIELD_SESSION_COMMANDS = Util.intToStringMaxRadix(3); private static final String FIELD_PLAYER_COMMANDS_FROM_SESSION = Util.intToStringMaxRadix(4); @@ -106,7 +111,7 @@ import java.util.List; private static final String FIELD_IN_PROCESS_BINDER = Util.intToStringMaxRadix(10); private static final String FIELD_PLATFORM_TOKEN = Util.intToStringMaxRadix(12); - // Next field key = 14 + // Next field key = 15 public Bundle toBundleForRemoteProcess(int controllerInterfaceVersion) { Bundle bundle = new Bundle(); @@ -118,6 +123,21 @@ import java.util.List; FIELD_CUSTOM_LAYOUT, BundleCollectionUtil.toBundleArrayList(customLayout, CommandButton::toBundle)); } + if (!mediaButtonPreferences.isEmpty()) { + if (controllerInterfaceVersion >= 7) { + bundle.putParcelableArrayList( + FIELD_MEDIA_BUTTON_PREFERENCES, + BundleCollectionUtil.toBundleArrayList( + mediaButtonPreferences, CommandButton::toBundle)); + } else { + // Controller doesn't support media button preferences, send the list as a custom layout. + // TODO: b/332877990 - More accurately reflect media button preferences as custom layout. + bundle.putParcelableArrayList( + FIELD_CUSTOM_LAYOUT, + BundleCollectionUtil.toBundleArrayList( + mediaButtonPreferences, CommandButton::toBundle)); + } + } if (!commandButtonsForMediaItems.isEmpty()) { bundle.putParcelableArrayList( FIELD_COMMAND_BUTTONS_FOR_MEDIA_ITEMS, @@ -166,11 +186,20 @@ import java.util.List; IBinder sessionBinder = checkNotNull(BundleCompat.getBinder(bundle, FIELD_SESSION_BINDER)); @Nullable PendingIntent sessionActivity = bundle.getParcelable(FIELD_SESSION_ACTIVITY); @Nullable - List commandButtonArrayList = bundle.getParcelableArrayList(FIELD_CUSTOM_LAYOUT); + List customLayoutArrayList = bundle.getParcelableArrayList(FIELD_CUSTOM_LAYOUT); ImmutableList customLayout = - commandButtonArrayList != null + customLayoutArrayList != null ? BundleCollectionUtil.fromBundleList( - b -> CommandButton.fromBundle(b, sessionInterfaceVersion), commandButtonArrayList) + b -> CommandButton.fromBundle(b, sessionInterfaceVersion), customLayoutArrayList) + : ImmutableList.of(); + @Nullable + List mediaButtonPreferencesArrayList = + bundle.getParcelableArrayList(FIELD_MEDIA_BUTTON_PREFERENCES); + ImmutableList mediaButtonPreferences = + mediaButtonPreferencesArrayList != null + ? BundleCollectionUtil.fromBundleList( + b -> CommandButton.fromBundle(b, sessionInterfaceVersion), + mediaButtonPreferencesArrayList) : ImmutableList.of(); @Nullable List commandButtonsForMediaItemsArrayList = @@ -212,6 +241,7 @@ import java.util.List; IMediaSession.Stub.asInterface(sessionBinder), sessionActivity, customLayout, + mediaButtonPreferences, commandButtonsForMediaItems, sessionCommands, playerCommandsFromSession, diff --git a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java index da535383bd..010440a67f 100644 --- a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java +++ b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java @@ -51,7 +51,6 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.util.Arrays; -import java.util.List; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -299,19 +298,20 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi @Override public final MediaNotification createNotification( MediaSession mediaSession, - ImmutableList customLayout, + ImmutableList mediaButtonPreferences, MediaNotification.ActionFactory actionFactory, Callback onNotificationChangedCallback) { ensureNotificationChannel(); - ImmutableList.Builder customLayoutWithEnabledCommandButtonsOnly = + // TODO: b/332877990 - More accurately reflect media button preferences in the notification. + ImmutableList.Builder mediaButtonPreferencesWithEnabledCommandButtonsOnly = new ImmutableList.Builder<>(); - for (int i = 0; i < customLayout.size(); i++) { - CommandButton button = customLayout.get(i); + for (int i = 0; i < mediaButtonPreferences.size(); i++) { + CommandButton button = mediaButtonPreferences.get(i); if (button.sessionCommand != null && button.sessionCommand.commandCode == SessionCommand.COMMAND_CODE_CUSTOM && button.isEnabled) { - customLayoutWithEnabledCommandButtonsOnly.add(customLayout.get(i)); + mediaButtonPreferencesWithEnabledCommandButtonsOnly.add(mediaButtonPreferences.get(i)); } } Player player = mediaSession.getPlayer(); @@ -325,7 +325,7 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi getMediaButtons( mediaSession, player.getAvailableCommands(), - customLayoutWithEnabledCommandButtonsOnly.build(), + mediaButtonPreferencesWithEnabledCommandButtonsOnly.build(), !Util.shouldShowPlayButton( player, mediaSession.getShowPlayButtonIfPlaybackIsSuppressed())), builder, @@ -424,18 +424,18 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi * be customized by defining the index of the command in compact view of up to 3 commands in their * extras with key {@link DefaultMediaNotificationProvider#COMMAND_KEY_COMPACT_VIEW_INDEX}. * - *

To make the custom layout and commands work, you need to {@linkplain - * MediaSession#setCustomLayout(List) set the custom layout of commands} and add the custom + *

To make the media button preferences and custom commands work, you need to {@linkplain + * MediaSession#setMediaButtonPreferences set the media button preferences} and add the custom * commands to the available commands when a controller {@linkplain * MediaSession.Callback#onConnect(MediaSession, MediaSession.ControllerInfo) connects to the - * session}. Controllers that connect after you called {@link MediaSession#setCustomLayout(List)} - * need the custom command set in {@link MediaSession.Callback#onPostConnect(MediaSession, - * MediaSession.ControllerInfo)} also. + * session}. Controllers that connect after you called {@link + * MediaSession#setMediaButtonPreferences} need the custom command set in {@link + * MediaSession.Callback#onPostConnect(MediaSession, MediaSession.ControllerInfo)} too. * * @param session The media session. * @param playerCommands The available player commands. - * @param customLayout The {@linkplain MediaSession#setCustomLayout(List) custom layout of - * commands}. + * @param mediaButtonPreferences The {@linkplain MediaSession#setMediaButtonPreferences media + * button preferences}. * @param showPauseButton Whether the notification should show a pause button (e.g., because the * player is currently playing content), otherwise show a play button to start playback. * @return The ordered list of command buttons to be placed on the notification. @@ -443,7 +443,7 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi protected ImmutableList getMediaButtons( MediaSession session, Player.Commands playerCommands, - ImmutableList customLayout, + ImmutableList mediaButtonPreferences, boolean showPauseButton) { // Skip to previous action. ImmutableList.Builder commandButtons = new ImmutableList.Builder<>(); @@ -488,8 +488,8 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi .setDisplayName(context.getString(R.string.media3_controls_seek_to_next_description)) .build()); } - for (int i = 0; i < customLayout.size(); i++) { - CommandButton button = customLayout.get(i); + for (int i = 0; i < mediaButtonPreferences.size(); i++) { + CommandButton button = mediaButtonPreferences.get(i); if (button.sessionCommand != null && button.sessionCommand.commandCode == SessionCommand.COMMAND_CODE_CUSTOM) { commandButtons.add(button); diff --git a/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java b/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java index 6107180e24..7602c190e1 100644 --- a/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java +++ b/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java @@ -1495,13 +1495,12 @@ import java.util.concurrent.TimeoutException; } /** - * Converts {@link CustomAction} in the {@link PlaybackStateCompat} to the custom layout which is - * the list of the {@link CommandButton}. + * Converts {@link CustomAction} in the {@link PlaybackStateCompat} to media button preferences. * - * @param state playback state - * @return custom layout. Always non-null. + * @param state The {@link PlaybackStateCompat}. + * @return The media button preferences. */ - public static ImmutableList convertToCustomLayout( + public static ImmutableList convertToMediaButtonPreferences( @Nullable PlaybackStateCompat state) { if (state == null) { return ImmutableList.of(); @@ -1510,7 +1509,7 @@ import java.util.concurrent.TimeoutException; if (customActions == null) { return ImmutableList.of(); } - ImmutableList.Builder layout = new ImmutableList.Builder<>(); + ImmutableList.Builder mediaButtonPreferences = new ImmutableList.Builder<>(); for (CustomAction customAction : customActions) { String action = customAction.getAction(); @Nullable Bundle extras = customAction.getExtras(); @@ -1521,15 +1520,16 @@ import java.util.concurrent.TimeoutException; MediaConstants.EXTRAS_KEY_COMMAND_BUTTON_ICON_COMPAT, /* defaultValue= */ CommandButton.ICON_UNDEFINED) : CommandButton.ICON_UNDEFINED; + // TODO: b/332877990 - Set appropriate slots based on available player commands. CommandButton button = new CommandButton.Builder(icon, customAction.getIcon()) .setSessionCommand(new SessionCommand(action, extras == null ? Bundle.EMPTY : extras)) .setDisplayName(customAction.getName()) .setEnabled(true) .build(); - layout.add(button); + mediaButtonPreferences.add(button); } - return layout.build(); + return mediaButtonPreferences.build(); } /** Converts {@link AudioAttributesCompat} into {@link AudioAttributes}. */ diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaController.java b/libraries/session/src/main/java/androidx/media3/session/MediaController.java index 8b56611944..cf250de172 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaController.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaController.java @@ -409,6 +409,8 @@ public class MediaController implements Player { /** * Called when the {@linkplain #getCustomLayout() custom layout} changed. * + *

This method will be deprecated, prefer to use {@link #onMediaButtonPreferencesChanged}. + * *

The custom layout can change when either the session {@linkplain * MediaSession#setCustomLayout changes the custom layout}, or when the session {@linkplain * MediaSession#setAvailableCommands(MediaSession.ControllerInfo, SessionCommands, Commands) @@ -424,6 +426,25 @@ public class MediaController implements Player { @UnstableApi default void onCustomLayoutChanged(MediaController controller, List layout) {} + /** + * Called when the {@linkplain #getMediaButtonPreferences() media button preferences} changed. + * + *

The media button preferences can change when either the session {@linkplain + * MediaSession#setMediaButtonPreferences changes the media button preferences}, or when the + * session {@linkplain MediaSession#setAvailableCommands(MediaSession.ControllerInfo, + * SessionCommands, Commands) changes the available commands} for a controller that affect + * whether buttons of the media button preferences are enabled or disabled. + * + *

Note that the {@linkplain CommandButton#isEnabled enabled} flag is set to {@code false} if + * the available commands do not allow to use a button. + * + * @param controller The controller. + * @param mediaButtonPreferences The ordered list of {@linkplain CommandButton command buttons}. + */ + @UnstableApi + default void onMediaButtonPreferencesChanged( + MediaController controller, List mediaButtonPreferences) {} + /** * Called when the available session commands are changed by session. * @@ -1094,6 +1115,8 @@ public class MediaController implements Player { /** * Returns the custom layout. * + *

This method will be deprecated, prefer to use {@link #getMediaButtonPreferences()} instead. + * *

After being connected, a change of the custom layout is reported with {@link * Listener#onCustomLayoutChanged(MediaController, List)}. * @@ -1104,8 +1127,24 @@ public class MediaController implements Player { */ @UnstableApi public final ImmutableList getCustomLayout() { + return getMediaButtonPreferences(); + } + + /** + * Returns the media button preferences. + * + *

After being connected, a change of the media button preferences is reported with {@link + * Listener#onMediaButtonPreferencesChanged(MediaController, List)}. + * + *

Note that the {@linkplain CommandButton#isEnabled enabled} flag is set to {@code false} if + * the available commands do not allow to use a button. + * + * @return The media button preferences. + */ + @UnstableApi + public final ImmutableList getMediaButtonPreferences() { verifyApplicationThread(); - return isConnected() ? impl.getCustomLayout() : ImmutableList.of(); + return isConnected() ? impl.getMediaButtonPreferences() : ImmutableList.of(); } /** @@ -2168,7 +2207,7 @@ public class MediaController implements Player { ListenableFuture sendCustomCommand(SessionCommand command, Bundle args); - ImmutableList getCustomLayout(); + ImmutableList getMediaButtonPreferences(); ImmutableList getCommandButtonsForMediaItem(MediaItem mediaItem); 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 dc75a339ea..8152b2ed53 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java @@ -126,7 +126,8 @@ import org.checkerframework.checker.nullness.qual.NonNull; private PlayerInfo playerInfo; @Nullable private PendingIntent sessionActivity; private ImmutableList customLayoutOriginal; - private ImmutableList customLayoutWithUnavailableButtonsDisabled; + private ImmutableList mediaButtonPreferencesOriginal; + private ImmutableList resolvedMediaButtonPreferences; private ImmutableMap commandButtonsForMediaItemsMap; private SessionCommands sessionCommands; private Commands playerCommandsFromSession; @@ -155,7 +156,8 @@ import org.checkerframework.checker.nullness.qual.NonNull; surfaceSize = Size.UNKNOWN; sessionCommands = SessionCommands.EMPTY; customLayoutOriginal = ImmutableList.of(); - customLayoutWithUnavailableButtonsDisabled = ImmutableList.of(); + mediaButtonPreferencesOriginal = ImmutableList.of(); + resolvedMediaButtonPreferences = ImmutableList.of(); commandButtonsForMediaItemsMap = ImmutableMap.of(); playerCommandsFromSession = Commands.EMPTY; playerCommandsFromPlayer = Commands.EMPTY; @@ -745,8 +747,8 @@ import org.checkerframework.checker.nullness.qual.NonNull; } @Override - public ImmutableList getCustomLayout() { - return customLayoutWithUnavailableButtonsDisabled; + public ImmutableList getMediaButtonPreferences() { + return resolvedMediaButtonPreferences; } @Override @@ -2652,9 +2654,13 @@ import org.checkerframework.checker.nullness.qual.NonNull; createIntersectedCommandsEnsuringCommandReleaseAvailable( playerCommandsFromSession, playerCommandsFromPlayer); customLayoutOriginal = result.customLayout; - customLayoutWithUnavailableButtonsDisabled = - CommandButton.copyWithUnavailableButtonsDisabled( - result.customLayout, sessionCommands, intersectedPlayerCommands); + mediaButtonPreferencesOriginal = result.mediaButtonPreferences; + resolvedMediaButtonPreferences = + resolveMediaButtonPreferences( + mediaButtonPreferencesOriginal, + customLayoutOriginal, + sessionCommands, + intersectedPlayerCommands); ImmutableMap.Builder commandButtonsForMediaItems = new ImmutableMap.Builder<>(); for (int i = 0; i < result.commandButtonsForMediaItems.size(); i++) { @@ -2834,13 +2840,17 @@ import org.checkerframework.checker.nullness.qual.NonNull; intersectedPlayerCommandsChanged = !Util.areEqual(intersectedPlayerCommands, prevIntersectedPlayerCommands); } - boolean customLayoutChanged = false; + boolean mediaButtonPreferencesChanged = false; if (sessionCommandsChanged || intersectedPlayerCommandsChanged) { - ImmutableList oldCustomLayout = customLayoutWithUnavailableButtonsDisabled; - customLayoutWithUnavailableButtonsDisabled = - CommandButton.copyWithUnavailableButtonsDisabled( - customLayoutOriginal, sessionCommands, intersectedPlayerCommands); - customLayoutChanged = !customLayoutWithUnavailableButtonsDisabled.equals(oldCustomLayout); + ImmutableList oldMediaButtonPreferences = resolvedMediaButtonPreferences; + resolvedMediaButtonPreferences = + resolveMediaButtonPreferences( + mediaButtonPreferencesOriginal, + customLayoutOriginal, + sessionCommands, + intersectedPlayerCommands); + mediaButtonPreferencesChanged = + !resolvedMediaButtonPreferences.equals(oldMediaButtonPreferences); } if (intersectedPlayerCommandsChanged) { listeners.sendEvent( @@ -2853,12 +2863,14 @@ import org.checkerframework.checker.nullness.qual.NonNull; listener -> listener.onAvailableSessionCommandsChanged(getInstance(), sessionCommands)); } - if (customLayoutChanged) { + if (mediaButtonPreferencesChanged) { getInstance() .notifyControllerListener( - listener -> - listener.onCustomLayoutChanged( - getInstance(), customLayoutWithUnavailableButtonsDisabled)); + listener -> { + listener.onCustomLayoutChanged(getInstance(), resolvedMediaButtonPreferences); + listener.onMediaButtonPreferencesChanged( + getInstance(), resolvedMediaButtonPreferences); + }); } } @@ -2876,50 +2888,84 @@ import org.checkerframework.checker.nullness.qual.NonNull; playerCommandsFromSession, playerCommandsFromPlayer); boolean intersectedPlayerCommandsChanged = !Util.areEqual(intersectedPlayerCommands, prevIntersectedPlayerCommands); - boolean customLayoutChanged = false; + boolean mediaButtonPreferencesChanged = false; if (intersectedPlayerCommandsChanged) { - ImmutableList oldCustomLayout = customLayoutWithUnavailableButtonsDisabled; - customLayoutWithUnavailableButtonsDisabled = - CommandButton.copyWithUnavailableButtonsDisabled( - customLayoutOriginal, sessionCommands, intersectedPlayerCommands); - customLayoutChanged = !customLayoutWithUnavailableButtonsDisabled.equals(oldCustomLayout); + ImmutableList oldMediaButtonPreferences = resolvedMediaButtonPreferences; + resolvedMediaButtonPreferences = + resolveMediaButtonPreferences( + mediaButtonPreferencesOriginal, + customLayoutOriginal, + sessionCommands, + intersectedPlayerCommands); + mediaButtonPreferencesChanged = + !resolvedMediaButtonPreferences.equals(oldMediaButtonPreferences); listeners.sendEvent( /* eventFlag= */ Player.EVENT_AVAILABLE_COMMANDS_CHANGED, listener -> listener.onAvailableCommandsChanged(intersectedPlayerCommands)); } - if (customLayoutChanged) { + if (mediaButtonPreferencesChanged) { getInstance() .notifyControllerListener( - listener -> - listener.onCustomLayoutChanged( - getInstance(), customLayoutWithUnavailableButtonsDisabled)); + listener -> { + listener.onCustomLayoutChanged(getInstance(), resolvedMediaButtonPreferences); + listener.onMediaButtonPreferencesChanged( + getInstance(), resolvedMediaButtonPreferences); + }); } } - // Calling deprecated listener callback method for backwards compatibility. - @SuppressWarnings("deprecation") void onSetCustomLayout(int seq, List layout) { if (!isConnected()) { return; } - ImmutableList oldCustomLayout = customLayoutWithUnavailableButtonsDisabled; + ImmutableList oldMediaButtonPreferences = resolvedMediaButtonPreferences; customLayoutOriginal = ImmutableList.copyOf(layout); - customLayoutWithUnavailableButtonsDisabled = - CommandButton.copyWithUnavailableButtonsDisabled( - layout, sessionCommands, intersectedPlayerCommands); - boolean hasCustomLayoutChanged = - !Objects.equals(customLayoutWithUnavailableButtonsDisabled, oldCustomLayout); + resolvedMediaButtonPreferences = + resolveMediaButtonPreferences( + mediaButtonPreferencesOriginal, layout, sessionCommands, intersectedPlayerCommands); + boolean mediaButtonPreferencesChanged = + !Objects.equals(resolvedMediaButtonPreferences, oldMediaButtonPreferences); getInstance() .notifyControllerListener( listener -> { ListenableFuture future = checkNotNull( - listener.onSetCustomLayout( - getInstance(), customLayoutWithUnavailableButtonsDisabled), + listener.onSetCustomLayout(getInstance(), resolvedMediaButtonPreferences), "MediaController.Listener#onSetCustomLayout() must not return null"); - if (hasCustomLayoutChanged) { - listener.onCustomLayoutChanged( - getInstance(), customLayoutWithUnavailableButtonsDisabled); + if (mediaButtonPreferencesChanged) { + listener.onCustomLayoutChanged(getInstance(), resolvedMediaButtonPreferences); + listener.onMediaButtonPreferencesChanged( + getInstance(), resolvedMediaButtonPreferences); + } + sendControllerResultWhenReady(seq, future); + }); + } + + void onSetMediaButtonPreferences(int seq, List mediaButtonPreferences) { + if (!isConnected()) { + return; + } + ImmutableList oldMediaButtonPreferences = resolvedMediaButtonPreferences; + mediaButtonPreferencesOriginal = ImmutableList.copyOf(mediaButtonPreferences); + resolvedMediaButtonPreferences = + resolveMediaButtonPreferences( + mediaButtonPreferences, + customLayoutOriginal, + sessionCommands, + intersectedPlayerCommands); + boolean mediaButtonPreferencesChanged = + !Objects.equals(resolvedMediaButtonPreferences, oldMediaButtonPreferences); + getInstance() + .notifyControllerListener( + listener -> { + ListenableFuture future = + checkNotNull( + listener.onSetCustomLayout(getInstance(), resolvedMediaButtonPreferences), + "MediaController.Listener#onSetCustomLayout() must not return null"); + if (mediaButtonPreferencesChanged) { + listener.onCustomLayoutChanged(getInstance(), resolvedMediaButtonPreferences); + listener.onMediaButtonPreferencesChanged( + getInstance(), resolvedMediaButtonPreferences); } sendControllerResultWhenReady(seq, future); }); @@ -3277,6 +3323,18 @@ import org.checkerframework.checker.nullness.qual.NonNull; return newMediaItemIndex; } + private static ImmutableList resolveMediaButtonPreferences( + List mediaButtonPreferences, + List customLayout, + SessionCommands sessionCommands, + Player.Commands playerCommands) { + // TODO: b/332877990 - When using custom layout, set correct slots based on available commands. + return CommandButton.copyWithUnavailableButtonsDisabled( + mediaButtonPreferences.isEmpty() ? customLayout : mediaButtonPreferences, + sessionCommands, + playerCommands); + } + private static Commands createIntersectedCommandsEnsuringCommandReleaseAvailable( Commands commandFromSession, Commands commandsFromPlayer) { Commands intersectedCommands = MediaUtils.intersect(commandFromSession, commandsFromPlayer); diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java index f3730d85cc..d8e2dc2d7d 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java @@ -202,7 +202,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; maskedPlayerInfo, controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, - controllerInfo.customLayout, + controllerInfo.mediaButtonPreferences, controllerInfo.sessionExtras, /* sessionError= */ null); updateStateMaskedControllerInfo( @@ -268,7 +268,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; /* playerError= */ null), controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, - controllerInfo.customLayout, + controllerInfo.mediaButtonPreferences, controllerInfo.sessionExtras, /* sessionError= */ null); updateStateMaskedControllerInfo( @@ -391,7 +391,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; maskedPlayerInfo, controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, - controllerInfo.customLayout, + controllerInfo.mediaButtonPreferences, controllerInfo.sessionExtras, /* sessionError= */ null); updateStateMaskedControllerInfo( @@ -432,8 +432,8 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; } @Override - public ImmutableList getCustomLayout() { - return controllerInfo.customLayout; + public ImmutableList getMediaButtonPreferences() { + return controllerInfo.mediaButtonPreferences; } @Override @@ -556,7 +556,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.playerInfo.copyWithPlaybackParameters(playbackParameters), controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, - controllerInfo.customLayout, + controllerInfo.mediaButtonPreferences, controllerInfo.sessionExtras, /* sessionError= */ null); updateStateMaskedControllerInfo( @@ -577,7 +577,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.playerInfo.copyWithPlaybackParameters(new PlaybackParameters(speed)), controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, - controllerInfo.customLayout, + controllerInfo.mediaButtonPreferences, controllerInfo.sessionExtras, /* sessionError= */ null); updateStateMaskedControllerInfo( @@ -671,7 +671,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; maskedPlayerInfo, controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, - controllerInfo.customLayout, + controllerInfo.mediaButtonPreferences, controllerInfo.sessionExtras, /* sessionError= */ null); updateStateMaskedControllerInfo( @@ -736,7 +736,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; maskedPlayerInfo, controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, - controllerInfo.customLayout, + controllerInfo.mediaButtonPreferences, controllerInfo.sessionExtras, /* sessionError= */ null); updateStateMaskedControllerInfo( @@ -790,7 +790,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; maskedPlayerInfo, controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, - controllerInfo.customLayout, + controllerInfo.mediaButtonPreferences, controllerInfo.sessionExtras, /* sessionError= */ null); updateStateMaskedControllerInfo( @@ -858,7 +858,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; maskedPlayerInfo, controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, - controllerInfo.customLayout, + controllerInfo.mediaButtonPreferences, controllerInfo.sessionExtras, /* sessionError= */ null); updateStateMaskedControllerInfo( @@ -972,7 +972,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.playerInfo.copyWithRepeatMode(repeatMode), controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, - controllerInfo.customLayout, + controllerInfo.mediaButtonPreferences, controllerInfo.sessionExtras, /* sessionError= */ null); updateStateMaskedControllerInfo( @@ -1000,7 +1000,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.playerInfo.copyWithShuffleModeEnabled(shuffleModeEnabled), controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, - controllerInfo.customLayout, + controllerInfo.mediaButtonPreferences, controllerInfo.sessionExtras, /* sessionError= */ null); updateStateMaskedControllerInfo( @@ -1137,7 +1137,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.playerInfo.copyWithDeviceVolume(volume, isDeviceMuted), controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, - controllerInfo.customLayout, + controllerInfo.mediaButtonPreferences, controllerInfo.sessionExtras, /* sessionError= */ null); updateStateMaskedControllerInfo( @@ -1170,7 +1170,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.playerInfo.copyWithDeviceVolume(volume + 1, isDeviceMuted), controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, - controllerInfo.customLayout, + controllerInfo.mediaButtonPreferences, controllerInfo.sessionExtras, /* sessionError= */ null); updateStateMaskedControllerInfo( @@ -1202,7 +1202,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.playerInfo.copyWithDeviceVolume(volume - 1, isDeviceMuted), controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, - controllerInfo.customLayout, + controllerInfo.mediaButtonPreferences, controllerInfo.sessionExtras, /* sessionError= */ null); updateStateMaskedControllerInfo( @@ -1237,7 +1237,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.playerInfo.copyWithDeviceVolume(volume, muted), controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, - controllerInfo.customLayout, + controllerInfo.mediaButtonPreferences, controllerInfo.sessionExtras, /* sessionError= */ null); updateStateMaskedControllerInfo( @@ -1276,7 +1276,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; Player.PLAYBACK_SUPPRESSION_REASON_NONE), controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, - controllerInfo.customLayout, + controllerInfo.mediaButtonPreferences, controllerInfo.sessionExtras, /* sessionError= */ null); updateStateMaskedControllerInfo( @@ -1625,13 +1625,18 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; if (notifyConnected) { getInstance().notifyAccepted(); - if (!oldControllerInfo.customLayout.equals(newControllerInfo.customLayout)) { + if (!oldControllerInfo.mediaButtonPreferences.equals( + newControllerInfo.mediaButtonPreferences)) { getInstance() .notifyControllerListener( listener -> { ignoreFuture( - listener.onSetCustomLayout(getInstance(), newControllerInfo.customLayout)); - listener.onCustomLayoutChanged(getInstance(), newControllerInfo.customLayout); + listener.onSetCustomLayout( + getInstance(), newControllerInfo.mediaButtonPreferences)); + listener.onCustomLayoutChanged( + getInstance(), newControllerInfo.mediaButtonPreferences); + listener.onMediaButtonPreferencesChanged( + getInstance(), newControllerInfo.mediaButtonPreferences); }); } return; @@ -1758,13 +1763,18 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; listener.onAvailableSessionCommandsChanged( getInstance(), newControllerInfo.availableSessionCommands)); } - if (!oldControllerInfo.customLayout.equals(newControllerInfo.customLayout)) { + if (!oldControllerInfo.mediaButtonPreferences.equals( + newControllerInfo.mediaButtonPreferences)) { getInstance() .notifyControllerListener( listener -> { ignoreFuture( - listener.onSetCustomLayout(getInstance(), newControllerInfo.customLayout)); - listener.onCustomLayoutChanged(getInstance(), newControllerInfo.customLayout); + listener.onSetCustomLayout( + getInstance(), newControllerInfo.mediaButtonPreferences)); + listener.onCustomLayoutChanged( + getInstance(), newControllerInfo.mediaButtonPreferences); + listener.onMediaButtonPreferencesChanged( + getInstance(), newControllerInfo.mediaButtonPreferences); }); } if (newControllerInfo.sessionError != null) { @@ -1913,7 +1923,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerInfo.playerInfo, controllerInfo.availableSessionCommands, controllerInfo.availablePlayerCommands, - controllerInfo.customLayout, + controllerInfo.mediaButtonPreferences, extras, /* sessionError= */ null); getInstance() @@ -1988,7 +1998,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; boolean shuffleModeEnabled; SessionCommands availableSessionCommands; Commands availablePlayerCommands; - ImmutableList customLayout; + ImmutableList mediaButtonPreferences; boolean isQueueChanged = oldLegacyPlayerInfo.queue != newLegacyPlayerInfo.queue; currentTimeline = @@ -2074,11 +2084,12 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; availableSessionCommands = LegacyConversions.convertToSessionCommands( newLegacyPlayerInfo.playbackStateCompat, isSessionReady); - customLayout = - LegacyConversions.convertToCustomLayout(newLegacyPlayerInfo.playbackStateCompat); + mediaButtonPreferences = + LegacyConversions.convertToMediaButtonPreferences( + newLegacyPlayerInfo.playbackStateCompat); } else { availableSessionCommands = oldControllerInfo.availableSessionCommands; - customLayout = oldControllerInfo.customLayout; + mediaButtonPreferences = oldControllerInfo.mediaButtonPreferences; } // Note: Sets the available player command here although it can be obtained before session is // ready. It's to follow the decision on MediaController to disallow any commands before @@ -2164,7 +2175,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; shuffleModeEnabled, availableSessionCommands, availablePlayerCommands, - customLayout, + mediaButtonPreferences, newLegacyPlayerInfo.sessionExtras, playerError, sessionError, @@ -2334,7 +2345,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; boolean shuffleModeEnabled, SessionCommands availableSessionCommands, Commands availablePlayerCommands, - ImmutableList customLayout, + ImmutableList mediaButtonPreferences, Bundle sessionExtras, @Nullable PlaybackException playerError, @Nullable SessionError sessionError, @@ -2411,7 +2422,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; playerInfo, availableSessionCommands, availablePlayerCommands, - customLayout, + mediaButtonPreferences, sessionExtras, sessionError); } @@ -2622,7 +2633,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; public final PlayerInfo playerInfo; public final SessionCommands availableSessionCommands; public final Commands availablePlayerCommands; - public final ImmutableList customLayout; + public final ImmutableList mediaButtonPreferences; public final Bundle sessionExtras; @Nullable public final SessionError sessionError; @@ -2630,7 +2641,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; playerInfo = PlayerInfo.DEFAULT.copyWithTimeline(QueueTimeline.DEFAULT); availableSessionCommands = SessionCommands.EMPTY; availablePlayerCommands = Commands.EMPTY; - customLayout = ImmutableList.of(); + mediaButtonPreferences = ImmutableList.of(); sessionExtras = Bundle.EMPTY; sessionError = null; } @@ -2639,13 +2650,13 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; PlayerInfo playerInfo, SessionCommands availableSessionCommands, Commands availablePlayerCommands, - ImmutableList customLayout, + ImmutableList mediaButtonPreferences, @Nullable Bundle sessionExtras, @Nullable SessionError sessionError) { this.playerInfo = playerInfo; this.availableSessionCommands = availableSessionCommands; this.availablePlayerCommands = availablePlayerCommands; - this.customLayout = customLayout; + this.mediaButtonPreferences = mediaButtonPreferences; this.sessionExtras = sessionExtras == null ? Bundle.EMPTY : sessionExtras; this.sessionError = sessionError; } 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 57c6d99d48..babfea061f 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerStub.java @@ -30,6 +30,7 @@ import androidx.media3.common.util.BundleCollectionUtil; import androidx.media3.common.util.Log; import androidx.media3.session.MediaLibraryService.LibraryParams; import androidx.media3.session.PlayerInfo.BundlingExclusions; +import com.google.common.collect.ImmutableList; import java.lang.ref.WeakReference; import java.util.List; import org.checkerframework.checker.nullness.qual.NonNull; @@ -39,7 +40,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 = 6; + public static final int VERSION_INT = 7; private final WeakReference controller; @@ -129,6 +130,30 @@ import org.checkerframework.checker.nullness.qual.NonNull; dispatchControllerTaskOnHandler(controller -> controller.onSetCustomLayout(seq, layout)); } + @Override + public void onSetMediaButtonPreferences(int seq, @Nullable List commandButtonBundleList) { + if (commandButtonBundleList == null) { + return; + } + ImmutableList mediaButtonPreferences; + try { + int sessionInterfaceVersion = getSessionInterfaceVersion(); + if (sessionInterfaceVersion == C.INDEX_UNSET) { + // Stale event. + return; + } + mediaButtonPreferences = + BundleCollectionUtil.fromBundleList( + bundle -> CommandButton.fromBundle(bundle, sessionInterfaceVersion), + commandButtonBundleList); + } catch (RuntimeException e) { + Log.w(TAG, "Ignoring malformed Bundle for CommandButton", e); + return; + } + dispatchControllerTaskOnHandler( + controller -> controller.onSetMediaButtonPreferences(seq, mediaButtonPreferences)); + } + @Override public void onAvailableCommandsChangedFromSession( int seq, @Nullable Bundle sessionCommandsBundle, @Nullable Bundle playerCommandsBundle) { diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaLibraryService.java b/libraries/session/src/main/java/androidx/media3/session/MediaLibraryService.java index 554ed2f859..67639fb12c 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaLibraryService.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaLibraryService.java @@ -543,6 +543,10 @@ public abstract class MediaLibraryService extends MediaSessionService { /** * Sets the custom layout of the session. * + *

This method will be deprecated, prefer to use {@link #setMediaButtonPreferences}. Note + * that the media button preferences use {@link CommandButton#slots} to define the allowed + * button placement. + * *

The buttons are converted to custom actions in the legacy media session playback state * for legacy controllers (see {@code * PlaybackStateCompat.Builder#addCustomAction(PlaybackStateCompat.CustomAction)}). When @@ -564,11 +568,42 @@ public abstract class MediaLibraryService extends MediaSessionService { * @return The builder to allow chaining. */ @UnstableApi + @CanIgnoreReturnValue @Override public Builder setCustomLayout(List customLayout) { return super.setCustomLayout(customLayout); } + /** + * Sets the media button preferences. + * + *

The button are converted to custom actions in the legacy media session playback state + * for legacy controllers (see {@code + * PlaybackStateCompat.Builder#addCustomAction(PlaybackStateCompat.CustomAction)}). When + * converting, the {@linkplain SessionCommand#customExtras custom extras of the session + * command} is used for the extras of the legacy custom action. + * + *

Controllers that connect have the media button preferences of the session available with + * the initial connection result by default. Media button preferences specific to a controller + * can be set when the controller {@linkplain MediaSession.Callback#onConnect connects} by + * using an {@link ConnectionResult.AcceptedResultBuilder}. + * + *

Use {@code MediaSession.setMediaButtonPreferences(..)} to update the media button + * preferences during the life time of the session. + * + *

On the controller side, the {@linkplain CommandButton#isEnabled enabled} flag is set to + * {@code false} if the available commands of a controller do not allow to use a button. + * + * @param mediaButtonPreferences The ordered list of {@link CommandButton command buttons}. + * @return The builder to allow chaining. + */ + @CanIgnoreReturnValue + @UnstableApi + @Override + public Builder setMediaButtonPreferences(List mediaButtonPreferences) { + return super.setMediaButtonPreferences(mediaButtonPreferences); + } + /** * Sets whether a play button is shown if playback is {@linkplain * Player#getPlaybackSuppressionReason() suppressed}. @@ -660,6 +695,7 @@ public abstract class MediaLibraryService extends MediaSessionService { player, sessionActivity, customLayout, + mediaButtonPreferences, commandButtonsForMediaItems, callback, tokenExtras, @@ -677,6 +713,7 @@ public abstract class MediaLibraryService extends MediaSessionService { Player player, @Nullable PendingIntent sessionActivity, ImmutableList customLayout, + ImmutableList mediaButtonPreferences, ImmutableList commandButtonsForMediaItems, MediaSession.Callback callback, Bundle tokenExtras, @@ -691,6 +728,7 @@ public abstract class MediaLibraryService extends MediaSessionService { player, sessionActivity, customLayout, + mediaButtonPreferences, commandButtonsForMediaItems, callback, tokenExtras, @@ -708,6 +746,7 @@ public abstract class MediaLibraryService extends MediaSessionService { Player player, @Nullable PendingIntent sessionActivity, ImmutableList customLayout, + ImmutableList mediaButtonPreferences, ImmutableList commandButtonsForMediaItems, MediaSession.Callback callback, Bundle tokenExtras, @@ -723,6 +762,7 @@ public abstract class MediaLibraryService extends MediaSessionService { player, sessionActivity, customLayout, + mediaButtonPreferences, commandButtonsForMediaItems, (Callback) callback, tokenExtras, diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java index db2b779995..e864a64bf3 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java @@ -75,6 +75,7 @@ import java.util.concurrent.Future; Player player, @Nullable PendingIntent sessionActivity, ImmutableList customLayout, + ImmutableList mediaButtonPreferences, ImmutableList commandButtonsForMediaItems, MediaLibrarySession.Callback callback, Bundle tokenExtras, @@ -90,6 +91,7 @@ import java.util.concurrent.Future; player, sessionActivity, customLayout, + mediaButtonPreferences, commandButtonsForMediaItems, callback, tokenExtras, diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaNotification.java b/libraries/session/src/main/java/androidx/media3/session/MediaNotification.java index 3ac4a3a514..847abd4400 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaNotification.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaNotification.java @@ -27,7 +27,6 @@ import androidx.core.graphics.drawable.IconCompat; import androidx.media3.common.Player; import androidx.media3.common.util.UnstableApi; import com.google.common.collect.ImmutableList; -import java.util.List; /** A notification for media playbacks. */ public final class MediaNotification { @@ -145,15 +144,15 @@ public final class MediaNotification { * @param mediaSession The media session. * @param actionFactory The {@link ActionFactory} for creating notification {@link * NotificationCompat.Action actions}. - * @param customLayout The custom layout {@linkplain MediaSession#setCustomLayout(List) set by - * the session}. + * @param mediaButtonPreferences The media button preferences {@linkplain + * MediaSession#setMediaButtonPreferences set by the session}. * @param onNotificationChangedCallback A callback that the provider needs to notify when the * notification has changed and needs to be posted again, for example after a bitmap has * been loaded asynchronously. */ MediaNotification createNotification( MediaSession mediaSession, - ImmutableList customLayout, + ImmutableList mediaButtonPreferences, ActionFactory actionFactory, Callback onNotificationChangedCallback); diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java b/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java index 791c748fbc..14f2406a97 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java @@ -159,9 +159,9 @@ import java.util.concurrent.TimeoutException; // Ignore. } } - ImmutableList customLayout = + ImmutableList mediaButtonPreferences = mediaNotificationController != null - ? mediaNotificationController.getCustomLayout() + ? mediaNotificationController.getMediaButtonPreferences() : ImmutableList.of(); MediaNotification.Provider.Callback callback = notification -> @@ -172,7 +172,7 @@ import java.util.concurrent.TimeoutException; () -> { MediaNotification mediaNotification = this.mediaNotificationProvider.createNotification( - session, customLayout, actionFactory, callback); + session, mediaButtonPreferences, actionFactory, callback); mainExecutor.execute( () -> updateNotificationInternal( @@ -320,7 +320,8 @@ import java.util.concurrent.TimeoutException; } @Override - public void onCustomLayoutChanged(MediaController controller, List layout) { + public void onMediaButtonPreferencesChanged( + MediaController controller, List mediaButtonPreferences) { mediaSessionService.onUpdateNotificationInternal( session, /* startInForegroundWhenPaused= */ false); } 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 6f5541d97b..c19bf17c8a 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -369,6 +369,10 @@ public class MediaSession { /** * Sets the custom layout of the session. * + *

This method will be deprecated, prefer to use {@link #setMediaButtonPreferences}. Note + * that the media button preferences use {@link CommandButton#slots} to define the allowed + * button placement. + * *

The button are converted to custom actions in the legacy media session playback state for * legacy controllers (see {@code * PlaybackStateCompat.Builder#addCustomAction(PlaybackStateCompat.CustomAction)}). When @@ -389,12 +393,43 @@ public class MediaSession { * @param customLayout The ordered list of {@link CommandButton command buttons}. * @return The builder to allow chaining. */ + @CanIgnoreReturnValue @UnstableApi @Override public Builder setCustomLayout(List customLayout) { return super.setCustomLayout(customLayout); } + /** + * Sets the media button preferences. + * + *

The button are converted to custom actions in the legacy media session playback state for + * legacy controllers (see {@code + * PlaybackStateCompat.Builder#addCustomAction(PlaybackStateCompat.CustomAction)}). When + * converting, the {@linkplain SessionCommand#customExtras custom extras of the session command} + * is used for the extras of the legacy custom action. + * + *

Controllers that connect have the media button preferences of the session available with + * the initial connection result by default. Media button preferences specific to a controller + * can be set when the controller {@linkplain MediaSession.Callback#onConnect connects} by using + * an {@link ConnectionResult.AcceptedResultBuilder}. + * + *

Use {@code MediaSession.setMediaButtonPreferences(..)} to update the media button + * preferences during the life time of the session. + * + *

On the controller side, the {@linkplain CommandButton#isEnabled enabled} flag is set to + * {@code false} if the available commands of a controller do not allow to use a button. + * + * @param mediaButtonPreferences The ordered list of {@link CommandButton command buttons}. + * @return The builder to allow chaining. + */ + @CanIgnoreReturnValue + @UnstableApi + @Override + public Builder setMediaButtonPreferences(List mediaButtonPreferences) { + return super.setMediaButtonPreferences(mediaButtonPreferences); + } + /** * Sets whether periodic position updates should be sent to controllers while playing. If false, * no periodic position updates are sent to controllers. @@ -455,6 +490,7 @@ public class MediaSession { player, sessionActivity, customLayout, + mediaButtonPreferences, commandButtonsForMediaItems, callback, tokenExtras, @@ -682,6 +718,7 @@ public class MediaSession { Player player, @Nullable PendingIntent sessionActivity, ImmutableList customLayout, + ImmutableList mediaButtonPreferences, ImmutableList commandButtonsForMediaItems, Callback callback, Bundle tokenExtras, @@ -703,6 +740,7 @@ public class MediaSession { player, sessionActivity, customLayout, + mediaButtonPreferences, commandButtonsForMediaItems, callback, tokenExtras, @@ -719,6 +757,7 @@ public class MediaSession { Player player, @Nullable PendingIntent sessionActivity, ImmutableList customLayout, + ImmutableList mediaButtonPreferences, ImmutableList commandButtonsForMediaItems, Callback callback, Bundle tokenExtras, @@ -734,6 +773,7 @@ public class MediaSession { player, sessionActivity, customLayout, + mediaButtonPreferences, commandButtonsForMediaItems, callback, tokenExtras, @@ -912,12 +952,12 @@ public class MediaSession { * *

Use this controller info to set {@linkplain #setAvailableCommands(ControllerInfo, * SessionCommands, Player.Commands) available commands} and {@linkplain - * #setCustomLayout(ControllerInfo, List) custom layout} that are consistently applied to the - * media notification on all API levels. + * #setMediaButtonPreferences(ControllerInfo, List) media button preferences} that are + * consistently applied to the media notification on all API levels. * *

Available {@linkplain SessionCommands session commands} of the media notification controller - * are used to enable or disable buttons of the custom layout before it is passed to the - * {@linkplain MediaNotification.Provider#createNotification(MediaSession, ImmutableList, + * are used to enable or disable buttons of the media button preferences before they are passed to + * the {@linkplain MediaNotification.Provider#createNotification(MediaSession, ImmutableList, * MediaNotification.ActionFactory, MediaNotification.Provider.Callback) notification provider}. * Disabled command buttons are not converted to notification actions when using {@link * DefaultMediaNotificationProvider}. This affects the media notification displayed by System UI @@ -925,9 +965,9 @@ public class MediaSession { * *

The available session commands of the media notification controller are used to maintain * custom actions of the platform session (see {@code PlaybackStateCompat.getCustomActions()}). - * Command buttons of the custom layout are disabled or enabled according to the available session - * commands. Disabled command buttons are not converted to custom actions of the platform session. - * This affects the media notification displayed by System UI starting * with API 33. * @@ -969,7 +1009,11 @@ public class MediaSession { } /** - * Sets the custom layout for the given Media3 controller. + * Sets the custom layout for the given controller. + * + *

This method will be deprecated, prefer to use {@link + * #setMediaButtonPreferences(ControllerInfo, List)}. Note that the media button preferences use + * {@link CommandButton#slots} to define the allowed button placement. * *

Make sure to have the session commands of all command buttons of the custom layout * {@linkplain MediaController#getAvailableSessionCommands() available for controllers}. Include @@ -1003,7 +1047,11 @@ public class MediaSession { } /** - * Sets the custom layout that can initially be set when building the session. + * Sets the custom layout for all controllers. + * + *

This method will be deprecated, prefer to use {@link #setMediaButtonPreferences(List)}. Note + * that the media button preferences use {@link CommandButton#slots} to define the allowed button + * placement. * *

Calling this method broadcasts the custom layout to all connected Media3 controllers, * including the {@linkplain #getMediaNotificationControllerInfo() media notification controller}. @@ -1021,13 +1069,78 @@ public class MediaSession { * the controller {@linkplain MediaSession.Callback#onConnect connects} by using an {@link * ConnectionResult.AcceptedResultBuilder}. * - * @param layout The ordered list of {@link CommandButton}. + * @param layout The ordered list of {@linkplain CommandButton command buttons}. */ public final void setCustomLayout(List layout) { checkNotNull(layout, "layout must not be null"); impl.setCustomLayout(ImmutableList.copyOf(layout)); } + /** + * Sets the media button preferences for the given controller. + * + *

Make sure to have the session commands of all command buttons of the media button + * preferences {@linkplain MediaController#getAvailableSessionCommands() available for + * controllers}. Include the custom session commands a controller should be able to send in the + * available commands of the connection result {@linkplain + * MediaSession.Callback#onConnect(MediaSession, ControllerInfo) that your app returns when the + * controller connects}. The {@link CommandButton#isEnabled} flag is set according to the + * available commands of the controller and overrides a value that may have been set by the app. + * + *

On the controller side, {@link + * MediaController.Listener#onMediaButtonPreferencesChanged(MediaController, List)} is only called + * if the new media button preferences are different to the media button preferences the {@link + * MediaController#getMediaButtonPreferences() controller already has available}. Note that this + * comparison uses {@link CommandButton#equals} and therefore ignores {@link + * CommandButton#extras}. + * + *

On the controller side, the {@linkplain CommandButton#isEnabled enabled} flag is set to + * {@code false} if the available commands of the controller do not allow to use a button. + * + *

Interoperability: This call has no effect when called for a {@linkplain + * ControllerInfo#LEGACY_CONTROLLER_VERSION legacy controller}. + * + * @param controller The controller for which to set the media button preferences. + * @param mediaButtonPreferences The ordered list of {@linkplain CommandButton command buttons}. + */ + @UnstableApi + @CanIgnoreReturnValue + public final ListenableFuture setMediaButtonPreferences( + ControllerInfo controller, List mediaButtonPreferences) { + checkNotNull(controller, "controller must not be null"); + checkNotNull(mediaButtonPreferences, "media button preferences must not be null"); + return impl.setMediaButtonPreferences(controller, ImmutableList.copyOf(mediaButtonPreferences)); + } + + /** + * Sets the media button preferences for all controllers. + * + *

Calling this method broadcasts the media button preferences to all connected Media3 + * controllers, including the {@linkplain #getMediaNotificationControllerInfo() media notification + * controller}. + * + *

On the controller side, the {@linkplain CommandButton#isEnabled enabled} flag is set to + * {@code false} if the available commands of a controller do not allow to use a button. + * + *

{@link MediaController.Listener#onMediaButtonPreferencesChanged(MediaController, List)} is + * only called if the new media button preferences are different to the media button preferences + * the {@linkplain MediaController#getMediaButtonPreferences() controller already has available}. + * Note that {@link Bundle extras} are ignored when comparing {@linkplain CommandButton command + * buttons}. + * + *

Controllers that connect after calling this method will have the new media button + * preferences available with the initial connection result. Media button preferences specific to + * a controller can be set when the controller {@linkplain MediaSession.Callback#onConnect + * connects} by using an {@link ConnectionResult.AcceptedResultBuilder}. + * + * @param mediaButtonPreferences The ordered list of {@linkplain CommandButton command buttons}. + */ + @UnstableApi + public final void setMediaButtonPreferences(List mediaButtonPreferences) { + checkNotNull(mediaButtonPreferences, "media button preferences must not be null"); + impl.setMediaButtonPreferences(ImmutableList.copyOf(mediaButtonPreferences)); + } + /** * Sets the new available commands for the controller. * @@ -1055,6 +1168,10 @@ public class MediaSession { /** * Returns the custom layout of the session. * + *

This method will be deprecated, prefer to use {@link #getMediaButtonPreferences()} instead. + * Note that the media button preferences use {@link CommandButton#slots} to define the allowed + * button placement. + * *

For informational purpose only. Mutations on the {@link Bundle} of either a {@link * CommandButton} or a {@link SessionCommand} do not have effect. To change the custom layout use * {@link #setCustomLayout(List)} or {@link #setCustomLayout(ControllerInfo, List)}. @@ -1064,6 +1181,18 @@ public class MediaSession { return impl.getCustomLayout(); } + /** + * Returns the media button preferences of the session. + * + *

For informational purpose only. Mutations on the {@link Bundle} of either a {@link + * CommandButton} or a {@link SessionCommand} do not have effect. To change the media button + * preferences use {@link #setMediaButtonPreferences}. + */ + @UnstableApi + public ImmutableList getMediaButtonPreferences() { + return impl.getMediaButtonPreferences(); + } + /** * Broadcasts a custom command to all connected controllers. * @@ -1313,7 +1442,8 @@ public class MediaSession { * *

If this callback is not overridden, it allows all controllers to connect that can access * the session. All session and player commands are made available and the {@linkplain - * MediaSession#getCustomLayout() custom layout of the session} is included. + * MediaSession#getMediaButtonPreferences() media button preferences of the session} are + * included. * *

Note that the player commands in {@link ConnectionResult#availablePlayerCommands} will be * intersected with the {@link Player#getAvailableCommands() available commands} of the @@ -1325,8 +1455,8 @@ public class MediaSession { * returned by {@link MediaController.Builder#buildAsync()}. * *

The controller isn't connected yet, so calls to the controller (e.g. {@link - * #sendCustomCommand}, {@link #setCustomLayout}) will be ignored. Use {@link #onPostConnect} - * for custom initialization of the controller instead. + * #sendCustomCommand}, {@link #setMediaButtonPreferences}) will be ignored. Use {@link + * #onPostConnect} for custom initialization of the controller instead. * *

Interoperability: If a legacy controller is connecting to the session then this callback * may block the main thread, even if it's called on a different application thread. If it's @@ -1346,8 +1476,8 @@ public class MediaSession { * controller. * *

Note that calls to the controller (e.g. {@link #sendCustomCommand}, {@link - * #setCustomLayout}) work here but don't work in {@link #onConnect} because the controller - * isn't connected yet in {@link #onConnect}. + * #setMediaButtonPreferences}) work here but don't work in {@link #onConnect} because the + * controller isn't connected yet in {@link #onConnect}. * * @param session The session for this event. * @param controller The {@linkplain ControllerInfo controller} information. @@ -1738,7 +1868,7 @@ public class MediaSession { /** * A result for {@link Callback#onConnect(MediaSession, ControllerInfo)} to denote the set of - * available commands and the custom layout for a {@link ControllerInfo controller}. + * available commands and the media button preferences for a {@link ControllerInfo controller}. */ public static final class ConnectionResult { @@ -1748,6 +1878,7 @@ public class MediaSession { private SessionCommands availableSessionCommands; private Player.Commands availablePlayerCommands = DEFAULT_PLAYER_COMMANDS; @Nullable private ImmutableList customLayout; + @Nullable private ImmutableList mediaButtonPreferences; @Nullable private Bundle sessionExtras; @Nullable private PendingIntent sessionActivity; @@ -1799,13 +1930,17 @@ public class MediaSession { * Sets the custom layout, overriding the {@linkplain MediaSession#getCustomLayout() custom * layout of the session}. * + *

This method will be deprecated, prefer to use {@link #setMediaButtonPreferences}. Note + * that the media button preferences use {@link CommandButton#slots} to define the allowed + * button placement. + * *

The default is null to indicate that the custom layout of the session should be used. * *

Make sure to have the session commands of all command buttons of the custom layout - * included in the {@linkplain #setAvailableSessionCommands(SessionCommands)} available - * session commands} On the controller side, the {@linkplain CommandButton#isEnabled enabled} - * flag is set to {@code false} if the available commands of the controller do not allow to - * use a button. + * included in the {@linkplain #setAvailableSessionCommands(SessionCommands) available session + * commands}. On the controller side, the {@linkplain CommandButton#isEnabled enabled} flag is + * set to {@code false} if the available commands of the controller do not allow to use a + * button. */ @CanIgnoreReturnValue public AcceptedResultBuilder setCustomLayout(@Nullable List customLayout) { @@ -1813,6 +1948,27 @@ public class MediaSession { return this; } + /** + * Sets the media button preferences, overriding the {@linkplain + * MediaSession#getMediaButtonPreferences() media button preferences of the session}. + * + *

The default is null to indicate that the media button preferences of the session should + * be used. + * + *

Make sure to have the session commands of all command buttons of the media button + * preferences included in the {@linkplain #setAvailableSessionCommands(SessionCommands) + * available session commands}. On the controller side, the {@linkplain + * CommandButton#isEnabled enabled} flag is set to {@code false} if the available commands of + * the controller do not allow to use a button. + */ + @CanIgnoreReturnValue + public AcceptedResultBuilder setMediaButtonPreferences( + @Nullable List mediaButtonPreferences) { + this.mediaButtonPreferences = + mediaButtonPreferences == null ? null : ImmutableList.copyOf(mediaButtonPreferences); + return this; + } + /** * Sets the session extras, overriding the {@linkplain MediaSession#getSessionExtras() extras * of the session}. @@ -1844,6 +2000,7 @@ public class MediaSession { availableSessionCommands, availablePlayerCommands, customLayout, + mediaButtonPreferences, sessionExtras, sessionActivity); } @@ -1873,6 +2030,12 @@ public class MediaSession { /** The custom layout or null if the custom layout of the session should be used. */ @UnstableApi @Nullable public final ImmutableList customLayout; + /** + * The media button preferences or null if the media button preferences of the session should be + * used. + */ + @UnstableApi @Nullable public final ImmutableList mediaButtonPreferences; + /** The session extras. */ @UnstableApi @Nullable public final Bundle sessionExtras; @@ -1885,12 +2048,14 @@ public class MediaSession { SessionCommands availableSessionCommands, Player.Commands availablePlayerCommands, @Nullable ImmutableList customLayout, + @Nullable ImmutableList mediaButtonPreferences, @Nullable Bundle sessionExtras, @Nullable PendingIntent sessionActivity) { isAccepted = accepted; this.availableSessionCommands = availableSessionCommands; this.availablePlayerCommands = availablePlayerCommands; this.customLayout = customLayout; + this.mediaButtonPreferences = mediaButtonPreferences; this.sessionExtras = sessionExtras; this.sessionActivity = sessionActivity; } @@ -1900,8 +2065,8 @@ public class MediaSession { * *

Commands are specific to the controller receiving this connection result. * - *

The controller receives {@linkplain MediaSession#getCustomLayout() the custom layout of - * the session}. + *

The controller receives {@linkplain MediaSession#getMediaButtonPreferences() the media + * button preferences of the session}. * *

See {@link AcceptedResultBuilder} for a more flexible way to accept a connection. */ @@ -1912,6 +2077,7 @@ public class MediaSession { availableSessionCommands, availablePlayerCommands, /* customLayout= */ null, + /* mediaButtonPreferences= */ null, /* sessionExtras= */ null, /* sessionActivity= */ null); } @@ -1923,6 +2089,7 @@ public class MediaSession { SessionCommands.EMPTY, Player.Commands.EMPTY, /* customLayout= */ ImmutableList.of(), + /* mediaButtonPreferences= */ ImmutableList.of(), /* sessionExtras= */ Bundle.EMPTY, /* sessionActivity= */ null); } @@ -1943,8 +2110,7 @@ public class MediaSession { PlayerInfo playerInfo, Player.Commands availableCommands, boolean excludeTimeline, - boolean excludeTracks, - int controllerInterfaceVersion) + boolean excludeTracks) throws RemoteException {} default void onPeriodicSessionPositionInfoChanged( @@ -1961,6 +2127,9 @@ public class MediaSession { default void setCustomLayout(int seq, List layout) throws RemoteException {} + default void setMediaButtonPreferences(int seq, List mediaButtonPreferences) + throws RemoteException {} + default void onSessionActivityChanged(int seq, PendingIntent sessionActivity) throws RemoteException {} @@ -2105,6 +2274,7 @@ public class MediaSession { /* package */ @MonotonicNonNull BitmapLoader bitmapLoader; /* package */ boolean playIfSuppressed; /* package */ ImmutableList customLayout; + /* package */ ImmutableList mediaButtonPreferences; /* package */ ImmutableList commandButtonsForMediaItems; /* package */ boolean isPeriodicPositionUpdateEnabled; @@ -2117,6 +2287,7 @@ public class MediaSession { tokenExtras = Bundle.EMPTY; sessionExtras = Bundle.EMPTY; customLayout = ImmutableList.of(); + mediaButtonPreferences = ImmutableList.of(); playIfSuppressed = true; isPeriodicPositionUpdateEnabled = true; commandButtonsForMediaItems = ImmutableList.of(); @@ -2174,6 +2345,13 @@ public class MediaSession { return (BuilderT) this; } + @CanIgnoreReturnValue + @SuppressWarnings("unchecked") + public BuilderT setMediaButtonPreferences(List mediaButtonPreferences) { + this.mediaButtonPreferences = ImmutableList.copyOf(mediaButtonPreferences); + return (BuilderT) this; + } + @CanIgnoreReturnValue @SuppressWarnings("unchecked") public BuilderT setShowPlayButtonIfPlaybackIsSuppressed( 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 9f9c0e045b..b3127736c7 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -152,6 +152,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private long sessionPositionUpdateDelayMs; private boolean isMediaNotificationControllerConnected; private ImmutableList customLayout; + private ImmutableList mediaButtonPreferences; private Bundle sessionExtras; @SuppressWarnings("argument.type.incompatible") // Using this in System.identityHashCode @@ -162,6 +163,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; Player player, @Nullable PendingIntent sessionActivity, ImmutableList customLayout, + ImmutableList mediaButtonPreferences, ImmutableList commandButtonsForMediaItems, MediaSession.Callback callback, Bundle tokenExtras, @@ -183,6 +185,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; sessionId = id; this.sessionActivity = sessionActivity; this.customLayout = customLayout; + this.mediaButtonPreferences = mediaButtonPreferences; this.commandButtonsForMediaItems = commandButtonsForMediaItems; this.callback = callback; this.sessionExtras = sessionExtras; @@ -246,6 +249,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; player, playIfSuppressed, customLayout, + mediaButtonPreferences, connectionResult.availableSessionCommands, connectionResult.availablePlayerCommands, sessionExtras); @@ -272,6 +276,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; player, playIfSuppressed, playerWrapper.getCustomLayout(), + playerWrapper.getMediaButtonPreferences(), playerWrapper.getAvailableSessionCommands(), playerWrapper.getAvailablePlayerCommands(), playerWrapper.getLegacyExtras())); @@ -511,11 +516,45 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; (controller, seq) -> controller.setCustomLayout(seq, customLayout)); } + /** + * Sets the media button preferences for the given {@link MediaController}. + * + * @param controller The controller. + * @param mediaButtonPreferences The media button preferences. + * @return The session result from the controller. + */ + public ListenableFuture setMediaButtonPreferences( + ControllerInfo controller, ImmutableList mediaButtonPreferences) { + if (isMediaNotificationController(controller)) { + playerWrapper.setMediaButtonPreferences(mediaButtonPreferences); + sessionLegacyStub.updateLegacySessionPlaybackState(playerWrapper); + } + return dispatchRemoteControllerTask( + controller, + (controller1, seq) -> controller1.setMediaButtonPreferences(seq, mediaButtonPreferences)); + } + + /** + * Sets the media button preferences of the session and sends the media button preferences to all + * controllers. + */ + public void setMediaButtonPreferences(ImmutableList mediaButtonPreferences) { + this.mediaButtonPreferences = mediaButtonPreferences; + playerWrapper.setMediaButtonPreferences(mediaButtonPreferences); + dispatchRemoteControllerTaskWithoutReturn( + (controller, seq) -> controller.setMediaButtonPreferences(seq, mediaButtonPreferences)); + } + /** Returns the custom layout. */ public ImmutableList getCustomLayout() { return customLayout; } + /** Returns the media button preferences. */ + public ImmutableList getMediaButtonPreferences() { + return mediaButtonPreferences; + } + /** Returns the command buttons for media items. */ public ImmutableList getCommandButtonsForMediaItems() { return commandButtonsForMediaItems; @@ -612,12 +651,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; getPlayerWrapper().getAvailableCommands()); checkStateNotNull(controller.getControllerCb()) .onPlayerInfoChanged( - seq, - playerInfo, - intersectedCommands, - excludeTimeline, - excludeTracks, - controller.getInterfaceVersion()); + seq, playerInfo, intersectedCommands, excludeTimeline, excludeTracks); } catch (DeadObjectException e) { onDeadObjectException(controller); } catch (RemoteException e) { @@ -679,6 +713,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; .setAvailableSessionCommands(playerWrapper.getAvailableSessionCommands()) .setAvailablePlayerCommands(playerWrapper.getAvailablePlayerCommands()) .setCustomLayout(playerWrapper.getCustomLayout()) + .setMediaButtonPreferences(playerWrapper.getMediaButtonPreferences()) .build(); } MediaSession.ConnectionResult connectionResult = @@ -691,6 +726,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; connectionResult.customLayout != null ? connectionResult.customLayout : instance.getCustomLayout()); + playerWrapper.setMediaButtonPreferences( + connectionResult.mediaButtonPreferences != null + ? connectionResult.mediaButtonPreferences + : instance.getMediaButtonPreferences()); setAvailableFrameworkControllerCommands( connectionResult.availableSessionCommands, connectionResult.availablePlayerCommands); } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index 1019e04351..662b0cb36b 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -1115,6 +1115,11 @@ import org.checkerframework.checker.initialization.qual.Initialized; updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper()); } + @Override + public void setMediaButtonPreferences(int seq, List mediaButtonPreferences) { + updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper()); + } + @Override public void onSessionExtrasChanged(int seq, Bundle sessionExtras) { sessionCompat.setExtras(sessionExtras); diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java index 5bdaebabe5..6756aa730e 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java @@ -761,7 +761,8 @@ public abstract class MediaSessionService extends Service { request.libraryVersion, request.controllerInterfaceVersion, isTrusted, - new MediaSessionStub.Controller2Cb(caller), + new MediaSessionStub.Controller2Cb( + caller, request.controllerInterfaceVersion), request.connectionHints, request.maxCommandsForMediaItems); 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 aa710084c1..7d3ab92733 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java @@ -535,6 +535,9 @@ import java.util.concurrent.ExecutionException; connectionResult.customLayout != null ? connectionResult.customLayout : sessionImpl.getCustomLayout(), + connectionResult.mediaButtonPreferences != null + ? connectionResult.mediaButtonPreferences + : sessionImpl.getMediaButtonPreferences(), sessionImpl.getCommandButtonsForMediaItems(), connectionResult.availableSessionCommands, connectionResult.availablePlayerCommands, @@ -636,7 +639,7 @@ import java.util.concurrent.ExecutionException; request.libraryVersion, request.controllerInterfaceVersion, sessionManager.isTrustedForMediaControl(remoteUserInfo), - new MediaSessionStub.Controller2Cb(caller), + new MediaSessionStub.Controller2Cb(caller, request.controllerInterfaceVersion), request.connectionHints, request.maxCommandsForMediaItems); connect(caller, controllerInfo); @@ -2006,9 +2009,11 @@ import java.util.concurrent.ExecutionException; /* package */ static final class Controller2Cb implements ControllerCb { private final IMediaController iController; + private final int controllerInterfaceVersion; - public Controller2Cb(IMediaController callback) { - iController = callback; + public Controller2Cb(IMediaController callback, int controllerInterfaceVersion) { + this.iController = callback; + this.controllerInterfaceVersion = controllerInterfaceVersion; } public IBinder getCallbackBinder() { @@ -2032,8 +2037,7 @@ import java.util.concurrent.ExecutionException; PlayerInfo playerInfo, Player.Commands availableCommands, boolean excludeTimeline, - boolean excludeTracks, - int controllerInterfaceVersion) + boolean excludeTracks) throws RemoteException { Assertions.checkState(controllerInterfaceVersion != 0); // The bundling exclusions merge the performance overrides with the available commands. @@ -2072,6 +2076,22 @@ import java.util.concurrent.ExecutionException; sequenceNumber, BundleCollectionUtil.toBundleList(layout, CommandButton::toBundle)); } + @Override + public void setMediaButtonPreferences( + int sequenceNumber, List mediaButtonPreferences) throws RemoteException { + if (controllerInterfaceVersion >= 7) { + iController.onSetMediaButtonPreferences( + sequenceNumber, + BundleCollectionUtil.toBundleList(mediaButtonPreferences, CommandButton::toBundle)); + } else { + // Controller doesn't support media button preferences, send the list as a custom layout. + // TODO: b/332877990 - More accurately reflect media button preferences as custom layout. + iController.onSetCustomLayout( + sequenceNumber, + BundleCollectionUtil.toBundleList(mediaButtonPreferences, CommandButton::toBundle)); + } + } + @Override public void onSessionActivityChanged(int sequenceNumber, PendingIntent sessionActivity) throws RemoteException { diff --git a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java index 71f0ad9cd4..c56f8f7971 100644 --- a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java +++ b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java @@ -87,6 +87,7 @@ import java.util.List; @Nullable private LegacyError legacyError; @Nullable private Bundle legacyExtras; private ImmutableList customLayout; + private ImmutableList mediaButtonPreferences; private SessionCommands availableSessionCommands; private Commands availablePlayerCommands; @@ -94,12 +95,14 @@ import java.util.List; Player player, boolean playIfSuppressed, ImmutableList customLayout, + ImmutableList mediaButtonPreferences, SessionCommands availableSessionCommands, Commands availablePlayerCommands, @Nullable Bundle legacyExtras) { super(player); this.playIfSuppressed = playIfSuppressed; this.customLayout = customLayout; + this.mediaButtonPreferences = mediaButtonPreferences; this.availableSessionCommands = availableSessionCommands; this.availablePlayerCommands = availablePlayerCommands; this.legacyExtras = legacyExtras; @@ -123,10 +126,18 @@ import java.util.List; this.customLayout = customLayout; } + public void setMediaButtonPreferences(ImmutableList mediaButtonPreferences) { + this.mediaButtonPreferences = mediaButtonPreferences; + } + /* package */ ImmutableList getCustomLayout() { return customLayout; } + /* package */ ImmutableList getMediaButtonPreferences() { + return mediaButtonPreferences; + } + public void setLegacyExtras(@Nullable Bundle extras) { if (extras != null) { checkArgument(!extras.containsKey(EXTRAS_KEY_PLAYBACK_SPEED_COMPAT)); @@ -1061,8 +1072,11 @@ import java.util.List; .setBufferedPosition(compatBufferedPosition) .setExtras(extras); - for (int i = 0; i < customLayout.size(); i++) { - CommandButton commandButton = customLayout.get(i); + // TODO: b/332877990 - More accurately reflect media button preferences as custom actions. + List buttonsForCustomActions = + mediaButtonPreferences.isEmpty() ? customLayout : mediaButtonPreferences; + for (int i = 0; i < buttonsForCustomActions.size(); i++) { + CommandButton commandButton = buttonsForCustomActions.get(i); SessionCommand sessionCommand = commandButton.sessionCommand; if (sessionCommand != null && commandButton.isEnabled diff --git a/libraries/session/src/test/java/androidx/media3/session/ConnectionStateTest.java b/libraries/session/src/test/java/androidx/media3/session/ConnectionStateTest.java new file mode 100644 index 0000000000..29fa2ed207 --- /dev/null +++ b/libraries/session/src/test/java/androidx/media3/session/ConnectionStateTest.java @@ -0,0 +1,140 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.session; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import androidx.media3.common.MediaLibraryInfo; +import androidx.media3.common.Player; +import androidx.media3.test.utils.TestExoPlayerBuilder; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableList; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link ConnectionState}. */ +@RunWith(AndroidJUnit4.class) +public class ConnectionStateTest { + + @Test + public void roundTripViaBundle_restoresEqualInstance() { + Context context = ApplicationProvider.getApplicationContext(); + Player player = new TestExoPlayerBuilder(context).build(); + MediaSession session = new MediaSession.Builder(context, player).build(); + Bundle tokenExtras = new Bundle(); + tokenExtras.putString("key", "token"); + Bundle sessionExtras = new Bundle(); + sessionExtras.putString("key", "session"); + ConnectionState connectionState = + new ConnectionState( + MediaLibraryInfo.VERSION_INT, + MediaSessionStub.VERSION_INT, + new MediaSessionStub(session.getImpl()), + /* sessionActivity= */ PendingIntent.getActivity( + context, /* requestCode= */ 0, new Intent(), /* flags= */ 0), + /* customLayout= */ ImmutableList.of( + new CommandButton.Builder(CommandButton.ICON_ARTIST) + .setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT) + .build()), + /* mediaButtonPreferences= */ ImmutableList.of( + new CommandButton.Builder(CommandButton.ICON_HEART_FILLED) + .setPlayerCommand(Player.COMMAND_PREPARE) + .build()), + /* commandButtonsForMediaItems= */ ImmutableList.of( + new CommandButton.Builder(CommandButton.ICON_NEXT) + .setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT) + .build()), + new SessionCommands.Builder().add(new SessionCommand("action", Bundle.EMPTY)).build(), + /* playerCommandsFromSession= */ new Player.Commands.Builder() + .add(Player.COMMAND_GET_AUDIO_ATTRIBUTES) + .build(), + /* playerCommandsFromPlayer= */ new Player.Commands.Builder() + .add(Player.COMMAND_CHANGE_MEDIA_ITEMS) + .build(), + tokenExtras, + sessionExtras, + PlayerInfo.DEFAULT.copyWithIsPlaying(true), + session.getPlatformToken()); + + ConnectionState restoredConnectionState = + ConnectionState.fromBundle( + connectionState.toBundleForRemoteProcess(MediaControllerStub.VERSION_INT)); + session.release(); + player.release(); + + assertThat(restoredConnectionState.libraryVersion).isEqualTo(connectionState.libraryVersion); + assertThat(restoredConnectionState.sessionInterfaceVersion) + .isEqualTo(connectionState.sessionInterfaceVersion); + assertThat(restoredConnectionState.sessionActivity).isEqualTo(connectionState.sessionActivity); + assertThat(restoredConnectionState.sessionBinder).isEqualTo(connectionState.sessionBinder); + assertThat(restoredConnectionState.customLayout).isEqualTo(connectionState.customLayout); + assertThat(restoredConnectionState.mediaButtonPreferences) + .isEqualTo(connectionState.mediaButtonPreferences); + assertThat(restoredConnectionState.commandButtonsForMediaItems) + .isEqualTo(connectionState.commandButtonsForMediaItems); + assertThat(restoredConnectionState.sessionCommands).isEqualTo(connectionState.sessionCommands); + assertThat(restoredConnectionState.playerCommandsFromSession) + .isEqualTo(connectionState.playerCommandsFromSession); + assertThat(restoredConnectionState.playerCommandsFromPlayer) + .isEqualTo(connectionState.playerCommandsFromPlayer); + assertThat(restoredConnectionState.tokenExtras.getString("key")).isEqualTo("token"); + assertThat(restoredConnectionState.sessionExtras.getString("key")).isEqualTo("session"); + assertThat(restoredConnectionState.playerInfo.isPlaying).isTrue(); + assertThat(restoredConnectionState.platformToken).isEqualTo(connectionState.platformToken); + } + + @Test + public void + roundTripViaBundle_controllerVersion6OrLower_usesMediaButtonPreferencesAsCustomLayout() { + Context context = ApplicationProvider.getApplicationContext(); + Player player = new TestExoPlayerBuilder(context).build(); + MediaSession session = new MediaSession.Builder(context, player).build(); + ConnectionState connectionState = + new ConnectionState( + MediaLibraryInfo.VERSION_INT, + MediaSessionStub.VERSION_INT, + new MediaSessionStub(session.getImpl()), + /* sessionActivity= */ null, + /* customLayout= */ ImmutableList.of(), + /* mediaButtonPreferences= */ ImmutableList.of( + new CommandButton.Builder(CommandButton.ICON_HEART_FILLED) + .setPlayerCommand(Player.COMMAND_PREPARE) + .build()), + /* commandButtonsForMediaItems= */ ImmutableList.of(), + SessionCommands.EMPTY, + /* playerCommandsFromSession= */ Player.Commands.EMPTY, + /* playerCommandsFromPlayer= */ Player.Commands.EMPTY, + /* tokenExtras= */ Bundle.EMPTY, + /* sessionExtras= */ Bundle.EMPTY, + PlayerInfo.DEFAULT, + session.getPlatformToken()); + + ConnectionState restoredConnectionState = + ConnectionState.fromBundle( + connectionState.toBundleForRemoteProcess(/* controllerInterfaceVersion= */ 6)); + session.release(); + player.release(); + + assertThat(restoredConnectionState.customLayout) + .isEqualTo(connectionState.mediaButtonPreferences); + assertThat(restoredConnectionState.mediaButtonPreferences).isEmpty(); + } +} diff --git a/libraries/session/src/test/java/androidx/media3/session/LegacyConversionsTest.java b/libraries/session/src/test/java/androidx/media3/session/LegacyConversionsTest.java index 2fca50659b..2bcacfb63d 100644 --- a/libraries/session/src/test/java/androidx/media3/session/LegacyConversionsTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/LegacyConversionsTest.java @@ -1080,12 +1080,12 @@ public final class LegacyConversionsTest { } @Test - public void convertToCustomLayout_withNull_returnsEmptyList() { - assertThat(LegacyConversions.convertToCustomLayout(null)).isEmpty(); + public void convertToMediaButtonPreferences_withNull_returnsEmptyList() { + assertThat(LegacyConversions.convertToMediaButtonPreferences(null)).isEmpty(); } @Test - public void convertToCustomLayout_withoutIconConstantInExtras() { + public void convertToMediaButtonPreferences_withoutIconConstantInExtras() { String extraKey = "key"; String extraValue = "value"; String actionStr = "action"; @@ -1107,7 +1107,7 @@ public final class LegacyConversionsTest { .addCustomAction(action) .build(); - ImmutableList buttons = LegacyConversions.convertToCustomLayout(state); + ImmutableList buttons = LegacyConversions.convertToMediaButtonPreferences(state); assertThat(buttons).hasSize(1); CommandButton button = buttons.get(0); @@ -1120,7 +1120,7 @@ public final class LegacyConversionsTest { } @Test - public void convertToCustomLayout_withIconConstantInExtras() { + public void convertToMediaButtonPreferences_withIconConstantInExtras() { String actionStr = "action"; String displayName = "display_name"; int iconRes = 21; @@ -1140,7 +1140,7 @@ public final class LegacyConversionsTest { .addCustomAction(action) .build(); - ImmutableList buttons = LegacyConversions.convertToCustomLayout(state); + ImmutableList buttons = LegacyConversions.convertToMediaButtonPreferences(state); assertThat(buttons).hasSize(1); CommandButton button = buttons.get(0); diff --git a/libraries/session/src/test/java/androidx/media3/session/MediaSessionServiceTest.java b/libraries/session/src/test/java/androidx/media3/session/MediaSessionServiceTest.java index 7ca5925bd9..9eaaa94ecd 100644 --- a/libraries/session/src/test/java/androidx/media3/session/MediaSessionServiceTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/MediaSessionServiceTest.java @@ -259,6 +259,112 @@ public class MediaSessionServiceTest { serviceController.destroy(); } + @Test + public void mediaNotificationController_setMediaButtonPreferences_correctNotificationActions() + throws TimeoutException { + SessionCommand command1 = new SessionCommand("command1", Bundle.EMPTY); + SessionCommand command2 = new SessionCommand("command2", Bundle.EMPTY); + SessionCommand command3 = new SessionCommand("command3", Bundle.EMPTY); + SessionCommand command4 = new SessionCommand("command4", Bundle.EMPTY); + CommandButton button1 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("customAction1") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(command1) + .build(); + CommandButton button2 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("customAction2") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(command2) + .build(); + CommandButton button3 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("customAction3") + .setEnabled(false) + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(command3) + .build(); + CommandButton button4 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("customAction4") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(command4) + .build(); + ExoPlayer player = new TestExoPlayerBuilder(context).build(); + MediaSession session = + new MediaSession.Builder(context, player) + .setMediaButtonPreferences(ImmutableList.of(button1, button2, button3, button4)) + .setCallback( + new MediaSession.Callback() { + @Override + public MediaSession.ConnectionResult onConnect( + MediaSession session, MediaSession.ControllerInfo controller) { + if (session.isMediaNotificationController(controller)) { + return new MediaSession.ConnectionResult.AcceptedResultBuilder(session) + .setAvailableSessionCommands( + MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS + .buildUpon() + .add(command1) + .add(command2) + .add(command3) + .build()) + .build(); + } + return new MediaSession.ConnectionResult.AcceptedResultBuilder(session).build(); + } + }) + .build(); + ServiceController serviceController = Robolectric.buildService(TestService.class); + TestService service = serviceController.create().get(); + service.setMediaNotificationProvider( + new DefaultMediaNotificationProvider( + service, + mediaSession -> 2000, + DefaultMediaNotificationProvider.DEFAULT_CHANNEL_ID, + DefaultMediaNotificationProvider.DEFAULT_CHANNEL_NAME_RESOURCE_ID)); + service.addSession(session); + // Play media to create a notification. + player.setMediaItems( + ImmutableList.of( + MediaItem.fromUri("asset:///media/mp4/sample.mp4"), + MediaItem.fromUri("asset:///media/mp4/sample.mp4"))); + player.prepare(); + player.play(); + runMainLooperUntil(() -> notificationManager.getActiveNotifications().length == 1); + + StatusBarNotification mediaNotification = getStatusBarNotification(2000); + + assertThat(mediaNotification.getNotification().actions).hasLength(5); + assertThat(mediaNotification.getNotification().actions[0].title.toString()) + .isEqualTo("Seek to previous item"); + assertThat(mediaNotification.getNotification().actions[1].title.toString()).isEqualTo("Pause"); + assertThat(mediaNotification.getNotification().actions[2].title.toString()) + .isEqualTo("Seek to next item"); + assertThat(mediaNotification.getNotification().actions[3].title.toString()) + .isEqualTo("customAction1"); + assertThat(mediaNotification.getNotification().actions[4].title.toString()) + .isEqualTo("customAction2"); + + player.pause(); + session.setMediaButtonPreferences( + session.getMediaNotificationControllerInfo(), ImmutableList.of(button2)); + ShadowLooper.idleMainLooper(); + mediaNotification = getStatusBarNotification(2000); + + assertThat(mediaNotification.getNotification().actions).hasLength(4); + assertThat(mediaNotification.getNotification().actions[0].title.toString()) + .isEqualTo("Seek to previous item"); + assertThat(mediaNotification.getNotification().actions[1].title.toString()).isEqualTo("Play"); + assertThat(mediaNotification.getNotification().actions[2].title.toString()) + .isEqualTo("Seek to next item"); + assertThat(mediaNotification.getNotification().actions[3].title.toString()) + .isEqualTo("customAction2"); + session.release(); + player.release(); + serviceController.destroy(); + } + @Test public void mediaNotificationController_setAvailableCommands_correctNotificationActions() throws TimeoutException { @@ -281,7 +387,7 @@ public class MediaSessionServiceTest { MediaSession session = new MediaSession.Builder(context, player) .setId("1") - .setCustomLayout(ImmutableList.of(button1, button2)) + .setMediaButtonPreferences(ImmutableList.of(button1, button2)) .setCallback( new MediaSession.Callback() { @Override diff --git a/libraries/session/src/test/java/androidx/media3/session/PlayerWrapperTest.java b/libraries/session/src/test/java/androidx/media3/session/PlayerWrapperTest.java index 9790fdf7d7..e0016dc76c 100644 --- a/libraries/session/src/test/java/androidx/media3/session/PlayerWrapperTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/PlayerWrapperTest.java @@ -47,7 +47,8 @@ public class PlayerWrapperTest { new PlayerWrapper( player, /* playIfSuppressed= */ true, - ImmutableList.of(), + /* customLayout= */ ImmutableList.of(), + /* mediaButtonPreferences= */ ImmutableList.of(), SessionCommands.EMPTY, Player.Commands.EMPTY, /* legacyExtras= */ null); diff --git a/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaController.aidl b/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaController.aidl index d74b940323..ca42a686e2 100644 --- a/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaController.aidl +++ b/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaController.aidl @@ -31,6 +31,7 @@ interface IRemoteMediaController { Bundle getConnectedSessionToken(String controllerId); Bundle getSessionExtras(String controllerId); Bundle getCustomLayout(String controllerId); + Bundle getMediaButtonPreferences(String controllerId); Bundle getAvailableCommands(String controllerId); PendingIntent getSessionActivity(String controllerId); void play(String controllerId); diff --git a/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaSession.aidl b/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaSession.aidl index 115e19e91a..437d53c709 100644 --- a/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaSession.aidl +++ b/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaSession.aidl @@ -32,6 +32,7 @@ interface IRemoteMediaSession { void release(String sessionId); void setAvailableCommands(String sessionId, in Bundle sessionCommands, in Bundle playerCommands); void setCustomLayout(String sessionId, in List layout); + void setMediaButtonPreferences(String sessionId, in List mediaButtonPreferences); void setSessionExtras(String sessionId, in Bundle extras); void setSessionExtrasForController(String sessionId, in String controllerKey, in Bundle extras); void sendError(String sessionId, String controllerKey, in Bundle SessionError); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest.java index 9282076b89..6b007ba218 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest.java @@ -1461,7 +1461,11 @@ public class MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest .build(); } }; - MediaSession mediaSession = createMediaSession(player, callback, customLayout); + MediaSession mediaSession = + new MediaSession.Builder(ApplicationProvider.getApplicationContext(), player) + .setCallback(callback) + .setCustomLayout(customLayout) + .build(); connectMediaNotificationController(mediaSession); MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); @@ -1621,6 +1625,212 @@ public class MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest releasePlayer(player); } + @Test + public void + playerWithMediaButtonPreferences_sessionBuiltWithMediaButtonPreferences_customActionsInInitialPlaybackState() + throws Exception { + Player player = createDefaultPlayer(); + Bundle extras1 = new Bundle(); + extras1.putString("key1", "value1"); + SessionCommand command1 = new SessionCommand("command1", extras1); + SessionCommand command2 = new SessionCommand("command2", Bundle.EMPTY); + SessionCommand command3 = new SessionCommand("command3", Bundle.EMPTY); + ImmutableList mediaButtonPreferences = + ImmutableList.of( + new CommandButton.Builder(CommandButton.ICON_PLAY) + .setDisplayName("button1") + .setSessionCommand(command1) + .build(), + new CommandButton.Builder(CommandButton.ICON_PAUSE) + .setDisplayName("button2") + .setSessionCommand(command2) + .build(), + new CommandButton.Builder(CommandButton.ICON_PAUSE) + .setDisplayName("button3") + .setEnabled(false) + .setSessionCommand(command3) + .build()); + MediaSession.Callback callback = + new MediaSession.Callback() { + @Override + public ConnectionResult onConnect( + MediaSession session, MediaSession.ControllerInfo controller) { + return new AcceptedResultBuilder(session) + .setAvailableSessionCommands( + ConnectionResult.DEFAULT_SESSION_COMMANDS + .buildUpon() + .add(command1) + .add(command3) + .build()) + .build(); + } + }; + MediaSession mediaSession = + new MediaSession.Builder(ApplicationProvider.getApplicationContext(), player) + .setCallback(callback) + .setMediaButtonPreferences(mediaButtonPreferences) + .build(); + connectMediaNotificationController(mediaSession); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + assertThat(controllerCompat.getPlaybackState().getCustomActions()).hasSize(1); + PlaybackStateCompat.CustomAction customAction = + controllerCompat.getPlaybackState().getCustomActions().get(0); + assertThat(customAction.getAction()).isEqualTo("command1"); + assertThat(customAction.getName().toString()).isEqualTo("button1"); + assertThat(customAction.getIcon()).isEqualTo(R.drawable.media3_icon_play); + assertThat(customAction.getExtras().get("key1")).isEqualTo("value1"); + assertThat(customAction.getExtras().get(MediaConstants.EXTRAS_KEY_COMMAND_BUTTON_ICON_COMPAT)) + .isEqualTo(CommandButton.ICON_PLAY); + mediaSession.release(); + releasePlayer(player); + } + + @Test + public void + playerWithMediaButtonPreferences_setMediaButtonPreferences_playbackStateChangedWithCustomActionsChanged() + throws Exception { + Player player = createDefaultPlayer(); + Bundle extras1 = new Bundle(); + extras1.putString("key1", "value1"); + Bundle extras2 = new Bundle(); + extras1.putString("key2", "value2"); + SessionCommand command1 = new SessionCommand("command1", extras1); + SessionCommand command2 = new SessionCommand("command2", extras2); + ImmutableList mediaButtonPreferences = + ImmutableList.of( + new CommandButton.Builder(CommandButton.ICON_PLAY) + .setDisplayName("button1") + .setSessionCommand(command1) + .build(), + new CommandButton.Builder(CommandButton.ICON_PAUSE) + .setDisplayName("button2") + .setSessionCommand(command2) + .build()); + MediaSession.Callback callback = + new MediaSession.Callback() { + @Override + public ConnectionResult onConnect( + MediaSession session, MediaSession.ControllerInfo controller) { + return new ConnectionResult.AcceptedResultBuilder(session) + .setAvailableSessionCommands( + ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().add(command1).build()) + .build(); + } + }; + MediaSession mediaSession = createMediaSession(player, callback); + connectMediaNotificationController(mediaSession); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + List initialCustomActions = + controllerCompat.getPlaybackState().getCustomActions(); + AtomicReference> reportedCustomActions = + new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + controllerCompat.registerCallback( + new MediaControllerCompat.Callback() { + @Override + public void onPlaybackStateChanged(PlaybackStateCompat state) { + reportedCustomActions.set(state.getCustomActions()); + latch.countDown(); + } + }, + threadTestRule.getHandler()); + + getInstrumentation() + .runOnMainSync(() -> mediaSession.setMediaButtonPreferences(mediaButtonPreferences)); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(initialCustomActions).isEmpty(); + assertThat(reportedCustomActions.get()).hasSize(1); + assertThat(reportedCustomActions.get().get(0).getAction()).isEqualTo("command1"); + assertThat(reportedCustomActions.get().get(0).getName().toString()).isEqualTo("button1"); + assertThat(reportedCustomActions.get().get(0).getIcon()).isEqualTo(R.drawable.media3_icon_play); + assertThat(reportedCustomActions.get().get(0).getExtras().get("key1")).isEqualTo("value1"); + assertThat( + reportedCustomActions + .get() + .get(0) + .getExtras() + .get(MediaConstants.EXTRAS_KEY_COMMAND_BUTTON_ICON_COMPAT)) + .isEqualTo(CommandButton.ICON_PLAY); + mediaSession.release(); + releasePlayer(player); + } + + @Test + public void + playerWithMediaButtonPreferences_setMediaButtonPreferencesForMediaNotificationController_playbackStateChangedWithCustomActionsChanged() + throws Exception { + Player player = createDefaultPlayer(); + Bundle extras1 = new Bundle(); + extras1.putString("key1", "value1"); + Bundle extras2 = new Bundle(); + extras1.putString("key2", "value2"); + SessionCommand command1 = new SessionCommand("command1", extras1); + SessionCommand command2 = new SessionCommand("command2", extras2); + ImmutableList mediaButtonPreferences = + ImmutableList.of( + new CommandButton.Builder(CommandButton.ICON_PLAY) + .setDisplayName("button1") + .setSessionCommand(command1) + .build(), + new CommandButton.Builder(CommandButton.ICON_PAUSE) + .setDisplayName("button2") + .setSessionCommand(command2) + .build()); + MediaSession.Callback callback = + new MediaSession.Callback() { + @Override + public ConnectionResult onConnect( + MediaSession session, MediaSession.ControllerInfo controller) { + return new ConnectionResult.AcceptedResultBuilder(session) + .setAvailableSessionCommands( + ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().add(command1).build()) + .build(); + } + }; + MediaSession mediaSession = createMediaSession(player, callback); + connectMediaNotificationController(mediaSession); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + List initialCustomActions = + controllerCompat.getPlaybackState().getCustomActions(); + AtomicReference> reportedCustomActions = + new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + controllerCompat.registerCallback( + new MediaControllerCompat.Callback() { + @Override + public void onPlaybackStateChanged(PlaybackStateCompat state) { + reportedCustomActions.set(state.getCustomActions()); + latch.countDown(); + } + }, + threadTestRule.getHandler()); + + getInstrumentation() + .runOnMainSync( + () -> + mediaSession.setMediaButtonPreferences( + mediaSession.getMediaNotificationControllerInfo(), mediaButtonPreferences)); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(initialCustomActions).isEmpty(); + assertThat(reportedCustomActions.get()).hasSize(1); + assertThat(reportedCustomActions.get().get(0).getAction()).isEqualTo("command1"); + assertThat(reportedCustomActions.get().get(0).getName().toString()).isEqualTo("button1"); + assertThat(reportedCustomActions.get().get(0).getIcon()).isEqualTo(R.drawable.media3_icon_play); + assertThat(reportedCustomActions.get().get(0).getExtras().get("key1")).isEqualTo("value1"); + assertThat( + reportedCustomActions + .get() + .get(0) + .getExtras() + .get(MediaConstants.EXTRAS_KEY_COMMAND_BUTTON_ICON_COMPAT)) + .isEqualTo(CommandButton.ICON_PLAY); + mediaSession.release(); + releasePlayer(player); + } + /** * Connect a controller that mimics the media notification controller that is connected by {@link * MediaNotificationManager} when the session is running in the service. @@ -1671,14 +1881,8 @@ public class MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest private static MediaSession createMediaSession( Player player, @Nullable MediaSession.Callback callback) { - return createMediaSession(player, callback, /* customLayout= */ ImmutableList.of()); - } - - private static MediaSession createMediaSession( - Player player, @Nullable MediaSession.Callback callback, List customLayout) { MediaSession.Builder session = - new MediaSession.Builder(ApplicationProvider.getApplicationContext(), player) - .setCustomLayout(customLayout); + new MediaSession.Builder(ApplicationProvider.getApplicationContext(), player); if (callback != null) { session.setCallback(callback); } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerWithMediaSessionCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerWithMediaSessionCompatTest.java index faf37a909c..1608360617 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerWithMediaSessionCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerWithMediaSessionCompatTest.java @@ -552,6 +552,74 @@ public class MediaControllerListenerWithMediaSessionCompatTest { .inOrder(); } + @Test + public void getMediaButtonPreferences() throws Exception { + CommandButton button1 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button1") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command1", Bundle.EMPTY)) + .build(); + CommandButton button2 = + new CommandButton.Builder(CommandButton.ICON_FAST_FORWARD) + .setDisplayName("button2") + .setSessionCommand(new SessionCommand("command2", Bundle.EMPTY)) + .build(); + ConditionVariable onMediaButtonPreferencesChangedCalled = new ConditionVariable(); + List> onMediaButtonPreferencesChangedArguments = new ArrayList<>(); + List> mediaButtonPreferencesFromGetter = new ArrayList<>(); + controllerTestRule.createController( + session.getSessionToken(), + new MediaController.Listener() { + @Override + public void onMediaButtonPreferencesChanged( + MediaController controller, List mediaButtonPreferences) { + onMediaButtonPreferencesChangedArguments.add(mediaButtonPreferences); + mediaButtonPreferencesFromGetter.add(controller.getMediaButtonPreferences()); + onMediaButtonPreferencesChangedCalled.open(); + } + }); + Bundle extras1 = new Bundle(); + extras1.putString("key", "value-1"); + PlaybackStateCompat.CustomAction customAction1 = + new PlaybackStateCompat.CustomAction.Builder( + "command1", "button1", /* icon= */ R.drawable.media3_notification_small_icon) + .setExtras(extras1) + .build(); + Bundle extras2 = new Bundle(); + extras2.putString("key", "value-2"); + extras2.putInt( + MediaConstants.EXTRAS_KEY_COMMAND_BUTTON_ICON_COMPAT, CommandButton.ICON_FAST_FORWARD); + PlaybackStateCompat.CustomAction customAction2 = + new PlaybackStateCompat.CustomAction.Builder( + "command2", "button2", /* icon= */ R.drawable.media3_icon_fast_forward) + .setExtras(extras2) + .build(); + PlaybackStateCompat.Builder playbackState1 = + new PlaybackStateCompat.Builder() + .addCustomAction(customAction1) + .addCustomAction(customAction2); + PlaybackStateCompat.Builder playbackState2 = + new PlaybackStateCompat.Builder().addCustomAction(customAction1); + + session.setPlaybackState(playbackState1.build()); + assertThat(onMediaButtonPreferencesChangedCalled.block(TIMEOUT_MS)).isTrue(); + onMediaButtonPreferencesChangedCalled.close(); + session.setPlaybackState(playbackState2.build()); + assertThat(onMediaButtonPreferencesChangedCalled.block(TIMEOUT_MS)).isTrue(); + + ImmutableList expectedFirstMediaButtonPreferences = + ImmutableList.of(button1.copyWithIsEnabled(true), button2.copyWithIsEnabled(true)); + ImmutableList expectedSecondMediaButtonPreferences = + ImmutableList.of(button1.copyWithIsEnabled(true)); + assertThat(onMediaButtonPreferencesChangedArguments) + .containsExactly(expectedFirstMediaButtonPreferences, expectedSecondMediaButtonPreferences) + .inOrder(); + assertThat(mediaButtonPreferencesFromGetter) + .containsExactly(expectedFirstMediaButtonPreferences, expectedSecondMediaButtonPreferences) + .inOrder(); + } + @Test public void getCurrentPosition_unknownPlaybackPosition_convertedToZero() throws Exception { session.setPlaybackState( diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java index d842e7a33b..2b474bf657 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java @@ -405,6 +405,7 @@ public class MediaControllerTest { button3.copyWithIsEnabled(false), button4.copyWithIsEnabled(true)) .inOrder(); + session.cleanUp(); } @Test @@ -452,6 +453,7 @@ public class MediaControllerTest { assertThat(getterCustomLayouts).hasSize(2); assertThat(getterCustomLayouts.get(0)).containsExactly(button.copyWithIsEnabled(false)); assertThat(getterCustomLayouts.get(1)).containsExactly(button.copyWithIsEnabled(true)); + session.cleanUp(); } @Test @@ -550,6 +552,369 @@ public class MediaControllerTest { session.cleanUp(); } + @Test + public void getMediaButtonPreferences_mediaButtonPreferencesBuiltWithSession_includedOnConnect() + throws Exception { + RemoteMediaSession session = + createRemoteMediaSession(TEST_GET_CUSTOM_LAYOUT, /* tokenExtras= */ null); + CommandButton button1 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button1") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command1", Bundle.EMPTY)) + .build(); + CommandButton button2 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button2") + .setEnabled(false) + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command2", Bundle.EMPTY)) + .build(); + CommandButton button3 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button3") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command3", Bundle.EMPTY)) + .build(); + CommandButton button4 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button4") + .setIconResId(R.drawable.media3_notification_small_icon) + .setPlayerCommand(Player.COMMAND_PLAY_PAUSE) + .build(); + CommandButton button5 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button5") + .setIconResId(R.drawable.media3_notification_small_icon) + .setPlayerCommand(Player.COMMAND_GET_TRACKS) + .build(); + setupMediaButtonPreferences( + session, ImmutableList.of(button1, button2, button3, button4, button5)); + MediaController controller = controllerTestRule.createController(session.getToken()); + + assertThat(threadTestRule.getHandler().postAndSync(controller::getMediaButtonPreferences)) + .containsExactly( + button1.copyWithIsEnabled(true), + button2.copyWithIsEnabled(false), + button3.copyWithIsEnabled(false), + button4.copyWithIsEnabled(true), + button5.copyWithIsEnabled(false)) + .inOrder(); + + session.cleanUp(); + } + + @Test + public void + getMediaButtonPreferences_sessionSetMediaButtonPreferences_mediaButtonPreferencesChanged() + throws Exception { + RemoteMediaSession session = + createRemoteMediaSession(TEST_GET_CUSTOM_LAYOUT, /* tokenExtras= */ null); + CommandButton button1 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button1") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command1", Bundle.EMPTY)) + .build(); + CommandButton button2 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button2") + .setEnabled(false) + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command2", Bundle.EMPTY)) + .build(); + CommandButton button3 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button3") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command3", Bundle.EMPTY)) + .build(); + CommandButton button4 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button4") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command4", Bundle.EMPTY)) + .build(); + CommandButton button5 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button5") + .setIconResId(R.drawable.media3_notification_small_icon) + .setPlayerCommand(Player.COMMAND_PLAY_PAUSE) + .build(); + CommandButton button6 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button6") + .setIconResId(R.drawable.media3_notification_small_icon) + .setPlayerCommand(Player.COMMAND_GET_TRACKS) + .build(); + setupMediaButtonPreferences(session, ImmutableList.of(button1, button3)); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference> reportedMediaButtonPreferences = new AtomicReference<>(); + MediaController controller = + controllerTestRule.createController( + session.getToken(), + Bundle.EMPTY, + new MediaController.Listener() { + @Override + public void onMediaButtonPreferencesChanged( + MediaController controller1, List layout) { + reportedMediaButtonPreferences.set(layout); + latch.countDown(); + } + }); + ImmutableList initialMediaButtonPreferencesFromGetter = + threadTestRule.getHandler().postAndSync(controller::getMediaButtonPreferences); + session.setMediaButtonPreferences( + ImmutableList.of(button1, button2, button4, button5, button6)); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + + ImmutableList newMediaButtonPreferencesFromGetter = + threadTestRule.getHandler().postAndSync(controller::getMediaButtonPreferences); + + assertThat(initialMediaButtonPreferencesFromGetter) + .containsExactly(button1.copyWithIsEnabled(true), button3.copyWithIsEnabled(false)) + .inOrder(); + ImmutableList expectedNewButtons = + ImmutableList.of( + button1.copyWithIsEnabled(true), + button2.copyWithIsEnabled(false), + button4.copyWithIsEnabled(false), + button5.copyWithIsEnabled(true), + button6.copyWithIsEnabled(false)); + assertThat(newMediaButtonPreferencesFromGetter) + .containsExactlyElementsIn(expectedNewButtons) + .inOrder(); + assertThat(reportedMediaButtonPreferences.get()) + .containsExactlyElementsIn(expectedNewButtons) + .inOrder(); + session.cleanUp(); + } + + @Test + public void + getMediaButtonPreferences_setAvailableCommandsOnSession_reportsMediaButtonPreferencesChanged() + throws Exception { + RemoteMediaSession session = createRemoteMediaSession(TEST_GET_CUSTOM_LAYOUT, null); + CommandButton button1 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button1") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command1", Bundle.EMPTY)) + .build(); + CommandButton button2 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button2") + .setEnabled(false) + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command2", Bundle.EMPTY)) + .build(); + CommandButton button3 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button3") + .setIconResId(R.drawable.media3_notification_small_icon) + .setPlayerCommand(Player.COMMAND_PLAY_PAUSE) + .build(); + CommandButton button4 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button4") + .setIconResId(R.drawable.media3_notification_small_icon) + .setPlayerCommand(Player.COMMAND_GET_TRACKS) + .build(); + setupMediaButtonPreferences(session, ImmutableList.of(button1, button2, button3, button4)); + CountDownLatch latch = new CountDownLatch(2); + List> reportedMediaButtonPreferencesChanged = new ArrayList<>(); + List> getterMediaButtonPreferencesChanged = new ArrayList<>(); + MediaController.Listener listener = + new MediaController.Listener() { + @Override + public void onMediaButtonPreferencesChanged( + MediaController controller, List layout) { + reportedMediaButtonPreferencesChanged.add(layout); + getterMediaButtonPreferencesChanged.add(controller.getMediaButtonPreferences()); + latch.countDown(); + } + }; + MediaController controller = + controllerTestRule.createController( + session.getToken(), /* connectionHints= */ Bundle.EMPTY, listener); + ImmutableList initialMediaButtonPreferences = + threadTestRule.getHandler().postAndSync(controller::getMediaButtonPreferences); + + // Remove commands in custom layout from available commands. + session.setAvailableCommands(SessionCommands.EMPTY, Player.Commands.EMPTY); + // Add one sesion and player command back. + session.setAvailableCommands( + new SessionCommands.Builder().add(button2.sessionCommand).build(), + new Player.Commands.Builder().add(Player.COMMAND_GET_TRACKS).build()); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(initialMediaButtonPreferences) + .containsExactly( + button1.copyWithIsEnabled(true), + button2.copyWithIsEnabled(false), + button3.copyWithIsEnabled(true), + button4.copyWithIsEnabled(false)); + assertThat(reportedMediaButtonPreferencesChanged).hasSize(2); + assertThat(reportedMediaButtonPreferencesChanged.get(0)) + .containsExactly( + button1.copyWithIsEnabled(false), + button2.copyWithIsEnabled(false), + button3.copyWithIsEnabled(false), + button4.copyWithIsEnabled(false)) + .inOrder(); + assertThat(reportedMediaButtonPreferencesChanged.get(1)) + .containsExactly( + button1.copyWithIsEnabled(false), + button2.copyWithIsEnabled(false), + button3.copyWithIsEnabled(false), + button4.copyWithIsEnabled(true)) + .inOrder(); + assertThat(getterMediaButtonPreferencesChanged).hasSize(2); + assertThat(getterMediaButtonPreferencesChanged.get(0)) + .containsExactly( + button1.copyWithIsEnabled(false), + button2.copyWithIsEnabled(false), + button3.copyWithIsEnabled(false), + button4.copyWithIsEnabled(false)) + .inOrder(); + assertThat(getterMediaButtonPreferencesChanged.get(1)) + .containsExactly( + button1.copyWithIsEnabled(false), + button2.copyWithIsEnabled(false), + button3.copyWithIsEnabled(false), + button4.copyWithIsEnabled(true)) + .inOrder(); + session.cleanUp(); + } + + @Test + public void + getMediaButtonPreferences_setAvailableCommandsOnPlayer_reportsMediaButtonPreferencesChanged() + throws Exception { + RemoteMediaSession session = createRemoteMediaSession(TEST_GET_CUSTOM_LAYOUT, null); + CommandButton button = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button") + .setIconResId(R.drawable.media3_notification_small_icon) + .setPlayerCommand(Player.COMMAND_PLAY_PAUSE) + .build(); + setupMediaButtonPreferences(session, ImmutableList.of(button)); + CountDownLatch latch = new CountDownLatch(2); + List> reportedMediaButtonPreferences = new ArrayList<>(); + List> getterMediaButtonPreferences = new ArrayList<>(); + MediaController.Listener listener = + new MediaController.Listener() { + @Override + public void onMediaButtonPreferencesChanged( + MediaController controller, List layout) { + reportedMediaButtonPreferences.add(layout); + getterMediaButtonPreferences.add(controller.getMediaButtonPreferences()); + latch.countDown(); + } + }; + MediaController controller = + controllerTestRule.createController( + session.getToken(), /* connectionHints= */ Bundle.EMPTY, listener); + ImmutableList initialMediaButtonPreferences = + threadTestRule.getHandler().postAndSync(controller::getMediaButtonPreferences); + + // Disable player command and then add it back. + session.getMockPlayer().notifyAvailableCommandsChanged(Player.Commands.EMPTY); + session + .getMockPlayer() + .notifyAvailableCommandsChanged( + new Player.Commands.Builder().add(Player.COMMAND_PLAY_PAUSE).build()); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(initialMediaButtonPreferences).containsExactly(button.copyWithIsEnabled(true)); + assertThat(reportedMediaButtonPreferences).hasSize(2); + assertThat(reportedMediaButtonPreferences.get(0)) + .containsExactly(button.copyWithIsEnabled(false)); + assertThat(reportedMediaButtonPreferences.get(1)) + .containsExactly(button.copyWithIsEnabled(true)); + assertThat(getterMediaButtonPreferences).hasSize(2); + assertThat(getterMediaButtonPreferences.get(0)) + .containsExactly(button.copyWithIsEnabled(false)); + assertThat(getterMediaButtonPreferences.get(1)).containsExactly(button.copyWithIsEnabled(true)); + session.cleanUp(); + } + + @Test + public void + getMediaButtonPreferences_sessionSetMediaButtonPreferencesNoChange_listenerNotCalledWithEqualPreferences() + throws Exception { + RemoteMediaSession session = + createRemoteMediaSession(TEST_GET_CUSTOM_LAYOUT, /* tokenExtras= */ null); + CommandButton button1 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button1") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command1", Bundle.EMPTY)) + .build(); + CommandButton button2 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button2") + .setEnabled(false) + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command2", Bundle.EMPTY)) + .build(); + CommandButton button3 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button3") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command3", Bundle.EMPTY)) + .build(); + CommandButton button4 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button4") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command4", Bundle.EMPTY)) + .build(); + setupMediaButtonPreferences(session, ImmutableList.of(button1, button2)); + CountDownLatch latch = new CountDownLatch(2); + List> reportedMediaButtonPreferences = new ArrayList<>(); + List> getterMediaButtonPreferences = new ArrayList<>(); + MediaController.Listener listener = + new MediaController.Listener() { + @Override + public void onMediaButtonPreferencesChanged( + MediaController controller, List layout) { + reportedMediaButtonPreferences.add(layout); + getterMediaButtonPreferences.add(controller.getMediaButtonPreferences()); + latch.countDown(); + } + }; + MediaController controller = + controllerTestRule.createController(session.getToken(), Bundle.EMPTY, listener); + ImmutableList initialMediaButtonPreferences = + threadTestRule.getHandler().postAndSync(controller::getMediaButtonPreferences); + + // First call does not trigger onMediaButtonPreferencesChanged. + session.setMediaButtonPreferences(ImmutableList.of(button1, button2)); + session.setMediaButtonPreferences(ImmutableList.of(button3, button4)); + session.setMediaButtonPreferences(ImmutableList.of(button1, button2)); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + CommandButton button1Enabled = button1.copyWithIsEnabled(true); + CommandButton button2Disabled = button2.copyWithIsEnabled(false); + CommandButton button3Disabled = button3.copyWithIsEnabled(false); + CommandButton button4Disabled = button4.copyWithIsEnabled(false); + assertThat(initialMediaButtonPreferences) + .containsExactly(button1Enabled, button2Disabled) + .inOrder(); + assertThat(reportedMediaButtonPreferences) + .containsExactly( + ImmutableList.of(button3Disabled, button4Disabled), + ImmutableList.of(button1Enabled, button2Disabled)) + .inOrder(); + assertThat(getterMediaButtonPreferences) + .containsExactly( + ImmutableList.of(button3Disabled, button4Disabled), + ImmutableList.of(button1Enabled, button2Disabled)) + .inOrder(); + session.cleanUp(); + } + @Test public void getCommandButtonsForMediaItem() throws Exception { RemoteMediaSession session = @@ -2035,4 +2400,22 @@ public class MediaControllerTest { session.setCustomLayout(ImmutableList.copyOf(customLayout)); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); } + + private void setupMediaButtonPreferences( + RemoteMediaSession session, List mediaButtonPreferences) + throws RemoteException, InterruptedException, Exception { + CountDownLatch latch = new CountDownLatch(1); + controllerTestRule.createController( + session.getToken(), + /* connectionHints= */ null, + new MediaController.Listener() { + @Override + public void onMediaButtonPreferencesChanged( + MediaController controller, List layout) { + latch.countDown(); + } + }); + session.setMediaButtonPreferences(ImmutableList.copyOf(mediaButtonPreferences)); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java index fcd4f9418f..daf81cdb6b 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java @@ -154,7 +154,7 @@ public class MediaSessionCallbackTest { } @Test - public void onConnect_acceptWithMissingSessionCommand_buttonDisabledAndPermissionDenied() + public void onConnect_setCustomLayoutWithMissingSessionCommand_buttonDisabledAndPermissionDenied() throws Exception { CommandButton button1 = new CommandButton.Builder(CommandButton.ICON_PLAY) @@ -169,7 +169,6 @@ public class MediaSessionCallbackTest { .setSessionCommand(new SessionCommand("command2", Bundle.EMPTY)) .setEnabled(true) .build(); - ImmutableList customLayout = ImmutableList.of(button1, button2); MediaSession.Callback callback = new MediaSession.Callback() { @Override @@ -193,12 +192,7 @@ public class MediaSessionCallbackTest { }; MediaSession session = sessionTestRule.ensureReleaseAfterTest( - new MediaSession.Builder(context, player) - .setCallback(callback) - .setCustomLayout(customLayout) - .setId( - "onConnect_acceptWithMissingSessionCommand_buttonDisabledAndPermissionDenied") - .build()); + new MediaSession.Builder(context, player).setCallback(callback).build()); RemoteMediaController remoteController = remoteControllerTestRule.createRemoteController(session.getToken()); @@ -211,6 +205,60 @@ public class MediaSessionCallbackTest { .isEqualTo(RESULT_SUCCESS); } + @Test + public void + onConnect_setMediaButtonPreferencesWithMissingSessionCommand_buttonDisabledAndPermissionDenied() + throws Exception { + CommandButton button1 = + new CommandButton.Builder(CommandButton.ICON_PLAY) + .setDisplayName("button1") + .setSessionCommand(new SessionCommand("command1", Bundle.EMPTY)) + .setEnabled(true) + .build(); + CommandButton button1Disabled = button1.copyWithIsEnabled(false); + CommandButton button2 = + new CommandButton.Builder(CommandButton.ICON_PAUSE) + .setDisplayName("button2") + .setSessionCommand(new SessionCommand("command2", Bundle.EMPTY)) + .setEnabled(true) + .build(); + MediaSession.Callback callback = + new MediaSession.Callback() { + @Override + public MediaSession.ConnectionResult onConnect( + MediaSession session, ControllerInfo controller) { + return new AcceptedResultBuilder(session) + .setAvailableSessionCommands( + new SessionCommands.Builder().add(button2.sessionCommand).build()) + .setMediaButtonPreferences(ImmutableList.of(button1, button2)) + .build(); + } + + @Override + public ListenableFuture onCustomCommand( + MediaSession session, + ControllerInfo controller, + SessionCommand customCommand, + Bundle args) { + return Futures.immediateFuture(new SessionResult(RESULT_SUCCESS)); + } + }; + MediaSession session = + sessionTestRule.ensureReleaseAfterTest( + new MediaSession.Builder(context, player).setCallback(callback).build()); + RemoteMediaController remoteController = + remoteControllerTestRule.createRemoteController(session.getToken()); + + ImmutableList mediaButtonPreferences = + remoteController.getMediaButtonPreferences(); + + assertThat(mediaButtonPreferences).containsExactly(button1Disabled, button2).inOrder(); + assertThat(remoteController.sendCustomCommand(button1.sessionCommand, Bundle.EMPTY).resultCode) + .isEqualTo(ERROR_PERMISSION_DENIED); + assertThat(remoteController.sendCustomCommand(button2.sessionCommand, Bundle.EMPTY).resultCode) + .isEqualTo(RESULT_SUCCESS); + } + @Test public void onConnect_emptyPlayerCommands_commandReleaseAlwaysIncluded() throws Exception { MediaSession.Callback callback = diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionServiceTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionServiceTest.java index 586ec6fb89..b672e1f238 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionServiceTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionServiceTest.java @@ -28,6 +28,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; import androidx.media3.common.ForwardingPlayer; import androidx.media3.common.MediaItem; @@ -236,8 +237,7 @@ public class MediaSessionServiceTest { } @Test - public void onCreate_mediaNotificationManagerController_correctSessionStateFromOnConnect() - throws Exception { + public void onCreate_withCustomLayout_correctSessionStateFromOnConnect() throws Exception { SessionCommand command1 = new SessionCommand("command1", Bundle.EMPTY); SessionCommand command2 = new SessionCommand("command2", Bundle.EMPTY); SessionCommand command3 = new SessionCommand("command3", Bundle.EMPTY); @@ -349,6 +349,121 @@ public class MediaSessionServiceTest { .blockUntilAllControllersUnbind(TIMEOUT_MS); } + @Test + public void onCreate_withMediaButtonPreferences_correctSessionStateFromOnConnect() + throws Exception { + SessionCommand command1 = new SessionCommand("command1", Bundle.EMPTY); + SessionCommand command2 = new SessionCommand("command2", Bundle.EMPTY); + SessionCommand command3 = new SessionCommand("command3", Bundle.EMPTY); + CommandButton button1 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button1") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(command1) + .build(); + CommandButton button2 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button2") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(command2) + .build(); + CommandButton button3 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button3") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(command3) + .build(); + Bundle testHints = new Bundle(); + testHints.putString("test_key", "test_value"); + List controllerInfoList = new ArrayList<>(); + CountDownLatch latch = new CountDownLatch(2); + TestHandler handler = new TestHandler(Looper.getMainLooper()); + ExoPlayer player = + handler.postAndSync( + () -> { + ExoPlayer exoPlayer = new TestExoPlayerBuilder(context).build(); + exoPlayer.setMediaItem(MediaItem.fromUri("asset:///media/mp4/sample.mp4")); + exoPlayer.prepare(); + return exoPlayer; + }); + MediaSession mediaSession = + new MediaSession.Builder(ApplicationProvider.getApplicationContext(), player) + .setMediaButtonPreferences(Lists.newArrayList(button1, button2)) + .setCallback( + new MediaSession.Callback() { + @Override + public MediaSession.ConnectionResult onConnect( + MediaSession session, ControllerInfo controller) { + controllerInfoList.add(controller); + if (session.isMediaNotificationController(controller)) { + latch.countDown(); + return new MediaSession.ConnectionResult.AcceptedResultBuilder(session) + .setAvailableSessionCommands( + SessionCommands.EMPTY.buildUpon().add(command1).add(command3).build()) + .setAvailablePlayerCommands(Player.Commands.EMPTY) + .setMediaButtonPreferences(ImmutableList.of(button1, button3)) + .build(); + } + latch.countDown(); + return new MediaSession.ConnectionResult.AcceptedResultBuilder(session).build(); + } + }) + .build(); + TestServiceRegistry.getInstance().setOnGetSessionHandler(controllerInfo -> mediaSession); + MediaControllerCompat mediaControllerCompat = + new MediaControllerCompat( + ApplicationProvider.getApplicationContext(), + MediaSessionCompat.Token.fromToken(mediaSession.getPlatformToken())); + CountDownLatch controllerReady = new CountDownLatch(1); + mediaControllerCompat.registerCallback( + new MediaControllerCompat.Callback() { + @Override + public void onSessionReady() { + controllerReady.countDown(); + } + }, + new Handler(Looper.getMainLooper())); + controllerReady.await(); + List initialCustomActionsInControllerCompat = + mediaControllerCompat.getPlaybackState().getCustomActions(); + + // Start the service by creating a remote controller. + RemoteMediaController remoteController = + controllerTestRule.createRemoteController(token, /* waitForConnection= */ true, testHints); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat( + controllerInfoList + .get(0) + .getConnectionHints() + .getBoolean( + MediaController.KEY_MEDIA_NOTIFICATION_CONTROLLER_FLAG, + /* defaultValue= */ false)) + .isTrue(); + assertThat(TestUtils.equals(controllerInfoList.get(1).getConnectionHints(), testHints)) + .isTrue(); + assertThat(mediaControllerCompat.getPlaybackState().getActions()) + .isEqualTo(PlaybackStateCompat.ACTION_SET_RATING); + assertThat(remoteController.getMediaButtonPreferences()) + .containsExactly(button1.copyWithIsEnabled(false), button2.copyWithIsEnabled(false)) + .inOrder(); + assertThat(initialCustomActionsInControllerCompat).isEmpty(); + assertThat(mediaControllerCompat.getPlaybackState().getCustomActions()).hasSize(2); + PlaybackStateCompat.CustomAction customAction1 = + mediaControllerCompat.getPlaybackState().getCustomActions().get(0); + PlaybackStateCompat.CustomAction customAction2 = + mediaControllerCompat.getPlaybackState().getCustomActions().get(1); + assertThat(customAction1.getAction()).isEqualTo("command1"); + assertThat(customAction1.getName().toString()).isEqualTo("button1"); + assertThat(customAction1.getIcon()).isEqualTo(R.drawable.media3_notification_small_icon); + assertThat(customAction2.getAction()).isEqualTo("command3"); + assertThat(customAction2.getName().toString()).isEqualTo("button3"); + assertThat(customAction2.getIcon()).isEqualTo(R.drawable.media3_notification_small_icon); + mediaSession.release(); + ((MockMediaSessionService) TestServiceRegistry.getInstance().getServiceInstance()) + .blockUntilAllControllersUnbind(TIMEOUT_MS); + } + /** * Tests whether {@link MediaSessionService#onGetSession(ControllerInfo)} is called when * controller tries to connect, with the proper arguments. diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaControllerProviderService.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaControllerProviderService.java index fbd0239562..d7c735e65a 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaControllerProviderService.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaControllerProviderService.java @@ -866,6 +866,20 @@ public class MediaControllerProviderService extends Service { return bundle; } + @Override + public Bundle getMediaButtonPreferences(String controllerId) throws RemoteException { + MediaController controller = mediaControllerMap.get(controllerId); + ArrayList mediaButtonPreferences = new ArrayList<>(); + ImmutableList commandButtons = + runOnHandler(controller::getMediaButtonPreferences); + for (CommandButton button : commandButtons) { + mediaButtonPreferences.add(button.toBundle()); + } + Bundle bundle = new Bundle(); + bundle.putParcelableArrayList(KEY_COMMAND_BUTTON_LIST, mediaButtonPreferences); + return bundle; + } + @Override public Bundle getAvailableCommands(String controllerId) throws RemoteException { MediaController controller = mediaControllerMap.get(controllerId); diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java index 2f94eb394b..fc55b0dbb8 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java @@ -622,6 +622,24 @@ public class MediaSessionProviderService extends Service { }); } + @Override + @SuppressWarnings("FutureReturnValueIgnored") + public void setMediaButtonPreferences(String sessionId, List mediaButtonPreferences) + throws RemoteException { + if (mediaButtonPreferences == null) { + return; + } + runOnHandler( + () -> { + ImmutableList.Builder builder = new ImmutableList.Builder<>(); + for (Bundle bundle : mediaButtonPreferences) { + builder.add(CommandButton.fromBundle(bundle, MediaSessionStub.VERSION_INT)); + } + MediaSession session = sessionMap.get(sessionId); + session.setMediaButtonPreferences(builder.build()); + }); + } + @Override public void setSessionExtras(String sessionId, Bundle extras) throws RemoteException { runOnHandler(() -> sessionMap.get(sessionId).setSessionExtras(extras)); diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaController.java b/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaController.java index b8143f31e8..f17a195c8c 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaController.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaController.java @@ -389,6 +389,17 @@ public class RemoteMediaController { return customLayout.build(); } + public ImmutableList getMediaButtonPreferences() throws RemoteException { + Bundle mediaButtonPreferencesBundle = binder.getMediaButtonPreferences(controllerId); + ArrayList list = + mediaButtonPreferencesBundle.getParcelableArrayList(KEY_COMMAND_BUTTON_LIST); + ImmutableList.Builder mediaButtonPreferences = new ImmutableList.Builder<>(); + for (Bundle bundle : list) { + mediaButtonPreferences.add(CommandButton.fromBundle(bundle, MediaSessionStub.VERSION_INT)); + } + return mediaButtonPreferences.build(); + } + public Player.Commands getAvailableCommands() throws RemoteException { Bundle commandsBundle = binder.getAvailableCommands(controllerId); return Player.Commands.fromBundle(commandsBundle); diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSession.java b/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSession.java index e7e8abd4b0..ede58ca05d 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSession.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaSession.java @@ -201,6 +201,15 @@ public class RemoteMediaSession { binder.setCustomLayout(sessionId, bundleList); } + public void setMediaButtonPreferences(List mediaButtonPreferences) + throws RemoteException { + List bundleList = new ArrayList<>(); + for (CommandButton button : mediaButtonPreferences) { + bundleList.add(button.toBundle()); + } + binder.setMediaButtonPreferences(sessionId, bundleList); + } + public void setSessionExtras(Bundle extras) throws RemoteException { binder.setSessionExtras(sessionId, extras); } diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/TestMediaBrowserListener.java b/libraries/test_session_current/src/main/java/androidx/media3/session/TestMediaBrowserListener.java index 288079d84a..30f95c6c94 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/TestMediaBrowserListener.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/TestMediaBrowserListener.java @@ -62,6 +62,12 @@ public final class TestMediaBrowserListener implements MediaBrowser.Listener { delegate.onCustomLayoutChanged(controller, layout); } + @Override + public void onMediaButtonPreferencesChanged( + MediaController controller, List mediaButtonPreferences) { + delegate.onMediaButtonPreferencesChanged(controller, mediaButtonPreferences); + } + @Override public void onExtrasChanged(MediaController controller, Bundle extras) { delegate.onExtrasChanged(controller, extras);