Sterowanie multimediami

Elementy sterujące multimediami w Androidzie znajdują się w pobliżu Szybkich ustawień. Sesje z różnych aplikacji są uporządkowane w karuzeli, którą można przewijać. Karuzela zawiera listę sesji w takim porządku:

  • strumieniowe odtwarzanie lokalnie na telefonie,
  • strumieniowanie zdalne, np. na urządzeniach zewnętrznych lub w sesjach przesyłania treści;
  • poprzednie sesje, które można wznowić, w kolejności, w jakiej były ostatnio odtwarzane;

Począwszy od Androida 13 (poziom API 33) użytkownicy mają dostęp do bogatego zestawu elementów sterujących multimediów w aplikacjach odtwarzających multimedia. Przyciski akcji w elementach sterujących multimediami są pobierane ze stanu Player.

Dzięki temu możesz prezentować spójny zestaw elementów sterowania multimediami i zapewnić lepsze sterowanie multimediami na różnych urządzeniach.

Na rysunku 1. widać, jak to wygląda na telefonie i tablecie.

elementy sterujące multimediami w wersji wyświetlanej na telefonach i tabletach,
            przy użyciu przykładowego utworu pokazującego, jak mogą wyglądać przyciski;
Rysunek 1. Elementy sterujące multimediami na telefonach i tabletach

System wyświetla maksymalnie 5 przycisków akcji w zależności od stanu Player, jak opisano w tabeli poniżej. W trybie kompaktowym wyświetlane są tylko 3 pierwsze sloty akcji. Jest to zgodne ze sposobem wyświetlania elementów sterujących multimediami na innych platformach Androida, takich jak Auto, Asystent czy Wear OS.

Boks Kryteria Działanie
1 playWhenReady jest fałszywy lub bieżący stan odtwarzania to STATE_ENDED. Odtwórz
playWhenReady jest równa 1, a obecny stan odtwarzania to STATE_BUFFERING. Wskaźnik postępu wczytywania
playWhenReady jest równa 1, a obecny stan odtwarzania to STATE_READY. Wstrzymaj
2 Dostępne jest polecenie dotyczące odtwarzacza COMMAND_SEEK_TO_PREVIOUS lub COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM. Wstecz
Nie jest dostępna ani komenda odtwarzacza COMMAND_SEEK_TO_PREVIOUS, ani COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM. Do wypełnienia boksu jest dostępna komenda niestandardowa z niestandardowego układu, która nie została jeszcze umieszczona. Możliwość
Dodatkowe dane sesji obejmują wartość logiczną true dla klucza EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV. Puste
3 Dostępne jest polecenie dotyczące odtwarzacza COMMAND_SEEK_TO_NEXT lub COMMAND_SEEK_TO_NEXT_MEDIA_ITEM. Dalej
Nie jest dostępna ani komenda odtwarzacza COMMAND_SEEK_TO_NEXT, ani COMMAND_SEEK_TO_NEXT_MEDIA_ITEM. Do wypełnienia boksu jest dostępna komenda niestandardowa z niestandardowego układu, która nie została jeszcze umieszczona. Możliwość
Dodatkowe dane sesji obejmują wartość logiczną true dla klucza EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT. Puste
4 Do wypełnienia boksu jest dostępne polecenie niestandardowe z niestandardowego układu, które nie zostało jeszcze umieszczone. Możliwość
5 Do wypełnienia boksu jest dostępne polecenie niestandardowe z niestandardowego układu, które nie zostało jeszcze umieszczone. Możliwość

Polecenia niestandardowe są umieszczane w kolejności, w jakiej zostały dodane do układu niestandardowego.

Dostosowywanie przycisków poleceń

Aby dostosować elementy sterujące systemem multimediów za pomocą Jetpack Media3, możesz odpowiednio ustawić niestandardowy układ sesji i dostępne polecenia sterowników podczas wdrażania MediaSessionService:

  1. onCreate() utwórz MediaSessionzdefiniuj niestandardowy układ przycisków poleceń.

  2. MediaSession.Callback.onConnect() autoryzuj kontrolery, definiując dostępne polecenia, w tym polecenia niestandardowe, w ConnectionResult.

  3. W sekcji MediaSession.Callback.onCustomCommand() odpowiada na polecenie niestandardowe wybrane przez użytkownika.

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);
    }
  }
}

Więcej informacji o konfigurowaniu MediaSession, aby klienci, tacy jak system, mogli łączyć się z Twoją aplikacją do multimediów, znajdziesz w artykule Przyznawanie kontroli innym klientom.

