Controles multimedia

Los controles de contenido multimedia de Android se encuentran cerca de la Configuración rápida. Se organizan en un carrusel deslizable las sesiones de varias apps. En el carrusel, se enumeran las sesiones en este orden:

  • Transmisiones locales en el teléfono
  • Transmisiones remotas, como las detectadas en dispositivos externos o sesiones de transmisión
  • Sesiones reanudables anteriores, en el orden en que se reprodujeron por última vez

A partir de Android 13 (nivel de API 33), para garantizar que los usuarios puedan acceder a un conjunto rico de controles multimedia para las apps que reproducen contenido multimedia, los botones de acción en los controles multimedia se derivan del estado Player.

De esta manera, puedes presentar un conjunto coherente de controles multimedia y una experiencia de control multimedia más pulida en todos los dispositivos.

En la Figura 1, se muestra un ejemplo de cómo se ve en un teléfono y una tablet, respectivamente.

Apariencia de los controles multimedia en teléfonos y tablets mediante un ejemplo de una pista de muestra que indica cómo pueden aparecer los botones
Figura 1: Controles multimedia en teléfonos y tablets

El sistema muestra hasta cinco botones de acción en función del estado de Player, como se describe en la siguiente tabla. En el modo compacto, solo se muestran las primeras tres ranuras de acción. Esto se alinea con la forma en que se renderizan los controles multimedia en otras plataformas de Android, como Auto, Asistente y Wear OS.

Ranura Criterios Acción
1 playWhenReady es falso o el estado de reproducción actual es STATE_ENDED. Reproducir
playWhenReady es verdadero y el estado de reproducción actual es STATE_BUFFERING. Ícono giratorio de carga
playWhenReady es verdadero y el estado de reproducción actual es STATE_READY. Pausar
2 El comando del reproductor COMMAND_SEEK_TO_PREVIOUS o COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM está disponible. Anterior
No están disponibles los comandos del jugador COMMAND_SEEK_TO_PREVIOUS ni COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, y hay un comando personalizado del diseño personalizado que aún no se colocó para completar el espacio. Personalizada
Los extras de la sesión incluyen un valor booleano true para la clave EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV. Vacío
3 El comando del reproductor COMMAND_SEEK_TO_NEXT o COMMAND_SEEK_TO_NEXT_MEDIA_ITEM está disponible. Siguiente
No están disponibles los comandos del jugador COMMAND_SEEK_TO_NEXT ni COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, y hay un comando personalizado del diseño personalizado que aún no se colocó para completar el espacio. Personalizada
Los extras de la sesión incluyen un valor booleano true para la clave EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT. Vacío
4 Hay un comando personalizado del diseño personalizado que aún no se colocó disponible para completar el espacio. Personalizada
5 Hay un comando personalizado del diseño personalizado que aún no se colocó disponible para completar el espacio. Personalizada

Los comandos personalizados se colocan en el orden en que se agregaron al diseño personalizado.

Personaliza los botones de comando

Para personalizar los controles multimedia del sistema con Jetpack Media3, puedes configurar el diseño personalizado de la sesión y los comandos disponibles de los controladores según corresponda cuando implementes un MediaSessionService:

  1. En onCreate(), compila un MediaSession y define el diseño personalizado de los botones de comando.

  2. En MediaSession.Callback.onConnect(), autoriza a los controladores definiendo sus comandos disponibles, incluidos los comandos personalizados, en ConnectionResult.

  3. En MediaSession.Callback.onCustomCommand(), responde al comando personalizado que selecciona el usuario.

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

Para obtener más información sobre cómo configurar tu MediaSession para que clientes como el sistema puedan conectarse a tu app de música, consulta Otorga control a otros clientes.

Con Jetpack Media3, cuando implementas un MediaSession, tu PlaybackState se mantiene automáticamente actualizado con el reproductor multimedia. De manera similar, cuando implementas un MediaSessionService, la biblioteca publica automáticamente una notificación MediaStyle por ti y la mantiene actualizada.

