Commandes multimédias

Sur Android, les commandes multimédias se trouvent près des Réglages rapides. Les sessions provenant de plusieurs applications sont organisées dans un carrousel à faire glisser. Le carrousel liste les sessions dans cet ordre:

  • Flux lus localement sur le téléphone
  • les flux distants, tels que ceux détectés sur des appareils externes ou des sessions de diffusion ;
  • Sessions précédentes avec reprise, dans l'ordre de la dernière exécution

À partir d'Android 13 (niveau d'API 33), pour que les utilisateurs puissent accéder à un ensemble complet de commandes multimédias pour les applications qui lisent des contenus multimédias, les boutons d'action des commandes multimédias sont dérivés de l'état Player.

De cette façon, vous pouvez présenter un ensemble cohérent de commandes multimédias et une expérience plus soignée sur tous les appareils.

La figure 1 montre à quoi cela ressemble sur un téléphone et une tablette.

Affichage des commandes multimédias sur les téléphones et les tablettes, à l'aide d'un exemple de piste montrant comment les boutons peuvent s'afficher
Figure 1 : Commandes multimédias sur les téléphones et les tablettes

Le système affiche jusqu'à cinq boutons d'action en fonction de l'état Player, comme décrit dans le tableau suivant. En mode compact, seuls les trois premiers emplacements d'action sont affichés. Cela s'aligne sur la façon dont les commandes multimédias sont affichées sur d'autres plates-formes Android telles qu'Auto, Assistant et Wear OS.

Encoche Critères Action
1 playWhenReady est "false", ou l'état de lecture actuel est STATE_ENDED. Lire
playWhenReady est "true" et l'état de la lecture actuel est STATE_BUFFERING. Boucle de chargement
playWhenReady est "true" et l'état de la lecture actuel est STATE_READY. Suspendre
2 La commande du lecteur COMMAND_SEEK_TO_PREVIOUS ou COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM est disponible. Précédent
Ni la commande du lecteur (COMMAND_SEEK_TO_PREVIOUS, ni COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) n'est disponible, et une commande personnalisée issue de la mise en page personnalisée qui n'a pas encore été placée est disponible pour occuper l'emplacement. Personnalisé
(Pas encore disponible avec Media3) Les extras PlaybackState incluent une valeur booléenne true pour la clé EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV. Vide
3 La commande du lecteur COMMAND_SEEK_TO_NEXT ou COMMAND_SEEK_TO_NEXT_MEDIA_ITEM est disponible. Suivant
Ni la commande du lecteur (COMMAND_SEEK_TO_NEXT, ni COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) n'est disponible, et une commande personnalisée issue de la mise en page personnalisée qui n'a pas encore été placée est disponible pour occuper l'emplacement. Personnalisé
(Pas encore disponible avec Media3) Les extras PlaybackState incluent une valeur booléenne true pour la clé EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT. Vide
4 Une commande personnalisée issue de la mise en page personnalisée qui n'a pas encore été placée est disponible pour remplir l'emplacement. Personnalisé
5 Une commande personnalisée issue de la mise en page personnalisée qui n'a pas encore été placée est disponible pour remplir l'emplacement. Personnalisé

Les commandes personnalisées sont placées dans l'ordre dans lequel elles ont été ajoutées à la mise en page personnalisée.

Personnaliser les boutons de commande

Pour personnaliser les commandes multimédias système avec Jetpack Media3, vous pouvez définir la mise en page personnalisée de la session et les commandes disponibles des contrôleurs en conséquence, lors de l'implémentation d'un MediaSessionService:

  1. Dans onCreate(), créez une MediaSession et définissez la mise en page personnalisée des boutons de commande.

  2. Dans MediaSession.Callback.onConnect(), autorisez les manettes en définissant les commandes disponibles, y compris les commandes personnalisées, dans le ConnectionResult.

  3. Dans MediaSession.Callback.onCustomCommand(), répondez à la commande personnalisée sélectionnée par l'utilisateur.

Kotlin

class PlaybackService : MediaSessionService() {
  private val customCommandFavorites = SessionCommand(ACTION_FAVORITES, Bundle.EMPTY)
  private var mediaSession: MediaSession? = null

  override fun onCreate() {
    super.onCreate()
    val favoriteButton =
      CommandButton.Builder()
        .setDisplayName("Save to favorites")
        .setIconResId(R.drawable.favorite_icon)
        .setSessionCommand(customCommandFavorites)
        .build()
    val player = ExoPlayer.Builder(this).build()
    // Build the session with a custom layout.
    mediaSession =
      MediaSession.Builder(this, player)
        .setCallback(MyCallback())
        .setCustomLayout(ImmutableList.of(favoriteButton))
        .build()
  }

