diff --git a/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java b/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java
index 7f4de3b6bd..d5cf2d4db5 100644
--- a/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java
+++ b/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java
@@ -43,6 +43,7 @@ import static androidx.media3.common.util.Assertions.checkNotNull;
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.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.MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS;
import static java.lang.Math.max;
@@ -539,6 +540,15 @@ import java.util.concurrent.TimeoutException;
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
&& extras.containsKey(MediaConstants.EXTRAS_KEY_MEDIA_DESCRIPTION_COMPAT_TITLE)) {
builder.setTitle(
@@ -826,6 +836,14 @@ import java.util.concurrent.TimeoutException;
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 subtitle;
CharSequence description;
@@ -1629,6 +1647,87 @@ import java.util.concurrent.TimeoutException;
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.
+ *
+ *
See Custom
+ * Browse Actions for Automotive OS.
+ *
+ * @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.
+ *
+ *
See Custom
+ * Browse Actions for Automotive OS.
+ *
+ * @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 {
try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) {
bitmap.compress(Bitmap.CompressFormat.PNG, /* ignored */ 0, stream);
diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplLegacy.java b/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplLegacy.java
index ce7ce491ab..46fcab9ab2 100644
--- a/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplLegacy.java
+++ b/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplLegacy.java
@@ -15,10 +15,12 @@
*/
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_PERMISSION_DENIED;
import static androidx.media3.session.SessionError.ERROR_SESSION_DISCONNECTED;
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.os.Bundle;
@@ -50,11 +52,11 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
private static final String TAG = "MB2ImplLegacy";
private final HashMap browserCompats = new HashMap<>();
-
private final HashMap> subscribeCallbacks = new HashMap<>();
-
private final MediaBrowser instance;
+ private ImmutableMap commandButtonsForMediaItems;
+
MediaBrowserImplLegacy(
Context context,
@UnderInitialization MediaBrowser instance,
@@ -63,6 +65,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
BitmapLoader bitmapLoader) {
super(context, instance, token, applicationLooper, bitmapLoader);
this.instance = instance;
+ commandButtonsForMediaItems = ImmutableMap.of();
}
@Override
@@ -91,7 +94,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
@Override
public ImmutableMap getCommandButtonsForMediaItemsMap() {
- return ImmutableMap.of();
+ return commandButtonsForMediaItems;
}
@Override
@@ -376,10 +379,40 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
// Shouldn't be happen. Internal error?
result.set(LibraryResult.ofError(ERROR_UNKNOWN));
} else {
+ Bundle extras = browserCompat.getExtras();
+ if (extras != null) {
+ ArrayList parcelableArrayList =
+ extras.getParcelableArrayList(
+ BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST);
+ if (parcelableArrayList != null) {
+ @Nullable
+ ImmutableMap.Builder 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()
+ .putAll(commandButtonsForMediaItems);
+ }
+ String customAction = checkNotNull(commandButton.sessionCommand).customAction;
+ commandButtonsForMediaItemsBuilder.put(customAction, commandButton);
+ }
+ }
+ if (commandButtonsForMediaItemsBuilder != null) {
+ commandButtonsForMediaItems = commandButtonsForMediaItemsBuilder.buildKeepingLast();
+ }
+ }
+ }
result.set(
LibraryResult.ofItem(
createRootMediaItem(browserCompat),
- LegacyConversions.convertToLibraryParams(context, browserCompat.getExtras())));
+ LegacyConversions.convertToLibraryParams(context, extras)));
}
}
diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaLibraryServiceLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaLibraryServiceLegacyStub.java
index 1b2e608a39..9351e67703 100644
--- a/libraries/session/src/main/java/androidx/media3/session/MediaLibraryServiceLegacyStub.java
+++ b/libraries/session/src/main/java/androidx/media3/session/MediaLibraryServiceLegacyStub.java
@@ -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.legacy.MediaBrowserCompat.EXTRA_PAGE;
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 android.annotation.SuppressLint;
@@ -126,6 +127,22 @@ import java.util.concurrent.atomic.AtomicReference;
.isSessionCommandAvailable(controller, SessionCommand.COMMAND_CODE_LIBRARY_SEARCH);
checkNotNull(extras)
.putBoolean(BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, isSearchSessionCommandAvailable);
+ ImmutableList commandButtonsForMediaItems =
+ librarySessionImpl.getCommandButtonsForMediaItems();
+ if (!commandButtonsForMediaItems.isEmpty()) {
+ ArrayList 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);
}
// No library root, but keep browser compat connected to allow getting session unless the
diff --git a/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserConstants.java b/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserConstants.java
index 00e91f0d9a..6964ce022c 100644
--- a/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserConstants.java
+++ b/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserConstants.java
@@ -29,10 +29,13 @@ public class MediaBrowserConstants {
public static final String ROOT_EXTRAS_KEY = "root_extras_key";
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_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 PARENT_ID = "parent_id";
diff --git a/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserServiceCompatConstants.java b/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserServiceCompatConstants.java
index 367abe3ba9..e31fcd1d46 100644
--- a/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserServiceCompatConstants.java
+++ b/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserServiceCompatConstants.java
@@ -30,6 +30,8 @@ public class MediaBrowserServiceCompatConstants {
public static final String TEST_GET_CHILDREN_NON_FATAL_AUTHENTICATION_ERROR =
"getLibraryRoot_nonFatalAuthenticationError_receivesPlaybackException";
public static final String TEST_SEND_CUSTOM_COMMAND = "sendCustomCommand";
+ public static final String TEST_MEDIA_ITEMS_WITH_BROWSE_ACTIONS =
+ "getLibraryRoot_withBrowseActions";
private MediaBrowserServiceCompatConstants() {}
}
diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaLibraryServiceTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaLibraryServiceTest.java
index 02e895e7a4..1d25aa8ad5 100644
--- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaLibraryServiceTest.java
+++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaLibraryServiceTest.java
@@ -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.MockMediaLibraryService.CONNECTION_HINTS_CUSTOM_LIBRARY_ROOT;
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_ARTIST;
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.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_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_PLAYABLE_ITEM;
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.session.PlaybackStateCompat;
import android.text.TextUtils;
+import androidx.media3.test.session.common.MediaBrowserConstants;
import androidx.media3.test.session.common.TestUtils;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.ext.truth.os.BundleSubject;
@@ -114,6 +117,71 @@ public class MediaBrowserCompatWithMediaLibraryServiceTest
.getExtras()
.getInt(ROOT_EXTRAS_KEY, /* defaultValue= */ ROOT_EXTRAS_VALUE + 1))
.isEqualTo(ROOT_EXTRAS_VALUE);
+ ArrayList 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,
// extra binder, and extra messenger.
@@ -166,6 +234,35 @@ public class MediaBrowserCompatWithMediaLibraryServiceTest
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 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
public void getItem_metadata() throws Exception {
String mediaId = MEDIA_ID_GET_ITEM_WITH_METADATA;
@@ -295,6 +392,14 @@ public class MediaBrowserCompatWithMediaLibraryServiceTest
.isEqualTo(EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED);
assertThat(mediaItem.getDescription().getIconBitmap()).isNotNull();
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();
}
}
diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserListenerWithMediaBrowserServiceCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserListenerWithMediaBrowserServiceCompatTest.java
index 907598f698..5cb7633b97 100644
--- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserListenerWithMediaBrowserServiceCompatTest.java
+++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserListenerWithMediaBrowserServiceCompatTest.java
@@ -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_WITH_NULL_LIST;
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_SEND_CUSTOM_COMMAND;
import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+import static java.util.Objects.requireNonNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.junit.Assert.assertThrows;
import android.content.Context;
+import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.media.MediaBrowserServiceCompat;
import androidx.media3.common.MediaItem;
+import androidx.media3.common.MediaMetadata;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.Player;
import androidx.media3.session.MediaLibraryService.LibraryParams;
@@ -118,6 +122,114 @@ public class MediaBrowserListenerWithMediaBrowserServiceCompatTest {
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 libraryResult =
+ threadTestRule
+ .getHandler()
+ .postAndSync(() -> mediaBrowser.getLibraryRoot(new LibraryParams.Builder().build()))
+ .get();
+ assertThat(libraryResult.resultCode).isEqualTo(RESULT_SUCCESS);
+
+ ImmutableList 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 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 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
public void onChildrenChanged_subscribeAndUnsubscribe() throws Exception {
String testParentId = "testOnChildrenChanged";
@@ -358,8 +470,7 @@ public class MediaBrowserListenerWithMediaBrowserServiceCompatTest {
() ->
browser.sendCustomCommand(
new SessionCommand(
- MediaBrowserConstants.COMMAND_ACTION_PLAYLIST_ADD,
- /* extras= */ Bundle.EMPTY),
+ MediaBrowserConstants.COMMAND_PLAYLIST_ADD, /* extras= */ Bundle.EMPTY),
/* args= */ Bundle.EMPTY));
Futures.addCallback(
resultFuture,
@@ -397,8 +508,7 @@ public class MediaBrowserListenerWithMediaBrowserServiceCompatTest {
() ->
browser.sendCustomCommand(
new SessionCommand(
- MediaBrowserConstants.COMMAND_ACTION_PLAYLIST_ADD,
- /* extras= */ Bundle.EMPTY),
+ MediaBrowserConstants.COMMAND_PLAYLIST_ADD, /* extras= */ Bundle.EMPTY),
args));
Futures.addCallback(
resultFuture,
diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java
index dec5175b8d..327292de0a 100644
--- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java
+++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java
@@ -61,6 +61,7 @@ import androidx.media3.common.VideoSize;
import androidx.media3.test.session.R;
import androidx.media3.test.session.common.HandlerThreadTestRule;
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.TestUtils;
import androidx.test.core.app.ApplicationProvider;
@@ -557,11 +558,12 @@ public class MediaControllerTest {
CommandButton playlistAddButton =
new CommandButton.Builder(CommandButton.ICON_PLAYLIST_ADD)
.setSessionCommand(
- new SessionCommand("androidx.media3.actions.playlist_add", Bundle.EMPTY))
+ new SessionCommand(MediaBrowserConstants.COMMAND_PLAYLIST_ADD, Bundle.EMPTY))
.build();
CommandButton radioButton =
new CommandButton.Builder(CommandButton.ICON_RADIO)
- .setSessionCommand(new SessionCommand("androidx.media3.actions.radio", Bundle.EMPTY))
+ .setSessionCommand(
+ new SessionCommand(MediaBrowserConstants.COMMAND_RADIO, Bundle.EMPTY))
.build();
MediaItem mediaItem =
new MediaItem.Builder()
@@ -570,8 +572,8 @@ public class MediaControllerTest {
new MediaMetadata.Builder()
.setSupportedCommands(
ImmutableList.of(
- "androidx.media3.actions.playlist_add",
- "androidx.media3.actions.radio",
+ MediaBrowserConstants.COMMAND_PLAYLIST_ADD,
+ MediaBrowserConstants.COMMAND_RADIO,
"invalid"))
.build())
.build();
@@ -595,7 +597,8 @@ public class MediaControllerTest {
.setMediaId("mediaId-1")
.setMediaMetadata(
new MediaMetadata.Builder()
- .setSupportedCommands(ImmutableList.of("androidx.media3.actions.playlist_add"))
+ .setSupportedCommands(
+ ImmutableList.of(MediaBrowserConstants.COMMAND_PLAYLIST_ADD))
.build())
.build();
CountDownLatch latch = new CountDownLatch(/* count= */ 1);
diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java
index 22dd0f2ac2..f2aee71628 100644
--- a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java
+++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java
@@ -103,6 +103,7 @@ import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util;
import androidx.media3.session.MediaSession.ControllerInfo;
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.TestHandler;
import androidx.media3.test.session.common.TestHandler.TestRunnable;
@@ -240,12 +241,13 @@ public class MediaSessionProviderService extends Service {
CommandButton playlistAddButton =
new CommandButton.Builder(CommandButton.ICON_PLAYLIST_ADD)
.setSessionCommand(
- new SessionCommand("androidx.media3.actions.playlist_add", Bundle.EMPTY))
+ new SessionCommand(
+ MediaBrowserConstants.COMMAND_PLAYLIST_ADD, Bundle.EMPTY))
.build();
CommandButton radioButton =
new CommandButton.Builder(CommandButton.ICON_RADIO)
.setSessionCommand(
- new SessionCommand("androidx.media3.actions.radio", Bundle.EMPTY))
+ new SessionCommand(MediaBrowserConstants.COMMAND_RADIO, Bundle.EMPTY))
.build();
builder.setCommandButtonsForMediaItems(
ImmutableList.of(playlistAddButton, radioButton));
diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaBrowserServiceCompat.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaBrowserServiceCompat.java
index 58f0a24f69..05362358bf 100644
--- a/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaBrowserServiceCompat.java
+++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaBrowserServiceCompat.java
@@ -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_NOT_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.MediaBrowserConstants.ROOT_EXTRAS;
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_WITH_NULL_LIST;
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_SEND_CUSTOM_COMMAND;
+import static java.lang.Math.min;
import android.content.Intent;
import android.net.Uri;
@@ -51,6 +56,7 @@ import androidx.media3.test.session.common.MediaBrowserConstants;
import androidx.media3.test.session.common.MediaBrowserServiceCompatConstants;
import com.google.common.collect.ImmutableList;
import java.lang.reflect.Method;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -276,6 +282,9 @@ public class MockMediaBrowserServiceCompat extends MediaBrowserServiceCompat {
case TEST_SEND_CUSTOM_COMMAND:
setProxyForTestSendCustomCommand();
break;
+ case TEST_MEDIA_ITEMS_WITH_BROWSE_ACTIONS:
+ setProxyForMediaItemsWithBrowseActions(session);
+ break;
default:
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 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 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 result) {
+ Bundle extras = new Bundle();
+ ArrayList 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 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(
MediaSessionCompat session, boolean isFatal) {
setMediaBrowserServiceProxy(
@@ -393,7 +516,7 @@ public class MockMediaBrowserServiceCompat extends MediaBrowserServiceCompat {
/* playbackSpeed= */ 1.0f)
.addCustomAction(
new PlaybackStateCompat.CustomAction.Builder(
- MediaBrowserConstants.COMMAND_ACTION_PLAYLIST_ADD,
+ MediaBrowserConstants.COMMAND_PLAYLIST_ADD,
"Add to playlist",
CommandButton.ICON_PLAYLIST_ADD)
.build())
@@ -405,7 +528,7 @@ public class MockMediaBrowserServiceCompat extends MediaBrowserServiceCompat {
@Override
public void onCustomAction(String action, Bundle extras, Result result) {
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)) {
resultBundle.putString("key-1", "error-from-service");
result.sendError(resultBundle);
diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java
index ac918345e6..8282e43ffd 100644
--- a/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java
+++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java
@@ -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.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_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_PLAYABLE_ITEM;
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID;
@@ -67,6 +68,7 @@ import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
+import android.net.Uri;
import android.os.Bundle;
import android.os.HandlerThread;
import android.os.IBinder;
@@ -79,6 +81,7 @@ import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util;
import androidx.media3.session.MediaSession.ControllerInfo;
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.TestUtils;
import com.google.common.collect.ImmutableList;
@@ -219,6 +222,10 @@ public class MockMediaLibraryService extends MediaLibraryService {
.getInt(
CONNECTION_HINTS_KEY_LIBRARY_ERROR_REPLICATION_MODE,
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 =
new MediaLibrarySession.Builder(
MockMediaLibraryService.this,
@@ -226,6 +233,23 @@ public class MockMediaLibraryService extends MediaLibraryService {
callback != null ? callback : new TestLibrarySessionCallback())
.setId(ID)
.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();
}
return session;
@@ -333,6 +357,10 @@ public class MockMediaLibraryService extends MediaLibraryService {
return Futures.immediateFuture(
LibraryResult.ofItem(
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:
return Futures.immediateFuture(
LibraryResult.ofItem(createMediaItemWithMetadata(mediaId), /* params= */ null));
@@ -589,11 +617,29 @@ public class MockMediaLibraryService extends MediaLibraryService {
mediaItem
.mediaMetadata
.buildUpon()
+ .setSupportedCommands(
+ ImmutableList.of(
+ MediaBrowserConstants.COMMAND_PLAYLIST_ADD,
+ MediaBrowserConstants.COMMAND_RADIO))
.setArtworkData(getArtworkData(), MediaMetadata.PICTURE_TYPE_FRONT_COVER)
.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) {
Bundle extras = new Bundle();
extras.putInt(EXTRAS_KEY_COMPLETION_STATUS, EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED);