Android의 미디어 컨트롤은 빠른 설정 근처에 있습니다. 여러 앱의 세션이 스와이프할 수 있는 캐러셀에 정렬됩니다. 캐러셀은 다음 순서로 세션을 나열합니다.
- 휴대전화에서 로컬로 재생되는 스트림
- 외부 기기나 전송 세션에서 감지된 것과 같은 원격 스트림
- 마지막으로 재생된 순서로 재개 가능한 이전 세션
Android 13 (API 수준 33)부터 사용자가 미디어를 재생하는 앱의 다양한 미디어 컨트롤 세트에 액세스할 수 있도록 미디어 컨트롤의 작업 버튼은 Player
상태에서 파생됩니다.
이렇게 하면 기기 전반에서 일관된 미디어 컨트롤 세트와 더 세련된 미디어 컨트롤 환경을 제공할 수 있습니다.
그림 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
를 구현할 때 세션의 맞춤 레이아웃과 컨트롤러의 사용 가능한 명령어를 적절하게 설정하면 됩니다.
onCreate()
에서MediaSession
를 빌드하고 명령어 버튼의 맞춤 레이아웃을 정의합니다.MediaSession.Callback.onConnect()
에서ConnectionResult
에 맞춤 명령어를 비롯한 사용 가능한 명령어를 정의하여 컨트롤러를 승인합니다.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 ListenableFutureonCustomCommand( 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
알림을 게시하고 최신 상태로 유지합니다.
작업 버튼에 응답
사용자가 시스템 미디어 컨트롤에서 작업 버튼을 탭하면 시스템의 MediaController
가 MediaSession
에 재생 명령을 전송합니다. 그런 다음 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를 사용하여 시스템 미디어 컨트롤과 통합하는 방법을 설명합니다.
시스템은 MediaSession
의 MediaMetadata
에서 다음 정보를 검색하여 사용 가능한 경우 표시합니다.
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 의 현재 상태는 다음 중 하나입니다.
|
로딩 스피너 |
PlaybackState 의 현재 상태는 다음 중 하나입니다.
|
|
일시중지 | 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()
을 사용하세요.
재생 재개를 지원하려면 앱에서 MediaBrowserService
및 MediaSession
을 구현해야 합니다. MediaSession
에서 onPlay()
콜백을 구현해야 합니다.
MediaBrowserService
구현
기기가 부팅되면 시스템에서는 가장 최근에 사용한 미디어 앱 5개를 찾고 각 앱에서 재생을 다시 시작하는 데 사용할 수 있는 컨트롤을 제공합니다.
시스템은 SystemUI의 연결을 통해 MediaBrowserService
에 연결하려고 합니다. 앱은 이러한 연결을 허용해야 합니다. 허용하지 않으면 재생 재개를 지원할 수 없습니다.
SystemUI의 연결은 패키지 이름 com.android.systemui
및 서명을 사용하여 식별 및 확인할 수 있습니다. SystemUI는 플랫폼 서명으로 서명됩니다. 플랫폼 서명을 확인하는 방법의 예는 UAMP 앱에서 확인할 수 있습니다.
재생 재개를 지원하려면 MediaBrowserService
에서 다음 동작을 구현해야 합니다.
onGetRoot()
는 null이 아닌 루트를 빠르게 반환해야 합니다. 다른 복잡한 로직은onLoadChildren()
에서 처리해야 합니다.onLoadChildren()
이 루트 미디어 ID에서 호출되면 결과에는 FLAG_PLAYABLE 하위 요소가 포함되어야 합니다.MediaBrowserService
는 EXTRA_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); }