Creazione di un servizio browser multimediale

La tua app deve dichiarare MediaBrowserService con un filtro per intent nel file manifest. Puoi scegliere il nome del servizio che preferisci, nell'esempio seguente, è "MediaPlaybackService".

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

Nota: l'implementazione consigliata di MediaBrowserService è MediaBrowserServiceCompat. definito nel libreria di supporto media-compat. In questa pagina il termine "MediaBrowserService" si riferisce a un'istanza di MediaBrowserServiceCompat.

Inizializzare la sessione multimediale

Quando il servizio riceve il metodo di callback del ciclo di vita onCreate(), deve eseguire questi passaggi:

Il codice onCreate() riportato di seguito illustra questi passaggi:

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

Gestisci connessioni client

Un MediaBrowserService ha due metodi per gestire le connessioni client: Controlli di onGetRoot() l'accesso al servizio onLoadChildren() consente a un cliente di creare e visualizzare un menu della gerarchia dei contenuti di MediaBrowserService.

Controllo delle connessioni client con onGetRoot()

Il metodo onGetRoot() restituisce il nodo radice della gerarchia dei contenuti. Se restituisce null, la connessione viene rifiutata.

Per consentire ai client di connettersi al tuo servizio e sfogliarne i contenuti multimediali: onGetRoot() deve restituire un BrowserRoot diverso da null, che è un ID root rappresenta la gerarchia dei contenuti.

Per consentire ai clienti di connettersi a MediaSession senza navigare, onGetRoot() deve comunque restituire un valore BrowserRoot diverso da null, ma l'ID root deve rappresentare un una gerarchia di contenuti vuota.

Un'implementazione tipica di onGetRoot() potrebbe avere il seguente aspetto:

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 hierarchy
        // 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 hierarchy
        // so onLoadChildren returns nothing. This disables the ability to browse for content.
        return new BrowserRoot(MY_EMPTY_MEDIA_ROOT_ID, null);
    }
}

In alcuni casi, potresti voler controllare chi può connettersi al tuo MediaBrowserService. Un modo è utilizzare un elenco di controllo dell'accesso (ACL) che specifica quali connessioni sono consentite o, in alternativa, enumera le connessioni da vietare. Esempio di come implementare un ACL che consente connessioni specifiche, Strumento di convalida pacchetti nel corso Universal Android Music Player di esempio.

Dovresti valutare l'opportunità di fornire gerarchie di contenuti diverse a seconda il tipo di client che sta eseguendo la query. In particolare, Android Auto limita il modo in cui gli utenti interagiscono con le app audio. Per ulteriori informazioni, consulta la sezione Riproduzione audio per Automatico. Tu può esaminare clientPackageName al momento della connessione per determinare il client e restituire un BrowserRoot diverso a seconda del client (o rootHints se ce ne sono).

Comunicazione di contenuti con onLoadChildren()

Una volta connesso, il client può attraversare la gerarchia dei contenuti effettuando chiamate ripetute a MediaBrowserCompat.subscribe() per creare una rappresentazione locale della UI. Il metodo subscribe() invia il callback onLoadChildren() al servizio, che restituisce un elenco di oggetti MediaBrowser.MediaItem.

Ogni MediaItem ha una stringa ID univoca, che è un token opaco. Quando un cliente vuole aprire un sottomenu o riprodurre un elemento, passa l'ID. Il tuo servizio è responsabile dell'associazione dell'ID al nodo del menu o al contenuto appropriato.

Una semplice implementazione di onLoadChildren() potrebbe essere simile alla seguente:

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 oggetti distribuiti da MediaBrowserService non devono contenere bitmap delle icone. Usa invece un Uri chiamando setIconUri() quando crei MediaDescription per ogni elemento.

Per un esempio di come implementare onLoadChildren(), guarda l'app di esempio Universal Android Music Player.

Il ciclo di vita del servizio di browser multimediali

Il comportamento di un servizio Android dipende dal fatto che sia stato avviato o associato a uno o più client. Una volta creato, un servizio può essere avviato, associato o entrambi. In tutti questi stati, è perfettamente funzionante e può svolgere le operazioni per cui è stato progettato. La differenza è la durata del servizio. Un servizio associato non viene eliminato fino a quando tutti i relativi client associati non vengono slegati. Un servizio avviato può essere arrestato ed eliminato in modo esplicito (supponendo che non sia più associato ad alcun client).

Quando un MediaBrowser in esecuzione in un'altra attività si connette a un MediaBrowserService, associa l'attività al servizio, vincolando il servizio (ma non avviato). Questo comportamento predefinito è integrato nella classe MediaBrowserServiceCompat.

Un servizio solo associato (e non avviato) viene eliminato quando tutti i relativi client vengono slegati. Se a questo punto l'attività dell'interfaccia utente si disconnette, il servizio viene eliminato. Questo non è un problema se non hai ancora riprodotto musica. Tuttavia, all'avvio della riproduzione, è probabile che l'utente si aspetti di continuare ad ascoltare anche dopo aver cambiato app. Non è consigliabile distruggere il player quando sblocchi la UI per farla funzionare con un'altra app.

Per questo motivo, devi assicurarti che il servizio venga avviato all'inizio per giocare chiamando startService(). R il servizio avviato deve essere arrestato in modo esplicito, indipendentemente dal fatto che sia associato o meno. Questo assicura che il player continui a funzionare anche se l'interfaccia utente di controllo slega l'attività.

