From aeb9d12a161e927c9fa674fd9a2974db52e1b143 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 25 Oct 2024 04:26:24 -0700 Subject: [PATCH] Add logic to convert custom layout to button preferences in controller This ensures that the slots are set according to the implicit placement rules for the legacy custom layouts. This has to be done when receiving custom actions from a MediaControllerCompat and for Media3 sessions setting the custom layout field. PiperOrigin-RevId: 689737951 --- .../media3/session/CommandButton.java | 77 ++++ .../media3/session/LegacyConversions.java | 14 +- .../session/MediaControllerImplBase.java | 52 ++- .../session/MediaControllerImplLegacy.java | 77 ++-- .../media3/session/CommandButtonTest.java | 370 ++++++++++++++++++ .../media3/session/LegacyConversionsTest.java | 13 +- ...lerListenerWithMediaSessionCompatTest.java | 193 ++++++++- .../media3/session/MediaControllerTest.java | 284 +++++++++++--- .../session/MediaSessionCallbackTest.java | 11 +- .../session/MediaSessionServiceTest.java | 10 +- 10 files changed, 993 insertions(+), 108 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 08e11655a1..39b58d826e 100644 --- a/libraries/session/src/main/java/androidx/media3/session/CommandButton.java +++ b/libraries/session/src/main/java/androidx/media3/session/CommandButton.java @@ -27,6 +27,7 @@ import android.text.TextUtils; import androidx.annotation.DrawableRes; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import androidx.media3.common.C; import androidx.media3.common.Player; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; @@ -799,6 +800,24 @@ public final class CommandButton { slots); } + /** Returns a copy with the new {@link #slots} value. */ + @CheckReturnValue + /* package */ CommandButton copyWithSlots(ImmutableIntArray slots) { + if (this.slots.equals(slots)) { + return this; + } + return new CommandButton( + sessionCommand, + playerCommand, + icon, + iconResId, + iconUri, + displayName, + new Bundle(extras), + isEnabled, + slots); + } + /** Checks the given command button for equality while ignoring {@link #extras}. */ @Override public boolean equals(@Nullable Object obj) { @@ -1153,4 +1172,62 @@ public final class CommandButton { return SLOT_OVERFLOW; } } + + /** + * Converts a list of buttons defined according to the implicit button placement rules for + * {@linkplain MediaSession#getCustomLayout custom layouts} to {@linkplain + * MediaSession#getMediaButtonPreferences media button preferences}. + * + * @param customLayout A list of buttons compatible with the placement rules of custom layouts. + * @param availablePlayerCommands The available {@link Player.Commands}. + * @param reservationExtras A {@link Bundle} with extras that may contain slot reservations via + * {@link MediaConstants#EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT} or {@link + * MediaConstants#EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV}. The bundle contents will not be + * modified. + * @return The list of buttons as media button preferences. + */ + /* package */ static ImmutableList getMediaButtonPreferencesFromCustomLayout( + List customLayout, + Player.Commands availablePlayerCommands, + Bundle reservationExtras) { + if (customLayout.isEmpty()) { + return ImmutableList.of(); + } + boolean hasDefaultBackCommand = + availablePlayerCommands.containsAny( + Player.COMMAND_SEEK_TO_PREVIOUS, Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM); + boolean hasDefaultForwardCommand = + availablePlayerCommands.containsAny( + Player.COMMAND_SEEK_TO_NEXT, Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM); + boolean hasBackSlotReservation = + reservationExtras.getBoolean( + MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV, /* defaultValue= */ false); + boolean hasForwardSlotReservation = + reservationExtras.getBoolean( + MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT, /* defaultValue= */ false); + int backButtonIndex = (hasDefaultBackCommand || hasBackSlotReservation) ? C.INDEX_UNSET : 0; + int forwardButtonIndex = + (hasDefaultForwardCommand || hasForwardSlotReservation) + ? C.INDEX_UNSET + : (backButtonIndex == 0 ? 1 : 0); + ImmutableList.Builder mediaButtonPreferences = ImmutableList.builder(); + for (int i = 0; i < customLayout.size(); i++) { + CommandButton button = customLayout.get(i); + if (i == backButtonIndex) { + if (forwardButtonIndex == C.INDEX_UNSET) { + mediaButtonPreferences.add( + button.copyWithSlots(ImmutableIntArray.of(SLOT_BACK, SLOT_OVERFLOW))); + } else { + mediaButtonPreferences.add( + button.copyWithSlots(ImmutableIntArray.of(SLOT_BACK, SLOT_FORWARD, SLOT_OVERFLOW))); + } + } else if (i == forwardButtonIndex) { + mediaButtonPreferences.add( + button.copyWithSlots(ImmutableIntArray.of(SLOT_FORWARD, SLOT_OVERFLOW))); + } else { + mediaButtonPreferences.add(button.copyWithSlots(ImmutableIntArray.of(SLOT_OVERFLOW))); + } + } + return mediaButtonPreferences.build(); + } } diff --git a/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java b/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java index 5359fcf723..56378917bc 100644 --- a/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java +++ b/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java @@ -1497,10 +1497,14 @@ import java.util.concurrent.TimeoutException; * Converts {@link CustomAction} in the {@link PlaybackStateCompat} to media button preferences. * * @param state The {@link PlaybackStateCompat}. + * @param availablePlayerCommands The available {@link Player.Commands}. + * @param sessionExtras The {@linkplain MediaControllerCompat#getExtras session-level extras}. * @return The media button preferences. */ public static ImmutableList convertToMediaButtonPreferences( - @Nullable PlaybackStateCompat state) { + @Nullable PlaybackStateCompat state, + Player.Commands availablePlayerCommands, + Bundle sessionExtras) { if (state == null) { return ImmutableList.of(); } @@ -1508,7 +1512,7 @@ import java.util.concurrent.TimeoutException; if (customActions == null) { return ImmutableList.of(); } - ImmutableList.Builder mediaButtonPreferences = new ImmutableList.Builder<>(); + ImmutableList.Builder customLayout = new ImmutableList.Builder<>(); for (CustomAction customAction : customActions) { String action = customAction.getAction(); @Nullable Bundle extras = customAction.getExtras(); @@ -1519,16 +1523,16 @@ import java.util.concurrent.TimeoutException; MediaConstants.EXTRAS_KEY_COMMAND_BUTTON_ICON_COMPAT, /* defaultValue= */ CommandButton.ICON_UNDEFINED) : CommandButton.ICON_UNDEFINED; - // TODO: b/332877990 - Set appropriate slots based on available player commands. CommandButton button = new CommandButton.Builder(icon, customAction.getIcon()) .setSessionCommand(new SessionCommand(action, extras == null ? Bundle.EMPTY : extras)) .setDisplayName(customAction.getName()) .setEnabled(true) .build(); - mediaButtonPreferences.add(button); + customLayout.add(button); } - return mediaButtonPreferences.build(); + return CommandButton.getMediaButtonPreferencesFromCustomLayout( + customLayout.build(), availablePlayerCommands, sessionExtras); } /** Converts {@link AudioAttributesCompat} into {@link AudioAttributes}. */ diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java index 8152b2ed53..c3b4936cad 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java @@ -2660,7 +2660,8 @@ import org.checkerframework.checker.nullness.qual.NonNull; mediaButtonPreferencesOriginal, customLayoutOriginal, sessionCommands, - intersectedPlayerCommands); + intersectedPlayerCommands, + result.sessionExtras); ImmutableMap.Builder commandButtonsForMediaItems = new ImmutableMap.Builder<>(); for (int i = 0; i < result.commandButtonsForMediaItems.size(); i++) { @@ -2848,7 +2849,8 @@ import org.checkerframework.checker.nullness.qual.NonNull; mediaButtonPreferencesOriginal, customLayoutOriginal, sessionCommands, - intersectedPlayerCommands); + intersectedPlayerCommands, + sessionExtras); mediaButtonPreferencesChanged = !resolvedMediaButtonPreferences.equals(oldMediaButtonPreferences); } @@ -2896,7 +2898,8 @@ import org.checkerframework.checker.nullness.qual.NonNull; mediaButtonPreferencesOriginal, customLayoutOriginal, sessionCommands, - intersectedPlayerCommands); + intersectedPlayerCommands, + sessionExtras); mediaButtonPreferencesChanged = !resolvedMediaButtonPreferences.equals(oldMediaButtonPreferences); listeners.sendEvent( @@ -2922,7 +2925,11 @@ import org.checkerframework.checker.nullness.qual.NonNull; customLayoutOriginal = ImmutableList.copyOf(layout); resolvedMediaButtonPreferences = resolveMediaButtonPreferences( - mediaButtonPreferencesOriginal, layout, sessionCommands, intersectedPlayerCommands); + mediaButtonPreferencesOriginal, + layout, + sessionCommands, + intersectedPlayerCommands, + sessionExtras); boolean mediaButtonPreferencesChanged = !Objects.equals(resolvedMediaButtonPreferences, oldMediaButtonPreferences); getInstance() @@ -2952,7 +2959,8 @@ import org.checkerframework.checker.nullness.qual.NonNull; mediaButtonPreferences, customLayoutOriginal, sessionCommands, - intersectedPlayerCommands); + intersectedPlayerCommands, + sessionExtras); boolean mediaButtonPreferencesChanged = !Objects.equals(resolvedMediaButtonPreferences, oldMediaButtonPreferences); getInstance() @@ -2975,9 +2983,27 @@ import org.checkerframework.checker.nullness.qual.NonNull; if (!isConnected()) { return; } + ImmutableList oldMediaButtonPreferences = resolvedMediaButtonPreferences; sessionExtras = extras; + resolvedMediaButtonPreferences = + resolveMediaButtonPreferences( + mediaButtonPreferencesOriginal, + customLayoutOriginal, + sessionCommands, + intersectedPlayerCommands, + sessionExtras); + boolean mediaButtonPreferencesChanged = + !Objects.equals(resolvedMediaButtonPreferences, oldMediaButtonPreferences); getInstance() - .notifyControllerListener(listener -> listener.onExtrasChanged(getInstance(), extras)); + .notifyControllerListener( + listener -> { + listener.onExtrasChanged(getInstance(), extras); + if (mediaButtonPreferencesChanged) { + listener.onCustomLayoutChanged(getInstance(), resolvedMediaButtonPreferences); + listener.onMediaButtonPreferencesChanged( + getInstance(), resolvedMediaButtonPreferences); + } + }); } public void onSetSessionActivity(int seq, PendingIntent sessionActivity) { @@ -3327,12 +3353,16 @@ import org.checkerframework.checker.nullness.qual.NonNull; List mediaButtonPreferences, List customLayout, SessionCommands sessionCommands, - Player.Commands playerCommands) { - // TODO: b/332877990 - When using custom layout, set correct slots based on available commands. + Player.Commands playerCommands, + Bundle sessionExtras) { + List resolvedButtons = mediaButtonPreferences; + if (resolvedButtons.isEmpty()) { + resolvedButtons = + CommandButton.getMediaButtonPreferencesFromCustomLayout( + customLayout, playerCommands, sessionExtras); + } return CommandButton.copyWithUnavailableButtonsDisabled( - mediaButtonPreferences.isEmpty() ? customLayout : mediaButtonPreferences, - sessionCommands, - playerCommands); + resolvedButtons, sessionCommands, playerCommands); } private static Commands createIntersectedCommandsEnsuringCommandReleaseAvailable( diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java index d8e2dc2d7d..746ff3f3c8 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java @@ -110,6 +110,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; private boolean connected; private LegacyPlayerInfo legacyPlayerInfo; private LegacyPlayerInfo pendingLegacyPlayerInfo; + private boolean hasPendingExtrasChange; private ControllerInfo controllerInfo; private long currentPositionMs; private long lastSetPlayWhenReadyCalledTimeMs; @@ -1575,6 +1576,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; controllerCompat.getRatingType(), getInstance().getTimeDiffMs(), getRoutingControllerId(controllerCompat), + hasPendingExtrasChange, context); Pair<@NullableType Integer, @NullableType Integer> reasons = calculateDiscontinuityAndTransitionReason( @@ -1589,6 +1591,13 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; newControllerInfo, /* discontinuityReason= */ reasons.first, /* mediaItemTransitionReason= */ reasons.second); + if (hasPendingExtrasChange) { + hasPendingExtrasChange = false; + getInstance() + .notifyControllerListener( + listener -> + listener.onExtrasChanged(getInstance(), newLegacyPlayerInfo.sessionExtras)); + } } private void updateStateMaskedControllerInfo( @@ -1918,16 +1927,10 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; @Override public void onExtrasChanged(@Nullable Bundle extras) { - controllerInfo = - new ControllerInfo( - controllerInfo.playerInfo, - controllerInfo.availableSessionCommands, - controllerInfo.availablePlayerCommands, - controllerInfo.mediaButtonPreferences, - extras, - /* sessionError= */ null); - getInstance() - .notifyControllerListener(listener -> listener.onExtrasChanged(getInstance(), extras)); + Bundle nonNullExtras = extras == null ? new Bundle() : extras; + pendingLegacyPlayerInfo = pendingLegacyPlayerInfo.copyWithSessionExtras(nonNullExtras); + hasPendingExtrasChange = true; + startWaitingForPendingChanges(); } @Override @@ -1989,6 +1992,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; @RatingCompat.Style int ratingType, long timeDiffMs, @Nullable String routingControllerId, + boolean hasPendingExtrasChange, Context context) { QueueTimeline currentTimeline; MediaMetadata mediaMetadata; @@ -2073,24 +2077,6 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; mediaMetadata = oldControllerInfo.playerInfo.mediaMetadata; } - playlistMetadata = - oldLegacyPlayerInfo.queueTitle == newLegacyPlayerInfo.queueTitle - ? oldControllerInfo.playerInfo.playlistMetadata - : LegacyConversions.convertToMediaMetadata(newLegacyPlayerInfo.queueTitle); - repeatMode = LegacyConversions.convertToRepeatMode(newLegacyPlayerInfo.repeatMode); - shuffleModeEnabled = - LegacyConversions.convertToShuffleModeEnabled(newLegacyPlayerInfo.shuffleMode); - if (oldLegacyPlayerInfo.playbackStateCompat != newLegacyPlayerInfo.playbackStateCompat) { - availableSessionCommands = - LegacyConversions.convertToSessionCommands( - newLegacyPlayerInfo.playbackStateCompat, isSessionReady); - mediaButtonPreferences = - LegacyConversions.convertToMediaButtonPreferences( - newLegacyPlayerInfo.playbackStateCompat); - } else { - availableSessionCommands = oldControllerInfo.availableSessionCommands; - mediaButtonPreferences = oldControllerInfo.mediaButtonPreferences; - } // Note: Sets the available player command here although it can be obtained before session is // ready. It's to follow the decision on MediaController to disallow any commands before // connection is made. @@ -2105,6 +2091,28 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; sessionFlags, isSessionReady); + playlistMetadata = + oldLegacyPlayerInfo.queueTitle == newLegacyPlayerInfo.queueTitle + ? oldControllerInfo.playerInfo.playlistMetadata + : LegacyConversions.convertToMediaMetadata(newLegacyPlayerInfo.queueTitle); + repeatMode = LegacyConversions.convertToRepeatMode(newLegacyPlayerInfo.repeatMode); + shuffleModeEnabled = + LegacyConversions.convertToShuffleModeEnabled(newLegacyPlayerInfo.shuffleMode); + if (oldLegacyPlayerInfo.playbackStateCompat != newLegacyPlayerInfo.playbackStateCompat + || hasPendingExtrasChange) { + availableSessionCommands = + LegacyConversions.convertToSessionCommands( + newLegacyPlayerInfo.playbackStateCompat, isSessionReady); + mediaButtonPreferences = + LegacyConversions.convertToMediaButtonPreferences( + newLegacyPlayerInfo.playbackStateCompat, + availablePlayerCommands, + newLegacyPlayerInfo.sessionExtras); + } else { + availableSessionCommands = oldControllerInfo.availableSessionCommands; + mediaButtonPreferences = oldControllerInfo.mediaButtonPreferences; + } + PlaybackException playerError = LegacyConversions.convertToPlaybackException(newLegacyPlayerInfo.playbackStateCompat); SessionError sessionError = @@ -2626,6 +2634,19 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; shuffleMode, sessionExtras); } + + @CheckResult + public LegacyPlayerInfo copyWithSessionExtras(Bundle sessionExtras) { + return new LegacyPlayerInfo( + playbackInfoCompat, + playbackStateCompat, + mediaMetadataCompat, + queue, + queueTitle, + repeatMode, + shuffleMode, + sessionExtras); + } } private static class ControllerInfo { 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 8e6745e832..d2f4394833 100644 --- a/libraries/session/src/test/java/androidx/media3/session/CommandButtonTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/CommandButtonTest.java @@ -543,4 +543,374 @@ public class CommandButtonTest { assertThat(restoredButtonAssumingOldSessionInterface.isEnabled).isTrue(); } + + @Test + public void + getMediaButtonPreferencesFromCustomLayout_withPrevAndNextCommands_returnsCorrectSlots() { + ImmutableList customLayout = + ImmutableList.of( + new CommandButton.Builder(CommandButton.ICON_ALBUM) + .setPlayerCommand(Player.COMMAND_PREPARE) + .build(), + new CommandButton.Builder(CommandButton.ICON_SHUFFLE_ON) + .setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT) + .build(), + new CommandButton.Builder(CommandButton.ICON_ARTIST) + .setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS) + .build()); + Bundle reservationBundle = new Bundle(); + reservationBundle.putBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV, false); + reservationBundle.putBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT, false); + Player.Commands playerCommands = + new Player.Commands.Builder() + .addAll(Player.COMMAND_SEEK_TO_NEXT, Player.COMMAND_SEEK_TO_PREVIOUS) + .build(); + + ImmutableList mediaButtonPreferences = + CommandButton.getMediaButtonPreferencesFromCustomLayout( + customLayout, playerCommands, reservationBundle); + + assertThat(mediaButtonPreferences) + .containsExactly( + new CommandButton.Builder(CommandButton.ICON_ALBUM) + .setPlayerCommand(Player.COMMAND_PREPARE) + .setSlots(CommandButton.SLOT_OVERFLOW) + .build(), + new CommandButton.Builder(CommandButton.ICON_SHUFFLE_ON) + .setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT) + .setSlots(CommandButton.SLOT_OVERFLOW) + .build(), + new CommandButton.Builder(CommandButton.ICON_ARTIST) + .setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS) + .setSlots(CommandButton.SLOT_OVERFLOW) + .build()) + .inOrder(); + } + + @Test + public void + getMediaButtonPreferencesFromCustomLayout_withPrevCommandNoNextReservation_returnsCorrectSlots() { + ImmutableList customLayout = + ImmutableList.of( + new CommandButton.Builder(CommandButton.ICON_ALBUM) + .setPlayerCommand(Player.COMMAND_PREPARE) + .build(), + new CommandButton.Builder(CommandButton.ICON_SHUFFLE_ON) + .setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT) + .build(), + new CommandButton.Builder(CommandButton.ICON_ARTIST) + .setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS) + .build()); + Bundle reservationBundle = new Bundle(); + reservationBundle.putBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV, false); + reservationBundle.putBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT, false); + Player.Commands playerCommands = + new Player.Commands.Builder().addAll(Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM).build(); + + ImmutableList mediaButtonPreferences = + CommandButton.getMediaButtonPreferencesFromCustomLayout( + customLayout, playerCommands, reservationBundle); + + assertThat(mediaButtonPreferences) + .containsExactly( + new CommandButton.Builder(CommandButton.ICON_ALBUM) + .setPlayerCommand(Player.COMMAND_PREPARE) + .setSlots(CommandButton.SLOT_FORWARD, CommandButton.SLOT_OVERFLOW) + .build(), + new CommandButton.Builder(CommandButton.ICON_SHUFFLE_ON) + .setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT) + .setSlots(CommandButton.SLOT_OVERFLOW) + .build(), + new CommandButton.Builder(CommandButton.ICON_ARTIST) + .setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS) + .setSlots(CommandButton.SLOT_OVERFLOW) + .build()) + .inOrder(); + } + + @Test + public void + getMediaButtonPreferencesFromCustomLayout_withPrevCommandAndNextReservation_returnsCorrectSlots() { + ImmutableList customLayout = + ImmutableList.of( + new CommandButton.Builder(CommandButton.ICON_ALBUM) + .setPlayerCommand(Player.COMMAND_PREPARE) + .build(), + new CommandButton.Builder(CommandButton.ICON_SHUFFLE_ON) + .setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT) + .build(), + new CommandButton.Builder(CommandButton.ICON_ARTIST) + .setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS) + .build()); + Bundle reservationBundle = new Bundle(); + reservationBundle.putBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV, false); + reservationBundle.putBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT, true); + Player.Commands playerCommands = + new Player.Commands.Builder().addAll(Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM).build(); + + ImmutableList mediaButtonPreferences = + CommandButton.getMediaButtonPreferencesFromCustomLayout( + customLayout, playerCommands, reservationBundle); + + assertThat(mediaButtonPreferences) + .containsExactly( + new CommandButton.Builder(CommandButton.ICON_ALBUM) + .setPlayerCommand(Player.COMMAND_PREPARE) + .setSlots(CommandButton.SLOT_OVERFLOW) + .build(), + new CommandButton.Builder(CommandButton.ICON_SHUFFLE_ON) + .setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT) + .setSlots(CommandButton.SLOT_OVERFLOW) + .build(), + new CommandButton.Builder(CommandButton.ICON_ARTIST) + .setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS) + .setSlots(CommandButton.SLOT_OVERFLOW) + .build()) + .inOrder(); + } + + @Test + public void + getMediaButtonPreferencesFromCustomLayout_withNextCommandNoPrevReservation_returnsCorrectSlots() { + ImmutableList customLayout = + ImmutableList.of( + new CommandButton.Builder(CommandButton.ICON_ALBUM) + .setPlayerCommand(Player.COMMAND_PREPARE) + .build(), + new CommandButton.Builder(CommandButton.ICON_SHUFFLE_ON) + .setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT) + .build(), + new CommandButton.Builder(CommandButton.ICON_ARTIST) + .setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS) + .build()); + Bundle reservationBundle = new Bundle(); + reservationBundle.putBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV, false); + reservationBundle.putBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT, false); + Player.Commands playerCommands = + new Player.Commands.Builder().addAll(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM).build(); + + ImmutableList mediaButtonPreferences = + CommandButton.getMediaButtonPreferencesFromCustomLayout( + customLayout, playerCommands, reservationBundle); + + assertThat(mediaButtonPreferences) + .containsExactly( + new CommandButton.Builder(CommandButton.ICON_ALBUM) + .setPlayerCommand(Player.COMMAND_PREPARE) + .setSlots(CommandButton.SLOT_BACK, CommandButton.SLOT_OVERFLOW) + .build(), + new CommandButton.Builder(CommandButton.ICON_SHUFFLE_ON) + .setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT) + .setSlots(CommandButton.SLOT_OVERFLOW) + .build(), + new CommandButton.Builder(CommandButton.ICON_ARTIST) + .setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS) + .setSlots(CommandButton.SLOT_OVERFLOW) + .build()) + .inOrder(); + } + + @Test + public void + getMediaButtonPreferencesFromCustomLayout_withNextCommandAndPrevReservation_returnsCorrectSlots() { + ImmutableList customLayout = + ImmutableList.of( + new CommandButton.Builder(CommandButton.ICON_ALBUM) + .setPlayerCommand(Player.COMMAND_PREPARE) + .build(), + new CommandButton.Builder(CommandButton.ICON_SHUFFLE_ON) + .setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT) + .build(), + new CommandButton.Builder(CommandButton.ICON_ARTIST) + .setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS) + .build()); + Bundle reservationBundle = new Bundle(); + reservationBundle.putBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV, true); + reservationBundle.putBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT, false); + Player.Commands playerCommands = + new Player.Commands.Builder().addAll(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM).build(); + + ImmutableList mediaButtonPreferences = + CommandButton.getMediaButtonPreferencesFromCustomLayout( + customLayout, playerCommands, reservationBundle); + + assertThat(mediaButtonPreferences) + .containsExactly( + new CommandButton.Builder(CommandButton.ICON_ALBUM) + .setPlayerCommand(Player.COMMAND_PREPARE) + .setSlots(CommandButton.SLOT_OVERFLOW) + .build(), + new CommandButton.Builder(CommandButton.ICON_SHUFFLE_ON) + .setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT) + .setSlots(CommandButton.SLOT_OVERFLOW) + .build(), + new CommandButton.Builder(CommandButton.ICON_ARTIST) + .setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS) + .setSlots(CommandButton.SLOT_OVERFLOW) + .build()) + .inOrder(); + } + + @Test + public void + getMediaButtonPreferencesFromCustomLayout_withoutPrevNextCommandsNoReservations_returnsCorrectSlots() { + ImmutableList customLayout = + ImmutableList.of( + new CommandButton.Builder(CommandButton.ICON_ALBUM) + .setPlayerCommand(Player.COMMAND_PREPARE) + .build(), + new CommandButton.Builder(CommandButton.ICON_SHUFFLE_ON) + .setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT) + .build(), + new CommandButton.Builder(CommandButton.ICON_ARTIST) + .setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS) + .build()); + Bundle reservationBundle = new Bundle(); + reservationBundle.putBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV, false); + reservationBundle.putBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT, false); + Player.Commands playerCommands = Player.Commands.EMPTY; + + ImmutableList mediaButtonPreferences = + CommandButton.getMediaButtonPreferencesFromCustomLayout( + customLayout, playerCommands, reservationBundle); + + assertThat(mediaButtonPreferences) + .containsExactly( + new CommandButton.Builder(CommandButton.ICON_ALBUM) + .setPlayerCommand(Player.COMMAND_PREPARE) + .setSlots( + CommandButton.SLOT_BACK, + CommandButton.SLOT_FORWARD, + CommandButton.SLOT_OVERFLOW) + .build(), + new CommandButton.Builder(CommandButton.ICON_SHUFFLE_ON) + .setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT) + .setSlots(CommandButton.SLOT_FORWARD, CommandButton.SLOT_OVERFLOW) + .build(), + new CommandButton.Builder(CommandButton.ICON_ARTIST) + .setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS) + .setSlots(CommandButton.SLOT_OVERFLOW) + .build()) + .inOrder(); + } + + @Test + public void + getMediaButtonPreferencesFromCustomLayout_withoutPrevNextCommandsAndPrevReservation_returnsCorrectSlots() { + ImmutableList customLayout = + ImmutableList.of( + new CommandButton.Builder(CommandButton.ICON_ALBUM) + .setPlayerCommand(Player.COMMAND_PREPARE) + .build(), + new CommandButton.Builder(CommandButton.ICON_SHUFFLE_ON) + .setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT) + .build(), + new CommandButton.Builder(CommandButton.ICON_ARTIST) + .setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS) + .build()); + Bundle reservationBundle = new Bundle(); + reservationBundle.putBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV, true); + reservationBundle.putBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT, false); + Player.Commands playerCommands = Player.Commands.EMPTY; + + ImmutableList mediaButtonPreferences = + CommandButton.getMediaButtonPreferencesFromCustomLayout( + customLayout, playerCommands, reservationBundle); + + assertThat(mediaButtonPreferences) + .containsExactly( + new CommandButton.Builder(CommandButton.ICON_ALBUM) + .setPlayerCommand(Player.COMMAND_PREPARE) + .setSlots(CommandButton.SLOT_FORWARD, CommandButton.SLOT_OVERFLOW) + .build(), + new CommandButton.Builder(CommandButton.ICON_SHUFFLE_ON) + .setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT) + .setSlots(CommandButton.SLOT_OVERFLOW) + .build(), + new CommandButton.Builder(CommandButton.ICON_ARTIST) + .setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS) + .setSlots(CommandButton.SLOT_OVERFLOW) + .build()) + .inOrder(); + } + + @Test + public void + getMediaButtonPreferencesFromCustomLayout_withoutPrevNextCommandsAndNextReservation_returnsCorrectSlots() { + ImmutableList customLayout = + ImmutableList.of( + new CommandButton.Builder(CommandButton.ICON_ALBUM) + .setPlayerCommand(Player.COMMAND_PREPARE) + .build(), + new CommandButton.Builder(CommandButton.ICON_SHUFFLE_ON) + .setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT) + .build(), + new CommandButton.Builder(CommandButton.ICON_ARTIST) + .setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS) + .build()); + Bundle reservationBundle = new Bundle(); + reservationBundle.putBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV, false); + reservationBundle.putBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT, true); + Player.Commands playerCommands = Player.Commands.EMPTY; + + ImmutableList mediaButtonPreferences = + CommandButton.getMediaButtonPreferencesFromCustomLayout( + customLayout, playerCommands, reservationBundle); + + assertThat(mediaButtonPreferences) + .containsExactly( + new CommandButton.Builder(CommandButton.ICON_ALBUM) + .setPlayerCommand(Player.COMMAND_PREPARE) + .setSlots(CommandButton.SLOT_BACK, CommandButton.SLOT_OVERFLOW) + .build(), + new CommandButton.Builder(CommandButton.ICON_SHUFFLE_ON) + .setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT) + .setSlots(CommandButton.SLOT_OVERFLOW) + .build(), + new CommandButton.Builder(CommandButton.ICON_ARTIST) + .setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS) + .setSlots(CommandButton.SLOT_OVERFLOW) + .build()) + .inOrder(); + } + + @Test + public void + getMediaButtonPreferencesFromCustomLayout_withoutPrevNextCommandsAndPrevNextReservations_returnsCorrectSlots() { + ImmutableList customLayout = + ImmutableList.of( + new CommandButton.Builder(CommandButton.ICON_ALBUM) + .setPlayerCommand(Player.COMMAND_PREPARE) + .build(), + new CommandButton.Builder(CommandButton.ICON_SHUFFLE_ON) + .setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT) + .build(), + new CommandButton.Builder(CommandButton.ICON_ARTIST) + .setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS) + .build()); + Bundle reservationBundle = new Bundle(); + reservationBundle.putBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV, true); + reservationBundle.putBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT, true); + Player.Commands playerCommands = Player.Commands.EMPTY; + + ImmutableList mediaButtonPreferences = + CommandButton.getMediaButtonPreferencesFromCustomLayout( + customLayout, playerCommands, reservationBundle); + + assertThat(mediaButtonPreferences) + .containsExactly( + new CommandButton.Builder(CommandButton.ICON_ALBUM) + .setPlayerCommand(Player.COMMAND_PREPARE) + .setSlots(CommandButton.SLOT_OVERFLOW) + .build(), + new CommandButton.Builder(CommandButton.ICON_SHUFFLE_ON) + .setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT) + .setSlots(CommandButton.SLOT_OVERFLOW) + .build(), + new CommandButton.Builder(CommandButton.ICON_ARTIST) + .setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS) + .setSlots(CommandButton.SLOT_OVERFLOW) + .build()) + .inOrder(); + } } diff --git a/libraries/session/src/test/java/androidx/media3/session/LegacyConversionsTest.java b/libraries/session/src/test/java/androidx/media3/session/LegacyConversionsTest.java index 2bcacfb63d..bfac5680a2 100644 --- a/libraries/session/src/test/java/androidx/media3/session/LegacyConversionsTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/LegacyConversionsTest.java @@ -1081,7 +1081,10 @@ public final class LegacyConversionsTest { @Test public void convertToMediaButtonPreferences_withNull_returnsEmptyList() { - assertThat(LegacyConversions.convertToMediaButtonPreferences(null)).isEmpty(); + assertThat( + LegacyConversions.convertToMediaButtonPreferences( + null, Player.Commands.EMPTY, Bundle.EMPTY)) + .isEmpty(); } @Test @@ -1107,7 +1110,9 @@ public final class LegacyConversionsTest { .addCustomAction(action) .build(); - ImmutableList buttons = LegacyConversions.convertToMediaButtonPreferences(state); + ImmutableList buttons = + LegacyConversions.convertToMediaButtonPreferences( + state, Player.Commands.EMPTY, Bundle.EMPTY); assertThat(buttons).hasSize(1); CommandButton button = buttons.get(0); @@ -1140,7 +1145,9 @@ public final class LegacyConversionsTest { .addCustomAction(action) .build(); - ImmutableList buttons = LegacyConversions.convertToMediaButtonPreferences(state); + ImmutableList buttons = + LegacyConversions.convertToMediaButtonPreferences( + state, Player.Commands.EMPTY, Bundle.EMPTY); assertThat(buttons).hasSize(1); CommandButton button = buttons.get(0); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerWithMediaSessionCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerWithMediaSessionCompatTest.java index 1608360617..c453b2ddd0 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerWithMediaSessionCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerWithMediaSessionCompatTest.java @@ -47,6 +47,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; import com.google.common.collect.ImmutableList; +import com.google.common.primitives.ImmutableIntArray; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import java.util.ArrayList; @@ -475,11 +476,16 @@ public class MediaControllerListenerWithMediaSessionCompatTest { .setDisplayName("button1") .setIconResId(R.drawable.media3_notification_small_icon) .setSessionCommand(new SessionCommand("command1", Bundle.EMPTY)) + .setEnabled(true) + .setSlots( + CommandButton.SLOT_BACK, CommandButton.SLOT_FORWARD, CommandButton.SLOT_OVERFLOW) .build(); CommandButton button2 = new CommandButton.Builder(CommandButton.ICON_FAST_FORWARD) .setDisplayName("button2") .setSessionCommand(new SessionCommand("command2", Bundle.EMPTY)) + .setEnabled(true) + .setSlots(CommandButton.SLOT_FORWARD, CommandButton.SLOT_OVERFLOW) .build(); ConditionVariable onSetCustomLayoutCalled = new ConditionVariable(); ConditionVariable onCustomLayoutChangedCalled = new ConditionVariable(); @@ -537,10 +543,8 @@ public class MediaControllerListenerWithMediaSessionCompatTest { assertThat(onSetCustomLayoutCalled.block(TIMEOUT_MS)).isTrue(); assertThat(onCustomLayoutChangedCalled.block(TIMEOUT_MS)).isTrue(); - ImmutableList expectedFirstCustomLayout = - ImmutableList.of(button1.copyWithIsEnabled(true), button2.copyWithIsEnabled(true)); - ImmutableList expectedSecondCustomLayout = - ImmutableList.of(button1.copyWithIsEnabled(true)); + ImmutableList expectedFirstCustomLayout = ImmutableList.of(button1, button2); + ImmutableList expectedSecondCustomLayout = ImmutableList.of(button1); assertThat(setCustomLayoutArguments) .containsExactly(expectedFirstCustomLayout, expectedSecondCustomLayout) .inOrder(); @@ -559,11 +563,16 @@ public class MediaControllerListenerWithMediaSessionCompatTest { .setDisplayName("button1") .setIconResId(R.drawable.media3_notification_small_icon) .setSessionCommand(new SessionCommand("command1", Bundle.EMPTY)) + .setEnabled(true) + .setSlots( + CommandButton.SLOT_BACK, CommandButton.SLOT_FORWARD, CommandButton.SLOT_OVERFLOW) .build(); CommandButton button2 = new CommandButton.Builder(CommandButton.ICON_FAST_FORWARD) .setDisplayName("button2") .setSessionCommand(new SessionCommand("command2", Bundle.EMPTY)) + .setEnabled(true) + .setSlots(CommandButton.SLOT_FORWARD, CommandButton.SLOT_OVERFLOW) .build(); ConditionVariable onMediaButtonPreferencesChangedCalled = new ConditionVariable(); List> onMediaButtonPreferencesChangedArguments = new ArrayList<>(); @@ -609,9 +618,8 @@ public class MediaControllerListenerWithMediaSessionCompatTest { assertThat(onMediaButtonPreferencesChangedCalled.block(TIMEOUT_MS)).isTrue(); ImmutableList expectedFirstMediaButtonPreferences = - ImmutableList.of(button1.copyWithIsEnabled(true), button2.copyWithIsEnabled(true)); - ImmutableList expectedSecondMediaButtonPreferences = - ImmutableList.of(button1.copyWithIsEnabled(true)); + ImmutableList.of(button1, button2); + ImmutableList expectedSecondMediaButtonPreferences = ImmutableList.of(button1); assertThat(onMediaButtonPreferencesChangedArguments) .containsExactly(expectedFirstMediaButtonPreferences, expectedSecondMediaButtonPreferences) .inOrder(); @@ -620,6 +628,177 @@ public class MediaControllerListenerWithMediaSessionCompatTest { .inOrder(); } + @Test + public void getMediaButtonPreferences_withPrevNextActions() 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> reportedMediaButtonPreferences = new ArrayList<>(); + controllerTestRule.createController( + session.getSessionToken(), + new MediaController.Listener() { + @Override + public void onMediaButtonPreferencesChanged( + MediaController controller, List mediaButtonPreferences) { + reportedMediaButtonPreferences.add(mediaButtonPreferences); + 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 playbackStatePrev = + new PlaybackStateCompat.Builder() + .addCustomAction(customAction1) + .addCustomAction(customAction2) + .setActions(PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS) + .build(); + PlaybackStateCompat playbackStateNext = + new PlaybackStateCompat.Builder() + .addCustomAction(customAction1) + .addCustomAction(customAction2) + .setActions(PlaybackStateCompat.ACTION_SKIP_TO_NEXT) + .build(); + PlaybackStateCompat playbackStatePrevNext = + new PlaybackStateCompat.Builder() + .addCustomAction(customAction1) + .addCustomAction(customAction2) + .setActions( + PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS + | PlaybackStateCompat.ACTION_SKIP_TO_NEXT) + .build(); + + session.setPlaybackState(playbackStatePrev); + assertThat(onMediaButtonPreferencesChangedCalled.block(TIMEOUT_MS)).isTrue(); + onMediaButtonPreferencesChangedCalled.close(); + session.setPlaybackState(playbackStateNext); + assertThat(onMediaButtonPreferencesChangedCalled.block(TIMEOUT_MS)).isTrue(); + onMediaButtonPreferencesChangedCalled.close(); + session.setPlaybackState(playbackStatePrevNext); + assertThat(onMediaButtonPreferencesChangedCalled.block(TIMEOUT_MS)).isTrue(); + + assertThat(reportedMediaButtonPreferences) + .containsExactly( + ImmutableList.of( + button1.copyWithSlots( + ImmutableIntArray.of(CommandButton.SLOT_FORWARD, CommandButton.SLOT_OVERFLOW)), + button2.copyWithSlots(ImmutableIntArray.of(CommandButton.SLOT_OVERFLOW))), + ImmutableList.of( + button1.copyWithSlots( + ImmutableIntArray.of(CommandButton.SLOT_BACK, CommandButton.SLOT_OVERFLOW)), + button2.copyWithSlots(ImmutableIntArray.of(CommandButton.SLOT_OVERFLOW))), + ImmutableList.of( + button1.copyWithSlots(ImmutableIntArray.of(CommandButton.SLOT_OVERFLOW)), + button2.copyWithSlots(ImmutableIntArray.of(CommandButton.SLOT_OVERFLOW)))) + .inOrder(); + } + + @Test + public void getMediaButtonPreferences_withSlotReservations() 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> reportedMediaButtonPreferences = new ArrayList<>(); + controllerTestRule.createController( + session.getSessionToken(), + new MediaController.Listener() { + @Override + public void onMediaButtonPreferencesChanged( + MediaController controller, List mediaButtonPreferences) { + reportedMediaButtonPreferences.add(mediaButtonPreferences); + 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 playbackState = + new PlaybackStateCompat.Builder() + .addCustomAction(customAction1) + .addCustomAction(customAction2) + .build(); + Bundle extrasPrevSlotReservation = new Bundle(); + extrasPrevSlotReservation.putBoolean( + androidx.media.utils.MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true); + Bundle extrasNextSlotReservation = new Bundle(); + extrasNextSlotReservation.putBoolean( + androidx.media.utils.MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true); + Bundle extrasPrevNextSlotReservation = new Bundle(); + extrasPrevNextSlotReservation.putBoolean( + androidx.media.utils.MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true); + extrasPrevNextSlotReservation.putBoolean( + androidx.media.utils.MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true); + + session.setExtras(extrasPrevSlotReservation); + session.setPlaybackState(playbackState); + assertThat(onMediaButtonPreferencesChangedCalled.block(TIMEOUT_MS)).isTrue(); + onMediaButtonPreferencesChangedCalled.close(); + session.setExtras(extrasNextSlotReservation); + assertThat(onMediaButtonPreferencesChangedCalled.block(TIMEOUT_MS)).isTrue(); + onMediaButtonPreferencesChangedCalled.close(); + session.setExtras(extrasPrevNextSlotReservation); + assertThat(onMediaButtonPreferencesChangedCalled.block(TIMEOUT_MS)).isTrue(); + + assertThat(reportedMediaButtonPreferences) + .containsExactly( + ImmutableList.of( + button1.copyWithSlots( + ImmutableIntArray.of(CommandButton.SLOT_FORWARD, CommandButton.SLOT_OVERFLOW)), + button2.copyWithSlots(ImmutableIntArray.of(CommandButton.SLOT_OVERFLOW))), + ImmutableList.of( + button1.copyWithSlots( + ImmutableIntArray.of(CommandButton.SLOT_BACK, CommandButton.SLOT_OVERFLOW)), + button2.copyWithSlots(ImmutableIntArray.of(CommandButton.SLOT_OVERFLOW))), + ImmutableList.of( + button1.copyWithSlots(ImmutableIntArray.of(CommandButton.SLOT_OVERFLOW)), + button2.copyWithSlots(ImmutableIntArray.of(CommandButton.SLOT_OVERFLOW)))) + .inOrder(); + } + @Test public void getCurrentPosition_unknownPlaybackPosition_convertedToZero() throws Exception { session.setPlaybackState( diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java index f42a2b9b7c..5e962d3f72 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java @@ -70,6 +70,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; import com.google.common.collect.ImmutableList; +import com.google.common.primitives.ImmutableIntArray; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import java.util.ArrayList; @@ -215,11 +216,11 @@ public class MediaControllerTest { assertThat(threadTestRule.getHandler().postAndSync(controller::getCustomLayout)) .containsExactly( - button1.copyWithIsEnabled(true), - button2.copyWithIsEnabled(false), - button3.copyWithIsEnabled(false), - button4.copyWithIsEnabled(true), - button5.copyWithIsEnabled(false)) + withBackForwardOverflowSlot(button1.copyWithIsEnabled(true)), + withForwardOverflowSlot(button2.copyWithIsEnabled(false)), + withOverflowSlot(button3.copyWithIsEnabled(false)), + withOverflowSlot(button4.copyWithIsEnabled(true)), + withOverflowSlot(button5.copyWithIsEnabled(false))) .inOrder(); session.cleanUp(); @@ -299,15 +300,17 @@ public class MediaControllerTest { threadTestRule.getHandler().postAndSync(controller::getCustomLayout); assertThat(initialCustomLayoutFromGetter) - .containsExactly(button1.copyWithIsEnabled(true), button3.copyWithIsEnabled(false)) + .containsExactly( + withBackForwardOverflowSlot(button1.copyWithIsEnabled(true)), + withForwardOverflowSlot(button3.copyWithIsEnabled(false))) .inOrder(); ImmutableList expectedNewButtons = ImmutableList.of( - button1.copyWithIsEnabled(true), - button2.copyWithIsEnabled(false), - button4.copyWithIsEnabled(false), - button5.copyWithIsEnabled(true), - button6.copyWithIsEnabled(false)); + withBackForwardOverflowSlot(button1.copyWithIsEnabled(true)), + withForwardOverflowSlot(button2.copyWithIsEnabled(false)), + withOverflowSlot(button4.copyWithIsEnabled(false)), + withOverflowSlot(button5.copyWithIsEnabled(true)), + withOverflowSlot(button6.copyWithIsEnabled(false))); assertThat(newCustomLayoutFromGetter).containsExactlyElementsIn(expectedNewButtons).inOrder(); assertThat(reportedCustomLayout.get()).containsExactlyElementsIn(expectedNewButtons).inOrder(); assertThat(reportedCustomLayoutChanged.get()) @@ -375,39 +378,39 @@ public class MediaControllerTest { assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(initialCustomLayout) .containsExactly( - button1.copyWithIsEnabled(true), - button2.copyWithIsEnabled(false), - button3.copyWithIsEnabled(true), - button4.copyWithIsEnabled(false)); + withBackForwardOverflowSlot(button1.copyWithIsEnabled(true)), + withForwardOverflowSlot(button2.copyWithIsEnabled(false)), + withOverflowSlot(button3.copyWithIsEnabled(true)), + withOverflowSlot(button4.copyWithIsEnabled(false))); assertThat(reportedCustomLayoutChanged).hasSize(2); assertThat(reportedCustomLayoutChanged.get(0)) .containsExactly( - button1.copyWithIsEnabled(false), - button2.copyWithIsEnabled(false), - button3.copyWithIsEnabled(false), - button4.copyWithIsEnabled(false)) + withBackForwardOverflowSlot(button1.copyWithIsEnabled(false)), + withForwardOverflowSlot(button2.copyWithIsEnabled(false)), + withOverflowSlot(button3.copyWithIsEnabled(false)), + withOverflowSlot(button4.copyWithIsEnabled(false))) .inOrder(); assertThat(reportedCustomLayoutChanged.get(1)) .containsExactly( - button1.copyWithIsEnabled(false), - button2.copyWithIsEnabled(false), - button3.copyWithIsEnabled(false), - button4.copyWithIsEnabled(true)) + withBackForwardOverflowSlot(button1.copyWithIsEnabled(false)), + withForwardOverflowSlot(button2.copyWithIsEnabled(false)), + withOverflowSlot(button3.copyWithIsEnabled(false)), + withOverflowSlot(button4.copyWithIsEnabled(true))) .inOrder(); assertThat(getterCustomLayoutChanged).hasSize(2); assertThat(getterCustomLayoutChanged.get(0)) .containsExactly( - button1.copyWithIsEnabled(false), - button2.copyWithIsEnabled(false), - button3.copyWithIsEnabled(false), - button4.copyWithIsEnabled(false)) + withBackForwardOverflowSlot(button1.copyWithIsEnabled(false)), + withForwardOverflowSlot(button2.copyWithIsEnabled(false)), + withOverflowSlot(button3.copyWithIsEnabled(false)), + withOverflowSlot(button4.copyWithIsEnabled(false))) .inOrder(); assertThat(getterCustomLayoutChanged.get(1)) .containsExactly( - button1.copyWithIsEnabled(false), - button2.copyWithIsEnabled(false), - button3.copyWithIsEnabled(false), - button4.copyWithIsEnabled(true)) + withBackForwardOverflowSlot(button1.copyWithIsEnabled(false)), + withForwardOverflowSlot(button2.copyWithIsEnabled(false)), + withOverflowSlot(button3.copyWithIsEnabled(false)), + withOverflowSlot(button4.copyWithIsEnabled(true))) .inOrder(); session.cleanUp(); } @@ -450,13 +453,18 @@ public class MediaControllerTest { new Player.Commands.Builder().add(Player.COMMAND_PLAY_PAUSE).build()); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(initialCustomLayout).containsExactly(button.copyWithIsEnabled(true)); + assertThat(initialCustomLayout) + .containsExactly(withBackForwardOverflowSlot(button.copyWithIsEnabled(true))); assertThat(reportedCustomLayouts).hasSize(2); - assertThat(reportedCustomLayouts.get(0)).containsExactly(button.copyWithIsEnabled(false)); - assertThat(reportedCustomLayouts.get(1)).containsExactly(button.copyWithIsEnabled(true)); + assertThat(reportedCustomLayouts.get(0)) + .containsExactly(withBackForwardOverflowSlot(button.copyWithIsEnabled(false))); + assertThat(reportedCustomLayouts.get(1)) + .containsExactly(withBackForwardOverflowSlot(button.copyWithIsEnabled(true))); assertThat(getterCustomLayouts).hasSize(2); - assertThat(getterCustomLayouts.get(0)).containsExactly(button.copyWithIsEnabled(false)); - assertThat(getterCustomLayouts.get(1)).containsExactly(button.copyWithIsEnabled(true)); + assertThat(getterCustomLayouts.get(0)) + .containsExactly(withBackForwardOverflowSlot(button.copyWithIsEnabled(false))); + assertThat(getterCustomLayouts.get(1)) + .containsExactly(withBackForwardOverflowSlot(button.copyWithIsEnabled(true))); session.cleanUp(); } @@ -526,36 +534,190 @@ public class MediaControllerTest { session.setCustomLayout(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(initialCustomLayout).containsExactly(button1Enabled, button2Disabled).inOrder(); + CommandButton button1EnabledBackSlot = + withBackForwardOverflowSlot(button1.copyWithIsEnabled(true)); + CommandButton button2DisabledForwardSlot = + withForwardOverflowSlot(button2.copyWithIsEnabled(false)); + CommandButton button3DisabledBackSlot = + withBackForwardOverflowSlot(button3.copyWithIsEnabled(false)); + CommandButton button4DisabledForwardSlot = + withForwardOverflowSlot(button4.copyWithIsEnabled(false)); + assertThat(initialCustomLayout) + .containsExactly(button1EnabledBackSlot, button2DisabledForwardSlot) + .inOrder(); assertThat(reportedCustomLayout) .containsExactly( - ImmutableList.of(button1Enabled, button2Disabled), - ImmutableList.of(button3Disabled, button4Disabled), - ImmutableList.of(button1Enabled, button2Disabled)) + ImmutableList.of(button1EnabledBackSlot, button2DisabledForwardSlot), + ImmutableList.of(button3DisabledBackSlot, button4DisabledForwardSlot), + ImmutableList.of(button1EnabledBackSlot, button2DisabledForwardSlot)) .inOrder(); assertThat(getterCustomLayout) .containsExactly( - ImmutableList.of(button1Enabled, button2Disabled), - ImmutableList.of(button3Disabled, button4Disabled), - ImmutableList.of(button1Enabled, button2Disabled)) + ImmutableList.of(button1EnabledBackSlot, button2DisabledForwardSlot), + ImmutableList.of(button3DisabledBackSlot, button4DisabledForwardSlot), + ImmutableList.of(button1EnabledBackSlot, button2DisabledForwardSlot)) .inOrder(); assertThat(reportedCustomLayoutChanged) .containsExactly( - ImmutableList.of(button3Disabled, button4Disabled), - ImmutableList.of(button1Enabled, button2Disabled)) + ImmutableList.of(button3DisabledBackSlot, button4DisabledForwardSlot), + ImmutableList.of(button1EnabledBackSlot, button2DisabledForwardSlot)) .inOrder(); assertThat(getterCustomLayoutChanged) .containsExactly( - ImmutableList.of(button3Disabled, button4Disabled), - ImmutableList.of(button1Enabled, button2Disabled)) + ImmutableList.of(button3DisabledBackSlot, button4DisabledForwardSlot), + ImmutableList.of(button1EnabledBackSlot, button2DisabledForwardSlot)) .inOrder(); session.cleanUp(); } + @Test + public void getCustomLayout_setAvailablePrevNextCommand_reportsCustomLayoutChanged() + throws Exception { + RemoteMediaSession session = createRemoteMediaSession(TEST_GET_CUSTOM_LAYOUT, null); + CommandButton button1 = + new CommandButton.Builder(CommandButton.ICON_ALBUM) + .setDisplayName("button1") + .setSessionCommand(new SessionCommand("command1", Bundle.EMPTY)) + .build(); + CommandButton button2 = + new CommandButton.Builder(CommandButton.ICON_REWIND) + .setDisplayName("button2") + .setSessionCommand(new SessionCommand("command2", Bundle.EMPTY)) + .build(); + CommandButton button3 = + new CommandButton.Builder(CommandButton.ICON_SHUFFLE_ON) + .setDisplayName("button3") + .setSessionCommand(new SessionCommand("command3", Bundle.EMPTY)) + .build(); + SessionCommands allSessionCommands = + new SessionCommands.Builder() + .add(button1.sessionCommand) + .add(button2.sessionCommand) + .add(button3.sessionCommand) + .build(); + setupCustomLayout(session, ImmutableList.of(button1, button2, button3)); + CountDownLatch latch = new CountDownLatch(4); + List> reportedCustomLayouts = new ArrayList<>(); + MediaController.Listener listener = + new MediaController.Listener() { + @Override + public void onCustomLayoutChanged( + MediaController controller, List layout) { + reportedCustomLayouts.add(layout); + latch.countDown(); + } + }; + controllerTestRule.createController( + session.getToken(), /* connectionHints= */ Bundle.EMPTY, listener); + + session.setAvailableCommands(allSessionCommands, Player.Commands.EMPTY); + session.setAvailableCommands( + allSessionCommands, new Player.Commands.Builder().add(Player.COMMAND_SEEK_TO_NEXT).build()); + session.setAvailableCommands( + allSessionCommands, + new Player.Commands.Builder().add(Player.COMMAND_SEEK_TO_PREVIOUS).build()); + session.setAvailableCommands( + allSessionCommands, + new Player.Commands.Builder() + .addAll(Player.COMMAND_SEEK_TO_NEXT, Player.COMMAND_SEEK_TO_PREVIOUS) + .build()); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(reportedCustomLayouts) + .containsExactly( + ImmutableList.of( + withBackForwardOverflowSlot(button1), + withForwardOverflowSlot(button2), + withOverflowSlot(button3)), + ImmutableList.of( + withBackOverflowSlot(button1), + withOverflowSlot(button2), + withOverflowSlot(button3)), + ImmutableList.of( + withForwardOverflowSlot(button1), + withOverflowSlot(button2), + withOverflowSlot(button3)), + ImmutableList.of( + withOverflowSlot(button1), withOverflowSlot(button2), withOverflowSlot(button3))); + session.cleanUp(); + } + + @Test + public void getCustomLayout_setSessionExtrasForPrevNextReservations_reportsCustomLayoutChanged() + throws Exception { + RemoteMediaSession session = createRemoteMediaSession(TEST_GET_CUSTOM_LAYOUT, null); + CommandButton button1 = + new CommandButton.Builder(CommandButton.ICON_ALBUM) + .setDisplayName("button1") + .setSessionCommand(new SessionCommand("command1", Bundle.EMPTY)) + .build(); + CommandButton button2 = + new CommandButton.Builder(CommandButton.ICON_REWIND) + .setDisplayName("button2") + .setSessionCommand(new SessionCommand("command2", Bundle.EMPTY)) + .build(); + CommandButton button3 = + new CommandButton.Builder(CommandButton.ICON_SHUFFLE_ON) + .setDisplayName("button3") + .setSessionCommand(new SessionCommand("command3", Bundle.EMPTY)) + .build(); + SessionCommands allSessionCommands = + new SessionCommands.Builder() + .add(button1.sessionCommand) + .add(button2.sessionCommand) + .add(button3.sessionCommand) + .build(); + setupCustomLayout(session, ImmutableList.of(button1, button2, button3)); + CountDownLatch latch = new CountDownLatch(4); + List> reportedCustomLayouts = new ArrayList<>(); + MediaController.Listener listener = + new MediaController.Listener() { + @Override + public void onCustomLayoutChanged( + MediaController controller, List layout) { + reportedCustomLayouts.add(layout); + latch.countDown(); + } + }; + Bundle extrasNextSlotReservation = new Bundle(); + extrasNextSlotReservation.putBoolean( + MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT, true); + Bundle extrasPrevSlotReservation = new Bundle(); + extrasPrevSlotReservation.putBoolean( + MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV, true); + Bundle extrasPrevNextSlotReservation = new Bundle(); + extrasPrevNextSlotReservation.putBoolean( + MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV, true); + extrasPrevNextSlotReservation.putBoolean( + MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT, true); + controllerTestRule.createController( + session.getToken(), /* connectionHints= */ Bundle.EMPTY, listener); + + session.setAvailableCommands(allSessionCommands, Player.Commands.EMPTY); + session.setSessionExtras(extrasNextSlotReservation); + session.setSessionExtras(extrasPrevSlotReservation); + session.setSessionExtras(extrasPrevNextSlotReservation); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(reportedCustomLayouts) + .containsExactly( + ImmutableList.of( + withBackForwardOverflowSlot(button1), + withForwardOverflowSlot(button2), + withOverflowSlot(button3)), + ImmutableList.of( + withBackOverflowSlot(button1), + withOverflowSlot(button2), + withOverflowSlot(button3)), + ImmutableList.of( + withForwardOverflowSlot(button1), + withOverflowSlot(button2), + withOverflowSlot(button3)), + ImmutableList.of( + withOverflowSlot(button1), withOverflowSlot(button2), withOverflowSlot(button3))); + session.cleanUp(); + } + @Test public void getMediaButtonPreferences_mediaButtonPreferencesBuiltWithSession_includedOnConnect() throws Exception { @@ -2422,4 +2584,24 @@ public class MediaControllerTest { session.setMediaButtonPreferences(ImmutableList.copyOf(mediaButtonPreferences)); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); } + + private static CommandButton withBackForwardOverflowSlot(CommandButton button) { + return button.copyWithSlots( + ImmutableIntArray.of( + CommandButton.SLOT_BACK, CommandButton.SLOT_FORWARD, CommandButton.SLOT_OVERFLOW)); + } + + private static CommandButton withBackOverflowSlot(CommandButton button) { + return button.copyWithSlots( + ImmutableIntArray.of(CommandButton.SLOT_BACK, CommandButton.SLOT_OVERFLOW)); + } + + private static CommandButton withForwardOverflowSlot(CommandButton button) { + return button.copyWithSlots( + ImmutableIntArray.of(CommandButton.SLOT_FORWARD, CommandButton.SLOT_OVERFLOW)); + } + + private static CommandButton withOverflowSlot(CommandButton button) { + return button.copyWithSlots(ImmutableIntArray.of(CommandButton.SLOT_OVERFLOW)); + } } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java index daf81cdb6b..9a3bcaccdd 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java @@ -56,6 +56,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; +import com.google.common.primitives.ImmutableIntArray; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; @@ -162,7 +163,6 @@ public class MediaSessionCallbackTest { .setSessionCommand(new SessionCommand("command1", Bundle.EMPTY)) .setEnabled(true) .build(); - CommandButton button1Disabled = button1.copyWithIsEnabled(false); CommandButton button2 = new CommandButton.Builder(CommandButton.ICON_PAUSE) .setDisplayName("button2") @@ -177,6 +177,7 @@ public class MediaSessionCallbackTest { return new AcceptedResultBuilder(session) .setAvailableSessionCommands( new SessionCommands.Builder().add(button2.sessionCommand).build()) + .setAvailablePlayerCommands(new Player.Commands.Builder().addAllCommands().build()) .setCustomLayout(ImmutableList.of(button1, button2)) .build(); } @@ -198,7 +199,13 @@ public class MediaSessionCallbackTest { ImmutableList layout = remoteController.getCustomLayout(); - assertThat(layout).containsExactly(button1Disabled, button2).inOrder(); + assertThat(layout) + .containsExactly( + button1 + .copyWithIsEnabled(false) + .copyWithSlots(ImmutableIntArray.of(CommandButton.SLOT_OVERFLOW)), + button2.copyWithSlots(ImmutableIntArray.of(CommandButton.SLOT_OVERFLOW))) + .inOrder(); assertThat(remoteController.sendCustomCommand(button1.sessionCommand, Bundle.EMPTY).resultCode) .isEqualTo(ERROR_PERMISSION_DENIED); assertThat(remoteController.sendCustomCommand(button2.sessionCommand, Bundle.EMPTY).resultCode) diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionServiceTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionServiceTest.java index db0b6c52f1..a133829d85 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionServiceTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionServiceTest.java @@ -47,6 +47,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.MediumTest; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; +import com.google.common.primitives.ImmutableIntArray; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import java.util.ArrayList; @@ -331,7 +332,14 @@ public class MediaSessionServiceTest { assertThat(mediaControllerCompat.getPlaybackState().getActions()) .isEqualTo(PlaybackStateCompat.ACTION_SET_RATING); assertThat(remoteController.getCustomLayout()) - .containsExactly(button1.copyWithIsEnabled(false), button2.copyWithIsEnabled(false)) + .containsExactly( + button1 + .copyWithIsEnabled(false) + .copyWithSlots( + ImmutableIntArray.of(CommandButton.SLOT_FORWARD, CommandButton.SLOT_OVERFLOW)), + button2 + .copyWithIsEnabled(false) + .copyWithSlots(ImmutableIntArray.of(CommandButton.SLOT_OVERFLOW))) .inOrder(); assertThat(initialCustomActionsInControllerCompat).isEmpty(); assertThat(mediaControllerCompat.getPlaybackState().getCustomActions()).hasSize(2);