Únete a ⁠ #Android11: The Beta Launch Show el 3 de junio.

Cómo crear un servicio de exploración multimedia

Tu app debe declarar el objeto MediaBrowserService con un filtro de intents en su manifiesto. Puedes elegir tu propio nombre de servicio; en el siguiente ejemplo, es "MediaPlaybackService".

<service android:name=".MediaPlaybackService">
      <intent-filter>
        <action android:name="android.media.browse.MediaBrowserService" />
      </intent-filter>
    </service>
    

Nota: La implementación recomendada de MediaBrowserService es MediaBrowserServiceCompat, que se define en la biblioteca de compatibilidad de media-compat. En esta página, el término "MediaBrowserService" hace referencia a una instancia de MediaBrowserServiceCompat.

Inicializa la sesión multimedia

Cuando el servicio recibe el método de devolución de llamada del ciclo de vida de onCreate(), debe realizar estos pasos:

En el siguiente código de onCreate(), se demuestran los pasos:

Kotlin

    private const val MY_MEDIA_ROOT_ID = "media_root_id"
    private const val MY_EMPTY_MEDIA_ROOT_ID = "empty_root_id"

    class MediaPlaybackService : MediaBrowserServiceCompat() {

        private var mediaSession: MediaSessionCompat? = null
        private lateinit var stateBuilder: PlaybackStateCompat.Builder

        override fun onCreate() {
            super.onCreate()

            // Create a MediaSessionCompat
            mediaSession = MediaSessionCompat(baseContext, LOG_TAG).apply {

                // Enable callbacks from MediaButtons and TransportControls
                setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
                        or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
                )

                // Set an initial PlaybackState with ACTION_PLAY, so media buttons can start the player
                stateBuilder = PlaybackStateCompat.Builder()
                        .setActions(PlaybackStateCompat.ACTION_PLAY
                                        or PlaybackStateCompat.ACTION_PLAY_PAUSE
                        )
                setPlaybackState(stateBuilder.build())

                // MySessionCallback() has methods that handle callbacks from a media controller
                setCallback(MySessionCallback())

                // Set the session's token so that client activities can communicate with it.
                setSessionToken(sessionToken)
            }
        }
    }
    

Java

    public class MediaPlaybackService extends MediaBrowserServiceCompat {
        private static final String MY_MEDIA_ROOT_ID = "media_root_id";
        private static final String MY_EMPTY_MEDIA_ROOT_ID = "empty_root_id";

        private MediaSessionCompat mediaSession;
        private PlaybackStateCompat.Builder stateBuilder;

        @Override
        public void onCreate() {
            super.onCreate();

            // Create a MediaSessionCompat
            mediaSession = new MediaSessionCompat(context, LOG_TAG);

            // Enable callbacks from MediaButtons and TransportControls
            mediaSession.setFlags(
                  MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
                  MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);

            // Set an initial PlaybackState with ACTION_PLAY, so media buttons can start the player
            stateBuilder = new PlaybackStateCompat.Builder()
                                .setActions(
                                    PlaybackStateCompat.ACTION_PLAY |
                                    PlaybackStateCompat.ACTION_PLAY_PAUSE);
            mediaSession.setPlaybackState(stateBuilder.build());

            // MySessionCallback() has methods that handle callbacks from a media controller
            mediaSession.setCallback(new MySessionCallback());

            // Set the session's token so that client activities can communicate with it.
            setSessionToken(mediaSession.getSessionToken());
        }
    }
    

Administra las conexiones de clientes

Un objeto MediaBrowserService tiene dos métodos que manejan conexiones de clientes: onGetRoot() controla el acceso al servicio y onLoadChildren() brinda la posibilidad de que un cliente compile y muestre un menú de la jerarquía de contenido de .

Cómo controlar las conexiones de clientes con onGetRoot()

El método onGetRoot() muestra el nodo raíz de la jerarquía de contenido. Si el método muestra un valor nulo, se rechaza la conexión.

