미디어 제어

Android의 미디어 컨트롤은 빠른 설정 근처에 있습니다. 여러 앱의 세션이 스와이프할 수 있는 캐러셀에 정렬됩니다. 캐러셀은 다음 순서로 세션을 나열합니다.

  • 휴대전화에서 로컬로 재생되는 스트림
  • 외부 기기나 전송 세션에서 감지된 것과 같은 원격 스트림
  • 마지막으로 재생된 순서로 재개 가능한 이전 세션

Android 13 (API 수준 33)부터 사용자가 미디어를 재생하는 앱의 다양한 미디어 컨트롤 세트에 액세스할 수 있도록 미디어 컨트롤의 작업 버튼은 Player 상태에서 파생됩니다.

이렇게 하면 기기 전반에서 일관된 미디어 컨트롤 세트와 더 세련된 미디어 컨트롤 환경을 제공할 수 있습니다.

그림 1은 스마트폰과 태블릿 기기에서 각각 어떻게 표시되는지 보여줍니다.

스마트폰과 태블릿 기기에 표시되는 방식과 관련된 미디어 컨트롤. 버튼이 표시될 수 있는 방식을 보여주는 샘플 트랙의 예시를 사용함
그림 1: 스마트폰 및 태블릿 기기의 미디어 컨트롤

시스템은 다음 표에 설명된 대로 Player 상태를 기반으로 작업 버튼을 최대 5개 표시합니다. 압축 모드에서는 처음 3개의 작업 슬롯만 표시됩니다. 이는 Auto, 어시스턴트, Wear OS와 같은 다른 Android 플랫폼에서 미디어 컨트롤이 렌더링되는 방식과 일치합니다.

슬롯 기준 작업
1 playWhenReady이 false이거나 현재 재생 상태STATE_ENDED입니다. 재생
playWhenReady가 true이고 현재 재생 상태STATE_BUFFERING입니다. 로딩 스피너
playWhenReady가 true이고 현재 재생 상태STATE_READY입니다. 일시중지
2 플레이어 명령어 COMMAND_SEEK_TO_PREVIOUS 또는 COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM를 사용할 수 있습니다. 이전
플레이어 명령어 COMMAND_SEEK_TO_PREVIOUS 또는 COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM를 사용할 수 없으며 아직 배치되지 않은 맞춤 레이아웃의 맞춤 명령어를 사용하여 슬롯을 채울 수 있습니다. 맞춤식
세션 추가 항목에는 EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV 키의 true 불리언 값이 포함됩니다. 비어 있음
3 플레이어 명령어 COMMAND_SEEK_TO_NEXT 또는 COMMAND_SEEK_TO_NEXT_MEDIA_ITEM를 사용할 수 있습니다. 다음
플레이어 명령어 COMMAND_SEEK_TO_NEXT 또는 COMMAND_SEEK_TO_NEXT_MEDIA_ITEM를 사용할 수 없으며 아직 배치되지 않은 맞춤 레이아웃의 맞춤 명령어를 사용하여 슬롯을 채울 수 있습니다. 맞춤식
세션 추가 항목에는 EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT 키의 true 불리언 값이 포함됩니다. 비어 있음
4 아직 배치되지 않은 맞춤 레이아웃의 맞춤 명령어를 사용하여 슬롯을 채울 수 있습니다. 맞춤식
5 아직 배치되지 않은 맞춤 레이아웃의 맞춤 명령어를 사용하여 슬롯을 채울 수 있습니다. 맞춤식

맞춤 명령어는 맞춤 레이아웃에 추가된 순서대로 배치됩니다.

명령어 버튼 맞춤설정

Jetpack Media3로 시스템 미디어 컨트롤을 맞춤설정하려면 MediaSessionService를 구현할 때 세션의 맞춤 레이아웃과 컨트롤러의 사용 가능한 명령어를 적절하게 설정하면 됩니다.

  1. onCreate()에서 MediaSession를 빌드하고 명령어 버튼의 맞춤 레이아웃을 정의합니다.

  2. MediaSession.Callback.onConnect()에서 ConnectionResult맞춤 명령어를 비롯한 사용 가능한 명령어를 정의하여 컨트롤러를 승인합니다.

  3. MediaSession.Callback.onCustomCommand()에서 사용자가 선택한 맞춤 명령어에 응답합니다.

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

자바

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

시스템과 같은 클라이언트가 미디어 앱에 연결할 수 있도록 MediaSession를 구성하는 방법을 자세히 알아보려면 다른 클라이언트에 제어 권한 부여를 참고하세요.