  private inner class MyCallback : MediaSession.Callback {
    override fun onConnect(
      session: MediaSession,
      controller: MediaSession.ControllerInfo
    ): ConnectionResult {
    // Set available player and session commands.
    return AcceptedResultBuilder(session)
      .setAvailablePlayerCommands(
        ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
          .remove(COMMAND_SEEK_TO_NEXT)
          .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
          .remove(COMMAND_SEEK_TO_PREVIOUS)
          .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
          .build()
      )
      .setAvailableSessionCommands(
        ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
          .add(customCommandFavorites)
          .build()
      )
      .build()
    }

    override fun onCustomCommand(
      session: MediaSession,
      controller: MediaSession.ControllerInfo,
      customCommand: SessionCommand,
      args: Bundle
    ): ListenableFuture {
      if (customCommand.customAction == ACTION_FAVORITES) {
        // Do custom logic here
        saveToFavorites(session.player.currentMediaItem)
        return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
      }
      return super.onCustomCommand(session, controller, customCommand, args)
    }
  }
}

Java

public class PlaybackService extends MediaSessionService {
  private static final SessionCommand CUSTOM_COMMAND_FAVORITES =
      new SessionCommand("ACTION_FAVORITES", Bundle.EMPTY);
  @Nullable private MediaSession mediaSession;

  public void onCreate() {
    super.onCreate();
    CommandButton favoriteButton =
        new CommandButton.Builder()
            .setDisplayName("Save to favorites")
            .setIconResId(R.drawable.favorite_icon)
            .setSessionCommand(CUSTOM_COMMAND_FAVORITES)
            .build();
    Player player = new ExoPlayer.Builder(this).build();
    // Build the session with a custom layout.
    mediaSession =
        new MediaSession.Builder(this, player)
            .setCallback(new MyCallback())
            .setCustomLayout(ImmutableList.of(favoriteButton))
            .build();
  }

  private static class MyCallback implements MediaSession.Callback {
    @Override
    public ConnectionResult onConnect(
        MediaSession session, MediaSession.ControllerInfo controller) {
      // Set available player and session commands.
      return new AcceptedResultBuilder(session)
          .setAvailablePlayerCommands(
              ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
                .remove(COMMAND_SEEK_TO_NEXT)
                .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
                .remove(COMMAND_SEEK_TO_PREVIOUS)
                .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
                .build())
          .setAvailableSessionCommands(
              ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
                .add(CUSTOM_COMMAND_FAVORITES)
                .build())
          .build();
    }

    public ListenableFuture onCustomCommand(
        MediaSession session,
        MediaSession.ControllerInfo controller,
        SessionCommand customCommand,
        Bundle args) {
      if (customCommand.customAction.equals(CUSTOM_COMMAND_FAVORITES.customAction)) {
        // Do custom logic here
        saveToFavorites(session.getPlayer().getCurrentMediaItem());
        return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS));
      }
      return MediaSession.Callback.super.onCustomCommand(
          session, controller, customCommand, args);
    }
  }
}

Pour en savoir plus sur la configuration de votre MediaSession afin que les clients tels que le système puissent se connecter à votre application multimédia, consultez Accorder le contrôle à d'autres clients.

Avec Jetpack Media3, lorsque vous implémentez un MediaSession, votre PlaybackState est automatiquement mis à jour avec le lecteur multimédia. De même, lorsque vous implémentez un MediaSessionService, la bibliothèque publie automatiquement une notification MediaStyle pour vous et la maintient à jour.

Répondre aux boutons d'action

Lorsqu'un utilisateur appuie sur un bouton d'action dans les commandes multimédias du système, le MediaController du système envoie une commande de lecture à votre MediaSession. MediaSession délègue ensuite ces commandes au joueur. Les commandes définies dans l'interface Player de Media3 sont automatiquement gérées par la session multimédia.

Reportez-vous à la section Ajouter des commandes personnalisées pour savoir comment répondre à une commande personnalisée.

Comportement des versions antérieures à Android 13

Pour assurer la rétrocompatibilité, l'UI système continue de fournir une autre mise en page qui utilise des actions de notification pour les applications qui ne sont pas mises à jour pour cibler Android 13 ou qui n'incluent pas d'informations PlaybackState. Les boutons d'action sont issus de la liste Notification.Action jointe à la notification MediaStyle. Le système affiche jusqu'à cinq actions dans l'ordre dans lequel elles ont été ajoutées. En mode compact, jusqu'à trois boutons sont affichés. Ils sont déterminés par les valeurs transmises dans setShowActionsInCompactView().

