From 12cb8034867582a0a3be26df66ebcbe9fbc54a3e Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 5 Nov 2024 05:03:29 -0800 Subject: [PATCH] Compat logic for MediaController.getCustomLayout When a new media session sets media button preferences, we need to "translate" them back to a custom layout to ensure the user preferences are represented as closely as possible when the controller uses the old button placement rules. PiperOrigin-RevId: 693306153 --- .../media3/session/MediaController.java | 9 +- .../session/MediaControllerImplBase.java | 102 +- .../session/MediaControllerImplLegacy.java | 5 + .../media3/session/MediaControllerTest.java | 1047 ++++++++++++++++- .../session/MediaSessionServiceTest.java | 3 +- 5 files changed, 1102 insertions(+), 64 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaController.java b/libraries/session/src/main/java/androidx/media3/session/MediaController.java index 16487eedaf..12f00cbe2e 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaController.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaController.java @@ -410,6 +410,8 @@ public class MediaController implements Player { * Called when the {@linkplain #getCustomLayout() custom layout} changed. * *

This method will be deprecated, prefer to use {@link #onMediaButtonPreferencesChanged}. + * Note that the media button preferences use {@link CommandButton#slots} to define the allowed + * button placement. * *

The custom layout can change when either the session {@linkplain * MediaSession#setCustomLayout changes the custom layout}, or when the session {@linkplain @@ -1116,6 +1118,8 @@ public class MediaController implements Player { * Returns the custom layout. * *

This method will be deprecated, prefer to use {@link #getMediaButtonPreferences()} instead. + * Note that the media button preferences use {@link CommandButton#slots} to define the allowed + * button placement. * *

After being connected, a change of the custom layout is reported with {@link * Listener#onCustomLayoutChanged(MediaController, List)}. @@ -1127,7 +1131,8 @@ public class MediaController implements Player { */ @UnstableApi public final ImmutableList getCustomLayout() { - return getMediaButtonPreferences(); + verifyApplicationThread(); + return isConnected() ? impl.getCustomLayout() : ImmutableList.of(); } /** @@ -2208,6 +2213,8 @@ public class MediaController implements Player { ImmutableList getMediaButtonPreferences(); + ImmutableList getCustomLayout(); + ImmutableList getCommandButtonsForMediaItem(MediaItem mediaItem); Bundle getSessionExtras(); 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 c3b4936cad..27dc593a0f 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java @@ -93,7 +93,6 @@ import com.google.common.util.concurrent.MoreExecutors; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Objects; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; @@ -128,6 +127,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; private ImmutableList customLayoutOriginal; private ImmutableList mediaButtonPreferencesOriginal; private ImmutableList resolvedMediaButtonPreferences; + private ImmutableList resolvedCustomLayout; private ImmutableMap commandButtonsForMediaItemsMap; private SessionCommands sessionCommands; private Commands playerCommandsFromSession; @@ -158,6 +158,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; customLayoutOriginal = ImmutableList.of(); mediaButtonPreferencesOriginal = ImmutableList.of(); resolvedMediaButtonPreferences = ImmutableList.of(); + resolvedCustomLayout = ImmutableList.of(); commandButtonsForMediaItemsMap = ImmutableMap.of(); playerCommandsFromSession = Commands.EMPTY; playerCommandsFromPlayer = Commands.EMPTY; @@ -751,6 +752,11 @@ import org.checkerframework.checker.nullness.qual.NonNull; return resolvedMediaButtonPreferences; } + @Override + public ImmutableList getCustomLayout() { + return resolvedCustomLayout; + } + @Override public ImmutableList getCommandButtonsForMediaItem(MediaItem mediaItem) { ImmutableList supportedActions = mediaItem.mediaMetadata.supportedCommands; @@ -2662,6 +2668,9 @@ import org.checkerframework.checker.nullness.qual.NonNull; sessionCommands, intersectedPlayerCommands, result.sessionExtras); + resolvedCustomLayout = + resolveCustomLayout( + resolvedMediaButtonPreferences, result.sessionExtras, intersectedPlayerCommands); ImmutableMap.Builder commandButtonsForMediaItems = new ImmutableMap.Builder<>(); for (int i = 0; i < result.commandButtonsForMediaItems.size(); i++) { @@ -2842,8 +2851,10 @@ import org.checkerframework.checker.nullness.qual.NonNull; !Util.areEqual(intersectedPlayerCommands, prevIntersectedPlayerCommands); } boolean mediaButtonPreferencesChanged = false; + boolean customLayoutChanged = false; if (sessionCommandsChanged || intersectedPlayerCommandsChanged) { ImmutableList oldMediaButtonPreferences = resolvedMediaButtonPreferences; + ImmutableList oldCustomLayout = resolvedCustomLayout; resolvedMediaButtonPreferences = resolveMediaButtonPreferences( mediaButtonPreferencesOriginal, @@ -2851,8 +2862,12 @@ import org.checkerframework.checker.nullness.qual.NonNull; sessionCommands, intersectedPlayerCommands, sessionExtras); + resolvedCustomLayout = + resolveCustomLayout( + resolvedMediaButtonPreferences, sessionExtras, intersectedPlayerCommands); mediaButtonPreferencesChanged = !resolvedMediaButtonPreferences.equals(oldMediaButtonPreferences); + customLayoutChanged = !resolvedCustomLayout.equals(oldCustomLayout); } if (intersectedPlayerCommandsChanged) { listeners.sendEvent( @@ -2865,14 +2880,17 @@ import org.checkerframework.checker.nullness.qual.NonNull; listener -> listener.onAvailableSessionCommandsChanged(getInstance(), sessionCommands)); } + if (customLayoutChanged) { + getInstance() + .notifyControllerListener( + listener -> listener.onCustomLayoutChanged(getInstance(), resolvedCustomLayout)); + } if (mediaButtonPreferencesChanged) { getInstance() .notifyControllerListener( - listener -> { - listener.onCustomLayoutChanged(getInstance(), resolvedMediaButtonPreferences); - listener.onMediaButtonPreferencesChanged( - getInstance(), resolvedMediaButtonPreferences); - }); + listener -> + listener.onMediaButtonPreferencesChanged( + getInstance(), resolvedMediaButtonPreferences)); } } @@ -2891,8 +2909,10 @@ import org.checkerframework.checker.nullness.qual.NonNull; boolean intersectedPlayerCommandsChanged = !Util.areEqual(intersectedPlayerCommands, prevIntersectedPlayerCommands); boolean mediaButtonPreferencesChanged = false; + boolean customLayoutChanged = false; if (intersectedPlayerCommandsChanged) { ImmutableList oldMediaButtonPreferences = resolvedMediaButtonPreferences; + ImmutableList oldCustomLayout = resolvedCustomLayout; resolvedMediaButtonPreferences = resolveMediaButtonPreferences( mediaButtonPreferencesOriginal, @@ -2900,20 +2920,27 @@ import org.checkerframework.checker.nullness.qual.NonNull; sessionCommands, intersectedPlayerCommands, sessionExtras); + resolvedCustomLayout = + resolveCustomLayout( + resolvedMediaButtonPreferences, sessionExtras, intersectedPlayerCommands); mediaButtonPreferencesChanged = !resolvedMediaButtonPreferences.equals(oldMediaButtonPreferences); + customLayoutChanged = !resolvedCustomLayout.equals(oldCustomLayout); listeners.sendEvent( /* eventFlag= */ Player.EVENT_AVAILABLE_COMMANDS_CHANGED, listener -> listener.onAvailableCommandsChanged(intersectedPlayerCommands)); } + if (customLayoutChanged) { + getInstance() + .notifyControllerListener( + listener -> listener.onCustomLayoutChanged(getInstance(), resolvedCustomLayout)); + } if (mediaButtonPreferencesChanged) { getInstance() .notifyControllerListener( - listener -> { - listener.onCustomLayoutChanged(getInstance(), resolvedMediaButtonPreferences); - listener.onMediaButtonPreferencesChanged( - getInstance(), resolvedMediaButtonPreferences); - }); + listener -> + listener.onMediaButtonPreferencesChanged( + getInstance(), resolvedMediaButtonPreferences)); } } @@ -2922,6 +2949,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; return; } ImmutableList oldMediaButtonPreferences = resolvedMediaButtonPreferences; + ImmutableList oldCustomLayout = resolvedCustomLayout; customLayoutOriginal = ImmutableList.copyOf(layout); resolvedMediaButtonPreferences = resolveMediaButtonPreferences( @@ -2930,17 +2958,23 @@ import org.checkerframework.checker.nullness.qual.NonNull; sessionCommands, intersectedPlayerCommands, sessionExtras); + resolvedCustomLayout = + resolveCustomLayout( + resolvedMediaButtonPreferences, sessionExtras, intersectedPlayerCommands); boolean mediaButtonPreferencesChanged = - !Objects.equals(resolvedMediaButtonPreferences, oldMediaButtonPreferences); + !resolvedMediaButtonPreferences.equals(oldMediaButtonPreferences); + boolean customLayoutChanged = !resolvedCustomLayout.equals(oldCustomLayout); getInstance() .notifyControllerListener( listener -> { ListenableFuture future = checkNotNull( - listener.onSetCustomLayout(getInstance(), resolvedMediaButtonPreferences), + listener.onSetCustomLayout(getInstance(), resolvedCustomLayout), "MediaController.Listener#onSetCustomLayout() must not return null"); + if (customLayoutChanged) { + listener.onCustomLayoutChanged(getInstance(), resolvedCustomLayout); + } if (mediaButtonPreferencesChanged) { - listener.onCustomLayoutChanged(getInstance(), resolvedMediaButtonPreferences); listener.onMediaButtonPreferencesChanged( getInstance(), resolvedMediaButtonPreferences); } @@ -2953,6 +2987,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; return; } ImmutableList oldMediaButtonPreferences = resolvedMediaButtonPreferences; + ImmutableList oldCustomLayout = resolvedCustomLayout; mediaButtonPreferencesOriginal = ImmutableList.copyOf(mediaButtonPreferences); resolvedMediaButtonPreferences = resolveMediaButtonPreferences( @@ -2961,17 +2996,23 @@ import org.checkerframework.checker.nullness.qual.NonNull; sessionCommands, intersectedPlayerCommands, sessionExtras); + resolvedCustomLayout = + resolveCustomLayout( + resolvedMediaButtonPreferences, sessionExtras, intersectedPlayerCommands); boolean mediaButtonPreferencesChanged = - !Objects.equals(resolvedMediaButtonPreferences, oldMediaButtonPreferences); + !resolvedMediaButtonPreferences.equals(oldMediaButtonPreferences); + boolean customLayoutChanged = !resolvedCustomLayout.equals(oldCustomLayout); getInstance() .notifyControllerListener( listener -> { ListenableFuture future = checkNotNull( - listener.onSetCustomLayout(getInstance(), resolvedMediaButtonPreferences), + listener.onSetCustomLayout(getInstance(), resolvedCustomLayout), "MediaController.Listener#onSetCustomLayout() must not return null"); + if (customLayoutChanged) { + listener.onCustomLayoutChanged(getInstance(), resolvedCustomLayout); + } if (mediaButtonPreferencesChanged) { - listener.onCustomLayoutChanged(getInstance(), resolvedMediaButtonPreferences); listener.onMediaButtonPreferencesChanged( getInstance(), resolvedMediaButtonPreferences); } @@ -2984,6 +3025,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; return; } ImmutableList oldMediaButtonPreferences = resolvedMediaButtonPreferences; + ImmutableList oldCustomLayout = resolvedCustomLayout; sessionExtras = extras; resolvedMediaButtonPreferences = resolveMediaButtonPreferences( @@ -2992,14 +3034,20 @@ import org.checkerframework.checker.nullness.qual.NonNull; sessionCommands, intersectedPlayerCommands, sessionExtras); + resolvedCustomLayout = + resolveCustomLayout( + resolvedMediaButtonPreferences, sessionExtras, intersectedPlayerCommands); boolean mediaButtonPreferencesChanged = - !Objects.equals(resolvedMediaButtonPreferences, oldMediaButtonPreferences); + !resolvedMediaButtonPreferences.equals(oldMediaButtonPreferences); + boolean customLayoutChanged = !resolvedCustomLayout.equals(oldCustomLayout); getInstance() .notifyControllerListener( listener -> { listener.onExtrasChanged(getInstance(), extras); + if (customLayoutChanged) { + listener.onCustomLayoutChanged(getInstance(), resolvedCustomLayout); + } if (mediaButtonPreferencesChanged) { - listener.onCustomLayoutChanged(getInstance(), resolvedMediaButtonPreferences); listener.onMediaButtonPreferencesChanged( getInstance(), resolvedMediaButtonPreferences); } @@ -3365,6 +3413,22 @@ import org.checkerframework.checker.nullness.qual.NonNull; resolvedButtons, sessionCommands, playerCommands); } + private static ImmutableList resolveCustomLayout( + List mediaButtonPreferences, + Bundle sessionExtras, + Player.Commands availableCommands) { + boolean backSlotAllowed = + !sessionExtras.getBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV) + && !availableCommands.containsAny( + Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, Player.COMMAND_SEEK_TO_PREVIOUS); + boolean forwardSlotAllowed = + !sessionExtras.getBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT) + && !availableCommands.containsAny( + Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, Player.COMMAND_SEEK_TO_NEXT); + return CommandButton.getCustomLayoutFromMediaButtonPreferences( + mediaButtonPreferences, backSlotAllowed, forwardSlotAllowed); + } + private static Commands createIntersectedCommandsEnsuringCommandReleaseAvailable( Commands commandFromSession, Commands commandsFromPlayer) { Commands intersectedCommands = MediaUtils.intersect(commandFromSession, commandsFromPlayer); 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 746ff3f3c8..c32362c439 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java @@ -437,6 +437,11 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; return controllerInfo.mediaButtonPreferences; } + @Override + public ImmutableList getCustomLayout() { + return controllerInfo.mediaButtonPreferences; + } + @Override public Bundle getSessionExtras() { return controllerInfo.sessionExtras; 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 5e962d3f72..b39524fbef 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 @@ -216,8 +216,8 @@ public class MediaControllerTest { assertThat(threadTestRule.getHandler().postAndSync(controller::getCustomLayout)) .containsExactly( - withBackForwardOverflowSlot(button1.copyWithIsEnabled(true)), - withForwardOverflowSlot(button2.copyWithIsEnabled(false)), + withBackSlot(button1.copyWithIsEnabled(true)), + withForwardSlot(button2.copyWithIsEnabled(false)), withOverflowSlot(button3.copyWithIsEnabled(false)), withOverflowSlot(button4.copyWithIsEnabled(true)), withOverflowSlot(button5.copyWithIsEnabled(false))) @@ -301,13 +301,13 @@ public class MediaControllerTest { assertThat(initialCustomLayoutFromGetter) .containsExactly( - withBackForwardOverflowSlot(button1.copyWithIsEnabled(true)), - withForwardOverflowSlot(button3.copyWithIsEnabled(false))) + withBackSlot(button1.copyWithIsEnabled(true)), + withForwardSlot(button3.copyWithIsEnabled(false))) .inOrder(); ImmutableList expectedNewButtons = ImmutableList.of( - withBackForwardOverflowSlot(button1.copyWithIsEnabled(true)), - withForwardOverflowSlot(button2.copyWithIsEnabled(false)), + withBackSlot(button1.copyWithIsEnabled(true)), + withForwardSlot(button2.copyWithIsEnabled(false)), withOverflowSlot(button4.copyWithIsEnabled(false)), withOverflowSlot(button5.copyWithIsEnabled(true)), withOverflowSlot(button6.copyWithIsEnabled(false))); @@ -378,37 +378,37 @@ public class MediaControllerTest { assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(initialCustomLayout) .containsExactly( - withBackForwardOverflowSlot(button1.copyWithIsEnabled(true)), - withForwardOverflowSlot(button2.copyWithIsEnabled(false)), + withBackSlot(button1.copyWithIsEnabled(true)), + withForwardSlot(button2.copyWithIsEnabled(false)), withOverflowSlot(button3.copyWithIsEnabled(true)), withOverflowSlot(button4.copyWithIsEnabled(false))); assertThat(reportedCustomLayoutChanged).hasSize(2); assertThat(reportedCustomLayoutChanged.get(0)) .containsExactly( - withBackForwardOverflowSlot(button1.copyWithIsEnabled(false)), - withForwardOverflowSlot(button2.copyWithIsEnabled(false)), + withBackSlot(button1.copyWithIsEnabled(false)), + withForwardSlot(button2.copyWithIsEnabled(false)), withOverflowSlot(button3.copyWithIsEnabled(false)), withOverflowSlot(button4.copyWithIsEnabled(false))) .inOrder(); assertThat(reportedCustomLayoutChanged.get(1)) .containsExactly( - withBackForwardOverflowSlot(button1.copyWithIsEnabled(false)), - withForwardOverflowSlot(button2.copyWithIsEnabled(false)), + withBackSlot(button1.copyWithIsEnabled(false)), + withForwardSlot(button2.copyWithIsEnabled(false)), withOverflowSlot(button3.copyWithIsEnabled(false)), withOverflowSlot(button4.copyWithIsEnabled(true))) .inOrder(); assertThat(getterCustomLayoutChanged).hasSize(2); assertThat(getterCustomLayoutChanged.get(0)) .containsExactly( - withBackForwardOverflowSlot(button1.copyWithIsEnabled(false)), - withForwardOverflowSlot(button2.copyWithIsEnabled(false)), + withBackSlot(button1.copyWithIsEnabled(false)), + withForwardSlot(button2.copyWithIsEnabled(false)), withOverflowSlot(button3.copyWithIsEnabled(false)), withOverflowSlot(button4.copyWithIsEnabled(false))) .inOrder(); assertThat(getterCustomLayoutChanged.get(1)) .containsExactly( - withBackForwardOverflowSlot(button1.copyWithIsEnabled(false)), - withForwardOverflowSlot(button2.copyWithIsEnabled(false)), + withBackSlot(button1.copyWithIsEnabled(false)), + withForwardSlot(button2.copyWithIsEnabled(false)), withOverflowSlot(button3.copyWithIsEnabled(false)), withOverflowSlot(button4.copyWithIsEnabled(true))) .inOrder(); @@ -453,18 +453,17 @@ public class MediaControllerTest { new Player.Commands.Builder().add(Player.COMMAND_PLAY_PAUSE).build()); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - assertThat(initialCustomLayout) - .containsExactly(withBackForwardOverflowSlot(button.copyWithIsEnabled(true))); + assertThat(initialCustomLayout).containsExactly(withBackSlot(button.copyWithIsEnabled(true))); assertThat(reportedCustomLayouts).hasSize(2); assertThat(reportedCustomLayouts.get(0)) - .containsExactly(withBackForwardOverflowSlot(button.copyWithIsEnabled(false))); + .containsExactly(withBackSlot(button.copyWithIsEnabled(false))); assertThat(reportedCustomLayouts.get(1)) - .containsExactly(withBackForwardOverflowSlot(button.copyWithIsEnabled(true))); + .containsExactly(withBackSlot(button.copyWithIsEnabled(true))); assertThat(getterCustomLayouts).hasSize(2); assertThat(getterCustomLayouts.get(0)) - .containsExactly(withBackForwardOverflowSlot(button.copyWithIsEnabled(false))); + .containsExactly(withBackSlot(button.copyWithIsEnabled(false))); assertThat(getterCustomLayouts.get(1)) - .containsExactly(withBackForwardOverflowSlot(button.copyWithIsEnabled(true))); + .containsExactly(withBackSlot(button.copyWithIsEnabled(true))); session.cleanUp(); } @@ -534,14 +533,10 @@ public class MediaControllerTest { session.setCustomLayout(ImmutableList.of(button1, button2)); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); - CommandButton button1EnabledBackSlot = - withBackForwardOverflowSlot(button1.copyWithIsEnabled(true)); - CommandButton button2DisabledForwardSlot = - withForwardOverflowSlot(button2.copyWithIsEnabled(false)); - CommandButton button3DisabledBackSlot = - withBackForwardOverflowSlot(button3.copyWithIsEnabled(false)); - CommandButton button4DisabledForwardSlot = - withForwardOverflowSlot(button4.copyWithIsEnabled(false)); + CommandButton button1EnabledBackSlot = withBackSlot(button1.copyWithIsEnabled(true)); + CommandButton button2DisabledForwardSlot = withForwardSlot(button2.copyWithIsEnabled(false)); + CommandButton button3DisabledBackSlot = withBackSlot(button3.copyWithIsEnabled(false)); + CommandButton button4DisabledForwardSlot = withForwardSlot(button4.copyWithIsEnabled(false)); assertThat(initialCustomLayout) .containsExactly(button1EnabledBackSlot, button2DisabledForwardSlot) .inOrder(); @@ -626,17 +621,11 @@ public class MediaControllerTest { assertThat(reportedCustomLayouts) .containsExactly( ImmutableList.of( - withBackForwardOverflowSlot(button1), - withForwardOverflowSlot(button2), - withOverflowSlot(button3)), + withBackSlot(button1), withForwardSlot(button2), withOverflowSlot(button3)), ImmutableList.of( - withBackOverflowSlot(button1), - withOverflowSlot(button2), - withOverflowSlot(button3)), + withBackSlot(button1), withOverflowSlot(button2), withOverflowSlot(button3)), ImmutableList.of( - withForwardOverflowSlot(button1), - withOverflowSlot(button2), - withOverflowSlot(button3)), + withForwardSlot(button1), withOverflowSlot(button2), withOverflowSlot(button3)), ImmutableList.of( withOverflowSlot(button1), withOverflowSlot(button2), withOverflowSlot(button3))); session.cleanUp(); @@ -700,6 +689,361 @@ public class MediaControllerTest { assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(reportedCustomLayouts) + .containsExactly( + ImmutableList.of( + withBackSlot(button1), withForwardSlot(button2), withOverflowSlot(button3)), + ImmutableList.of( + withBackSlot(button1), withOverflowSlot(button2), withOverflowSlot(button3)), + ImmutableList.of( + withForwardSlot(button1), withOverflowSlot(button2), withOverflowSlot(button3)), + ImmutableList.of( + withOverflowSlot(button1), withOverflowSlot(button2), withOverflowSlot(button3))); + session.cleanUp(); + } + + @Test + public void getMediaButtonPreferences_customLayoutBuiltWithSession_includedOnConnect() + throws Exception { + RemoteMediaSession session = + createRemoteMediaSession(TEST_GET_CUSTOM_LAYOUT, /* tokenExtras= */ null); + CommandButton button1 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button1") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command1", Bundle.EMPTY)) + .build(); + CommandButton button2 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button2") + .setEnabled(false) + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command2", Bundle.EMPTY)) + .build(); + CommandButton button3 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button3") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command3", Bundle.EMPTY)) + .build(); + CommandButton button4 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button4") + .setIconResId(R.drawable.media3_notification_small_icon) + .setPlayerCommand(Player.COMMAND_PLAY_PAUSE) + .build(); + CommandButton button5 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button5") + .setIconResId(R.drawable.media3_notification_small_icon) + .setPlayerCommand(Player.COMMAND_GET_TRACKS) + .build(); + setupCustomLayout(session, ImmutableList.of(button1, button2, button3, button4, button5)); + MediaController controller = controllerTestRule.createController(session.getToken()); + + assertThat(threadTestRule.getHandler().postAndSync(controller::getMediaButtonPreferences)) + .containsExactly( + withBackForwardOverflowSlot(button1.copyWithIsEnabled(true)), + withForwardOverflowSlot(button2.copyWithIsEnabled(false)), + withOverflowSlot(button3.copyWithIsEnabled(false)), + withOverflowSlot(button4.copyWithIsEnabled(true)), + withOverflowSlot(button5.copyWithIsEnabled(false))) + .inOrder(); + + session.cleanUp(); + } + + @Test + public void getMediaButtonPreferences_sessionSetCustomLayout_mediaButtonPreferencesChanged() + throws Exception { + RemoteMediaSession session = + createRemoteMediaSession(TEST_GET_CUSTOM_LAYOUT, /* tokenExtras= */ null); + CommandButton button1 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button1") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command1", Bundle.EMPTY)) + .build(); + CommandButton button2 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button2") + .setEnabled(false) + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command2", Bundle.EMPTY)) + .build(); + CommandButton button3 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button3") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command3", Bundle.EMPTY)) + .build(); + CommandButton button4 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button4") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command4", Bundle.EMPTY)) + .build(); + CommandButton button5 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button5") + .setIconResId(R.drawable.media3_notification_small_icon) + .setPlayerCommand(Player.COMMAND_PLAY_PAUSE) + .build(); + CommandButton button6 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button6") + .setIconResId(R.drawable.media3_notification_small_icon) + .setPlayerCommand(Player.COMMAND_GET_TRACKS) + .build(); + setupCustomLayout(session, ImmutableList.of(button1, button3)); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference> reportedMediaButtonPreferences = new AtomicReference<>(); + MediaController controller = + controllerTestRule.createController( + session.getToken(), + Bundle.EMPTY, + new MediaController.Listener() { + @Override + public void onMediaButtonPreferencesChanged( + MediaController controller1, List mediaButtonPreferences) { + reportedMediaButtonPreferences.set(mediaButtonPreferences); + latch.countDown(); + } + }); + ImmutableList initialMediaButtonPreferencesFromGetter = + threadTestRule.getHandler().postAndSync(controller::getMediaButtonPreferences); + session.setCustomLayout(ImmutableList.of(button1, button2, button4, button5, button6)); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + + ImmutableList newMediaButtonPreferencesFromGetter = + threadTestRule.getHandler().postAndSync(controller::getMediaButtonPreferences); + + assertThat(initialMediaButtonPreferencesFromGetter) + .containsExactly( + withBackForwardOverflowSlot(button1.copyWithIsEnabled(true)), + withForwardOverflowSlot(button3.copyWithIsEnabled(false))) + .inOrder(); + ImmutableList expectedNewButtons = + ImmutableList.of( + withBackForwardOverflowSlot(button1.copyWithIsEnabled(true)), + withForwardOverflowSlot(button2.copyWithIsEnabled(false)), + withOverflowSlot(button4.copyWithIsEnabled(false)), + withOverflowSlot(button5.copyWithIsEnabled(true)), + withOverflowSlot(button6.copyWithIsEnabled(false))); + assertThat(newMediaButtonPreferencesFromGetter) + .containsExactlyElementsIn(expectedNewButtons) + .inOrder(); + assertThat(reportedMediaButtonPreferences.get()) + .containsExactlyElementsIn(expectedNewButtons) + .inOrder(); + session.cleanUp(); + } + + @Test + public void + getMediaButtonPreferences_setAvailableCommandsOnSessionAfterSetCustomLayout_reportsMediaButtonPreferencesChanged() + throws Exception { + RemoteMediaSession session = createRemoteMediaSession(TEST_GET_CUSTOM_LAYOUT, null); + CommandButton button1 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button1") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command1", Bundle.EMPTY)) + .build(); + CommandButton button2 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button2") + .setEnabled(false) + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command2", Bundle.EMPTY)) + .build(); + CommandButton button3 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button3") + .setIconResId(R.drawable.media3_notification_small_icon) + .setPlayerCommand(Player.COMMAND_PLAY_PAUSE) + .build(); + CommandButton button4 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button4") + .setIconResId(R.drawable.media3_notification_small_icon) + .setPlayerCommand(Player.COMMAND_GET_TRACKS) + .build(); + setupCustomLayout(session, ImmutableList.of(button1, button2, button3, button4)); + CountDownLatch latch = new CountDownLatch(2); + List> reportedMediaButtonPreferences = new ArrayList<>(); + List> getterMediaButtonPreferences = new ArrayList<>(); + MediaController.Listener listener = + new MediaController.Listener() { + @Override + public void onMediaButtonPreferencesChanged( + MediaController controller, List mediaButtonPreferences) { + reportedMediaButtonPreferences.add(mediaButtonPreferences); + getterMediaButtonPreferences.add(controller.getMediaButtonPreferences()); + latch.countDown(); + } + }; + MediaController controller = + controllerTestRule.createController( + session.getToken(), /* connectionHints= */ Bundle.EMPTY, listener); + ImmutableList initialMediaButtonPreferences = + threadTestRule.getHandler().postAndSync(controller::getMediaButtonPreferences); + + // Remove commands in custom layout from available commands. + session.setAvailableCommands(SessionCommands.EMPTY, Player.Commands.EMPTY); + // Add one session and player command back. + session.setAvailableCommands( + new SessionCommands.Builder().add(button2.sessionCommand).build(), + new Player.Commands.Builder().add(Player.COMMAND_GET_TRACKS).build()); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(initialMediaButtonPreferences) + .containsExactly( + withBackForwardOverflowSlot(button1.copyWithIsEnabled(true)), + withForwardOverflowSlot(button2.copyWithIsEnabled(false)), + withOverflowSlot(button3.copyWithIsEnabled(true)), + withOverflowSlot(button4.copyWithIsEnabled(false))); + assertThat(reportedMediaButtonPreferences).hasSize(2); + assertThat(reportedMediaButtonPreferences.get(0)) + .containsExactly( + withBackForwardOverflowSlot(button1.copyWithIsEnabled(false)), + withForwardOverflowSlot(button2.copyWithIsEnabled(false)), + withOverflowSlot(button3.copyWithIsEnabled(false)), + withOverflowSlot(button4.copyWithIsEnabled(false))) + .inOrder(); + assertThat(reportedMediaButtonPreferences.get(1)) + .containsExactly( + withBackForwardOverflowSlot(button1.copyWithIsEnabled(false)), + withForwardOverflowSlot(button2.copyWithIsEnabled(false)), + withOverflowSlot(button3.copyWithIsEnabled(false)), + withOverflowSlot(button4.copyWithIsEnabled(true))) + .inOrder(); + assertThat(getterMediaButtonPreferences).hasSize(2); + assertThat(getterMediaButtonPreferences.get(0)) + .containsExactly( + withBackForwardOverflowSlot(button1.copyWithIsEnabled(false)), + withForwardOverflowSlot(button2.copyWithIsEnabled(false)), + withOverflowSlot(button3.copyWithIsEnabled(false)), + withOverflowSlot(button4.copyWithIsEnabled(false))) + .inOrder(); + assertThat(getterMediaButtonPreferences.get(1)) + .containsExactly( + withBackForwardOverflowSlot(button1.copyWithIsEnabled(false)), + withForwardOverflowSlot(button2.copyWithIsEnabled(false)), + withOverflowSlot(button3.copyWithIsEnabled(false)), + withOverflowSlot(button4.copyWithIsEnabled(true))) + .inOrder(); + session.cleanUp(); + } + + @Test + public void + getMediaButtonPreferences_setAvailableCommandsOnPlayerAfterSetCustomLayout_reportsMediaButtonPreferencesChanged() + throws Exception { + RemoteMediaSession session = createRemoteMediaSession(TEST_GET_CUSTOM_LAYOUT, null); + CommandButton button = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button") + .setIconResId(R.drawable.media3_notification_small_icon) + .setPlayerCommand(Player.COMMAND_PLAY_PAUSE) + .build(); + setupCustomLayout(session, ImmutableList.of(button)); + CountDownLatch latch = new CountDownLatch(2); + List> reportedMediaButtonPreferences = new ArrayList<>(); + List> getterMediaButtonPreferences = new ArrayList<>(); + MediaController.Listener listener = + new MediaController.Listener() { + @Override + public void onMediaButtonPreferencesChanged( + MediaController controller, List mediaButtonPreferences) { + reportedMediaButtonPreferences.add(mediaButtonPreferences); + getterMediaButtonPreferences.add(controller.getMediaButtonPreferences()); + latch.countDown(); + } + }; + MediaController controller = + controllerTestRule.createController( + session.getToken(), /* connectionHints= */ Bundle.EMPTY, listener); + ImmutableList initialMediaButtonPreferences = + threadTestRule.getHandler().postAndSync(controller::getMediaButtonPreferences); + + // Disable player command and then add it back. + session.getMockPlayer().notifyAvailableCommandsChanged(Player.Commands.EMPTY); + session + .getMockPlayer() + .notifyAvailableCommandsChanged( + new Player.Commands.Builder().add(Player.COMMAND_PLAY_PAUSE).build()); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(initialMediaButtonPreferences) + .containsExactly(withBackForwardOverflowSlot(button.copyWithIsEnabled(true))); + assertThat(reportedMediaButtonPreferences).hasSize(2); + assertThat(reportedMediaButtonPreferences.get(0)) + .containsExactly(withBackForwardOverflowSlot(button.copyWithIsEnabled(false))); + assertThat(reportedMediaButtonPreferences.get(1)) + .containsExactly(withBackForwardOverflowSlot(button.copyWithIsEnabled(true))); + assertThat(getterMediaButtonPreferences).hasSize(2); + assertThat(getterMediaButtonPreferences.get(0)) + .containsExactly(withBackForwardOverflowSlot(button.copyWithIsEnabled(false))); + assertThat(getterMediaButtonPreferences.get(1)) + .containsExactly(withBackForwardOverflowSlot(button.copyWithIsEnabled(true))); + session.cleanUp(); + } + + @Test + public void + getMediaButtonPreferences_setAvailablePrevNextCommandAfterSetCustomLayout_reportsMediaButtonPreferencesChanged() + 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> reportedMediaButtonPreferences = new ArrayList<>(); + MediaController.Listener listener = + new MediaController.Listener() { + @Override + public void onMediaButtonPreferencesChanged( + MediaController controller, List mediaButtonPreferences) { + reportedMediaButtonPreferences.add(mediaButtonPreferences); + 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(reportedMediaButtonPreferences) .containsExactly( ImmutableList.of( withBackForwardOverflowSlot(button1), @@ -718,6 +1062,618 @@ public class MediaControllerTest { session.cleanUp(); } + @Test + public void + getMediaButtonPreferences_setSessionExtrasForPrevNextReservationsAfterSetCustomLayout_reportsMediaButtonPreferencesChanged() + 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> reportedMediaButtonPreferences = new ArrayList<>(); + MediaController.Listener listener = + new MediaController.Listener() { + @Override + public void onMediaButtonPreferencesChanged( + MediaController controller, List mediaButtonPreferences) { + reportedMediaButtonPreferences.add(mediaButtonPreferences); + 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(reportedMediaButtonPreferences) + .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_mediaButtonPreferencesBuiltWithSession_includedOnConnect() + throws Exception { + RemoteMediaSession session = + createRemoteMediaSession(TEST_GET_CUSTOM_LAYOUT, /* tokenExtras= */ null); + CommandButton button1 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button1") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command1", Bundle.EMPTY)) + .build(); + CommandButton button2 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button2") + .setEnabled(false) + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command2", Bundle.EMPTY)) + .build(); + CommandButton button3 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button3") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command3", Bundle.EMPTY)) + .build(); + CommandButton button4 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button4") + .setIconResId(R.drawable.media3_notification_small_icon) + .setPlayerCommand(Player.COMMAND_PLAY_PAUSE) + .setSlots(CommandButton.SLOT_OVERFLOW) + .build(); + CommandButton button5 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button5") + .setIconResId(R.drawable.media3_notification_small_icon) + .setPlayerCommand(Player.COMMAND_GET_TRACKS) + .build(); + setupMediaButtonPreferences( + session, ImmutableList.of(button1, button2, button3, button4, button5)); + MediaController controller = controllerTestRule.createController(session.getToken()); + + assertThat(threadTestRule.getHandler().postAndSync(controller::getCustomLayout)) + .containsExactly( + withOverflowSlot(button1.copyWithIsEnabled(true)), + withOverflowSlot(button2.copyWithIsEnabled(false)), + withOverflowSlot(button3.copyWithIsEnabled(false)), + withOverflowSlot(button4.copyWithIsEnabled(true)), + withOverflowSlot(button5.copyWithIsEnabled(false))) + .inOrder(); + + session.cleanUp(); + } + + @Test + public void getCustomLayout_sessionSetMediaButtonPreferences_customLayoutChanged() + throws Exception { + RemoteMediaSession session = + createRemoteMediaSession(TEST_GET_CUSTOM_LAYOUT, /* tokenExtras= */ null); + CommandButton button1 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button1") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command1", Bundle.EMPTY)) + .build(); + CommandButton button2 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button2") + .setEnabled(false) + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command2", Bundle.EMPTY)) + .build(); + CommandButton button3 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button3") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command3", Bundle.EMPTY)) + .build(); + CommandButton button4 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button4") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command4", Bundle.EMPTY)) + .build(); + CommandButton button5 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button5") + .setIconResId(R.drawable.media3_notification_small_icon) + .setPlayerCommand(Player.COMMAND_PLAY_PAUSE) + .setSlots(CommandButton.SLOT_OVERFLOW) + .build(); + CommandButton button6 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button6") + .setIconResId(R.drawable.media3_notification_small_icon) + .setPlayerCommand(Player.COMMAND_GET_TRACKS) + .build(); + setupMediaButtonPreferences(session, ImmutableList.of(button1, button3)); + CountDownLatch latch = new CountDownLatch(2); + AtomicReference> reportedCustomLayout = new AtomicReference<>(); + AtomicReference> reportedCustomLayoutChanged = new AtomicReference<>(); + MediaController controller = + controllerTestRule.createController( + session.getToken(), + Bundle.EMPTY, + new MediaController.Listener() { + @Override + public ListenableFuture onSetCustomLayout( + MediaController controller1, List layout) { + latch.countDown(); + reportedCustomLayout.set(layout); + return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS)); + } + + @Override + public void onCustomLayoutChanged( + MediaController controller1, List layout) { + reportedCustomLayoutChanged.set(layout); + latch.countDown(); + } + }); + ImmutableList initialCustomLayoutFromGetter = + threadTestRule.getHandler().postAndSync(controller::getCustomLayout); + session.setMediaButtonPreferences( + ImmutableList.of(button1, button2, button4, button5, button6)); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + + ImmutableList newCustomLayoutFromGetter = + threadTestRule.getHandler().postAndSync(controller::getCustomLayout); + + assertThat(initialCustomLayoutFromGetter) + .containsExactly( + withOverflowSlot(button1.copyWithIsEnabled(true)), + withOverflowSlot(button3.copyWithIsEnabled(false))) + .inOrder(); + ImmutableList expectedNewButtons = + ImmutableList.of( + withOverflowSlot(button1.copyWithIsEnabled(true)), + withOverflowSlot(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()) + .containsExactlyElementsIn(expectedNewButtons) + .inOrder(); + session.cleanUp(); + } + + @Test + public void + getCustomLayout_setAvailableCommandsOnSessionAfterSetMediaButtonPreferences_reportsCustomLayoutChanged() + throws Exception { + RemoteMediaSession session = createRemoteMediaSession(TEST_GET_CUSTOM_LAYOUT, null); + CommandButton button1 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button1") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command1", Bundle.EMPTY)) + .build(); + CommandButton button2 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button2") + .setEnabled(false) + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command2", Bundle.EMPTY)) + .build(); + CommandButton button3 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button3") + .setIconResId(R.drawable.media3_notification_small_icon) + .setPlayerCommand(Player.COMMAND_PLAY_PAUSE) + .setSlots(CommandButton.SLOT_OVERFLOW) + .build(); + CommandButton button4 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button4") + .setIconResId(R.drawable.media3_notification_small_icon) + .setPlayerCommand(Player.COMMAND_GET_TRACKS) + .build(); + setupMediaButtonPreferences(session, ImmutableList.of(button1, button2, button3, button4)); + CountDownLatch latch = new CountDownLatch(2); + List> reportedCustomLayoutChanged = new ArrayList<>(); + List> getterCustomLayoutChanged = new ArrayList<>(); + MediaController.Listener listener = + new MediaController.Listener() { + @Override + public void onCustomLayoutChanged( + MediaController controller, List layout) { + reportedCustomLayoutChanged.add(layout); + getterCustomLayoutChanged.add(controller.getCustomLayout()); + latch.countDown(); + } + }; + MediaController controller = + controllerTestRule.createController( + session.getToken(), /* connectionHints= */ Bundle.EMPTY, listener); + ImmutableList initialCustomLayout = + threadTestRule.getHandler().postAndSync(controller::getCustomLayout); + + // Remove commands in custom layout from available commands. + session.setAvailableCommands(SessionCommands.EMPTY, Player.Commands.EMPTY); + // Add one session and player command back. + session.setAvailableCommands( + new SessionCommands.Builder().add(button2.sessionCommand).build(), + new Player.Commands.Builder().add(Player.COMMAND_GET_TRACKS).build()); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(initialCustomLayout) + .containsExactly( + withOverflowSlot(button1.copyWithIsEnabled(true)), + withOverflowSlot(button2.copyWithIsEnabled(false)), + withOverflowSlot(button3.copyWithIsEnabled(true)), + withOverflowSlot(button4.copyWithIsEnabled(false))); + assertThat(reportedCustomLayoutChanged).hasSize(2); + assertThat(reportedCustomLayoutChanged.get(0)) + .containsExactly( + withOverflowSlot(button1.copyWithIsEnabled(false)), + withOverflowSlot(button2.copyWithIsEnabled(false)), + withOverflowSlot(button3.copyWithIsEnabled(false)), + withOverflowSlot(button4.copyWithIsEnabled(false))) + .inOrder(); + assertThat(reportedCustomLayoutChanged.get(1)) + .containsExactly( + withOverflowSlot(button1.copyWithIsEnabled(false)), + withOverflowSlot(button2.copyWithIsEnabled(false)), + withOverflowSlot(button3.copyWithIsEnabled(false)), + withOverflowSlot(button4.copyWithIsEnabled(true))) + .inOrder(); + assertThat(getterCustomLayoutChanged).hasSize(2); + assertThat(getterCustomLayoutChanged.get(0)) + .containsExactly( + withOverflowSlot(button1.copyWithIsEnabled(false)), + withOverflowSlot(button2.copyWithIsEnabled(false)), + withOverflowSlot(button3.copyWithIsEnabled(false)), + withOverflowSlot(button4.copyWithIsEnabled(false))) + .inOrder(); + assertThat(getterCustomLayoutChanged.get(1)) + .containsExactly( + withOverflowSlot(button1.copyWithIsEnabled(false)), + withOverflowSlot(button2.copyWithIsEnabled(false)), + withOverflowSlot(button3.copyWithIsEnabled(false)), + withOverflowSlot(button4.copyWithIsEnabled(true))) + .inOrder(); + session.cleanUp(); + } + + @Test + public void + getCustomLayout_setAvailableCommandsOnPlayerAfterSetMediaButtonPreferences_reportsCustomLayoutChanged() + throws Exception { + RemoteMediaSession session = createRemoteMediaSession(TEST_GET_CUSTOM_LAYOUT, null); + CommandButton button = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button") + .setIconResId(R.drawable.media3_notification_small_icon) + .setPlayerCommand(Player.COMMAND_PLAY_PAUSE) + .setSlots(CommandButton.SLOT_OVERFLOW) + .build(); + setupMediaButtonPreferences(session, ImmutableList.of(button)); + CountDownLatch latch = new CountDownLatch(2); + List> reportedCustomLayouts = new ArrayList<>(); + List> getterCustomLayouts = new ArrayList<>(); + MediaController.Listener listener = + new MediaController.Listener() { + @Override + public void onCustomLayoutChanged( + MediaController controller, List layout) { + reportedCustomLayouts.add(layout); + getterCustomLayouts.add(controller.getCustomLayout()); + latch.countDown(); + } + }; + MediaController controller = + controllerTestRule.createController( + session.getToken(), /* connectionHints= */ Bundle.EMPTY, listener); + ImmutableList initialCustomLayout = + threadTestRule.getHandler().postAndSync(controller::getCustomLayout); + + // Disable player command and then add it back. + session.getMockPlayer().notifyAvailableCommandsChanged(Player.Commands.EMPTY); + session + .getMockPlayer() + .notifyAvailableCommandsChanged( + new Player.Commands.Builder().add(Player.COMMAND_PLAY_PAUSE).build()); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(initialCustomLayout) + .containsExactly(withOverflowSlot(button.copyWithIsEnabled(true))); + assertThat(reportedCustomLayouts).hasSize(2); + assertThat(reportedCustomLayouts.get(0)) + .containsExactly(withOverflowSlot(button.copyWithIsEnabled(false))); + assertThat(reportedCustomLayouts.get(1)) + .containsExactly(withOverflowSlot(button.copyWithIsEnabled(true))); + assertThat(getterCustomLayouts).hasSize(2); + assertThat(getterCustomLayouts.get(0)) + .containsExactly(withOverflowSlot(button.copyWithIsEnabled(false))); + assertThat(getterCustomLayouts.get(1)) + .containsExactly(withOverflowSlot(button.copyWithIsEnabled(true))); + session.cleanUp(); + } + + @Test + public void + getCustomLayout_sessionSetMediaButtonPreferencesNoChange_listenerNotCalledWithEqualLayout() + throws Exception { + RemoteMediaSession session = + createRemoteMediaSession(TEST_GET_CUSTOM_LAYOUT, /* tokenExtras= */ null); + CommandButton button1 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button1") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command1", Bundle.EMPTY)) + .build(); + CommandButton button2 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button2") + .setEnabled(false) + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command2", Bundle.EMPTY)) + .build(); + CommandButton button3 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button3") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command3", Bundle.EMPTY)) + .build(); + CommandButton button4 = + new CommandButton.Builder(CommandButton.ICON_UNDEFINED) + .setDisplayName("button4") + .setIconResId(R.drawable.media3_notification_small_icon) + .setSessionCommand(new SessionCommand("command4", Bundle.EMPTY)) + .build(); + setupMediaButtonPreferences(session, ImmutableList.of(button1, button2)); + CountDownLatch latch = new CountDownLatch(5); + List> reportedCustomLayout = new ArrayList<>(); + List> getterCustomLayout = new ArrayList<>(); + List> reportedCustomLayoutChanged = new ArrayList<>(); + List> getterCustomLayoutChanged = new ArrayList<>(); + MediaController.Listener listener = + new MediaController.Listener() { + @Override + public ListenableFuture onSetCustomLayout( + MediaController controller, List layout) { + reportedCustomLayout.add(layout); + getterCustomLayout.add(controller.getCustomLayout()); + latch.countDown(); + return MediaController.Listener.super.onSetCustomLayout(controller, layout); + } + + @Override + public void onCustomLayoutChanged( + MediaController controller, List layout) { + reportedCustomLayoutChanged.add(layout); + getterCustomLayoutChanged.add(controller.getCustomLayout()); + latch.countDown(); + } + }; + MediaController controller = + controllerTestRule.createController(session.getToken(), Bundle.EMPTY, listener); + ImmutableList initialCustomLayout = + threadTestRule.getHandler().postAndSync(controller::getCustomLayout); + + // First call does not trigger onCustomLayoutChanged. + session.setMediaButtonPreferences(ImmutableList.of(button1, button2)); + session.setMediaButtonPreferences(ImmutableList.of(button3, button4)); + session.setMediaButtonPreferences(ImmutableList.of(button1, button2)); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + CommandButton button1Enabled = withOverflowSlot(button1.copyWithIsEnabled(true)); + CommandButton button2Disabled = withOverflowSlot(button2.copyWithIsEnabled(false)); + CommandButton button3Disabled = withOverflowSlot(button3.copyWithIsEnabled(false)); + CommandButton button4Disabled = withOverflowSlot(button4.copyWithIsEnabled(false)); + assertThat(initialCustomLayout).containsExactly(button1Enabled, button2Disabled).inOrder(); + assertThat(reportedCustomLayout) + .containsExactly( + ImmutableList.of(button1Enabled, button2Disabled), + ImmutableList.of(button3Disabled, button4Disabled), + ImmutableList.of(button1Enabled, button2Disabled)) + .inOrder(); + assertThat(getterCustomLayout) + .containsExactly( + ImmutableList.of(button1Enabled, button2Disabled), + ImmutableList.of(button3Disabled, button4Disabled), + ImmutableList.of(button1Enabled, button2Disabled)) + .inOrder(); + assertThat(reportedCustomLayoutChanged) + .containsExactly( + ImmutableList.of(button3Disabled, button4Disabled), + ImmutableList.of(button1Enabled, button2Disabled)) + .inOrder(); + assertThat(getterCustomLayoutChanged) + .containsExactly( + ImmutableList.of(button3Disabled, button4Disabled), + ImmutableList.of(button1Enabled, button2Disabled)) + .inOrder(); + session.cleanUp(); + } + + @Test + public void + getCustomLayout_setAvailablePrevNextCommandAfterSetMediaButtonPreferences_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)) + .setSlots(CommandButton.SLOT_FORWARD) + .build(); + CommandButton button2 = + new CommandButton.Builder(CommandButton.ICON_REWIND) + .setDisplayName("button2") + .setSessionCommand(new SessionCommand("command2", Bundle.EMPTY)) + .setSlots(CommandButton.SLOT_BACK) + .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(); + setupMediaButtonPreferences(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( + withBackSlot(button2), withForwardSlot(button1), withOverflowSlot(button3)), + ImmutableList.of(withBackSlot(button2), withOverflowSlot(button3)), + ImmutableList.of(withForwardSlot(button1), withOverflowSlot(button3)), + ImmutableList.of(withOverflowSlot(button3))); + session.cleanUp(); + } + + @Test + public void + getCustomLayout_setSessionExtrasForPrevNextReservationsAfterSetMediaButtonPreferences_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)) + .setSlots(CommandButton.SLOT_FORWARD) + .build(); + CommandButton button2 = + new CommandButton.Builder(CommandButton.ICON_REWIND) + .setDisplayName("button2") + .setSessionCommand(new SessionCommand("command2", Bundle.EMPTY)) + .setSlots(CommandButton.SLOT_BACK) + .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(); + setupMediaButtonPreferences(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( + withBackSlot(button2), withForwardSlot(button1), withOverflowSlot(button3)), + ImmutableList.of(withBackSlot(button2), withOverflowSlot(button3)), + ImmutableList.of(withForwardSlot(button1), withOverflowSlot(button3)), + ImmutableList.of(withOverflowSlot(button3))); + session.cleanUp(); + } + @Test public void getMediaButtonPreferences_mediaButtonPreferencesBuiltWithSession_includedOnConnect() throws Exception { @@ -2551,7 +3507,7 @@ public class MediaControllerTest { } private void setupCustomLayout(RemoteMediaSession session, List customLayout) - throws RemoteException, InterruptedException, Exception { + throws Exception { CountDownLatch latch = new CountDownLatch(1); controllerTestRule.createController( session.getToken(), @@ -2568,8 +3524,7 @@ public class MediaControllerTest { } private void setupMediaButtonPreferences( - RemoteMediaSession session, List mediaButtonPreferences) - throws RemoteException, InterruptedException, Exception { + RemoteMediaSession session, List mediaButtonPreferences) throws Exception { CountDownLatch latch = new CountDownLatch(1); controllerTestRule.createController( session.getToken(), @@ -2601,6 +3556,14 @@ public class MediaControllerTest { ImmutableIntArray.of(CommandButton.SLOT_FORWARD, CommandButton.SLOT_OVERFLOW)); } + private static CommandButton withBackSlot(CommandButton button) { + return button.copyWithSlots(ImmutableIntArray.of(CommandButton.SLOT_BACK)); + } + + private static CommandButton withForwardSlot(CommandButton button) { + return button.copyWithSlots(ImmutableIntArray.of(CommandButton.SLOT_FORWARD)); + } + 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/MediaSessionServiceTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionServiceTest.java index a133829d85..ee847e6eb9 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 @@ -335,8 +335,7 @@ public class MediaSessionServiceTest { .containsExactly( button1 .copyWithIsEnabled(false) - .copyWithSlots( - ImmutableIntArray.of(CommandButton.SLOT_FORWARD, CommandButton.SLOT_OVERFLOW)), + .copyWithSlots(ImmutableIntArray.of(CommandButton.SLOT_FORWARD)), button2 .copyWithIsEnabled(false) .copyWithSlots(ImmutableIntArray.of(CommandButton.SLOT_OVERFLOW)))