Jetpack Media3를 사용하면 MediaSession를 구현할 때 PlaybackState가 미디어 플레이어와 자동으로 최신 상태로 유지됩니다. 마찬가지로 MediaSessionService를 구현하면 라이브러리가 자동으로 MediaStyle 알림을 게시하고 최신 상태로 유지합니다.

작업 버튼에 응답

사용자가 시스템 미디어 컨트롤에서 작업 버튼을 탭하면 시스템의 MediaControllerMediaSession에 재생 명령을 전송합니다. 그런 다음 MediaSession는 이러한 명령어를 플레이어에게 위임합니다. Media3의 Player 인터페이스에 정의된 명령어는 미디어 세션에서 자동으로 처리됩니다.

맞춤 명령어에 응답하는 방법에 관한 안내는 맞춤 명령어 추가를 참고하세요.

Android 13 이전 동작

이전 버전과의 호환성을 위해 시스템 UI는 Android 13을 타겟팅하도록 업데이트되지 않거나 PlaybackState 정보를 포함하지 않는 앱의 알림 작업을 사용하는 대체 레이아웃을 계속 제공합니다. 작업 버튼은 MediaStyle 알림에 연결된 Notification.Action 목록에서 파생됩니다. 시스템은 작업이 추가된 순서대로 최대 5개까지 표시합니다. 압축 모드에서는 setShowActionsInCompactView()에 전달된 값에 따라 최대 3개의 버튼이 표시됩니다.

맞춤 작업은 PlaybackState에 추가된 순서대로 배치됩니다.

다음 코드 예는 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()

자바

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

미디어 재개 지원

미디어 재개를 사용하면 사용자가 앱을 시작할 필요 없이 캐러셀에서 이전 세션을 다시 시작할 수 있습니다. 재생이 시작되면 사용자는 일반적인 방법으로 미디어 컨트롤과 상호작용합니다.

재생 재개 기능은 설정 앱의 소리 > 미디어 옵션에서 사용 설정 또는 사용 중지할 수 있습니다. 사용자는 확장된 캐러셀을 스와이프한 후 표시되는 톱니바퀴 아이콘을 탭하여 설정에 액세스할 수도 있습니다.

Media3는 미디어 재개를 더 쉽게 지원할 수 있는 API를 제공합니다. 이 기능을 구현하는 방법은 Media3를 사용한 재생 재개 문서를 참고하세요.

기존 미디어 API 사용

이 섹션에서는 기존 MediaCompat API를 사용하여 시스템 미디어 컨트롤과 통합하는 방법을 설명합니다.

시스템은 MediaSessionMediaMetadata에서 다음 정보를 검색하여 사용 가능한 경우 표시합니다.

  • METADATA_KEY_ALBUM_ART_URI
  • METADATA_KEY_TITLE
  • METADATA_KEY_DISPLAY_TITLE
  • METADATA_KEY_ARTIST
  • METADATA_KEY_DURATION (지속 시간이 설정되지 않은 경우 탐색 막대에 진행률이 표시되지 않음)

유효하고 정확한 미디어 컨트롤 알림을 받으려면 METADATA_KEY_TITLE 또는 METADATA_KEY_DISPLAY_TITLE 메타데이터의 값을 현재 재생 중인 미디어의 제목으로 설정하세요.

미디어 플레이어는 현재 재생 중인 미디어의 경과 시간과 MediaSession PlaybackState에 매핑된 탐색 막대를 표시합니다.

미디어 플레이어는 현재 재생 중인 미디어의 진행률과 MediaSession PlaybackState에 매핑된 탐색 막대를 표시합니다. 탐색 메뉴를 사용하면 사용자가 위치를 변경할 수 있으며 미디어 항목의 경과 시간이 표시됩니다. 탐색 막대를 사용 설정하려면 PlaybackState.Builder#setActions를 구현하고 ACTION_SEEK_TO를 포함해야 합니다.

슬롯 작업 기준
1 재생 PlaybackState의 현재 상태는 다음 중 하나입니다.
  • STATE_NONE
  • STATE_STOPPED
  • STATE_PAUSED
  • STATE_ERROR
로딩 스피너 PlaybackState의 현재 상태는 다음 중 하나입니다.
  • STATE_CONNECTING
  • STATE_BUFFERING