Les actions personnalisées sont placées dans l'ordre dans lequel elles ont été ajoutées au PlaybackState.

L'exemple de code suivant montre comment ajouter des actions à la notification MediaStyle :

Kotlin

import androidx.core.app.NotificationCompat
import androidx.media3.session.MediaStyleNotificationHelper

var notification = NotificationCompat.Builder(context, CHANNEL_ID)
        // Show controls on lock screen even when user hides sensitive content.
        .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
        .setSmallIcon(R.drawable.ic_stat_player)
        // Add media control buttons that invoke intents in your media service
        .addAction(R.drawable.ic_prev, "Previous", prevPendingIntent) // #0
        .addAction(R.drawable.ic_pause, "Pause", pausePendingIntent) // #1
        .addAction(R.drawable.ic_next, "Next", nextPendingIntent) // #2
        // Apply the media style template
        .setStyle(MediaStyleNotificationHelper.MediaStyle(mediaSession)
                .setShowActionsInCompactView(1 /* #1: pause button */))
        .setContentTitle("Wonderful music")
        .setContentText("My Awesome Band")
        .setLargeIcon(albumArtBitmap)
        .build()

Java

import androidx.core.app.NotificationCompat;
import androidx.media3.session.MediaStyleNotificationHelper;

NotificationCompat.Builder notification = new NotificationCompat.Builder(context, CHANNEL_ID)
        .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
        .setSmallIcon(R.drawable.ic_stat_player)
        .addAction(R.drawable.ic_prev, "Previous", prevPendingIntent)
        .addAction(R.drawable.ic_pause, "Pause", pausePendingIntent)
        .addAction(R.drawable.ic_next, "Next", nextPendingIntent)
        .setStyle(new MediaStyleNotificationHelper.MediaStyle(mediaSession)
                .setShowActionsInCompactView(1 /* #1: pause button */))
        .setContentTitle("Wonderful music")
        .setContentText("My Awesome Band")
        .setLargeIcon(albumArtBitmap)
        .build();

Prendre en charge la reprise du contenu multimédia

La reprise du contenu multimédia permet aux utilisateurs de redémarrer les sessions précédentes à partir du carrousel sans avoir à démarrer l'application. Lorsque la lecture commence, l'utilisateur interagit avec les commandes multimédias de la manière habituelle.

Vous pouvez activer et désactiver la fonctionnalité de reprise de la lecture à l'aide de l'application Paramètres, sous les options Son > Multimédia. L'utilisateur peut également accéder aux paramètres en appuyant sur l'icône en forme de roue dentée qui apparaît après avoir balayé le carrousel développé.

Media3 propose des API qui facilitent la prise en charge de la reprise multimédia. Consultez la documentation Reprise de la lecture avec Media3 pour obtenir des conseils sur la mise en œuvre de cette fonctionnalité.

Utiliser les anciennes API multimédias

Cette section explique comment intégrer les commandes multimédias du système à l'aide des anciennes API MediaCompat.

Le système récupère les informations suivantes à partir du MediaMetadata de MediaSession et les affiche lorsqu'elles sont disponibles:

  • METADATA_KEY_ALBUM_ART_URI
  • METADATA_KEY_TITLE
  • METADATA_KEY_DISPLAY_TITLE
  • METADATA_KEY_ARTIST
  • METADATA_KEY_DURATION (si la durée n'est pas définie, la barre de recherche n'affiche pas de progression)

Pour vous assurer de disposer d'une notification de commandes multimédias valide et précise, définissez la valeur des métadonnées METADATA_KEY_TITLE ou METADATA_KEY_DISPLAY_TITLE sur le titre du contenu multimédia en cours de lecture.

Le lecteur multimédia affiche le temps écoulé pour le contenu multimédia en cours de lecture, ainsi qu'une barre de recherche mappée à l'élément MediaSession PlaybackState.

Le lecteur multimédia affiche la progression du contenu multimédia en cours de lecture, ainsi qu'une barre de recherche mappée à la PlaybackState MediaSession. La barre de recherche permet aux utilisateurs de modifier la position et affiche le temps écoulé pour l'élément multimédia. Pour activer la barre de recherche, vous devez implémenter PlaybackState.Builder#setActions et inclure ACTION_SEEK_TO.

