Creazione di un servizio browser multimediale

L'app deve dichiarare MediaBrowserService con un filtro per intent nel file manifest. Puoi scegliere un nome personalizzato per il servizio, che 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. definita nella 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());
    }
}

Gestione delle connessioni client

Un MediaBrowserService ha due metodi per gestire le connessioni client: onGetRoot() controlla l'accesso al servizio e onLoadChildren() consente a un client 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 il metodo restituisce un valore null, la connessione viene rifiutata.

Per consentire ai client di connettersi al tuo servizio e sfogliare i relativi contenuti multimediali, onGetRoot() deve restituire un BrowserRoot con valore non null, ovvero un ID radice che rappresenta la gerarchia dei contenuti.

Per consentire ai client di connettersi alla MediaSession senza navigare, onGetRoot() deve comunque restituire un BrowserRoot con valore non null, ma l'ID root deve rappresentare una gerarchia di contenuti vuota.

Una tipica implementazione 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 a MediaBrowserService. Un modo è utilizzare un elenco di controllo dell'accesso (ACL) che specifichi quali connessioni sono consentite o, in alternativa, elenca quali connessioni sono vietate. Per un esempio di come implementare un ACL che consente connessioni specifiche, consulta la classe PackageValidator nell'app di esempio Universal Android Music Player.

Valuta la possibilità di fornire gerarchie di contenuti diverse a seconda del tipo di client che esegue 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 automatica. Puoi controllare il clientPackageName al momento della connessione per determinare il tipo di client e restituire un BrowserRoot diverso a seconda del client (o rootHints se disponibile).

Comunicazione dei 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, trasmette l'ID. Il servizio è responsabile dell'associazione dell'ID al nodo di menu o alla voce di contenuto appropriati.

Una semplice implementazione di onLoadChildren() potrebbe avere il seguente aspetto:

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

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

Ciclo di vita del servizio browser multimediale

Il comportamento di un servizio Android dipende dal fatto che sia avviato o associato a uno o più client. Dopo la creazione, un servizio può essere avviato, associato o entrambi. In tutti questi stati, è completamente funzionante e può eseguire le attività per cui è stato progettato. La differenza è la durata del servizio. Un servizio associato non viene eliminato fino a quando tutti i client associati non vengono svincolati. Un servizio avviato può essere interrotto 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, rendendo il servizio associato (ma non avviato). Questo comportamento predefinito è integrato nella classe MediaBrowserServiceCompat.

Un servizio che è solo associato (e non avviato) viene eliminato quando tutti i suoi client si svincolano. Se l'attività UI si disconnette a questo punto, il servizio viene eliminato. Questo non è un problema se non hai ancora riprodotto alcuna musica. Tuttavia, quando inizia la riproduzione, l'utente probabilmente si aspetta di continuare ad ascoltare anche dopo aver cambiato app. Non vuoi distruggere il player quando svincoli l'interfaccia utente per lavorare con un'altra app.

Per questo motivo, devi assicurarti che il servizio venga avviato quando inizia la riproduzione chiamando startService(). Un servizio avviato deve essere arrestato in modo esplicito, indipendentemente dal fatto che sia associato o meno. Ciò garantisce che il player continui a essere eseguito anche se l'attività dell'interfaccia utente di controllo si svincola.

Per interrompere un servizio avviato, chiama il numero Context.stopService() o stopSelf(). Il sistema arresta ed elimina il servizio il prima possibile. Tuttavia, se uno o più client sono ancora associati al servizio, la chiamata per arrestare il servizio viene ritardata fino a quando tutti i client si svincolano.

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 di sessioni multimediali. Riassumendo:

  • Il servizio viene creato quando viene avviato in risposta a un pulsante multimediale o quando viene associata un'attività (dopo la connessione tramite il suo MediaBrowser).
  • Il callback onPlay() della sessione multimediale deve includere il codice che chiama startService(). Ciò garantisce che il servizio venga avviato e continui a essere eseguito, anche quando tutte le attività MediaBrowser della UI associate si svincolano.
  • Il callback onStop() dovrebbe chiamare stopSelf(). Se il servizio è stato avviato, viene interrotto. Inoltre, il servizio viene eliminato se non vi sono attività associate. In caso contrario, il servizio rimane associato fino a quando tutte le sue attività non vengono svincolate. Se viene ricevuta una chiamata 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 del servizio

Utilizzo delle notifiche MediaStyle con un servizio in primo piano

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

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

Crea e visualizza la notifica quando inizia la riproduzione del player. Il modo migliore per farlo è utilizzare il metodo MediaSessionCompat.Callback.onPlay().

L'esempio seguente utilizza NotificationCompat.MediaStyle, progettato per le app multimediali. Mostra come creare una notifica che mostri i metadati e i controlli di trasporto. Il metodo pratico getController() ti consente di creare un controller multimediale direttamente dalla tua 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 queste impostazioni di NotificationCompat:

  • Quando usi setContentIntent(), il servizio si avvia automaticamente quando viene fatto clic sulla notifica, una comoda funzionalità.
  • In una situazione "non attendibile" come la schermata di blocco, la visibilità predefinita per i contenuti delle notifiche è VISIBILITY_PRIVATE. Se vuoi vedere i controlli di trasporto sulla schermata di blocco, VISIBILITY_PUBLIC è la scelta giusta.
  • Fai attenzione quando imposti il colore dello sfondo. In una notifica ordinaria in Android 5.0 o versioni successive, il colore viene applicato solo allo sfondo della piccola icona dell'app. Tuttavia, per le notifiche MediaStyle precedenti ad Android 7.0, per l'intero sfondo viene utilizzato il colore. Verifica il colore di sfondo. Sono delicati sugli occhi ed evita colori estremamente brillanti o fluorescenti.

Queste impostazioni sono disponibili solo quando utilizzi NotificationCompat.MediaStyle:

  • Utilizza setMediaSession() per associare la notifica alla sessione. In questo modo le app di terze parti e i dispositivi associati possono accedere alla sessione e controllarla.
  • Usa setShowActionsInCompactView() per aggiungere fino a 3 azioni da mostrare nella contentView di dimensioni standard della notifica. (qui è specificato il pulsante di pausa).
  • In Android 5.0 (livello API 21) e versioni successive puoi far scorrere una notifica per interrompere il player quando il servizio non è più in esecuzione in primo piano. Non puoi eseguire questa operazione nelle versioni precedenti. Per 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 notifica chiamando setShowCancelButton(true) e setCancelButtonIntent().

Quando aggiungi i pulsanti Metti in pausa e Annulla, avrai bisogno di un PendingIntent da collegare all'azione di riproduzione. Il metodo MediaButtonReceiver.buildMediaButtonPendingIntent() esegue il compito di convertire un'azione PlaybackState in PendingIntent.