diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java index 0cb74a5d7c..9dac8c175e 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -622,6 +622,17 @@ public class MediaSession { return impl.setCustomLayout(controller, layout); } + /** + * Sets the custom layout and broadcasts it to all connected controllers including the legacy + * controllers. + * + * @param layout The ordered list of {@link CommandButton}. + */ + public void setCustomLayout(List layout) { + checkNotNull(layout, "layout must not be null"); + impl.setCustomLayout(layout); + } + /** * Sets the new available commands for the controller. * diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java index 07caa4e5ef..f7f83018d6 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -70,6 +70,7 @@ import androidx.media3.session.MediaSession.ControllerInfo; import androidx.media3.session.MediaSession.MediaItemFiller; import androidx.media3.session.MediaSession.SessionCallback; import androidx.media3.session.SequencedFutureManager.SequencedFuture; +import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import java.lang.ref.WeakReference; @@ -343,6 +344,12 @@ import org.checkerframework.checker.initialization.qual.Initialized; controller, (controller1, seq) -> controller1.setCustomLayout(seq, layout)); } + public void setCustomLayout(List layout) { + playerWrapper.setCustomLayout(ImmutableList.copyOf(layout)); + dispatchRemoteControllerTaskWithoutReturn( + (controller, seq) -> controller.setCustomLayout(seq, layout)); + } + public void setAvailableCommands( ControllerInfo controller, SessionCommands sessionCommands, Player.Commands playerCommands) { if (sessionStub.getConnectedControllersManager().isConnected(controller)) { diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index 0f476641f5..8b92613b67 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -34,6 +34,7 @@ import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Util.postOrRun; import static androidx.media3.session.MediaUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES; +import static androidx.media3.session.SessionCommand.COMMAND_CODE_CUSTOM; import static androidx.media3.session.SessionResult.RESULT_ERROR_UNKNOWN; import static androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED; import static androidx.media3.session.SessionResult.RESULT_SUCCESS; @@ -640,10 +641,7 @@ import org.checkerframework.checker.initialization.qual.Initialized; private void dispatchSessionTaskWithSessionCommand( SessionCommand sessionCommand, SessionTask task) { dispatchSessionTaskWithSessionCommandInternal( - sessionCommand, - SessionCommand.COMMAND_CODE_CUSTOM, - task, - sessionCompat.getCurrentControllerInfo()); + sessionCommand, COMMAND_CODE_CUSTOM, task, sessionCompat.getCurrentControllerInfo()); } private void dispatchSessionTaskWithSessionCommandInternal( @@ -880,6 +878,13 @@ import org.checkerframework.checker.initialization.qual.Initialized; .setPlaybackState(sessionImpl.getPlayerWrapper().createPlaybackStateCompat()); } + @Override + public void setCustomLayout(int seq, List layout) { + sessionImpl + .getSessionCompat() + .setPlaybackState(sessionImpl.getPlayerWrapper().createPlaybackStateCompat()); + } + @Override public void onPlayWhenReadyChanged( int seq, boolean playWhenReady, @Player.PlaybackSuppressionReason int reason) diff --git a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java index 300f7bfd03..41716c5edd 100644 --- a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java +++ b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java @@ -45,6 +45,7 @@ import androidx.media3.common.VideoSize; import androidx.media3.common.text.Cue; import androidx.media3.common.util.Log; import androidx.media3.common.util.Util; +import com.google.common.collect.ImmutableList; import java.util.List; /** @@ -59,10 +60,12 @@ import java.util.List; private int legacyStatusCode; @Nullable private String legacyErrorMessage; @Nullable private Bundle legacyErrorExtras; + private ImmutableList customLayout; public PlayerWrapper(Player player) { super(player); legacyStatusCode = STATUS_CODE_SUCCESS_COMPAT; + customLayout = ImmutableList.of(); } /** @@ -91,6 +94,11 @@ import java.util.List; return legacyStatusCode; } + /** Sets the custom layout. */ + public void setCustomLayout(ImmutableList customLayout) { + this.customLayout = customLayout; + } + /** Clears the legacy error status. */ public void clearLegacyErrorStatus() { legacyStatusCode = STATUS_CODE_SUCCESS_COMPAT; @@ -766,8 +774,6 @@ import java.util.List; | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_REWIND - | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS - | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_SET_RATING | PlaybackStateCompat.ACTION_SEEK_TO @@ -783,6 +789,14 @@ import java.util.List; | PlaybackStateCompat.ACTION_SET_REPEAT_MODE | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE | PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED; + if (getAvailableCommands().contains(COMMAND_SEEK_TO_PREVIOUS) + || getAvailableCommands().contains(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)) { + allActions |= PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; + } + if (getAvailableCommands().contains(COMMAND_SEEK_TO_NEXT) + || getAvailableCommands().contains(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)) { + allActions |= PlaybackStateCompat.ACTION_SKIP_TO_NEXT; + } long queueItemId = MediaUtils.convertToQueueItemId(getCurrentMediaItemIndex()); PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder() @@ -794,6 +808,22 @@ import java.util.List; .setActions(allActions) .setActiveQueueItemId(queueItemId) .setBufferedPosition(getBufferedPosition()); + + for (int i = 0; i < customLayout.size(); i++) { + CommandButton commandButton = customLayout.get(i); + if (commandButton.sessionCommand != null) { + SessionCommand sessionCommand = commandButton.sessionCommand; + if (sessionCommand.commandCode == SessionCommand.COMMAND_CODE_CUSTOM) { + builder.addCustomAction( + new PlaybackStateCompat.CustomAction.Builder( + sessionCommand.customAction, + commandButton.displayName, + commandButton.iconResId) + .setExtras(sessionCommand.customExtras) + .build()); + } + } + } if (playerError != null) { builder.setErrorMessage( PlaybackStateCompat.ERROR_CODE_UNKNOWN_ERROR, Util.castNonNull(playerError.getMessage())); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java index 830b04d1ae..61f3e5742b 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java @@ -57,6 +57,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 java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -767,6 +768,55 @@ public class MediaControllerCompatCallbackWithMediaSessionTest { assertThat(controllerCompat.getPlaybackState().getPosition()).isEqualTo(testSeekPosition); } + @Test + public void customLayoutChanged_updatesPlaybackStateCompat() throws Exception { + + AtomicReference playbackStateRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + MediaControllerCompat.Callback callback = + new MediaControllerCompat.Callback() { + @Override + public void onPlaybackStateChanged(PlaybackStateCompat state) { + playbackStateRef.set(state); + latch.countDown(); + } + }; + controllerCompat.registerCallback(callback, handler); + + List customLayout = new ArrayList<>(); + Bundle customCommandBundle1 = new Bundle(); + customCommandBundle1.putString("customKey1", "customValue1"); + customLayout.add( + new CommandButton.Builder() + .setDisplayName("customCommandName1") + .setIconResId(1) + .setSessionCommand(new SessionCommand("customCommandAction1", customCommandBundle1)) + .build()); + Bundle customCommandBundle2 = new Bundle(); + customCommandBundle2.putString("customKey2", "customValue2"); + customLayout.add( + new CommandButton.Builder() + .setDisplayName("customCommandName2") + .setIconResId(2) + .setSessionCommand(new SessionCommand("customCommandAction2", customCommandBundle2)) + .build()); + + session.setCustomLayout(customLayout); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + List customActions = + playbackStateRef.get().getCustomActions(); + assertThat(customActions).hasSize(2); + assertThat(customActions.get(0).getAction()).isEqualTo("customCommandAction1"); + assertThat(customActions.get(0).getName()).isEqualTo("customCommandName1"); + assertThat(customActions.get(0).getIcon()).isEqualTo(1); + assertThat(TestUtils.equals(customActions.get(0).getExtras(), customCommandBundle1)).isTrue(); + assertThat(customActions.get(1).getAction()).isEqualTo("customCommandAction2"); + assertThat(customActions.get(1).getName()).isEqualTo("customCommandName2"); + assertThat(customActions.get(1).getIcon()).isEqualTo(2); + assertThat(TestUtils.equals(customActions.get(1).getExtras(), customCommandBundle2)).isTrue(); + } + @Test public void currentMediaItemChange() throws Exception { int testItemIndex = 3; diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java index e8b5c6dafd..c005fd5cd2 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java @@ -89,6 +89,7 @@ import androidx.media3.test.session.common.MockActivity; import androidx.media3.test.session.common.TestHandler; import androidx.media3.test.session.common.TestHandler.TestRunnable; import androidx.media3.test.session.common.TestUtils; +import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -398,15 +399,12 @@ public class MediaSessionProviderService extends Service { } runOnHandler( () -> { - List buttons = new ArrayList<>(); + ImmutableList.Builder builder = new ImmutableList.Builder<>(); for (Bundle bundle : layout) { - buttons.add(CommandButton.CREATOR.fromBundle(bundle)); + builder.add(CommandButton.CREATOR.fromBundle(bundle)); } MediaSession session = sessionMap.get(sessionId); - List controllerInfos = MediaTestUtils.getTestControllerInfos(session); - for (ControllerInfo info : controllerInfos) { - session.setCustomLayout(info, buttons); - } + session.setCustomLayout(builder.build()); }); }