Tworzenie usługi przeglądarki do multimediów

Aplikacja musi zadeklarować MediaBrowserService z intencją filtra w pliku manifestu. Możesz wybrać własną nazwę usługi. w poniższym przykładzie jest to „MediaPlaybackService”.

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

Uwaga: zalecana implementacja MediaBrowserService jest MediaBrowserServiceCompat. , która jest definiowana w bibliotekę pomocy Media-Compat, Na tej stronie hasło „Media BrowserService” odnosi się do instancji z MediaBrowserServiceCompat.

Inicjowanie sesji multimediów

Gdy usługa otrzyma metodę wywołania zwrotnego cyklu życia onCreate(), powinna wykonać te czynności:

Poniższy kod onCreate() ilustruje te czynności:

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 z klientami

MediaBrowserService udostępnia 2 metody obsługi połączeń z klientem: onGetRoot() elementu sterującego dostęp do usługi oraz onLoadChildren() umożliwia klientowi utworzenie i wyświetlenie menu hierarchii treści w MediaBrowserService.

Kontrola połączeń klientów z usługą onGetRoot()

Metoda onGetRoot() zwraca węzeł główny w hierarchii treści. Jeśli zwraca wartość null, połączenie jest odrzucane.

Aby umożliwić klientom połączenie się z Twoją usługą i przeglądanie jej treści multimedialnych, onGetRoot() musi zwracać niepustą wartość BrowserRoot, która jest identyfikatorem głównym, który reprezentuje hierarchię treści.

Aby umożliwić klientom łączenie się z MediaSession bez przeglądania, onGetRoot() nadal musi zwracać niepustą wartość 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 kontrolować, kto może się łączyć na urządzeniu MediaBrowserService. Jednym ze sposobów jest użycie listy kontroli dostępu (ACL) określający, które połączenia są dozwolone, lub wylicza które połączenia powinny być zabronione. Przykład implementacji listy kontroli dostępu (ACL) który zezwala na konkretne połączenia, zobacz PackageValidator. w uniwersalnym odtwarzaczu muzyki na Androida przykładową aplikację.

Możesz podać różne hierarchie treści w zależności od tego, typ klienta, który wysyła zapytanie. Android Auto ogranicza użytkownicy korzystają z aplikacji audio. Więcej informacji znajdziesz w sekcji Odtwarzanie dźwięku dla Automatyczna. Ty może sprawdzić clientPackageName w momencie połączenia, aby określić klienta i zwracają różne wartości BrowserRoot w zależności od klienta (lub rootHints ).

Komunikacja treści z urządzeniem onLoadChildren()

Po nawiązaniu połączenia klient może przechodzić przez hierarchię treści, wielokrotnie wykonując wywołania MediaBrowserCompat.subscribe(), aby utworzyć lokalną reprezentację interfejsu użytkownika. Metoda subscribe() wysyła do usługi wywołanie zwrotne onLoadChildren(), które zwraca listę obiektów MediaBrowser.MediaItem.

Każdy element MediaItem ma unikalny ciąg identyfikatora, który jest nieprzezroczystym tokenem. Gdy klient chce otworzyć menu podrzędne lub odtworzyć element, przekazuje jego identyfikator. Twoja usługa odpowiada za powiązanie identyfikatora z odpowiednim węzłem menu lub elementem treści.

Prosta implementacja atrybutu 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: MediaItem obiekty dostarczone przez usługę Media BrowserService nie powinien zawierać map bitowych ikon. Zamiast tego użyj numeru Uri, dzwoniąc setIconUri() podczas tworzenia MediaDescription dla każdego elementu.

Przykład implementacji onLoadChildren() znajdziesz w przykładowej aplikacji Uniwersalny odtwarzacz muzyki na Androida.

Cykl życia usługi przeglądarki multimediów

