Aplikacja musi zadeklarować MediaBrowserService za pomocą filtra intencji w pliku manifestu. Możesz wybrać własną nazwę usługi. W tym przykładzie wybrana nazwa usługi to MediaPlaybackService.
<service android:name=".MediaPlaybackService">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
Inicjowanie sesji multimedialnej
Gdy usługa otrzyma metodę wywołania zwrotnego cyklu życia onCreate(), powinna wykonać te czynności:
- Utwórz i zainicjuj sesję multimedialną.
- Ustawianie wywołania zwrotnego sesji multimediów
- Ustawianie tokena sesji multimediów
Poniższy kod onCreate() pokazuje te kroki:
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()); } }
Zarządzanie połączeniami klientów
MediaBrowserService ma 2 metody obsługi połączeń klientów:onGetRoot() kontroluje dostęp do usługi, a onLoadChildren() umożliwia klientowi tworzenie i wyświetlanie menu hierarchii treści MediaBrowserService.
Kontrolowanie połączeń klientów za pomocą onGetRoot()
Metoda onGetRoot() zwraca węzeł główny hierarchii treści. Jeśli metoda zwraca wartość null, połączenie jest odrzucane.
Aby umożliwić klientom łączenie się z usługą i przeglądanie jej treści multimedialnych, onGetRoot()musi zwracać niepusty element BrowserRoot, który jest identyfikatorem głównym reprezentującym hierarchię treści.
Aby umożliwić klientom łączenie się z sesją MediaSession bez przeglądania, onGetRoot()musi nadal zwracać niepusty obiekt BrowserRoot, ale identyfikator główny powinien reprezentować pustą hierarchię treści.
Typowa implementacja onGetRoot() może wyglądać tak:
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); } }
W niektórych przypadkach możesz chcieć kontrolować, kto może łączyć się z Twoim MediaBrowserService. Jednym ze sposobów jest użycie listy kontroli dostępu (ACL), która określa, które połączenia są dozwolone, lub alternatywnie wymienia połączenia, które powinny być zabronione. Przykład implementacji listy kontroli dostępu (ACL), która zezwala na określone połączenia, znajdziesz w klasie PackageValidator w przykładowej aplikacji Universal Android Music Player.
Warto rozważyć udostępnianie różnych hierarchii treści w zależności od tego, jaki typ klienta wysyła zapytanie. Android Auto ogranicza w szczególności sposób, w jaki użytkownicy korzystają z aplikacji audio. Więcej informacji znajdziesz w artykule Odtwarzanie dźwięku w Androidzie Auto. W momencie połączenia możesz sprawdzić clientPackageName, aby określić typ klienta, i zwrócić inny BrowserRoot w zależności od klienta (lub rootHints, jeśli nie ma żadnego).
Komunikowanie treści z użytkownikiem onLoadChildren()
Po nawiązaniu połączenia klient może przeglądać hierarchię treści, wielokrotnie wywołując funkcję MediaBrowserCompat.subscribe(), aby utworzyć lokalną reprezentację interfejsu. Metoda subscribe() wysyła wywołanie zwrotneonLoadChildren() do usługi, która zwraca listę obiektów MediaBrowser.MediaItem.
Każdy element MediaItem ma unikalny ciąg znaków identyfikatora, który jest nieprzezroczystym tokenem. Gdy klient chce otworzyć podmenu lub odtworzyć element, przekazuje jego identyfikator. Twoja usługa jest odpowiedzialna za powiązanie identyfikatora z odpowiednim węzłem menu lub elementem treści.
Prosta implementacja funkcji onLoadChildren() może wyglądać tak:
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); }
Uwaga: obiekty dostarczane przez MediaBrowserService
nie powinny zawierać bitmap ikon.MediaItem Zamiast tego użyj Uri, dzwoniąc pod numer setIconUri(), gdy tworzysz MediaDescription dla każdego produktu.
Przykład implementacji onLoadChildren() znajdziesz w przykładowej aplikacji Universal
Android Music Player.
Cykl życia usługi przeglądarki multimediów
Działanie usługi na Androidzie zależy od tego, czy jest ona uruchomiona czy powiązana z co najmniej jednym klientem. Po utworzeniu usługi można ją uruchomić, powiązać lub wykonać obie te czynności. W każdym z tych stanów usługa jest w pełni funkcjonalna i może wykonywać zadania, do których została zaprojektowana. Różnica polega na tym, jak długo będzie ona dostępna. Powiązana usługa nie jest niszczona, dopóki wszyscy powiązani z nią klienci nie zostaną od niej odłączeni. Uruchomioną usługę można jawnie zatrzymać i usunąć (zakładając, że nie jest już powiązana z żadnymi klientami).
Gdy MediaBrowser działający w innym działaniu połączy się z MediaBrowserService, powiąże działanie z usługą, co spowoduje, że usługa będzie powiązana (ale nie uruchomiona). To domyślne zachowanie jest wbudowane w klasę MediaBrowserServiceCompat.
Usługa, która jest tylko powiązana (a nie uruchomiona), jest niszczona, gdy wszyscy jej klienci odłączą się od niej. Jeśli w tym momencie aktywność interfejsu użytkownika zostanie przerwana, usługa zostanie zniszczona. Jeśli nie odtwarzasz jeszcze muzyki, nie stanowi to problemu. Gdy jednak odtwarzanie się rozpocznie, użytkownik prawdopodobnie oczekuje, że będzie mógł słuchać dalej nawet po przełączeniu aplikacji. Nie chcesz niszczyć odtwarzacza, gdy odłączasz interfejs, aby pracować z inną aplikacją.
Dlatego musisz mieć pewność, że usługa jest uruchamiana, gdy zaczyna odtwarzać treści, wywołując funkcję startService(). Uruchomioną usługę należy wyraźnie zatrzymać, niezależnie od tego, czy jest powiązana. Dzięki temu odtwarzacz będzie nadal działać, nawet jeśli aktywność interfejsu sterującego zostanie odłączona.
Aby zatrzymać uruchomioną usługę, zadzwoń pod numer Context.stopService() lub stopSelf(). System zatrzymuje i usuwa usługę tak szybko, jak to możliwe. Jeśli jednak co najmniej 1 klient jest nadal powiązany z usługą, wywołanie zatrzymania usługi jest opóźnione do momentu, gdy wszyscy klienci odłączą się od niej.
Cykl życia MediaBrowserService zależy od sposobu jego utworzenia, liczby klientów, z którymi jest powiązany, oraz wywołań, które otrzymuje z wywołań zwrotnych sesji multimedialnej. Podsumowując:
- Usługa jest tworzona, gdy jest uruchamiana w odpowiedzi na naciśnięcie przycisku multimediów lub gdy aktywność się z nią łączy (po połączeniu za pomocą
MediaBrowser). - Wywołanie zwrotne sesji multimedialnej
onPlay()powinno zawierać kod, który wywołuje funkcjęstartService(). Dzięki temu usługa uruchamia się i działa nawet wtedy, gdy wszystkie powiązane z nią działania interfejsuMediaBrowserzostaną odłączone. - Wywołanie zwrotne
onStop()powinno wywoływać funkcjęstopSelf(). Jeśli usługa została uruchomiona, ta czynność ją zatrzyma. Dodatkowo usługa jest niszczona, jeśli nie są z nią powiązane żadne działania. W przeciwnym razie usługa pozostanie powiązana do momentu, gdy wszystkie jej działania zostaną od niej odłączone. (Jeśli przed zniszczeniem urządzenia nadejdzie kolejne połączeniestartService(), oczekujące zatrzymanie zostanie anulowane).
Ten schemat blokowy pokazuje, jak zarządza się cyklem życia usługi. Licznik zmiennych śledzi liczbę powiązanych klientów:

