Medienbrowser-Dienst erstellen

In der Manifestdatei Ihrer App muss das MediaBrowserService mit einem Intent-Filter deklariert werden. Sie können einen eigenen Dienstnamen auswählen. Im folgenden Beispiel ist der ausgewählte Dienstname MediaPlaybackService.

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

Media Session initialisieren

Wenn der Dienst die onCreate()-Lifecycle-Callback-Methode empfängt, sollte er die folgenden Schritte ausführen:

Der folgende onCreate()-Code veranschaulicht diese Schritte:

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

Clientverbindungen verwalten

Ein MediaBrowserService hat zwei Methoden, die Clientverbindungen verarbeiten: onGetRoot() steuert den Zugriff auf den Dienst und onLoadChildren() ermöglicht es einem Client, ein Menü der Inhaltshierarchie des MediaBrowserService zu erstellen und anzuzeigen.

Clientverbindungen mit onGetRoot() steuern

Die Methode onGetRoot() gibt den Stammknoten der Inhaltshierarchie zurück. Wenn die Methode „null“ zurückgibt, wird die Verbindung abgelehnt.

Damit Clients eine Verbindung zu Ihrem Dienst herstellen und seine Medieninhalte durchsuchen können, muss onGetRoot() einen BrowserRoot zurückgeben, der nicht null ist. Dies ist eine Stamm-ID, die Ihre Inhaltshierarchie darstellt.

Damit Clients eine Verbindung zu deiner MediaSession herstellen können, ohne zu browsen, muss onGetRoot() weiterhin einen BrowserRoot zurückgeben, der nicht null ist. Die Stamm-ID sollte jedoch eine leere Inhaltshierarchie darstellen.

Eine typische Implementierung von onGetRoot() könnte so aussehen:

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 einigen Fällen möchten Sie möglicherweise steuern, wer sich mit Ihrem MediaBrowserService verbinden kann. Eine Möglichkeit besteht darin, eine Zugriffssteuerungsliste (Access Control List, ACL) zu verwenden, in der angegeben wird, welche Verbindungen zulässig sind oder welche Verbindungen verboten werden sollen. Ein Beispiel für die Implementierung einer ACL, die bestimmte Verbindungen zulässt, finden Sie in der Klasse PackageValidator in der Beispiel-App Universal Android Music Player.

Je nachdem, welcher Client die Anfrage stellt, sollten Sie unterschiedliche Inhaltshierarchien bereitstellen. Insbesondere schränkt Android Auto ein, wie Nutzer mit Audio-Apps interagieren. Weitere Informationen finden Sie unter Audio für Auto abspielen. Sie können den clientPackageName zum Zeitpunkt der Verbindung prüfen, um den Clienttyp zu ermitteln, und je nach Client einen anderen BrowserRoot zurückgeben (oder rootHints, falls vorhanden).

Inhalte mit onLoadChildren() teilen

Nachdem der Client eine Verbindung hergestellt hat, kann er die Inhaltshierarchie durchlaufen, indem er wiederholt MediaBrowserCompat.subscribe() aufruft, um eine lokale Darstellung der Benutzeroberfläche zu erstellen. Die Methode subscribe() sendet den Callback onLoadChildren() an den Dienst, der eine Liste von MediaBrowser.MediaItem-Objekten zurückgibt.

Jedes MediaItem hat einen eindeutigen ID-String, der ein intransparentes Token ist. Wenn ein Client ein Untermenü öffnen oder ein Element abspielen möchte, übergibt er die ID. Ihr Dienst ist dafür verantwortlich, die ID dem entsprechenden Menüknoten oder Inhaltselement zuzuordnen.

Eine einfache Implementierung von onLoadChildren() könnte so aussehen:

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

Hinweis : Von MediaBrowserService bereitgestellte MediaItem-Objekte sollten keine Symbol-Bitmaps enthalten. Verwenden Sie stattdessen ein Uri, indem Sie setIconUri() aufrufen, wenn Sie das MediaDescription für jedes Element erstellen.

Ein Beispiel für die Implementierung von onLoadChildren() finden Sie in der Beispiel-App Universal Android Music Player.

Lebenszyklus des Medienbrowser-Dienstes

Das Verhalten eines Android-Dienstes hängt davon ab, ob er gestartet oder an einen oder mehrere Clients gebunden ist. Nachdem ein Dienst erstellt wurde, kann er gestartet, gebunden oder beides werden. In all diesen Status ist der Dienst voll funktionsfähig und kann die Aufgaben ausführen, für die er entwickelt wurde. Der Unterschied besteht darin, wie lange der Dienst verfügbar sein wird. Ein gebundener Dienst wird erst zerstört, wenn alle gebundenen Clients die Bindung aufheben. Ein gestarteter Dienst kann explizit beendet und zerstört werden, sofern er nicht mehr an Clients gebunden ist.

Wenn ein MediaBrowser, das in einer anderen Aktivität ausgeführt wird, eine Verbindung zu einem MediaBrowserService herstellt, wird die Aktivität an den Dienst gebunden. Der Dienst ist dann gebunden (aber nicht gestartet). Dieses Standardverhalten ist in die Klasse MediaBrowserServiceCompat integriert.

Ein Dienst, der nur gebunden (und nicht gestartet) ist, wird zerstört, wenn alle Clients die Bindung aufheben. Wenn die UI-Aktivität an diesem Punkt getrennt wird, wird der Dienst beendet. Das ist kein Problem, wenn Sie noch keine Musik abgespielt haben. Wenn die Wiedergabe jedoch gestartet wird, erwartet der Nutzer wahrscheinlich, dass er auch nach dem Wechseln der App weiter zuhören kann. Sie möchten den Player nicht zerstören, wenn Sie die Benutzeroberfläche entbinden, um mit einer anderen App zu arbeiten.

Aus diesem Grund müssen Sie dafür sorgen, dass der Dienst gestartet wird, wenn die Wiedergabe durch Aufrufen von startService() beginnt. Ein gestarteter Dienst muss explizit beendet werden, unabhängig davon, ob er gebunden ist oder nicht. So wird sichergestellt, dass der Player weiterhin funktioniert, auch wenn die Aktivität der steuernden Benutzeroberfläche entbunden wird.

Rufen Sie Context.stopService() oder stopSelf() auf, um einen gestarteten Dienst zu beenden. Das System beendet und löscht den Dienst so schnell wie möglich. Wenn jedoch ein oder mehrere Clients noch an den Dienst gebunden sind, wird der Aufruf zum Beenden des Dienstes verzögert, bis alle Clients die Bindung aufheben.

Der Lebenszyklus von MediaBrowserService wird durch die Art und Weise gesteuert, wie er erstellt wird, die Anzahl der Clients, die daran gebunden sind, und die Aufrufe, die er von Media-Sitzungs-Callbacks empfängt. Zusammenfassung:

  • Der Dienst wird erstellt, wenn er als Reaktion auf eine Media-Schaltfläche gestartet wird oder wenn eine Aktivität daran gebunden wird (nachdem die Verbindung über das MediaBrowser hergestellt wurde).
  • Der Media-Sitzungs-Callback onPlay() sollte Code enthalten, der startService() aufruft. So wird sichergestellt, dass der Dienst gestartet wird und weiter ausgeführt wird, auch wenn alle daran gebundenen MediaBrowser-Aktivitäten der Benutzeroberfläche entbunden werden.
  • Im onStop()-Callback sollte stopSelf() aufgerufen werden. Wenn der Dienst gestartet wurde, wird er dadurch beendet. Außerdem wird der Dienst beendet, wenn keine Aktivitäten daran gebunden sind. Andernfalls bleibt der Dienst gebunden, bis alle seine Aktivitäten entbunden werden. Wenn ein nachfolgender startService()-Aufruf empfangen wird, bevor der Dienst beendet wird, wird der ausstehende Stopp abgebrochen.

Das folgende Flussdiagramm zeigt, wie der Lebenszyklus eines Dienstes verwaltet wird. Der Variablencounter erfasst die Anzahl der verknüpften Clients:

Dienstlebenszyklus

MediaStyle-Benachrichtigungen mit einem Dienst im Vordergrund verwenden

Wenn ein Dienst Medien wiedergibt, sollte er im Vordergrund ausgeführt werden. So weiß das System, dass der Dienst eine nützliche Funktion ausführt und nicht beendet werden sollte, wenn der Arbeitsspeicher des Systems knapp wird. Ein Dienst im Vordergrund muss eine Benachrichtigung anzeigen, damit der Nutzer darüber informiert ist und ihn optional steuern kann. Der onPlay()-Callback sollte den Dienst in den Vordergrund stellen. Beachten Sie, dass dies eine spezielle Bedeutung von „Vordergrund“ ist. Während Android den Dienst für die Prozessverwaltung als Dienst im Vordergrund betrachtet, wird der Player für den Nutzer im Hintergrund wiedergegeben, während eine andere App auf dem Bildschirm im „Vordergrund“ sichtbar ist.)

Wenn ein Dienst im Vordergrund ausgeführt wird, muss er eine Benachrichtigung anzeigen, idealerweise mit einer oder mehreren Transportsteuerungen. Die Benachrichtigung sollte auch nützliche Informationen aus den Metadaten der Sitzung enthalten.

Erstellen und zeigen Sie die Benachrichtigung an, wenn die Wiedergabe beginnt. Am besten tun Sie das in der Methode MediaSessionCompat.Callback.onPlay().

Im folgenden Beispiel wird NotificationCompat.MediaStyle verwendet, das für Media-Apps entwickelt wurde. Es wird gezeigt, wie eine Benachrichtigung erstellt wird, in der Metadaten und Transportsteuerungen angezeigt werden. Mit der Convenience-Methode getController() können Sie eine Mediensteuerung direkt über Ihre Mediensitzung erstellen.

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

Beachten Sie bei der Verwendung von MediaStyle-Benachrichtigungen das Verhalten der folgenden NotificationCompat-Einstellungen:

  • Wenn Sie setContentIntent() verwenden, wird Ihr Dienst automatisch gestartet, wenn auf die Benachrichtigung geklickt wird.
  • In einer „nicht vertrauenswürdigen“ Situation wie dem Sperrbildschirm ist die Standardsichtbarkeit für Benachrichtigungsinhalte VISIBILITY_PRIVATE. Wahrscheinlich möchtest du die Transportsteuerung auf dem Sperrbildschirm sehen. In diesem Fall ist VISIBILITY_PUBLIC die richtige Option.
  • Seien Sie vorsichtig, wenn Sie die Hintergrundfarbe festlegen. Bei einer normalen Benachrichtigung in Android 5.0 oder höher wird die Farbe nur auf den Hintergrund des kleinen App-Symbols angewendet. Bei MediaStyle-Benachrichtigungen vor Android 7.0 wird die Farbe jedoch für den gesamten Benachrichtigungshintergrund verwendet. Hintergrundfarbe testen Achten Sie auf eine angenehme Darstellung und vermeiden Sie extrem helle oder fluoreszierende Farben.

Diese Einstellungen sind nur verfügbar, wenn Sie NotificationCompat.MediaStyle verwenden:

  • Verwenden Sie setMediaSession(), um die Benachrichtigung Ihrer Sitzung zuzuordnen. Dadurch können Drittanbieter-Apps und Companion-Geräte auf die Sitzung zugreifen und sie steuern.
  • Mit setShowActionsInCompactView() können Sie bis zu drei Aktionen hinzufügen, die in der contentView-Ansicht der Benachrichtigung in Standardgröße angezeigt werden. (Hier wird der Pause-Button angegeben.)
  • Unter Android 5.0 (API-Level 21) und höher können Sie eine Benachrichtigung wegwischen, um die Wiedergabe zu beenden, sobald der Dienst nicht mehr im Vordergrund ausgeführt wird. In früheren Versionen ist das nicht möglich. Damit Nutzer die Benachrichtigung entfernen und die Wiedergabe vor Android 5.0 (API-Ebene 21) beenden können, können Sie eine Schaltfläche zum Abbrechen in der oberen rechten Ecke der Benachrichtigung hinzufügen, indem Sie setShowCancelButton(true) und setCancelButtonIntent() aufrufen.

Wenn Sie die Schaltflächen zum Pausieren und Abbrechen hinzufügen, benötigen Sie ein PendingIntent, das Sie an die Wiedergabeaktion anhängen können. Die Methode MediaButtonReceiver.buildMediaButtonPendingIntent() wandelt eine PlaybackState-Aktion in einen PendingIntent um.

AVRCP-Mediendurchsuchen aktivieren

Zusätzlich zu benutzerdefinierten Apps wie Android Auto fungiert die Bluetooth-Ebene des Systems auch als Client für Ihr MediaBrowserService, um das drahtlose Durchsuchen des Katalogs per Fernbedienung (AVRCP) zu ermöglichen.

Unter Android 16 und Android 17 ist es erforderlich, dass Apps, die Media3 nicht verwenden, eine bestimmte Aktivität mit einem Intent-Filter zur Verfügung stellen, die für das Browsen validiert werden muss.

Fügen Sie diesen spezifischen Intent-Filter einer exportierten Aktivität in Ihrem AndroidManifest.xml hinzu. Beachten Sie, dass CATEGORY_DEFAULT absichtlich weggelassen wird, damit Ihre App nicht in allgemeinen „Öffnen mit“-Menüs für lokale Audiodateien angezeigt wird:

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