Controles multimedia

Los controles multimedia de Android se encuentran cerca de la Configuración rápida. Se organizan en un carrusel deslizable las sesiones de varias apps. El carrusel enumera 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 una conjunto de controles multimedia para apps que reproducen contenido multimedia, botones de acción en controles multimedia se derivan del estado Player.

De esta manera, puedes presentar un conjunto coherente de controles multimedia de control de contenido multimedia 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 que se describe en la siguiente tabla. En el modo compacto, solo las primeras tres acciones las ranuras restantes. Esto se alinea con la forma en que los controles multimedia se renderizan en otros Plataformas de Android, como Auto, Asistente y Wear OS

Ranura Criterios Acción
1 playWhenReady es falso o la 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
Ni el comando COMMAND_SEEK_TO_PREVIOUS ni el COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM del reproductor están disponibles, y hay un comando personalizado del diseño personalizado que aún no se colocó disponible para llenar el espacio. Personalizada
(aún no es compatible con Media3) Los extras de PlaybackState 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
Ni el comando COMMAND_SEEK_TO_NEXT ni el COMMAND_SEEK_TO_NEXT_MEDIA_ITEM del reproductor están disponibles, y hay un comando personalizado del diseño personalizado que aún no se colocó disponible para llenar el espacio. Personalizada
(aún no es compatible con Media3) Los extras de PlaybackState incluyen un valor booleano true para la clave EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT. Vacío
4 Un comando personalizado del diseño personalizado que aún no se ha colocado está disponible para llenar el espacio. Personalizada
5 Un comando personalizado del diseño personalizado que aún no se ha colocado está disponible para llenar el espacio. Personalizada

Los comandos personalizados se colocan en el orden en que se agregaron a la un diseño personalizado.

Personaliza los botones de comando

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

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

  2. En MediaSession.Callback.onConnect(), para autorizar a los controladores definiendo los comandos disponibles, como comandos personalizados, en el ConnectionResult.

  3. En MediaSession.Callback.onCustomCommand(), responder 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);
    }
  }
}

Si quieres obtener más información para configurar tu MediaSession, de modo que clientes como el de red puede conectarse a tu app de música, consulta Otorgar control a otros clientes.

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

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, la MediaController envía un comando de reproducción a tu MediaSession. El Luego, MediaSession delega esos comandos al reproductor. Comandos definido en el archivo Player de Media3 interfaz de la nube son controladas automáticamente por el sesión.

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

Comportamiento anterior a Android 13

Para ofrecer retrocompatibilidad, la IU del sistema sigue proporcionando un diseño alternativo. que use acciones de notificación para apps que no se actualizan a Android 13 o que no incluyen información de PlaybackState. Los botones de acción son derivada de la lista Notification.Action adjunta a MediaStyle notificación. El sistema muestra hasta cinco acciones en el orden en que que se agregaron. En el modo compacto, se muestran hasta tres botones, determinados por el valores pasados 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 MediaStyle notificación :

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

Admite la reanudación de contenido multimedia

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

La función de reanudación de la reproducción se puede activar o desactivar en la aplicación Configuración. en la sección Sonido > Opciones multimedia. El usuario también puede acceder a la Configuración tocando el icono de ajustes que aparece después de deslizar el carrusel.

Media3 ofrece APIs para facilitar la compatibilidad con la reanudación de contenido multimedia. Consulta la Reanudación de la reproducción con Media3 de Google para obtener orientación sobre la implementación de 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 mediante las APIs heredadas de MediaCompat.

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 no se configura la duración, la barra deslizante no mostrar progreso)

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

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 deslizante que se asigna al PlaybackState de MediaSession. La barra deslizante Permite que los usuarios cambien la posición y muestra el tiempo transcurrido del contenido multimedia. elemento. Para habilitar la barra de búsqueda, debes implementar PlaybackState.Builder#setActions e incluye 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 realizó.
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 realizó.
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ó.

Agrega acciones estándar

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

Para reproducir, pausar, anterior y siguiente, establece estas acciones en el 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 horarios anteriores ni en los siguientes, no agregues ACTION_SKIP_TO_PREVIOUS o ACTION_SKIP_TO_NEXT, y, en su lugar, agrega extras 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 agrégala a PlaybackState en su lugar. 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 las acciones de PlaybackState

Cuando un usuario presiona un botón, SystemUI usa MediaController.TransportControls para enviar un comando de vuelta a MediaSession. Debes registrar una devolución de llamada que puedan responder adecuadamente a esos 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);
}