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
This commit is contained in:
tonihei 2024-10-17 09:51:58 -07:00 committed by Copybara-Service
parent e851a1419d
commit 627b7a3e56
34 changed files with 1769 additions and 181 deletions

View File

@ -49,7 +49,8 @@ oneway interface IMediaController {
void onExtrasChanged(int seq, in Bundle extras) = 3011; void onExtrasChanged(int seq, in Bundle extras) = 3011;
void onSessionActivityChanged(int seq, in PendingIntent pendingIntent) = 3013; void onSessionActivityChanged(int seq, in PendingIntent pendingIntent) = 3013;
void onError(int seq, in Bundle sessionError) = 3014; void onError(int seq, in Bundle sessionError) = 3014;
// Next Id for MediaController: 3015 void onSetMediaButtonPreferences(int seq, in List<Bundle> commandButtonList) = 3015;
// Next Id for MediaController: 3016
void onChildrenChanged( void onChildrenChanged(
int seq, String parentId, int itemCount, in @nullable Bundle libraryParams) = 4000; int seq, String parentId, int itemCount, in @nullable Bundle libraryParams) = 4000;

View File

@ -58,6 +58,8 @@ import java.util.List;
public final ImmutableList<CommandButton> customLayout; public final ImmutableList<CommandButton> customLayout;
public final ImmutableList<CommandButton> mediaButtonPreferences;
@Nullable public final Token platformToken; @Nullable public final Token platformToken;
public final ImmutableList<CommandButton> commandButtonsForMediaItems; public final ImmutableList<CommandButton> commandButtonsForMediaItems;
@ -68,6 +70,7 @@ import java.util.List;
IMediaSession sessionBinder, IMediaSession sessionBinder,
@Nullable PendingIntent sessionActivity, @Nullable PendingIntent sessionActivity,
ImmutableList<CommandButton> customLayout, ImmutableList<CommandButton> customLayout,
ImmutableList<CommandButton> mediaButtonPreferences,
ImmutableList<CommandButton> commandButtonsForMediaItems, ImmutableList<CommandButton> commandButtonsForMediaItems,
SessionCommands sessionCommands, SessionCommands sessionCommands,
Player.Commands playerCommandsFromSession, Player.Commands playerCommandsFromSession,
@ -81,6 +84,7 @@ import java.util.List;
this.sessionBinder = sessionBinder; this.sessionBinder = sessionBinder;
this.sessionActivity = sessionActivity; this.sessionActivity = sessionActivity;
this.customLayout = customLayout; this.customLayout = customLayout;
this.mediaButtonPreferences = mediaButtonPreferences;
this.commandButtonsForMediaItems = commandButtonsForMediaItems; this.commandButtonsForMediaItems = commandButtonsForMediaItems;
this.sessionCommands = sessionCommands; this.sessionCommands = sessionCommands;
this.playerCommandsFromSession = playerCommandsFromSession; 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_BINDER = Util.intToStringMaxRadix(1);
private static final String FIELD_SESSION_ACTIVITY = Util.intToStringMaxRadix(2); 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_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_COMMAND_BUTTONS_FOR_MEDIA_ITEMS = Util.intToStringMaxRadix(13);
private static final String FIELD_SESSION_COMMANDS = Util.intToStringMaxRadix(3); private static final String FIELD_SESSION_COMMANDS = Util.intToStringMaxRadix(3);
private static final String FIELD_PLAYER_COMMANDS_FROM_SESSION = Util.intToStringMaxRadix(4); 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_IN_PROCESS_BINDER = Util.intToStringMaxRadix(10);
private static final String FIELD_PLATFORM_TOKEN = Util.intToStringMaxRadix(12); private static final String FIELD_PLATFORM_TOKEN = Util.intToStringMaxRadix(12);
// Next field key = 14 // Next field key = 15
public Bundle toBundleForRemoteProcess(int controllerInterfaceVersion) { public Bundle toBundleForRemoteProcess(int controllerInterfaceVersion) {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
@ -118,6 +123,21 @@ import java.util.List;
FIELD_CUSTOM_LAYOUT, FIELD_CUSTOM_LAYOUT,
BundleCollectionUtil.toBundleArrayList(customLayout, CommandButton::toBundle)); 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()) { if (!commandButtonsForMediaItems.isEmpty()) {
bundle.putParcelableArrayList( bundle.putParcelableArrayList(
FIELD_COMMAND_BUTTONS_FOR_MEDIA_ITEMS, FIELD_COMMAND_BUTTONS_FOR_MEDIA_ITEMS,
@ -166,11 +186,20 @@ import java.util.List;
IBinder sessionBinder = checkNotNull(BundleCompat.getBinder(bundle, FIELD_SESSION_BINDER)); IBinder sessionBinder = checkNotNull(BundleCompat.getBinder(bundle, FIELD_SESSION_BINDER));
@Nullable PendingIntent sessionActivity = bundle.getParcelable(FIELD_SESSION_ACTIVITY); @Nullable PendingIntent sessionActivity = bundle.getParcelable(FIELD_SESSION_ACTIVITY);
@Nullable @Nullable
List<Bundle> commandButtonArrayList = bundle.getParcelableArrayList(FIELD_CUSTOM_LAYOUT); List<Bundle> customLayoutArrayList = bundle.getParcelableArrayList(FIELD_CUSTOM_LAYOUT);
ImmutableList<CommandButton> customLayout = ImmutableList<CommandButton> customLayout =
commandButtonArrayList != null customLayoutArrayList != null
? BundleCollectionUtil.fromBundleList( ? BundleCollectionUtil.fromBundleList(
b -> CommandButton.fromBundle(b, sessionInterfaceVersion), commandButtonArrayList) b -> CommandButton.fromBundle(b, sessionInterfaceVersion), customLayoutArrayList)
: ImmutableList.of();
@Nullable
List<Bundle> mediaButtonPreferencesArrayList =
bundle.getParcelableArrayList(FIELD_MEDIA_BUTTON_PREFERENCES);
ImmutableList<CommandButton> mediaButtonPreferences =
mediaButtonPreferencesArrayList != null
? BundleCollectionUtil.fromBundleList(
b -> CommandButton.fromBundle(b, sessionInterfaceVersion),
mediaButtonPreferencesArrayList)
: ImmutableList.of(); : ImmutableList.of();
@Nullable @Nullable
List<Bundle> commandButtonsForMediaItemsArrayList = List<Bundle> commandButtonsForMediaItemsArrayList =
@ -212,6 +241,7 @@ import java.util.List;
IMediaSession.Stub.asInterface(sessionBinder), IMediaSession.Stub.asInterface(sessionBinder),
sessionActivity, sessionActivity,
customLayout, customLayout,
mediaButtonPreferences,
commandButtonsForMediaItems, commandButtonsForMediaItems,
sessionCommands, sessionCommands,
playerCommandsFromSession, playerCommandsFromSession,

View File

@ -51,7 +51,6 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CancellationException; import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@ -299,19 +298,20 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
@Override @Override
public final MediaNotification createNotification( public final MediaNotification createNotification(
MediaSession mediaSession, MediaSession mediaSession,
ImmutableList<CommandButton> customLayout, ImmutableList<CommandButton> mediaButtonPreferences,
MediaNotification.ActionFactory actionFactory, MediaNotification.ActionFactory actionFactory,
Callback onNotificationChangedCallback) { Callback onNotificationChangedCallback) {
ensureNotificationChannel(); ensureNotificationChannel();
ImmutableList.Builder<CommandButton> customLayoutWithEnabledCommandButtonsOnly = // TODO: b/332877990 - More accurately reflect media button preferences in the notification.
ImmutableList.Builder<CommandButton> mediaButtonPreferencesWithEnabledCommandButtonsOnly =
new ImmutableList.Builder<>(); new ImmutableList.Builder<>();
for (int i = 0; i < customLayout.size(); i++) { for (int i = 0; i < mediaButtonPreferences.size(); i++) {
CommandButton button = customLayout.get(i); CommandButton button = mediaButtonPreferences.get(i);
if (button.sessionCommand != null if (button.sessionCommand != null
&& button.sessionCommand.commandCode == SessionCommand.COMMAND_CODE_CUSTOM && button.sessionCommand.commandCode == SessionCommand.COMMAND_CODE_CUSTOM
&& button.isEnabled) { && button.isEnabled) {
customLayoutWithEnabledCommandButtonsOnly.add(customLayout.get(i)); mediaButtonPreferencesWithEnabledCommandButtonsOnly.add(mediaButtonPreferences.get(i));
} }
} }
Player player = mediaSession.getPlayer(); Player player = mediaSession.getPlayer();
@ -325,7 +325,7 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
getMediaButtons( getMediaButtons(
mediaSession, mediaSession,
player.getAvailableCommands(), player.getAvailableCommands(),
customLayoutWithEnabledCommandButtonsOnly.build(), mediaButtonPreferencesWithEnabledCommandButtonsOnly.build(),
!Util.shouldShowPlayButton( !Util.shouldShowPlayButton(
player, mediaSession.getShowPlayButtonIfPlaybackIsSuppressed())), player, mediaSession.getShowPlayButtonIfPlaybackIsSuppressed())),
builder, 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 * 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}. * extras with key {@link DefaultMediaNotificationProvider#COMMAND_KEY_COMPACT_VIEW_INDEX}.
* *
* <p>To make the custom layout and commands work, you need to {@linkplain * <p>To make the media button preferences and custom commands work, you need to {@linkplain
* MediaSession#setCustomLayout(List) set the custom layout of commands} and add the custom * MediaSession#setMediaButtonPreferences set the media button preferences} and add the custom
* commands to the available commands when a controller {@linkplain * commands to the available commands when a controller {@linkplain
* MediaSession.Callback#onConnect(MediaSession, MediaSession.ControllerInfo) connects to the * MediaSession.Callback#onConnect(MediaSession, MediaSession.ControllerInfo) connects to the
* session}. Controllers that connect after you called {@link MediaSession#setCustomLayout(List)} * session}. Controllers that connect after you called {@link
* need the custom command set in {@link MediaSession.Callback#onPostConnect(MediaSession, * MediaSession#setMediaButtonPreferences} need the custom command set in {@link
* MediaSession.ControllerInfo)} also. * MediaSession.Callback#onPostConnect(MediaSession, MediaSession.ControllerInfo)} too.
* *
* @param session The media session. * @param session The media session.
* @param playerCommands The available player commands. * @param playerCommands The available player commands.
* @param customLayout The {@linkplain MediaSession#setCustomLayout(List) custom layout of * @param mediaButtonPreferences The {@linkplain MediaSession#setMediaButtonPreferences media
* commands}. * button preferences}.
* @param showPauseButton Whether the notification should show a pause button (e.g., because the * @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. * 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. * @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<CommandButton> getMediaButtons( protected ImmutableList<CommandButton> getMediaButtons(
MediaSession session, MediaSession session,
Player.Commands playerCommands, Player.Commands playerCommands,
ImmutableList<CommandButton> customLayout, ImmutableList<CommandButton> mediaButtonPreferences,
boolean showPauseButton) { boolean showPauseButton) {
// Skip to previous action. // Skip to previous action.
ImmutableList.Builder<CommandButton> commandButtons = new ImmutableList.Builder<>(); ImmutableList.Builder<CommandButton> 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)) .setDisplayName(context.getString(R.string.media3_controls_seek_to_next_description))
.build()); .build());
} }
for (int i = 0; i < customLayout.size(); i++) { for (int i = 0; i < mediaButtonPreferences.size(); i++) {
CommandButton button = customLayout.get(i); CommandButton button = mediaButtonPreferences.get(i);
if (button.sessionCommand != null if (button.sessionCommand != null
&& button.sessionCommand.commandCode == SessionCommand.COMMAND_CODE_CUSTOM) { && button.sessionCommand.commandCode == SessionCommand.COMMAND_CODE_CUSTOM) {
commandButtons.add(button); commandButtons.add(button);

View File

@ -1495,13 +1495,12 @@ import java.util.concurrent.TimeoutException;
} }
/** /**
* Converts {@link CustomAction} in the {@link PlaybackStateCompat} to the custom layout which is * Converts {@link CustomAction} in the {@link PlaybackStateCompat} to media button preferences.
* the list of the {@link CommandButton}.
* *
* @param state playback state * @param state The {@link PlaybackStateCompat}.
* @return custom layout. Always non-null. * @return The media button preferences.
*/ */
public static ImmutableList<CommandButton> convertToCustomLayout( public static ImmutableList<CommandButton> convertToMediaButtonPreferences(
@Nullable PlaybackStateCompat state) { @Nullable PlaybackStateCompat state) {
if (state == null) { if (state == null) {
return ImmutableList.of(); return ImmutableList.of();
@ -1510,7 +1509,7 @@ import java.util.concurrent.TimeoutException;
if (customActions == null) { if (customActions == null) {
return ImmutableList.of(); return ImmutableList.of();
} }
ImmutableList.Builder<CommandButton> layout = new ImmutableList.Builder<>(); ImmutableList.Builder<CommandButton> mediaButtonPreferences = new ImmutableList.Builder<>();
for (CustomAction customAction : customActions) { for (CustomAction customAction : customActions) {
String action = customAction.getAction(); String action = customAction.getAction();
@Nullable Bundle extras = customAction.getExtras(); @Nullable Bundle extras = customAction.getExtras();
@ -1521,15 +1520,16 @@ import java.util.concurrent.TimeoutException;
MediaConstants.EXTRAS_KEY_COMMAND_BUTTON_ICON_COMPAT, MediaConstants.EXTRAS_KEY_COMMAND_BUTTON_ICON_COMPAT,
/* defaultValue= */ CommandButton.ICON_UNDEFINED) /* defaultValue= */ CommandButton.ICON_UNDEFINED)
: CommandButton.ICON_UNDEFINED; : CommandButton.ICON_UNDEFINED;
// TODO: b/332877990 - Set appropriate slots based on available player commands.
CommandButton button = CommandButton button =
new CommandButton.Builder(icon, customAction.getIcon()) new CommandButton.Builder(icon, customAction.getIcon())
.setSessionCommand(new SessionCommand(action, extras == null ? Bundle.EMPTY : extras)) .setSessionCommand(new SessionCommand(action, extras == null ? Bundle.EMPTY : extras))
.setDisplayName(customAction.getName()) .setDisplayName(customAction.getName())
.setEnabled(true) .setEnabled(true)
.build(); .build();
layout.add(button); mediaButtonPreferences.add(button);
} }
return layout.build(); return mediaButtonPreferences.build();
} }
/** Converts {@link AudioAttributesCompat} into {@link AudioAttributes}. */ /** Converts {@link AudioAttributesCompat} into {@link AudioAttributes}. */

View File

@ -409,6 +409,8 @@ public class MediaController implements Player {
/** /**
* Called when the {@linkplain #getCustomLayout() custom layout} changed. * Called when the {@linkplain #getCustomLayout() custom layout} changed.
* *
* <p>This method will be deprecated, prefer to use {@link #onMediaButtonPreferencesChanged}.
*
* <p>The custom layout can change when either the session {@linkplain * <p>The custom layout can change when either the session {@linkplain
* MediaSession#setCustomLayout changes the custom layout}, or when the session {@linkplain * MediaSession#setCustomLayout changes the custom layout}, or when the session {@linkplain
* MediaSession#setAvailableCommands(MediaSession.ControllerInfo, SessionCommands, Commands) * MediaSession#setAvailableCommands(MediaSession.ControllerInfo, SessionCommands, Commands)
@ -424,6 +426,25 @@ public class MediaController implements Player {
@UnstableApi @UnstableApi
default void onCustomLayoutChanged(MediaController controller, List<CommandButton> layout) {} default void onCustomLayoutChanged(MediaController controller, List<CommandButton> layout) {}
/**
* Called when the {@linkplain #getMediaButtonPreferences() media button preferences} changed.
*
* <p>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.
*
* <p>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<CommandButton> mediaButtonPreferences) {}
/** /**
* Called when the available session commands are changed by session. * Called when the available session commands are changed by session.
* *
@ -1094,6 +1115,8 @@ public class MediaController implements Player {
/** /**
* Returns the custom layout. * Returns the custom layout.
* *
* <p>This method will be deprecated, prefer to use {@link #getMediaButtonPreferences()} instead.
*
* <p>After being connected, a change of the custom layout is reported with {@link * <p>After being connected, a change of the custom layout is reported with {@link
* Listener#onCustomLayoutChanged(MediaController, List)}. * Listener#onCustomLayoutChanged(MediaController, List)}.
* *
@ -1104,8 +1127,24 @@ public class MediaController implements Player {
*/ */
@UnstableApi @UnstableApi
public final ImmutableList<CommandButton> getCustomLayout() { public final ImmutableList<CommandButton> getCustomLayout() {
return getMediaButtonPreferences();
}
/**
* Returns the media button preferences.
*
* <p>After being connected, a change of the media button preferences is reported with {@link
* Listener#onMediaButtonPreferencesChanged(MediaController, List)}.
*
* <p>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<CommandButton> getMediaButtonPreferences() {
verifyApplicationThread(); verifyApplicationThread();
return isConnected() ? impl.getCustomLayout() : ImmutableList.of(); return isConnected() ? impl.getMediaButtonPreferences() : ImmutableList.of();
} }
/** /**
@ -2168,7 +2207,7 @@ public class MediaController implements Player {
ListenableFuture<SessionResult> sendCustomCommand(SessionCommand command, Bundle args); ListenableFuture<SessionResult> sendCustomCommand(SessionCommand command, Bundle args);
ImmutableList<CommandButton> getCustomLayout(); ImmutableList<CommandButton> getMediaButtonPreferences();
ImmutableList<CommandButton> getCommandButtonsForMediaItem(MediaItem mediaItem); ImmutableList<CommandButton> getCommandButtonsForMediaItem(MediaItem mediaItem);

View File

@ -126,7 +126,8 @@ import org.checkerframework.checker.nullness.qual.NonNull;
private PlayerInfo playerInfo; private PlayerInfo playerInfo;
@Nullable private PendingIntent sessionActivity; @Nullable private PendingIntent sessionActivity;
private ImmutableList<CommandButton> customLayoutOriginal; private ImmutableList<CommandButton> customLayoutOriginal;
private ImmutableList<CommandButton> customLayoutWithUnavailableButtonsDisabled; private ImmutableList<CommandButton> mediaButtonPreferencesOriginal;
private ImmutableList<CommandButton> resolvedMediaButtonPreferences;
private ImmutableMap<String, CommandButton> commandButtonsForMediaItemsMap; private ImmutableMap<String, CommandButton> commandButtonsForMediaItemsMap;
private SessionCommands sessionCommands; private SessionCommands sessionCommands;
private Commands playerCommandsFromSession; private Commands playerCommandsFromSession;
@ -155,7 +156,8 @@ import org.checkerframework.checker.nullness.qual.NonNull;
surfaceSize = Size.UNKNOWN; surfaceSize = Size.UNKNOWN;
sessionCommands = SessionCommands.EMPTY; sessionCommands = SessionCommands.EMPTY;
customLayoutOriginal = ImmutableList.of(); customLayoutOriginal = ImmutableList.of();
customLayoutWithUnavailableButtonsDisabled = ImmutableList.of(); mediaButtonPreferencesOriginal = ImmutableList.of();
resolvedMediaButtonPreferences = ImmutableList.of();
commandButtonsForMediaItemsMap = ImmutableMap.of(); commandButtonsForMediaItemsMap = ImmutableMap.of();
playerCommandsFromSession = Commands.EMPTY; playerCommandsFromSession = Commands.EMPTY;
playerCommandsFromPlayer = Commands.EMPTY; playerCommandsFromPlayer = Commands.EMPTY;
@ -745,8 +747,8 @@ import org.checkerframework.checker.nullness.qual.NonNull;
} }
@Override @Override
public ImmutableList<CommandButton> getCustomLayout() { public ImmutableList<CommandButton> getMediaButtonPreferences() {
return customLayoutWithUnavailableButtonsDisabled; return resolvedMediaButtonPreferences;
} }
@Override @Override
@ -2652,9 +2654,13 @@ import org.checkerframework.checker.nullness.qual.NonNull;
createIntersectedCommandsEnsuringCommandReleaseAvailable( createIntersectedCommandsEnsuringCommandReleaseAvailable(
playerCommandsFromSession, playerCommandsFromPlayer); playerCommandsFromSession, playerCommandsFromPlayer);
customLayoutOriginal = result.customLayout; customLayoutOriginal = result.customLayout;
customLayoutWithUnavailableButtonsDisabled = mediaButtonPreferencesOriginal = result.mediaButtonPreferences;
CommandButton.copyWithUnavailableButtonsDisabled( resolvedMediaButtonPreferences =
result.customLayout, sessionCommands, intersectedPlayerCommands); resolveMediaButtonPreferences(
mediaButtonPreferencesOriginal,
customLayoutOriginal,
sessionCommands,
intersectedPlayerCommands);
ImmutableMap.Builder<String, CommandButton> commandButtonsForMediaItems = ImmutableMap.Builder<String, CommandButton> commandButtonsForMediaItems =
new ImmutableMap.Builder<>(); new ImmutableMap.Builder<>();
for (int i = 0; i < result.commandButtonsForMediaItems.size(); i++) { for (int i = 0; i < result.commandButtonsForMediaItems.size(); i++) {
@ -2834,13 +2840,17 @@ import org.checkerframework.checker.nullness.qual.NonNull;
intersectedPlayerCommandsChanged = intersectedPlayerCommandsChanged =
!Util.areEqual(intersectedPlayerCommands, prevIntersectedPlayerCommands); !Util.areEqual(intersectedPlayerCommands, prevIntersectedPlayerCommands);
} }
boolean customLayoutChanged = false; boolean mediaButtonPreferencesChanged = false;
if (sessionCommandsChanged || intersectedPlayerCommandsChanged) { if (sessionCommandsChanged || intersectedPlayerCommandsChanged) {
ImmutableList<CommandButton> oldCustomLayout = customLayoutWithUnavailableButtonsDisabled; ImmutableList<CommandButton> oldMediaButtonPreferences = resolvedMediaButtonPreferences;
customLayoutWithUnavailableButtonsDisabled = resolvedMediaButtonPreferences =
CommandButton.copyWithUnavailableButtonsDisabled( resolveMediaButtonPreferences(
customLayoutOriginal, sessionCommands, intersectedPlayerCommands); mediaButtonPreferencesOriginal,
customLayoutChanged = !customLayoutWithUnavailableButtonsDisabled.equals(oldCustomLayout); customLayoutOriginal,
sessionCommands,
intersectedPlayerCommands);
mediaButtonPreferencesChanged =
!resolvedMediaButtonPreferences.equals(oldMediaButtonPreferences);
} }
if (intersectedPlayerCommandsChanged) { if (intersectedPlayerCommandsChanged) {
listeners.sendEvent( listeners.sendEvent(
@ -2853,12 +2863,14 @@ import org.checkerframework.checker.nullness.qual.NonNull;
listener -> listener ->
listener.onAvailableSessionCommandsChanged(getInstance(), sessionCommands)); listener.onAvailableSessionCommandsChanged(getInstance(), sessionCommands));
} }
if (customLayoutChanged) { if (mediaButtonPreferencesChanged) {
getInstance() getInstance()
.notifyControllerListener( .notifyControllerListener(
listener -> listener -> {
listener.onCustomLayoutChanged( listener.onCustomLayoutChanged(getInstance(), resolvedMediaButtonPreferences);
getInstance(), customLayoutWithUnavailableButtonsDisabled)); listener.onMediaButtonPreferencesChanged(
getInstance(), resolvedMediaButtonPreferences);
});
} }
} }
@ -2876,50 +2888,84 @@ import org.checkerframework.checker.nullness.qual.NonNull;
playerCommandsFromSession, playerCommandsFromPlayer); playerCommandsFromSession, playerCommandsFromPlayer);
boolean intersectedPlayerCommandsChanged = boolean intersectedPlayerCommandsChanged =
!Util.areEqual(intersectedPlayerCommands, prevIntersectedPlayerCommands); !Util.areEqual(intersectedPlayerCommands, prevIntersectedPlayerCommands);
boolean customLayoutChanged = false; boolean mediaButtonPreferencesChanged = false;
if (intersectedPlayerCommandsChanged) { if (intersectedPlayerCommandsChanged) {
ImmutableList<CommandButton> oldCustomLayout = customLayoutWithUnavailableButtonsDisabled; ImmutableList<CommandButton> oldMediaButtonPreferences = resolvedMediaButtonPreferences;
customLayoutWithUnavailableButtonsDisabled = resolvedMediaButtonPreferences =
CommandButton.copyWithUnavailableButtonsDisabled( resolveMediaButtonPreferences(
customLayoutOriginal, sessionCommands, intersectedPlayerCommands); mediaButtonPreferencesOriginal,
customLayoutChanged = !customLayoutWithUnavailableButtonsDisabled.equals(oldCustomLayout); customLayoutOriginal,
sessionCommands,
intersectedPlayerCommands);
mediaButtonPreferencesChanged =
!resolvedMediaButtonPreferences.equals(oldMediaButtonPreferences);
listeners.sendEvent( listeners.sendEvent(
/* eventFlag= */ Player.EVENT_AVAILABLE_COMMANDS_CHANGED, /* eventFlag= */ Player.EVENT_AVAILABLE_COMMANDS_CHANGED,
listener -> listener.onAvailableCommandsChanged(intersectedPlayerCommands)); listener -> listener.onAvailableCommandsChanged(intersectedPlayerCommands));
} }
if (customLayoutChanged) { if (mediaButtonPreferencesChanged) {
getInstance() getInstance()
.notifyControllerListener( .notifyControllerListener(
listener -> listener -> {
listener.onCustomLayoutChanged( listener.onCustomLayoutChanged(getInstance(), resolvedMediaButtonPreferences);
getInstance(), customLayoutWithUnavailableButtonsDisabled)); listener.onMediaButtonPreferencesChanged(
getInstance(), resolvedMediaButtonPreferences);
});
} }
} }
// Calling deprecated listener callback method for backwards compatibility.
@SuppressWarnings("deprecation")
void onSetCustomLayout(int seq, List<CommandButton> layout) { void onSetCustomLayout(int seq, List<CommandButton> layout) {
if (!isConnected()) { if (!isConnected()) {
return; return;
} }
ImmutableList<CommandButton> oldCustomLayout = customLayoutWithUnavailableButtonsDisabled; ImmutableList<CommandButton> oldMediaButtonPreferences = resolvedMediaButtonPreferences;
customLayoutOriginal = ImmutableList.copyOf(layout); customLayoutOriginal = ImmutableList.copyOf(layout);
customLayoutWithUnavailableButtonsDisabled = resolvedMediaButtonPreferences =
CommandButton.copyWithUnavailableButtonsDisabled( resolveMediaButtonPreferences(
layout, sessionCommands, intersectedPlayerCommands); mediaButtonPreferencesOriginal, layout, sessionCommands, intersectedPlayerCommands);
boolean hasCustomLayoutChanged = boolean mediaButtonPreferencesChanged =
!Objects.equals(customLayoutWithUnavailableButtonsDisabled, oldCustomLayout); !Objects.equals(resolvedMediaButtonPreferences, oldMediaButtonPreferences);
getInstance() getInstance()
.notifyControllerListener( .notifyControllerListener(
listener -> { listener -> {
ListenableFuture<SessionResult> future = ListenableFuture<SessionResult> future =
checkNotNull( checkNotNull(
listener.onSetCustomLayout( listener.onSetCustomLayout(getInstance(), resolvedMediaButtonPreferences),
getInstance(), customLayoutWithUnavailableButtonsDisabled),
"MediaController.Listener#onSetCustomLayout() must not return null"); "MediaController.Listener#onSetCustomLayout() must not return null");
if (hasCustomLayoutChanged) { if (mediaButtonPreferencesChanged) {
listener.onCustomLayoutChanged( listener.onCustomLayoutChanged(getInstance(), resolvedMediaButtonPreferences);
getInstance(), customLayoutWithUnavailableButtonsDisabled); listener.onMediaButtonPreferencesChanged(
getInstance(), resolvedMediaButtonPreferences);
}
sendControllerResultWhenReady(seq, future);
});
}
void onSetMediaButtonPreferences(int seq, List<CommandButton> mediaButtonPreferences) {
if (!isConnected()) {
return;
}
ImmutableList<CommandButton> oldMediaButtonPreferences = resolvedMediaButtonPreferences;
mediaButtonPreferencesOriginal = ImmutableList.copyOf(mediaButtonPreferences);
resolvedMediaButtonPreferences =
resolveMediaButtonPreferences(
mediaButtonPreferences,
customLayoutOriginal,
sessionCommands,
intersectedPlayerCommands);
boolean mediaButtonPreferencesChanged =
!Objects.equals(resolvedMediaButtonPreferences, oldMediaButtonPreferences);
getInstance()
.notifyControllerListener(
listener -> {
ListenableFuture<SessionResult> 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); sendControllerResultWhenReady(seq, future);
}); });
@ -3277,6 +3323,18 @@ import org.checkerframework.checker.nullness.qual.NonNull;
return newMediaItemIndex; return newMediaItemIndex;
} }
private static ImmutableList<CommandButton> resolveMediaButtonPreferences(
List<CommandButton> mediaButtonPreferences,
List<CommandButton> 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( private static Commands createIntersectedCommandsEnsuringCommandReleaseAvailable(
Commands commandFromSession, Commands commandsFromPlayer) { Commands commandFromSession, Commands commandsFromPlayer) {
Commands intersectedCommands = MediaUtils.intersect(commandFromSession, commandsFromPlayer); Commands intersectedCommands = MediaUtils.intersect(commandFromSession, commandsFromPlayer);

View File

@ -202,7 +202,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
maskedPlayerInfo, maskedPlayerInfo,
controllerInfo.availableSessionCommands, controllerInfo.availableSessionCommands,
controllerInfo.availablePlayerCommands, controllerInfo.availablePlayerCommands,
controllerInfo.customLayout, controllerInfo.mediaButtonPreferences,
controllerInfo.sessionExtras, controllerInfo.sessionExtras,
/* sessionError= */ null); /* sessionError= */ null);
updateStateMaskedControllerInfo( updateStateMaskedControllerInfo(
@ -268,7 +268,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
/* playerError= */ null), /* playerError= */ null),
controllerInfo.availableSessionCommands, controllerInfo.availableSessionCommands,
controllerInfo.availablePlayerCommands, controllerInfo.availablePlayerCommands,
controllerInfo.customLayout, controllerInfo.mediaButtonPreferences,
controllerInfo.sessionExtras, controllerInfo.sessionExtras,
/* sessionError= */ null); /* sessionError= */ null);
updateStateMaskedControllerInfo( updateStateMaskedControllerInfo(
@ -391,7 +391,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
maskedPlayerInfo, maskedPlayerInfo,
controllerInfo.availableSessionCommands, controllerInfo.availableSessionCommands,
controllerInfo.availablePlayerCommands, controllerInfo.availablePlayerCommands,
controllerInfo.customLayout, controllerInfo.mediaButtonPreferences,
controllerInfo.sessionExtras, controllerInfo.sessionExtras,
/* sessionError= */ null); /* sessionError= */ null);
updateStateMaskedControllerInfo( updateStateMaskedControllerInfo(
@ -432,8 +432,8 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
} }
@Override @Override
public ImmutableList<CommandButton> getCustomLayout() { public ImmutableList<CommandButton> getMediaButtonPreferences() {
return controllerInfo.customLayout; return controllerInfo.mediaButtonPreferences;
} }
@Override @Override
@ -556,7 +556,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
controllerInfo.playerInfo.copyWithPlaybackParameters(playbackParameters), controllerInfo.playerInfo.copyWithPlaybackParameters(playbackParameters),
controllerInfo.availableSessionCommands, controllerInfo.availableSessionCommands,
controllerInfo.availablePlayerCommands, controllerInfo.availablePlayerCommands,
controllerInfo.customLayout, controllerInfo.mediaButtonPreferences,
controllerInfo.sessionExtras, controllerInfo.sessionExtras,
/* sessionError= */ null); /* sessionError= */ null);
updateStateMaskedControllerInfo( updateStateMaskedControllerInfo(
@ -577,7 +577,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
controllerInfo.playerInfo.copyWithPlaybackParameters(new PlaybackParameters(speed)), controllerInfo.playerInfo.copyWithPlaybackParameters(new PlaybackParameters(speed)),
controllerInfo.availableSessionCommands, controllerInfo.availableSessionCommands,
controllerInfo.availablePlayerCommands, controllerInfo.availablePlayerCommands,
controllerInfo.customLayout, controllerInfo.mediaButtonPreferences,
controllerInfo.sessionExtras, controllerInfo.sessionExtras,
/* sessionError= */ null); /* sessionError= */ null);
updateStateMaskedControllerInfo( updateStateMaskedControllerInfo(
@ -671,7 +671,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
maskedPlayerInfo, maskedPlayerInfo,
controllerInfo.availableSessionCommands, controllerInfo.availableSessionCommands,
controllerInfo.availablePlayerCommands, controllerInfo.availablePlayerCommands,
controllerInfo.customLayout, controllerInfo.mediaButtonPreferences,
controllerInfo.sessionExtras, controllerInfo.sessionExtras,
/* sessionError= */ null); /* sessionError= */ null);
updateStateMaskedControllerInfo( updateStateMaskedControllerInfo(
@ -736,7 +736,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
maskedPlayerInfo, maskedPlayerInfo,
controllerInfo.availableSessionCommands, controllerInfo.availableSessionCommands,
controllerInfo.availablePlayerCommands, controllerInfo.availablePlayerCommands,
controllerInfo.customLayout, controllerInfo.mediaButtonPreferences,
controllerInfo.sessionExtras, controllerInfo.sessionExtras,
/* sessionError= */ null); /* sessionError= */ null);
updateStateMaskedControllerInfo( updateStateMaskedControllerInfo(
@ -790,7 +790,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
maskedPlayerInfo, maskedPlayerInfo,
controllerInfo.availableSessionCommands, controllerInfo.availableSessionCommands,
controllerInfo.availablePlayerCommands, controllerInfo.availablePlayerCommands,
controllerInfo.customLayout, controllerInfo.mediaButtonPreferences,
controllerInfo.sessionExtras, controllerInfo.sessionExtras,
/* sessionError= */ null); /* sessionError= */ null);
updateStateMaskedControllerInfo( updateStateMaskedControllerInfo(
@ -858,7 +858,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
maskedPlayerInfo, maskedPlayerInfo,
controllerInfo.availableSessionCommands, controllerInfo.availableSessionCommands,
controllerInfo.availablePlayerCommands, controllerInfo.availablePlayerCommands,
controllerInfo.customLayout, controllerInfo.mediaButtonPreferences,
controllerInfo.sessionExtras, controllerInfo.sessionExtras,
/* sessionError= */ null); /* sessionError= */ null);
updateStateMaskedControllerInfo( updateStateMaskedControllerInfo(
@ -972,7 +972,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
controllerInfo.playerInfo.copyWithRepeatMode(repeatMode), controllerInfo.playerInfo.copyWithRepeatMode(repeatMode),
controllerInfo.availableSessionCommands, controllerInfo.availableSessionCommands,
controllerInfo.availablePlayerCommands, controllerInfo.availablePlayerCommands,
controllerInfo.customLayout, controllerInfo.mediaButtonPreferences,
controllerInfo.sessionExtras, controllerInfo.sessionExtras,
/* sessionError= */ null); /* sessionError= */ null);
updateStateMaskedControllerInfo( updateStateMaskedControllerInfo(
@ -1000,7 +1000,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
controllerInfo.playerInfo.copyWithShuffleModeEnabled(shuffleModeEnabled), controllerInfo.playerInfo.copyWithShuffleModeEnabled(shuffleModeEnabled),
controllerInfo.availableSessionCommands, controllerInfo.availableSessionCommands,
controllerInfo.availablePlayerCommands, controllerInfo.availablePlayerCommands,
controllerInfo.customLayout, controllerInfo.mediaButtonPreferences,
controllerInfo.sessionExtras, controllerInfo.sessionExtras,
/* sessionError= */ null); /* sessionError= */ null);
updateStateMaskedControllerInfo( updateStateMaskedControllerInfo(
@ -1137,7 +1137,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
controllerInfo.playerInfo.copyWithDeviceVolume(volume, isDeviceMuted), controllerInfo.playerInfo.copyWithDeviceVolume(volume, isDeviceMuted),
controllerInfo.availableSessionCommands, controllerInfo.availableSessionCommands,
controllerInfo.availablePlayerCommands, controllerInfo.availablePlayerCommands,
controllerInfo.customLayout, controllerInfo.mediaButtonPreferences,
controllerInfo.sessionExtras, controllerInfo.sessionExtras,
/* sessionError= */ null); /* sessionError= */ null);
updateStateMaskedControllerInfo( updateStateMaskedControllerInfo(
@ -1170,7 +1170,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
controllerInfo.playerInfo.copyWithDeviceVolume(volume + 1, isDeviceMuted), controllerInfo.playerInfo.copyWithDeviceVolume(volume + 1, isDeviceMuted),
controllerInfo.availableSessionCommands, controllerInfo.availableSessionCommands,
controllerInfo.availablePlayerCommands, controllerInfo.availablePlayerCommands,
controllerInfo.customLayout, controllerInfo.mediaButtonPreferences,
controllerInfo.sessionExtras, controllerInfo.sessionExtras,
/* sessionError= */ null); /* sessionError= */ null);
updateStateMaskedControllerInfo( updateStateMaskedControllerInfo(
@ -1202,7 +1202,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
controllerInfo.playerInfo.copyWithDeviceVolume(volume - 1, isDeviceMuted), controllerInfo.playerInfo.copyWithDeviceVolume(volume - 1, isDeviceMuted),
controllerInfo.availableSessionCommands, controllerInfo.availableSessionCommands,
controllerInfo.availablePlayerCommands, controllerInfo.availablePlayerCommands,
controllerInfo.customLayout, controllerInfo.mediaButtonPreferences,
controllerInfo.sessionExtras, controllerInfo.sessionExtras,
/* sessionError= */ null); /* sessionError= */ null);
updateStateMaskedControllerInfo( updateStateMaskedControllerInfo(
@ -1237,7 +1237,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
controllerInfo.playerInfo.copyWithDeviceVolume(volume, muted), controllerInfo.playerInfo.copyWithDeviceVolume(volume, muted),
controllerInfo.availableSessionCommands, controllerInfo.availableSessionCommands,
controllerInfo.availablePlayerCommands, controllerInfo.availablePlayerCommands,
controllerInfo.customLayout, controllerInfo.mediaButtonPreferences,
controllerInfo.sessionExtras, controllerInfo.sessionExtras,
/* sessionError= */ null); /* sessionError= */ null);
updateStateMaskedControllerInfo( updateStateMaskedControllerInfo(
@ -1276,7 +1276,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
Player.PLAYBACK_SUPPRESSION_REASON_NONE), Player.PLAYBACK_SUPPRESSION_REASON_NONE),
controllerInfo.availableSessionCommands, controllerInfo.availableSessionCommands,
controllerInfo.availablePlayerCommands, controllerInfo.availablePlayerCommands,
controllerInfo.customLayout, controllerInfo.mediaButtonPreferences,
controllerInfo.sessionExtras, controllerInfo.sessionExtras,
/* sessionError= */ null); /* sessionError= */ null);
updateStateMaskedControllerInfo( updateStateMaskedControllerInfo(
@ -1625,13 +1625,18 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
if (notifyConnected) { if (notifyConnected) {
getInstance().notifyAccepted(); getInstance().notifyAccepted();
if (!oldControllerInfo.customLayout.equals(newControllerInfo.customLayout)) { if (!oldControllerInfo.mediaButtonPreferences.equals(
newControllerInfo.mediaButtonPreferences)) {
getInstance() getInstance()
.notifyControllerListener( .notifyControllerListener(
listener -> { listener -> {
ignoreFuture( ignoreFuture(
listener.onSetCustomLayout(getInstance(), newControllerInfo.customLayout)); listener.onSetCustomLayout(
listener.onCustomLayoutChanged(getInstance(), newControllerInfo.customLayout); getInstance(), newControllerInfo.mediaButtonPreferences));
listener.onCustomLayoutChanged(
getInstance(), newControllerInfo.mediaButtonPreferences);
listener.onMediaButtonPreferencesChanged(
getInstance(), newControllerInfo.mediaButtonPreferences);
}); });
} }
return; return;
@ -1758,13 +1763,18 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
listener.onAvailableSessionCommandsChanged( listener.onAvailableSessionCommandsChanged(
getInstance(), newControllerInfo.availableSessionCommands)); getInstance(), newControllerInfo.availableSessionCommands));
} }
if (!oldControllerInfo.customLayout.equals(newControllerInfo.customLayout)) { if (!oldControllerInfo.mediaButtonPreferences.equals(
newControllerInfo.mediaButtonPreferences)) {
getInstance() getInstance()
.notifyControllerListener( .notifyControllerListener(
listener -> { listener -> {
ignoreFuture( ignoreFuture(
listener.onSetCustomLayout(getInstance(), newControllerInfo.customLayout)); listener.onSetCustomLayout(
listener.onCustomLayoutChanged(getInstance(), newControllerInfo.customLayout); getInstance(), newControllerInfo.mediaButtonPreferences));
listener.onCustomLayoutChanged(
getInstance(), newControllerInfo.mediaButtonPreferences);
listener.onMediaButtonPreferencesChanged(
getInstance(), newControllerInfo.mediaButtonPreferences);
}); });
} }
if (newControllerInfo.sessionError != null) { if (newControllerInfo.sessionError != null) {
@ -1913,7 +1923,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
controllerInfo.playerInfo, controllerInfo.playerInfo,
controllerInfo.availableSessionCommands, controllerInfo.availableSessionCommands,
controllerInfo.availablePlayerCommands, controllerInfo.availablePlayerCommands,
controllerInfo.customLayout, controllerInfo.mediaButtonPreferences,
extras, extras,
/* sessionError= */ null); /* sessionError= */ null);
getInstance() getInstance()
@ -1988,7 +1998,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
boolean shuffleModeEnabled; boolean shuffleModeEnabled;
SessionCommands availableSessionCommands; SessionCommands availableSessionCommands;
Commands availablePlayerCommands; Commands availablePlayerCommands;
ImmutableList<CommandButton> customLayout; ImmutableList<CommandButton> mediaButtonPreferences;
boolean isQueueChanged = oldLegacyPlayerInfo.queue != newLegacyPlayerInfo.queue; boolean isQueueChanged = oldLegacyPlayerInfo.queue != newLegacyPlayerInfo.queue;
currentTimeline = currentTimeline =
@ -2074,11 +2084,12 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
availableSessionCommands = availableSessionCommands =
LegacyConversions.convertToSessionCommands( LegacyConversions.convertToSessionCommands(
newLegacyPlayerInfo.playbackStateCompat, isSessionReady); newLegacyPlayerInfo.playbackStateCompat, isSessionReady);
customLayout = mediaButtonPreferences =
LegacyConversions.convertToCustomLayout(newLegacyPlayerInfo.playbackStateCompat); LegacyConversions.convertToMediaButtonPreferences(
newLegacyPlayerInfo.playbackStateCompat);
} else { } else {
availableSessionCommands = oldControllerInfo.availableSessionCommands; availableSessionCommands = oldControllerInfo.availableSessionCommands;
customLayout = oldControllerInfo.customLayout; mediaButtonPreferences = oldControllerInfo.mediaButtonPreferences;
} }
// Note: Sets the available player command here although it can be obtained before session is // 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 // 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, shuffleModeEnabled,
availableSessionCommands, availableSessionCommands,
availablePlayerCommands, availablePlayerCommands,
customLayout, mediaButtonPreferences,
newLegacyPlayerInfo.sessionExtras, newLegacyPlayerInfo.sessionExtras,
playerError, playerError,
sessionError, sessionError,
@ -2334,7 +2345,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
boolean shuffleModeEnabled, boolean shuffleModeEnabled,
SessionCommands availableSessionCommands, SessionCommands availableSessionCommands,
Commands availablePlayerCommands, Commands availablePlayerCommands,
ImmutableList<CommandButton> customLayout, ImmutableList<CommandButton> mediaButtonPreferences,
Bundle sessionExtras, Bundle sessionExtras,
@Nullable PlaybackException playerError, @Nullable PlaybackException playerError,
@Nullable SessionError sessionError, @Nullable SessionError sessionError,
@ -2411,7 +2422,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
playerInfo, playerInfo,
availableSessionCommands, availableSessionCommands,
availablePlayerCommands, availablePlayerCommands,
customLayout, mediaButtonPreferences,
sessionExtras, sessionExtras,
sessionError); sessionError);
} }
@ -2622,7 +2633,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
public final PlayerInfo playerInfo; public final PlayerInfo playerInfo;
public final SessionCommands availableSessionCommands; public final SessionCommands availableSessionCommands;
public final Commands availablePlayerCommands; public final Commands availablePlayerCommands;
public final ImmutableList<CommandButton> customLayout; public final ImmutableList<CommandButton> mediaButtonPreferences;
public final Bundle sessionExtras; public final Bundle sessionExtras;
@Nullable public final SessionError sessionError; @Nullable public final SessionError sessionError;
@ -2630,7 +2641,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
playerInfo = PlayerInfo.DEFAULT.copyWithTimeline(QueueTimeline.DEFAULT); playerInfo = PlayerInfo.DEFAULT.copyWithTimeline(QueueTimeline.DEFAULT);
availableSessionCommands = SessionCommands.EMPTY; availableSessionCommands = SessionCommands.EMPTY;
availablePlayerCommands = Commands.EMPTY; availablePlayerCommands = Commands.EMPTY;
customLayout = ImmutableList.of(); mediaButtonPreferences = ImmutableList.of();
sessionExtras = Bundle.EMPTY; sessionExtras = Bundle.EMPTY;
sessionError = null; sessionError = null;
} }
@ -2639,13 +2650,13 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
PlayerInfo playerInfo, PlayerInfo playerInfo,
SessionCommands availableSessionCommands, SessionCommands availableSessionCommands,
Commands availablePlayerCommands, Commands availablePlayerCommands,
ImmutableList<CommandButton> customLayout, ImmutableList<CommandButton> mediaButtonPreferences,
@Nullable Bundle sessionExtras, @Nullable Bundle sessionExtras,
@Nullable SessionError sessionError) { @Nullable SessionError sessionError) {
this.playerInfo = playerInfo; this.playerInfo = playerInfo;
this.availableSessionCommands = availableSessionCommands; this.availableSessionCommands = availableSessionCommands;
this.availablePlayerCommands = availablePlayerCommands; this.availablePlayerCommands = availablePlayerCommands;
this.customLayout = customLayout; this.mediaButtonPreferences = mediaButtonPreferences;
this.sessionExtras = sessionExtras == null ? Bundle.EMPTY : sessionExtras; this.sessionExtras = sessionExtras == null ? Bundle.EMPTY : sessionExtras;
this.sessionError = sessionError; this.sessionError = sessionError;
} }