Cómo responder a los botones de acción

Cuando un usuario presiona un botón de acción en los controles multimedia del sistema, el MediaController del sistema envía un comando de reproducción a tu MediaSession. Luego, MediaSession delega esos comandos al jugador. La sesión de Media3 controla automáticamente los comandos definidos en la interfaz Player de Media3.

Consulta Cómo agregar comandos personalizados para obtener orientación sobre cómo responder a un comando personalizado.

Comportamiento anterior a Android 13

Para la retrocompatibilidad, la IU del sistema sigue proporcionando un diseño alternativo que usa acciones de notificación para apps que no se actualizan para orientarse a Android 13 o que no incluyen información de PlaybackState. Los botones de acción se derivan de la lista Notification.Action adjunta a la notificación MediaStyle. El sistema muestra hasta cinco acciones en el orden en que se agregaron. En el modo compacto, se muestran hasta tres botones, determinados por los valores que se pasan a setShowActionsInCompactView().

Las acciones personalizadas se muestran en el orden en que se agregaron a PlaybackState.

En el siguiente ejemplo de código, se muestra cómo agregar acciones a la notificación 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();

Cómo admitir la reanudación de contenido multimedia

La reanudación de contenido multimedia permite a los usuarios reiniciar sesiones anteriores desde el carrusel sin tener que iniciar la app. Cuando comienza la reproducción, el usuario interactúa con los controles de contenido multimedia de la manera habitual.

La función de reanudación de la reproducción se puede activar y desactivar en la app de Configuración, en las opciones Sonido > Audio. El usuario también puede acceder a Configuración presionando el ícono de ajustes que aparece después de deslizar el carrusel expandido.

Media3 ofrece APIs para facilitar la compatibilidad con la reanudación de contenido multimedia. Consulta la documentación sobre la reanudación de la reproducción con Media3 para obtener orientación sobre cómo implementar esta función.

Usa las APIs de contenido multimedia heredadas

En esta sección, se explica cómo realizar la integración con los controles multimedia del sistema con las APIs de MediaCompat heredadas.

El sistema recupera la siguiente información de MediaMetadata de la MediaSession y la muestra cuando está disponible:

  • METADATA_KEY_ALBUM_ART_URI
  • METADATA_KEY_TITLE
  • METADATA_KEY_DISPLAY_TITLE
  • METADATA_KEY_ARTIST
  • METADATA_KEY_DURATION (si la duración no está configurada, la barra de búsqueda no mostrará el progreso)

Para asegurarte de tener una notificación de control multimedia válida y precisa, establece el valor de los metadatos METADATA_KEY_TITLE o METADATA_KEY_DISPLAY_TITLE en el título del contenido multimedia que se está reproduciendo.

El reproductor de contenido multimedia muestra el tiempo transcurrido del contenido multimedia que se está reproduciendo, junto con una barra de búsqueda que se asigna a la PlaybackState de la MediaSession.

El reproductor multimedia muestra el progreso del contenido multimedia que se está reproduciendo, junto con una barra de búsqueda que se asigna a la PlaybackState de la MediaSession. La barra de desplazamiento permite a los usuarios cambiar la posición y muestra el tiempo transcurrido del elemento multimedia. Para que se habilite la barra de búsqueda, debes implementar PlaybackState.Builder#setActions e incluir ACTION_SEEK_TO.

Ranura Acción Criterios
1 Reproducir El estado actual de PlaybackState es uno de los siguientes:
  • STATE_NONE
  • STATE_STOPPED
  • STATE_PAUSED
  • STATE_ERROR
Ícono giratorio de carga El estado actual de PlaybackState es uno de los siguientes:
  • STATE_CONNECTING
  • STATE_BUFFERING
