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:
parent
d72ffe4ba1
commit
1d89c35f7b
@ -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.
|
||||||
*
|
*
|
||||||
|
@ -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)) {
|
||||||
|
@ -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)
|
||||||
|
@ -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()));
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user