Kontrol media

Kontrol media di Android terletak di dekat Setelan Cepat. Sesi dari beberapa aplikasi disusun dalam carousel yang dapat digeser. Carousel ini mencantumkan sesi dalam urutan berikut:

  • Streaming yang diputar secara lokal di ponsel
  • Streaming jarak jauh, seperti yang terdeteksi pada perangkat eksternal atau sesi transmisi
  • Sesi sebelumnya yang dapat dilanjutkan, sesuai urutan pemutaran terakhirnya

Mulai Android 13 (API level 33), untuk memastikan pengguna dapat mengakses kumpulan kontrol media yang beragam untuk aplikasi yang memutar media, tombol tindakan pada kontrol media berasal dari status Player.

Dengan cara ini, Anda dapat menampilkan kumpulan kontrol media yang konsisten dan pengalaman kontrol media yang lebih baik di seluruh perangkat.

Gambar 1 menunjukkan contoh tampilannya di perangkat ponsel dan tablet.

Dalam hal tampilannya di perangkat ponsel dan tablet, kontrol media
            menggunakan contoh jalur sampel yang menunjukkan tampilan tombol
Gambar 1: Kontrol media di perangkat ponsel dan tablet

Sistem menampilkan hingga lima tombol tindakan berdasarkan status Player seperti yang dijelaskan dalam tabel berikut. Dalam mode ringkas, hanya tiga slot tindakan pertama yang ditampilkan. Hal ini selaras dengan cara kontrol media dirender di platform Android lainnya seperti Auto, Asisten, dan Wear OS.

Slot Kriteria Tindakan
1 playWhenReady salah atau status pemutaran saat ini adalah STATE_ENDED. Putar
playWhenReady bernilai benar dan status pemutaran saat ini adalah STATE_BUFFERING. Memuat indikator lingkaran berputar
playWhenReady bernilai benar dan status pemutaran saat ini adalah STATE_READY. Jeda
2 Perintah pemutar COMMAND_SEEK_TO_PREVIOUS atau COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM tersedia. Sebelumnya
Perintah pemain COMMAND_SEEK_TO_PREVIOUS atau COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM tidak tersedia, dan perintah kustom dari tata letak kustom yang belum ditempatkan tersedia untuk mengisi slot. Kustom
Tambahan sesi menyertakan nilai boolean true untuk kunci EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV. Kosong
3 Perintah pemutar COMMAND_SEEK_TO_NEXT atau COMMAND_SEEK_TO_NEXT_MEDIA_ITEM tersedia. Berikutnya
Perintah pemain COMMAND_SEEK_TO_NEXT atau COMMAND_SEEK_TO_NEXT_MEDIA_ITEM tidak tersedia, dan perintah kustom dari tata letak kustom yang belum ditempatkan tersedia untuk mengisi slot. Kustom
Tambahan sesi menyertakan nilai boolean true untuk kunci EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT. Kosong
4 Perintah kustom dari tata letak kustom yang belum ditempatkan tersedia untuk mengisi slot. Kustom
5 Perintah kustom dari tata letak kustom yang belum ditempatkan tersedia untuk mengisi slot. Kustom

Perintah kustom ditempatkan sesuai urutan penambahannya ke tata letak kustom.

Menyesuaikan tombol perintah

Untuk menyesuaikan kontrol media sistem dengan Jetpack Media3, Anda dapat menetapkan tata letak kustom sesi dan perintah pengontrol yang tersedia, saat menerapkan MediaSessionService:

  1. Di onCreate(), build MediaSession dan tentukan tata letak kustom tombol perintah.

  2. Di MediaSession.Callback.onConnect(), izinkan pengontrol dengan menentukan perintah yang tersedia, termasuk perintah kustom, di ConnectionResult.

  3. Di MediaSession.Callback.onCustomCommand(), respons perintah kustom yang dipilih oleh pengguna.

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

Untuk mempelajari lebih lanjut cara mengonfigurasi MediaSession agar klien seperti sistem dapat terhubung ke aplikasi media Anda, lihat Memberikan kontrol ke klien lain.

Dengan Jetpack Media3, saat Anda menerapkan MediaSession, PlaybackState akan otomatis diperbarui dengan pemutar media. Demikian pula, saat Anda menerapkan MediaSessionService, library akan otomatis memublikasikan notifikasi MediaStyle untuk Anda dan terus memperbaruinya.

