Creazione di un servizio browser multimediale

La tua app deve dichiarare MediaBrowserService con un filtro per intent nel manifest. Puoi scegliere il nome del servizio. Nell'esempio seguente, il nome del servizio scelto è MediaPlaybackService.

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

Inizializzare la sessione multimediale

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

Il seguente codice onCreate() mostra 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());
    }
}

Gestire le connessioni client

Un MediaBrowserService ha due metodi che gestiscono 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.

Controllare le connessioni client con onGetRoot()

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

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

Per consentire ai client di connettersi a MediaSession senza sfogliare, onGetRoot() deve comunque restituire un BrowserRoot non nullo, ma l'ID radice deve rappresentare una gerarchia dei contenuti vuota.

Una tipica implementazione di onGetRoot() potrebbe essere simile alla seguente:

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 specifica quali connessioni sono consentite o, in alternativa, elenca le connessioni che devono essere vietate. Per un esempio di come implementare un ACL che consenta connessioni specifiche, consulta la classe PackageValidator nell' app di esempio Universal Android Music Player.

Dovresti prendere in considerazione 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 Riprodurre audio per Auto. Puoi esaminare clientPackageName al momento della connessione per determinare il tipo di client e restituire un BrowserRoot diverso a seconda del client (o rootHints, se presente).

Comunicare i contenuti con onLoadChildren()

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

Ogni MediaItem ha una stringa ID univoca, che è un token opaco. Quando un client vuole aprire un sottomenu o riprodurre un elemento, passa l'ID. Il tuo servizio è responsabile dell'associazione dell'ID al nodo del menu o all'elemento di contenuti 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 gli oggetti forniti da MediaBrowserService non devono contenere bitmap delle icone. Utilizza invece un Uri chiamando setIconUri() quando crei MediaDescription per ogni elemento.

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

Il ciclo di vita del servizio di navigazione multimediale

Il comportamento di un servizio Android service dipende dal fatto che sia avviato o associato a uno o più client. Dopo la creazione di un servizio, può essere avviato, associato o entrambi. In tutti questi stati, è completamente funzionale e può svolgere il lavoro per cui è stato progettato. La differenza è la durata del servizio. Un servizio associato non viene eliminato finché tutti i client associati non annullano l'associazione. 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, rendendo il servizio associato (ma non avviato). Questo comportamento predefinito è integrato nella classe MediaBrowserServiceCompat.

Un servizio associato (e non avviato) viene eliminato quando tutti i client annullano l'associazione. Se l'attività dell'UI si disconnette a questo punto, il servizio viene eliminato. Questo non è un problema se non hai ancora riprodotto musica. Tuttavia, quando la riproduzione inizia, l'utente probabilmente si aspetta di continuare ad ascoltare anche dopo aver cambiato app. Non vuoi eliminare il player quando annulli l'associazione dell'UI 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. In questo modo, il player continua a funzionare anche se l'attività dell'UI di controllo annulla l'associazione.

Per arrestare un servizio avviato, chiama 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 finché tutti i client non annullano l'associazione.

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 della sessione multimediale. In sintesi:

  • Il servizio viene creato quando viene avviato in risposta a un pulsante multimediale o quando un'attività si associa (dopo la connessione tramite MediaBrowser).
  • Il callback onPlay() della sessione multimediale deve includere il codice che chiama startService(). In questo modo, il servizio viene avviato e continua a essere eseguito, anche quando tutte le attività MediaBrowser dell'UI associate annullano l'associazione.
  • Il callback onStop() deve chiamare stopSelf(). Se il servizio è stato avviato, viene arrestato. Inoltre, il servizio viene eliminato se non sono associate attività. In caso contrario, il servizio rimane associato finché tutte le attività non annullano l'associazione. (Se viene ricevuta una chiamata startService() successiva prima che il servizio venga eliminato, l'arresto in attesa viene annullato.)

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

Ciclo di vita del servizio

Utilizzare le notifiche MediaStyle con un servizio in primo piano

Quando un servizio è in riproduzione, deve essere eseguito in primo piano. In questo modo, il sistema sa che il servizio sta svolgendo una funzione utile e non deve essere terminato se il sistema ha poca memoria. Un servizio in primo piano deve visualizzare una notifica in modo che l'utente ne sia a conoscenza e possa, facoltativamente, controllarlo. Il callback onPlay() deve mettere il servizio in primo piano. (Tieni presente che questo è un significato speciale di "primo piano". Sebbene Android consideri il servizio in primo piano ai fini della gestione dei processi, 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, idealmente con uno o più controlli di trasporto. La notifica deve includere anche informazioni utili dai metadati della sessione.

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

L'esempio seguente utilizza NotificationCompat.MediaStyle, progettato per le app multimediali. Mostra come creare una notifica che visualizza i metadati e i controlli di trasporto. Il metodo pratico 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 queste impostazioni di NotificationCompat:

  • Quando utilizzi setContentIntent(), il servizio viene avviato automaticamente quando fai clic sulla notifica, una funzionalità utile.
  • In una situazione "non attendibile" come la schermata di blocco, la visibilità predefinita per i contenuti delle notifiche è VISIBILITY_PRIVATE. Probabilmente vuoi visualizzare i controlli di trasporto sulla schermata di blocco, quindi VISIBILITY_PUBLIC è la soluzione ideale.
  • Fai attenzione quando imposti il colore di sfondo. In una notifica normale 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, il colore viene utilizzato per l'intero sfondo della notifica. Testa il colore di sfondo. Non affaticare gli occhi ed evita colori estremamente luminosi 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 complementari possono accedere alla sessione e controllarla.
  • Utilizza setShowActionsInCompactView() per aggiungere fino a 3 azioni da visualizzare nella contentView di dimensioni standard della notifica. (Qui viene specificato il pulsante Metti in pausa.)
  • In Android 5.0 (livello API 21) e versioni successive, puoi scorrere via una notifica per arrestare il player una volta che il servizio non è più in esecuzione in primo piano. Non puoi farlo 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 allegare all'azione di riproduzione. Il metodo MediaButtonReceiver.buildMediaButtonPendingIntent() converte un'azione PlaybackState in un PendingIntent.

Attivare la navigazione multimediale AVRCP

Oltre alle app personalizzate come Android Auto, anche il livello Bluetooth del sistema funge da client per MediaBrowserService per facilitare la navigazione remota wireless nel catalogo (AVRCP).

Su Android 16 e Android 17, la piattaforma richiede che le app che non utilizzano Media3 espongano un'attività specifica con un filtro per intent da convalidare per la navigazione.

Aggiungi questo filtro per intent specifico a un'attività esportata in AndroidManifest.xml. Tieni presente che CATEGORY_DEFAULT viene omesso intenzionalmente per impedire che la tua app venga visualizzata nei menu generici "Apri con" per i file audio locali:

<activity
    android:name=".BluetoothValidationActivity"
    android:exported="true"
    android:theme="@android:style/Theme.NoDisplay"
    android:excludeFromRecents="true"
    android:noHistory="true">
  <intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <data android:scheme="content" />
    <data android:host="media" />
    <!-- Specific path check used by Bluetooth stack for validation -->
    <data android:pathPrefix="/internal/audio/media/" />
    <data android:mimeType="audio/*" />
  </intent-filter>
</activity>