From b8ec6b836bfcc06865e6682ea01583ed9209f3ff Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 25 Sep 2024 12:37:38 -0700 Subject: [PATCH] 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 --- .../media3/session/LegacyConversions.java | 99 ++++++++++++++ .../session/MediaBrowserImplLegacy.java | 41 +++++- .../MediaLibraryServiceLegacyStub.java | 17 +++ .../session/common/MediaBrowserConstants.java | 5 +- .../MediaBrowserServiceCompatConstants.java | 2 + ...wserCompatWithMediaLibraryServiceTest.java | 105 +++++++++++++++ ...enerWithMediaBrowserServiceCompatTest.java | 118 +++++++++++++++- .../media3/session/MediaControllerTest.java | 13 +- .../session/MediaSessionProviderService.java | 6 +- .../MockMediaBrowserServiceCompat.java | 127 +++++++++++++++++- .../session/MockMediaLibraryService.java | 46 +++++++ 11 files changed, 561 insertions(+), 18 deletions(-) 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);