Merespons tombol tindakan

Saat pengguna mengetuk tombol tindakan di kontrol media sistem, MediaController sistem akan mengirim perintah pemutaran ke MediaSession Anda. MediaSession kemudian mendelegasikan perintah tersebut ke pemain. Perintah yang ditentukan di antarmuka Player Media3 ditangani secara otomatis oleh sesi media.

Lihat Menambahkan perintah kustom untuk mendapatkan panduan tentang cara merespons perintah kustom.

Perilaku Pra-Android 13

Untuk kompatibilitas mundur, UI Sistem terus menyediakan tata letak alternatif yang menggunakan tindakan notifikasi untuk aplikasi yang tidak diupdate untuk menargetkan Android 13, atau yang tidak menyertakan informasi PlaybackState. Tombol tindakan berasal dari daftar Notification.Action yang dilampirkan ke notifikasi MediaStyle. Sistem menampilkan hingga lima tindakan sesuai urutan ditambahkannya. Dalam mode ringkas, hingga tiga tombol ditampilkan, yang ditentukan oleh nilai yang diteruskan ke setShowActionsInCompactView().

Tindakan kustom ditempatkan sesuai urutan penambahannya ke PlaybackState.

Contoh kode berikut mengilustrasikan cara menambahkan tindakan ke notifikasi 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();

Mendukung kelanjutan media

Lanjutan media memungkinkan pengguna memulai ulang sesi sebelumnya dari carousel tanpa harus memulai aplikasi. Saat pemutaran dimulai, pengguna berinteraksi dengan kontrol media seperti biasa.

Fitur melanjutkan pemutaran dapat diaktifkan dan dinonaktifkan menggunakan aplikasi Setelan, di bagian opsi Suara > Media. Pengguna juga dapat mengakses Setelan dengan mengetuk ikon roda gigi yang muncul setelah menggeser carousel yang diperluas.

Media3 menawarkan API untuk memudahkan dukungan untuk melanjutkan media. Lihat dokumentasi Lanjutan pemutaran dengan Media3 untuk mendapatkan panduan tentang cara menerapkan fitur ini.

Menggunakan API media lama

Bagian ini menjelaskan cara berintegrasi dengan kontrol media sistem menggunakan MediaCompat API lama.

Sistem mengambil informasi berikut dari MediaMetadata MediaSession, lalu menampilkannya saat tersedia:

  • METADATA_KEY_ALBUM_ART_URI
  • METADATA_KEY_TITLE
  • METADATA_KEY_DISPLAY_TITLE
  • METADATA_KEY_ARTIST
  • METADATA_KEY_DURATION (Jika durasi tidak ditetapkan, kolom pencari tidak akan menampilkan progres)

Untuk memastikan Anda memiliki notifikasi kontrol media yang valid dan akurat, tetapkan nilai metadata METADATA_KEY_TITLE atau METADATA_KEY_DISPLAY_TITLE ke judul media yang sedang diputar.

Pemutar media menampilkan waktu berlalu untuk media yang sedang diputar, beserta kolom pencari yang dipetakan ke MediaSession PlaybackState.

Pemutar media menampilkan progres untuk media yang sedang diputar, beserta kolom pencari yang dipetakan ke MediaSession PlaybackState. Panel cari memungkinkan pengguna mengubah posisi dan menampilkan waktu yang berlalu untuk item media. Agar kolom pencari diaktifkan, Anda harus mengimplementasikan PlaybackState.Builder#setActions dan menyertakan ACTION_SEEK_TO.

Slot Tindakan Kriteria
1 Putar Status PlaybackState saat ini adalah salah satu dari berikut:
  • STATE_NONE
  • STATE_STOPPED
  • STATE_PAUSED
  • STATE_ERROR
Memuat indikator lingkaran berputar Status PlaybackState saat ini adalah salah satu dari berikut:
  • STATE_CONNECTING
  • STATE_BUFFERING