Para permitir que los clientes se conecten a tu servicio y exploren su contenido multimedia, onGetRoot() debe mostrar un valor no nulo de BrowserRoot, que es un ID raíz que representa tu jerarquía de contenido.

Para permitir que los clientes se conecten a tu MediaSession sin navegar, onGetRoot() igual debe mostrar un valor de BrowserRoot no nulo, pero el ID raíz debe representar una jerarquía de contenido vacía.

Una implementación típica de onGetRoot() podría verse de esta manera:

Kotlin

    override fun onGetRoot(
            clientPackageName: String,
            clientUid: Int,
            rootHints: Bundle?
    ): MediaBrowserServiceCompat.BrowserRoot {

        // (Optional) Control the level of access for the specified package name.
        // You'll need to write your own logic to do this.
        return if (allowBrowsing(clientPackageName, clientUid)) {
            // Returns a root ID that clients can use with onLoadChildren() to retrieve
            // the content hierarchy.
            MediaBrowserServiceCompat.BrowserRoot(MY_MEDIA_ROOT_ID, null)
        } else {
            // Clients can connect, but this BrowserRoot is an empty hierachy
            // so onLoadChildren returns nothing. This disables the ability to browse for content.
            MediaBrowserServiceCompat.BrowserRoot(MY_EMPTY_MEDIA_ROOT_ID, null)
        }
    }
    

Java

    @Override
    public BrowserRoot onGetRoot(String clientPackageName, int clientUid,
        Bundle rootHints) {

        // (Optional) Control the level of access for the specified package name.
        // You'll need to write your own logic to do this.
        if (allowBrowsing(clientPackageName, clientUid)) {
            // Returns a root ID that clients can use with onLoadChildren() to retrieve
            // the content hierarchy.
            return new BrowserRoot(MY_MEDIA_ROOT_ID, null);
        } else {
            // Clients can connect, but this BrowserRoot is an empty hierachy
            // so onLoadChildren returns nothing. This disables the ability to browse for content.
            return new BrowserRoot(MY_EMPTY_MEDIA_ROOT_ID, null);
        }
    }
    

En algunos casos, sería conveniente implementar un esquema de lista blanca/negra para controlar las conexiones. Para ver un ejemplo de lista blanca, consulta la clase PackageValidator en la app de Universal Android Music Player de muestra.

Deberías proporcionar diferentes jerarquías de contenido según el tipo de cliente que realice la búsqueda. En particular, Android Auto limita la forma en la que los usuarios interactúan con las apps de audio. Para obtener más información, consulta Cómo reproducir audio en Auto. Puedes mirar el objeto clientPackageName en el momento de la conexión para determinar el tipo de cliente y mostrar un objeto BrowserRoot distinto según el cliente (o rootHints si hay).

Cómo enviar contenido con onLoadChildren()

Luego de que el cliente se conecta, puede desviar la jerarquía de contenido realizando llamadas repetidas a MediaBrowserCompat.subscribe() para compilar una representación local de la IU. El método subscribe() envía la devolución de llamada onLoadChildren() al servicio, el cual muestra una lista de objetos MediaBrowser.MediaItem.

Cada MediaItem tiene una string de ID único, que es un token opaco. Cuando un cliente quiere abrir un submenú o reproducir un elemento, pasa el ID. Tu servicio está a cargo de asociar el ID con el nodo de menú o elemento de contenido apropiado.

Una implementación simple de onLoadChildren() podría verse de esta manera:

Kotlin

    override fun onLoadChildren(
            parentMediaId: String,
            result: MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>>
    ) {
        //  Browsing not allowed
        if (MY_EMPTY_MEDIA_ROOT_ID == parentMediaId) {
            result.sendResult(null)
            return
        }

        // Assume for example that the music catalog is already loaded/cached.

        val mediaItems = emptyList<MediaBrowserCompat.MediaItem>()

        // Check if this is the root menu:
        if (MY_MEDIA_ROOT_ID == parentMediaId) {
            // Build the MediaItem objects for the top level,
            // and put them in the mediaItems list...
        } else {
            // Examine the passed parentMediaId to see which submenu we're at,
            // and put the children of that menu in the mediaItems list...
        }
        result.sendResult(mediaItems)
    }
    

