Add interoperability for media item commands

See https://developer.android.com/training/cars/media#custom_browse_actions

- `MediaMetadata.supportedCommands` is converted to an array list of
  strings into the extras of `MediaDescriptionCompat` with `DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST` and vice versa.

- The set of media item command buttons of a session is passed in the
root hints to a legacy browser that connects. A Media3 browser connected
to a legacy service, gets the set of all commands after calling
`getLibraryRoot()`.

#cherrypick

PiperOrigin-RevId: 678807473
This commit is contained in:
bachinger 2024-09-25 12:37:38 -07:00 committed by Copybara-Service
parent 686c3fe7f5
commit b8ec6b836b
11 changed files with 561 additions and 18 deletions

View File

@ -43,6 +43,7 @@ import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Util.constrainValue; import static androidx.media3.common.util.Util.constrainValue;
import static androidx.media3.session.MediaConstants.EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY; import static androidx.media3.session.MediaConstants.EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY;
import static androidx.media3.session.legacy.MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS; import static androidx.media3.session.legacy.MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS;
import static androidx.media3.session.legacy.MediaConstants.DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST;
import static androidx.media3.session.legacy.MediaMetadataCompat.PREFERRED_DESCRIPTION_ORDER; import static androidx.media3.session.legacy.MediaMetadataCompat.PREFERRED_DESCRIPTION_ORDER;
import static androidx.media3.session.legacy.MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS; import static androidx.media3.session.legacy.MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS;
import static java.lang.Math.max; import static java.lang.Math.max;
@ -539,6 +540,15 @@ import java.util.concurrent.TimeoutException;
extras.remove(MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT); extras.remove(MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT);
} }
if (extras != null
&& extras.containsKey(DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST)) {
builder.setSupportedCommands(
ImmutableList.copyOf(
checkNotNull(
extras.getStringArrayList(
DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST))));
}
if (extras != null if (extras != null
&& extras.containsKey(MediaConstants.EXTRAS_KEY_MEDIA_DESCRIPTION_COMPAT_TITLE)) { && extras.containsKey(MediaConstants.EXTRAS_KEY_MEDIA_DESCRIPTION_COMPAT_TITLE)) {
builder.setTitle( builder.setTitle(
@ -826,6 +836,14 @@ import java.util.concurrent.TimeoutException;
MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT, checkNotNull(metadata.mediaType)); MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT, checkNotNull(metadata.mediaType));
} }
} }
if (!metadata.supportedCommands.isEmpty()) {
if (extras == null) {
extras = new Bundle();
}
extras.putStringArrayList(
DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST,
new ArrayList<>(metadata.supportedCommands));
}
CharSequence title; CharSequence title;
CharSequence subtitle; CharSequence subtitle;
CharSequence description; CharSequence description;
@ -1629,6 +1647,87 @@ import java.util.concurrent.TimeoutException;
return playbackInfoCompat.getCurrentVolume() == 0; return playbackInfoCompat.getCurrentVolume() == 0;
} }
/**
* Converts a {@linkplain Bundle custom browse action} to a {@link CommandButton}. Returns null if
* the bundle doesn't contain sufficient information to build a command button.
*
* <p>See <a href="https://developer.android.com/training/cars/media#custom_browse_actions">Custom
* Browse Actions for Automotive OS</a>.
*
* @param browseActionBundle The bundle containing the information of a browse action.
* @return The resulting {@link CommandButton} or null.
*/
@Nullable
public static CommandButton convertCustomBrowseActionToCommandButton(Bundle browseActionBundle) {
String commandAction =
browseActionBundle.getString(
androidx.media3.session.legacy.MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID);
if (commandAction == null) {
return null;
}
@Nullable
CommandButton.Builder commandButton =
new CommandButton.Builder()
.setSessionCommand(new SessionCommand(commandAction, Bundle.EMPTY));
String label =
browseActionBundle.getString(
androidx.media3.session.legacy.MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL);
if (label != null) {
commandButton.setDisplayName(label);
}
String iconUri =
browseActionBundle.getString(
androidx.media3.session.legacy.MediaConstants
.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI);
if (iconUri != null) {
try {
commandButton.setIconUri(Uri.parse(iconUri));
} catch (Throwable t) {
Log.e(TAG, "error parsing icon URI of legacy browser action " + commandAction, t);
}
}
Bundle actionExtras =
browseActionBundle.getBundle(
androidx.media3.session.legacy.MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_EXTRAS);
if (actionExtras != null) {
commandButton.setExtras(actionExtras);
}
return commandButton.build();
}
/**
* Converts a {@link CommandButton} to a {@link Bundle} according to the browse action
* specification of Automotive OS.
*
* <p>See <a href="https://developer.android.com/training/cars/media#custom_browse_actions">Custom
* Browse Actions for Automotive OS</a>.
*
* @param commandButton The {@link CommandButton} to convert.
* @return The resulting {@link Bundle}.
*/
public static Bundle convertToBundle(CommandButton commandButton) {
Bundle buttonBundle = new Bundle();
if (commandButton.sessionCommand != null) {
buttonBundle.putString(
androidx.media3.session.legacy.MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID,
commandButton.sessionCommand.customAction);
}
buttonBundle.putString(
androidx.media3.session.legacy.MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL,
commandButton.displayName.toString());
if (commandButton.iconUri != null) {
buttonBundle.putString(
androidx.media3.session.legacy.MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI,
commandButton.iconUri.toString());
}
if (!commandButton.extras.isEmpty()) {
buttonBundle.putBundle(
androidx.media3.session.legacy.MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_EXTRAS,
commandButton.extras);
}
return buttonBundle;
}
private static byte[] convertToByteArray(Bitmap bitmap) throws IOException { private static byte[] convertToByteArray(Bitmap bitmap) throws IOException {
try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) { try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) {
bitmap.compress(Bitmap.CompressFormat.PNG, /* ignored */ 0, stream); bitmap.compress(Bitmap.CompressFormat.PNG, /* ignored */ 0, stream);

View File

@ -15,10 +15,12 @@
*/ */
package androidx.media3.session; package androidx.media3.session;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.session.SessionError.ERROR_BAD_VALUE; import static androidx.media3.session.SessionError.ERROR_BAD_VALUE;
import static androidx.media3.session.SessionError.ERROR_PERMISSION_DENIED; import static androidx.media3.session.SessionError.ERROR_PERMISSION_DENIED;
import static androidx.media3.session.SessionError.ERROR_SESSION_DISCONNECTED; import static androidx.media3.session.SessionError.ERROR_SESSION_DISCONNECTED;
import static androidx.media3.session.SessionError.ERROR_UNKNOWN; import static androidx.media3.session.SessionError.ERROR_UNKNOWN;
import static androidx.media3.session.legacy.MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
@ -50,11 +52,11 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
private static final String TAG = "MB2ImplLegacy"; private static final String TAG = "MB2ImplLegacy";
private final HashMap<LibraryParams, MediaBrowserCompat> browserCompats = new HashMap<>(); private final HashMap<LibraryParams, MediaBrowserCompat> browserCompats = new HashMap<>();
private final HashMap<String, List<SubscribeCallback>> subscribeCallbacks = new HashMap<>(); private final HashMap<String, List<SubscribeCallback>> subscribeCallbacks = new HashMap<>();
private final MediaBrowser instance; private final MediaBrowser instance;
private ImmutableMap<String, CommandButton> commandButtonsForMediaItems;
MediaBrowserImplLegacy( MediaBrowserImplLegacy(
Context context, Context context,
@UnderInitialization MediaBrowser instance, @UnderInitialization MediaBrowser instance,
@ -63,6 +65,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
BitmapLoader bitmapLoader) { BitmapLoader bitmapLoader) {
super(context, instance, token, applicationLooper, bitmapLoader); super(context, instance, token, applicationLooper, bitmapLoader);
this.instance = instance; this.instance = instance;
commandButtonsForMediaItems = ImmutableMap.of();
} }
@Override @Override
@ -91,7 +94,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
@Override @Override
public ImmutableMap<String, CommandButton> getCommandButtonsForMediaItemsMap() { public ImmutableMap<String, CommandButton> getCommandButtonsForMediaItemsMap() {
return ImmutableMap.of(); return commandButtonsForMediaItems;
} }
@Override @Override
@ -376,10 +379,40 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
// Shouldn't be happen. Internal error? // Shouldn't be happen. Internal error?
result.set(LibraryResult.ofError(ERROR_UNKNOWN)); result.set(LibraryResult.ofError(ERROR_UNKNOWN));
} else { } else {
Bundle extras = browserCompat.getExtras();
if (extras != null) {
ArrayList<Bundle> parcelableArrayList =
extras.getParcelableArrayList(
BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST);
if (parcelableArrayList != null) {
@Nullable
ImmutableMap.Builder<String, CommandButton> commandButtonsForMediaItemsBuilder = null;
// Converting custom browser action bundles to media item command buttons.
for (int i = 0; i < parcelableArrayList.size(); i++) {
CommandButton commandButton =
LegacyConversions.convertCustomBrowseActionToCommandButton(
parcelableArrayList.get(i));
if (commandButton != null) {
if (commandButtonsForMediaItemsBuilder == null) {
// Merge all media item command button of different legacy roots into a single
// map. Last wins in case of duplicate action names.
commandButtonsForMediaItemsBuilder =
new ImmutableMap.Builder<String, CommandButton>()
.putAll(commandButtonsForMediaItems);
}
String customAction = checkNotNull(commandButton.sessionCommand).customAction;
commandButtonsForMediaItemsBuilder.put(customAction, commandButton);
}
}
if (commandButtonsForMediaItemsBuilder != null) {
commandButtonsForMediaItems = commandButtonsForMediaItemsBuilder.buildKeepingLast();
}
}
}
result.set( result.set(
LibraryResult.ofItem( LibraryResult.ofItem(
createRootMediaItem(browserCompat), createRootMediaItem(browserCompat),
LegacyConversions.convertToLibraryParams(context, browserCompat.getExtras()))); LegacyConversions.convertToLibraryParams(context, extras)));
} }
} }