Encoche Action Critères
1 Lire L'état actuel de la PlaybackState est l'un des suivants :
  • STATE_NONE
  • STATE_STOPPED
  • STATE_PAUSED
  • STATE_ERROR
Boucle de chargement L'état actuel de la PlaybackState est l'un des suivants :
  • STATE_CONNECTING
  • STATE_BUFFERING
Suspendre L'état actuel de PlaybackState n'est aucun des éléments ci-dessus.
2 Précédent Les actions PlaybackState incluent ACTION_SKIP_TO_PREVIOUS.
Personnalisé Les actions PlaybackState n'incluent pas ACTION_SKIP_TO_PREVIOUS et les actions personnalisées PlaybackState incluent une action personnalisée qui n'a pas encore été placée.
Vide Les extras PlaybackState incluent une valeur booléenne true pour la clé SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV.
3 Suivant Les actions PlaybackState incluent ACTION_SKIP_TO_NEXT.
Personnalisé Les actions PlaybackState n'incluent pas ACTION_SKIP_TO_NEXT et les actions personnalisées PlaybackState incluent une action personnalisée qui n'a pas encore été placée.
Vide Les extras PlaybackState incluent une valeur booléenne true pour la clé SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT.
4 Personnalisé Les actions personnalisées PlaybackState incluent une action personnalisée qui n'a pas encore été placée.
5 Personnalisé Les actions personnalisées PlaybackState incluent une action personnalisée qui n'a pas encore été placée.

Ajouter des actions standards

Les exemples de code suivants montrent comment ajouter des actions standards et personnalisées PlaybackState.

Pour lire, mettre en pause, précédent et suivant, définissez ces actions dans le PlaybackState de la session multimédia.

Kotlin

val session = MediaSessionCompat(context, TAG)
val playbackStateBuilder = PlaybackStateCompat.Builder()
val style = NotificationCompat.MediaStyle()

// For this example, the media is currently paused:
val state = PlaybackStateCompat.STATE_PAUSED
val position = 0L
val playbackSpeed = 1f
playbackStateBuilder.setState(state, position, playbackSpeed)

// And the user can play, skip to next or previous, and seek
val stateActions = PlaybackStateCompat.ACTION_PLAY
    or PlaybackStateCompat.ACTION_PLAY_PAUSE
    or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
    or PlaybackStateCompat.ACTION_SKIP_TO_NEXT
    or PlaybackStateCompat.ACTION_SEEK_TO // adding the seek action enables seeking with the seekbar
playbackStateBuilder.setActions(stateActions)

// ... do more setup here ...

session.setPlaybackState(playbackStateBuilder.build())
style.setMediaSession(session.sessionToken)
notificationBuilder.setStyle(style)

Java

MediaSessionCompat session = new MediaSessionCompat(context, TAG);
PlaybackStateCompat.Builder playbackStateBuilder = new PlaybackStateCompat.Builder();
NotificationCompat.MediaStyle style = new NotificationCompat.MediaStyle();

// For this example, the media is currently paused:
int state = PlaybackStateCompat.STATE_PAUSED;
long position = 0L;
float playbackSpeed = 1f;
playbackStateBuilder.setState(state, position, playbackSpeed);

// And the user can play, skip to next or previous, and seek
long stateActions = PlaybackStateCompat.ACTION_PLAY
    | PlaybackStateCompat.ACTION_PLAY_PAUSE
    | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
    | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
    | PlaybackStateCompat.ACTION_SEEK_TO; // adding this enables the seekbar thumb
playbackStateBuilder.setActions(stateActions);

// ... do more setup here ...

session.setPlaybackState(playbackStateBuilder.build());
style.setMediaSession(session.getSessionToken());
notificationBuilder.setStyle(style);

Si vous ne voulez pas de boutons dans les emplacements précédents ou suivants, n'ajoutez pas ACTION_SKIP_TO_PREVIOUS ni ACTION_SKIP_TO_NEXT, et ajoutez plutôt des extras à la session:

Kotlin

session.setExtras(Bundle().apply {
    putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true)
    putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true)
})

Java

Bundle extras = new Bundle();
extras.putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true);
extras.putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true);
session.setExtras(extras);

Ajouter des actions personnalisées

Pour les autres actions que vous souhaitez afficher sur les commandes multimédias, vous pouvez créer un PlaybackStateCompat.CustomAction et l'ajouter au PlaybackState. Ces actions s'affichent dans l'ordre dans lequel elles ont été ajoutées.

