Add logic to convert button preferences to custom layout in sessions

This ensures that media button preferences with slots for BACK,
FORWARD and OVERFLOW are converted to the legacy custom layout
according to the implicit placement rules. This has to happen
when populating the MediaSessionCompat and when generating the
notification for older APIs. Sending these preferences to older
Media3 controllers can filter them down to the custom layout the
session would have used previously, but there is no need to adjust
the reservation extras because the older Media3 custom layout has
no concept of these extras.

PiperOrigin-RevId: 689761637
This commit is contained in:
tonihei 2024-10-25 06:05:59 -07:00 committed by Copybara-Service
parent 8811b454bb
commit a15571c8ee
10 changed files with 589 additions and 36 deletions

View File

@ -1173,6 +1173,63 @@ public final class CommandButton {
} }
} }
/**
* Converts a list of buttons defined as {@linkplain MediaSession#getMediaButtonPreferences media
* button preferences} to the list of buttons for a {@linkplain MediaSession#getCustomLayout
* custom layout} according to the implicit button placement rules applied for custom layouts.
*
* @param mediaButtonPreferences The list of buttons as media button preferences.
* @param reservationExtras A writable {@link Bundle} that receives the extras for slot
* reservations via {@link MediaConstants#EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT} or {@link
* MediaConstants#EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV} to match the returned custom
* layout.
* @return A list of buttons compatible with the placement rules of custom layouts.
*/
/* package */ static ImmutableList<CommandButton> getCustomLayoutFromMediaButtonPreferences(
List<CommandButton> mediaButtonPreferences, Bundle reservationExtras) {
if (mediaButtonPreferences.isEmpty()) {
reservationExtras.putBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV, true);
reservationExtras.putBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT, true);
return ImmutableList.of();
}
int backButtonIndex = C.INDEX_UNSET;
int forwardButtonIndex = C.INDEX_UNSET;
for (int i = 0; i < mediaButtonPreferences.size(); i++) {
CommandButton button = mediaButtonPreferences.get(i);
for (int s = 0; s < button.slots.length(); s++) {
@Slot int slot = button.slots.get(s);
if (slot == SLOT_OVERFLOW) {
// Will go into overflow.
break;
} else if (backButtonIndex == C.INDEX_UNSET && slot == SLOT_BACK) {
backButtonIndex = i;
} else if (forwardButtonIndex == C.INDEX_UNSET && slot == SLOT_FORWARD) {
forwardButtonIndex = i;
}
}
}
boolean hasBackButton = backButtonIndex != C.INDEX_UNSET;
boolean hasForwardButton = forwardButtonIndex != C.INDEX_UNSET;
reservationExtras.putBoolean(
MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV, !hasBackButton);
reservationExtras.putBoolean(
MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT, !hasForwardButton);
ImmutableList.Builder<CommandButton> customLayout = ImmutableList.builder();
if (hasBackButton) {
customLayout.add(mediaButtonPreferences.get(backButtonIndex));
}
if (hasForwardButton) {
customLayout.add(mediaButtonPreferences.get(forwardButtonIndex));
}
for (int i = 0; i < mediaButtonPreferences.size(); i++) {
CommandButton button = mediaButtonPreferences.get(i);
if (i != backButtonIndex && i != forwardButtonIndex && button.slots.contains(SLOT_OVERFLOW)) {
customLayout.add(button);
}
}
return customLayout.build();
}
/** /**
* Converts a list of buttons defined according to the implicit button placement rules for * Converts a list of buttons defined according to the implicit button placement rules for
* {@linkplain MediaSession#getCustomLayout custom layouts} to {@linkplain * {@linkplain MediaSession#getCustomLayout custom layouts} to {@linkplain

View File

@ -131,11 +131,13 @@ import java.util.List;
mediaButtonPreferences, CommandButton::toBundle)); mediaButtonPreferences, CommandButton::toBundle));
} else { } else {
// Controller doesn't support media button preferences, send the list as a custom layout. // 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. // Ignore reservation extras as they were not directly supported in older controllers.
ImmutableList<CommandButton> customLayout =
CommandButton.getCustomLayoutFromMediaButtonPreferences(
mediaButtonPreferences, /* reservationExtras= */ new Bundle());
bundle.putParcelableArrayList( bundle.putParcelableArrayList(
FIELD_CUSTOM_LAYOUT, FIELD_CUSTOM_LAYOUT,
BundleCollectionUtil.toBundleArrayList( BundleCollectionUtil.toBundleArrayList(customLayout, CommandButton::toBundle));
mediaButtonPreferences, CommandButton::toBundle));
} }
} }
if (!commandButtonsForMediaItems.isEmpty()) { if (!commandButtonsForMediaItems.isEmpty()) {

View File

@ -46,6 +46,7 @@ import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.session.MediaStyleNotificationHelper.MediaStyle; import androidx.media3.session.MediaStyleNotificationHelper.MediaStyle;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.primitives.ImmutableIntArray;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
@ -303,7 +304,6 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
Callback onNotificationChangedCallback) { Callback onNotificationChangedCallback) {
ensureNotificationChannel(); ensureNotificationChannel();
// TODO: b/332877990 - More accurately reflect media button preferences in the notification.
ImmutableList.Builder<CommandButton> mediaButtonPreferencesWithEnabledCommandButtonsOnly = ImmutableList.Builder<CommandButton> mediaButtonPreferencesWithEnabledCommandButtonsOnly =
new ImmutableList.Builder<>(); new ImmutableList.Builder<>();
for (int i = 0; i < mediaButtonPreferences.size(); i++) { for (int i = 0; i < mediaButtonPreferences.size(); i++) {
@ -445,9 +445,24 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
Player.Commands playerCommands, Player.Commands playerCommands,
ImmutableList<CommandButton> mediaButtonPreferences, ImmutableList<CommandButton> mediaButtonPreferences,
boolean showPauseButton) { boolean showPauseButton) {
// Skip to previous action. Bundle reservations = new Bundle();
ImmutableList<CommandButton> customLayout =
CommandButton.getCustomLayoutFromMediaButtonPreferences(
mediaButtonPreferences, reservations);
boolean hasCustomBackButton =
!reservations.getBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV);
boolean hasCustomForwardButton =
!reservations.getBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT);
int nextCustomLayoutIndex = 0;
ImmutableList.Builder<CommandButton> commandButtons = new ImmutableList.Builder<>(); ImmutableList.Builder<CommandButton> commandButtons = new ImmutableList.Builder<>();
if (playerCommands.containsAny(COMMAND_SEEK_TO_PREVIOUS, COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)) { if (hasCustomBackButton) {
commandButtons.add(
customLayout
.get(nextCustomLayoutIndex++)
.copyWithSlots(ImmutableIntArray.of(CommandButton.SLOT_BACK)));
} else if (playerCommands.containsAny(
COMMAND_SEEK_TO_PREVIOUS, COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)) {
commandButtons.add( commandButtons.add(
new CommandButton.Builder(CommandButton.ICON_PREVIOUS) new CommandButton.Builder(CommandButton.ICON_PREVIOUS)
.setPlayerCommand(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) .setPlayerCommand(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
@ -470,20 +485,21 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
.build()); .build());
} }
} }
// Skip to next action. if (hasCustomForwardButton) {
if (playerCommands.containsAny(COMMAND_SEEK_TO_NEXT, COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)) { commandButtons.add(
customLayout
.get(nextCustomLayoutIndex++)
.copyWithSlots(ImmutableIntArray.of(CommandButton.SLOT_FORWARD)));
} else if (playerCommands.containsAny(COMMAND_SEEK_TO_NEXT, COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)) {
commandButtons.add( commandButtons.add(
new CommandButton.Builder(CommandButton.ICON_NEXT) new CommandButton.Builder(CommandButton.ICON_NEXT)
.setPlayerCommand(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) .setPlayerCommand(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
.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 < mediaButtonPreferences.size(); i++) { for (int i = nextCustomLayoutIndex; i < customLayout.size(); i++) {
CommandButton button = mediaButtonPreferences.get(i); commandButtons.add(
if (button.sessionCommand != null customLayout.get(i).copyWithSlots(ImmutableIntArray.of(CommandButton.SLOT_OVERFLOW)));
&& button.sessionCommand.commandCode == SessionCommand.COMMAND_CODE_CUSTOM) {
commandButtons.add(button);
}
} }
return commandButtons.build(); return commandButtons.build();
} }

View File

@ -526,7 +526,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public ListenableFuture<SessionResult> setMediaButtonPreferences( public ListenableFuture<SessionResult> setMediaButtonPreferences(
ControllerInfo controller, ImmutableList<CommandButton> mediaButtonPreferences) { ControllerInfo controller, ImmutableList<CommandButton> mediaButtonPreferences) {
if (isMediaNotificationController(controller)) { if (isMediaNotificationController(controller)) {
playerWrapper.setMediaButtonPreferences(mediaButtonPreferences); setLegacyMediaButtonPreferences(mediaButtonPreferences);
sessionLegacyStub.updateLegacySessionPlaybackState(playerWrapper); sessionLegacyStub.updateLegacySessionPlaybackState(playerWrapper);
} }
return dispatchRemoteControllerTask( return dispatchRemoteControllerTask(
@ -540,7 +540,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
*/ */
public void setMediaButtonPreferences(ImmutableList<CommandButton> mediaButtonPreferences) { public void setMediaButtonPreferences(ImmutableList<CommandButton> mediaButtonPreferences) {
this.mediaButtonPreferences = mediaButtonPreferences; this.mediaButtonPreferences = mediaButtonPreferences;
playerWrapper.setMediaButtonPreferences(mediaButtonPreferences); setLegacyMediaButtonPreferences(mediaButtonPreferences);
dispatchRemoteControllerTaskWithoutReturn( dispatchRemoteControllerTaskWithoutReturn(
(controller, seq) -> controller.setMediaButtonPreferences(seq, mediaButtonPreferences)); (controller, seq) -> controller.setMediaButtonPreferences(seq, mediaButtonPreferences));
} }
@ -722,14 +722,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
"Callback.onConnect must return non-null future"); "Callback.onConnect must return non-null future");
if (isMediaNotificationController(controller) && connectionResult.isAccepted) { if (isMediaNotificationController(controller) && connectionResult.isAccepted) {
isMediaNotificationControllerConnected = true; isMediaNotificationControllerConnected = true;
ImmutableList<CommandButton> mediaButtonPreferences =
connectionResult.mediaButtonPreferences != null
? connectionResult.mediaButtonPreferences
: instance.getMediaButtonPreferences();
if (mediaButtonPreferences.isEmpty()) {
playerWrapper.setCustomLayout( playerWrapper.setCustomLayout(
connectionResult.customLayout != null connectionResult.customLayout != null
? connectionResult.customLayout ? connectionResult.customLayout
: instance.getCustomLayout()); : instance.getCustomLayout());
playerWrapper.setMediaButtonPreferences( } else {
connectionResult.mediaButtonPreferences != null setLegacyMediaButtonPreferences(mediaButtonPreferences);
? connectionResult.mediaButtonPreferences }
: instance.getMediaButtonPreferences());
setAvailableFrameworkControllerCommands( setAvailableFrameworkControllerCommands(
connectionResult.availableSessionCommands, connectionResult.availablePlayerCommands); connectionResult.availableSessionCommands, connectionResult.availablePlayerCommands);
} }
@ -1037,6 +1041,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
} }
private void setLegacyMediaButtonPreferences(
ImmutableList<CommandButton> mediaButtonPreferences) {
boolean extrasChanged = playerWrapper.setMediaButtonPreferences(mediaButtonPreferences);
if (extrasChanged) {
sessionLegacyStub.getSessionCompat().setExtras(playerWrapper.getLegacyExtras());
}
}
private void setAvailableFrameworkControllerCommands( private void setAvailableFrameworkControllerCommands(
SessionCommands sessionCommands, Player.Commands playerCommands) { SessionCommands sessionCommands, Player.Commands playerCommands) {
boolean commandGetTimelineChanged = boolean commandGetTimelineChanged =

View File

@ -1085,10 +1085,13 @@ import org.checkerframework.checker.initialization.qual.Initialized;
onRepeatModeChanged(seq, newPlayerWrapper.getRepeatMode()); onRepeatModeChanged(seq, newPlayerWrapper.getRepeatMode());
} }
// Forcefully update playback info to update VolumeProviderCompat attached to the // Forcefully update device info to update VolumeProviderCompat attached to the old player.
// old player.
onDeviceInfoChanged(seq, newPlayerWrapper.getDeviceInfo()); onDeviceInfoChanged(seq, newPlayerWrapper.getDeviceInfo());
if (hasChangedSlotReservationExtras(oldPlayerWrapper, newPlayerWrapper)) {
sessionCompat.setExtras(newPlayerWrapper.getLegacyExtras());
}
// Rest of changes are all notified via PlaybackStateCompat. // Rest of changes are all notified via PlaybackStateCompat.
maybeUpdateFlags(newPlayerWrapper); maybeUpdateFlags(newPlayerWrapper);
@Nullable MediaItem newMediaItem = newPlayerWrapper.getCurrentMediaItemWithCommandCheck(); @Nullable MediaItem newMediaItem = newPlayerWrapper.getCurrentMediaItemWithCommandCheck();
@ -1122,8 +1125,9 @@ import org.checkerframework.checker.initialization.qual.Initialized;
@Override @Override
public void onSessionExtrasChanged(int seq, Bundle sessionExtras) { public void onSessionExtrasChanged(int seq, Bundle sessionExtras) {
sessionCompat.setExtras(sessionExtras); PlayerWrapper playerWrapper = sessionImpl.getPlayerWrapper();
sessionImpl.getPlayerWrapper().setLegacyExtras(sessionExtras); playerWrapper.setLegacyExtras(sessionExtras);
sessionCompat.setExtras(playerWrapper.getLegacyExtras());
sessionCompat.setPlaybackState(sessionImpl.getPlayerWrapper().createPlaybackStateCompat()); sessionCompat.setPlaybackState(sessionImpl.getPlayerWrapper().createPlaybackStateCompat());
} }
@ -1421,6 +1425,28 @@ import org.checkerframework.checker.initialization.qual.Initialized;
} }
} }
private static boolean hasChangedSlotReservationExtras(
@Nullable PlayerWrapper oldPlayerWrapper, PlayerWrapper newPlayerWrapper) {
if (oldPlayerWrapper == null) {
return true;
}
Bundle oldExtras = oldPlayerWrapper.getLegacyExtras();
boolean oldPrevReservation =
oldExtras.getBoolean(
MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV, /* defaultVale= */ false);
boolean oldNextReservation =
oldExtras.getBoolean(
MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT, /* defaultVale= */ false);
Bundle newExtras = newPlayerWrapper.getLegacyExtras();
boolean newPrevReservation =
newExtras.getBoolean(
MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV, /* defaultVale= */ false);
boolean newNextReservation =
newExtras.getBoolean(
MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT, /* defaultVale= */ false);
return (oldPrevReservation != newPrevReservation) || (oldNextReservation != newNextReservation);
}
private static class ConnectionTimeoutHandler extends Handler { private static class ConnectionTimeoutHandler extends Handler {
private static final int MSG_CONNECTION_TIMED_OUT = 1001; private static final int MSG_CONNECTION_TIMED_OUT = 1001;

View File

@ -2085,10 +2085,13 @@ import java.util.concurrent.ExecutionException;
BundleCollectionUtil.toBundleList(mediaButtonPreferences, CommandButton::toBundle)); BundleCollectionUtil.toBundleList(mediaButtonPreferences, CommandButton::toBundle));
} else { } else {
// Controller doesn't support media button preferences, send the list as a custom layout. // 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. // Ignore reservation extras as they were not directly supported in older controllers.
ImmutableList<CommandButton> customLayout =
CommandButton.getCustomLayoutFromMediaButtonPreferences(
mediaButtonPreferences, /* reservationExtras= */ new Bundle());
iController.onSetCustomLayout( iController.onSetCustomLayout(
sequenceNumber, sequenceNumber,
BundleCollectionUtil.toBundleList(mediaButtonPreferences, CommandButton::toBundle)); BundleCollectionUtil.toBundleList(customLayout, CommandButton::toBundle));
} }
} }