Jeda Status PlaybackState saat ini bukan satu pun dari yang disebutkan di atas.
2 Sebelumnya Tindakan PlaybackState menyertakan ACTION_SKIP_TO_PREVIOUS.
Kustom Tindakan PlaybackState tidak menyertakan ACTION_SKIP_TO_PREVIOUS, dan tindakan kustom PlaybackState menyertakan tindakan kustom yang belum dilakukan.
Kosong Tambahan PlaybackState menyertakan nilai boolean true untuk kunci SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV.
3 Berikutnya Tindakan PlaybackState menyertakan ACTION_SKIP_TO_NEXT.
Kustom Tindakan PlaybackState tidak menyertakan ACTION_SKIP_TO_NEXT, dan tindakan kustom PlaybackState menyertakan tindakan kustom yang belum dilakukan.
Kosong Tambahan PlaybackState menyertakan nilai boolean true untuk kunci SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT.
4 Kustom Tindakan kustom PlaybackState menyertakan tindakan kustom yang belum dilakukan.
5 Kustom Tindakan kustom PlaybackState menyertakan tindakan kustom yang belum dilakukan.

Menambahkan tindakan standar

Contoh kode berikut mengilustrasikan cara menambahkan tindakan standar dan kustom PlaybackState.

Untuk putar, jeda, sebelumnya, dan berikutnya, tetapkan tindakan ini di PlaybackState untuk sesi media.

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

Jika Anda tidak ingin tombol apa pun di slot sebelumnya atau berikutnya, jangan tambahkan ACTION_SKIP_TO_PREVIOUS atau ACTION_SKIP_TO_NEXT, dan tambahkan tambahan ke sesi:

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

Menambahkan tindakan kustom

Untuk tindakan lain yang ingin ditampilkan di kontrol media, Anda dapat membuat PlaybackStateCompat.CustomAction dan menambahkannya ke PlaybackState. Tindakan ini ditampilkan sesuai urutan penambahannya.

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

Merespons tindakan PlaybackState

Saat pengguna mengetuk tombol, SystemUI menggunakan MediaController.TransportControls untuk mengirim perintah kembali ke MediaSession. Anda perlu mendaftarkan callback yang dapat merespons peristiwa ini dengan benar.

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

Melanjutkan Media

Agar aplikasi pemutar media muncul di area setelan cepat, Anda harus membuat notifikasi MediaStyle dengan token MediaSession yang valid.

Untuk menampilkan judul notifikasi MediaStyle, gunakan NotificationBuilder.setContentTitle().

Untuk menampilkan ikon merek bagi pemutar media, gunakan NotificationBuilder.setSmallIcon().

Untuk mendukung pelanjutan pemutaran, aplikasi harus mengimplementasikan MediaBrowserService dan MediaSession. MediaSession Anda harus mengimplementasikan callback onPlay().

Implementasi MediaBrowserService

Setelah perangkat melakukan booting, sistem akan mencari lima aplikasi media yang terakhir digunakan, lalu menyediakan kontrol yang dapat digunakan untuk memulai ulang pemutaran dari setiap aplikasi.

Sistem akan mencoba menghubungi MediaBrowserService dengan koneksi dari SystemUI. Aplikasi Anda harus mengizinkan koneksi semacam ini, karena jika tidak, aplikasi tidak dapat mendukung pelanjutan pemutaran.

Koneksi dari SystemUI dapat diidentifikasi dan diverifikasi menggunakan nama paket com.android.systemui dan tanda tangan. SystemUI ditandatangani dengan tanda tangan platform. Lihat contoh cara memeriksa tanda tangan platform di aplikasi UAMP.

Untuk mendukung pelanjutan pemutaran, MediaBrowserService harus mengimplementasikan perilaku berikut:

  • onGetRoot() harus menampilkan root non-null dengan cepat. Logika kompleks lainnya harus ditangani di onLoadChildren()

  • Saat onLoadChildren() dipanggil pada ID media root, hasilnya harus memuat turunan FLAG_PLAYABLE.

  • MediaBrowserService harus menampilkan item media yang terakhir diputar saat menerima kueri EXTRA_RECENT. Nilai yang dihasilkan harus berupa item media aktual, bukan fungsi generik.

  • MediaBrowserService harus menyediakan MediaDescription yang sesuai, dengan judul dan subjudul yang tidak kosong. Class ini juga harus menetapkan URI ikon atau bitmap ikon.

Contoh kode berikut ini menggambarkan cara mengimplementasikan 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);
}