Kotlin

val customAction = PlaybackStateCompat.CustomAction.Builder(
    "com.example.MY_CUSTOM_ACTION", // action ID
    "Custom Action", // title - used as content description for the button
    R.drawable.ic_custom_action
).build()

playbackStateBuilder.addCustomAction(customAction)

Java

PlaybackStateCompat.CustomAction customAction = new PlaybackStateCompat.CustomAction.Builder(
        "com.example.MY_CUSTOM_ACTION", // action ID
        "Custom Action", // title - used as content description for the button
        R.drawable.ic_custom_action
).build();

playbackStateBuilder.addCustomAction(customAction);

Répondre aux actions PlaybackState

Lorsqu'un utilisateur appuie sur un bouton, SystemUI utilise MediaController.TransportControls pour renvoyer une commande à MediaSession. Vous devez enregistrer un rappel capable de répondre correctement à ces événements.

Kotlin

val callback = object: MediaSession.Callback() {
    override fun onPlay() {
        // start playback
    }

    override fun onPause() {
        // pause playback
    }

    override fun onSkipToPrevious() {
        // skip to previous
    }

    override fun onSkipToNext() {
        // skip to next
    }

    override fun onSeekTo(pos: Long) {
        // jump to position in track
    }

    override fun onCustomAction(action: String, extras: Bundle?) {
        when (action) {
            CUSTOM_ACTION_1 -> doCustomAction1(extras)
            CUSTOM_ACTION_2 -> doCustomAction2(extras)
            else -> {
                Log.w(TAG, "Unknown custom action $action")
            }
        }
    }

}

session.setCallback(callback)

Java

MediaSession.Callback callback = new MediaSession.Callback() {
    @Override
    public void onPlay() {
        // start playback
    }

    @Override
    public void onPause() {
        // pause playback
    }

    @Override
    public void onSkipToPrevious() {
        // skip to previous
    }

    @Override
    public void onSkipToNext() {
        // skip to next
    }

    @Override
    public void onSeekTo(long pos) {
        // jump to position in track
    }

    @Override
    public void onCustomAction(String action, Bundle extras) {
        if (action.equals(CUSTOM_ACTION_1)) {
            doCustomAction1(extras);
        } else if (action.equals(CUSTOM_ACTION_2)) {
            doCustomAction2(extras);
        } else {
            Log.w(TAG, "Unknown custom action " + action);
        }
    }
};

Reprise du contenu multimédia

Pour que votre application de lecture apparaisse dans la zone des paramètres de configuration rapide, vous devez créer une notification MediaStyle avec un jeton MediaSession valide.

Pour afficher le titre de la notification MediaStyle, utilisez NotificationBuilder.setContentTitle().

Pour afficher l'icône de marque du lecteur multimédia, utilisez NotificationBuilder.setSmallIcon().

Pour permettre la reprise de la lecture, les applications doivent implémenter MediaBrowserService et MediaSession. Votre MediaSession doit implémenter le rappel onPlay().

Implémentation de MediaBrowserService

Une fois l'appareil démarré, le système recherche les cinq dernières applications multimédias utilisées et fournit des commandes permettant de relancer la lecture à partir de chaque application.

Le système tente de contacter votre MediaBrowserService à l'aide d'une connexion provenant de SystemUI. Votre application doit autoriser ces connexions, sinon elle ne peut pas prendre en charge la reprise de la lecture.

Les connexions à partir de SystemUI peuvent être identifiées et vérifiées à l'aide du nom de package com.android.systemui et de la signature. SystemUI est signé avec la signature de la plate-forme. Vous trouverez un exemple de vérification par rapport à la signature de la plate-forme dans l'application UAMP.

Pour permettre la reprise de la lecture, votre MediaBrowserService doit implémenter les comportements suivants:

  • onGetRoot() doit renvoyer rapidement une racine non nulle. Toute autre logique complexe doit être gérée dans onLoadChildren().

  • Lorsque onLoadChildren() est appelé sur l'ID multimédia racine, le résultat doit contenir un enfant FLAG_PLAYABLE.

  • MediaBrowserService doit renvoyer le dernier élément multimédia lu lorsqu'il reçoit une requête EXTRA_Recent. La valeur renvoyée doit être un élément multimédia réel et non une fonction générique.

  • MediaBrowserService doit fournir une MediaDescription appropriée avec un titre et un sous-titre non vides. Il doit également définir un URI d'icône ou un bitmap d'icône.

Les exemples de code suivants montrent comment implémenter 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);
}