Korzystanie z powiadomień MediaStyle w usłudze działającej na pierwszym planie
Gdy usługa jest odtwarzana, powinna działać na pierwszym planie. Dzięki temu system wie, że usługa wykonuje przydatną funkcję i nie należy jej zamykać, jeśli w systemie jest mało pamięci. Usługa działająca na pierwszym planie musi wyświetlać powiadomienie, aby użytkownik wiedział o jej działaniu i mógł nią opcjonalnie sterować. Wywołanie zwrotne onPlay() powinno umieścić usługę na pierwszym planie. (Zwróć uwagę, że jest to specjalne znaczenie słowa „pierwszy plan”. Android uznaje usługę za działającą na pierwszym planie na potrzeby zarządzania procesami, ale dla użytkownika odtwarzacz działa w tle, podczas gdy na ekranie na „pierwszym planie” jest widoczna inna aplikacja).
Gdy usługa działa na pierwszym planie, musi wyświetlać powiadomienie, najlepiej z co najmniej 1 elementem sterującym odtwarzaniem. Powiadomienie powinno też zawierać przydatne informacje z metadanych sesji.
Utwórz i wyświetl powiadomienie, gdy odtwarzacz zacznie odtwarzać. Najlepiej zrobić to w metodzie MediaSessionCompat.Callback.onPlay().
W przykładzie poniżej użyto elementu NotificationCompat.MediaStyle, który jest przeznaczony dla aplikacji multimedialnych. Pokazuje, jak utworzyć powiadomienie, które wyświetla metadane i elementy sterujące transportem. Metoda wygody
getController()
umożliwia utworzenie kontrolera multimediów bezpośrednio z sesji multimedialnej.
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());
Podczas korzystania z powiadomień MediaStyle pamiętaj o działaniu tych ustawień NotificationCompat:
- Gdy używasz
setContentIntent(), usługa uruchamia się automatycznie po kliknięciu powiadomienia, co jest bardzo przydatne. - W sytuacji „niezaufanej”, takiej jak ekran blokady, domyślna widoczność treści powiadomień to
VISIBILITY_PRIVATE. Prawdopodobnie chcesz widzieć elementy sterujące odtwarzaniem na ekranie blokady, więc wybierzVISIBILITY_PUBLIC. - Zachowaj ostrożność podczas ustawiania koloru tła. W zwykłym powiadomieniu na urządzeniu z Androidem 5.0 lub nowszym kolor jest stosowany tylko do tła małej ikony aplikacji. W przypadku powiadomień w stylu MediaStyle w wersjach Androida starszych niż 7.0 kolor jest używany jako tło całego powiadomienia. Sprawdź kolor tła. Wybieraj kolory, które nie męczą wzroku, i unikaj bardzo jasnych lub fluorescencyjnych barw.
Te ustawienia są dostępne tylko wtedy, gdy używasz NotificationCompat.MediaStyle:
- Użyj
setMediaSession(), aby powiązać powiadomienie z sesją. Umożliwia to aplikacjom innych firm i urządzeniom towarzyszącym dostęp do sesji i sterowanie nią. - Użyj
setShowActionsInCompactView(), aby dodać maksymalnie 3 działania, które będą wyświetlane w widoku treści o standardowym rozmiarze w powiadomieniu. (Tutaj określony jest przycisk wstrzymania). - W Androidzie 5.0 (poziom interfejsu API 21) i nowszym możesz przesunąć powiadomienie, aby zatrzymać odtwarzacz, gdy usługa nie będzie już działać na pierwszym planie. W starszych wersjach nie jest to możliwe. Aby umożliwić użytkownikom usunięcie powiadomienia i zatrzymanie odtwarzania przed Androidem 5.0 (API na poziomie 21), możesz dodać przycisk anulowania w prawym górnym rogu powiadomienia, wywołując funkcje
setShowCancelButton(true)isetCancelButtonIntent().
Gdy dodasz przyciski wstrzymania i anulowania, musisz dołączyć do działania odtwarzania element PendingIntent. Metoda MediaButtonReceiver.buildMediaButtonPendingIntent() przekształca działanie PlaybackState w PendingIntent.
Włącz przeglądanie multimediów w AVRCP
Oprócz aplikacji niestandardowych, takich jak Android Auto, warstwa Bluetooth systemu działa również jako klient MediaBrowserService, aby ułatwić bezprzewodowe zdalne przeglądanie katalogu (AVRCP).
Na Androidzie 16 i 17 platforma wymaga, aby aplikacje, które nie korzystają z Media3, udostępniały konkretną aktywność z filtrem intencji, aby można było zweryfikować możliwość przeglądania.
Dodaj ten konkretny filtr intencji do wyeksportowanego działania w AndroidManifest.xml. Zauważ, że celowo pominięto CATEGORY_DEFAULT, aby zapobiec wyświetlaniu aplikacji w ogólnych menu „Otwórz za pomocą” w przypadku lokalnych plików audio:
<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>