View File

@ -23,6 +23,7 @@ import static androidx.media3.session.LibraryResult.RESULT_SUCCESS;
import static androidx.media3.session.MediaUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES; import static androidx.media3.session.MediaUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES;
import static androidx.media3.session.legacy.MediaBrowserCompat.EXTRA_PAGE; import static androidx.media3.session.legacy.MediaBrowserCompat.EXTRA_PAGE;
import static androidx.media3.session.legacy.MediaBrowserCompat.EXTRA_PAGE_SIZE; import static androidx.media3.session.legacy.MediaBrowserCompat.EXTRA_PAGE_SIZE;
import static androidx.media3.session.legacy.MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST;
import static androidx.media3.session.legacy.MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED; import static androidx.media3.session.legacy.MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
@ -126,6 +127,22 @@ import java.util.concurrent.atomic.AtomicReference;
.isSessionCommandAvailable(controller, SessionCommand.COMMAND_CODE_LIBRARY_SEARCH); .isSessionCommandAvailable(controller, SessionCommand.COMMAND_CODE_LIBRARY_SEARCH);
checkNotNull(extras) checkNotNull(extras)
.putBoolean(BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, isSearchSessionCommandAvailable); .putBoolean(BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, isSearchSessionCommandAvailable);
ImmutableList<CommandButton> commandButtonsForMediaItems =
librarySessionImpl.getCommandButtonsForMediaItems();
if (!commandButtonsForMediaItems.isEmpty()) {
ArrayList<Bundle> browserActionBundles = new ArrayList<>();
for (int i = 0; i < commandButtonsForMediaItems.size(); i++) {
CommandButton commandButton = commandButtonsForMediaItems.get(i);
if (commandButton.sessionCommand != null
&& commandButton.sessionCommand.commandCode == SessionCommand.COMMAND_CODE_CUSTOM) {
browserActionBundles.add(LegacyConversions.convertToBundle(commandButton));
}
}
if (!browserActionBundles.isEmpty()) {
extras.putParcelableArrayList(
BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST, browserActionBundles);
}
}
return new BrowserRoot(result.value.mediaId, extras); return new BrowserRoot(result.value.mediaId, extras);
} }
// No library root, but keep browser compat connected to allow getting session unless the // No library root, but keep browser compat connected to allow getting session unless the

View File

