Media Controls

Android 11 updates how media controls are displayed, making use of the MediaSession and MediaRouter2 APIs to render controls and audio output information.

Media controls in Android 11 are located near the Quick Settings. Sessions from multiple apps are arranged in a swipeable carousel. The carousel lists sessions in this order:

  • Streams playing locally on the phone
  • Remote streams, such as those detected on external devices or cast sessions
  • Previous resumable sessions, in the order they were last played

Users can restart previous sessions from the carousel without having to start the app. When playback begins, the user interacts with the media controls in the usual way.

Supporting playback resumption

In order to use this feature, you must enable Media resumption in the Developer Options settings.

To make your player app appear in the quick setting settings area, you must create a MediaStyle notification with a valid MediaSession token.

To display the brand icon for the media player, use NotificationBuilder.setSmallIcon().

To support playback resumption, apps must implement a MediaBrowserService and MediaSession.

The playback resumption feature can be be turned off using the Settings app, under the Sound > Media options. The user can also access Settings by tapping the gear icon that appears after swiping on the expanded carousel.

MediaBrowserService implementation

After the device boots, the system looks for the five most recently used media apps, and provides controls that can be used to restart playing from each app.

The system attempts to contact your MediaBrowserService with a connection from SystemUI. Your app must allow such connections, otherwise it cannot support playback resumption.

Connections from SystemUI can be identified and verified using the package name com.android.systemui and signature. The SystemUI is signed with the platform signature. An example of how to check against the platform signature can be found in the UAMP app.

In order to support playback resumption, your MediaBrowserService must implement these behaviors:

  • onGetRoot() must return a non-null root quickly. Other complex logic should be handled in onLoadChildren()

  • When onLoadChildren() is called on the root media ID, the result must contain a FLAG_PLAYABLE child.

  • MediaBrowserService should return the most recently played media item when they receive an EXTRA_RECENT query. The value returned should be an actual media item rather than generic function.

  • MediaBrowserService must provide an appropriate MediaDescription with a non-empty title and subtitle. It should also set an icon URI or an icon bitmap.

The following code examples illustrate how to implement onGetRoot().

Kotlin

override fun onGetRoot(
    clientPackageName: String,
    clientUid: Int,
    rootHints: Bundle?
): BrowserRoot? {
    ...
    // Verify that the specified package is SystemUI. You'll need to write your 
    // own logic to do this.
    if (isSystem(clientPackageName, clientUid)) {
        rootHints?.let {
            if (it.getBoolean(BrowserRoot.EXTRA_RECENT)) {
                // Return a tree with a single playable media item for resumption.
                val extras = Bundle().apply {
                    putBoolean(BrowserRoot.EXTRA_RECENT, true)
                }
                return BrowserRoot(MY_RECENTS_ROOT_ID, extras)
            }
        }
        // You can return your normal tree if the EXTRA_RECENT flag is not present.
        return BrowserRoot(MY_MEDIA_ROOT_ID, null)
    }
    // Return an empty tree to disallow browsing.
    return BrowserRoot(MY_EMPTY_ROOT_ID, null)

Java

@Override
public BrowserRoot onGetRoot(String clientPackageName, int clientUid,
    Bundle rootHints) {
    ...
    // Verify that the specified package is SystemUI. You'll need to write your
    // own logic to do this.
    if (isSystem(clientPackageName, clientUid)) {
        if (rootHints != null) {
            if (rootHints.getBoolean(BrowserRoot.EXTRA_RECENT)) {
                // Return a tree with a single playable media item for resumption.
                Bundle extras = new Bundle();
                extras.putBoolean(BrowserRoot.EXTRA_RECENT, true);
                return new BrowserRoot(MY_RECENTS_ROOT_ID, extras);
            }
        }
        // You can return your normal tree if the EXTRA_RECENT flag is not present.
        return new BrowserRoot(MY_MEDIA_ROOT_ID, null);
    }
    // Return an empty tree to disallow browsing.
    return new BrowserRoot(MY_EMPTY_ROOT_ID, null);
}

MediaSession implementation

The system retrieves the following information from the MediaSession's MediaMetadata, and displays it when it is available:

  • METADATA_KEY_ALBUM_ART_URI
  • METADATA_KEY_TITLE
  • METADATA_KEY_ARTIST
  • METADATA_KEY_DURATION (If the duration isn’t set the seek bar doesn't show progress)

In order to support play resumption, your MediaSession must implement a MediaSession callback for onPlay().

The media player shows the elapsed time for the currently playing media, along with a seek bar which is mapped to the MediaSession PlaybackState.

In order for the the seek bar to work correctly, you must implement PlaybackState.Builder#setActions and include ACTION_SEEK_TO. Otherwise the player only shows the elapsed time and duration.

To set the player controls, use Notification.Builder#setActions. Only the actions indicated with Notification.MediaStyle#setShowsActionsInCompactView will be displayed in the media player appearing in the collapsed quick settings.