Per interrompere un servizio avviato, chiama Context.stopService() o stopSelf(). Il sistema si arresta e distrugge il servizio appena possibile. Tuttavia, se uno o più client sono ancora associati al servizio, la chiamata per arrestare il servizio viene ritardata fino allo slegamento di tutti i client.

Il ciclo di vita di MediaBrowserService è controllato dal modo in cui viene creato, dal numero di client associati e dalle chiamate che riceve dai callback delle sessioni multimediali. In sintesi:

  • Il servizio viene creato quando viene avviato in risposta a un pulsante multimediale o quando un'attività si associa al servizio (dopo la connessione tramite MediaBrowser).
  • Il callback della sessione multimediale onPlay() deve includere il codice che effettua la chiamata a startService(). In questo modo, il servizio viene avviato e continua a essere eseguito, anche quando tutte le attività MediaBrowser dell'UI associate a quest'ultimo vengono slegate.
  • Il callback onStop() deve chiamare stopSelf(). Se il servizio è stato avviato, viene interrotto. Inoltre, il servizio viene eliminato se non sono associate attività. In caso contrario, il servizio rimane associato fino a quando tutte le sue attività non vengono svincolate. (se viene ricevuta una chiamata a startService() successiva prima che il servizio venga eliminato, l'interruzione in attesa viene annullata.)

Il seguente diagramma di flusso mostra come viene gestito il ciclo di vita di un servizio. Il contatore di variabili tiene traccia del numero di client associati:

Ciclo di vita dei servizi

Utilizzo delle notifiche di MediaStyle con un servizio in primo piano

Quando un servizio è in riproduzione, dovrebbe essere in esecuzione in primo piano. Ciò consente al sistema di sapere che il servizio sta svolgendo una funzione utile e non dovrebbe essere interrotto se la memoria del sistema è insufficiente. Un servizio in primo piano deve visualizzare una notifica in modo che l'utente sia a conoscenza del problema e possa controllarlo se necessario. Il callback onPlay() deve mettere il servizio in primo piano. Tieni presente che questo è un significato speciale di "primo piano". Android considera il servizio in primo piano ai fini della gestione dei processi, mentre per l'utente il player è in riproduzione in background, mentre un'altra app è visibile in "primo piano" sullo schermo.)

Quando un servizio viene eseguito in primo piano, deve visualizzare una notifica, possibilmente con uno o più controlli di trasporto. La notifica deve includere anche informazioni utili tratte dai metadati della sessione.

Crea e mostra la notifica quando inizia la riproduzione del player. Il modo migliore per farlo è all'interno del metodo MediaSessionCompat.Callback.onPlay().

L'esempio riportato di seguito utilizza la classe NotificationCompat.MediaStyle, che è stata progettata per le app multimediali. Mostra come creare una notifica che mostri i metadati e i controlli di trasporto. Il metodo di convenienza getController() consente di creare un controller multimediale direttamente dalla sessione multimediale.

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

Quando utilizzi le notifiche MediaStyle, tieni presente il comportamento di questi Impostazioni NotificationCompat:

  • Quando usi setContentIntent(), il servizio si avvia automaticamente quando la notifica un clic, una pratica funzione.
  • In uno stato "non attendibile" situazione come la schermata di blocco, la visibilità predefinita per i contenuti delle notifiche è VISIBILITY_PRIVATE. Probabilmente vorrai vedere controlli per il trasporto nella schermata di blocco: VISIBILITY_PUBLIC è la scelta giusta.
  • Fai attenzione quando imposti il colore di sfondo. In una notifica ordinaria Android 5.0 o versioni successive, il colore viene applicato solo allo sfondo dei piccola icona dell'app. Ma per le notifiche MediaStyle precedenti ad Android 7.0, il colore viene utilizzato per l'intero sfondo delle notifiche. Testa il colore dello sfondo. Vai delicato sugli occhi ed evitare colori estremamente luminosi o fluorescenti.

Queste impostazioni sono disponibili solo quando utilizzi NotificationCompat.MediaStyle:

  • Usa setMediaSession() per associare la notifica alla tua sessione. In questo modo le app di terze parti e dispositivi associati per accedere alla sessione e controllarla.
  • Usa setShowActionsInCompactView() per aggiungere fino a 3 azioni da mostrare in l'elemento contentView di dimensioni standard della notifica. (Qui il pulsante Pausa è specificato.)
  • In Android 5.0 (livello API 21) e versioni successive puoi far scorrere una notifica per interrompere player quando il servizio non è più in esecuzione in primo piano. Non puoi eseguire questa operazione nelle versioni precedenti. Consentire agli utenti di rimuovere la notifica e interrompere la riproduzione prima di Android 5.0 (livello API 21), puoi aggiungere un pulsante Annulla nell'angolo in alto a destra della chiamando setShowCancelButton(true) e setCancelButtonIntent().

Quando aggiungi i pulsanti per la pausa e l'annullamento, dovrai allegare un PendingIntent all'azione di riproduzione. Il metodo MediaButtonReceiver.buildMediaButtonPendingIntent() svolge il compito di convertire un'azione PlaybackState in un PendingIntent.