Add media item command buttons for Media3 controllers
Note that unlike the legacy implementation, custom media items commands can be used for any media items with Media3 API. This includes `MediaItem` instances that are received from sources different to `MediaLibraryService` methods. Hence when connected against a Media3 session these custom commands can be used with a `MediaController` as well as with a `MediaBrowser`. Interoperability with `MediaBrowserServiceCompat` will be added in a follow up CL. Issue: androidx/media#1474 #cherrypick PiperOrigin-RevId: 678782860
This commit is contained in:
parent
0ea63e3fa6
commit
686c3fe7f5
@ -88,6 +88,11 @@
|
||||
* Handle `IllegalArgumentException` thrown by devices of certain
|
||||
manufacturers when setting the broadcast receiver for media button
|
||||
intents ([#1730](https://github.com/androidx/media/issues/1730)).
|
||||
* Add command buttons for media items. This adds the Media3 API for what
|
||||
was known as `Custom browse actions` in the legacy world. No
|
||||
interoperability with the legacy API is provided with this change. See
|
||||
<a href="https://developer.android.com/training/cars/media#custom_browse_actions">Custom
|
||||
Browse actions of AAOS</a>.
|
||||
* UI:
|
||||
* Make the stretched/cropped video in
|
||||
`PlayerView`-in-Compose-`AndroidView` workaround opt-in, due to issues
|
||||
|
@ -30,6 +30,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
@ -85,8 +86,11 @@ public final class MediaMetadata {
|
||||
@Nullable private CharSequence station;
|
||||
@Nullable private @MediaType Integer mediaType;
|
||||
@Nullable private Bundle extras;
|
||||
private ImmutableList<String> supportedCommands;
|
||||
|
||||
public Builder() {}
|
||||
public Builder() {
|
||||
supportedCommands = ImmutableList.of();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // Assigning from deprecated fields.
|
||||
private Builder(MediaMetadata mediaMetadata) {
|
||||
@ -123,6 +127,7 @@ public final class MediaMetadata {
|
||||
this.compilation = mediaMetadata.compilation;
|
||||
this.station = mediaMetadata.station;
|
||||
this.mediaType = mediaMetadata.mediaType;
|
||||
this.supportedCommands = mediaMetadata.supportedCommands;
|
||||
this.extras = mediaMetadata.extras;
|
||||
}
|
||||
|
||||
@ -440,6 +445,17 @@ public final class MediaMetadata {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the IDs of the supported commands (see for instance {@code
|
||||
* CommandButton.sessionCommand.customAction} of the Media3 session module).
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
public Builder setSupportedCommands(List<String> supportedCommands) {
|
||||
this.supportedCommands = ImmutableList.copyOf(supportedCommands);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all fields supported by the {@link Metadata.Entry entries} within the {@link Metadata}.
|
||||
*
|
||||
@ -1123,6 +1139,12 @@ public final class MediaMetadata {
|
||||
*/
|
||||
@Nullable public final Bundle extras;
|
||||
|
||||
/**
|
||||
* The IDs of the supported commands of this media item (see for instance {@code
|
||||
* CommandButton.sessionCommand.customAction} of the Media3 session module).
|
||||
*/
|
||||
@UnstableApi public final ImmutableList<String> supportedCommands;
|
||||
|
||||
@SuppressWarnings("deprecation") // Assigning deprecated fields.
|
||||
private MediaMetadata(Builder builder) {
|
||||
// Handle compatibility for deprecated fields.
|
||||
@ -1175,6 +1197,7 @@ public final class MediaMetadata {
|
||||
this.compilation = builder.compilation;
|
||||
this.station = builder.station;
|
||||
this.mediaType = mediaType;
|
||||
this.supportedCommands = builder.supportedCommands;
|
||||
this.extras = builder.extras;
|
||||
}
|
||||
|
||||
|
@ -60,12 +60,15 @@ import java.util.List;
|
||||
|
||||
@Nullable public final Token platformToken;
|
||||
|
||||
public final ImmutableList<CommandButton> commandButtonsForMediaItems;
|
||||
|
||||
public ConnectionState(
|
||||
int libraryVersion,
|
||||
int sessionInterfaceVersion,
|
||||
IMediaSession sessionBinder,
|
||||
@Nullable PendingIntent sessionActivity,
|
||||
ImmutableList<CommandButton> customLayout,
|
||||
ImmutableList<CommandButton> commandButtonsForMediaItems,
|
||||
SessionCommands sessionCommands,
|
||||
Player.Commands playerCommandsFromSession,
|
||||
Player.Commands playerCommandsFromPlayer,
|
||||
@ -78,6 +81,7 @@ import java.util.List;
|
||||
this.sessionBinder = sessionBinder;
|
||||
this.sessionActivity = sessionActivity;
|
||||
this.customLayout = customLayout;
|
||||
this.commandButtonsForMediaItems = commandButtonsForMediaItems;
|
||||
this.sessionCommands = sessionCommands;
|
||||
this.playerCommandsFromSession = playerCommandsFromSession;
|
||||
this.playerCommandsFromPlayer = playerCommandsFromPlayer;
|
||||
@ -91,6 +95,7 @@ import java.util.List;
|
||||
private static final String FIELD_SESSION_BINDER = Util.intToStringMaxRadix(1);
|
||||
private static final String FIELD_SESSION_ACTIVITY = Util.intToStringMaxRadix(2);
|
||||
private static final String FIELD_CUSTOM_LAYOUT = Util.intToStringMaxRadix(9);
|
||||
private static final String FIELD_COMMAND_BUTTONS_FOR_MEDIA_ITEMS = Util.intToStringMaxRadix(13);
|
||||
private static final String FIELD_SESSION_COMMANDS = Util.intToStringMaxRadix(3);
|
||||
private static final String FIELD_PLAYER_COMMANDS_FROM_SESSION = Util.intToStringMaxRadix(4);
|
||||
private static final String FIELD_PLAYER_COMMANDS_FROM_PLAYER = Util.intToStringMaxRadix(5);
|
||||
@ -101,7 +106,7 @@ import java.util.List;
|
||||
private static final String FIELD_IN_PROCESS_BINDER = Util.intToStringMaxRadix(10);
|
||||
private static final String FIELD_PLATFORM_TOKEN = Util.intToStringMaxRadix(12);
|
||||
|
||||
// Next field key = 13
|
||||
// Next field key = 14
|
||||
|
||||
public Bundle toBundleForRemoteProcess(int controllerInterfaceVersion) {
|
||||
Bundle bundle = new Bundle();
|
||||
@ -113,6 +118,12 @@ import java.util.List;
|
||||
FIELD_CUSTOM_LAYOUT,
|
||||
BundleCollectionUtil.toBundleArrayList(customLayout, CommandButton::toBundle));
|
||||
}
|
||||
if (!commandButtonsForMediaItems.isEmpty()) {
|
||||
bundle.putParcelableArrayList(
|
||||
FIELD_COMMAND_BUTTONS_FOR_MEDIA_ITEMS,
|
||||
BundleCollectionUtil.toBundleArrayList(
|
||||
commandButtonsForMediaItems, CommandButton::toBundle));
|
||||
}
|
||||
bundle.putBundle(FIELD_SESSION_COMMANDS, sessionCommands.toBundle());
|
||||
bundle.putBundle(FIELD_PLAYER_COMMANDS_FROM_SESSION, playerCommandsFromSession.toBundle());
|
||||
bundle.putBundle(FIELD_PLAYER_COMMANDS_FROM_PLAYER, playerCommandsFromPlayer.toBundle());
|
||||
@ -161,6 +172,15 @@ import java.util.List;
|
||||
? BundleCollectionUtil.fromBundleList(
|
||||
b -> CommandButton.fromBundle(b, sessionInterfaceVersion), commandButtonArrayList)
|
||||
: ImmutableList.of();
|
||||
@Nullable
|
||||
List<Bundle> commandButtonsForMediaItemsArrayList =
|
||||
bundle.getParcelableArrayList(FIELD_COMMAND_BUTTONS_FOR_MEDIA_ITEMS);
|
||||
ImmutableList<CommandButton> commandButtonsForMediaItems =
|
||||
commandButtonsForMediaItemsArrayList != null
|
||||
? BundleCollectionUtil.fromBundleList(
|
||||
b -> CommandButton.fromBundle(b, sessionInterfaceVersion),
|
||||
commandButtonsForMediaItemsArrayList)
|
||||
: ImmutableList.of();
|
||||
@Nullable Bundle sessionCommandsBundle = bundle.getBundle(FIELD_SESSION_COMMANDS);
|
||||
SessionCommands sessionCommands =
|
||||
sessionCommandsBundle == null
|
||||
@ -192,6 +212,7 @@ import java.util.List;
|
||||
IMediaSession.Stub.asInterface(sessionBinder),
|
||||
sessionActivity,
|
||||
customLayout,
|
||||
commandButtonsForMediaItems,
|
||||
sessionCommands,
|
||||
playerCommandsFromSession,
|
||||
playerCommandsFromPlayer,
|
||||
|
@ -34,6 +34,7 @@ import androidx.media3.session.legacy.MediaBrowserCompat;
|
||||
import androidx.media3.session.legacy.MediaBrowserCompat.ItemCallback;
|
||||
import androidx.media3.session.legacy.MediaBrowserCompat.SubscriptionCallback;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
@ -88,6 +89,11 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
|
||||
return super.getAvailableSessionCommands();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableMap<String, CommandButton> getCommandButtonsForMediaItemsMap() {
|
||||
return ImmutableMap.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListenableFuture<LibraryResult<MediaItem>> getLibraryRoot(@Nullable LibraryParams params) {
|
||||
if (!getInstance()
|
||||
|
@ -99,6 +99,11 @@ public final class MediaConstants {
|
||||
androidx.media3.session.legacy.MediaConstants
|
||||
.PLAYBACK_STATE_EXTRAS_KEY_ERROR_RESOLUTION_USING_CAR_APP_LIBRARY_INTENT;
|
||||
|
||||
/** {@link Bundle} key used for a media item ID. */
|
||||
@UnstableApi
|
||||
public static final String EXTRA_KEY_MEDIA_ID =
|
||||
androidx.media3.session.legacy.MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID;
|
||||
|
||||
/**
|
||||
* {@link Bundle} key to indicate a preference that a region of space for the skip to next control
|
||||
* should always be blocked out in the UI, even when the seek to next standard action is not
|
||||
|
@ -62,6 +62,7 @@ import androidx.media3.common.util.Util;
|
||||
import androidx.media3.datasource.DataSourceBitmapLoader;
|
||||
import androidx.media3.session.legacy.MediaBrowserCompat;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
@ -636,6 +637,27 @@ public class MediaController implements Player {
|
||||
return impl.isConnected();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the command buttons that are supported for the given {@link MediaItem}.
|
||||
*
|
||||
* @param mediaItem The media item for which to get command buttons.
|
||||
* @return The {@linkplain CommandButton command buttons} that are supported for the given media
|
||||
* item.
|
||||
*/
|
||||
@UnstableApi
|
||||
public final ImmutableList<CommandButton> getCommandButtonsForMediaItem(MediaItem mediaItem) {
|
||||
ImmutableMap<String, CommandButton> buttonMap = impl.getCommandButtonsForMediaItemsMap();
|
||||
ImmutableList<String> supportedActions = mediaItem.mediaMetadata.supportedCommands;
|
||||
ImmutableList.Builder<CommandButton> commandButtonsForMediaItem = new ImmutableList.Builder<>();
|
||||
for (int i = 0; i < supportedActions.size(); i++) {
|
||||
CommandButton commandButton = buttonMap.get(supportedActions.get(i));
|
||||
if (commandButton != null) {
|
||||
commandButtonsForMediaItem.add(commandButton);
|
||||
}
|
||||
}
|
||||
return commandButtonsForMediaItem.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void play() {
|
||||
verifyApplicationThread();
|
||||
@ -1022,6 +1044,35 @@ public class MediaController implements Player {
|
||||
return createDisconnectedFuture();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a custom command to the session for the given {@linkplain MediaItem media item}.
|
||||
*
|
||||
* <p>Calling this method is equivalent to calling {@link #sendCustomCommand(SessionCommand,
|
||||
* Bundle)} and including the {@linkplain MediaItem#mediaId media ID} in the argument bundle with
|
||||
* key {@link MediaConstants#EXTRA_KEY_MEDIA_ID}.
|
||||
*
|
||||
* <p>A command is not accepted if it is not a custom command or the command is not in the list of
|
||||
* {@linkplain #getAvailableSessionCommands() available session commands}.
|
||||
*
|
||||
* <p>Interoperability: When connected to {@code
|
||||
* android.support.v4.media.session.MediaSessionCompat}, {@link SessionResult#resultCode} will
|
||||
* return the custom result code from the {@code android.os.ResultReceiver#onReceiveResult(int,
|
||||
* Bundle)} instead of the standard result codes defined in the {@link SessionResult}.
|
||||
*
|
||||
* @param command The custom command.
|
||||
* @param mediaItem The media item for which the command is sent.
|
||||
* @param args The additional arguments. May be empty.
|
||||
* @return A {@link ListenableFuture} of {@link SessionResult} representing the pending
|
||||
* completion.
|
||||
*/
|
||||
@UnstableApi
|
||||
public final ListenableFuture<SessionResult> sendCustomCommand(
|
||||
SessionCommand command, MediaItem mediaItem, Bundle args) {
|
||||
Bundle augnentedBundle = new Bundle(args);
|
||||
augnentedBundle.putString(MediaConstants.EXTRA_KEY_MEDIA_ID, mediaItem.mediaId);
|
||||
return sendCustomCommand(command, augnentedBundle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the custom layout.
|
||||
*
|
||||
@ -2091,6 +2142,8 @@ public class MediaController implements Player {
|
||||
|
||||
ImmutableList<CommandButton> getCustomLayout();
|
||||
|
||||
ImmutableMap<String, CommandButton> getCommandButtonsForMediaItemsMap();
|
||||
|
||||
Bundle getSessionExtras();
|
||||
|
||||
Timeline getCurrentTimeline();
|
||||
|
@ -86,6 +86,7 @@ import androidx.media3.session.MediaController.MediaControllerImpl;
|
||||
import androidx.media3.session.PlayerInfo.BundlingExclusions;
|
||||
import androidx.media3.session.legacy.MediaBrowserCompat;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
@ -126,6 +127,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
@Nullable private PendingIntent sessionActivity;
|
||||
private ImmutableList<CommandButton> customLayoutOriginal;
|
||||
private ImmutableList<CommandButton> customLayoutWithUnavailableButtonsDisabled;
|
||||
private ImmutableMap<String, CommandButton> commandButtonsForMediaItemsMap;
|
||||
private SessionCommands sessionCommands;
|
||||
private Commands playerCommandsFromSession;
|
||||
private Commands playerCommandsFromPlayer;
|
||||
@ -153,6 +155,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
sessionCommands = SessionCommands.EMPTY;
|
||||
customLayoutOriginal = ImmutableList.of();
|
||||
customLayoutWithUnavailableButtonsDisabled = ImmutableList.of();
|
||||
commandButtonsForMediaItemsMap = ImmutableMap.of();
|
||||
playerCommandsFromSession = Commands.EMPTY;
|
||||
playerCommandsFromPlayer = Commands.EMPTY;
|
||||
intersectedPlayerCommands =
|
||||
@ -733,6 +736,11 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
return customLayoutWithUnavailableButtonsDisabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableMap<String, CommandButton> getCommandButtonsForMediaItemsMap() {
|
||||
return commandButtonsForMediaItemsMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle getSessionExtras() {
|
||||
return sessionExtras;
|
||||
@ -2619,6 +2627,16 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
customLayoutWithUnavailableButtonsDisabled =
|
||||
CommandButton.copyWithUnavailableButtonsDisabled(
|
||||
result.customLayout, sessionCommands, intersectedPlayerCommands);
|
||||
ImmutableMap.Builder<String, CommandButton> commandButtonsForMediaItems =
|
||||
new ImmutableMap.Builder<>();
|
||||
for (int i = 0; i < result.commandButtonsForMediaItems.size(); i++) {
|
||||
CommandButton commandButton = result.commandButtonsForMediaItems.get(i);
|
||||
if (commandButton.sessionCommand != null
|
||||
&& commandButton.sessionCommand.commandCode == SessionCommand.COMMAND_CODE_CUSTOM) {
|
||||
commandButtonsForMediaItems.put(commandButton.sessionCommand.customAction, commandButton);
|
||||
}
|
||||
}
|
||||
commandButtonsForMediaItemsMap = commandButtonsForMediaItems.buildOrThrow();
|
||||
playerInfo = result.playerInfo;
|
||||
MediaSession.Token platformToken =
|
||||
result.platformToken == null ? token.getPlatformToken() : result.platformToken;
|
||||
|
@ -76,6 +76,7 @@ import androidx.media3.session.legacy.PlaybackStateCompat;
|
||||
import androidx.media3.session.legacy.RatingCompat;
|
||||
import androidx.media3.session.legacy.VolumeProviderCompat;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
@ -420,6 +421,11 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
|
||||
return controllerInfo.customLayout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableMap<String, CommandButton> getCommandButtonsForMediaItemsMap() {
|
||||
return ImmutableMap.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle getSessionExtras() {
|
||||
return controllerInfo.sessionExtras;
|
||||
|
@ -630,6 +630,18 @@ public abstract class MediaLibraryService extends MediaSessionService {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets {@link CommandButton command buttons} that can be added as {@link
|
||||
* MediaMetadata.Builder#setSupportedCommands(List) supported media item commands}.
|
||||
*
|
||||
* @param commandButtons The command buttons.
|
||||
*/
|
||||
@UnstableApi
|
||||
@Override
|
||||
public Builder setCommandButtonsForMediaItems(List<CommandButton> commandButtons) {
|
||||
return super.setCommandButtonsForMediaItems(commandButtons);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a {@link MediaLibrarySession}.
|
||||
*
|
||||
@ -648,6 +660,7 @@ public abstract class MediaLibraryService extends MediaSessionService {
|
||||
player,
|
||||
sessionActivity,
|
||||
customLayout,
|
||||
commandButtonsForMediaItems,
|
||||
callback,
|
||||
tokenExtras,
|
||||
sessionExtras,
|
||||
@ -664,6 +677,7 @@ public abstract class MediaLibraryService extends MediaSessionService {
|
||||
Player player,
|
||||
@Nullable PendingIntent sessionActivity,
|
||||
ImmutableList<CommandButton> customLayout,
|
||||
ImmutableList<CommandButton> commandButtonsForMediaItems,
|
||||
MediaSession.Callback callback,
|
||||
Bundle tokenExtras,
|
||||
Bundle sessionExtras,
|
||||
@ -677,6 +691,7 @@ public abstract class MediaLibraryService extends MediaSessionService {
|
||||
player,
|
||||
sessionActivity,
|
||||
customLayout,
|
||||
commandButtonsForMediaItems,
|
||||
callback,
|
||||
tokenExtras,
|
||||
sessionExtras,
|
||||
@ -693,6 +708,7 @@ public abstract class MediaLibraryService extends MediaSessionService {
|
||||
Player player,
|
||||
@Nullable PendingIntent sessionActivity,
|
||||
ImmutableList<CommandButton> customLayout,
|
||||
ImmutableList<CommandButton> commandButtonsForMediaItems,
|
||||
MediaSession.Callback callback,
|
||||
Bundle tokenExtras,
|
||||
Bundle sessionExtras,
|
||||
@ -707,6 +723,7 @@ public abstract class MediaLibraryService extends MediaSessionService {
|
||||
player,
|
||||
sessionActivity,
|
||||
customLayout,
|
||||
commandButtonsForMediaItems,
|
||||
(Callback) callback,
|
||||
tokenExtras,
|
||||
sessionExtras,
|
||||
|
@ -75,6 +75,7 @@ import java.util.concurrent.Future;
|
||||
Player player,
|
||||
@Nullable PendingIntent sessionActivity,
|
||||
ImmutableList<CommandButton> customLayout,
|
||||
ImmutableList<CommandButton> commandButtonsForMediaItems,
|
||||
MediaLibrarySession.Callback callback,
|
||||
Bundle tokenExtras,
|
||||
Bundle sessionExtras,
|
||||
@ -89,6 +90,7 @@ import java.util.concurrent.Future;
|
||||
player,
|
||||
sessionActivity,
|
||||
customLayout,
|
||||
commandButtonsForMediaItems,
|
||||
callback,
|
||||
tokenExtras,
|
||||
sessionExtras,
|
||||
|
@ -425,6 +425,18 @@ public class MediaSession {
|
||||
return super.setShowPlayButtonIfPlaybackIsSuppressed(showPlayButtonIfPlaybackIsSuppressed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets {@link CommandButton command buttons} that can be added as {@linkplain
|
||||
* MediaMetadata.Builder#setSupportedCommands(List) supported media item commands}.
|
||||
*
|
||||
* @param commandButtons The command buttons.
|
||||
*/
|
||||
@UnstableApi
|
||||
@Override
|
||||
public Builder setCommandButtonsForMediaItems(List<CommandButton> commandButtons) {
|
||||
return super.setCommandButtonsForMediaItems(commandButtons);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a {@link MediaSession}.
|
||||
*
|
||||
@ -443,6 +455,7 @@ public class MediaSession {
|
||||
player,
|
||||
sessionActivity,
|
||||
customLayout,
|
||||
commandButtonsForMediaItems,
|
||||
callback,
|
||||
tokenExtras,
|
||||
sessionExtras,
|
||||
@ -654,6 +667,7 @@ public class MediaSession {
|
||||
Player player,
|
||||
@Nullable PendingIntent sessionActivity,
|
||||
ImmutableList<CommandButton> customLayout,
|
||||
ImmutableList<CommandButton> commandButtonsForMediaItems,
|
||||
Callback callback,
|
||||
Bundle tokenExtras,
|
||||
Bundle sessionExtras,
|
||||
@ -674,6 +688,7 @@ public class MediaSession {
|
||||
player,
|
||||
sessionActivity,
|
||||
customLayout,
|
||||
commandButtonsForMediaItems,
|
||||
callback,
|
||||
tokenExtras,
|
||||
sessionExtras,
|
||||
@ -689,6 +704,7 @@ public class MediaSession {
|
||||
Player player,
|
||||
@Nullable PendingIntent sessionActivity,
|
||||
ImmutableList<CommandButton> customLayout,
|
||||
ImmutableList<CommandButton> commandButtonsForMediaItems,
|
||||
Callback callback,
|
||||
Bundle tokenExtras,
|
||||
Bundle sessionExtras,
|
||||
@ -703,6 +719,7 @@ public class MediaSession {
|
||||
player,
|
||||
sessionActivity,
|
||||
customLayout,
|
||||
commandButtonsForMediaItems,
|
||||
callback,
|
||||
tokenExtras,
|
||||
sessionExtras,
|
||||
@ -2073,6 +2090,7 @@ public class MediaSession {
|
||||
/* package */ @MonotonicNonNull BitmapLoader bitmapLoader;
|
||||
/* package */ boolean playIfSuppressed;
|
||||
/* package */ ImmutableList<CommandButton> customLayout;
|
||||
/* package */ ImmutableList<CommandButton> commandButtonsForMediaItems;
|
||||
/* package */ boolean isPeriodicPositionUpdateEnabled;
|
||||
|
||||
public BuilderBase(Context context, Player player, CallbackT callback) {
|
||||
@ -2086,6 +2104,7 @@ public class MediaSession {
|
||||
customLayout = ImmutableList.of();
|
||||
playIfSuppressed = true;
|
||||
isPeriodicPositionUpdateEnabled = true;
|
||||
commandButtonsForMediaItems = ImmutableList.of();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@ -2140,6 +2159,12 @@ public class MediaSession {
|
||||
return (BuilderT) this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public BuilderT setCommandButtonsForMediaItems(List<CommandButton> commandButtons) {
|
||||
this.commandButtonsForMediaItems = ImmutableList.copyOf(commandButtons);
|
||||
return (BuilderT) this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public BuilderT setPeriodicPositionUpdateEnabled(boolean isPeriodicPositionUpdateEnabled) {
|
||||
this.isPeriodicPositionUpdateEnabled = isPeriodicPositionUpdateEnabled;
|
||||
|
@ -132,6 +132,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
private final Handler mainHandler;
|
||||
private final boolean playIfSuppressed;
|
||||
private final boolean isPeriodicPositionUpdateEnabled;
|
||||
private final ImmutableList<CommandButton> commandButtonsForMediaItems;
|
||||
|
||||
private PlayerInfo playerInfo;
|
||||
private PlayerWrapper playerWrapper;
|
||||
@ -161,6 +162,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
Player player,
|
||||
@Nullable PendingIntent sessionActivity,
|
||||
ImmutableList<CommandButton> customLayout,
|
||||
ImmutableList<CommandButton> commandButtonsForMediaItems,
|
||||
MediaSession.Callback callback,
|
||||
Bundle tokenExtras,
|
||||
Bundle sessionExtras,
|
||||
@ -181,6 +183,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
sessionId = id;
|
||||
this.sessionActivity = sessionActivity;
|
||||
this.customLayout = customLayout;
|
||||
this.commandButtonsForMediaItems = commandButtonsForMediaItems;
|
||||
this.callback = callback;
|
||||
this.sessionExtras = sessionExtras;
|
||||
this.bitmapLoader = bitmapLoader;
|
||||
@ -513,6 +516,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
return customLayout;
|
||||
}
|
||||
|
||||
/** Returns the command buttons for media items. */
|
||||
public ImmutableList<CommandButton> getCommandButtonsForMediaItems() {
|
||||
return commandButtonsForMediaItems;
|
||||
}
|
||||
|
||||
public void setSessionExtras(Bundle sessionExtras) {
|
||||
this.sessionExtras = sessionExtras;
|
||||
dispatchRemoteControllerTaskWithoutReturn(
|
||||
|
@ -535,6 +535,7 @@ import java.util.concurrent.ExecutionException;
|
||||
connectionResult.customLayout != null
|
||||
? connectionResult.customLayout
|
||||
: sessionImpl.getCustomLayout(),
|
||||
sessionImpl.getCommandButtonsForMediaItems(),
|
||||
connectionResult.availableSessionCommands,
|
||||
connectionResult.availablePlayerCommands,
|
||||
playerWrapper.getAvailableCommands(),
|
||||
|
@ -21,6 +21,8 @@ public class MediaSessionConstants {
|
||||
// Test method names
|
||||
public static final String TEST_GET_SESSION_ACTIVITY = "testGetSessionActivity";
|
||||
public static final String TEST_GET_CUSTOM_LAYOUT = "testGetCustomLayout";
|
||||
public static final String TEST_GET_COMMAND_BUTTONS_FOR_MEDIA_ITEMS =
|
||||
"testGetCommandButtonsForMediaItems";
|
||||
public static final String TEST_WITH_CUSTOM_COMMANDS = "testWithCustomCommands";
|
||||
public static final String TEST_CONTROLLER_LISTENER_SESSION_REJECTS = "connection_sessionRejects";
|
||||
public static final String TEST_IS_SESSION_COMMAND_AVAILABLE = "testIsSessionCommandAvailable";
|
||||
|
@ -21,6 +21,7 @@ import static androidx.media3.session.MediaUtils.createPlayerCommandsWithout;
|
||||
import static androidx.media3.test.session.common.CommonConstants.DEFAULT_TEST_NAME;
|
||||
import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME;
|
||||
import static androidx.media3.test.session.common.MediaSessionConstants.KEY_AVAILABLE_SESSION_COMMANDS;
|
||||
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_GET_COMMAND_BUTTONS_FOR_MEDIA_ITEMS;
|
||||
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_GET_CUSTOM_LAYOUT;
|
||||
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_GET_SESSION_ACTIVITY;
|
||||
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_IS_SESSION_COMMAND_AVAILABLE;
|
||||
@ -66,8 +67,10 @@ import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
@ -547,6 +550,90 @@ public class MediaControllerTest {
|
||||
session.cleanUp();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getCommandButtonsForMediaItem() throws Exception {
|
||||
RemoteMediaSession session =
|
||||
createRemoteMediaSession(TEST_GET_COMMAND_BUTTONS_FOR_MEDIA_ITEMS, /* tokenExtras= */ null);
|
||||
CommandButton playlistAddButton =
|
||||
new CommandButton.Builder(CommandButton.ICON_PLAYLIST_ADD)
|
||||
.setSessionCommand(
|
||||
new SessionCommand("androidx.media3.actions.playlist_add", Bundle.EMPTY))
|
||||
.build();
|
||||
CommandButton radioButton =
|
||||
new CommandButton.Builder(CommandButton.ICON_RADIO)
|
||||
.setSessionCommand(new SessionCommand("androidx.media3.actions.radio", Bundle.EMPTY))
|
||||
.build();
|
||||
MediaItem mediaItem =
|
||||
new MediaItem.Builder()
|
||||
.setMediaId("mediaId")
|
||||
.setMediaMetadata(
|
||||
new MediaMetadata.Builder()
|
||||
.setSupportedCommands(
|
||||
ImmutableList.of(
|
||||
"androidx.media3.actions.playlist_add",
|
||||
"androidx.media3.actions.radio",
|
||||
"invalid"))
|
||||
.build())
|
||||
.build();
|
||||
MediaController controller = controllerTestRule.createController(session.getToken());
|
||||
|
||||
ImmutableList<CommandButton> commandButtons =
|
||||
threadTestRule
|
||||
.getHandler()
|
||||
.postAndSync(() -> controller.getCommandButtonsForMediaItem(mediaItem));
|
||||
|
||||
assertThat(commandButtons).containsExactly(playlistAddButton, radioButton).inOrder();
|
||||
session.cleanUp();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendCustomCommandForMediaItem() throws Exception {
|
||||
RemoteMediaSession session =
|
||||
createRemoteMediaSession(TEST_GET_COMMAND_BUTTONS_FOR_MEDIA_ITEMS, /* tokenExtras= */ null);
|
||||
MediaItem mediaItem =
|
||||
new MediaItem.Builder()
|
||||
.setMediaId("mediaId-1")
|
||||
.setMediaMetadata(
|
||||
new MediaMetadata.Builder()
|
||||
.setSupportedCommands(ImmutableList.of("androidx.media3.actions.playlist_add"))
|
||||
.build())
|
||||
.build();
|
||||
CountDownLatch latch = new CountDownLatch(/* count= */ 1);
|
||||
AtomicReference<SessionResult> sessionResultRef = new AtomicReference<>();
|
||||
MediaController controller = controllerTestRule.createController(session.getToken());
|
||||
|
||||
Futures.addCallback(
|
||||
threadTestRule
|
||||
.getHandler()
|
||||
.postAndSync(
|
||||
() -> {
|
||||
CommandButton commandButton =
|
||||
controller.getCommandButtonsForMediaItem(mediaItem).get(0);
|
||||
return controller.sendCustomCommand(
|
||||
commandButton.sessionCommand, mediaItem, Bundle.EMPTY);
|
||||
}),
|
||||
new FutureCallback<SessionResult>() {
|
||||
@Override
|
||||
public void onSuccess(SessionResult result) {
|
||||
sessionResultRef.set(result);
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable t) {
|
||||
latch.countDown();
|
||||
}
|
||||
},
|
||||
MoreExecutors.directExecutor());
|
||||
|
||||
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
|
||||
assertThat(sessionResultRef.get()).isNotNull();
|
||||
assertThat(sessionResultRef.get().resultCode).isEqualTo(SessionResult.RESULT_SUCCESS);
|
||||
assertThat(sessionResultRef.get().extras.getString(MediaConstants.EXTRA_KEY_MEDIA_ID))
|
||||
.isEqualTo("mediaId-1");
|
||||
session.cleanUp();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getSessionExtras_includedInConnectionStateWhenConnecting() throws Exception {
|
||||
RemoteMediaSession session =
|
||||
|
@ -63,6 +63,7 @@ import static androidx.media3.test.session.common.MediaSessionConstants.KEY_CONT
|
||||
import static androidx.media3.test.session.common.MediaSessionConstants.NOTIFICATION_CONTROLLER_KEY;
|
||||
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_COMMAND_GET_TRACKS;
|
||||
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_CONTROLLER_LISTENER_SESSION_REJECTS;
|
||||
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_GET_COMMAND_BUTTONS_FOR_MEDIA_ITEMS;
|
||||
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_GET_CUSTOM_LAYOUT;
|
||||
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_GET_SESSION_ACTIVITY;
|
||||
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_IS_SESSION_COMMAND_AVAILABLE;
|
||||
@ -71,6 +72,7 @@ import static androidx.media3.test.session.common.MediaSessionConstants.TEST_ON_
|
||||
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_ON_VIDEO_SIZE_CHANGED;
|
||||
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_SET_SHOW_PLAY_BUTTON_IF_SUPPRESSED_TO_FALSE;
|
||||
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_WITH_CUSTOM_COMMANDS;
|
||||
import static com.google.common.util.concurrent.Futures.immediateFuture;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
@ -233,6 +235,58 @@ public class MediaSessionProviderService extends Service {
|
||||
});
|
||||
break;
|
||||
}
|
||||
case TEST_GET_COMMAND_BUTTONS_FOR_MEDIA_ITEMS:
|
||||
{
|
||||
CommandButton playlistAddButton =
|
||||
new CommandButton.Builder(CommandButton.ICON_PLAYLIST_ADD)
|
||||
.setSessionCommand(
|
||||
new SessionCommand("androidx.media3.actions.playlist_add", Bundle.EMPTY))
|
||||
.build();
|
||||
CommandButton radioButton =
|
||||
new CommandButton.Builder(CommandButton.ICON_RADIO)
|
||||
.setSessionCommand(
|
||||
new SessionCommand("androidx.media3.actions.radio", Bundle.EMPTY))
|
||||
.build();
|
||||
builder.setCommandButtonsForMediaItems(
|
||||
ImmutableList.of(playlistAddButton, radioButton));
|
||||
builder.setCallback(
|
||||
new MediaSession.Callback() {
|
||||
@Override
|
||||
public MediaSession.ConnectionResult onConnect(
|
||||
MediaSession session, ControllerInfo controller) {
|
||||
return new MediaSession.ConnectionResult.AcceptedResultBuilder(session)
|
||||
.setAvailableSessionCommands(
|
||||
new SessionCommands.Builder()
|
||||
.add(checkNotNull(playlistAddButton.sessionCommand))
|
||||
.add(checkNotNull(radioButton.sessionCommand))
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListenableFuture<SessionResult> onCustomCommand(
|
||||
MediaSession session,
|
||||
ControllerInfo controller,
|
||||
SessionCommand customCommand,
|
||||
Bundle args) {
|
||||
SessionResult sessionResult =
|
||||
new SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED);
|
||||
if (customCommand.equals(playlistAddButton.sessionCommand)
|
||||
|| customCommand.equals(radioButton.sessionCommand)) {
|
||||
Bundle extras = new Bundle();
|
||||
String receivedMediaId = args.getString(MediaConstants.EXTRA_KEY_MEDIA_ID);
|
||||
@SessionResult.Code int resultCode = SessionResult.RESULT_ERROR_BAD_VALUE;
|
||||
if (receivedMediaId != null) {
|
||||
extras.putString(MediaConstants.EXTRA_KEY_MEDIA_ID, receivedMediaId);
|
||||
resultCode = SessionResult.RESULT_SUCCESS;
|
||||
}
|
||||
sessionResult = new SessionResult(resultCode, extras);
|
||||
}
|
||||
return immediateFuture(sessionResult);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case TEST_CONTROLLER_LISTENER_SESSION_REJECTS:
|
||||
{
|
||||
builder.setCallback(
|
||||
|
Loading…
x
Reference in New Issue
Block a user