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 * Fix issue where the current position jumps back when the controller
replaces the current item replaces the current item
([#951](https://github.com/androidx/media/issues/951)). ([#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: * UI:
* Fallback to include audio track language name if `Locale` cannot * Fallback to include audio track language name if `Locale` cannot
identify a display name identify a display name

View File

@ -412,6 +412,7 @@ public final class CommandButton implements Bundleable {
extras = Bundle.EMPTY; extras = Bundle.EMPTY;
playerCommand = Player.COMMAND_INVALID; playerCommand = Player.COMMAND_INVALID;
icon = ICON_UNDEFINED; icon = ICON_UNDEFINED;
enabled = true;
} }
/** /**
@ -507,6 +508,12 @@ public final class CommandButton implements Bundleable {
/** /**
* Sets whether the button is enabled. * 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. * @param enabled Whether the button is enabled.
* @return This builder for chaining. * @return This builder for chaining.
*/ */
@ -571,7 +578,13 @@ public final class CommandButton implements Bundleable {
*/ */
@UnstableApi public final Bundle extras; @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; public final boolean isEnabled;
private CommandButton( 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 * Returns a list of command buttons with the {@link CommandButton#isEnabled} flag set to false if
* to the available commands passed in. * the corresponding command is not available.
*/ */
/* package */ static ImmutableList<CommandButton> getEnabledCommandButtons( /* package */ static ImmutableList<CommandButton> copyWithUnavailableButtonsDisabled(
List<CommandButton> commandButtons, List<CommandButton> commandButtons,
SessionCommands sessionCommands, SessionCommands sessionCommands,
Player.Commands playerCommands) { 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++) { for (int i = 0; i < commandButtons.size(); i++) {
CommandButton button = commandButtons.get(i); CommandButton button = commandButtons.get(i);
enabledButtons.add( if (isButtonCommandAvailable(button, sessionCommands, playerCommands)) {
button.copyWithIsEnabled(isEnabled(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 button The command button.
* @param sessionCommands The available session commands. * @param sessionCommands The available session commands.
* @param playerCommands The available player 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) { CommandButton button, SessionCommands sessionCommands, Player.Commands playerCommands) {
return (button.sessionCommand != null && sessionCommands.contains(button.sessionCommand)) return (button.sessionCommand != null && sessionCommands.contains(button.sessionCommand))
|| (button.playerCommand != Player.COMMAND_INVALID || (button.playerCommand != Player.COMMAND_INVALID
@ -706,7 +723,7 @@ public final class CommandButton implements Bundleable {
if (iconUri != null) { if (iconUri != null) {
bundle.putParcelable(FIELD_ICON_URI, iconUri); bundle.putParcelable(FIELD_ICON_URI, iconUri);
} }
if (isEnabled) { if (!isEnabled) {
bundle.putBoolean(FIELD_ENABLED, isEnabled); bundle.putBoolean(FIELD_ENABLED, isEnabled);
} }
return bundle; return bundle;
@ -722,9 +739,18 @@ public final class CommandButton implements Bundleable {
@SuppressWarnings("deprecation") // Deprecated instance of deprecated class @SuppressWarnings("deprecation") // Deprecated instance of deprecated class
public static final Creator<CommandButton> CREATOR = CommandButton::fromBundle; 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 @UnstableApi
public static CommandButton fromBundle(Bundle bundle) { 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 Bundle sessionCommandBundle = bundle.getBundle(FIELD_SESSION_COMMAND);
@Nullable @Nullable
SessionCommand sessionCommand = SessionCommand sessionCommand =
@ -735,7 +761,10 @@ public final class CommandButton implements Bundleable {
int iconResId = bundle.getInt(FIELD_ICON_RES_ID, /* defaultValue= */ 0); int iconResId = bundle.getInt(FIELD_ICON_RES_ID, /* defaultValue= */ 0);
CharSequence displayName = bundle.getCharSequence(FIELD_DISPLAY_NAME, /* defaultValue= */ ""); CharSequence displayName = bundle.getCharSequence(FIELD_DISPLAY_NAME, /* defaultValue= */ "");
@Nullable Bundle extras = bundle.getBundle(FIELD_EXTRAS); @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); @Nullable Uri iconUri = bundle.getParcelable(FIELD_ICON_URI);
@Icon int icon = bundle.getInt(FIELD_ICON, /* defaultValue= */ ICON_UNDEFINED); @Icon int icon = bundle.getInt(FIELD_ICON, /* defaultValue= */ ICON_UNDEFINED);
Builder builder = new Builder(); Builder builder = new Builder();

View File

@ -164,7 +164,8 @@ import java.util.List;
List<Bundle> commandButtonArrayList = bundle.getParcelableArrayList(FIELD_CUSTOM_LAYOUT); List<Bundle> commandButtonArrayList = bundle.getParcelableArrayList(FIELD_CUSTOM_LAYOUT);
ImmutableList<CommandButton> customLayout = ImmutableList<CommandButton> customLayout =
commandButtonArrayList != null commandButtonArrayList != null
? BundleCollectionUtil.fromBundleList(CommandButton::fromBundle, commandButtonArrayList) ? BundleCollectionUtil.fromBundleList(
b -> CommandButton.fromBundle(b, sessionInterfaceVersion), commandButtonArrayList)
: ImmutableList.of(); : ImmutableList.of();
@Nullable Bundle sessionCommandsBundle = bundle.getBundle(FIELD_SESSION_COMMANDS); @Nullable Bundle sessionCommandsBundle = bundle.getBundle(FIELD_SESSION_COMMANDS);
SessionCommands sessionCommands = 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 * changes the available commands} for a controller that affect whether buttons of the custom
* layout are enabled or disabled. * 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 controller The controller.
* @param layout The ordered list of {@linkplain CommandButton command buttons}. * @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 * <p>After being connected, a change of the custom layout is reported with {@link
* Listener#onCustomLayoutChanged(MediaController, List)}. * 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. * @return The custom layout.
*/ */
@UnstableApi @UnstableApi

View File

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

View File

@ -112,8 +112,18 @@ import org.checkerframework.checker.nullness.qual.NonNull;
} }
List<CommandButton> layout; List<CommandButton> layout;
try { 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 = layout =
BundleCollectionUtil.fromBundleList(CommandButton::fromBundle, commandButtonBundleList); BundleCollectionUtil.fromBundleList(
bundle -> CommandButton.fromBundle(bundle, sessionInterfaceVersion),
commandButtonBundleList);
} catch (RuntimeException e) { } catch (RuntimeException e) {
Log.w(TAG, "Ignoring malformed Bundle for CommandButton", e); Log.w(TAG, "Ignoring malformed Bundle for CommandButton", e);
return; return;

View File

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

View File

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

View File

@ -1043,10 +1043,11 @@ import java.util.List;
for (int i = 0; i < customLayout.size(); i++) { for (int i = 0; i < customLayout.size(); i++) {
CommandButton commandButton = customLayout.get(i); CommandButton commandButton = customLayout.get(i);
if (commandButton.sessionCommand != null) {
SessionCommand sessionCommand = commandButton.sessionCommand; SessionCommand sessionCommand = commandButton.sessionCommand;
if (sessionCommand.commandCode == SessionCommand.COMMAND_CODE_CUSTOM if (sessionCommand != null
&& CommandButton.isEnabled( && commandButton.isEnabled
&& sessionCommand.commandCode == SessionCommand.COMMAND_CODE_CUSTOM
&& CommandButton.isButtonCommandAvailable(
commandButton, availableSessionCommands, availablePlayerCommands)) { commandButton, availableSessionCommands, availablePlayerCommands)) {
Bundle actionExtras = sessionCommand.customExtras; Bundle actionExtras = sessionCommand.customExtras;
if (commandButton.icon != CommandButton.ICON_UNDEFINED) { if (commandButton.icon != CommandButton.ICON_UNDEFINED) {
@ -1056,14 +1057,11 @@ import java.util.List;
} }
builder.addCustomAction( builder.addCustomAction(
new PlaybackStateCompat.CustomAction.Builder( new PlaybackStateCompat.CustomAction.Builder(
sessionCommand.customAction, sessionCommand.customAction, commandButton.displayName, commandButton.iconResId)
commandButton.displayName,
commandButton.iconResId)
.setExtras(actionExtras) .setExtras(actionExtras)
.build()); .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

@ -31,7 +31,8 @@ import org.junit.runner.RunWith;
public class CommandButtonTest { public class CommandButtonTest {
@Test @Test
public void isEnabled_playerCommandAvailableOrUnavailableInPlayerCommands_isEnabledCorrectly() { public void
isButtonCommandAvailable_playerCommandAvailableOrUnavailableInPlayerCommands_isEnabledCorrectly() {
CommandButton button = CommandButton button =
new CommandButton.Builder() new CommandButton.Builder()
.setDisplayName("button") .setDisplayName("button")
@ -41,14 +42,18 @@ public class CommandButtonTest {
Player.Commands availablePlayerCommands = Player.Commands availablePlayerCommands =
Player.Commands.EMPTY.buildUpon().add(Player.COMMAND_SEEK_TO_NEXT).build(); 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(); .isFalse();
assertThat(CommandButton.isEnabled(button, SessionCommands.EMPTY, availablePlayerCommands)) assertThat(
CommandButton.isButtonCommandAvailable(
button, SessionCommands.EMPTY, availablePlayerCommands))
.isTrue(); .isTrue();
} }
@Test @Test
public void isEnabled_sessionCommandAvailableOrUnavailable_isEnabledCorrectly() { public void isButtonCommandAvailable_sessionCommandAvailableOrUnavailable_isEnabledCorrectly() {
SessionCommand command1 = new SessionCommand("command1", Bundle.EMPTY); SessionCommand command1 = new SessionCommand("command1", Bundle.EMPTY);
CommandButton button = CommandButton button =
new CommandButton.Builder() new CommandButton.Builder()
@ -59,14 +64,18 @@ public class CommandButtonTest {
SessionCommands availableSessionCommands = SessionCommands availableSessionCommands =
SessionCommands.EMPTY.buildUpon().add(command1).build(); 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(); .isFalse();
assertThat(CommandButton.isEnabled(button, availableSessionCommands, Player.Commands.EMPTY)) assertThat(
CommandButton.isButtonCommandAvailable(
button, availableSessionCommands, Player.Commands.EMPTY))
.isTrue(); .isTrue();
} }
@Test @Test
public void getEnabledCommandButtons() { public void copyWithUnavailableButtonsDisabled() {
CommandButton button1 = CommandButton button1 =
new CommandButton.Builder() new CommandButton.Builder()
.setDisplayName("button1") .setDisplayName("button1")
@ -86,11 +95,11 @@ public class CommandButtonTest {
Player.Commands.EMPTY.buildUpon().add(Player.COMMAND_SEEK_TO_PREVIOUS).build(); Player.Commands.EMPTY.buildUpon().add(Player.COMMAND_SEEK_TO_PREVIOUS).build();
assertThat( assertThat(
CommandButton.getEnabledCommandButtons( CommandButton.copyWithUnavailableButtonsDisabled(
ImmutableList.of(button1, button2), SessionCommands.EMPTY, Player.Commands.EMPTY)) ImmutableList.of(button1, button2), SessionCommands.EMPTY, Player.Commands.EMPTY))
.containsExactly(button1, button2); .containsExactly(button1.copyWithIsEnabled(false), button2.copyWithIsEnabled(false));
assertThat( assertThat(
CommandButton.getEnabledCommandButtons( CommandButton.copyWithUnavailableButtonsDisabled(
ImmutableList.of(button1, button2), ImmutableList.of(button1, button2),
availableSessionCommands, availableSessionCommands,
availablePlayerCommands)) availablePlayerCommands))
@ -134,7 +143,8 @@ public class CommandButtonTest {
.setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS) .setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS)
.build(); .build();
CommandButton serialisedButton = CommandButton.fromBundle(button.toBundle()); CommandButton serialisedButton =
CommandButton.fromBundle(button.toBundle(), MediaSessionStub.VERSION_INT);
assertThat(serialisedButton.iconUri).isEqualTo(uri); assertThat(serialisedButton.iconUri).isEqualTo(uri);
} }
@ -148,7 +158,8 @@ public class CommandButtonTest {
.setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS) .setPlayerCommand(Player.COMMAND_SEEK_TO_PREVIOUS)
.build(); .build();
CommandButton serialisedButton = CommandButton.fromBundle(button.toBundle()); CommandButton serialisedButton =
CommandButton.fromBundle(button.toBundle(), MediaSessionStub.VERSION_INT);
assertThat(serialisedButton.iconUri).isNull(); assertThat(serialisedButton.iconUri).isNull();
} }
@ -180,7 +191,8 @@ public class CommandButtonTest {
.setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT) .setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT)
.build(); .build();
assertThat(button).isEqualTo(CommandButton.fromBundle(button.toBundle())); assertThat(button)
.isEqualTo(CommandButton.fromBundle(button.toBundle(), MediaSessionStub.VERSION_INT));
assertThat(button) assertThat(button)
.isNotEqualTo( .isNotEqualTo(
new CommandButton.Builder() new CommandButton.Builder()
@ -205,7 +217,7 @@ public class CommandButtonTest {
assertThat(button) assertThat(button)
.isNotEqualTo( .isNotEqualTo(
new CommandButton.Builder() new CommandButton.Builder()
.setEnabled(true) .setEnabled(false)
.setDisplayName("button") .setDisplayName("button")
.setIconResId(R.drawable.media3_notification_small_icon) .setIconResId(R.drawable.media3_notification_small_icon)
.setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT) .setPlayerCommand(Player.COMMAND_SEEK_TO_NEXT)
@ -380,11 +392,11 @@ public class CommandButtonTest {
new CommandButton.Builder().setPlayerCommand(Player.COMMAND_PLAY_PAUSE).build(); new CommandButton.Builder().setPlayerCommand(Player.COMMAND_PLAY_PAUSE).build();
CommandButton restoredButtonWithSessionCommand = CommandButton restoredButtonWithSessionCommand =
CommandButton.fromBundle(buttonWithSessionCommand.toBundle()); CommandButton.fromBundle(buttonWithSessionCommand.toBundle(), MediaSessionStub.VERSION_INT);
CommandButton restoredButtonWithPlayerCommand = CommandButton restoredButtonWithPlayerCommand =
CommandButton.fromBundle(buttonWithPlayerCommand.toBundle()); CommandButton.fromBundle(buttonWithPlayerCommand.toBundle(), MediaSessionStub.VERSION_INT);
CommandButton restoredButtonWithDefaultValues = CommandButton restoredButtonWithDefaultValues =
CommandButton.fromBundle(buttonWithDefaultValues.toBundle()); CommandButton.fromBundle(buttonWithDefaultValues.toBundle(), MediaSessionStub.VERSION_INT);
assertThat(restoredButtonWithSessionCommand).isEqualTo(buttonWithSessionCommand); assertThat(restoredButtonWithSessionCommand).isEqualTo(buttonWithSessionCommand);
assertThat(restoredButtonWithSessionCommand.extras.get("key")).isEqualTo("value"); assertThat(restoredButtonWithSessionCommand.extras.get("key")).isEqualTo("value");
@ -392,4 +404,19 @@ public class CommandButtonTest {
assertThat(restoredButtonWithPlayerCommand.extras.get("key")).isEqualTo("value"); assertThat(restoredButtonWithPlayerCommand.extras.get("key")).isEqualTo("value");
assertThat(restoredButtonWithDefaultValues).isEqualTo(buttonWithDefaultValues); 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 { throws TimeoutException {
SessionCommand command1 = new SessionCommand("command1", Bundle.EMPTY); SessionCommand command1 = new SessionCommand("command1", Bundle.EMPTY);
SessionCommand command2 = new SessionCommand("command2", 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 = CommandButton button1 =
new CommandButton.Builder() new CommandButton.Builder()
.setDisplayName("customAction1") .setDisplayName("customAction1")
@ -170,10 +172,23 @@ public class MediaSessionServiceTest {
.setIconResId(R.drawable.media3_notification_small_icon) .setIconResId(R.drawable.media3_notification_small_icon)
.setSessionCommand(command2) .setSessionCommand(command2)
.build(); .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(); ExoPlayer player = new TestExoPlayerBuilder(context).build();
MediaSession session = MediaSession session =
new MediaSession.Builder(context, player) new MediaSession.Builder(context, player)
.setCustomLayout(ImmutableList.of(button1, button2)) .setCustomLayout(ImmutableList.of(button1, button2, button3, button4))
.setCallback( .setCallback(
new MediaSession.Callback() { new MediaSession.Callback() {
@Override @Override
@ -186,6 +201,7 @@ public class MediaSessionServiceTest {
.buildUpon() .buildUpon()
.add(command1) .add(command1)
.add(command2) .add(command2)
.add(command3)
.build()) .build())
.build(); .build();
} }

View File

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

View File

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

View File

@ -319,7 +319,9 @@ public class MediaSessionServiceTest {
.isTrue(); .isTrue();
assertThat(mediaControllerCompat.getPlaybackState().getActions()) assertThat(mediaControllerCompat.getPlaybackState().getActions())
.isEqualTo(PlaybackStateCompat.ACTION_SET_RATING); .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(initialCustomLayoutInControllerCompat).isEmpty();
assertThat(LegacyConversions.convertToCustomLayout(mediaControllerCompat.getPlaybackState())) assertThat(LegacyConversions.convertToCustomLayout(mediaControllerCompat.getPlaybackState()))
.containsExactly(button1.copyWithIsEnabled(true), button3.copyWithIsEnabled(true)) .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("command1", Bundle.EMPTY))
.add(new SessionCommand("command2", Bundle.EMPTY)) .add(new SessionCommand("command2", Bundle.EMPTY))
.build(), .build(),
Player.Commands.EMPTY); new Player.Commands.Builder().add(Player.COMMAND_PLAY_PAUSE).build());
} }
}); });
break; break;
@ -549,7 +549,7 @@ public class MediaSessionProviderService extends Service {
() -> { () -> {
ImmutableList.Builder<CommandButton> builder = new ImmutableList.Builder<>(); ImmutableList.Builder<CommandButton> builder = new ImmutableList.Builder<>();
for (Bundle bundle : layout) { for (Bundle bundle : layout) {
builder.add(CommandButton.fromBundle(bundle)); builder.add(CommandButton.fromBundle(bundle, MediaSessionStub.VERSION_INT));
} }
MediaSession session = sessionMap.get(sessionId); MediaSession session = sessionMap.get(sessionId);
session.setCustomLayout(builder.build()); session.setCustomLayout(builder.build());

View File

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