Działanie usługi na Androida zależy od tego, czy jest rozpoczęta czy powiązana z co najmniej 1 klientem. Po utworzeniu usługi można ją uruchomić, powiązać lub jedno i drugie. We wszystkich tych stanach urządzenie jest w pełni funkcjonalne i może wykonywać swoją pracę, do czego jest przeznaczone. Różnica polega na tym, jak długo będzie istnieć usługa. Powiązana usługa nie zostanie zniszczona, dopóki wszystkie jej powiązane klienty nie zostaną usunięte. Uruchomiona usługa może zostać bezpośrednio zatrzymana i zniszczona (przy założeniu, że nie jest już powiązana z żadnym klientem).

Gdy obiekt MediaBrowser uruchomiony w innym działaniu łączy się z usługą MediaBrowserService, wiąże to działanie z usługą w ten sposób, że usługa jest powiązana (ale nie uruchomiona). To domyślne działanie jest wbudowane w klasę MediaBrowserServiceCompat.

Usługa, która jest powiązana tylko (i nie została uruchomiona), zostaje zniszczona w momencie usunięcia powiązania wszystkich jej klientów. Jeśli działanie interfejsu użytkownika zostanie na tym etapie odłączone, usługa zostanie zniszczona. Nie stanowi to problemu, jeśli nie udało Ci się jeszcze odtworzyć żadnej muzyki. Jednak po rozpoczęciu odtwarzania użytkownik prawdopodobnie oczekuje, że będzie dalej słuchać muzyki nawet po przełączeniu aplikacji. Gdy usuniesz powiązanie interfejsu użytkownika z inną aplikacją, odtwarzacz nie zostanie zniszczony.

Z tego powodu należy sprawdzić, czy usługa została uruchomiona wraz z jej uruchomieniem. aby odtworzyć, dzwoniąc pod numer startService(). O uruchomiona usługa musi być bezpośrednio zatrzymana, niezależnie od tego, czy jest powiązana. Ten zapewnia, że odtwarzacz będzie działać nawet wtedy, gdy interfejs sterujący aktywności.

Aby zatrzymać uruchomioną usługę, zadzwoń pod numer Context.stopService() lub stopSelf(). System jak najszybciej zatrzymuje i zniszczy usługę. Jeśli jednak co najmniej jeden klient jest nadal powiązany z usługą, wywołanie zatrzymania usługi jest opóźnione do momentu usunięcia powiązania wszystkich klientów.

Cykl życia obiektu MediaBrowserService zależy od sposobu jego utworzenia, liczby powiązanych z nim klientów i odbierania wywołań, które otrzymuje z wywołań zwrotnych sesji multimediów. Podsumowując:

  • Usługa jest tworzona po uruchomieniu w odpowiedzi na przycisk multimediów lub powiązanie z nią działania (po nawiązaniu połączenia za pomocą interfejsu MediaBrowser).
  • Wywołanie zwrotne sesji multimediów onPlay() powinno zawierać kod wywołujący startService(). Dzięki temu usługa zostanie uruchomiona i będzie działać nawet po usunięciu powiązania wszystkich powiązanych z nią działań interfejsu MediaBrowser w interfejsie użytkownika.
  • Wywołanie zwrotne onStop() powinno wywołać metodę stopSelf(). Jeśli usługa została uruchomiona, zostanie zatrzymana. Ponadto usługa jest zniszczona, jeśli nie są z nią powiązane żadne działania. W przeciwnym razie usługa pozostanie powiązana, dopóki wszystkie jej działania nie zostaną usunięte. Jeśli następne wywołanie startService() zostanie odebrane przed zniszczeniem usługi, oczekujące zatrzymanie zostanie anulowane.

Na poniższym schemacie blokowym przedstawiono zarządzanie cyklem życia usługi. Licznik zmiennych śledzi liczbę powiązanych klientów:

Cykl życia usługi

Używanie powiadomień MediaStyle z usługą na pierwszym planie

