From a15571c8ee6aa1b9a81bad0f72a2016613620515 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 25 Oct 2024 06:05:59 -0700 Subject: [PATCH] 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 --- .../media3/session/CommandButton.java | 57 +++++ .../media3/session/ConnectionState.java | 8 +- .../DefaultMediaNotificationProvider.java | 38 +++- .../media3/session/MediaSessionImpl.java | 28 ++- .../session/MediaSessionLegacyStub.java | 34 ++- .../media3/session/MediaSessionStub.java | 7 +- .../media3/session/PlayerWrapper.java | 53 ++++- .../media3/session/CommandButtonTest.java | 197 ++++++++++++++++++ .../DefaultMediaNotificationProviderTest.java | 54 +++++ ...tateCompatActionsWithMediaSessionTest.java | 149 +++++++++++++ 10 files changed, 589 insertions(+), 36 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/CommandButton.java b/libraries/session/src/main/java/androidx/media3/session/CommandButton.java index 39b58d826e..8c1ab8256c 100644 --- a/libraries/session/src/main/java/androidx/media3/session/CommandButton.java +++ b/libraries/session/src/main/java/androidx/media3/session/CommandButton.java @@ -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 getCustomLayoutFromMediaButtonPreferences( + List 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 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 * {@linkplain MediaSession#getCustomLayout custom layouts} to {@linkplain diff --git a/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java b/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java index cd0bf10fd8..756c891cbf 100644 --- a/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java +++ b/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java @@ -131,11 +131,13 @@ import java.util.List; 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. + // Ignore reservation extras as they were not directly supported in older controllers. + ImmutableList customLayout = + CommandButton.getCustomLayoutFromMediaButtonPreferences( + mediaButtonPreferences, /* reservationExtras= */ new Bundle()); bundle.putParcelableArrayList( FIELD_CUSTOM_LAYOUT, - BundleCollectionUtil.toBundleArrayList( - mediaButtonPreferences, CommandButton::toBundle)); + BundleCollectionUtil.toBundleArrayList(customLayout, CommandButton::toBundle)); } } if (!commandButtonsForMediaItems.isEmpty()) { diff --git a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java index c5a72d9669..d4efae6ee2 100644 --- a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java +++ b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java @@ -46,6 +46,7 @@ import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.session.MediaStyleNotificationHelper.MediaStyle; 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.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -303,7 +304,6 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi Callback onNotificationChangedCallback) { ensureNotificationChannel(); - // TODO: b/332877990 - More accurately reflect media button preferences in the notification. ImmutableList.Builder mediaButtonPreferencesWithEnabledCommandButtonsOnly = new ImmutableList.Builder<>(); for (int i = 0; i < mediaButtonPreferences.size(); i++) { @@ -445,9 +445,24 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi Player.Commands playerCommands, ImmutableList mediaButtonPreferences, boolean showPauseButton) { - // Skip to previous action. + Bundle reservations = new Bundle(); + ImmutableList 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 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( new CommandButton.Builder(CommandButton.ICON_PREVIOUS) .setPlayerCommand(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) @@ -470,20 +485,21 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi .build()); } } - // Skip to next action. - if (playerCommands.containsAny(COMMAND_SEEK_TO_NEXT, COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)) { + if (hasCustomForwardButton) { + 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( new CommandButton.Builder(CommandButton.ICON_NEXT) .setPlayerCommand(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) .setDisplayName(context.getString(R.string.media3_controls_seek_to_next_description)) .build()); } - for (int i = 0; i < mediaButtonPreferences.size(); i++) { - CommandButton button = mediaButtonPreferences.get(i); - if (button.sessionCommand != null - && button.sessionCommand.commandCode == SessionCommand.COMMAND_CODE_CUSTOM) { - commandButtons.add(button); - } + for (int i = nextCustomLayoutIndex; i < customLayout.size(); i++) { + commandButtons.add( + customLayout.get(i).copyWithSlots(ImmutableIntArray.of(CommandButton.SLOT_OVERFLOW))); } return commandButtons.build(); } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java index b3127736c7..a8ebabe83f 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -526,7 +526,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; public ListenableFuture setMediaButtonPreferences( ControllerInfo controller, ImmutableList mediaButtonPreferences) { if (isMediaNotificationController(controller)) { - playerWrapper.setMediaButtonPreferences(mediaButtonPreferences); + setLegacyMediaButtonPreferences(mediaButtonPreferences); sessionLegacyStub.updateLegacySessionPlaybackState(playerWrapper); } return dispatchRemoteControllerTask( @@ -540,7 +540,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; */ public void setMediaButtonPreferences(ImmutableList mediaButtonPreferences) { this.mediaButtonPreferences = mediaButtonPreferences; - playerWrapper.setMediaButtonPreferences(mediaButtonPreferences); + setLegacyMediaButtonPreferences(mediaButtonPreferences); dispatchRemoteControllerTaskWithoutReturn( (controller, seq) -> controller.setMediaButtonPreferences(seq, mediaButtonPreferences)); } @@ -722,14 +722,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; "Callback.onConnect must return non-null future"); if (isMediaNotificationController(controller) && connectionResult.isAccepted) { isMediaNotificationControllerConnected = true; - playerWrapper.setCustomLayout( - connectionResult.customLayout != null - ? connectionResult.customLayout - : instance.getCustomLayout()); - playerWrapper.setMediaButtonPreferences( + ImmutableList mediaButtonPreferences = connectionResult.mediaButtonPreferences != null ? connectionResult.mediaButtonPreferences - : instance.getMediaButtonPreferences()); + : instance.getMediaButtonPreferences(); + if (mediaButtonPreferences.isEmpty()) { + playerWrapper.setCustomLayout( + connectionResult.customLayout != null + ? connectionResult.customLayout + : instance.getCustomLayout()); + } else { + setLegacyMediaButtonPreferences(mediaButtonPreferences); + } setAvailableFrameworkControllerCommands( connectionResult.availableSessionCommands, connectionResult.availablePlayerCommands); } @@ -1037,6 +1041,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } } + private void setLegacyMediaButtonPreferences( + ImmutableList mediaButtonPreferences) { + boolean extrasChanged = playerWrapper.setMediaButtonPreferences(mediaButtonPreferences); + if (extrasChanged) { + sessionLegacyStub.getSessionCompat().setExtras(playerWrapper.getLegacyExtras()); + } + } + private void setAvailableFrameworkControllerCommands( SessionCommands sessionCommands, Player.Commands playerCommands) { boolean commandGetTimelineChanged = diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index 662b0cb36b..6daba01ee5 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -1085,10 +1085,13 @@ import org.checkerframework.checker.initialization.qual.Initialized; onRepeatModeChanged(seq, newPlayerWrapper.getRepeatMode()); } - // Forcefully update playback info to update VolumeProviderCompat attached to the - // old player. + // Forcefully update device info to update VolumeProviderCompat attached to the old player. onDeviceInfoChanged(seq, newPlayerWrapper.getDeviceInfo()); + if (hasChangedSlotReservationExtras(oldPlayerWrapper, newPlayerWrapper)) { + sessionCompat.setExtras(newPlayerWrapper.getLegacyExtras()); + } + // Rest of changes are all notified via PlaybackStateCompat. maybeUpdateFlags(newPlayerWrapper); @Nullable MediaItem newMediaItem = newPlayerWrapper.getCurrentMediaItemWithCommandCheck(); @@ -1122,8 +1125,9 @@ import org.checkerframework.checker.initialization.qual.Initialized; @Override public void onSessionExtrasChanged(int seq, Bundle sessionExtras) { - sessionCompat.setExtras(sessionExtras); - sessionImpl.getPlayerWrapper().setLegacyExtras(sessionExtras); + PlayerWrapper playerWrapper = sessionImpl.getPlayerWrapper(); + playerWrapper.setLegacyExtras(sessionExtras); + sessionCompat.setExtras(playerWrapper.getLegacyExtras()); 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 final int MSG_CONNECTION_TIMED_OUT = 1001; diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java index 7d3ab92733..1a374eb6cc 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java @@ -2085,10 +2085,13 @@ import java.util.concurrent.ExecutionException; 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. + // Ignore reservation extras as they were not directly supported in older controllers. + ImmutableList customLayout = + CommandButton.getCustomLayoutFromMediaButtonPreferences( + mediaButtonPreferences, /* reservationExtras= */ new Bundle()); iController.onSetCustomLayout( sequenceNumber, - BundleCollectionUtil.toBundleList(mediaButtonPreferences, CommandButton::toBundle)); + BundleCollectionUtil.toBundleList(customLayout, CommandButton::toBundle)); } } diff --git a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java index d0592c71d6..e41b6a2332 100644 --- a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java +++ b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java @@ -106,6 +106,11 @@ import java.util.List; this.availableSessionCommands = availableSessionCommands; this.availablePlayerCommands = availablePlayerCommands; this.legacyExtras = legacyExtras; + if (!mediaButtonPreferences.isEmpty()) { + this.customLayout = + CommandButton.getCustomLayoutFromMediaButtonPreferences( + mediaButtonPreferences, this.legacyExtras); + } } public void setAvailableCommands( @@ -126,8 +131,31 @@ import java.util.List; this.customLayout = customLayout; } - public void setMediaButtonPreferences(ImmutableList 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 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 getCustomLayout() { @@ -141,6 +169,11 @@ import java.util.List; public void setLegacyExtras(Bundle extras) { checkArgument(!extras.containsKey(EXTRAS_KEY_PLAYBACK_SPEED_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; } @@ -1040,6 +1073,14 @@ import java.util.List; for (int i = 0; i < availableCommands.size(); 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 = isCommandAvailable(COMMAND_GET_TIMELINE) ? LegacyConversions.convertToQueueItemId(getCurrentMediaItemIndex()) @@ -1064,18 +1105,14 @@ import java.util.List; .setActiveQueueItemId(queueItemId) .setBufferedPosition(compatBufferedPosition) .setExtras(extras); - - // TODO: b/332877990 - More accurately reflect media button preferences as custom actions. - List buttonsForCustomActions = - mediaButtonPreferences.isEmpty() ? customLayout : mediaButtonPreferences; - for (int i = 0; i < buttonsForCustomActions.size(); i++) { - CommandButton commandButton = buttonsForCustomActions.get(i); + for (int i = 0; i < customLayout.size(); i++) { + CommandButton commandButton = customLayout.get(i); SessionCommand sessionCommand = commandButton.sessionCommand; if (sessionCommand != null && commandButton.isEnabled && sessionCommand.commandCode == SessionCommand.COMMAND_CODE_CUSTOM && CommandButton.isButtonCommandAvailable( - commandButton, availableSessionCommands, availablePlayerCommands)) { + commandButton, availableSessionCommands, availableCommands)) { Bundle actionExtras = sessionCommand.customExtras; if (commandButton.icon != CommandButton.ICON_UNDEFINED) { actionExtras = new Bundle(sessionCommand.customExtras); diff --git a/libraries/session/src/test/java/androidx/media3/session/CommandButtonTest.java b/libraries/session/src/test/java/androidx/media3/session/CommandButtonTest.java index d2f4394833..50c750930d 100644 --- a/libraries/session/src/test/java/androidx/media3/session/CommandButtonTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/CommandButtonTest.java @@ -544,6 +544,203 @@ public class CommandButtonTest { assertThat(restoredButtonAssumingOldSessionInterface.isEnabled).isTrue(); } + @Test + public void getCustomLayoutFromMediaButtonPreferences_noBackForwardSlots_returnsCorrectButtons() { + ImmutableList 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 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 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 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 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 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 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 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 public void getMediaButtonPreferencesFromCustomLayout_withPrevAndNextCommands_returnsCorrectSlots() { diff --git a/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java b/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java index 7828f02c0d..a39d097cb6 100644 --- a/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java @@ -196,6 +196,60 @@ public class DefaultMediaNotificationProviderTest { 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 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 public void addNotificationActions_customCompactViewDeclarations_correctCompactViewIndices() { DefaultMediaNotificationProvider defaultMediaNotificationProvider = diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest.java index f3bfc56b8c..eef19dcaf4 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest.java @@ -1640,15 +1640,18 @@ public class MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest new CommandButton.Builder(CommandButton.ICON_PLAY) .setDisplayName("button1") .setSessionCommand(command1) + .setSlots(CommandButton.SLOT_OVERFLOW) .build(), new CommandButton.Builder(CommandButton.ICON_PAUSE) .setDisplayName("button2") .setSessionCommand(command2) + .setSlots(CommandButton.SLOT_OVERFLOW) .build(), new CommandButton.Builder(CommandButton.ICON_PAUSE) .setDisplayName("button3") .setEnabled(false) .setSessionCommand(command3) + .setSlots(CommandButton.SLOT_OVERFLOW) .build()); MediaSession.Callback callback = new MediaSession.Callback() { @@ -1702,10 +1705,12 @@ public class MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest new CommandButton.Builder(CommandButton.ICON_PLAY) .setDisplayName("button1") .setSessionCommand(command1) + .setSlots(CommandButton.SLOT_OVERFLOW) .build(), new CommandButton.Builder(CommandButton.ICON_PAUSE) .setDisplayName("button2") .setSessionCommand(command2) + .setSlots(CommandButton.SLOT_OVERFLOW) .build()); MediaSession.Callback callback = new MediaSession.Callback() { @@ -1773,10 +1778,12 @@ public class MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest new CommandButton.Builder(CommandButton.ICON_PLAY) .setDisplayName("button1") .setSessionCommand(command1) + .setSlots(CommandButton.SLOT_OVERFLOW) .build(), new CommandButton.Builder(CommandButton.ICON_PAUSE) .setDisplayName("button2") .setSessionCommand(command2) + .setSlots(CommandButton.SLOT_OVERFLOW) .build()); MediaSession.Callback callback = new MediaSession.Callback() { @@ -1831,6 +1838,148 @@ public class MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest 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 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 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 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 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 * MediaNotificationManager} when the session is running in the service.