Java

    @Override
    public void onLoadChildren(final String parentMediaId,
        final Result<List<MediaItem>> result) {

        //  Browsing not allowed
        if (TextUtils.equals(MY_EMPTY_MEDIA_ROOT_ID, parentMediaId)) {
            result.sendResult(null);
            return;
        }

        // Assume for example that the music catalog is already loaded/cached.

        List<MediaItem> mediaItems = new ArrayList<>();

        // Check if this is the root menu:
        if (MY_MEDIA_ROOT_ID.equals(parentMediaId)) {
            // Build the MediaItem objects for the top level,
            // and put them in the mediaItems list...
        } else {
            // Examine the passed parentMediaId to see which submenu we're at,
            // and put the children of that menu in the mediaItems list...
        }
        result.sendResult(mediaItems);
    }
    

Nota:MediaItem Los objetos que muestra MediaBrowserService no deben contener mapas de bits de íconos. En cambio, llama a setIconUri() para usar un Uri cuando compiles la MediaDescription para cada elemento.

Para ver un ejemplo de cómo implementar onLoadChildren(), consulta la app de Universal Android Music Player de muestra.

Ciclo de vida del servicio de exploración multimedia

El comportamiento de un servicio de Android depende de si está iniciado o está vinculado a uno o más clientes. Después de crear un servicio, se puede iniciar o vincular, o ambas opciones. En todos estos estados, es completamente funcional y puede realizar las tareas para las que se diseñó. La diferencia es la duración del servicio. No se destruye un servicio vinculado hasta que todos sus clientes vinculados se hayan desvinculado. Se puede detener y destruir de manera explícita un servicio iniciado (suponiendo que ya no está vinculado a ningún cliente).

Cuando un MediaBrowser que se ejecuta en otra actividad se conecta a un MediaBrowserService, vincula la actividad al servicio, con lo cual el servicio se vincula, pero no se inicia. Este comportamiento predeterminado está integrado en la clase MediaBrowserServiceCompat.

Un servicio que solo está vinculado (y no iniciado) se destruye cuando se desvinculan todos sus clientes. Si se desconecta la actividad de tu IU en este punto, se destruye el servicio. Esto no es un problema si aún no reprodujiste música. Sin embargo, cuando se inicie la reproducción, es posible que el usuario espere seguir escuchando la música, incluso después de cambiar a otra app. No es la idea destruir el reproductor cuando desvinculas la IU para usar otra app.

Por tal motivo, debes llamar a startService() para asegurarte de que el servicio esté iniciado cuando comience a reproducir contenido. Un servicio iniciado se debe detener de manera explícita, ya sea que esté vinculado o no. De esa manera garantizas que tu reproductor siga funcionando incluso si se desvincula la actividad de la IU que lo controla.

Para detener un servicio iniciado, llama a Context.stopService() o a stopSelf(). El sistema detiene y destruye el servicio lo antes posible. Sin embargo, si uno o más clientes siguen vinculados al servicio, la llamada para detenerlo se demora hasta que todos sus clientes se hayan desvinculado.

El ciclo de vida de MediaBrowserService está controlado por la forma en que se crea, el número de clientes vinculados a él y las llamadas que recibe de las devoluciones de llamada de la sesión multimedia. En resumen:

  • Se crea el servicio cuando se inicia en respuesta a un botón multimedia o cuando se vincula una actividad a él (después de conectarse a través de su MediaBrowser).
  • La devolución de llamada onPlay() de la sesión multimedia debe incluir código que llame a startService(). Esto garantiza que se inicie y se siga ejecutando el servicio, incluso si se desvinculan todas las actividades MediaBrowser de la IU que estaban vinculadas.
  • La devolución de llamada onStop() debe llamar a stopSelf(). Si el servicio está iniciado, esta acción lo detiene. Además, se destruye el servicio si no hay actividades vinculadas a él. De lo contrario, permanece vinculado hasta que se desvinculan todas sus actividades. (Si se recibe una llamada startService() posterior antes de que se destruya el servicio, se cancela la detención pendiente).

