Tworzenie usługi przeglądarki do multimediów

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:

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 interfejsu MediaBrowser zostaną 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łączenie startService(), 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:

Cykl życia usługi

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 wybierz VISIBILITY_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)setCancelButtonIntent().

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>