Implement custom browse actions

Similar to how you use custom playback actions to support unique capabilities in the playback view, you can use custom browse actions to support unique capabilities in browsing views. For example, you can use custom browse actions so that users can download playlists or add an item to a queue.

When more custom actions exist than displayed by the Original Equipment Manufacturer (OEM), an overflow menu is displayed to the user. Each custom browse action is defined with an:

  • Action ID: Unique string identifier
  • Action label: Text displayed to the user
  • Action icon uniform resource identifier (URI): Vector drawable that can be tintable

Custom browse action overflow

Figure 1. Custom browse action overflow.

You define a list of custom browse actions globally as part of your BrowseRoot. Then attach a subset of these actions to individual MediaItem.

When a user interacts with a custom browse action, your app receives a callback in onCustomAction. You then handle the action and update the list of actions for the MediaItem, if necessary. This is useful for stateful actions like Favorite and Download. For actions that need no updating, such as Play Radio, you needn't update the list of actions.

Custom browse action toolbar

Figure 2. Custom browse action toolbar.

You can also attach custom browse actions to a browse node root. These actions are displayed in a secondary toolbar under the primary toolbar.

To add custom browse actions to your app:

  1. Override two methods in your MediaBrowserServiceCompat implementation:

  2. Parse the action limits at runtime:

    In onGetRoot, get the maximum number of actions allowed for each MediaItem using the key BROWSER_ROOT_HINTS_KEY_CUSTOM_BROWSER_ACTION_LIMIT in the rootHints Bundle. A limit of 0 indicates that the feature is not supported by the system.

  3. Build the global list of custom browse actions. For each action, create a Bundle object with these keys:

    • Action ID EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID
    • Action label EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL
    • Action icon URI EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI
  4. Add all the action Bundle objects to a list.

  5. Add the global list to your BrowseRoot. In the BrowseRoot extras Bundle, add the list of actions as a Parcelable ArrayList using the key BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST.

  6. Add actions to your MediaItem objects. You can add actions to individual MediaItem objects by including the list of action IDs in the MediaDescriptionCompat extras using the key DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST. This list must be a subset of the global list of actions you defined in the BrowseRoot.

  7. Handle actions and return progress or results:

Update the action state

To override these methods in MediaBrowserServiceCompat:

public void onLoadItem(String itemId, @NonNull Result<MediaBrowserCompat.MediaItem> result)

and

public void onCustomAction(@NonNull String action, Bundle extras, @NonNull Result<Bundle> result)

Parse actions limit

Check how many custom browse actions are supported:

public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, Bundle rootHints) {
    rootHints.getInt(
            MediaConstants.BROWSER_ROOT_HINTS_KEY_CUSTOM_BROWSER_ACTION_LIMIT, 0)
}

Build a custom browse action

Each action needs to be packed into a separate Bundle.

  • Action ID:

    bundle.putString(MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID,
                    "<ACTION_ID>")
    
  • Action label:

    bundle.putString(MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL,
                    "<ACTION_LABEL>")
    
  • Action icon URI:

    bundle.putString(MediaConstants.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI,
                    "<ACTION_ICON_URI>")
    

Add custom browse actions to Parcelable ArrayList

Add all custom browse action Bundle objects into an ArrayList:

private ArrayList<Bundle> createCustomActionsList(
                                        CustomBrowseAction browseActions) {
    ArrayList<Bundle> browseActionsBundle = new ArrayList<>();
    for (CustomBrowseAction browseAction : browseActions) {
        Bundle action = new Bundle();
        action.putString(EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID,
                browseAction.mId);
        action.putString(EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL,
                getString(browseAction.mLabelResId));
        action.putString(EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI,
                browseAction.mIcon);
        browseActionsBundle.add(action);
    }
    return browseActionsBundle;
}

Add custom browse action list to browse root

public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid,
                             Bundle rootHints) {
    Bundle browserRootExtras = new Bundle();
    browserRootExtras.putParcelableArrayList(
            BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST,
            createCustomActionsList()));
    mRoot = new BrowserRoot(ROOT_ID, browserRootExtras);
    return mRoot;
}

Add actions to a MediaItem

Browse Actions IDs in a MediaItem need to be a subset of the global list of Browse Actions given on onGetRoot. Actions not in the global list are ignored.

MediaDescriptionCompat buildDescription (long id, String title, String subtitle,
                String description, Uri iconUri, Uri mediaUri,
                ArrayList<String> browseActionIds) {

    MediaDescriptionCompat.Builder bob = new MediaDescriptionCompat.Builder();
    bob.setMediaId(id);
    bob.setTitle(title);
    bob.setSubtitle(subtitle);
    bob.setDescription(description);
    bob.setIconUri(iconUri);
    bob.setMediaUri(mediaUri);

    Bundle extras = new Bundle();
    extras.putStringArrayList(
          DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST,
          browseActionIds);

    bob.setExtras(extras);
    return bob.build();
}
MediaItem mediaItem = new MediaItem(buildDescription(...), flags);

Build onCustomAction result

To build the result:

  1. Parse mediaId from Bundle extras

    @Override
    public void onCustomAction(
                @NonNull String action, Bundle extras, @NonNull Result<Bundle> result){
        String mediaId = extras.getString(MediaConstans.EXTRAS_KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID);
                }
    
  2. For asynchronous results, detach the result, result.detach.

  3. Build the result bundle:

    1. Display a message to the user:

      mResultBundle.putString(EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE,
                    mContext.getString(stringRes))
      
    2. Update the item (use to update actions in an item):

      mResultBundle.putString(EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM, mediaId);
      
    3. Open the playback view:

      //Shows user the PBV without changing the playback state
      mResultBundle.putString(EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_SHOW_PLAYING_ITEM, null);
      
    4. Update the browse node:

      //Change current browse node to mediaId
      mResultBundle.putString(EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_BROWSE_NODE, mediaId);
      
  4. Check the result:

    • Error: Call result.sendError(resultBundle)
    • Progress update: Call result.sendProgressUpdate(resultBundle)
    • Finish: Call result.sendResult(resultBundle)

Update the action state

By using the result.sendProgressUpdate(resultBundle) method with the EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM key, you can update the MediaItem to reflect the new state of the action. This lets you provide real-time feedback to the user about the progress and result of their action.

Sample download action

This example describes how you can use this feature to implement a download action with three states:

  • Download is the initial state of the action. When the user selects this action, you can swap it with Downloading and call sendProgressUpdate to update the user interface (UI).

  • Downloading state indicates that the download is in progress. You can use this state to show a progress bar or another indicator to the user.

  • Downloaded state indicates that the download is complete. When the download finishes, you can swap Downloading with Downloaded and call sendResult with the EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM key to indicate that the item should be refreshed. Additionally, you can use the EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE key to display a success message to the user.

This approach lets you provide clear feedback to the user about the download process and its current state. You can add more detail with icons to show 25%, 50%, and 75% download states.

Sample favorite action

Another example is a favorite action with two states:

This approach provides a clear and consistent way for users to manage their favorite items. These examples showcase the flexibility of custom browse actions and how you can use them to implement a variety of functionalities with real-time feedback for an enhanced user experience in the car's Media app.

You can see a comprehensive sample implementation of this feature in the TestMediaApp project.