View File

@ -30,6 +30,7 @@ import androidx.media3.common.util.BundleCollectionUtil;
import androidx.media3.common.util.Log; import androidx.media3.common.util.Log;
import androidx.media3.session.MediaLibraryService.LibraryParams; import androidx.media3.session.MediaLibraryService.LibraryParams;
import androidx.media3.session.PlayerInfo.BundlingExclusions; import androidx.media3.session.PlayerInfo.BundlingExclusions;
import com.google.common.collect.ImmutableList;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.List; import java.util.List;
import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.NonNull;
@ -39,7 +40,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
private static final String TAG = "MediaControllerStub"; private static final String TAG = "MediaControllerStub";
/** The version of the IMediaController interface. */ /** The version of the IMediaController interface. */
public static final int VERSION_INT = 6; public static final int VERSION_INT = 7;
private final WeakReference<MediaControllerImplBase> controller; private final WeakReference<MediaControllerImplBase> controller;
@ -129,6 +130,30 @@ import org.checkerframework.checker.nullness.qual.NonNull;
dispatchControllerTaskOnHandler(controller -> controller.onSetCustomLayout(seq, layout)); dispatchControllerTaskOnHandler(controller -> controller.onSetCustomLayout(seq, layout));
} }
@Override
public void onSetMediaButtonPreferences(int seq, @Nullable List<Bundle> commandButtonBundleList) {
if (commandButtonBundleList == null) {
return;
}
ImmutableList<CommandButton> 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 @Override
public void onAvailableCommandsChangedFromSession( public void onAvailableCommandsChangedFromSession(
int seq, @Nullable Bundle sessionCommandsBundle, @Nullable Bundle playerCommandsBundle) { int seq, @Nullable Bundle sessionCommandsBundle, @Nullable Bundle playerCommandsBundle) {

View File

@ -543,6 +543,10 @@ public abstract class MediaLibraryService extends MediaSessionService {
/** /**
* Sets the custom layout of the session. * Sets the custom layout of the session.
* *
* <p>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.
*
* <p>The buttons are converted to custom actions in the legacy media session playback state * <p>The buttons are converted to custom actions in the legacy media session playback state
* for legacy controllers (see {@code * for legacy controllers (see {@code
* PlaybackStateCompat.Builder#addCustomAction(PlaybackStateCompat.CustomAction)}). When * PlaybackStateCompat.Builder#addCustomAction(PlaybackStateCompat.CustomAction)}). When
@ -564,11 +568,42 @@ public abstract class MediaLibraryService extends MediaSessionService {
* @return The builder to allow chaining. * @return The builder to allow chaining.
*/ */
@UnstableApi @UnstableApi
@CanIgnoreReturnValue
@Override @Override
public Builder setCustomLayout(List<CommandButton> customLayout) { public Builder setCustomLayout(List<CommandButton> customLayout) {
return super.setCustomLayout(customLayout); return super.setCustomLayout(customLayout);
} }
/**
* Sets the media button preferences.
*
* <p>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.
*
* <p>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}.
*
* <p>Use {@code MediaSession.setMediaButtonPreferences(..)} to update the media button
* preferences during the life time of the session.
*
* <p>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<CommandButton> mediaButtonPreferences) {
return super.setMediaButtonPreferences(mediaButtonPreferences);
}
/** /**
* Sets whether a play button is shown if playback is {@linkplain * Sets whether a play button is shown if playback is {@linkplain
* Player#getPlaybackSuppressionReason() suppressed}. * Player#getPlaybackSuppressionReason() suppressed}.
@ -660,6 +695,7 @@ public abstract class MediaLibraryService extends MediaSessionService {
player, player,
sessionActivity, sessionActivity,
customLayout, customLayout,
mediaButtonPreferences,
commandButtonsForMediaItems, commandButtonsForMediaItems,
callback, callback,
tokenExtras, tokenExtras,
@ -677,6 +713,7 @@ public abstract class MediaLibraryService extends MediaSessionService {
Player player, Player player,
@Nullable PendingIntent sessionActivity, @Nullable PendingIntent sessionActivity,
ImmutableList<CommandButton> customLayout, ImmutableList<CommandButton> customLayout,
ImmutableList<CommandButton> mediaButtonPreferences,
ImmutableList<CommandButton> commandButtonsForMediaItems, ImmutableList<CommandButton> commandButtonsForMediaItems,
MediaSession.Callback callback, MediaSession.Callback callback,
Bundle tokenExtras, Bundle tokenExtras,
@ -691,6 +728,7 @@ public abstract class MediaLibraryService extends MediaSessionService {
player, player,
sessionActivity, sessionActivity,
customLayout, customLayout,
mediaButtonPreferences,
commandButtonsForMediaItems, commandButtonsForMediaItems,
callback, callback,
tokenExtras, tokenExtras,
@ -708,6 +746,7 @@ public abstract class MediaLibraryService extends MediaSessionService {
Player player, Player player,
@Nullable PendingIntent sessionActivity, @Nullable PendingIntent sessionActivity,
ImmutableList<CommandButton> customLayout, ImmutableList<CommandButton> customLayout,
ImmutableList<CommandButton> mediaButtonPreferences,
ImmutableList<CommandButton> commandButtonsForMediaItems, ImmutableList<CommandButton> commandButtonsForMediaItems,
MediaSession.Callback callback, MediaSession.Callback callback,
Bundle tokenExtras, Bundle tokenExtras,
@ -723,6 +762,7 @@ public abstract class MediaLibraryService extends MediaSessionService {
player, player,
sessionActivity, sessionActivity,
customLayout, customLayout,
mediaButtonPreferences,
commandButtonsForMediaItems, commandButtonsForMediaItems,
(Callback) callback, (Callback) callback,
tokenExtras, tokenExtras,

View File

@ -75,6 +75,7 @@ import java.util.concurrent.Future;
Player player, Player player,
@Nullable PendingIntent sessionActivity, @Nullable PendingIntent sessionActivity,
ImmutableList<CommandButton> customLayout, ImmutableList<CommandButton> customLayout,
ImmutableList<CommandButton> mediaButtonPreferences,
ImmutableList<CommandButton> commandButtonsForMediaItems, ImmutableList<CommandButton> commandButtonsForMediaItems,
MediaLibrarySession.Callback callback, MediaLibrarySession.Callback callback,
Bundle tokenExtras, Bundle tokenExtras,
@ -90,6 +91,7 @@ import java.util.concurrent.Future;
player, player,
sessionActivity, sessionActivity,
customLayout, customLayout,
mediaButtonPreferences,
commandButtonsForMediaItems, commandButtonsForMediaItems,
callback, callback,
tokenExtras, tokenExtras,

View File

@ -27,7 +27,6 @@ import androidx.core.graphics.drawable.IconCompat;
import androidx.media3.common.Player; import androidx.media3.common.Player;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.util.List;
/** A notification for media playbacks. */ /** A notification for media playbacks. */
public final class MediaNotification { public final class MediaNotification {
@ -145,15 +144,15 @@ public final class MediaNotification {
* @param mediaSession The media session. * @param mediaSession The media session.
* @param actionFactory The {@link ActionFactory} for creating notification {@link * @param actionFactory The {@link ActionFactory} for creating notification {@link
* NotificationCompat.Action actions}. * NotificationCompat.Action actions}.
* @param customLayout The custom layout {@linkplain MediaSession#setCustomLayout(List) set by * @param mediaButtonPreferences The media button preferences {@linkplain
* the session}. * MediaSession#setMediaButtonPreferences set by the session}.
* @param onNotificationChangedCallback A callback that the provider needs to notify when the * @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 * notification has changed and needs to be posted again, for example after a bitmap has
* been loaded asynchronously. * been loaded asynchronously.
*/ */
MediaNotification createNotification( MediaNotification createNotification(
MediaSession mediaSession, MediaSession mediaSession,
ImmutableList<CommandButton> customLayout, ImmutableList<CommandButton> mediaButtonPreferences,
ActionFactory actionFactory, ActionFactory actionFactory,
Callback onNotificationChangedCallback); Callback onNotificationChangedCallback);

View File

@ -159,9 +159,9 @@ import java.util.concurrent.TimeoutException;
// Ignore. // Ignore.
} }
} }
ImmutableList<CommandButton> customLayout = ImmutableList<CommandButton> mediaButtonPreferences =
mediaNotificationController != null mediaNotificationController != null
? mediaNotificationController.getCustomLayout() ? mediaNotificationController.getMediaButtonPreferences()
: ImmutableList.of(); : ImmutableList.of();
MediaNotification.Provider.Callback callback = MediaNotification.Provider.Callback callback =
notification -> notification ->
@ -172,7 +172,7 @@ import java.util.concurrent.TimeoutException;
() -> { () -> {
MediaNotification mediaNotification = MediaNotification mediaNotification =
this.mediaNotificationProvider.createNotification( this.mediaNotificationProvider.createNotification(
session, customLayout, actionFactory, callback); session, mediaButtonPreferences, actionFactory, callback);
mainExecutor.execute( mainExecutor.execute(
() -> () ->
updateNotificationInternal( updateNotificationInternal(
@ -320,7 +320,8 @@ import java.util.concurrent.TimeoutException;
} }
@Override @Override
public void onCustomLayoutChanged(MediaController controller, List<CommandButton> layout) { public void onMediaButtonPreferencesChanged(
MediaController controller, List<CommandButton> mediaButtonPreferences) {
mediaSessionService.onUpdateNotificationInternal( mediaSessionService.onUpdateNotificationInternal(
session, /* startInForegroundWhenPaused= */ false); session, /* startInForegroundWhenPaused= */ false);
} }

View File

@ -369,6 +369,10 @@ public class MediaSession {
/** /**
* Sets the custom layout of the session. * Sets the custom layout of the session.
* *
* <p>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.
*
* <p>The button are converted to custom actions in the legacy media session playback state for * <p>The button are converted to custom actions in the legacy media session playback state for
* legacy controllers (see {@code * legacy controllers (see {@code
* PlaybackStateCompat.Builder#addCustomAction(PlaybackStateCompat.CustomAction)}). When * PlaybackStateCompat.Builder#addCustomAction(PlaybackStateCompat.CustomAction)}). When
@ -389,12 +393,43 @@ public class MediaSession {
* @param customLayout The ordered list of {@link CommandButton command buttons}. * @param customLayout The ordered list of {@link CommandButton command buttons}.
* @return The builder to allow chaining. * @return The builder to allow chaining.
*/ */
@CanIgnoreReturnValue
@UnstableApi @UnstableApi
@Override @Override
public Builder setCustomLayout(List<CommandButton> customLayout) { public Builder setCustomLayout(List<CommandButton> customLayout) {
return super.setCustomLayout(customLayout); return super.setCustomLayout(customLayout);
} }
/**
* Sets the media button preferences.
*
* <p>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.
*
* <p>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}.
*
* <p>Use {@code MediaSession.setMediaButtonPreferences(..)} to update the media button
* preferences during the life time of the session.
*
* <p>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<CommandButton> mediaButtonPreferences) {
return super.setMediaButtonPreferences(mediaButtonPreferences);
}
/** /**
* Sets whether periodic position updates should be sent to controllers while playing. If false, * Sets whether periodic position updates should be sent to controllers while playing. If false,
* no periodic position updates are sent to controllers. * no periodic position updates are sent to controllers.
@ -455,6 +490,7 @@ public class MediaSession {
player, player,
sessionActivity, sessionActivity,
customLayout, customLayout,
mediaButtonPreferences,
commandButtonsForMediaItems, commandButtonsForMediaItems,
callback, callback,
tokenExtras, tokenExtras,
@ -682,6 +718,7 @@ public class MediaSession {
Player player, Player player,
@Nullable PendingIntent sessionActivity, @Nullable PendingIntent sessionActivity,
ImmutableList<CommandButton> customLayout, ImmutableList<CommandButton> customLayout,
ImmutableList<CommandButton> mediaButtonPreferences,
ImmutableList<CommandButton> commandButtonsForMediaItems, ImmutableList<CommandButton> commandButtonsForMediaItems,
Callback callback, Callback callback,
Bundle tokenExtras, Bundle tokenExtras,
@ -703,6 +740,7 @@ public class MediaSession {
player, player,
sessionActivity, sessionActivity,
customLayout, customLayout,
mediaButtonPreferences,
commandButtonsForMediaItems, commandButtonsForMediaItems,
callback, callback,
tokenExtras, tokenExtras,
@ -719,6 +757,7 @@ public class MediaSession {
Player player, Player player,
@Nullable PendingIntent sessionActivity, @Nullable PendingIntent sessionActivity,
ImmutableList<CommandButton> customLayout, ImmutableList<CommandButton> customLayout,
ImmutableList<CommandButton> mediaButtonPreferences,
ImmutableList<CommandButton> commandButtonsForMediaItems, ImmutableList<CommandButton> commandButtonsForMediaItems,
Callback callback, Callback callback,
Bundle tokenExtras, Bundle tokenExtras,
@ -734,6 +773,7 @@ public class MediaSession {
player, player,
sessionActivity, sessionActivity,
customLayout, customLayout,
mediaButtonPreferences,
commandButtonsForMediaItems, commandButtonsForMediaItems,
callback, callback,
tokenExtras, tokenExtras,
@ -912,12 +952,12 @@ public class MediaSession {
* *
* <p>Use this controller info to set {@linkplain #setAvailableCommands(ControllerInfo, * <p>Use this controller info to set {@linkplain #setAvailableCommands(ControllerInfo,
* SessionCommands, Player.Commands) available commands} and {@linkplain * SessionCommands, Player.Commands) available commands} and {@linkplain
* #setCustomLayout(ControllerInfo, List) custom layout} that are consistently applied to the * #setMediaButtonPreferences(ControllerInfo, List) media button preferences} that are
* media notification on all API levels. * consistently applied to the media notification on all API levels.
* *
* <p>Available {@linkplain SessionCommands session commands} of the media notification controller * <p>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 * are used to enable or disable buttons of the media button preferences before they are passed to
* {@linkplain MediaNotification.Provider#createNotification(MediaSession, ImmutableList, * the {@linkplain MediaNotification.Provider#createNotification(MediaSession, ImmutableList,
* MediaNotification.ActionFactory, MediaNotification.Provider.Callback) notification provider}. * MediaNotification.ActionFactory, MediaNotification.Provider.Callback) notification provider}.
* Disabled command buttons are not converted to notification actions when using {@link * Disabled command buttons are not converted to notification actions when using {@link
* DefaultMediaNotificationProvider}. This affects the media notification displayed by System UI * DefaultMediaNotificationProvider}. This affects the media notification displayed by System UI
@ -925,9 +965,9 @@ public class MediaSession {
* *
* <p>The available session commands of the media notification controller are used to maintain * <p>The available session commands of the media notification controller are used to maintain
* custom actions of the platform session (see {@code PlaybackStateCompat.getCustomActions()}). * 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 * Command buttons of the media button preferences are disabled or enabled according to the
* commands. Disabled command buttons are not converted to custom actions of the platform session. * available session commands. Disabled command buttons are not converted to custom actions of the
* This affects the media notification displayed by System UI <a * platform session. This affects the media notification displayed by System UI <a
* href="https://developer.android.com/about/versions/13/behavior-changes-13#playback-controls">starting * href="https://developer.android.com/about/versions/13/behavior-changes-13#playback-controls">starting
* with API 33</a>. * with API 33</a>.
* *
@ -969,7 +1009,11 @@ public class MediaSession {
} }
/** /**
* Sets the custom layout for the given Media3 controller. * Sets the custom layout for the given controller.
*
* <p>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.
* *
* <p>Make sure to have the session commands of all command buttons of the custom layout * <p>Make sure to have the session commands of all command buttons of the custom layout
* {@linkplain MediaController#getAvailableSessionCommands() available for controllers}. Include * {@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.
*
* <p>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.
* *
* <p>Calling this method broadcasts the custom layout to all connected Media3 controllers, * <p>Calling this method broadcasts the custom layout to all connected Media3 controllers,
* including the {@linkplain #getMediaNotificationControllerInfo() media notification controller}. * 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 * the controller {@linkplain MediaSession.Callback#onConnect connects} by using an {@link
* ConnectionResult.AcceptedResultBuilder}. * 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<CommandButton> layout) { public final void setCustomLayout(List<CommandButton> layout) {
checkNotNull(layout, "layout must not be null"); checkNotNull(layout, "layout must not be null");
impl.setCustomLayout(ImmutableList.copyOf(layout)); impl.setCustomLayout(ImmutableList.copyOf(layout));
} }
/**
* Sets the media button preferences for the given controller.
*
* <p>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.
*
* <p>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}.
*
* <p>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.
*
* <p>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<SessionResult> setMediaButtonPreferences(
ControllerInfo controller, List<CommandButton> 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.
*
* <p>Calling this method broadcasts the media button preferences to all connected Media3
* controllers, including the {@linkplain #getMediaNotificationControllerInfo() media notification
* controller}.
*
* <p>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.
*
* <p>{@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}.
*
* <p>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<CommandButton> mediaButtonPreferences) {
checkNotNull(mediaButtonPreferences, "media button preferences must not be null");
impl.setMediaButtonPreferences(ImmutableList.copyOf(mediaButtonPreferences));
}
/** /**
* Sets the new available commands for the controller. * Sets the new available commands for the controller.
* *
@ -1055,6 +1168,10 @@ public class MediaSession {
/** /**
* Returns the custom layout of the session. * Returns the custom layout of the session.
* *
* <p>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.
*
* <p>For informational purpose only. Mutations on the {@link Bundle} of either a {@link * <p>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 * CommandButton} or a {@link SessionCommand} do not have effect. To change the custom layout use
* {@link #setCustomLayout(List)} or {@link #setCustomLayout(ControllerInfo, List)}. * {@link #setCustomLayout(List)} or {@link #setCustomLayout(ControllerInfo, List)}.
@ -1064,6 +1181,18 @@ public class MediaSession {
return impl.getCustomLayout(); return impl.getCustomLayout();
} }
/**
* Returns the media button preferences of the session.
*
* <p>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<CommandButton> getMediaButtonPreferences() {
return impl.getMediaButtonPreferences();
}
/** /**
* Broadcasts a custom command to all connected controllers. * Broadcasts a custom command to all connected controllers.
* *
@ -1313,7 +1442,8 @@ public class MediaSession {
* *
* <p>If this callback is not overridden, it allows all controllers to connect that can access * <p>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 * 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.
* *
* <p>Note that the player commands in {@link ConnectionResult#availablePlayerCommands} will be * <p>Note that the player commands in {@link ConnectionResult#availablePlayerCommands} will be
* intersected with the {@link Player#getAvailableCommands() available commands} of the * intersected with the {@link Player#getAvailableCommands() available commands} of the
@ -1325,8 +1455,8 @@ public class MediaSession {
* returned by {@link MediaController.Builder#buildAsync()}. * returned by {@link MediaController.Builder#buildAsync()}.
* *
* <p>The controller isn't connected yet, so calls to the controller (e.g. {@link * <p>The controller isn't connected yet, so calls to the controller (e.g. {@link
* #sendCustomCommand}, {@link #setCustomLayout}) will be ignored. Use {@link #onPostConnect} * #sendCustomCommand}, {@link #setMediaButtonPreferences}) will be ignored. Use {@link
* for custom initialization of the controller instead. * #onPostConnect} for custom initialization of the controller instead.
* *
* <p>Interoperability: If a legacy controller is connecting to the session then this callback * <p>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 * 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. * controller.
* *
* <p>Note that calls to the controller (e.g. {@link #sendCustomCommand}, {@link * <p>Note that calls to the controller (e.g. {@link #sendCustomCommand}, {@link
* #setCustomLayout}) work here but don't work in {@link #onConnect} because the controller * #setMediaButtonPreferences}) work here but don't work in {@link #onConnect} because the
* isn't connected yet in {@link #onConnect}. * controller isn't connected yet in {@link #onConnect}.
* *
* @param session The session for this event. * @param session The session for this event.
* @param controller The {@linkplain ControllerInfo controller} information. * @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 * 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 { public static final class ConnectionResult {
@ -1748,6 +1878,7 @@ public class MediaSession {
private SessionCommands availableSessionCommands; private SessionCommands availableSessionCommands;
private Player.Commands availablePlayerCommands = DEFAULT_PLAYER_COMMANDS; private Player.Commands availablePlayerCommands = DEFAULT_PLAYER_COMMANDS;
@Nullable private ImmutableList<CommandButton> customLayout; @Nullable private ImmutableList<CommandButton> customLayout;
@Nullable private ImmutableList<CommandButton> mediaButtonPreferences;
@Nullable private Bundle sessionExtras; @Nullable private Bundle sessionExtras;
@Nullable private PendingIntent sessionActivity; @Nullable private PendingIntent sessionActivity;
@ -1799,13 +1930,17 @@ public class MediaSession {
* Sets the custom layout, overriding the {@linkplain MediaSession#getCustomLayout() custom * Sets the custom layout, overriding the {@linkplain MediaSession#getCustomLayout() custom
* layout of the session}. * layout of the session}.
* *
* <p>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.
*
* <p>The default is null to indicate that the custom layout of the session should be used. * <p>The default is null to indicate that the custom layout of the session should be used.
* *
* <p>Make sure to have the session commands of all command buttons of the custom layout * <p>Make sure to have the session commands of all command buttons of the custom layout
* included in the {@linkplain #setAvailableSessionCommands(SessionCommands)} available * included in the {@linkplain #setAvailableSessionCommands(SessionCommands) available session
* session commands} On the controller side, the {@linkplain CommandButton#isEnabled enabled} * commands}. On the controller side, the {@linkplain CommandButton#isEnabled enabled} flag is
* flag is set to {@code false} if the available commands of the controller do not allow to * set to {@code false} if the available commands of the controller do not allow to use a
* use a button. * button.
*/ */
@CanIgnoreReturnValue @CanIgnoreReturnValue
public AcceptedResultBuilder setCustomLayout(@Nullable List<CommandButton> customLayout) { public AcceptedResultBuilder setCustomLayout(@Nullable List<CommandButton> customLayout) {
@ -1813,6 +1948,27 @@ public class MediaSession {
return this; return this;
} }
/**
* Sets the media button preferences, overriding the {@linkplain
* MediaSession#getMediaButtonPreferences() media button preferences of the session}.
*
* <p>The default is null to indicate that the media button preferences of the session should
* be used.
*
* <p>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<CommandButton> mediaButtonPreferences) {
this.mediaButtonPreferences =
mediaButtonPreferences == null ? null : ImmutableList.copyOf(mediaButtonPreferences);
return this;
}
/** /**
* Sets the session extras, overriding the {@linkplain MediaSession#getSessionExtras() extras * Sets the session extras, overriding the {@linkplain MediaSession#getSessionExtras() extras
* of the session}. * of the session}.
@ -1844,6 +2000,7 @@ public class MediaSession {
availableSessionCommands, availableSessionCommands,
availablePlayerCommands, availablePlayerCommands,
customLayout, customLayout,
mediaButtonPreferences,
sessionExtras, sessionExtras,
sessionActivity); sessionActivity);
} }
@ -1873,6 +2030,12 @@ public class MediaSession {
/** The custom layout or null if the custom layout of the session should be used. */ /** The custom layout or null if the custom layout of the session should be used. */
@UnstableApi @Nullable public final ImmutableList<CommandButton> customLayout; @UnstableApi @Nullable public final ImmutableList<CommandButton> customLayout;
/**
* The media button preferences or null if the media button preferences of the session should be
* used.
*/
@UnstableApi @Nullable public final ImmutableList<CommandButton> mediaButtonPreferences;
/** The session extras. */ /** The session extras. */
@UnstableApi @Nullable public final Bundle sessionExtras; @UnstableApi @Nullable public final Bundle sessionExtras;
@ -1885,12 +2048,14 @@ public class MediaSession {
SessionCommands availableSessionCommands, SessionCommands availableSessionCommands,
Player.Commands availablePlayerCommands, Player.Commands availablePlayerCommands,
@Nullable ImmutableList<CommandButton> customLayout, @Nullable ImmutableList<CommandButton> customLayout,
@Nullable ImmutableList<CommandButton> mediaButtonPreferences,
@Nullable Bundle sessionExtras, @Nullable Bundle sessionExtras,
@Nullable PendingIntent sessionActivity) { @Nullable PendingIntent sessionActivity) {
isAccepted = accepted; isAccepted = accepted;
this.availableSessionCommands = availableSessionCommands; this.availableSessionCommands = availableSessionCommands;
this.availablePlayerCommands = availablePlayerCommands; this.availablePlayerCommands = availablePlayerCommands;
this.customLayout = customLayout; this.customLayout = customLayout;
this.mediaButtonPreferences = mediaButtonPreferences;
this.sessionExtras = sessionExtras; this.sessionExtras = sessionExtras;
this.sessionActivity = sessionActivity; this.sessionActivity = sessionActivity;
} }
@ -1900,8 +2065,8 @@ public class MediaSession {
* *
* <p>Commands are specific to the controller receiving this connection result. * <p>Commands are specific to the controller receiving this connection result.
* *
* <p>The controller receives {@linkplain MediaSession#getCustomLayout() the custom layout of * <p>The controller receives {@linkplain MediaSession#getMediaButtonPreferences() the media
* the session}. * button preferences of the session}.
* *
* <p>See {@link AcceptedResultBuilder} for a more flexible way to accept a connection. * <p>See {@link AcceptedResultBuilder} for a more flexible way to accept a connection.
*/ */
@ -1912,6 +2077,7 @@ public class MediaSession {
availableSessionCommands, availableSessionCommands,
availablePlayerCommands, availablePlayerCommands,
/* customLayout= */ null, /* customLayout= */ null,
/* mediaButtonPreferences= */ null,
/* sessionExtras= */ null, /* sessionExtras= */ null,
/* sessionActivity= */ null); /* sessionActivity= */ null);
} }
@ -1923,6 +2089,7 @@ public class MediaSession {
SessionCommands.EMPTY, SessionCommands.EMPTY,
Player.Commands.EMPTY, Player.Commands.EMPTY,
/* customLayout= */ ImmutableList.of(), /* customLayout= */ ImmutableList.of(),
/* mediaButtonPreferences= */ ImmutableList.of(),
/* sessionExtras= */ Bundle.EMPTY, /* sessionExtras= */ Bundle.EMPTY,
/* sessionActivity= */ null); /* sessionActivity= */ null);
} }
@ -1943,8 +2110,7 @@ public class MediaSession {
PlayerInfo playerInfo, PlayerInfo playerInfo,
Player.Commands availableCommands, Player.Commands availableCommands,
boolean excludeTimeline, boolean excludeTimeline,
boolean excludeTracks, boolean excludeTracks)
int controllerInterfaceVersion)
throws RemoteException {} throws RemoteException {}
default void onPeriodicSessionPositionInfoChanged( default void onPeriodicSessionPositionInfoChanged(
@ -1961,6 +2127,9 @@ public class MediaSession {
default void setCustomLayout(int seq, List<CommandButton> layout) throws RemoteException {} default void setCustomLayout(int seq, List<CommandButton> layout) throws RemoteException {}
default void setMediaButtonPreferences(int seq, List<CommandButton> mediaButtonPreferences)
throws RemoteException {}
default void onSessionActivityChanged(int seq, PendingIntent sessionActivity) default void onSessionActivityChanged(int seq, PendingIntent sessionActivity)
throws RemoteException {} throws RemoteException {}
@ -2105,6 +2274,7 @@ public class MediaSession {
/* package */ @MonotonicNonNull BitmapLoader bitmapLoader; /* package */ @MonotonicNonNull BitmapLoader bitmapLoader;
/* package */ boolean playIfSuppressed; /* package */ boolean playIfSuppressed;
/* package */ ImmutableList<CommandButton> customLayout; /* package */ ImmutableList<CommandButton> customLayout;
/* package */ ImmutableList<CommandButton> mediaButtonPreferences;
/* package */ ImmutableList<CommandButton> commandButtonsForMediaItems; /* package */ ImmutableList<CommandButton> commandButtonsForMediaItems;
/* package */ boolean isPeriodicPositionUpdateEnabled; /* package */ boolean isPeriodicPositionUpdateEnabled;
@ -2117,6 +2287,7 @@ public class MediaSession {
tokenExtras = Bundle.EMPTY; tokenExtras = Bundle.EMPTY;
sessionExtras = Bundle.EMPTY; sessionExtras = Bundle.EMPTY;
customLayout = ImmutableList.of(); customLayout = ImmutableList.of();
mediaButtonPreferences = ImmutableList.of();
playIfSuppressed = true; playIfSuppressed = true;
isPeriodicPositionUpdateEnabled = true; isPeriodicPositionUpdateEnabled = true;
commandButtonsForMediaItems = ImmutableList.of(); commandButtonsForMediaItems = ImmutableList.of();
@ -2174,6 +2345,13 @@ public class MediaSession {
return (BuilderT) this; return (BuilderT) this;
} }
@CanIgnoreReturnValue
@SuppressWarnings("unchecked")
public BuilderT setMediaButtonPreferences(List<CommandButton> mediaButtonPreferences) {
this.mediaButtonPreferences = ImmutableList.copyOf(mediaButtonPreferences);
return (BuilderT) this;
}
@CanIgnoreReturnValue @CanIgnoreReturnValue
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public BuilderT setShowPlayButtonIfPlaybackIsSuppressed( public BuilderT setShowPlayButtonIfPlaybackIsSuppressed(

View File

@ -152,6 +152,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private long sessionPositionUpdateDelayMs; private long sessionPositionUpdateDelayMs;
private boolean isMediaNotificationControllerConnected; private boolean isMediaNotificationControllerConnected;
private ImmutableList<CommandButton> customLayout; private ImmutableList<CommandButton> customLayout;
private ImmutableList<CommandButton> mediaButtonPreferences;
private Bundle sessionExtras; private Bundle sessionExtras;
@SuppressWarnings("argument.type.incompatible") // Using this in System.identityHashCode @SuppressWarnings("argument.type.incompatible") // Using this in System.identityHashCode
@ -162,6 +163,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
Player player, Player player,
@Nullable PendingIntent sessionActivity, @Nullable PendingIntent sessionActivity,
ImmutableList<CommandButton> customLayout, ImmutableList<CommandButton> customLayout,
ImmutableList<CommandButton> mediaButtonPreferences,
ImmutableList<CommandButton> commandButtonsForMediaItems, ImmutableList<CommandButton> commandButtonsForMediaItems,
MediaSession.Callback callback, MediaSession.Callback callback,
Bundle tokenExtras, Bundle tokenExtras,
@ -183,6 +185,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
sessionId = id; sessionId = id;
this.sessionActivity = sessionActivity; this.sessionActivity = sessionActivity;
this.customLayout = customLayout; this.customLayout = customLayout;
this.mediaButtonPreferences = mediaButtonPreferences;
this.commandButtonsForMediaItems = commandButtonsForMediaItems; this.commandButtonsForMediaItems = commandButtonsForMediaItems;
this.callback = callback; this.callback = callback;
this.sessionExtras = sessionExtras; this.sessionExtras = sessionExtras;
@ -246,6 +249,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
player, player,
playIfSuppressed, playIfSuppressed,
customLayout, customLayout,
mediaButtonPreferences,
connectionResult.availableSessionCommands, connectionResult.availableSessionCommands,
connectionResult.availablePlayerCommands, connectionResult.availablePlayerCommands,
sessionExtras); sessionExtras);
@ -272,6 +276,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
player, player,
playIfSuppressed, playIfSuppressed,
playerWrapper.getCustomLayout(), playerWrapper.getCustomLayout(),
playerWrapper.getMediaButtonPreferences(),
playerWrapper.getAvailableSessionCommands(), playerWrapper.getAvailableSessionCommands(),
playerWrapper.getAvailablePlayerCommands(), playerWrapper.getAvailablePlayerCommands(),
playerWrapper.getLegacyExtras())); playerWrapper.getLegacyExtras()));
@ -511,11 +516,45 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
(controller, seq) -> controller.setCustomLayout(seq, customLayout)); (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<SessionResult> setMediaButtonPreferences(
ControllerInfo controller, ImmutableList<CommandButton> 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<CommandButton> mediaButtonPreferences) {
this.mediaButtonPreferences = mediaButtonPreferences;
playerWrapper.setMediaButtonPreferences(mediaButtonPreferences);
dispatchRemoteControllerTaskWithoutReturn(
(controller, seq) -> controller.setMediaButtonPreferences(seq, mediaButtonPreferences));
}
/** Returns the custom layout. */ /** Returns the custom layout. */
public ImmutableList<CommandButton> getCustomLayout() { public ImmutableList<CommandButton> getCustomLayout() {
return customLayout; return customLayout;
} }
/** Returns the media button preferences. */
public ImmutableList<CommandButton> getMediaButtonPreferences() {
return mediaButtonPreferences;
}
/** Returns the command buttons for media items. */ /** Returns the command buttons for media items. */
public ImmutableList<CommandButton> getCommandButtonsForMediaItems() { public ImmutableList<CommandButton> getCommandButtonsForMediaItems() {
return commandButtonsForMediaItems; return commandButtonsForMediaItems;
@ -612,12 +651,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
getPlayerWrapper().getAvailableCommands()); getPlayerWrapper().getAvailableCommands());
checkStateNotNull(controller.getControllerCb()) checkStateNotNull(controller.getControllerCb())
.onPlayerInfoChanged( .onPlayerInfoChanged(
seq, seq, playerInfo, intersectedCommands, excludeTimeline, excludeTracks);
playerInfo,
intersectedCommands,
excludeTimeline,
excludeTracks,
controller.getInterfaceVersion());
} catch (DeadObjectException e) { } catch (DeadObjectException e) {
onDeadObjectException(controller); onDeadObjectException(controller);
} catch (RemoteException e) { } catch (RemoteException e) {
@ -679,6 +713,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
.setAvailableSessionCommands(playerWrapper.getAvailableSessionCommands()) .setAvailableSessionCommands(playerWrapper.getAvailableSessionCommands())
.setAvailablePlayerCommands(playerWrapper.getAvailablePlayerCommands()) .setAvailablePlayerCommands(playerWrapper.getAvailablePlayerCommands())
.setCustomLayout(playerWrapper.getCustomLayout()) .setCustomLayout(playerWrapper.getCustomLayout())
.setMediaButtonPreferences(playerWrapper.getMediaButtonPreferences())
.build(); .build();
} }
MediaSession.ConnectionResult connectionResult = MediaSession.ConnectionResult connectionResult =
@ -691,6 +726,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
connectionResult.customLayout != null connectionResult.customLayout != null
? connectionResult.customLayout ? connectionResult.customLayout
: instance.getCustomLayout()); : instance.getCustomLayout());
playerWrapper.setMediaButtonPreferences(
connectionResult.mediaButtonPreferences != null
? connectionResult.mediaButtonPreferences
: instance.getMediaButtonPreferences());
setAvailableFrameworkControllerCommands( setAvailableFrameworkControllerCommands(
connectionResult.availableSessionCommands, connectionResult.availablePlayerCommands); connectionResult.availableSessionCommands, connectionResult.availablePlayerCommands);
} }

