Add MediaSession.setCustomLayout(List<CommandButton>)

This provides a way for apps to send a custom layout to media3
controllers and legacy controllers by making sure to include custom
actions in the legacy playback state when built in the PlayerWrapper
for broadcasting.

PiperOrigin-RevId: 447967600
This commit is contained in:
bachinger 2022-05-11 13:11:22 +01:00 committed by Ian Baker
parent d72ffe4ba1
commit 1d89c35f7b
6 changed files with 113 additions and 12 deletions

View File

@ -622,6 +622,17 @@ public class MediaSession {
return impl.setCustomLayout(controller, layout); 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<CommandButton> layout) {
checkNotNull(layout, "layout must not be null");
impl.setCustomLayout(layout);
}
/** /**
* Sets the new available commands for the controller. * Sets the new available commands for the controller.
* *

View File

@ -70,6 +70,7 @@ import androidx.media3.session.MediaSession.ControllerInfo;
import androidx.media3.session.MediaSession.MediaItemFiller; import androidx.media3.session.MediaSession.MediaItemFiller;
import androidx.media3.session.MediaSession.SessionCallback; import androidx.media3.session.MediaSession.SessionCallback;
import androidx.media3.session.SequencedFutureManager.SequencedFuture; 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.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
@ -343,6 +344,12 @@ import org.checkerframework.checker.initialization.qual.Initialized;
controller, (controller1, seq) -> controller1.setCustomLayout(seq, layout)); controller, (controller1, seq) -> controller1.setCustomLayout(seq, layout));
} }
public void setCustomLayout(List<CommandButton> layout) {
playerWrapper.setCustomLayout(ImmutableList.copyOf(layout));
dispatchRemoteControllerTaskWithoutReturn(
(controller, seq) -> controller.setCustomLayout(seq, layout));
}
public void setAvailableCommands( public void setAvailableCommands(
ControllerInfo controller, SessionCommands sessionCommands, Player.Commands playerCommands) { ControllerInfo controller, SessionCommands sessionCommands, Player.Commands playerCommands) {
if (sessionStub.getConnectedControllersManager().isConnected(controller)) { if (sessionStub.getConnectedControllersManager().isConnected(controller)) {

View File

@ -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.Assertions.checkStateNotNull;
import static androidx.media3.common.util.Util.postOrRun; import static androidx.media3.common.util.Util.postOrRun;
import static androidx.media3.session.MediaUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES; 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_ERROR_UNKNOWN;
import static androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED; import static androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED;
import static androidx.media3.session.SessionResult.RESULT_SUCCESS; import static androidx.media3.session.SessionResult.RESULT_SUCCESS;
@ -640,10 +641,7 @@ import org.checkerframework.checker.initialization.qual.Initialized;
private void dispatchSessionTaskWithSessionCommand( private void dispatchSessionTaskWithSessionCommand(
SessionCommand sessionCommand, SessionTask task) { SessionCommand sessionCommand, SessionTask task) {
dispatchSessionTaskWithSessionCommandInternal( dispatchSessionTaskWithSessionCommandInternal(
sessionCommand, sessionCommand, COMMAND_CODE_CUSTOM, task, sessionCompat.getCurrentControllerInfo());
SessionCommand.COMMAND_CODE_CUSTOM,
task,
sessionCompat.getCurrentControllerInfo());
} }
private void dispatchSessionTaskWithSessionCommandInternal( private void dispatchSessionTaskWithSessionCommandInternal(
@ -880,6 +878,13 @@ import org.checkerframework.checker.initialization.qual.Initialized;
.setPlaybackState(sessionImpl.getPlayerWrapper().createPlaybackStateCompat()); .setPlaybackState(sessionImpl.getPlayerWrapper().createPlaybackStateCompat());
} }
@Override
public void setCustomLayout(int seq, List<CommandButton> layout) {
sessionImpl
.getSessionCompat()
.setPlaybackState(sessionImpl.getPlayerWrapper().createPlaybackStateCompat());
}
@Override @Override
public void onPlayWhenReadyChanged( public void onPlayWhenReadyChanged(
int seq, boolean playWhenReady, @Player.PlaybackSuppressionReason int reason) int seq, boolean playWhenReady, @Player.PlaybackSuppressionReason int reason)

View File

@ -45,6 +45,7 @@ import androidx.media3.common.VideoSize;
import androidx.media3.common.text.Cue; import androidx.media3.common.text.Cue;
import androidx.media3.common.util.Log; import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList;
import java.util.List; import java.util.List;
/** /**
@ -59,10 +60,12 @@ import java.util.List;
private int legacyStatusCode; private int legacyStatusCode;
@Nullable private String legacyErrorMessage; @Nullable private String legacyErrorMessage;
@Nullable private Bundle legacyErrorExtras; @Nullable private Bundle legacyErrorExtras;
private ImmutableList<CommandButton> customLayout;
public PlayerWrapper(Player player) { public PlayerWrapper(Player player) {
super(player); super(player);
legacyStatusCode = STATUS_CODE_SUCCESS_COMPAT; legacyStatusCode = STATUS_CODE_SUCCESS_COMPAT;
customLayout = ImmutableList.of();
} }
/** /**
@ -91,6 +94,11 @@ import java.util.List;
return legacyStatusCode; return legacyStatusCode;
} }
/** Sets the custom layout. */
public void setCustomLayout(ImmutableList<CommandButton> customLayout) {
this.customLayout = customLayout;
}
/** Clears the legacy error status. */ /** Clears the legacy error status. */
public void clearLegacyErrorStatus() { public void clearLegacyErrorStatus() {
legacyStatusCode = STATUS_CODE_SUCCESS_COMPAT; legacyStatusCode = STATUS_CODE_SUCCESS_COMPAT;
@ -766,8 +774,6 @@ import java.util.List;
| PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_PAUSE
| PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PLAY
| PlaybackStateCompat.ACTION_REWIND | PlaybackStateCompat.ACTION_REWIND
| PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
| PlaybackStateCompat.ACTION_SKIP_TO_NEXT
| PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_FAST_FORWARD
| PlaybackStateCompat.ACTION_SET_RATING | PlaybackStateCompat.ACTION_SET_RATING
| PlaybackStateCompat.ACTION_SEEK_TO | PlaybackStateCompat.ACTION_SEEK_TO
@ -783,6 +789,14 @@ import java.util.List;
| PlaybackStateCompat.ACTION_SET_REPEAT_MODE | PlaybackStateCompat.ACTION_SET_REPEAT_MODE
| PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE
| PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED; | 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()); long queueItemId = MediaUtils.convertToQueueItemId(getCurrentMediaItemIndex());
PlaybackStateCompat.Builder builder = PlaybackStateCompat.Builder builder =
new PlaybackStateCompat.Builder() new PlaybackStateCompat.Builder()
@ -794,6 +808,22 @@ import java.util.List;
.setActions(allActions) .setActions(allActions)
.setActiveQueueItemId(queueItemId) .setActiveQueueItemId(queueItemId)
.setBufferedPosition(getBufferedPosition()); .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) { if (playerError != null) {
builder.setErrorMessage( builder.setErrorMessage(
PlaybackStateCompat.ERROR_CODE_UNKNOWN_ERROR, Util.castNonNull(playerError.getMessage())); PlaybackStateCompat.ERROR_CODE_UNKNOWN_ERROR, Util.castNonNull(playerError.getMessage()));

View File

@ -57,6 +57,7 @@ import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest; import androidx.test.filters.LargeTest;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -767,6 +768,55 @@ public class MediaControllerCompatCallbackWithMediaSessionTest {
assertThat(controllerCompat.getPlaybackState().getPosition()).isEqualTo(testSeekPosition); assertThat(controllerCompat.getPlaybackState().getPosition()).isEqualTo(testSeekPosition);
} }
@Test
public void customLayoutChanged_updatesPlaybackStateCompat() throws Exception {
AtomicReference<PlaybackStateCompat> 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<CommandButton> 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<PlaybackStateCompat.CustomAction> 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 @Test
public void currentMediaItemChange() throws Exception { public void currentMediaItemChange() throws Exception {
int testItemIndex = 3; int testItemIndex = 3;

View File

@ -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;
import androidx.media3.test.session.common.TestHandler.TestRunnable; import androidx.media3.test.session.common.TestHandler.TestRunnable;
import androidx.media3.test.session.common.TestUtils; import androidx.media3.test.session.common.TestUtils;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -398,15 +399,12 @@ public class MediaSessionProviderService extends Service {
} }
runOnHandler( runOnHandler(
() -> { () -> {
List<CommandButton> buttons = new ArrayList<>(); ImmutableList.Builder<CommandButton> builder = new ImmutableList.Builder<>();
for (Bundle bundle : layout) { for (Bundle bundle : layout) {
buttons.add(CommandButton.CREATOR.fromBundle(bundle)); builder.add(CommandButton.CREATOR.fromBundle(bundle));
} }
MediaSession session = sessionMap.get(sessionId); MediaSession session = sessionMap.get(sessionId);
List<ControllerInfo> controllerInfos = MediaTestUtils.getTestControllerInfos(session); session.setCustomLayout(builder.build());
for (ControllerInfo info : controllerInfos) {
session.setCustomLayout(info, buttons);
}
}); });
} }