Pausar El estado actual de PlaybackState no es ninguno de los anteriores.
2 Anterior Las acciones de PlaybackState incluyen ACTION_SKIP_TO_PREVIOUS.
Personalizada Las acciones de PlaybackState no incluyen ACTION_SKIP_TO_PREVIOUS, y las acciones personalizadas de PlaybackState incluyen una acción personalizada que todavía no se implementó.
Vacío Los extras de PlaybackState incluyen un valor booleano true para la clave SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV.
3 Siguiente Las acciones de PlaybackState incluyen ACTION_SKIP_TO_NEXT.
Personalizada Las acciones de PlaybackState no incluyen ACTION_SKIP_TO_NEXT, y las acciones personalizadas de PlaybackState incluyen una acción personalizada que todavía no se implementó.
Vacío Los extras de PlaybackState incluyen un valor booleano true para la clave SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT.
4 Personalizada Las acciones personalizadas de PlaybackState incluyen una acción personalizada que todavía no se realizó.
5 Personalizada Las acciones personalizadas de PlaybackState incluyen una acción personalizada que todavía no se realizó.

Cómo agregar acciones estándar

En los siguientes ejemplos de código, se muestra cómo agregar acciones estándar y personalizadas de PlaybackState.

Para reproducir, pausar, ir al elemento anterior y al siguiente, establece estas acciones en PlaybackState para la sesión multimedia.

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 no quieres que haya botones en los espacios anterior o siguiente, no agregues ACTION_SKIP_TO_PREVIOUS ni ACTION_SKIP_TO_NEXT, sino que agrega elementos adicionales a la sesión:

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

Agrega acciones personalizadas

Para otras acciones que quieras mostrar en los controles multimedia, puedes crear un PlaybackStateCompat.CustomAction y agregarlo a PlaybackState. Estas acciones se muestran en el orden en que se agregaron.

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

Cómo responder a acciones de PlaybackState

Cuando un usuario presiona un botón, SystemUI usa MediaController.TransportControls para enviar un comando a MediaSession. Debes registrar una devolución de llamada que pueda responder correctamente a estos eventos.

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

Reanudación de contenido multimedia

Si deseas que la aplicación de reproducción aparezca en el área de configuración rápida, debes crear una notificación MediaStyle con un token MediaSession válido.

Para mostrar el título de la notificación MediaStyle, usa NotificationBuilder.setContentTitle().

Para mostrar el ícono de marca del reproductor de contenido multimedia, usa NotificationBuilder.setSmallIcon().

Para admitir la reanudación de la reproducción, las apps deben implementar un MediaBrowserService y una MediaSession. Tu MediaSession debe implementar la devolución de llamada onPlay().

Implementación de MediaBrowserService

Después de que se inicia el dispositivo, el sistema busca las últimas cinco apps de contenido multimedia y proporciona controles que se pueden usar para reiniciar la reproducción desde cada app.

El sistema intenta comunicarse con tu MediaBrowserService mediante una conexión desde SystemUI. Tu app debe permitir estas conexiones; de lo contrario, no podrá reanudar la reproducción.

Se pueden identificar y verificar las conexiones de SystemUI usando el nombre de paquete com.android.systemui y la firma. La interfaz de sistema lleva la firma de la plataforma. Puedes encontrar un ejemplo de cómo verificar la firma de la plataforma en la app de UAMP.

Para admitir la reanudación de la reproducción, tu MediaBrowserService debe implementar estos comportamientos:

  • onGetRoot() debe mostrar rápidamente una raíz no nula. Se debería manejar otra lógica compleja en onLoadChildren().

  • Cuando se llama a onLoadChildren() en el ID de contenido multimedia raíz, el resultado debe contener un elemento FLAG_PLAYABLE secundario.

  • MediaBrowserService debería mostrar el elemento multimedia reproducido más recientemente cuando reciba una consulta EXTRA_RECENT. El valor que se muestre debería ser un elemento multimedia real en lugar de una función genérica.

  • MediaBrowserService debe proporcionar una MediaDescription adecuada, con un título y un subtítulo que no estén vacíos. También debería establecer un URI de ícono o un mapa de bits de ícono.

En los siguientes ejemplos de código, se muestra cómo implementar 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);
}