El siguiente diagrama de flujo demuestra cómo se administra el ciclo de vida de un servicio. El contador de variables realiza un seguimiento del número de clientes vinculados:

Ciclo de vida del servicio

Cómo usar notificaciones MediaStyle con un servicio en primer plano

Cuando se está reproduciendo un servicio, debe ejecutarse en primer plano. De este modo, el sistema sabe que el servicio está realizando una función útil y que no se debe cerrar si el sistema tiene poca memoria. Un servicio en primer plano debe mostrar una notificación para que el usuario esté al tanto y tenga la opción de controlarlo. La devolución de llamada onPlay() debe colocar el servicio en primer plano. (Ten en cuenta que este es un significado especial de "primer plano". Si bien Android considera que el servicio está en primer plano para los fines de administración de procesos, desde el punto de vista del usuario, el reproductor se está ejecutando en segundo plano y hay otra app visible en el "primer plano" de la pantalla).

Cuando un servicio se ejecuta en primer plano, debe mostrar una notificación, idealmente con uno o más controles de transporte. La notificación también debe incluir información útil proveniente de los metadatos de la sesión.

Compila y muestra la notificación cuando el reproductor comience a reproducir contenido. El mejor lugar para hacerlo es dentro del método MediaSessionCompat.Callback.onPlay().

El ejemplo que se muestra a continuación usa el objeto NotificationCompat.MediaStyle, que está diseñado para apps de contenido multimedia. Indica cómo compilar una notificación que muestre metadatos y controles de transporte. El método de conveniencia getController() te permite crear un controlador multimedia directamente desde la sesión multimedia.

Kotlin

    // Given a media session and its context (usually the component containing the session)
    // Create a NotificationCompat.Builder

    // Get the session's metadata
    val controller = mediaSession.controller
    val mediaMetadata = controller.metadata
    val description = mediaMetadata.description

    val builder = NotificationCompat.Builder(context, channelId).apply {
        // Add the metadata for the currently playing track
        setContentTitle(description.title)
        setContentText(description.subtitle)
        setSubText(description.description)
        setLargeIcon(description.iconBitmap)

        // Enable launching the player by clicking the notification
        setContentIntent(controller.sessionActivity)

        // Stop the service when the notification is swiped away
        setDeleteIntent(
                MediaButtonReceiver.buildMediaButtonPendingIntent(
                        context,
                        PlaybackStateCompat.ACTION_STOP
                )
        )

        // Make the transport controls visible on the lockscreen
        setVisibility(NotificationCompat.VISIBILITY_PUBLIC)

        // Add an app icon and set its accent color
        // Be careful about the color
        setSmallIcon(R.drawable.notification_icon)
        color = ContextCompat.getColor(context, R.color.primaryDark)

        // Add a pause button
        addAction(
                NotificationCompat.Action(
                        R.drawable.pause,
                        getString(R.string.pause),
                        MediaButtonReceiver.buildMediaButtonPendingIntent(
                                context,
                                PlaybackStateCompat.ACTION_PLAY_PAUSE
                        )
                )
        )

        // Take advantage of MediaStyle features
        setStyle(android.support.v4.media.app.NotificationCompat.MediaStyle()
                .setMediaSession(mediaSession.sessionToken)
                .setShowActionsInCompactView(0)

                // Add a cancel button
                .setShowCancelButton(true)
                .setCancelButtonIntent(
                        MediaButtonReceiver.buildMediaButtonPendingIntent(
                                context,
                                PlaybackStateCompat.ACTION_STOP
                        )
                )
        )
    }

    // Display the notification and place the service in the foreground
    startForeground(id, builder.build())
    