W Jetpack Media3, gdy wdrożesz MediaSession, Twoja PlaybackStatebędzie automatycznie aktualizowana przez odtwarzacz multimediów. Podobnie, gdy wdrożysz MediaSessionService, biblioteka automatycznie opublikuje MediaStyle powiadomienie i będzie je aktualizować.

Odpowiadanie na przyciski poleceń

Gdy użytkownik kliknie przycisk działania w systemie sterowania multimediów, system MediaController wyśle do MediaSession polecenie odtwarzania. Następnie MediaSession przekazuje te polecenia odtwarzaczowi. Polecenia zdefiniowane w interfejsie Player Media3 są obsługiwane automatycznie przez sesję multimediów.

Więcej informacji o tym, jak odpowiadać na polecenia niestandardowe, znajdziesz w artykule Dodawanie poleceń niestandardowych.

Zachowanie w wersjach starszych niż Android 13

Ze względu na zgodność wsteczną interfejs systemowy nadal udostępnia alternatywny układ, który używa działań powiadomienia w przypadku aplikacji, które nie zostały zaktualizowane do wersji Android 13 lub nie zawierają informacji PlaybackState. Przyciski poleceń są pobierane z listy Notification.Action dołączonej do powiadomienia MediaStyle. System wyświetla maksymalnie 5 działań w kolejności, w jakiej zostały dodane. W trybie kompaktowym wyświetlane są maksymalnie 3 przyciski, które zależą od wartości przekazanych do setShowActionsInCompactView().

Działania niestandardowe są umieszczane w takim samym porządku, w jakim zostały dodane do PlaybackState.

Ten przykładowy kod pokazuje, jak dodać działania do powiadomienia 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();

Obsługa wznawiania multimediów

Wznowienie multimediów umożliwia użytkownikom wznowienie poprzednich sesji z karuzeli bez konieczności uruchamiania aplikacji. Po rozpoczęciu odtwarzania użytkownik może normalnie korzystać z elementów sterujących multimediów.

Funkcję wznawiania odtwarzania można włączać i wyłączać w aplikacji Ustawienia w sekcji Dźwięk > Multimedia. Użytkownik może też otworzyć Ustawienia, klikając ikonę koła zębatego, która pojawia się po przesunięciu rozszerzonego karuzela.

Media3 oferuje interfejsy API, które ułatwiają obsługę wznawiania multimediów. Więcej informacji o wdrażaniu tej funkcji znajdziesz w dokumentacji Wznowienie odtwarzania za pomocą Media3.

Korzystanie ze starszych interfejsów API mediów

Z tej sekcji dowiesz się, jak zintegrować się z systemowymi elementami sterującymi multimediami za pomocą starszych interfejsów MediaCompat API.

System pobiera te informacje z MediaSessionMediaMetadata i wyświetla je, gdy są dostępne:

  • METADATA_KEY_ALBUM_ART_URI
  • METADATA_KEY_TITLE
  • METADATA_KEY_DISPLAY_TITLE
  • METADATA_KEY_ARTIST
  • METADATA_KEY_DURATION (jeśli czas trwania nie jest ustawiony, pasek przewijania nie pokazuje postępu)

Aby mieć prawidłowe i dokładne powiadomienie o sterowaniu multimediami, ustaw wartość metadanych METADATA_KEY_TITLE lub METADATA_KEY_DISPLAY_TITLE na tytuł odtwarzanego obecnie multimediów.

Odtwarzacz multimediów wyświetla upłynięty czas odtwarzania multimediów oraz suwak przesuwania, który jest mapowany na MediaSession PlaybackState.

Odtwarzacz multimediów pokazuje postęp odtwarzania bieżących multimediów oraz pasek przewijania mapowany na MediaSession PlaybackState. Pasek przesuwania umożliwia użytkownikom zmianę pozycji i wyświetla upływający czas odtwarzania treści multimedialnej. Aby włączyć suwak, musisz zaimplementować PlaybackState.Builder#setActions i uwzględnić ACTION_SEEK_TO.

Boks Działanie Kryteria
1 Odtwórz Obecny stan PlaybackState to jeden z tych:
  • STATE_NONE
  • STATE_STOPPED
  • STATE_PAUSED
  • STATE_ERROR
Wskaźnik postępu wczytywania Obecny stan usługi PlaybackState to:
  • STATE_CONNECTING
  • STATE_BUFFERING