Gdy usługa jest odtwarzana, powinna być uruchomiona na pierwszym planie. Dzięki temu system wie, że usługa wykonuje przydatną funkcję i nie powinna zostać zatrzymana, jeśli w systemie jest za mało pamięci. Usługa działająca na pierwszym planie musi wyświetlać powiadomienie, aby użytkownik wiedział o niej i opcjonalnie mógł ją kontrolować. Wywołanie zwrotne onPlay() powinno umieścić usługę na pierwszym planie. (Pamiętaj, że jest to szczególne znaczenie wyrażenia „pierwszy plan”. Android określa usługę na pierwszym planie w celu zarządzania procesem, ale dla użytkownika odtwarzacz jest odtwarzany w tle, podczas gdy inna aplikacja jest widoczna na pierwszym planie. na ekranie).

Gdy usługa działa na pierwszym planie, musi wyświetlać powiadomienie, najlepiej z co najmniej 1 elementem sterującym transportem. Powiadomienie powinno też zawierać przydatne informacje z metadanych sesji.

Utwórz i wyświetl powiadomienie, gdy odtwarzacz rozpocznie grę. Najlepiej zrobić to w metodzie MediaSessionCompat.Callback.onPlay().

W przykładzie poniżej użyto parametru NotificationCompat.MediaStyle, który powstał z myślą o aplikacjach do multimediów. Pokazuje on, jak utworzyć powiadomienie z metadanymi i elementami sterującymi transportu. Metoda wygodna getController() umożliwia utworzenie kontrolera multimediów bezpośrednio z sesji multimediów.

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());

Korzystając z powiadomień MediaStyle, pamiętaj o tych Ustawienia NotificationCompat:

  • Jeśli użyjesz aplikacji setContentIntent(), zostanie ona uruchomiona automatycznie po otrzymaniu powiadomienia jest bardzo przydatna.
  • W „niezaufanym” sytuacja np. ekranu blokady, domyślna widoczność treści powiadomień to VISIBILITY_PRIVATE. Prawdopodobnie chcesz zobaczyć elementy sterujące transportem na ekranie blokady, więc warto wybrać VISIBILITY_PUBLIC.
  • Zachowaj ostrożność podczas ustawiania koloru tła. W zwykłym powiadomieniu w Androida w wersji 5.0 lub nowszej, kolor jest stosowany tylko do tła małej ikony aplikacji. Jednak w przypadku powiadomień MediaStyle w wersjach starszych niż 7.0 kolorem jest używane do określania całego tła powiadomienia. Przetestuj kolor tła. Otwórz są delikatne dla oczu i unikaj stosowania bardzo jasnych i fluorescencyjnych kolorów.

Te ustawienia są dostępne tylko w przypadku używania NotificationCompat.MediaStyle:

  • Użyj formatu: setMediaSession() aby powiązać powiadomienie z sesją. Zezwala to aplikacjom innych firm i urządzeniach towarzyszących, aby umożliwić jej dostęp do sesji i kontrolowanie jej.
  • Użyj opcji setShowActionsInCompactView(), aby dodać maksymalnie 3 działania, które będą pokazywane do standardowego rozmiaru powiadomienia. (w tym miejscu przycisk wstrzymania jest określony).
  • W Androidzie 5.0 (poziom interfejsu API 21) i nowszych możesz przesunąć powiadomienie poza ekran, aby zatrzymać , gdy usługa przestanie działać na pierwszym planie. Nie można zrobić we wcześniejszych wersjach. Aby umożliwić użytkownikom usunięcie powiadomienia i zatrzymanie odtwarzania przed Androidem 5.0 (poziom interfejsu API 21) możesz dodać przycisk anulowania w prawym górnym rogu powiadomienia, dzwoniąc pod numer setShowCancelButton(true) i setCancelButtonIntent().

Gdy dodasz przyciski wstrzymania i anulowania, potrzebujesz intencji PendingIntent do załączenia do czynności odtwarzania. Metoda MediaButtonReceiver.buildMediaButtonPendingIntent() wykonuje zadanie konwersji z działaniem OdtwarzanieState w intencji PendingIntent.