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);