@ -29,10 +29,13 @@ public class MediaBrowserConstants {
public static final String ROOT_EXTRAS_KEY = "root_extras_key"; public static final String ROOT_EXTRAS_KEY = "root_extras_key";
public static final int ROOT_EXTRAS_VALUE = 4321; public static final int ROOT_EXTRAS_VALUE = 4321;
public static final String COMMAND_ACTION_PLAYLIST_ADD = "androidx.media3.actions.playlist_add"; public static final String COMMAND_PLAYLIST_ADD = "androidx.media3.commands.playlist_add";
public static final String COMMAND_RADIO = "androidx.media3.commands.radio";
public static final String MEDIA_ID_GET_BROWSABLE_ITEM = "media_id_get_browsable_item"; public static final String MEDIA_ID_GET_BROWSABLE_ITEM = "media_id_get_browsable_item";
public static final String MEDIA_ID_GET_PLAYABLE_ITEM = "media_id_get_playable_item"; public static final String MEDIA_ID_GET_PLAYABLE_ITEM = "media_id_get_playable_item";
public static final String MEDIA_ID_GET_ITEM_WITH_BROWSE_ACTIONS =
"media_id_item_with_browse_actions";
public static final String MEDIA_ID_GET_ITEM_WITH_METADATA = "media_id_get_item_with_metadata"; public static final String MEDIA_ID_GET_ITEM_WITH_METADATA = "media_id_get_item_with_metadata";
public static final String PARENT_ID = "parent_id"; public static final String PARENT_ID = "parent_id";

View File

@ -30,6 +30,8 @@ public class MediaBrowserServiceCompatConstants {
public static final String TEST_GET_CHILDREN_NON_FATAL_AUTHENTICATION_ERROR = public static final String TEST_GET_CHILDREN_NON_FATAL_AUTHENTICATION_ERROR =
"getLibraryRoot_nonFatalAuthenticationError_receivesPlaybackException"; "getLibraryRoot_nonFatalAuthenticationError_receivesPlaybackException";
public static final String TEST_SEND_CUSTOM_COMMAND = "sendCustomCommand"; public static final String TEST_SEND_CUSTOM_COMMAND = "sendCustomCommand";
public static final String TEST_MEDIA_ITEMS_WITH_BROWSE_ACTIONS =
"getLibraryRoot_withBrowseActions";
private MediaBrowserServiceCompatConstants() {} private MediaBrowserServiceCompatConstants() {}
} }

View File

@ -22,6 +22,7 @@ import static androidx.media3.session.MediaLibraryService.MediaLibrarySession.LI
import static androidx.media3.session.MediaLibraryService.MediaLibrarySession.LIBRARY_ERROR_REPLICATION_MODE_NON_FATAL; import static androidx.media3.session.MediaLibraryService.MediaLibrarySession.LIBRARY_ERROR_REPLICATION_MODE_NON_FATAL;
import static androidx.media3.session.MockMediaLibraryService.CONNECTION_HINTS_CUSTOM_LIBRARY_ROOT; import static androidx.media3.session.MockMediaLibraryService.CONNECTION_HINTS_CUSTOM_LIBRARY_ROOT;
import static androidx.media3.session.MockMediaLibraryService.createNotifyChildrenChangedBundle; import static androidx.media3.session.MockMediaLibraryService.createNotifyChildrenChangedBundle;
import static androidx.media3.session.legacy.MediaConstants.DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST;
import static androidx.media3.test.session.common.CommonConstants.METADATA_ALBUM_TITLE; import static androidx.media3.test.session.common.CommonConstants.METADATA_ALBUM_TITLE;
import static androidx.media3.test.session.common.CommonConstants.METADATA_ARTIST; import static androidx.media3.test.session.common.CommonConstants.METADATA_ARTIST;
import static androidx.media3.test.session.common.CommonConstants.METADATA_ARTWORK_URI; import static androidx.media3.test.session.common.CommonConstants.METADATA_ARTWORK_URI;
@ -37,6 +38,7 @@ import static androidx.media3.test.session.common.MediaBrowserConstants.CUSTOM_A
import static androidx.media3.test.session.common.MediaBrowserConstants.GET_CHILDREN_RESULT; import static androidx.media3.test.session.common.MediaBrowserConstants.GET_CHILDREN_RESULT;
import static androidx.media3.test.session.common.MediaBrowserConstants.LONG_LIST_COUNT; import static androidx.media3.test.session.common.MediaBrowserConstants.LONG_LIST_COUNT;
import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_BROWSABLE_ITEM; import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_BROWSABLE_ITEM;
import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_ITEM_WITH_BROWSE_ACTIONS;
import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_ITEM_WITH_METADATA; import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_ITEM_WITH_METADATA;
import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_PLAYABLE_ITEM; import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_PLAYABLE_ITEM;
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID;
@ -78,6 +80,7 @@ import android.support.v4.media.MediaBrowserCompat.SubscriptionCallback;
import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.session.PlaybackStateCompat; import android.support.v4.media.session.PlaybackStateCompat;
import android.text.TextUtils; import android.text.TextUtils;
import androidx.media3.test.session.common.MediaBrowserConstants;
import androidx.media3.test.session.common.TestUtils; import androidx.media3.test.session.common.TestUtils;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.ext.truth.os.BundleSubject; import androidx.test.ext.truth.os.BundleSubject;
@ -114,6 +117,71 @@ public class MediaBrowserCompatWithMediaLibraryServiceTest
.getExtras() .getExtras()
.getInt(ROOT_EXTRAS_KEY, /* defaultValue= */ ROOT_EXTRAS_VALUE + 1)) .getInt(ROOT_EXTRAS_KEY, /* defaultValue= */ ROOT_EXTRAS_VALUE + 1))
.isEqualTo(ROOT_EXTRAS_VALUE); .isEqualTo(ROOT_EXTRAS_VALUE);
ArrayList<Bundle> mediaItemCommandButtons =
browserCompat
.getExtras()
.getParcelableArrayList(
androidx.media3.session.legacy.MediaConstants
.BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST);
assertThat(mediaItemCommandButtons).hasSize(2);
assertThat(
mediaItemCommandButtons
.get(0)
.getString(
androidx.media3.session.legacy.MediaConstants
.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID))
.isEqualTo(MediaBrowserConstants.COMMAND_PLAYLIST_ADD);
assertThat(
mediaItemCommandButtons
.get(0)
.getString(
androidx.media3.session.legacy.MediaConstants
.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL))
.isEqualTo("Add to playlist");
assertThat(
mediaItemCommandButtons
.get(0)
.getString(
androidx.media3.session.legacy.MediaConstants
.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI))
.isEqualTo("http://www.example.com/icon/playlist_add");
assertThat(
mediaItemCommandButtons
.get(0)
.getBundle(
androidx.media3.session.legacy.MediaConstants
.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_EXTRAS)
.getString("key-1"))
.isEqualTo("playlist_add");
assertThat(
mediaItemCommandButtons
.get(1)
.getString(
androidx.media3.session.legacy.MediaConstants
.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID))
.isEqualTo(MediaBrowserConstants.COMMAND_RADIO);
assertThat(
mediaItemCommandButtons
.get(1)
.getString(
androidx.media3.session.legacy.MediaConstants
.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL))
.isEqualTo("Radio station");
assertThat(
mediaItemCommandButtons
.get(1)
.getString(
androidx.media3.session.legacy.MediaConstants
.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI))
.isEqualTo("http://www.example.com/icon/radio");
assertThat(
mediaItemCommandButtons
.get(1)
.getBundle(
androidx.media3.session.legacy.MediaConstants
.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_EXTRAS)
.getString("key-1"))
.isEqualTo("radio");
// Note: Cannot use equals() here because browser compat's extra contains server version, // Note: Cannot use equals() here because browser compat's extra contains server version,
// extra binder, and extra messenger. // extra binder, and extra messenger.
@ -166,6 +234,35 @@ public class MediaBrowserCompatWithMediaLibraryServiceTest
assertThat(itemRef.get().getDescription().getIconBitmap()).isNotNull(); assertThat(itemRef.get().getDescription().getIconBitmap()).isNotNull();
} }
@Test
public void getItem_playableWithBrowseActions_browseActionCorrectlyConverted() throws Exception {
String mediaId = MEDIA_ID_GET_ITEM_WITH_BROWSE_ACTIONS;
connectAndWait(/* rootHints= */ Bundle.EMPTY);
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<MediaItem> itemRef = new AtomicReference<>();
browserCompat.getItem(
mediaId,
new ItemCallback() {
@Override
public void onItemLoaded(MediaItem item) {
itemRef.set(item);
latch.countDown();
}
});
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(
itemRef
.get()
.getDescription()
.getExtras()
.getStringArrayList(DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST))
.containsExactly(
MediaBrowserConstants.COMMAND_PLAYLIST_ADD, MediaBrowserConstants.COMMAND_RADIO)
.inOrder();
}
@Test @Test
public void getItem_metadata() throws Exception { public void getItem_metadata() throws Exception {
String mediaId = MEDIA_ID_GET_ITEM_WITH_METADATA; String mediaId = MEDIA_ID_GET_ITEM_WITH_METADATA;
@ -295,6 +392,14 @@ public class MediaBrowserCompatWithMediaLibraryServiceTest
.isEqualTo(EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED); .isEqualTo(EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED);
assertThat(mediaItem.getDescription().getIconBitmap()).isNotNull(); assertThat(mediaItem.getDescription().getIconBitmap()).isNotNull();
assertThat(onChildrenLoadedWithBundleCalled.get()).isFalse(); assertThat(onChildrenLoadedWithBundleCalled.get()).isFalse();
assertThat(
mediaItem
.getDescription()
.getExtras()
.getStringArrayList(DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST))
.containsExactly(
MediaBrowserConstants.COMMAND_PLAYLIST_ADD, MediaBrowserConstants.COMMAND_RADIO)
.inOrder();
} }
} }