일시중지 PlaybackState의 현재 상태는 위에 없습니다.
2 이전 PlaybackState 작업에는 ACTION_SKIP_TO_PREVIOUS가 포함됩니다.
맞춤식 PlaybackState 작업에는 ACTION_SKIP_TO_PREVIOUS가 포함되지 않고 PlaybackState 맞춤 작업에는 아직 배치되지 않은 맞춤 작업이 포함됩니다.
비어 있음 PlaybackState extras에는 SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV 키의 true 불리언 값이 포함됩니다.
3 다음 PlaybackState 작업에는 ACTION_SKIP_TO_NEXT가 포함됩니다.
맞춤식 PlaybackState 작업에는 ACTION_SKIP_TO_NEXT가 포함되지 않고 PlaybackState 맞춤 작업에는 아직 배치되지 않은 맞춤 작업이 포함됩니다.
비어 있음 PlaybackState extras에는 SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT 키의 true 불리언 값이 포함됩니다.
4 맞춤식 PlaybackState 맞춤 작업에는 아직 배치되지 않은 맞춤 작업이 포함됩니다.
5 맞춤식 PlaybackState 맞춤 작업에는 아직 배치되지 않은 맞춤 작업이 포함됩니다.

표준 작업 추가

다음 코드 예에서는 PlaybackState 표준 작업과 맞춤 작업을 추가하는 방법을 보여줍니다.

재생, 일시중지, 이전, 다음의 경우 미디어 세션의 PlaybackState에서 이러한 작업을 설정합니다.

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)

자바

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

이전 또는 다음 슬롯에 버튼을 표시하지 않으려면 ACTION_SKIP_TO_PREVIOUS 또는 ACTION_SKIP_TO_NEXT를 추가하지 말고 대신 세션에 추가 항목을 추가합니다.

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

자바

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

맞춤 작업 추가

미디어 컨트롤에 표시할 다른 작업의 경우 PlaybackStateCompat.CustomAction를 만들어 PlaybackState에 추가하면 됩니다. 이러한 작업은 추가된 순서대로 표시됩니다.

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)

자바

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

PlaybackState 작업에 응답

사용자가 버튼을 탭하면 SystemUI는 MediaController.TransportControls를 사용하여 MediaSession에 명령어를 다시 전송합니다. 이러한 이벤트에 올바르게 응답할 수 있는 콜백을 등록해야 합니다.

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)

자바

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

미디어 재개

빠른 설정 영역에 플레이어 앱을 표시하려면 유효한 MediaSession 토큰으로 MediaStyle 알림을 만들어야 합니다.

MediaStyle 알림의 제목을 표시하려면 NotificationBuilder.setContentTitle()를 사용하세요.

미디어 플레이어의 브랜드 아이콘을 표시하려면 NotificationBuilder.setSmallIcon()을 사용하세요.

재생 재개를 지원하려면 앱에서 MediaBrowserServiceMediaSession을 구현해야 합니다. MediaSession에서 onPlay() 콜백을 구현해야 합니다.

MediaBrowserService 구현

기기가 부팅되면 시스템에서는 가장 최근에 사용한 미디어 앱 5개를 찾고 각 앱에서 재생을 다시 시작하는 데 사용할 수 있는 컨트롤을 제공합니다.

시스템은 SystemUI의 연결을 통해 MediaBrowserService에 연결하려고 합니다. 앱은 이러한 연결을 허용해야 합니다. 허용하지 않으면 재생 재개를 지원할 수 없습니다.

SystemUI의 연결은 패키지 이름 com.android.systemui 및 서명을 사용하여 식별 및 확인할 수 있습니다. SystemUI는 플랫폼 서명으로 서명됩니다. 플랫폼 서명을 확인하는 방법의 예는 UAMP 앱에서 확인할 수 있습니다.

재생 재개를 지원하려면 MediaBrowserService에서 다음 동작을 구현해야 합니다.

  • onGetRoot()는 null이 아닌 루트를 빠르게 반환해야 합니다. 다른 복잡한 로직은 onLoadChildren()에서 처리해야 합니다.

  • onLoadChildren()이 루트 미디어 ID에서 호출되면 결과에는 FLAG_PLAYABLE 하위 요소가 포함되어야 합니다.

  • MediaBrowserServiceEXTRA_RECENT 쿼리를 수신할 때 가장 최근에 재생된 미디어 항목을 반환해야 합니다. 반환되는 값은 일반 함수가 아닌 실제 미디어 항목이어야 합니다.

  • MediaBrowserService는 비어 있지 않은 제목자막이 있는 적절한 MediaDescription을 제공해야 합니다. 아이콘 URI아이콘 비트맵도 설정해야 합니다.

다음 코드 예에서는 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);
}