Tworzenie usługi przeglądarki do multimediów

Aplikacja musi zadeklarować uprawnienie MediaBrowserService za pomocą elementuIntent-filter w pliku manifestu. Możesz wybrać własną nazwę usługi. W tym 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 to MediaBrowserServiceCompat. zdefiniowanej w bibliotece pomocy media-compat. Na tej stronie termin „MediaBrowserService” odnosi się do wystąpienia usługi 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() obrazuje 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 z klientami

MediaBrowserService ma 2 metody obsługi połączeń z klientami: onGetRoot() kontroluje dostęp do usługi oraz onLoadChildren() umożliwia klientowi utworzenie i wyświetlenie menu hierarchii treści MediaBrowserService.

Kontroluję połączenia klienta z usługą 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 Twoją usługą i przeglądanie jej zawartości multimedialnej, funkcja onGetRoot() musi zwracać niepustą wartość BrowserRoot, czyli identyfikator główny reprezentujący hierarchię treści.

Aby umożliwić klientom łączenie się z Twoją sekcją MediaSession bez konieczności przeglądania, funkcja onGetRoot() nadal musi zwracać niezerową 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że być potrzebna kontrola, kto może łączyć się z Twoim urządzeniem MediaBrowserService. Jednym ze sposobów jest użycie listy kontroli dostępu (ACL), która określa, które połączenia są dozwolone, lub określa, które połączenia powinny być zabronione. Przykład implementacji listy kontroli dostępu (ACL) zezwalającej na określone połączenia znajdziesz w opisie klasy PackageValidator w przykładowej aplikacji Universal Android Music Player.

Warto rozważyć podanie różnych hierarchii treści w zależności od typu klienta tworzonego przez zapytanie. W szczególności Android Auto ogranicza sposób, w jaki użytkownicy korzystają z aplikacji audio. Więcej informacji znajdziesz w sekcji Automatyczne odtwarzanie dźwięku. Możesz sprawdzać clientPackageName podczas połączenia, aby określić typ klienta i zwracać różne wartości BrowserRoot w zależności od klienta (lub rootHints, jeśli występuje).

Przesyłanie treści do: onLoadChildren()

Po nawiązaniu połączenia klient może przeszukać hierarchię treści, wielokrotnie wywołując funkcję MediaBrowserCompat.subscribe() w celu utworzenia lokalnej reprezentacji interfejsu. Metoda subscribe() wysyła do usługi wywołanie zwrotne onLoadChildren(), które zwraca listę obiektów MediaBrowser.MediaItem.

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

Prosta implementacja 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 MediaItem dostarczane przez MediaBrowserService nie powinny zawierać map bitowych ikon. Użyj zamiast niego interfejsu Uri, wywołując setIconUri() podczas tworzenia MediaDescription dla każdego elementu.

Przykład implementacji onLoadChildren() znajdziesz w przykładowej aplikacji Universal Android Music Player.

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

Sposób działania usługi na Androida zależy od tego, czy jest rozpoczęta, czy powiązana z jednym lub kilkoma klientami. Po utworzeniu usługa może być uruchamiana, powiązana lub może być używana jednocześnie. We wszystkich tych stanach 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 usługa będzie istnieć. Powiązana usługa nie jest zniszczona, dopóki wszystkie powiązane z nią klienty nie zostaną usunięte. Uruchomioną usługę można bezpośrednio zatrzymać i zniszczyć (przy założeniu, że nie jest już powiązana z żadnymi klientami).

Gdy MediaBrowser uruchomiony w innej aktywności łączy się z MediaBrowserService, wiąże aktywność z usługą, włączając usługę (ale nie uruchomioną). To działanie domyślne jest wbudowane w klasę MediaBrowserServiceCompat.

Usługa, która jest tylko powiązana (i nie została uruchomiona), jest niszczona, gdy wszystkie jej klienty rozłączą się. Jeśli w tym momencie aktywność interfejsu użytkownika zostanie rozłączona, usługa zostanie zniszczona. To nie jest problem, jeśli nie masz jeszcze odtworzonej muzyki. Jednak po rozpoczęciu odtwarzania użytkownik prawdopodobnie oczekuje, że będzie dalej słuchać nawet po zmianie aplikacji. Nie chcesz zniszczyć odtwarzacza po usunięciu powiązania interfejsu, aby działał z inną aplikacją.

Dlatego musisz mieć pewność, że usługa uruchomi się wraz z odtwarzaniem, wywołując startService(). Uruchomiona usługa musi zostać wyraźnie zatrzymana niezależnie od tego, czy jest powiązana. Gwarantuje to, że odtwarzacz będzie działał nawet wtedy, gdy powiązanie elementu sterującego interfejsu zostanie usunięte.

Aby zatrzymać uruchomioną usługę, zadzwoń pod numer Context.stopService() lub stopSelf(). System zatrzymuje się i niszczy usługę tak szybko, jak to możliwe. Jeśli jednak z usługą jest nadal powiązany co najmniej jeden klient, wywołanie w celu 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 wywołań odbieranych z wywołań zwrotnych sesji multimediów. Podsumujmy:

  • Usługa jest tworzona w momencie uruchomienia w odpowiedzi na przycisk multimediów lub po powiązaniu działania z nią (po połączeniu przez MediaBrowser).
  • Wywołanie zwrotne sesji multimedialnej onPlay() powinno zawierać kod wywołujący metodę startService(). Zapewnia to włączenie i dalsze działanie usługi, nawet po usunięciu powiązania wszystkich powiązanych z nią działań MediaBrowser interfejsu.
  • Wywołanie zwrotne onStop() powinno mieć wywołanie stopSelf(). Jeśli usługa została uruchomiona, zostanie zatrzymana. Ponadto usługa jest niszczona, 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 kolejne wywołanie startService() zostanie odebrane przed zniszczeniem usługi, oczekujące zatrzymanie zostanie anulowane).

Poniższy schemat blokowy przedstawia sposób zarządzania cyklem życia usługi. Licznik zmiennych śledzi liczbę powiązanych klientów:

Cykl życia usługi

Używanie powiadomień MediaStyle w przypadku usługi 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ć zamknięta, jeśli w systemie jest mało pamięci. Usługa na pierwszym planie musi wyświetlać powiadomienie, aby użytkownik wiedział o nim i może nim opcjonalnie sterować. Wywołanie zwrotne onPlay() powinno umieścić usługę na pierwszym planie. (Pamiętaj, że jest to szczególne znaczenie „pierwszego planu”. Android traktuje usługę na pierwszym planie w celach zarządzania procesami, ale dla użytkownika odtwarzacz działa 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 ustawieniami transportu. Powiadomienie powinno też zawierać przydatne informacje pochodzące z metadanych sesji.

Utwórz i wyświetlaj powiadomienie, gdy odtwarzacz zacznie grać. Najlepiej to zrobić w metodzie MediaSessionCompat.Callback.onPlay().

W poniższym przykładzie użyto komponentu NotificationCompat.MediaStyle, który jest przeznaczony dla aplikacji do multimediów. Pokazuje on, jak utworzyć powiadomienie zawierające metadane i ustawienia przesyłania. Ta wygodna metoda 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 działaniu tych ustawień powiadomieńKomunikacji:

  • Jeśli używasz setContentIntent(), usługa uruchamia się automatycznie po kliknięciu powiadomienia. Jest to przydatna funkcja.
  • W sytuacjach „niezaufanych”, takich jak ekran blokady, domyślna widoczność treści powiadomień to VISIBILITY_PRIVATE. Zapewne chcesz, aby na ekranie blokady były widoczne elementy sterujące transportem, dlatego zalecamy korzystanie z VISIBILITY_PUBLIC.
  • Zachowaj ostrożność podczas ustawiania koloru tła. W zwykłym powiadomieniu na Androidzie w wersji 5.0 lub nowszej kolor jest stosowany tylko do tła małej ikony aplikacji. Jednak w przypadku powiadomień MediaStyle starszych niż Android 7.0 kolor jest używany dla całego tła powiadomienia. Przetestuj kolor tła. Uważaj na oczy i unikaj bardzo jasnych oraz fluorescencyjnych kolorów.

Te ustawienia są dostępne tylko wtedy, gdy używasz metody NotificationCompat.MediaStyle:

  • Użyj setMediaSession(), aby powiązać powiadomienie ze swoją sesją. Dzięki temu aplikacje innych firm i urządzenia towarzyszące mają dostęp do sesji i mogą ją kontrolować.
  • Użyj setShowActionsInCompactView(), aby dodać maksymalnie 3 działania, które będą wyświetlane w elemencie contentView powiadomienia w standardowym rozmiarze. (tu jest określony przycisk wstrzymywania).
  • W Androidzie 5.0 (poziom interfejsu API 21) i nowszych możesz przesunąć powiadomienie, aby zatrzymać odtwarzacz, gdy usługa przestanie działać na pierwszym planie. Nie można tego 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, wywołując setShowCancelButton(true) i setCancelButtonIntent().

Gdy dodasz przyciski wstrzymywania i anulowania, musisz dodać intencję PendingIntent, która będzie dołączana do działania związanego z odtwarzaniem. Metoda MediaButtonReceiver.buildMediaButtonPendingIntent() przekształca działanie PlaybackState w PendingIntent.