View File

@ -31,19 +31,23 @@ import static androidx.media3.test.session.common.MediaBrowserServiceCompatConst
import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_CHILDREN_NON_FATAL_AUTHENTICATION_ERROR; import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_CHILDREN_NON_FATAL_AUTHENTICATION_ERROR;
import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_CHILDREN_WITH_NULL_LIST; import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_CHILDREN_WITH_NULL_LIST;
import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_LIBRARY_ROOT; import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_LIBRARY_ROOT;
import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_MEDIA_ITEMS_WITH_BROWSE_ACTIONS;
import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_ON_CHILDREN_CHANGED_SUBSCRIBE_AND_UNSUBSCRIBE; import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_ON_CHILDREN_CHANGED_SUBSCRIBE_AND_UNSUBSCRIBE;
import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_SEND_CUSTOM_COMMAND; import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_SEND_CUSTOM_COMMAND;
import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS; import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static java.util.Objects.requireNonNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
import android.content.Context; import android.content.Context;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media.MediaBrowserServiceCompat; import androidx.media.MediaBrowserServiceCompat;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.PlaybackException; import androidx.media3.common.PlaybackException;
import androidx.media3.common.Player; import androidx.media3.common.Player;
import androidx.media3.session.MediaLibraryService.LibraryParams; import androidx.media3.session.MediaLibraryService.LibraryParams;
@ -118,6 +122,114 @@ public class MediaBrowserListenerWithMediaBrowserServiceCompatTest {
assertThat(thrown).hasCauseThat().isInstanceOf(SecurityException.class); assertThat(thrown).hasCauseThat().isInstanceOf(SecurityException.class);
} }
@Test
public void getLibraryRoot_browseActionsAvailable() throws Exception {
remoteService.setProxyForTest(TEST_MEDIA_ITEMS_WITH_BROWSE_ACTIONS);
CommandButton playlistAddButton =
new CommandButton.Builder()
.setDisplayName("Add to playlist")
.setIconUri(Uri.parse("https://www.example.com/icon/playlist_add"))
.setSessionCommand(
new SessionCommand(MediaBrowserConstants.COMMAND_PLAYLIST_ADD, Bundle.EMPTY))
.build();
CommandButton radioButton =
new CommandButton.Builder()
.setDisplayName("Radio station")
.setIconUri(Uri.parse("https://www.example.com/icon/radio"))
.setSessionCommand(
new SessionCommand(MediaBrowserConstants.COMMAND_RADIO, Bundle.EMPTY))
.build();
MediaItem mediaItem =
new MediaItem.Builder()
.setMediaId("mediaId")
.setMediaMetadata(
new MediaMetadata.Builder()
.setSupportedCommands(
ImmutableList.of(
MediaBrowserConstants.COMMAND_PLAYLIST_ADD,
MediaBrowserConstants.COMMAND_RADIO,
"invalid"))
.build())
.build();
MediaBrowser mediaBrowser = createBrowser(/* listener= */ null);
// When connected to a legacy browser service, the library root needs to be requested
// before media item commands are available.
LibraryResult<MediaItem> libraryResult =
threadTestRule
.getHandler()
.postAndSync(() -> mediaBrowser.getLibraryRoot(new LibraryParams.Builder().build()))
.get();
assertThat(libraryResult.resultCode).isEqualTo(RESULT_SUCCESS);
ImmutableList<CommandButton> commandButtons =
mediaBrowser.getCommandButtonsForMediaItem(mediaItem);
assertThat(commandButtons).containsExactly(playlistAddButton, radioButton).inOrder();
assertThat(commandButtons.get(0).extras.getString("key-1")).isEqualTo("playlist_add");
assertThat(commandButtons.get(1).extras.getString("key-1")).isEqualTo("radio");
}
@Test
public void getItem_supportedCommandActions_convertedCorrectly() throws Exception {
remoteService.setProxyForTest(TEST_MEDIA_ITEMS_WITH_BROWSE_ACTIONS);
MediaBrowser mediaBrowser = createBrowser(/* listener= */ null);
CommandButton playlistAddButton =
new CommandButton.Builder()
.setDisplayName("Add to playlist")
.setIconUri(Uri.parse("https://www.example.com/icon/playlist_add"))
.setSessionCommand(
new SessionCommand(MediaBrowserConstants.COMMAND_PLAYLIST_ADD, Bundle.EMPTY))
.build();
CommandButton radioButton =
new CommandButton.Builder()
.setDisplayName("Radio station")
.setIconUri(Uri.parse("https://www.example.com/icon/radio"))
.setSessionCommand(
new SessionCommand(MediaBrowserConstants.COMMAND_RADIO, Bundle.EMPTY))
.build();
// When connected to a legacy browser service, the library root needs to be requested
// before media item commands are available.
LibraryResult<MediaItem> libraryResult =
threadTestRule
.getHandler()
.postAndSync(() -> mediaBrowser.getLibraryRoot(new LibraryParams.Builder().build()))
.get();
assertThat(libraryResult.resultCode).isEqualTo(RESULT_SUCCESS);
MediaItem mediaItem =
threadTestRule.getHandler().postAndSync(() -> mediaBrowser.getItem("mediaId")).get().value;
ImmutableList<CommandButton> commandButtons =
threadTestRule
.getHandler()
.postAndSync(
() -> mediaBrowser.getCommandButtonsForMediaItem(requireNonNull(mediaItem)));
assertThat(commandButtons).containsExactly(playlistAddButton, radioButton).inOrder();
assertThat(commandButtons.get(0).extras.getString("key-1")).isEqualTo("playlist_add");
assertThat(commandButtons.get(1).extras.getString("key-1")).isEqualTo("radio");
}
@Test
public void sendCustomCommandWithMediaItem_mediaItemIdConvertedCorrectly() throws Exception {
remoteService.setProxyForTest(TEST_MEDIA_ITEMS_WITH_BROWSE_ACTIONS);
MediaBrowser mediaBrowser = createBrowser(/* listener= */ null);
MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaIdFromCommand").build();
SessionResult sessionResult =
threadTestRule
.getHandler()
.postAndSync(
() ->
mediaBrowser.sendCustomCommand(
new SessionCommand(MediaBrowserConstants.COMMAND_RADIO, Bundle.EMPTY),
mediaItem,
/* args= */ Bundle.EMPTY))
.get();
assertThat(sessionResult.extras.getString(MediaConstants.EXTRA_KEY_MEDIA_ID))
.isEqualTo("mediaIdFromCommand");
}
@Test @Test
public void onChildrenChanged_subscribeAndUnsubscribe() throws Exception { public void onChildrenChanged_subscribeAndUnsubscribe() throws Exception {
String testParentId = "testOnChildrenChanged"; String testParentId = "testOnChildrenChanged";
@ -358,8 +470,7 @@ public class MediaBrowserListenerWithMediaBrowserServiceCompatTest {
() -> () ->
browser.sendCustomCommand( browser.sendCustomCommand(
new SessionCommand( new SessionCommand(
MediaBrowserConstants.COMMAND_ACTION_PLAYLIST_ADD, MediaBrowserConstants.COMMAND_PLAYLIST_ADD, /* extras= */ Bundle.EMPTY),
/* extras= */ Bundle.EMPTY),
/* args= */ Bundle.EMPTY)); /* args= */ Bundle.EMPTY));
Futures.addCallback( Futures.addCallback(
resultFuture, resultFuture,
@ -397,8 +508,7 @@ public class MediaBrowserListenerWithMediaBrowserServiceCompatTest {
() -> () ->
browser.sendCustomCommand( browser.sendCustomCommand(
new SessionCommand( new SessionCommand(
MediaBrowserConstants.COMMAND_ACTION_PLAYLIST_ADD, MediaBrowserConstants.COMMAND_PLAYLIST_ADD, /* extras= */ Bundle.EMPTY),
/* extras= */ Bundle.EMPTY),
args)); args));
Futures.addCallback( Futures.addCallback(
resultFuture, resultFuture,

View File

@ -61,6 +61,7 @@ import androidx.media3.common.VideoSize;
import androidx.media3.test.session.R; import androidx.media3.test.session.R;
import androidx.media3.test.session.common.HandlerThreadTestRule; import androidx.media3.test.session.common.HandlerThreadTestRule;
import androidx.media3.test.session.common.MainLooperTestRule; import androidx.media3.test.session.common.MainLooperTestRule;
import androidx.media3.test.session.common.MediaBrowserConstants;
import androidx.media3.test.session.common.PollingCheck; import androidx.media3.test.session.common.PollingCheck;
import androidx.media3.test.session.common.TestUtils; import androidx.media3.test.session.common.TestUtils;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
@ -557,11 +558,12 @@ public class MediaControllerTest {
CommandButton playlistAddButton = CommandButton playlistAddButton =
new CommandButton.Builder(CommandButton.ICON_PLAYLIST_ADD) new CommandButton.Builder(CommandButton.ICON_PLAYLIST_ADD)
.setSessionCommand( .setSessionCommand(
new SessionCommand("androidx.media3.actions.playlist_add", Bundle.EMPTY)) new SessionCommand(MediaBrowserConstants.COMMAND_PLAYLIST_ADD, Bundle.EMPTY))
.build(); .build();
CommandButton radioButton = CommandButton radioButton =
new CommandButton.Builder(CommandButton.ICON_RADIO) new CommandButton.Builder(CommandButton.ICON_RADIO)
.setSessionCommand(new SessionCommand("androidx.media3.actions.radio", Bundle.EMPTY)) .setSessionCommand(
new SessionCommand(MediaBrowserConstants.COMMAND_RADIO, Bundle.EMPTY))
.build(); .build();
MediaItem mediaItem = MediaItem mediaItem =
new MediaItem.Builder() new MediaItem.Builder()
@ -570,8 +572,8 @@ public class MediaControllerTest {
new MediaMetadata.Builder() new MediaMetadata.Builder()
.setSupportedCommands( .setSupportedCommands(
ImmutableList.of( ImmutableList.of(
"androidx.media3.actions.playlist_add", MediaBrowserConstants.COMMAND_PLAYLIST_ADD,
"androidx.media3.actions.radio", MediaBrowserConstants.COMMAND_RADIO,
"invalid")) "invalid"))
.build()) .build())
.build(); .build();
@ -595,7 +597,8 @@ public class MediaControllerTest {
.setMediaId("mediaId-1") .setMediaId("mediaId-1")
.setMediaMetadata( .setMediaMetadata(
new MediaMetadata.Builder() new MediaMetadata.Builder()
.setSupportedCommands(ImmutableList.of("androidx.media3.actions.playlist_add")) .setSupportedCommands(
ImmutableList.of(MediaBrowserConstants.COMMAND_PLAYLIST_ADD))
.build()) .build())
.build(); .build();
CountDownLatch latch = new CountDownLatch(/* count= */ 1); CountDownLatch latch = new CountDownLatch(/* count= */ 1);

View File

@ -103,6 +103,7 @@ import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.session.MediaSession.ControllerInfo; import androidx.media3.session.MediaSession.ControllerInfo;
import androidx.media3.test.session.common.IRemoteMediaSession; import androidx.media3.test.session.common.IRemoteMediaSession;
import androidx.media3.test.session.common.MediaBrowserConstants;
import androidx.media3.test.session.common.MockActivity; import androidx.media3.test.session.common.MockActivity;
import androidx.media3.test.session.common.TestHandler; import androidx.media3.test.session.common.TestHandler;
import androidx.media3.test.session.common.TestHandler.TestRunnable; import androidx.media3.test.session.common.TestHandler.TestRunnable;
@ -240,12 +241,13 @@ public class MediaSessionProviderService extends Service {
CommandButton playlistAddButton = CommandButton playlistAddButton =
new CommandButton.Builder(CommandButton.ICON_PLAYLIST_ADD) new CommandButton.Builder(CommandButton.ICON_PLAYLIST_ADD)
.setSessionCommand( .setSessionCommand(
new SessionCommand("androidx.media3.actions.playlist_add", Bundle.EMPTY)) new SessionCommand(
MediaBrowserConstants.COMMAND_PLAYLIST_ADD, Bundle.EMPTY))
.build(); .build();
CommandButton radioButton = CommandButton radioButton =
new CommandButton.Builder(CommandButton.ICON_RADIO) new CommandButton.Builder(CommandButton.ICON_RADIO)
.setSessionCommand( .setSessionCommand(
new SessionCommand("androidx.media3.actions.radio", Bundle.EMPTY)) new SessionCommand(MediaBrowserConstants.COMMAND_RADIO, Bundle.EMPTY))
.build(); .build();
builder.setCommandButtonsForMediaItems( builder.setCommandButtonsForMediaItems(
ImmutableList.of(playlistAddButton, radioButton)); ImmutableList.of(playlistAddButton, radioButton));

View File

@ -19,6 +19,9 @@ import static androidx.media3.session.MediaConstants.EXTRAS_KEY_COMPLETION_STATU
import static androidx.media3.session.MediaConstants.EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED; import static androidx.media3.session.MediaConstants.EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED;
import static androidx.media3.session.MediaConstants.EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED; import static androidx.media3.session.MediaConstants.EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED;
import static androidx.media3.session.MediaConstants.EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED; import static androidx.media3.session.MediaConstants.EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED;
import static androidx.media3.session.legacy.MediaConstants.BROWSER_ROOT_HINTS_KEY_CUSTOM_BROWSER_ACTION_LIMIT;
import static androidx.media3.session.legacy.MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST;
import static androidx.media3.session.legacy.MediaConstants.DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST;
import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME;
import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_EXTRAS; import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_EXTRAS;
import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_ID; import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_ID;
@ -29,8 +32,10 @@ import static androidx.media3.test.session.common.MediaBrowserServiceCompatConst
import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_CHILDREN_NON_FATAL_AUTHENTICATION_ERROR; import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_CHILDREN_NON_FATAL_AUTHENTICATION_ERROR;
import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_CHILDREN_WITH_NULL_LIST; import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_CHILDREN_WITH_NULL_LIST;
import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_LIBRARY_ROOT; import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_GET_LIBRARY_ROOT;
import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_MEDIA_ITEMS_WITH_BROWSE_ACTIONS;
import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_ON_CHILDREN_CHANGED_SUBSCRIBE_AND_UNSUBSCRIBE; import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_ON_CHILDREN_CHANGED_SUBSCRIBE_AND_UNSUBSCRIBE;
import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_SEND_CUSTOM_COMMAND; import static androidx.media3.test.session.common.MediaBrowserServiceCompatConstants.TEST_SEND_CUSTOM_COMMAND;
import static java.lang.Math.min;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
@ -51,6 +56,7 @@ import androidx.media3.test.session.common.MediaBrowserConstants;
import androidx.media3.test.session.common.MediaBrowserServiceCompatConstants; import androidx.media3.test.session.common.MediaBrowserServiceCompatConstants;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -276,6 +282,9 @@ public class MockMediaBrowserServiceCompat extends MediaBrowserServiceCompat {
case TEST_SEND_CUSTOM_COMMAND: case TEST_SEND_CUSTOM_COMMAND:
setProxyForTestSendCustomCommand(); setProxyForTestSendCustomCommand();
break; break;
case TEST_MEDIA_ITEMS_WITH_BROWSE_ACTIONS:
setProxyForMediaItemsWithBrowseActions(session);
break;
default: default:
throw new IllegalArgumentException("Unknown testName: " + testName); throw new IllegalArgumentException("Unknown testName: " + testName);
} }
@ -341,6 +350,120 @@ public class MockMediaBrowserServiceCompat extends MediaBrowserServiceCompat {
}); });
} }
private void setProxyForMediaItemsWithBrowseActions(MediaSessionCompat session) {
// See https://developer.android.com/training/cars/media#custom_browse_actions
Bundle playlistAddBrowseAction = new Bundle();
Bundle playlistAddExtras = new Bundle();
playlistAddExtras.putString("key-1", "playlist_add");
playlistAddBrowseAction.putString(
androidx.media3.session.legacy.MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID,
MediaBrowserConstants.COMMAND_PLAYLIST_ADD);
playlistAddBrowseAction.putString(
androidx.media3.session.legacy.MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL,
"Add to playlist");
playlistAddBrowseAction.putString(
androidx.media3.session.legacy.MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI,
"https://www.example.com/icon/playlist_add");
playlistAddBrowseAction.putBundle(
androidx.media3.session.legacy.MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_EXTRAS,
playlistAddExtras);
Bundle radioBrowseAction = new Bundle();
Bundle radioExtras = new Bundle();
radioExtras.putString("key-1", "radio");
radioBrowseAction.putString(
androidx.media3.session.legacy.MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID,
MediaBrowserConstants.COMMAND_RADIO);
radioBrowseAction.putString(
androidx.media3.session.legacy.MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL,
"Radio station");
radioBrowseAction.putString(
androidx.media3.session.legacy.MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI,
"https://www.example.com/icon/radio");
radioBrowseAction.putBundle(
androidx.media3.session.legacy.MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_EXTRAS,
radioExtras);
ImmutableList<Bundle> browseActions =
ImmutableList.of(playlistAddBrowseAction, radioBrowseAction);
setMediaBrowserServiceProxy(
new MockMediaBrowserServiceCompat.Proxy() {
@Override
public BrowserRoot onGetRoot(
String clientPackageName, int clientUid, Bundle rootHints) {
int actionLimit =
rootHints.getInt(
BROWSER_ROOT_HINTS_KEY_CUSTOM_BROWSER_ACTION_LIMIT,
/* defaultValue= */ browseActions.size());
Bundle extras = new Bundle(rootHints);
ArrayList<Bundle> browseActionList = new ArrayList<>();
for (int i = 0; i < min(actionLimit, browseActions.size()); i++) {
browseActionList.add(browseActions.get(i));
}
extras.putParcelableArrayList(
BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST, browseActionList);
session.setPlaybackState(
new PlaybackStateCompat.Builder()
.setState(
PlaybackStateCompat.STATE_PLAYING,
/* position= */ 123L,
/* playbackSpeed= */ 1.0f)
.addCustomAction(
new PlaybackStateCompat.CustomAction.Builder(
MediaBrowserConstants.COMMAND_PLAYLIST_ADD,
"Add to playlist",
CommandButton.ICON_PLAYLIST_ADD)
.build())
.addCustomAction(
new PlaybackStateCompat.CustomAction.Builder(
MediaBrowserConstants.COMMAND_RADIO,
"Radio station",
CommandButton.ICON_RADIO)
.build())
.build());
return new BrowserRoot(ROOT_ID, extras);
}
@Override
public void onLoadItem(String itemId, Result<MediaItem> result) {
Bundle extras = new Bundle();
ArrayList<String> supportedActions = new ArrayList<>();
supportedActions.add(MediaBrowserConstants.COMMAND_PLAYLIST_ADD);
supportedActions.add(MediaBrowserConstants.COMMAND_RADIO);
extras.putStringArrayList(
DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST, supportedActions);
MediaDescriptionCompat description =
new MediaDescriptionCompat.Builder()
.setMediaId(itemId)
.setExtras(extras)
.setTitle("title of " + itemId)
.build();
result.sendResult(new MediaItem(description, MediaItem.FLAG_PLAYABLE));
}
@Override
public void onCustomAction(String action, Bundle extras, Result<Bundle> result) {
if (action.equals(MediaBrowserConstants.COMMAND_PLAYLIST_ADD)
|| action.equals(MediaBrowserConstants.COMMAND_RADIO)) {
Bundle resultBundle = new Bundle();
if (extras.containsKey(
androidx.media3.session.legacy.MediaConstants
.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID)) {
resultBundle.putString(
MediaConstants.EXTRA_KEY_MEDIA_ID,
extras.getString(
androidx.media3.session.legacy.MediaConstants
.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID));
}
session.setExtras(resultBundle);
result.sendResult(resultBundle);
}
}
});
}
private void getChildren_authenticationError_receivesPlaybackException( private void getChildren_authenticationError_receivesPlaybackException(
MediaSessionCompat session, boolean isFatal) { MediaSessionCompat session, boolean isFatal) {
setMediaBrowserServiceProxy( setMediaBrowserServiceProxy(
@ -393,7 +516,7 @@ public class MockMediaBrowserServiceCompat extends MediaBrowserServiceCompat {
/* playbackSpeed= */ 1.0f) /* playbackSpeed= */ 1.0f)
.addCustomAction( .addCustomAction(
new PlaybackStateCompat.CustomAction.Builder( new PlaybackStateCompat.CustomAction.Builder(
MediaBrowserConstants.COMMAND_ACTION_PLAYLIST_ADD, MediaBrowserConstants.COMMAND_PLAYLIST_ADD,
"Add to playlist", "Add to playlist",
CommandButton.ICON_PLAYLIST_ADD) CommandButton.ICON_PLAYLIST_ADD)
.build()) .build())
@ -405,7 +528,7 @@ public class MockMediaBrowserServiceCompat extends MediaBrowserServiceCompat {
@Override @Override
public void onCustomAction(String action, Bundle extras, Result<Bundle> result) { public void onCustomAction(String action, Bundle extras, Result<Bundle> result) {
Bundle resultBundle = new Bundle(); Bundle resultBundle = new Bundle();
if (action.equals(MediaBrowserConstants.COMMAND_ACTION_PLAYLIST_ADD)) { if (action.equals(MediaBrowserConstants.COMMAND_PLAYLIST_ADD)) {
if (extras.getBoolean("request_error", /* defaultValue= */ false)) { if (extras.getBoolean("request_error", /* defaultValue= */ false)) {
resultBundle.putString("key-1", "error-from-service"); resultBundle.putString("key-1", "error-from-service");
result.sendError(resultBundle); result.sendError(resultBundle);

View File

@ -38,6 +38,7 @@ import static androidx.media3.test.session.common.MediaBrowserConstants.EXTRAS_K
import static androidx.media3.test.session.common.MediaBrowserConstants.GET_CHILDREN_RESULT; import static androidx.media3.test.session.common.MediaBrowserConstants.GET_CHILDREN_RESULT;
import static androidx.media3.test.session.common.MediaBrowserConstants.LONG_LIST_COUNT; import static androidx.media3.test.session.common.MediaBrowserConstants.LONG_LIST_COUNT;
import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_BROWSABLE_ITEM; import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_BROWSABLE_ITEM;
import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_ITEM_WITH_BROWSE_ACTIONS;
import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_ITEM_WITH_METADATA; import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_ITEM_WITH_METADATA;
import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_PLAYABLE_ITEM; import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_PLAYABLE_ITEM;
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID;
@ -67,6 +68,7 @@ import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.IBinder; import android.os.IBinder;
@ -79,6 +81,7 @@ import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.session.MediaSession.ControllerInfo; import androidx.media3.session.MediaSession.ControllerInfo;
import androidx.media3.test.session.common.CommonConstants; import androidx.media3.test.session.common.CommonConstants;
import androidx.media3.test.session.common.MediaBrowserConstants;
import androidx.media3.test.session.common.TestHandler; import androidx.media3.test.session.common.TestHandler;
import androidx.media3.test.session.common.TestUtils; import androidx.media3.test.session.common.TestUtils;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
@ -219,6 +222,10 @@ public class MockMediaLibraryService extends MediaLibraryService {
.getInt( .getInt(
CONNECTION_HINTS_KEY_LIBRARY_ERROR_REPLICATION_MODE, CONNECTION_HINTS_KEY_LIBRARY_ERROR_REPLICATION_MODE,
LIBRARY_ERROR_REPLICATION_MODE_FATAL); LIBRARY_ERROR_REPLICATION_MODE_FATAL);
Bundle playlistAddExtras = new Bundle();
playlistAddExtras.putString("key-1", "playlist_add");
Bundle radioExtras = new Bundle();
radioExtras.putString("key-1", "radio");
session = session =
new MediaLibrarySession.Builder( new MediaLibrarySession.Builder(
MockMediaLibraryService.this, MockMediaLibraryService.this,
@ -226,6 +233,23 @@ public class MockMediaLibraryService extends MediaLibraryService {
callback != null ? callback : new TestLibrarySessionCallback()) callback != null ? callback : new TestLibrarySessionCallback())
.setId(ID) .setId(ID)
.setLibraryErrorReplicationMode(libraryErrorReplicationMode) .setLibraryErrorReplicationMode(libraryErrorReplicationMode)
.setCommandButtonsForMediaItems(
ImmutableList.of(
new CommandButton.Builder(CommandButton.ICON_PLAYLIST_ADD)
.setDisplayName("Add to playlist")
.setIconUri(Uri.parse("http://www.example.com/icon/playlist_add"))
.setSessionCommand(
new SessionCommand(
MediaBrowserConstants.COMMAND_PLAYLIST_ADD, Bundle.EMPTY))
.setExtras(playlistAddExtras)
.build(),
new CommandButton.Builder(CommandButton.ICON_RADIO)
.setDisplayName("Radio station")
.setIconUri(Uri.parse("http://www.example.com/icon/radio"))
.setSessionCommand(
new SessionCommand(MediaBrowserConstants.COMMAND_RADIO, Bundle.EMPTY))
.setExtras(radioExtras)
.build()))
.build(); .build();
} }
return session; return session;
@ -333,6 +357,10 @@ public class MockMediaLibraryService extends MediaLibraryService {
return Futures.immediateFuture( return Futures.immediateFuture(
LibraryResult.ofItem( LibraryResult.ofItem(
createPlayableMediaItemWithArtworkData(mediaId), /* params= */ null)); createPlayableMediaItemWithArtworkData(mediaId), /* params= */ null));
case MEDIA_ID_GET_ITEM_WITH_BROWSE_ACTIONS:
return Futures.immediateFuture(
LibraryResult.ofItem(
createPlayableMediaItemWithBrowseActions(mediaId), /* params= */ null));
case MEDIA_ID_GET_ITEM_WITH_METADATA: case MEDIA_ID_GET_ITEM_WITH_METADATA:
return Futures.immediateFuture( return Futures.immediateFuture(
LibraryResult.ofItem(createMediaItemWithMetadata(mediaId), /* params= */ null)); LibraryResult.ofItem(createMediaItemWithMetadata(mediaId), /* params= */ null));
@ -589,11 +617,29 @@ public class MockMediaLibraryService extends MediaLibraryService {
mediaItem mediaItem
.mediaMetadata .mediaMetadata
.buildUpon() .buildUpon()
.setSupportedCommands(
ImmutableList.of(
MediaBrowserConstants.COMMAND_PLAYLIST_ADD,
MediaBrowserConstants.COMMAND_RADIO))
.setArtworkData(getArtworkData(), MediaMetadata.PICTURE_TYPE_FRONT_COVER) .setArtworkData(getArtworkData(), MediaMetadata.PICTURE_TYPE_FRONT_COVER)
.build(); .build();
return mediaItem.buildUpon().setMediaMetadata(mediaMetadataWithArtwork).build(); return mediaItem.buildUpon().setMediaMetadata(mediaMetadataWithArtwork).build();
} }
private MediaItem createPlayableMediaItemWithBrowseActions(String mediaId) {
MediaItem mediaItem = createPlayableMediaItem(mediaId);
MediaMetadata mediaMetadataWithBrowseActions =
mediaItem
.mediaMetadata
.buildUpon()
.setSupportedCommands(
ImmutableList.of(
MediaBrowserConstants.COMMAND_PLAYLIST_ADD,
MediaBrowserConstants.COMMAND_RADIO))
.build();
return mediaItem.buildUpon().setMediaMetadata(mediaMetadataWithBrowseActions).build();
}
private static MediaItem createPlayableMediaItem(String mediaId) { private static MediaItem createPlayableMediaItem(String mediaId) {
Bundle extras = new Bundle(); Bundle extras = new Bundle();
extras.putInt(EXTRAS_KEY_COMPLETION_STATUS, EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED); extras.putInt(EXTRAS_KEY_COMPLETION_STATUS, EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED);