Refine auto-update logic of CommandButton.isEnabled

We currently update this value for controllers to match the
availability of the associated command. This however makes it
impossible to mark a button as unavailable if the command is
available. This can be refined by only setting the 'enabled'
field to false if the command is not available, not the other
way round. And we should also enable the button by default as
disabling is the unusual case not many apps will use.

In addition, this change fixes missing update logic when the
player commands changed and it adds some additional test coverage
for all these cases.

PiperOrigin-RevId: 612881016
This commit is contained in:
tonihei 2024-03-05 10:04:45 -08:00 committed by Copybara-Service
parent 8b59888766
commit 9abe9e2a97
16 changed files with 376 additions and 118 deletions

View File

@ -58,6 +58,9 @@
* Fix issue where the current position jumps back when the controller
replaces the current item
([#951](https://github.com/androidx/media/issues/951)).
* Change default of `CommandButton.enabled` to `true` and ensure the value
can stay false for controllers even if the associated command is
available.
* UI:
* Fallback to include audio track language name if `Locale` cannot
identify a display name

View File

@ -412,6 +412,7 @@ public final class CommandButton implements Bundleable {
extras = Bundle.EMPTY;
playerCommand = Player.COMMAND_INVALID;
icon = ICON_UNDEFINED;
enabled = true;
}
/**
@ -507,6 +508,12 @@ public final class CommandButton implements Bundleable {
/**
* Sets whether the button is enabled.
*
* <p>Note that this value will be set to {@code false} for {@link MediaController} instances if
* the corresponding command is not available to this controller (see {@link #setPlayerCommand}
* and {@link #setSessionCommand}).
*
* <p>The default value is {@code true}.
*
* @param enabled Whether the button is enabled.
* @return This builder for chaining.
*/
@ -571,7 +578,13 @@ public final class CommandButton implements Bundleable {
*/
@UnstableApi public final Bundle extras;
/** Whether it's enabled. */
/**
* Whether the button is enabled.
*
* <p>Note that this value will be set to {@code false} for {@link MediaController} instances if
* the corresponding command is not available to this controller (see {@link #playerCommand} and
* {@link #sessionCommand}).
*/
public final boolean isEnabled;
private CommandButton(
@ -639,31 +652,35 @@ public final class CommandButton implements Bundleable {
}
/**
* Returns a list of command buttons with the {@link CommandButton#isEnabled} flag set according
* to the available commands passed in.
* Returns a list of command buttons with the {@link CommandButton#isEnabled} flag set to false if
* the corresponding command is not available.
*/
/* package */ static ImmutableList<CommandButton> getEnabledCommandButtons(
/* package */ static ImmutableList<CommandButton> copyWithUnavailableButtonsDisabled(
List<CommandButton> commandButtons,
SessionCommands sessionCommands,
Player.Commands playerCommands) {
ImmutableList.Builder<CommandButton> enabledButtons = new ImmutableList.Builder<>();
ImmutableList.Builder<CommandButton> updatedButtons = new ImmutableList.Builder<>();
for (int i = 0; i < commandButtons.size(); i++) {
CommandButton button = commandButtons.get(i);
enabledButtons.add(
button.copyWithIsEnabled(isEnabled(button, sessionCommands, playerCommands)));
if (isButtonCommandAvailable(button, sessionCommands, playerCommands)) {
updatedButtons.add(button);
} else {
updatedButtons.add(button.copyWithIsEnabled(false));
}
}
return enabledButtons.build();
return updatedButtons.build();
}
/**
* Returns whether the {@link CommandButton} is enabled given the available commands passed in.
* Returns whether the required command ({@link #playerCommand} or {@link #sessionCommand}) for
* the button is available.
*
* @param button The command button.
* @param sessionCommands The available session commands.
* @param playerCommands The available player commands.
* @return Whether the button is enabled given the available commands.
* @return Whether the command required for this button is available.
*/
/* package */ static boolean isEnabled(
/* package */ static boolean isButtonCommandAvailable(
CommandButton button, SessionCommands sessionCommands, Player.Commands playerCommands) {
return (button.sessionCommand != null && sessionCommands.contains(button.sessionCommand))
|| (button.playerCommand != Player.COMMAND_INVALID
@ -706,7 +723,7 @@ public final class CommandButton implements Bundleable {
if (iconUri != null) {
bundle.putParcelable(FIELD_ICON_URI, iconUri);
}
if (isEnabled) {
if (!isEnabled) {
bundle.putBoolean(FIELD_ENABLED, isEnabled);
}
return bundle;
@ -722,9 +739,18 @@ public final class CommandButton implements Bundleable {
@SuppressWarnings("deprecation") // Deprecated instance of deprecated class
public static final Creator<CommandButton> CREATOR = CommandButton::fromBundle;
/** Restores a {@code CommandButton} from a {@link Bundle}. */
/**
* @deprecated Use {@link #fromBundle(Bundle, int)} instead.
*/
@Deprecated
@UnstableApi
public static CommandButton fromBundle(Bundle bundle) {
return fromBundle(bundle, MediaSessionStub.VERSION_INT);
}
/** Restores a {@code CommandButton} from a {@link Bundle}. */
@UnstableApi
public static CommandButton fromBundle(Bundle bundle, int sessionInterfaceVersion) {
@Nullable Bundle sessionCommandBundle = bundle.getBundle(FIELD_SESSION_COMMAND);
@Nullable
SessionCommand sessionCommand =
@ -735,7 +761,10 @@ public final class CommandButton implements Bundleable {
int iconResId = bundle.getInt(FIELD_ICON_RES_ID, /* defaultValue= */ 0);
CharSequence displayName = bundle.getCharSequence(FIELD_DISPLAY_NAME, /* defaultValue= */ "");
@Nullable Bundle extras = bundle.getBundle(FIELD_EXTRAS);
boolean enabled = bundle.getBoolean(FIELD_ENABLED, /* defaultValue= */ false);
// Before sessionInterfaceVersion == 3, the session expected this value to be meaningless and we
// can only assume it was meant to be true.
boolean enabled =
sessionInterfaceVersion < 3 || bundle.getBoolean(FIELD_ENABLED, /* defaultValue= */ true);
@Nullable Uri iconUri = bundle.getParcelable(FIELD_ICON_URI);
@Icon int icon = bundle.getInt(FIELD_ICON, /* defaultValue= */ ICON_UNDEFINED);
Builder builder = new Builder();

View File

@ -164,7 +164,8 @@ import java.util.List;
List<Bundle> commandButtonArrayList = bundle.getParcelableArrayList(FIELD_CUSTOM_LAYOUT);
ImmutableList<CommandButton> customLayout =
commandButtonArrayList != null
? BundleCollectionUtil.fromBundleList(CommandButton::fromBundle, commandButtonArrayList)
? BundleCollectionUtil.fromBundleList(
b -> CommandButton.fromBundle(b, sessionInterfaceVersion), commandButtonArrayList)
: ImmutableList.of();
@Nullable Bundle sessionCommandsBundle = bundle.getBundle(FIELD_SESSION_COMMANDS);
SessionCommands sessionCommands =

View File

@ -388,6 +388,9 @@ public class MediaController implements Player {
* changes the available commands} for a controller that affect whether buttons of the custom
* layout are enabled or disabled.
*
* <p>Note that the {@linkplain CommandButton#isEnabled enabled} flag is set to {@code false} if
* the available commands do not allow to use a button.
*
* @param controller The controller.
* @param layout The ordered list of {@linkplain CommandButton command buttons}.
*/
@ -976,6 +979,9 @@ public class MediaController implements Player {
* <p>After being connected, a change of the custom layout is reported with {@link
* Listener#onCustomLayoutChanged(MediaController, List)}.
*
* <p>Note that the {@linkplain CommandButton#isEnabled enabled} flag is set to {@code false} if
* the available commands do not allow to use a button.
*
* @return The custom layout.
*/
@UnstableApi

View File

@ -120,7 +120,8 @@ import org.checkerframework.checker.nullness.qual.NonNull;
private boolean released;
private PlayerInfo playerInfo;
@Nullable private PendingIntent sessionActivity;
private ImmutableList<CommandButton> customLayout;
private ImmutableList<CommandButton> customLayoutOriginal;
private ImmutableList<CommandButton> customLayoutWithUnavailableButtonsDisabled;
private SessionCommands sessionCommands;
private Commands playerCommandsFromSession;
private Commands playerCommandsFromPlayer;
@ -146,7 +147,8 @@ import org.checkerframework.checker.nullness.qual.NonNull;
playerInfo = PlayerInfo.DEFAULT;
surfaceSize = Size.UNKNOWN;
sessionCommands = SessionCommands.EMPTY;
customLayout = ImmutableList.of();
customLayoutOriginal = ImmutableList.of();
customLayoutWithUnavailableButtonsDisabled = ImmutableList.of();
playerCommandsFromSession = Commands.EMPTY;
playerCommandsFromPlayer = Commands.EMPTY;
intersectedPlayerCommands =
@ -726,7 +728,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
@Override
public ImmutableList<CommandButton> getCustomLayout() {
return customLayout;
return customLayoutWithUnavailableButtonsDisabled;
}
@Override
@ -2611,8 +2613,9 @@ import org.checkerframework.checker.nullness.qual.NonNull;
intersectedPlayerCommands =
createIntersectedCommandsEnsuringCommandReleaseAvailable(
playerCommandsFromSession, playerCommandsFromPlayer);
customLayout =
CommandButton.getEnabledCommandButtons(
customLayoutOriginal = result.customLayout;
customLayoutWithUnavailableButtonsDisabled =
CommandButton.copyWithUnavailableButtonsDisabled(
result.customLayout, sessionCommands, intersectedPlayerCommands);
playerInfo = result.playerInfo;
try {
@ -2765,6 +2768,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
if (!playerCommandsChanged && !sessionCommandsChanged) {
return;
}
this.sessionCommands = sessionCommands;
boolean intersectedPlayerCommandsChanged = false;
if (playerCommandsChanged) {
playerCommandsFromSession = playerCommands;
@ -2776,13 +2780,12 @@ import org.checkerframework.checker.nullness.qual.NonNull;
!Util.areEqual(intersectedPlayerCommands, prevIntersectedPlayerCommands);
}
boolean customLayoutChanged = false;
if (sessionCommandsChanged) {
this.sessionCommands = sessionCommands;
ImmutableList<CommandButton> oldCustomLayout = customLayout;
customLayout =
CommandButton.getEnabledCommandButtons(
customLayout, sessionCommands, intersectedPlayerCommands);
customLayoutChanged = !customLayout.equals(oldCustomLayout);
if (sessionCommandsChanged || intersectedPlayerCommandsChanged) {
ImmutableList<CommandButton> oldCustomLayout = customLayoutWithUnavailableButtonsDisabled;
customLayoutWithUnavailableButtonsDisabled =
CommandButton.copyWithUnavailableButtonsDisabled(
customLayoutOriginal, sessionCommands, intersectedPlayerCommands);
customLayoutChanged = !customLayoutWithUnavailableButtonsDisabled.equals(oldCustomLayout);
}
if (intersectedPlayerCommandsChanged) {
listeners.sendEvent(
@ -2798,7 +2801,9 @@ import org.checkerframework.checker.nullness.qual.NonNull;
if (customLayoutChanged) {
getInstance()
.notifyControllerListener(
listener -> listener.onCustomLayoutChanged(getInstance(), customLayout));
listener ->
listener.onCustomLayoutChanged(
getInstance(), customLayoutWithUnavailableButtonsDisabled));
}
}
@ -2816,11 +2821,24 @@ import org.checkerframework.checker.nullness.qual.NonNull;
playerCommandsFromSession, playerCommandsFromPlayer);
boolean intersectedPlayerCommandsChanged =
!Util.areEqual(intersectedPlayerCommands, prevIntersectedPlayerCommands);
boolean customLayoutChanged = false;
if (intersectedPlayerCommandsChanged) {
ImmutableList<CommandButton> oldCustomLayout = customLayoutWithUnavailableButtonsDisabled;
customLayoutWithUnavailableButtonsDisabled =
CommandButton.copyWithUnavailableButtonsDisabled(
customLayoutOriginal, sessionCommands, intersectedPlayerCommands);
customLayoutChanged = !customLayoutWithUnavailableButtonsDisabled.equals(oldCustomLayout);
listeners.sendEvent(
/* eventFlag= */ Player.EVENT_AVAILABLE_COMMANDS_CHANGED,
listener -> listener.onAvailableCommandsChanged(intersectedPlayerCommands));
}
if (customLayoutChanged) {
getInstance()
.notifyControllerListener(
listener ->
listener.onCustomLayoutChanged(
getInstance(), customLayoutWithUnavailableButtonsDisabled));
}
}
// Calling deprecated listener callback method for backwards compatibility.
@ -2829,19 +2847,24 @@ import org.checkerframework.checker.nullness.qual.NonNull;
if (!isConnected()) {
return;
}
ImmutableList<CommandButton> oldCustomLayout = customLayout;
customLayout =
CommandButton.getEnabledCommandButtons(layout, sessionCommands, intersectedPlayerCommands);
boolean hasCustomLayoutChanged = !Objects.equals(customLayout, oldCustomLayout);
ImmutableList<CommandButton> oldCustomLayout = customLayoutWithUnavailableButtonsDisabled;
customLayoutOriginal = ImmutableList.copyOf(layout);
customLayoutWithUnavailableButtonsDisabled =
CommandButton.copyWithUnavailableButtonsDisabled(
layout, sessionCommands, intersectedPlayerCommands);
boolean hasCustomLayoutChanged =
!Objects.equals(customLayoutWithUnavailableButtonsDisabled, oldCustomLayout);
getInstance()
.notifyControllerListener(
listener -> {
ListenableFuture<SessionResult> future =
checkNotNull(
listener.onSetCustomLayout(getInstance(), customLayout),
listener.onSetCustomLayout(
getInstance(), customLayoutWithUnavailableButtonsDisabled),
"MediaController.Listener#onSetCustomLayout() must not return null");
if (hasCustomLayoutChanged) {
listener.onCustomLayoutChanged(getInstance(), customLayout);
listener.onCustomLayoutChanged(
getInstance(), customLayoutWithUnavailableButtonsDisabled);
}
sendControllerResultWhenReady(seq, future);
});

View File

@ -112,8 +112,18 @@ import org.checkerframework.checker.nullness.qual.NonNull;
}
List<CommandButton> layout;
try {
@Nullable MediaControllerImplBase controller = this.controller.get();
@Nullable
SessionToken connectedToken = controller == null ? null : controller.getConnectedToken();
if (connectedToken == null) {
// Stale event.
return;
}
int sessionInterfaceVersion = connectedToken.getInterfaceVersion();
layout =
BundleCollectionUtil.fromBundleList(CommandButton::fromBundle, commandButtonBundleList);
BundleCollectionUtil.fromBundleList(
bundle -> CommandButton.fromBundle(bundle, sessionInterfaceVersion),
commandButtonBundleList);
} catch (RuntimeException e) {
Log.w(TAG, "Ignoring malformed Bundle for CommandButton", e);
return;

View File

@ -388,6 +388,9 @@ public class MediaSession {
* <p>Use {@code MediaSession.setCustomLayout(..)} to update the custom layout during the life
* time of the session.
*
* <p>On the controller side, the {@linkplain CommandButton#isEnabled enabled} flag is set to
* {@code false} if the available commands of a controller do not allow to use a button.
*
* @param customLayout The ordered list of {@link CommandButton command buttons}.
* @return The builder to allow chaining.
*/
@ -911,7 +914,8 @@ public class MediaSession {
* MediaController#getCustomLayout() controller already has available}. Note that this comparison
* uses {@link CommandButton#equals} and therefore ignores {@link CommandButton#extras}.
*
* <p>It's up to controller's decision how to represent the layout in its own UI.
* <p>On the controller side, the {@linkplain CommandButton#isEnabled enabled} flag is set to
* {@code false} if the available commands of the controller do not allow to use a button.
*
* <p>Interoperability: This call has no effect when called for a {@linkplain
* ControllerInfo#LEGACY_CONTROLLER_VERSION legacy controller}.
@ -933,9 +937,8 @@ public class MediaSession {
* <p>Calling this method broadcasts the custom layout to all connected Media3 controllers,
* including the {@linkplain #getMediaNotificationControllerInfo() media notification controller}.
*
* <p>On the controller side, the {@linkplain CommandButton#isEnabled enabled} flag is set
* according to the available commands of the controller which overrides a value that has been set
* by the session.
* <p>On the controller side, the {@linkplain CommandButton#isEnabled enabled} flag is set to
* {@code false} if the available commands of a controller do not allow to use a button.
*
* <p>{@link MediaController.Listener#onCustomLayoutChanged(MediaController, List)} is only called
* if the new custom layout is different to the custom layout the {@linkplain
@ -1653,7 +1656,9 @@ public class MediaSession {
*
* <p>Make sure to have the session commands of all command buttons of the custom layout
* included in the {@linkplain #setAvailableSessionCommands(SessionCommands)} available
* session commands}.
* session commands} On the controller side, the {@linkplain CommandButton#isEnabled enabled}
* flag is set to {@code false} if the available commands of the controller do not allow to
* use a button.
*/
@CanIgnoreReturnValue
public AcceptedResultBuilder setCustomLayout(@Nullable List<CommandButton> customLayout) {

View File

@ -113,7 +113,7 @@ import java.util.concurrent.ExecutionException;
private static final String TAG = "MediaSessionStub";
/** The version of the IMediaSession interface. */
public static final int VERSION_INT = 2;
public static final int VERSION_INT = 3;
/**
* Sequence number used when a controller method is triggered on the sesison side that wasn't

View File

@ -1043,25 +1043,23 @@ import java.util.List;
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
&& CommandButton.isEnabled(
commandButton, availableSessionCommands, availablePlayerCommands)) {
Bundle actionExtras = sessionCommand.customExtras;
if (commandButton.icon != CommandButton.ICON_UNDEFINED) {
actionExtras = new Bundle(sessionCommand.customExtras);
actionExtras.putInt(
MediaConstants.EXTRAS_KEY_COMMAND_BUTTON_ICON_COMPAT, commandButton.icon);
}
builder.addCustomAction(
new PlaybackStateCompat.CustomAction.Builder(
sessionCommand.customAction,
commandButton.displayName,
commandButton.iconResId)
.setExtras(actionExtras)
.build());
SessionCommand sessionCommand = commandButton.sessionCommand;
if (sessionCommand != null
&& commandButton.isEnabled
&& sessionCommand.commandCode == SessionCommand.COMMAND_CODE_CUSTOM
&& CommandButton.isButtonCommandAvailable(
commandButton, availableSessionCommands, availablePlayerCommands)) {
Bundle actionExtras = sessionCommand.customExtras;
if (commandButton.icon != CommandButton.ICON_UNDEFINED) {
actionExtras = new Bundle(sessionCommand.customExtras);
actionExtras.putInt(
MediaConstants.EXTRAS_KEY_COMMAND_BUTTON_ICON_COMPAT, commandButton.icon);
}
builder.addCustomAction(
new PlaybackStateCompat.CustomAction.Builder(
sessionCommand.customAction, commandButton.displayName, commandButton.iconResId)
.setExtras(actionExtras)
.build());
}
}
if (playerError != null) {

View File

@ -31,7 +31,8 @@ import org.junit.runner.RunWith;
public class CommandButtonTest {
@Test
public void isEnabled_playerCommandAvailableOrUnavailableInPlayerCommands_isEnabledCorrectly() {
public void
isButtonCommandAvailable_playerCommandAvailableOrUnavailableInPlayerCommands_isEnabledCorrectly() {
CommandButton button =
new CommandButton.Builder()
.setDisplayName("button")
@ -41,14 +42,18 @@ public class CommandButtonTest {
Player.Commands availablePlayerCommands =
Player.Commands.EMPTY.buildUpon().add(Player.COMMAND_SEEK_TO_NEXT).build();
assertThat(CommandButton.isEnabled(button, SessionCommands.EMPTY, Player.Commands.EMPTY))
assertThat(
CommandButton.isButtonCommandAvailable(
button, SessionCommands.EMPTY, Player.Commands.EMPTY))
.isFalse();
assertThat(CommandButton.isEnabled(button, SessionCommands.EMPTY, availablePlayerCommands))
assertThat(
CommandButton.isButtonCommandAvailable(
button, SessionCommands.EMPTY, availablePlayerCommands))
.isTrue();
}
@Test
public void isEnabled_sessionCommandAvailableOrUnavailable_isEnabledCorrectly() {
public void isButtonCommandAvailable_sessionCommandAvailableOrUnavailable_isEnabledCorrectly() {
SessionCommand command1 = new SessionCommand("command1", Bundle.EMPTY);
CommandButton button =
new CommandButton.Builder()
@ -59,14 +64,18 @@ public class CommandButtonTest {
SessionCommands availableSessionCommands =
SessionCommands.EMPTY.buildUpon().add(command1).build();
assertThat(CommandButton.isEnabled(button, SessionCommands.EMPTY, Player.Commands.EMPTY))
assertThat(
CommandButton.isButtonCommandAvailable(
button, SessionCommands.EMPTY, Player.Commands.EMPTY))
.isFalse();
assertThat(CommandButton.isEnabled(button, availableSessionCommands, Player.Commands.EMPTY))
assertThat(
CommandButton.isButtonCommandAvailable(
button, availableSessionCommands, Player.Commands.EMPTY))
.isTrue();
}
@Test
public void getEnabledCommandButtons() {
public void copyWithUnavailableButtonsDisabled() {
CommandButton button1 =
new CommandButton.Builder()
.setDisplayName("button1")
@ -86,11 +95,11 @@ public class CommandButtonTest {
Player.Commands.EMPTY.buildUpon().add(Player.COMMAND_SEEK_TO_PREVIOUS).build();
assertThat(
CommandButton.getEnabledCommandButtons(
CommandButton.copyWithUnavailableButtonsDisabled(
ImmutableList.of(button1, button2), SessionCommands.EMPTY, Player.Commands.EMPTY))
.containsExactly(button1, button2);
.containsExactly(button1.copyWithIsEnabled(false), button2.copyWithIsEnabled(false));
assertThat(
CommandButton.getEnabledCommandButtons(
CommandButton.copyWithUnavailableButtonsDisabled(
ImmutableList.of(button1, button2),
availableSessionCommands,
availablePlayerCommands))
@ -134,7 +143,8 @@ public class CommandButtonTest {
.setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS)
.build();
CommandButton serialisedButton = CommandButton.fromBundle(button.toBundle());
CommandButton serialisedButton =
CommandButton.fromBundle(button.toBundle(), MediaSessionStub.VERSION_INT);
assertThat(serialisedButton.iconUri).isEqualTo(uri);
}
@ -148,7 +158,8 @@ public class CommandButtonTest {
.setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS)
.build();
CommandButton serialisedButton = CommandButton.fromBundle(button.toBundle());
CommandButton serialisedButton =
CommandButton.fromBundle(button.toBundle(), MediaSessionStub.VERSION_INT);
assertThat(serialisedButton.iconUri).isNull();
}
@ -180,7 +191,8 @@ public class CommandButtonTest {
.setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT)
.build();
assertThat(button).isEqualTo(CommandButton.fromBundle(button.toBundle()));
assertThat(button)
.isEqualTo(CommandButton.fromBundle(button.toBundle(), MediaSessionStub.VERSION_INT));
assertThat(button)
.isNotEqualTo(
new CommandButton.Builder()
@ -205,7 +217,7 @@ public class CommandButtonTest {
assertThat(button)
.isNotEqualTo(
new CommandButton.Builder()
.setEnabled(true)
.setEnabled(false)
.setDisplayName("button")
.setIconResId(R.drawable.media3_notification_small_icon)
.setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT)
@ -380,11 +392,11 @@ public class CommandButtonTest {
new CommandButton.Builder().setPlayerCommand(Player.COMMAND_PLAY_PAUSE).build();
CommandButton restoredButtonWithSessionCommand =
CommandButton.fromBundle(buttonWithSessionCommand.toBundle());
CommandButton.fromBundle(buttonWithSessionCommand.toBundle(), MediaSessionStub.VERSION_INT);
CommandButton restoredButtonWithPlayerCommand =
CommandButton.fromBundle(buttonWithPlayerCommand.toBundle());
CommandButton.fromBundle(buttonWithPlayerCommand.toBundle(), MediaSessionStub.VERSION_INT);
CommandButton restoredButtonWithDefaultValues =
CommandButton.fromBundle(buttonWithDefaultValues.toBundle());
CommandButton.fromBundle(buttonWithDefaultValues.toBundle(), MediaSessionStub.VERSION_INT);
assertThat(restoredButtonWithSessionCommand).isEqualTo(buttonWithSessionCommand);
assertThat(restoredButtonWithSessionCommand.extras.get("key")).isEqualTo("value");
@ -392,4 +404,19 @@ public class CommandButtonTest {
assertThat(restoredButtonWithPlayerCommand.extras.get("key")).isEqualTo("value");
assertThat(restoredButtonWithDefaultValues).isEqualTo(buttonWithDefaultValues);
}
@Test
public void fromBundle_withSessionInterfaceVersionLessThan3_setsEnabledToTrue() {
CommandButton buttonWithEnabledFalse =
new CommandButton.Builder()
.setEnabled(false)
.setSessionCommand(new SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING))
.build();
CommandButton restoredButtonAssumingOldSessionInterface =
CommandButton.fromBundle(
buttonWithEnabledFalse.toBundle(), /* sessionInterfaceVersion= */ 2);
assertThat(restoredButtonAssumingOldSessionInterface.isEnabled).isTrue();
}
}

View File

@ -158,6 +158,8 @@ public class MediaSessionServiceTest {
throws TimeoutException {
SessionCommand command1 = new SessionCommand("command1", Bundle.EMPTY);
SessionCommand command2 = new SessionCommand("command2", Bundle.EMPTY);
SessionCommand command3 = new SessionCommand("command3", Bundle.EMPTY);
SessionCommand command4 = new SessionCommand("command4", Bundle.EMPTY);
CommandButton button1 =
new CommandButton.Builder()
.setDisplayName("customAction1")
@ -170,10 +172,23 @@ public class MediaSessionServiceTest {
.setIconResId(R.drawable.media3_notification_small_icon)
.setSessionCommand(command2)
.build();
CommandButton button3 =
new CommandButton.Builder()
.setDisplayName("customAction3")
.setEnabled(false)
.setIconResId(R.drawable.media3_notification_small_icon)
.setSessionCommand(command3)
.build();
CommandButton button4 =
new CommandButton.Builder()
.setDisplayName("customAction4")
.setIconResId(R.drawable.media3_notification_small_icon)
.setSessionCommand(command4)
.build();
ExoPlayer player = new TestExoPlayerBuilder(context).build();
MediaSession session =
new MediaSession.Builder(context, player)
.setCustomLayout(ImmutableList.of(button1, button2))
.setCustomLayout(ImmutableList.of(button1, button2, button3, button4))
.setCallback(
new MediaSession.Callback() {
@Override
@ -186,6 +201,7 @@ public class MediaSessionServiceTest {
.buildUpon()
.add(command1)
.add(command2)
.add(command3)
.build())
.build();
}

View File

@ -1446,10 +1446,9 @@ public class MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest
Player player = createDefaultPlayer();
Bundle extras1 = new Bundle();
extras1.putString("key1", "value1");
Bundle extras2 = new Bundle();
extras1.putString("key2", "value2");
SessionCommand command1 = new SessionCommand("command1", extras1);
SessionCommand command2 = new SessionCommand("command2", extras2);
SessionCommand command2 = new SessionCommand("command2", Bundle.EMPTY);
SessionCommand command3 = new SessionCommand("command3", Bundle.EMPTY);
ImmutableList<CommandButton> customLayout =
ImmutableList.of(
new CommandButton.Builder()
@ -1461,6 +1460,12 @@ public class MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest
.setDisplayName("button2")
.setIconResId(R.drawable.media3_notification_pause)
.setSessionCommand(command2)
.build(),
new CommandButton.Builder()
.setDisplayName("button3")
.setEnabled(false)
.setIconResId(R.drawable.media3_notification_pause)
.setSessionCommand(command3)
.build());
MediaSession.Callback callback =
new MediaSession.Callback() {
@ -1469,7 +1474,11 @@ public class MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest
MediaSession session, MediaSession.ControllerInfo controller) {
return new AcceptedResultBuilder(session)
.setAvailableSessionCommands(
ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().add(command1).build())
ConnectionResult.DEFAULT_SESSION_COMMANDS
.buildUpon()
.add(command1)
.add(command3)
.build())
.build();
}
};

View File

@ -123,32 +123,31 @@ public class MediaControllerTest {
@Test
public void builder() throws Exception {
MediaController.Builder builder;
SessionToken token = remoteSession.getToken();
try {
builder = new MediaController.Builder(null, token);
new MediaController.Builder(null, token);
assertWithMessage("null context shouldn't be allowed").fail();
} catch (NullPointerException e) {
// expected. pass-through
}
try {
builder = new MediaController.Builder(context, null);
new MediaController.Builder(context, null);
assertWithMessage("null token shouldn't be allowed").fail();
} catch (NullPointerException e) {
// expected. pass-through
}
try {
builder = new MediaController.Builder(context, token).setListener(null);
new MediaController.Builder(context, token).setListener(null);
assertWithMessage("null listener shouldn't be allowed").fail();
} catch (NullPointerException e) {
// expected. pass-through
}
try {
builder = new MediaController.Builder(context, token).setApplicationLooper(null);
new MediaController.Builder(context, token).setApplicationLooper(null);
assertWithMessage("null looper shouldn't be allowed").fail();
} catch (NullPointerException e) {
// expected. pass-through
@ -182,6 +181,7 @@ public class MediaControllerTest {
CommandButton button2 =
new CommandButton.Builder()
.setDisplayName("button2")
.setEnabled(false)
.setIconResId(R.drawable.media3_notification_small_icon)
.setSessionCommand(new SessionCommand("command2", Bundle.EMPTY))
.build();
@ -191,11 +191,28 @@ public class MediaControllerTest {
.setIconResId(R.drawable.media3_notification_small_icon)
.setSessionCommand(new SessionCommand("command3", Bundle.EMPTY))
.build();
setupCustomLayout(session, ImmutableList.of(button1, button2, button3));
CommandButton button4 =
new CommandButton.Builder()
.setDisplayName("button4")
.setIconResId(R.drawable.media3_notification_small_icon)
.setPlayerCommand(Player.COMMAND_PLAY_PAUSE)
.build();
CommandButton button5 =
new CommandButton.Builder()
.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::getCustomLayout))
.containsExactly(button1.copyWithIsEnabled(true), button2.copyWithIsEnabled(true), button3)
.containsExactly(
button1.copyWithIsEnabled(true),
button2.copyWithIsEnabled(false),
button3.copyWithIsEnabled(false),
button4.copyWithIsEnabled(true),
button5.copyWithIsEnabled(false))
.inOrder();
session.cleanUp();
@ -214,6 +231,7 @@ public class MediaControllerTest {
CommandButton button2 =
new CommandButton.Builder()
.setDisplayName("button2")
.setEnabled(false)
.setIconResId(R.drawable.media3_notification_small_icon)
.setSessionCommand(new SessionCommand("command2", Bundle.EMPTY))
.build();
@ -229,7 +247,19 @@ public class MediaControllerTest {
.setIconResId(R.drawable.media3_notification_small_icon)
.setSessionCommand(new SessionCommand("command4", Bundle.EMPTY))
.build();
setupCustomLayout(session, ImmutableList.of(button1, button2));
CommandButton button5 =
new CommandButton.Builder()
.setDisplayName("button5")
.setIconResId(R.drawable.media3_notification_small_icon)
.setPlayerCommand(Player.COMMAND_PLAY_PAUSE)
.build();
CommandButton button6 =
new CommandButton.Builder()
.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(2);
AtomicReference<List<CommandButton>> reportedCustomLayout = new AtomicReference<>();
AtomicReference<List<CommandButton>> reportedCustomLayoutChanged = new AtomicReference<>();
@ -255,23 +285,32 @@ public class MediaControllerTest {
});
ImmutableList<CommandButton> initialCustomLayoutFromGetter =
threadTestRule.getHandler().postAndSync(controller::getCustomLayout);
session.setCustomLayout(ImmutableList.of(button3, button4));
session.setCustomLayout(ImmutableList.of(button1, button2, button4, button5, button6));
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
ImmutableList<CommandButton> newCustomLayoutFromGetter =
threadTestRule.getHandler().postAndSync(controller::getCustomLayout);
assertThat(initialCustomLayoutFromGetter)
.containsExactly(button1.copyWithIsEnabled(true), button2.copyWithIsEnabled(true))
.containsExactly(button1.copyWithIsEnabled(true), button3.copyWithIsEnabled(false))
.inOrder();
ImmutableList<CommandButton> expectedNewButtons =
ImmutableList.of(
button1.copyWithIsEnabled(true),
button2.copyWithIsEnabled(false),
button4.copyWithIsEnabled(false),
button5.copyWithIsEnabled(true),
button6.copyWithIsEnabled(false));
assertThat(newCustomLayoutFromGetter).containsExactlyElementsIn(expectedNewButtons).inOrder();
assertThat(reportedCustomLayout.get()).containsExactlyElementsIn(expectedNewButtons).inOrder();
assertThat(reportedCustomLayoutChanged.get())
.containsExactlyElementsIn(expectedNewButtons)
.inOrder();
assertThat(newCustomLayoutFromGetter).containsExactly(button3, button4).inOrder();
assertThat(reportedCustomLayout.get()).containsExactly(button3, button4).inOrder();
assertThat(reportedCustomLayoutChanged.get()).containsExactly(button3, button4).inOrder();
session.cleanUp();
}
@Test
public void getCustomLayout_setAvailableCommandsAddOrRemoveCommands_reportsCustomLayoutChanged()
public void getCustomLayout_setAvailableCommandsOnSession_reportsCustomLayoutChanged()
throws Exception {
RemoteMediaSession session = createRemoteMediaSession(TEST_GET_CUSTOM_LAYOUT, null);
CommandButton button1 =
@ -283,10 +322,23 @@ public class MediaControllerTest {
CommandButton button2 =
new CommandButton.Builder()
.setDisplayName("button2")
.setEnabled(false)
.setIconResId(R.drawable.media3_notification_small_icon)
.setSessionCommand(new SessionCommand("command2", Bundle.EMPTY))
.build();
setupCustomLayout(session, ImmutableList.of(button1, button2));
CommandButton button3 =
new CommandButton.Builder()
.setDisplayName("button3")
.setIconResId(R.drawable.media3_notification_small_icon)
.setPlayerCommand(Player.COMMAND_PLAY_PAUSE)
.build();
CommandButton button4 =
new CommandButton.Builder()
.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<List<CommandButton>> reportedCustomLayoutChanged = new ArrayList<>();
List<List<CommandButton>> getterCustomLayoutChanged = new ArrayList<>();
@ -308,23 +360,95 @@ public class MediaControllerTest {
// Remove commands in custom layout from available commands.
session.setAvailableCommands(SessionCommands.EMPTY, Player.Commands.EMPTY);
// Add one command back.
// Add one sesion and player command back.
session.setAvailableCommands(
new SessionCommands.Builder().add(button2.sessionCommand).build(), Player.Commands.EMPTY);
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(button1.copyWithIsEnabled(true), button2.copyWithIsEnabled(true));
.containsExactly(
button1.copyWithIsEnabled(true),
button2.copyWithIsEnabled(false),
button3.copyWithIsEnabled(true),
button4.copyWithIsEnabled(false));
assertThat(reportedCustomLayoutChanged).hasSize(2);
assertThat(reportedCustomLayoutChanged.get(0)).containsExactly(button1, button2).inOrder();
assertThat(reportedCustomLayoutChanged.get(0))
.containsExactly(
button1.copyWithIsEnabled(false),
button2.copyWithIsEnabled(false),
button3.copyWithIsEnabled(false),
button4.copyWithIsEnabled(false))
.inOrder();
assertThat(reportedCustomLayoutChanged.get(1))
.containsExactly(button1, button2.copyWithIsEnabled(true))
.containsExactly(
button1.copyWithIsEnabled(false),
button2.copyWithIsEnabled(false),
button3.copyWithIsEnabled(false),
button4.copyWithIsEnabled(true))
.inOrder();
assertThat(getterCustomLayoutChanged).hasSize(2);
assertThat(getterCustomLayoutChanged.get(0)).containsExactly(button1, button2).inOrder();
assertThat(getterCustomLayoutChanged.get(1))
.containsExactly(button1, button2.copyWithIsEnabled(true))
assertThat(getterCustomLayoutChanged.get(0))
.containsExactly(
button1.copyWithIsEnabled(false),
button2.copyWithIsEnabled(false),
button3.copyWithIsEnabled(false),
button4.copyWithIsEnabled(false))
.inOrder();
assertThat(getterCustomLayoutChanged.get(1))
.containsExactly(
button1.copyWithIsEnabled(false),
button2.copyWithIsEnabled(false),
button3.copyWithIsEnabled(false),
button4.copyWithIsEnabled(true))
.inOrder();
}
@Test
public void getCustomLayout_setAvailableCommandsOnPlayer_reportsCustomLayoutChanged()
throws Exception {
RemoteMediaSession session = createRemoteMediaSession(TEST_GET_CUSTOM_LAYOUT, null);
CommandButton button =
new CommandButton.Builder()
.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<List<CommandButton>> reportedCustomLayouts = new ArrayList<>();
List<List<CommandButton>> getterCustomLayouts = new ArrayList<>();
MediaController.Listener listener =
new MediaController.Listener() {
@Override
public void onCustomLayoutChanged(
MediaController controller, List<CommandButton> layout) {
reportedCustomLayouts.add(layout);
getterCustomLayouts.add(controller.getCustomLayout());
latch.countDown();
}
};
MediaController controller =
controllerTestRule.createController(
session.getToken(), /* connectionHints= */ Bundle.EMPTY, listener);
ImmutableList<CommandButton> 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(button.copyWithIsEnabled(true));
assertThat(reportedCustomLayouts).hasSize(2);
assertThat(reportedCustomLayouts.get(0)).containsExactly(button.copyWithIsEnabled(false));
assertThat(reportedCustomLayouts.get(1)).containsExactly(button.copyWithIsEnabled(true));
assertThat(getterCustomLayouts).hasSize(2);
assertThat(getterCustomLayouts.get(0)).containsExactly(button.copyWithIsEnabled(false));
assertThat(getterCustomLayouts.get(1)).containsExactly(button.copyWithIsEnabled(true));
}
@Test
@ -341,6 +465,7 @@ public class MediaControllerTest {
CommandButton button2 =
new CommandButton.Builder()
.setDisplayName("button2")
.setEnabled(false)
.setIconResId(R.drawable.media3_notification_small_icon)
.setSessionCommand(new SessionCommand("command2", Bundle.EMPTY))
.build();
@ -393,27 +518,31 @@ public class MediaControllerTest {
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
CommandButton button1Enabled = button1.copyWithIsEnabled(true);
CommandButton button2Enabled = button2.copyWithIsEnabled(true);
assertThat(initialCustomLayout).containsExactly(button1Enabled, button2Enabled).inOrder();
CommandButton button2Disabled = button2.copyWithIsEnabled(false);
CommandButton button3Disabled = button3.copyWithIsEnabled(false);
CommandButton button4Disabled = button4.copyWithIsEnabled(false);
assertThat(initialCustomLayout).containsExactly(button1Enabled, button2Disabled).inOrder();
assertThat(reportedCustomLayout)
.containsExactly(
ImmutableList.of(button1Enabled, button2Enabled),
ImmutableList.of(button3, button4),
ImmutableList.of(button1Enabled, button2Enabled))
ImmutableList.of(button1Enabled, button2Disabled),
ImmutableList.of(button3Disabled, button4Disabled),
ImmutableList.of(button1Enabled, button2Disabled))
.inOrder();
assertThat(getterCustomLayout)
.containsExactly(
ImmutableList.of(button1Enabled, button2Enabled),
ImmutableList.of(button3, button4),
ImmutableList.of(button1Enabled, button2Enabled))
ImmutableList.of(button1Enabled, button2Disabled),
ImmutableList.of(button3Disabled, button4Disabled),
ImmutableList.of(button1Enabled, button2Disabled))
.inOrder();
assertThat(reportedCustomLayoutChanged)
.containsExactly(
ImmutableList.of(button3, button4), ImmutableList.of(button1Enabled, button2Enabled))
ImmutableList.of(button3Disabled, button4Disabled),
ImmutableList.of(button1Enabled, button2Disabled))
.inOrder();
assertThat(getterCustomLayoutChanged)
.containsExactly(
ImmutableList.of(button3, button4), ImmutableList.of(button1Enabled, button2Enabled))
ImmutableList.of(button3Disabled, button4Disabled),
ImmutableList.of(button1Enabled, button2Disabled))
.inOrder();
session.cleanUp();
}

View File

@ -319,7 +319,9 @@ public class MediaSessionServiceTest {
.isTrue();
assertThat(mediaControllerCompat.getPlaybackState().getActions())
.isEqualTo(PlaybackStateCompat.ACTION_SET_RATING);
assertThat(remoteController.getCustomLayout()).containsExactly(button1, button2).inOrder();
assertThat(remoteController.getCustomLayout())
.containsExactly(button1.copyWithIsEnabled(false), button2.copyWithIsEnabled(false))
.inOrder();
assertThat(initialCustomLayoutInControllerCompat).isEmpty();
assertThat(LegacyConversions.convertToCustomLayout(mediaControllerCompat.getPlaybackState()))
.containsExactly(button1.copyWithIsEnabled(true), button3.copyWithIsEnabled(true))

View File

@ -207,7 +207,7 @@ public class MediaSessionProviderService extends Service {
.add(new SessionCommand("command1", Bundle.EMPTY))
.add(new SessionCommand("command2", Bundle.EMPTY))
.build(),
Player.Commands.EMPTY);
new Player.Commands.Builder().add(Player.COMMAND_PLAY_PAUSE).build());
}
});
break;
@ -549,7 +549,7 @@ public class MediaSessionProviderService extends Service {
() -> {
ImmutableList.Builder<CommandButton> builder = new ImmutableList.Builder<>();
for (Bundle bundle : layout) {
builder.add(CommandButton.fromBundle(bundle));
builder.add(CommandButton.fromBundle(bundle, MediaSessionStub.VERSION_INT));
}
MediaSession session = sessionMap.get(sessionId);
session.setCustomLayout(builder.build());

View File

@ -381,7 +381,7 @@ public class RemoteMediaController {
ArrayList<Bundle> list = customLayoutBundle.getParcelableArrayList(KEY_COMMAND_BUTTON_LIST);
ImmutableList.Builder<CommandButton> customLayout = new ImmutableList.Builder<>();
for (Bundle bundle : list) {
customLayout.add(CommandButton.fromBundle(bundle));
customLayout.add(CommandButton.fromBundle(bundle, MediaSessionStub.VERSION_INT));
}
return customLayout.build();
}