View File

@ -1115,6 +1115,11 @@ import org.checkerframework.checker.initialization.qual.Initialized;
updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper()); updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper());
} }
@Override
public void setMediaButtonPreferences(int seq, List<CommandButton> mediaButtonPreferences) {
updateLegacySessionPlaybackState(sessionImpl.getPlayerWrapper());
}
@Override @Override
public void onSessionExtrasChanged(int seq, Bundle sessionExtras) { public void onSessionExtrasChanged(int seq, Bundle sessionExtras) {
sessionCompat.setExtras(sessionExtras); sessionCompat.setExtras(sessionExtras);

View File

@ -761,7 +761,8 @@ public abstract class MediaSessionService extends Service {
request.libraryVersion, request.libraryVersion,
request.controllerInterfaceVersion, request.controllerInterfaceVersion,
isTrusted, isTrusted,
new MediaSessionStub.Controller2Cb(caller), new MediaSessionStub.Controller2Cb(
caller, request.controllerInterfaceVersion),
request.connectionHints, request.connectionHints,
request.maxCommandsForMediaItems); request.maxCommandsForMediaItems);

View File

@ -535,6 +535,9 @@ import java.util.concurrent.ExecutionException;
connectionResult.customLayout != null connectionResult.customLayout != null
? connectionResult.customLayout ? connectionResult.customLayout
: sessionImpl.getCustomLayout(), : sessionImpl.getCustomLayout(),
connectionResult.mediaButtonPreferences != null
? connectionResult.mediaButtonPreferences
: sessionImpl.getMediaButtonPreferences(),
sessionImpl.getCommandButtonsForMediaItems(), sessionImpl.getCommandButtonsForMediaItems(),
connectionResult.availableSessionCommands, connectionResult.availableSessionCommands,
connectionResult.availablePlayerCommands, connectionResult.availablePlayerCommands,
@ -636,7 +639,7 @@ import java.util.concurrent.ExecutionException;
request.libraryVersion, request.libraryVersion,
request.controllerInterfaceVersion, request.controllerInterfaceVersion,
sessionManager.isTrustedForMediaControl(remoteUserInfo), sessionManager.isTrustedForMediaControl(remoteUserInfo),
new MediaSessionStub.Controller2Cb(caller), new MediaSessionStub.Controller2Cb(caller, request.controllerInterfaceVersion),
request.connectionHints, request.connectionHints,
request.maxCommandsForMediaItems); request.maxCommandsForMediaItems);
connect(caller, controllerInfo); connect(caller, controllerInfo);
@ -2006,9 +2009,11 @@ import java.util.concurrent.ExecutionException;
/* package */ static final class Controller2Cb implements ControllerCb { /* package */ static final class Controller2Cb implements ControllerCb {
private final IMediaController iController; private final IMediaController iController;
private final int controllerInterfaceVersion;
public Controller2Cb(IMediaController callback) { public Controller2Cb(IMediaController callback, int controllerInterfaceVersion) {
iController = callback; this.iController = callback;
this.controllerInterfaceVersion = controllerInterfaceVersion;
} }
public IBinder getCallbackBinder() { public IBinder getCallbackBinder() {
@ -2032,8 +2037,7 @@ import java.util.concurrent.ExecutionException;
PlayerInfo playerInfo, PlayerInfo playerInfo,
Player.Commands availableCommands, Player.Commands availableCommands,
boolean excludeTimeline, boolean excludeTimeline,
boolean excludeTracks, boolean excludeTracks)
int controllerInterfaceVersion)
throws RemoteException { throws RemoteException {
Assertions.checkState(controllerInterfaceVersion != 0); Assertions.checkState(controllerInterfaceVersion != 0);
// The bundling exclusions merge the performance overrides with the available commands. // 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)); sequenceNumber, BundleCollectionUtil.toBundleList(layout, CommandButton::toBundle));
} }
@Override
public void setMediaButtonPreferences(
int sequenceNumber, List<CommandButton> 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 @Override
public void onSessionActivityChanged(int sequenceNumber, PendingIntent sessionActivity) public void onSessionActivityChanged(int sequenceNumber, PendingIntent sessionActivity)
throws RemoteException { throws RemoteException {

View File

@ -87,6 +87,7 @@ import java.util.List;
@Nullable private LegacyError legacyError; @Nullable private LegacyError legacyError;
@Nullable private Bundle legacyExtras; @Nullable private Bundle legacyExtras;
private ImmutableList<CommandButton> customLayout; private ImmutableList<CommandButton> customLayout;
private ImmutableList<CommandButton> mediaButtonPreferences;
private SessionCommands availableSessionCommands; private SessionCommands availableSessionCommands;
private Commands availablePlayerCommands; private Commands availablePlayerCommands;
@ -94,12 +95,14 @@ import java.util.List;
Player player, Player player,
boolean playIfSuppressed, boolean playIfSuppressed,
ImmutableList<CommandButton> customLayout, ImmutableList<CommandButton> customLayout,
ImmutableList<CommandButton> mediaButtonPreferences,
SessionCommands availableSessionCommands, SessionCommands availableSessionCommands,
Commands availablePlayerCommands, Commands availablePlayerCommands,
@Nullable Bundle legacyExtras) { @Nullable Bundle legacyExtras) {
super(player); super(player);
this.playIfSuppressed = playIfSuppressed; this.playIfSuppressed = playIfSuppressed;
this.customLayout = customLayout; this.customLayout = customLayout;
this.mediaButtonPreferences = mediaButtonPreferences;
this.availableSessionCommands = availableSessionCommands; this.availableSessionCommands = availableSessionCommands;
this.availablePlayerCommands = availablePlayerCommands; this.availablePlayerCommands = availablePlayerCommands;
this.legacyExtras = legacyExtras; this.legacyExtras = legacyExtras;
@ -123,10 +126,18 @@ import java.util.List;
this.customLayout = customLayout; this.customLayout = customLayout;
} }
public void setMediaButtonPreferences(ImmutableList<CommandButton> mediaButtonPreferences) {
this.mediaButtonPreferences = mediaButtonPreferences;
}
/* package */ ImmutableList<CommandButton> getCustomLayout() { /* package */ ImmutableList<CommandButton> getCustomLayout() {
return customLayout; return customLayout;
} }
/* package */ ImmutableList<CommandButton> getMediaButtonPreferences() {
return mediaButtonPreferences;
}
public void setLegacyExtras(@Nullable Bundle extras) { public void setLegacyExtras(@Nullable Bundle extras) {
if (extras != null) { if (extras != null) {
checkArgument(!extras.containsKey(EXTRAS_KEY_PLAYBACK_SPEED_COMPAT)); checkArgument(!extras.containsKey(EXTRAS_KEY_PLAYBACK_SPEED_COMPAT));
@ -1061,8 +1072,11 @@ import java.util.List;
.setBufferedPosition(compatBufferedPosition) .setBufferedPosition(compatBufferedPosition)
.setExtras(extras); .setExtras(extras);
for (int i = 0; i < customLayout.size(); i++) { // TODO: b/332877990 - More accurately reflect media button preferences as custom actions.
CommandButton commandButton = customLayout.get(i); List<CommandButton> buttonsForCustomActions =
mediaButtonPreferences.isEmpty() ? customLayout : mediaButtonPreferences;
for (int i = 0; i < buttonsForCustomActions.size(); i++) {
CommandButton commandButton = buttonsForCustomActions.get(i);
SessionCommand sessionCommand = commandButton.sessionCommand; SessionCommand sessionCommand = commandButton.sessionCommand;
if (sessionCommand != null if (sessionCommand != null
&& commandButton.isEnabled && commandButton.isEnabled

View File

@ -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();
}
}

View File

@ -1080,12 +1080,12 @@ public final class LegacyConversionsTest {
} }
@Test @Test
public void convertToCustomLayout_withNull_returnsEmptyList() { public void convertToMediaButtonPreferences_withNull_returnsEmptyList() {
assertThat(LegacyConversions.convertToCustomLayout(null)).isEmpty(); assertThat(LegacyConversions.convertToMediaButtonPreferences(null)).isEmpty();
} }
@Test @Test
public void convertToCustomLayout_withoutIconConstantInExtras() { public void convertToMediaButtonPreferences_withoutIconConstantInExtras() {
String extraKey = "key"; String extraKey = "key";
String extraValue = "value"; String extraValue = "value";
String actionStr = "action"; String actionStr = "action";
@ -1107,7 +1107,7 @@ public final class LegacyConversionsTest {
.addCustomAction(action) .addCustomAction(action)
.build(); .build();
ImmutableList<CommandButton> buttons = LegacyConversions.convertToCustomLayout(state); ImmutableList<CommandButton> buttons = LegacyConversions.convertToMediaButtonPreferences(state);
assertThat(buttons).hasSize(1); assertThat(buttons).hasSize(1);
CommandButton button = buttons.get(0); CommandButton button = buttons.get(0);
@ -1120,7 +1120,7 @@ public final class LegacyConversionsTest {
} }
@Test @Test
public void convertToCustomLayout_withIconConstantInExtras() { public void convertToMediaButtonPreferences_withIconConstantInExtras() {
String actionStr = "action"; String actionStr = "action";
String displayName = "display_name"; String displayName = "display_name";
int iconRes = 21; int iconRes = 21;
@ -1140,7 +1140,7 @@ public final class LegacyConversionsTest {
.addCustomAction(action) .addCustomAction(action)
.build(); .build();
ImmutableList<CommandButton> buttons = LegacyConversions.convertToCustomLayout(state); ImmutableList<CommandButton> buttons = LegacyConversions.convertToMediaButtonPreferences(state);
assertThat(buttons).hasSize(1); assertThat(buttons).hasSize(1);
CommandButton button = buttons.get(0); CommandButton button = buttons.get(0);

View File

@ -259,6 +259,112 @@ public class MediaSessionServiceTest {
serviceController.destroy(); 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<TestService> 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 @Test
public void mediaNotificationController_setAvailableCommands_correctNotificationActions() public void mediaNotificationController_setAvailableCommands_correctNotificationActions()
throws TimeoutException { throws TimeoutException {
@ -281,7 +387,7 @@ public class MediaSessionServiceTest {
MediaSession session = MediaSession session =
new MediaSession.Builder(context, player) new MediaSession.Builder(context, player)
.setId("1") .setId("1")
.setCustomLayout(ImmutableList.of(button1, button2)) .setMediaButtonPreferences(ImmutableList.of(button1, button2))
.setCallback( .setCallback(
new MediaSession.Callback() { new MediaSession.Callback() {
@Override @Override

View File

@ -47,7 +47,8 @@ public class PlayerWrapperTest {
new PlayerWrapper( new PlayerWrapper(
player, player,
/* playIfSuppressed= */ true, /* playIfSuppressed= */ true,
ImmutableList.of(), /* customLayout= */ ImmutableList.of(),
/* mediaButtonPreferences= */ ImmutableList.of(),
SessionCommands.EMPTY, SessionCommands.EMPTY,
Player.Commands.EMPTY, Player.Commands.EMPTY,
/* legacyExtras= */ null); /* legacyExtras= */ null);

View File

@ -31,6 +31,7 @@ interface IRemoteMediaController {
Bundle getConnectedSessionToken(String controllerId); Bundle getConnectedSessionToken(String controllerId);
Bundle getSessionExtras(String controllerId); Bundle getSessionExtras(String controllerId);
Bundle getCustomLayout(String controllerId); Bundle getCustomLayout(String controllerId);
Bundle getMediaButtonPreferences(String controllerId);
Bundle getAvailableCommands(String controllerId); Bundle getAvailableCommands(String controllerId);
PendingIntent getSessionActivity(String controllerId); PendingIntent getSessionActivity(String controllerId);
void play(String controllerId); void play(String controllerId);

View File

@ -32,6 +32,7 @@ interface IRemoteMediaSession {
void release(String sessionId); void release(String sessionId);
void setAvailableCommands(String sessionId, in Bundle sessionCommands, in Bundle playerCommands); void setAvailableCommands(String sessionId, in Bundle sessionCommands, in Bundle playerCommands);
void setCustomLayout(String sessionId, in List<Bundle> layout); void setCustomLayout(String sessionId, in List<Bundle> layout);
void setMediaButtonPreferences(String sessionId, in List<Bundle> mediaButtonPreferences);
void setSessionExtras(String sessionId, in Bundle extras); void setSessionExtras(String sessionId, in Bundle extras);
void setSessionExtrasForController(String sessionId, in String controllerKey, in Bundle extras); void setSessionExtrasForController(String sessionId, in String controllerKey, in Bundle extras);
void sendError(String sessionId, String controllerKey, in Bundle SessionError); void sendError(String sessionId, String controllerKey, in Bundle SessionError);

View File

@ -1461,7 +1461,11 @@ public class MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest
.build(); .build();
} }
}; };
MediaSession mediaSession = createMediaSession(player, callback, customLayout); MediaSession mediaSession =
new MediaSession.Builder(ApplicationProvider.getApplicationContext(), player)
.setCallback(callback)
.setCustomLayout(customLayout)
.build();
connectMediaNotificationController(mediaSession); connectMediaNotificationController(mediaSession);
MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession);
@ -1621,6 +1625,212 @@ public class MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest
releasePlayer(player); 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<CommandButton> 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<CommandButton> 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<PlaybackStateCompat.CustomAction> initialCustomActions =
controllerCompat.getPlaybackState().getCustomActions();
AtomicReference<List<PlaybackStateCompat.CustomAction>> 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<CommandButton> 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<PlaybackStateCompat.CustomAction> initialCustomActions =
controllerCompat.getPlaybackState().getCustomActions();
AtomicReference<List<PlaybackStateCompat.CustomAction>> 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 * Connect a controller that mimics the media notification controller that is connected by {@link
* MediaNotificationManager} when the session is running in the service. * MediaNotificationManager} when the session is running in the service.
@ -1671,14 +1881,8 @@ public class MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest
private static MediaSession createMediaSession( private static MediaSession createMediaSession(
Player player, @Nullable MediaSession.Callback callback) { Player player, @Nullable MediaSession.Callback callback) {
return createMediaSession(player, callback, /* customLayout= */ ImmutableList.of());
}
private static MediaSession createMediaSession(
Player player, @Nullable MediaSession.Callback callback, List<CommandButton> customLayout) {
MediaSession.Builder session = MediaSession.Builder session =
new MediaSession.Builder(ApplicationProvider.getApplicationContext(), player) new MediaSession.Builder(ApplicationProvider.getApplicationContext(), player);
.setCustomLayout(customLayout);
if (callback != null) { if (callback != null) {
session.setCallback(callback); session.setCallback(callback);
} }

View File

@ -552,6 +552,74 @@ public class MediaControllerListenerWithMediaSessionCompatTest {
.inOrder(); .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<List<CommandButton>> onMediaButtonPreferencesChangedArguments = new ArrayList<>();
List<List<CommandButton>> mediaButtonPreferencesFromGetter = new ArrayList<>();
controllerTestRule.createController(
session.getSessionToken(),
new MediaController.Listener() {
@Override
public void onMediaButtonPreferencesChanged(
MediaController controller, List<CommandButton> 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<CommandButton> expectedFirstMediaButtonPreferences =
ImmutableList.of(button1.copyWithIsEnabled(true), button2.copyWithIsEnabled(true));
ImmutableList<CommandButton> expectedSecondMediaButtonPreferences =
ImmutableList.of(button1.copyWithIsEnabled(true));
assertThat(onMediaButtonPreferencesChangedArguments)
.containsExactly(expectedFirstMediaButtonPreferences, expectedSecondMediaButtonPreferences)
.inOrder();
assertThat(mediaButtonPreferencesFromGetter)
.containsExactly(expectedFirstMediaButtonPreferences, expectedSecondMediaButtonPreferences)
.inOrder();
}
@Test @Test
public void getCurrentPosition_unknownPlaybackPosition_convertedToZero() throws Exception { public void getCurrentPosition_unknownPlaybackPosition_convertedToZero() throws Exception {
session.setPlaybackState( session.setPlaybackState(

View File

@ -405,6 +405,7 @@ public class MediaControllerTest {
button3.copyWithIsEnabled(false), button3.copyWithIsEnabled(false),
button4.copyWithIsEnabled(true)) button4.copyWithIsEnabled(true))
.inOrder(); .inOrder();
session.cleanUp();
} }
@Test @Test
@ -452,6 +453,7 @@ public class MediaControllerTest {
assertThat(getterCustomLayouts).hasSize(2); assertThat(getterCustomLayouts).hasSize(2);
assertThat(getterCustomLayouts.get(0)).containsExactly(button.copyWithIsEnabled(false)); assertThat(getterCustomLayouts.get(0)).containsExactly(button.copyWithIsEnabled(false));
assertThat(getterCustomLayouts.get(1)).containsExactly(button.copyWithIsEnabled(true)); assertThat(getterCustomLayouts.get(1)).containsExactly(button.copyWithIsEnabled(true));
session.cleanUp();
} }
@Test @Test
@ -550,6 +552,369 @@ public class MediaControllerTest {
session.cleanUp(); 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<List<CommandButton>> reportedMediaButtonPreferences = new AtomicReference<>();
MediaController controller =
controllerTestRule.createController(
session.getToken(),
Bundle.EMPTY,
new MediaController.Listener() {
@Override
public void onMediaButtonPreferencesChanged(
MediaController controller1, List<CommandButton> layout) {
reportedMediaButtonPreferences.set(layout);
latch.countDown();
}
});
ImmutableList<CommandButton> initialMediaButtonPreferencesFromGetter =
threadTestRule.getHandler().postAndSync(controller::getMediaButtonPreferences);
session.setMediaButtonPreferences(
ImmutableList.of(button1, button2, button4, button5, button6));
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
ImmutableList<CommandButton> newMediaButtonPreferencesFromGetter =
threadTestRule.getHandler().postAndSync(controller::getMediaButtonPreferences);
assertThat(initialMediaButtonPreferencesFromGetter)
.containsExactly(button1.copyWithIsEnabled(true), button3.copyWithIsEnabled(false))
.inOrder();
ImmutableList<CommandButton> 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<List<CommandButton>> reportedMediaButtonPreferencesChanged = new ArrayList<>();
List<List<CommandButton>> getterMediaButtonPreferencesChanged = new ArrayList<>();
MediaController.Listener listener =
new MediaController.Listener() {
@Override
public void onMediaButtonPreferencesChanged(
MediaController controller, List<CommandButton> layout) {
reportedMediaButtonPreferencesChanged.add(layout);
getterMediaButtonPreferencesChanged.add(controller.getMediaButtonPreferences());
latch.countDown();
}
};
MediaController controller =
controllerTestRule.createController(
session.getToken(), /* connectionHints= */ Bundle.EMPTY, listener);
ImmutableList<CommandButton> 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<List<CommandButton>> reportedMediaButtonPreferences = new ArrayList<>();
List<List<CommandButton>> getterMediaButtonPreferences = new ArrayList<>();
MediaController.Listener listener =
new MediaController.Listener() {
@Override
public void onMediaButtonPreferencesChanged(
MediaController controller, List<CommandButton> layout) {
reportedMediaButtonPreferences.add(layout);
getterMediaButtonPreferences.add(controller.getMediaButtonPreferences());
latch.countDown();
}
};
MediaController controller =
controllerTestRule.createController(
session.getToken(), /* connectionHints= */ Bundle.EMPTY, listener);
ImmutableList<CommandButton> 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<List<CommandButton>> reportedMediaButtonPreferences = new ArrayList<>();
List<List<CommandButton>> getterMediaButtonPreferences = new ArrayList<>();
MediaController.Listener listener =
new MediaController.Listener() {
@Override
public void onMediaButtonPreferencesChanged(
MediaController controller, List<CommandButton> layout) {
reportedMediaButtonPreferences.add(layout);
getterMediaButtonPreferences.add(controller.getMediaButtonPreferences());
latch.countDown();
}
};
MediaController controller =
controllerTestRule.createController(session.getToken(), Bundle.EMPTY, listener);
ImmutableList<CommandButton> 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 @Test
public void getCommandButtonsForMediaItem() throws Exception { public void getCommandButtonsForMediaItem() throws Exception {
RemoteMediaSession session = RemoteMediaSession session =
@ -2035,4 +2400,22 @@ public class MediaControllerTest {
session.setCustomLayout(ImmutableList.copyOf(customLayout)); session.setCustomLayout(ImmutableList.copyOf(customLayout));
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
} }
private void setupMediaButtonPreferences(
RemoteMediaSession session, List<CommandButton> 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<CommandButton> layout) {
latch.countDown();
}
});
session.setMediaButtonPreferences(ImmutableList.copyOf(mediaButtonPreferences));
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
}
} }

View File

@ -154,7 +154,7 @@ public class MediaSessionCallbackTest {
} }
@Test @Test
public void onConnect_acceptWithMissingSessionCommand_buttonDisabledAndPermissionDenied() public void onConnect_setCustomLayoutWithMissingSessionCommand_buttonDisabledAndPermissionDenied()
throws Exception { throws Exception {
CommandButton button1 = CommandButton button1 =
new CommandButton.Builder(CommandButton.ICON_PLAY) new CommandButton.Builder(CommandButton.ICON_PLAY)
@ -169,7 +169,6 @@ public class MediaSessionCallbackTest {
.setSessionCommand(new SessionCommand("command2", Bundle.EMPTY)) .setSessionCommand(new SessionCommand("command2", Bundle.EMPTY))
.setEnabled(true) .setEnabled(true)
.build(); .build();
ImmutableList<CommandButton> customLayout = ImmutableList.of(button1, button2);
MediaSession.Callback callback = MediaSession.Callback callback =
new MediaSession.Callback() { new MediaSession.Callback() {
@Override @Override
@ -193,12 +192,7 @@ public class MediaSessionCallbackTest {
}; };
MediaSession session = MediaSession session =
sessionTestRule.ensureReleaseAfterTest( sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player) new MediaSession.Builder(context, player).setCallback(callback).build());
.setCallback(callback)
.setCustomLayout(customLayout)
.setId(
"onConnect_acceptWithMissingSessionCommand_buttonDisabledAndPermissionDenied")
.build());
RemoteMediaController remoteController = RemoteMediaController remoteController =
remoteControllerTestRule.createRemoteController(session.getToken()); remoteControllerTestRule.createRemoteController(session.getToken());
@ -211,6 +205,60 @@ public class MediaSessionCallbackTest {
.isEqualTo(RESULT_SUCCESS); .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<SessionResult> 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<CommandButton> 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 @Test
public void onConnect_emptyPlayerCommands_commandReleaseAlwaysIncluded() throws Exception { public void onConnect_emptyPlayerCommands_commandReleaseAlwaysIncluded() throws Exception {
MediaSession.Callback callback = MediaSession.Callback callback =

View File

@ -28,6 +28,7 @@ import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat; import android.support.v4.media.session.PlaybackStateCompat;
import androidx.media3.common.ForwardingPlayer; import androidx.media3.common.ForwardingPlayer;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
@ -236,8 +237,7 @@ public class MediaSessionServiceTest {
} }
@Test @Test
public void onCreate_mediaNotificationManagerController_correctSessionStateFromOnConnect() public void onCreate_withCustomLayout_correctSessionStateFromOnConnect() throws Exception {
throws Exception {
SessionCommand command1 = new SessionCommand("command1", Bundle.EMPTY); SessionCommand command1 = new SessionCommand("command1", Bundle.EMPTY);
SessionCommand command2 = new SessionCommand("command2", Bundle.EMPTY); SessionCommand command2 = new SessionCommand("command2", Bundle.EMPTY);
SessionCommand command3 = new SessionCommand("command3", Bundle.EMPTY); SessionCommand command3 = new SessionCommand("command3", Bundle.EMPTY);
@ -349,6 +349,121 @@ public class MediaSessionServiceTest {
.blockUntilAllControllersUnbind(TIMEOUT_MS); .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<ControllerInfo> 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<PlaybackStateCompat.CustomAction> 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 * Tests whether {@link MediaSessionService#onGetSession(ControllerInfo)} is called when
* controller tries to connect, with the proper arguments. * controller tries to connect, with the proper arguments.

View File

@ -866,6 +866,20 @@ public class MediaControllerProviderService extends Service {
return bundle; return bundle;
} }
@Override
public Bundle getMediaButtonPreferences(String controllerId) throws RemoteException {
MediaController controller = mediaControllerMap.get(controllerId);
ArrayList<Bundle> mediaButtonPreferences = new ArrayList<>();
ImmutableList<CommandButton> 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 @Override
public Bundle getAvailableCommands(String controllerId) throws RemoteException { public Bundle getAvailableCommands(String controllerId) throws RemoteException {
MediaController controller = mediaControllerMap.get(controllerId); MediaController controller = mediaControllerMap.get(controllerId);

View File

@ -622,6 +622,24 @@ public class MediaSessionProviderService extends Service {
}); });
} }
@Override
@SuppressWarnings("FutureReturnValueIgnored")
public void setMediaButtonPreferences(String sessionId, List<Bundle> mediaButtonPreferences)
throws RemoteException {
if (mediaButtonPreferences == null) {
return;
}
runOnHandler(
() -> {
ImmutableList.Builder<CommandButton> 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 @Override
public void setSessionExtras(String sessionId, Bundle extras) throws RemoteException { public void setSessionExtras(String sessionId, Bundle extras) throws RemoteException {
runOnHandler(() -> sessionMap.get(sessionId).setSessionExtras(extras)); runOnHandler(() -> sessionMap.get(sessionId).setSessionExtras(extras));

View File

@ -389,6 +389,17 @@ public class RemoteMediaController {
return customLayout.build(); return customLayout.build();
} }
public ImmutableList<CommandButton> getMediaButtonPreferences() throws RemoteException {
Bundle mediaButtonPreferencesBundle = binder.getMediaButtonPreferences(controllerId);
ArrayList<Bundle> list =
mediaButtonPreferencesBundle.getParcelableArrayList(KEY_COMMAND_BUTTON_LIST);
ImmutableList.Builder<CommandButton> 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 { public Player.Commands getAvailableCommands() throws RemoteException {
Bundle commandsBundle = binder.getAvailableCommands(controllerId); Bundle commandsBundle = binder.getAvailableCommands(controllerId);
return Player.Commands.fromBundle(commandsBundle); return Player.Commands.fromBundle(commandsBundle);

View File

@ -201,6 +201,15 @@ public class RemoteMediaSession {
binder.setCustomLayout(sessionId, bundleList); binder.setCustomLayout(sessionId, bundleList);
} }
public void setMediaButtonPreferences(List<CommandButton> mediaButtonPreferences)
throws RemoteException {
List<Bundle> bundleList = new ArrayList<>();
for (CommandButton button : mediaButtonPreferences) {
bundleList.add(button.toBundle());
}
binder.setMediaButtonPreferences(sessionId, bundleList);
}
public void setSessionExtras(Bundle extras) throws RemoteException { public void setSessionExtras(Bundle extras) throws RemoteException {
binder.setSessionExtras(sessionId, extras); binder.setSessionExtras(sessionId, extras);
} }

View File

@ -62,6 +62,12 @@ public final class TestMediaBrowserListener implements MediaBrowser.Listener {
delegate.onCustomLayoutChanged(controller, layout); delegate.onCustomLayoutChanged(controller, layout);
} }
@Override
public void onMediaButtonPreferencesChanged(
MediaController controller, List<CommandButton> mediaButtonPreferences) {
delegate.onMediaButtonPreferencesChanged(controller, mediaButtonPreferences);
}
@Override @Override
public void onExtrasChanged(MediaController controller, Bundle extras) { public void onExtrasChanged(MediaController controller, Bundle extras) {
delegate.onExtrasChanged(controller, extras); delegate.onExtrasChanged(controller, extras);