Wstrzymaj Obecny stan PlaybackState nie jest żaden z wymienionych powyżej.
2 Wstecz PlaybackState Działania obejmują ACTION_SKIP_TO_PREVIOUS.
Możliwość PlaybackState Działania nie obejmują ACTION_SKIP_TO_PREVIOUSPlaybackState, a działania niestandardowe obejmują działanie niestandardowe, które nie zostało jeszcze umieszczone.
Puste PlaybackState extras obejmują wartość logiczną true dla klucza SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV.
3 Dalej PlaybackState Działania obejmują ACTION_SKIP_TO_NEXT.
Możliwość PlaybackState Działania nie obejmują ACTION_SKIP_TO_NEXTPlaybackState, a działania niestandardowe obejmują działanie niestandardowe, które nie zostało jeszcze umieszczone.
Puste PlaybackState extras obejmują wartość logiczną true dla klucza SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT.
4 Możliwość PlaybackState Działania niestandardowe obejmują działanie niestandardowe, które nie zostało jeszcze umieszczone.
5 Możliwość PlaybackState Działania niestandardowe obejmują działanie niestandardowe, które nie zostało jeszcze umieszczone.

Dodawanie działań standardowych

Poniższe przykłady kodu pokazują, jak dodawać standardowe i niestandardowe działania PlaybackState.

Aby ustawić odtwarzanie, wstrzymanie, odtwarzanie poprzedniego i następnego elementu, użyj PlaybackState w sesji multimedialnej.

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);

Jeśli nie chcesz umieszczać żadnych przycisków w poprzednich lub kolejnych slotach, nie dodawaj przycisków ACTION_SKIP_TO_PREVIOUS ani ACTION_SKIP_TO_NEXT, a zamiast tego dodaj dodatkowe elementy do sesji:

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);

Dodawanie działań niestandardowych

Jeśli chcesz wyświetlać inne działania w przyciskach sterowania multimediami, możesz utworzyć PlaybackStateCompat.CustomAction i dodać go do PlaybackState. Te działania są wyświetlane w kolejności dodania.

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);

Odpowiadanie na działania PlaybackState

Gdy użytkownik kliknie przycisk, SystemUI użyje interfejsu MediaController.TransportControls, aby wysłać polecenie do MediaSession. Musisz zarejestrować wywołanie zwrotne, które będzie odpowiednio reagować na te zdarzenia.

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);
        }
    }
};

Wznowienie multimediów

Aby aplikacja odtwarzacza była widoczna w obszarze szybkich ustawień, musisz utworzyć powiadomienie MediaStyle z ważnym tokenem MediaSession.

Aby wyświetlić tytuł powiadomienia MediaStyle, użyj elementu NotificationBuilder.setContentTitle().

Aby wyświetlić ikonę marki odtwarzacza, użyj atrybutu NotificationBuilder.setSmallIcon().

Aby obsługiwać wznawianie odtwarzania, aplikacje muszą implementować MediaBrowserService i MediaSession. MediaSession musi implementować wywołanie zwrotne onPlay().

Implementacja usługi MediaBrowserService

Po uruchomieniu urządzenia system wyszukuje 5 ostatnio używanych aplikacji multimedialnych i zapewnia elementy sterujące, za pomocą których można ponownie uruchomić odtwarzanie w każdej z nich.

System próbuje połączyć się z usługą MediaBrowserService za pomocą połączenia z interfejsem SystemUI. Aplikacja musi zezwalać na takie połączenia, w przeciwnym razie nie będzie można wznowić odtwarzania.

Połączenia z SystemUI można zidentyfikować i zweryfikować za pomocą nazwy pakietucom.android.systemui i podpisu. SystemUI jest podpisany podpisem platformy. Przykład sprawdzenia podpisu platformy znajdziesz w aplikacji UAMP.

Aby obsługiwać wznawianie odtwarzania, MediaBrowserService musi:

  • Funkcja onGetRoot() musi szybko zwracać niezerową wartość. Inna skomplikowana logika powinna być obsługiwana w onLoadChildren()

  • Gdy wywołujemy funkcję onLoadChildren() w przypadku głównego identyfikatora multimediów, wynik musi zawierać podelement FLAG_PLAYABLE.

  • MediaBrowserService powinien zwracać ostatnio odtwarzany element multimediów, gdy otrzyma zapytanie EXTRA_RECENT. Zwracana wartość powinna być rzeczywistym elementem multimedialnym, a nie ogólną funkcją.

  • MediaBrowserService musi zawierać odpowiednią MediaDescription z niepustym titlesubtitle. Należy też ustawić identyfikator URI ikony lub pikselową ikonę bitmapową.

Poniższe przykłady kodu pokazują, jak zaimplementować 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);
}