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:
- Crea e inizializza la sessione multimediale.
- Imposta il callback della sessione multimediale
- Impostare il token della sessione multimediale
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 il suo
MediaBrowser
). - Il callback della sessione multimediale
onPlay()
deve includere il codice che effettua la chiamata astartService()
. 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 chiamarestopSelf()
. 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 astartService()
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:
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, ma 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 è progettato per le app multimediali. Spiega 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 un account "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)
esetCancelButtonIntent()
.
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.