Java

    // Given a media session and its context (usually the component containing the session)
    // Create a NotificationCompat.Builder

    // Get the session's metadata
    MediaControllerCompat controller = mediaSession.getController();
    MediaMetadataCompat mediaMetadata = controller.getMetadata();
    MediaDescriptionCompat description = mediaMetadata.getDescription();

    NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId);

    builder
        // Add the metadata for the currently playing track
        .setContentTitle(description.getTitle())
        .setContentText(description.getSubtitle())
        .setSubText(description.getDescription())
        .setLargeIcon(description.getIconBitmap())

        // Enable launching the player by clicking the notification
        .setContentIntent(controller.getSessionActivity())

        // Stop the service when the notification is swiped away
        .setDeleteIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(context,
           PlaybackStateCompat.ACTION_STOP))

        // Make the transport controls visible on the lockscreen
        .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)

        // Add an app icon and set its accent color
        // Be careful about the color
        .setSmallIcon(R.drawable.notification_icon)
        .setColor(ContextCompat.getColor(context, R.color.primaryDark))

        // Add a pause button
        .addAction(new NotificationCompat.Action(
            R.drawable.pause, getString(R.string.pause),
            MediaButtonReceiver.buildMediaButtonPendingIntent(context,
                PlaybackStateCompat.ACTION_PLAY_PAUSE)))

        // Take advantage of MediaStyle features
        .setStyle(new MediaStyle()
            .setMediaSession(mediaSession.getSessionToken())
            .setShowActionsInCompactView(0)

            // Add a cancel button
           .setShowCancelButton(true)
           .setCancelButtonIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(context,
               PlaybackStateCompat.ACTION_STOP)));

    // Display the notification and place the service in the foreground
    startForeground(id, builder.build());
    

Cuando uses notificaciones MediaStyle, ten en cuenta el comportamiento de esta configuración de NotificationCompat:

  • Si usas setContentIntent(), tu servicio se inicia automáticamente cuando el usuario hace clic en la notificación, lo cual es una función útil.
  • En una situación "no segura", como la pantalla de bloqueo, la visibilidad predeterminada del contenido de la notificación es VISIBILITY_PRIVATE. Es probable que quieras ver los controles de transporte en la pantalla de bloqueo; si es así, usa VISIBILITY_PUBLIC.
  • Ten cuidado cuando establezcas el color de fondo. En una notificación normal en la versión 5.0 o en versiones posteriores de Android, el color se aplica solamente al fondo del ícono pequeño de la app. Sin embargo, para las notificaciones MediaStyle anteriores a Android 7.0, el color se utiliza en todo el fondo de las notificaciones. Prueba el color de fondo. Usa un tono que sea suave para la vista; evita los colores extremadamente brillantes o fluorescentes.

Esta configuración solo está disponible cuando usas NotificationCompat.MediaStyle:

  • Usa setMediaSession() para asociar la notificación con tu sesión. De este modo, las apps de terceros y los dispositivos complementarios pueden acceder a la sesión y controlarla.
  • Usa setShowActionsInCompactView() para agregar hasta 3 acciones que se mostrarán en la ContentView de tamaño estándar de la notificación. (Aquí se especifica el botón de pausa).
  • En Android 5.0 (API nivel 21) y versiones posteriores, puedes deslizar una notificación para detener el reproductor cuando el servicio no se esté ejecutando en primer plano. Esto no era posible en versiones anteriores. Para permitir que los usuarios quiten la notificación y detengan la reproducción en versiones previas a Android 5.0 (API nivel 21), puedes llamar a setShowCancelButton(true) y a setCancelButtonIntent() a fin de agregar un botón para cancelar en la esquina superior derecha de la notificación.

Cuando agregues los botones para pausar y cancelar, necesitarás adjuntar un PendingIntent a la acción de reproducción. El método MediaButtonReceiver.buildMediaButtonPendingIntent() hace la tarea de convertir una acción de PlaybackState en un PendingIntent.