View File

@ -106,6 +106,11 @@ import java.util.List;
this.availableSessionCommands = availableSessionCommands; this.availableSessionCommands = availableSessionCommands;
this.availablePlayerCommands = availablePlayerCommands; this.availablePlayerCommands = availablePlayerCommands;
this.legacyExtras = legacyExtras; this.legacyExtras = legacyExtras;
if (!mediaButtonPreferences.isEmpty()) {
this.customLayout =
CommandButton.getCustomLayoutFromMediaButtonPreferences(
mediaButtonPreferences, this.legacyExtras);
}
} }
public void setAvailableCommands( public void setAvailableCommands(
@ -126,8 +131,31 @@ import java.util.List;
this.customLayout = customLayout; this.customLayout = customLayout;
} }
public void setMediaButtonPreferences(ImmutableList<CommandButton> mediaButtonPreferences) { /**
* Sets new media button preferences.
*
* @param mediaButtonPreferences The list of {@link CommandButton} defining the media button
* preferences.
* @return Whether the {@linkplain #getLegacyExtras platform session extras} were updated as a
* result of this change.
*/
public boolean setMediaButtonPreferences(ImmutableList<CommandButton> mediaButtonPreferences) {
this.mediaButtonPreferences = mediaButtonPreferences; this.mediaButtonPreferences = mediaButtonPreferences;
boolean hadPrevReservation =
legacyExtras.getBoolean(
MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV, /* defaultVale= */ false);
boolean hadNextReservation =
legacyExtras.getBoolean(
MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT, /* defaultVale= */ false);
this.customLayout =
CommandButton.getCustomLayoutFromMediaButtonPreferences(
mediaButtonPreferences, legacyExtras);
return (legacyExtras.getBoolean(
MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV, /* defaultVale= */ false)
!= hadPrevReservation)
|| (legacyExtras.getBoolean(
MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT, /* defaultVale= */ false)
!= hadNextReservation);
} }
/* package */ ImmutableList<CommandButton> getCustomLayout() { /* package */ ImmutableList<CommandButton> getCustomLayout() {
@ -141,6 +169,11 @@ import java.util.List;
public void setLegacyExtras(Bundle extras) { public void setLegacyExtras(Bundle extras) {
checkArgument(!extras.containsKey(EXTRAS_KEY_PLAYBACK_SPEED_COMPAT)); checkArgument(!extras.containsKey(EXTRAS_KEY_PLAYBACK_SPEED_COMPAT));
checkArgument(!extras.containsKey(EXTRAS_KEY_MEDIA_ID_COMPAT)); checkArgument(!extras.containsKey(EXTRAS_KEY_MEDIA_ID_COMPAT));
if (!mediaButtonPreferences.isEmpty()) {
// Re-calculate custom layout in case we have to set any additional extras.
this.customLayout =
CommandButton.getCustomLayoutFromMediaButtonPreferences(mediaButtonPreferences, extras);
}
this.legacyExtras = extras; this.legacyExtras = extras;
} }
@ -1040,6 +1073,14 @@ import java.util.List;
for (int i = 0; i < availableCommands.size(); i++) { for (int i = 0; i < availableCommands.size(); i++) {
actions |= convertCommandToPlaybackStateActions(availableCommands.get(i)); actions |= convertCommandToPlaybackStateActions(availableCommands.get(i));
} }
if (!mediaButtonPreferences.isEmpty()
&& !legacyExtras.getBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV)) {
actions &= ~PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
}
if (!mediaButtonPreferences.isEmpty()
&& !legacyExtras.getBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT)) {
actions &= ~PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
}
long queueItemId = long queueItemId =
isCommandAvailable(COMMAND_GET_TIMELINE) isCommandAvailable(COMMAND_GET_TIMELINE)
? LegacyConversions.convertToQueueItemId(getCurrentMediaItemIndex()) ? LegacyConversions.convertToQueueItemId(getCurrentMediaItemIndex())
@ -1064,18 +1105,14 @@ import java.util.List;
.setActiveQueueItemId(queueItemId) .setActiveQueueItemId(queueItemId)
.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
&& sessionCommand.commandCode == SessionCommand.COMMAND_CODE_CUSTOM && sessionCommand.commandCode == SessionCommand.COMMAND_CODE_CUSTOM
&& CommandButton.isButtonCommandAvailable( && CommandButton.isButtonCommandAvailable(
commandButton, availableSessionCommands, availablePlayerCommands)) { commandButton, availableSessionCommands, availableCommands)) {
Bundle actionExtras = sessionCommand.customExtras; Bundle actionExtras = sessionCommand.customExtras;
if (commandButton.icon != CommandButton.ICON_UNDEFINED) { if (commandButton.icon != CommandButton.ICON_UNDEFINED) {
actionExtras = new Bundle(sessionCommand.customExtras); actionExtras = new Bundle(sessionCommand.customExtras);

View File

@ -544,6 +544,203 @@ public class CommandButtonTest {
assertThat(restoredButtonAssumingOldSessionInterface.isEnabled).isTrue(); assertThat(restoredButtonAssumingOldSessionInterface.isEnabled).isTrue();
} }
@Test
public void getCustomLayoutFromMediaButtonPreferences_noBackForwardSlots_returnsCorrectButtons() {
ImmutableList<CommandButton> mediaButtonPreferences =
ImmutableList.of(
new CommandButton.Builder(CommandButton.ICON_ALBUM)
.setPlayerCommand(Player.COMMAND_PREPARE)
.setSlots(CommandButton.SLOT_OVERFLOW, CommandButton.SLOT_BACK)
.build(),
new CommandButton.Builder(CommandButton.ICON_NEXT)
.setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT)
.setSlots(CommandButton.SLOT_FORWARD_SECONDARY)
.build(),
new CommandButton.Builder(CommandButton.ICON_REWIND)
.setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS)
.setSlots(CommandButton.SLOT_BACK_SECONDARY, CommandButton.SLOT_OVERFLOW)
.build());
Bundle reservationBundle = new Bundle();
ImmutableList<CommandButton> customLayout =
CommandButton.getCustomLayoutFromMediaButtonPreferences(
mediaButtonPreferences, reservationBundle);
assertThat(customLayout)
.containsExactly(
new CommandButton.Builder(CommandButton.ICON_ALBUM)
.setPlayerCommand(Player.COMMAND_PREPARE)
.setSlots(CommandButton.SLOT_OVERFLOW, CommandButton.SLOT_BACK)
.build(),
new CommandButton.Builder(CommandButton.ICON_REWIND)
.setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS)
.setSlots(CommandButton.SLOT_BACK_SECONDARY, CommandButton.SLOT_OVERFLOW)
.build())
.inOrder();
assertThat(
reservationBundle.getBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV))
.isTrue();
assertThat(
reservationBundle.getBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT))
.isTrue();
}
@Test
public void getCustomLayoutFromMediaButtonPreferences_withBackSlot_returnsCorrectButtons() {
ImmutableList<CommandButton> mediaButtonPreferences =
ImmutableList.of(
new CommandButton.Builder(CommandButton.ICON_ALBUM)
.setPlayerCommand(Player.COMMAND_PREPARE)
.setSlots(CommandButton.SLOT_OVERFLOW, CommandButton.SLOT_BACK)
.build(),
new CommandButton.Builder(CommandButton.ICON_PREVIOUS)
.setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS)
.setSlots(CommandButton.SLOT_BACK, CommandButton.SLOT_OVERFLOW)
.build(),
new CommandButton.Builder(CommandButton.ICON_NEXT)
.setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT)
.setSlots(CommandButton.SLOT_FORWARD_SECONDARY)
.build(),
new CommandButton.Builder(CommandButton.ICON_REWIND)
.setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS)
.setSlots(CommandButton.SLOT_BACK_SECONDARY, CommandButton.SLOT_OVERFLOW)
.build());
Bundle reservationBundle = new Bundle();
ImmutableList<CommandButton> customLayout =
CommandButton.getCustomLayoutFromMediaButtonPreferences(
mediaButtonPreferences, reservationBundle);
assertThat(customLayout)
.containsExactly(
new CommandButton.Builder(CommandButton.ICON_PREVIOUS)
.setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS)
.setSlots(CommandButton.SLOT_BACK, CommandButton.SLOT_OVERFLOW)
.build(),
new CommandButton.Builder(CommandButton.ICON_ALBUM)
.setPlayerCommand(Player.COMMAND_PREPARE)
.setSlots(CommandButton.SLOT_OVERFLOW, CommandButton.SLOT_BACK)
.build(),
new CommandButton.Builder(CommandButton.ICON_REWIND)
.setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS)
.setSlots(CommandButton.SLOT_BACK_SECONDARY, CommandButton.SLOT_OVERFLOW)
.build())
.inOrder();
assertThat(
reservationBundle.getBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV))
.isFalse();
assertThat(
reservationBundle.getBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT))
.isTrue();
}
@Test
public void getCustomLayoutFromMediaButtonPreferences_withForwardSlot_returnsCorrectButtons() {
ImmutableList<CommandButton> mediaButtonPreferences =
ImmutableList.of(
new CommandButton.Builder(CommandButton.ICON_ALBUM)
.setPlayerCommand(Player.COMMAND_PREPARE)
.setSlots(CommandButton.SLOT_OVERFLOW, CommandButton.SLOT_FORWARD)
.build(),
new CommandButton.Builder(CommandButton.ICON_NEXT)
.setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT)
.setSlots(CommandButton.SLOT_FORWARD, CommandButton.SLOT_OVERFLOW)
.build(),
new CommandButton.Builder(CommandButton.ICON_NEXT)
.setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT)
.setSlots(CommandButton.SLOT_FORWARD_SECONDARY)
.build(),
new CommandButton.Builder(CommandButton.ICON_REWIND)
.setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS)
.setSlots(CommandButton.SLOT_BACK_SECONDARY, CommandButton.SLOT_OVERFLOW)
.build());
Bundle reservationBundle = new Bundle();
ImmutableList<CommandButton> customLayout =
CommandButton.getCustomLayoutFromMediaButtonPreferences(
mediaButtonPreferences, reservationBundle);
assertThat(customLayout)
.containsExactly(
new CommandButton.Builder(CommandButton.ICON_NEXT)
.setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT)
.setSlots(CommandButton.SLOT_FORWARD, CommandButton.SLOT_OVERFLOW)
.build(),
new CommandButton.Builder(CommandButton.ICON_ALBUM)
.setPlayerCommand(Player.COMMAND_PREPARE)
.setSlots(CommandButton.SLOT_OVERFLOW, CommandButton.SLOT_FORWARD)
.build(),
new CommandButton.Builder(CommandButton.ICON_REWIND)
.setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS)
.setSlots(CommandButton.SLOT_BACK_SECONDARY, CommandButton.SLOT_OVERFLOW)
.build())
.inOrder();
assertThat(
reservationBundle.getBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV))
.isTrue();
assertThat(
reservationBundle.getBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT))
.isFalse();
}
@Test
public void
getCustomLayoutFromMediaButtonPreferences_withForwardAndBackSlot_returnsCorrectButtons() {
ImmutableList<CommandButton> mediaButtonPreferences =
ImmutableList.of(
new CommandButton.Builder(CommandButton.ICON_ALBUM)
.setPlayerCommand(Player.COMMAND_PREPARE)
.setSlots(CommandButton.SLOT_OVERFLOW, CommandButton.SLOT_FORWARD)
.build(),
new CommandButton.Builder(CommandButton.ICON_NEXT)
.setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT)
.setSlots(CommandButton.SLOT_FORWARD, CommandButton.SLOT_OVERFLOW)
.build(),
new CommandButton.Builder(CommandButton.ICON_NEXT)
.setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT)
.setSlots(CommandButton.SLOT_FORWARD_SECONDARY)
.build(),
new CommandButton.Builder(CommandButton.ICON_REWIND)
.setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS)
.setSlots(CommandButton.SLOT_BACK_SECONDARY, CommandButton.SLOT_OVERFLOW)
.build(),
new CommandButton.Builder(CommandButton.ICON_PREVIOUS)
.setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS)
.setSlots(CommandButton.SLOT_CENTRAL, CommandButton.SLOT_BACK)
.build());
Bundle reservationBundle = new Bundle();
ImmutableList<CommandButton> customLayout =
CommandButton.getCustomLayoutFromMediaButtonPreferences(
mediaButtonPreferences, reservationBundle);
assertThat(customLayout)
.containsExactly(
new CommandButton.Builder(CommandButton.ICON_PREVIOUS)
.setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS)
.setSlots(CommandButton.SLOT_CENTRAL, CommandButton.SLOT_BACK)
.build(),
new CommandButton.Builder(CommandButton.ICON_NEXT)
.setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT)
.setSlots(CommandButton.SLOT_FORWARD, CommandButton.SLOT_OVERFLOW)
.build(),
new CommandButton.Builder(CommandButton.ICON_ALBUM)
.setPlayerCommand(Player.COMMAND_PREPARE)
.setSlots(CommandButton.SLOT_OVERFLOW, CommandButton.SLOT_FORWARD)
.build(),
new CommandButton.Builder(CommandButton.ICON_REWIND)
.setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS)
.setSlots(CommandButton.SLOT_BACK_SECONDARY, CommandButton.SLOT_OVERFLOW)
.build())
.inOrder();
assertThat(
reservationBundle.getBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV))
.isFalse();
assertThat(
reservationBundle.getBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT))
.isFalse();
}
@Test @Test
public void public void
getMediaButtonPreferencesFromCustomLayout_withPrevAndNextCommands_returnsCorrectSlots() { getMediaButtonPreferencesFromCustomLayout_withPrevAndNextCommands_returnsCorrectSlots() {

View File

@ -196,6 +196,60 @@ public class DefaultMediaNotificationProviderTest {
assertThat(mediaButtons).containsExactly(customCommandButton); assertThat(mediaButtons).containsExactly(customCommandButton);
} }
@Test
public void getMediaButtons_customButtonsForPrevNextSlots_overridesDefaultPrevNextButtons() {
DefaultMediaNotificationProvider defaultMediaNotificationProvider =
new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext())
.build();
Commands commands = new Commands.Builder().addAllCommands().build();
SessionCommand customSessionCommand = new SessionCommand("", Bundle.EMPTY);
CommandButton customCommandButton =
new CommandButton.Builder(CommandButton.ICON_UNDEFINED)
.setDisplayName("displayName")
.setIconResId(R.drawable.media3_icon_circular_play)
.setSessionCommand(customSessionCommand)
.build();
CommandButton customBackButton =
new CommandButton.Builder(CommandButton.ICON_UNDEFINED)
.setSlots(CommandButton.SLOT_BACK)
.setDisplayName("displayName")
.setIconResId(R.drawable.media3_icon_circular_play)
.setSessionCommand(customSessionCommand)
.build();
CommandButton customForwardButton =
new CommandButton.Builder(CommandButton.ICON_UNDEFINED)
.setSlots(CommandButton.SLOT_FORWARD)
.setDisplayName("displayName")
.setIconResId(R.drawable.media3_icon_circular_play)
.setSessionCommand(customSessionCommand)
.build();
CommandButton customCentralButton =
new CommandButton.Builder(CommandButton.ICON_UNDEFINED)
.setSlots(CommandButton.SLOT_CENTRAL)
.setDisplayName("displayName")
.setIconResId(R.drawable.media3_icon_circular_play)
.setSessionCommand(customSessionCommand)
.build();
Player player = new TestExoPlayerBuilder(context).build();
MediaSession mediaSession = new MediaSession.Builder(context, player).build();
ImmutableList<CommandButton> mediaButtons =
defaultMediaNotificationProvider.getMediaButtons(
mediaSession,
commands,
ImmutableList.of(
customCommandButton, customForwardButton, customBackButton, customCentralButton),
/* showPauseButton= */ true);
mediaSession.release();
player.release();
assertThat(mediaButtons).hasSize(4);
assertThat(mediaButtons.get(0)).isEqualTo(customBackButton);
assertThat(mediaButtons.get(1).playerCommand).isEqualTo(Player.COMMAND_PLAY_PAUSE);
assertThat(mediaButtons.get(2)).isEqualTo(customForwardButton);
assertThat(mediaButtons.get(3)).isEqualTo(customCommandButton);
}
@Test @Test
public void addNotificationActions_customCompactViewDeclarations_correctCompactViewIndices() { public void addNotificationActions_customCompactViewDeclarations_correctCompactViewIndices() {
DefaultMediaNotificationProvider defaultMediaNotificationProvider = DefaultMediaNotificationProvider defaultMediaNotificationProvider =

View File

@ -1640,15 +1640,18 @@ public class MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest
new CommandButton.Builder(CommandButton.ICON_PLAY) new CommandButton.Builder(CommandButton.ICON_PLAY)
.setDisplayName("button1") .setDisplayName("button1")
.setSessionCommand(command1) .setSessionCommand(command1)
.setSlots(CommandButton.SLOT_OVERFLOW)
.build(), .build(),
new CommandButton.Builder(CommandButton.ICON_PAUSE) new CommandButton.Builder(CommandButton.ICON_PAUSE)
.setDisplayName("button2") .setDisplayName("button2")
.setSessionCommand(command2) .setSessionCommand(command2)
.setSlots(CommandButton.SLOT_OVERFLOW)
.build(), .build(),
new CommandButton.Builder(CommandButton.ICON_PAUSE) new CommandButton.Builder(CommandButton.ICON_PAUSE)
.setDisplayName("button3") .setDisplayName("button3")
.setEnabled(false) .setEnabled(false)
.setSessionCommand(command3) .setSessionCommand(command3)
.setSlots(CommandButton.SLOT_OVERFLOW)
.build()); .build());
MediaSession.Callback callback = MediaSession.Callback callback =
new MediaSession.Callback() { new MediaSession.Callback() {
@ -1702,10 +1705,12 @@ public class MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest
new CommandButton.Builder(CommandButton.ICON_PLAY) new CommandButton.Builder(CommandButton.ICON_PLAY)
.setDisplayName("button1") .setDisplayName("button1")
.setSessionCommand(command1) .setSessionCommand(command1)
.setSlots(CommandButton.SLOT_OVERFLOW)
.build(), .build(),
new CommandButton.Builder(CommandButton.ICON_PAUSE) new CommandButton.Builder(CommandButton.ICON_PAUSE)
.setDisplayName("button2") .setDisplayName("button2")
.setSessionCommand(command2) .setSessionCommand(command2)
.setSlots(CommandButton.SLOT_OVERFLOW)
.build()); .build());
MediaSession.Callback callback = MediaSession.Callback callback =
new MediaSession.Callback() { new MediaSession.Callback() {
@ -1773,10 +1778,12 @@ public class MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest
new CommandButton.Builder(CommandButton.ICON_PLAY) new CommandButton.Builder(CommandButton.ICON_PLAY)
.setDisplayName("button1") .setDisplayName("button1")
.setSessionCommand(command1) .setSessionCommand(command1)
.setSlots(CommandButton.SLOT_OVERFLOW)
.build(), .build(),
new CommandButton.Builder(CommandButton.ICON_PAUSE) new CommandButton.Builder(CommandButton.ICON_PAUSE)
.setDisplayName("button2") .setDisplayName("button2")
.setSessionCommand(command2) .setSessionCommand(command2)
.setSlots(CommandButton.SLOT_OVERFLOW)
.build()); .build());
MediaSession.Callback callback = MediaSession.Callback callback =
new MediaSession.Callback() { new MediaSession.Callback() {
@ -1831,6 +1838,148 @@ public class MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest
releasePlayer(player); releasePlayer(player);
} }
@Test
public void
playerWithMediaButtonPreferences_withBackForwardSlots_overridesPrevNextActionsWhenNeeded()
throws Exception {
Player player =
createPlayer(
/* onPostCreationTask= */ createdPlayer -> {
createdPlayer.setMediaItems(
ImmutableList.of(
MediaItem.fromUri("asset://media/wav/sample.wav"),
MediaItem.fromUri("asset://media/wav/sample.wav"),
MediaItem.fromUri("asset://media/wav/sample.wav")));
createdPlayer.seekToDefaultPosition(/* mediaItemIndex= */ 1);
});
SessionCommand command1 = new SessionCommand("command1", Bundle.EMPTY);
SessionCommand command2 = new SessionCommand("command2", Bundle.EMPTY);
SessionCommand command3 = new SessionCommand("command3", Bundle.EMPTY);
SessionCommand commandIgnored = new SessionCommand("shouldBeIgnored", Bundle.EMPTY);
ImmutableList<CommandButton> mediaButtonPreferencesWithBackForward =
ImmutableList.of(
new CommandButton.Builder(CommandButton.ICON_PLAY)
.setDisplayName("button1")
.setSessionCommand(command1)
.setSlots(CommandButton.SLOT_OVERFLOW)
.build(),
new CommandButton.Builder(CommandButton.ICON_PLAY)
.setDisplayName("shouldBeIgnored")
.setSessionCommand(commandIgnored)
.setSlots(CommandButton.SLOT_BACK_SECONDARY)
.build(),
new CommandButton.Builder(CommandButton.ICON_PAUSE)
.setDisplayName("button2")
.setSessionCommand(command2)
.setSlots(CommandButton.SLOT_FORWARD)
.build(),
new CommandButton.Builder(CommandButton.ICON_PAUSE)
.setDisplayName("button3")
.setSessionCommand(command3)
.setSlots(CommandButton.SLOT_BACK)
.build(),
new CommandButton.Builder(CommandButton.ICON_PLAY)
.setDisplayName("shouldBeIgnored")
.setSessionCommand(commandIgnored)
.setSlots(CommandButton.SLOT_BACK)
.build());
ImmutableList<CommandButton> mediaButtonPreferencesWithoutBackForward =
ImmutableList.of(
new CommandButton.Builder(CommandButton.ICON_PLAY)
.setDisplayName("button1")
.setSessionCommand(command1)
.setSlots(CommandButton.SLOT_OVERFLOW)
.build(),
new CommandButton.Builder(CommandButton.ICON_PLAY)
.setDisplayName("shouldBeIgnored")
.setSessionCommand(commandIgnored)
.setSlots(CommandButton.SLOT_BACK_SECONDARY)
.build(),
new CommandButton.Builder(CommandButton.ICON_PAUSE)
.setDisplayName("button2")
.setSessionCommand(command2)
.setSlots(CommandButton.SLOT_OVERFLOW)
.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(command2)
.add(command3)
.add(commandIgnored)
.build())
.build();
}
};
MediaSession mediaSession =
new MediaSession.Builder(ApplicationProvider.getApplicationContext(), player)
.setCallback(callback)
.setMediaButtonPreferences(mediaButtonPreferencesWithBackForward)
.build();
connectMediaNotificationController(mediaSession);
MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession);
CountDownLatch controllerUpdatedLatch = new CountDownLatch(1);
MediaControllerCompat.Callback controllerCallback =
new MediaControllerCompat.Callback() {
@Override
public void onPlaybackStateChanged(PlaybackStateCompat state) {
controllerUpdatedLatch.countDown();
}
};
List<PlaybackStateCompat.CustomAction> customActions1 =
controllerCompat.getPlaybackState().getCustomActions();
Bundle extras1 = controllerCompat.getExtras();
long actions1 = controllerCompat.getPlaybackState().getActions();
controllerCompat.registerCallback(controllerCallback, threadTestRule.getHandler());
mediaSession.setMediaButtonPreferences(mediaButtonPreferencesWithoutBackForward);
controllerUpdatedLatch.await(TIMEOUT_MS, MILLISECONDS);
List<PlaybackStateCompat.CustomAction> customActions2 =
controllerCompat.getPlaybackState().getCustomActions();
Bundle extras2 = controllerCompat.getExtras();
long actions2 = controllerCompat.getPlaybackState().getActions();
mediaSession.release();
releasePlayer(player);
assertThat(customActions1).hasSize(3);
assertThat(customActions1.get(0).getAction()).isEqualTo("command3");
assertThat(customActions1.get(1).getAction()).isEqualTo("command2");
assertThat(customActions1.get(2).getAction()).isEqualTo("command1");
assertThat(
extras1.getBoolean(
androidx.media.utils.MediaConstants
.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV))
.isFalse();
assertThat(
extras1.getBoolean(
androidx.media.utils.MediaConstants
.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT))
.isFalse();
assertThat(actions1 & PlaybackStateCompat.ACTION_SKIP_TO_NEXT).isEqualTo(0);
assertThat(actions1 & PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS).isEqualTo(0);
assertThat(customActions2).hasSize(2);
assertThat(customActions2.get(0).getAction()).isEqualTo("command1");
assertThat(customActions2.get(1).getAction()).isEqualTo("command2");
assertThat(
extras2.getBoolean(
androidx.media.utils.MediaConstants
.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV))
.isTrue();
assertThat(
extras2.getBoolean(
androidx.media.utils.MediaConstants
.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT))
.isTrue();
assertThat(actions2 & PlaybackStateCompat.ACTION_SKIP_TO_NEXT).isNotEqualTo(0);
assertThat(actions2 & PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS).isNotEqualTo(0);
}
/** /**
* 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.