Medienbrowser-Dienst erstellen

Deine App muss die MediaBrowserService mit einem Intent-Filter in ihrem Manifest deklarieren. Du kannst deinen eigenen Dienstnamen auswählen. Im folgenden Beispiel lautet das „MediaWiedergabeService“.

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

Hinweis: Die empfohlene Implementierung von MediaBrowserService ist MediaBrowserServiceCompat. die in der media-compat-Supportbibliothek definiert ist. Auf dieser Seite bezieht sich der Begriff „MediaBrowserService“ auf eine Instanz von MediaBrowserServiceCompat.

Mediensitzung initialisieren

Wenn der Dienst die Lebenszyklus-Callback-Methode onCreate() empfängt, sollte er folgende 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 zur Verarbeitung von Clientverbindungen: onGetRoot() steuert den Zugriff auf den Dienst und onLoadChildren() bietet einem Client die Möglichkeit, ein Menü der Inhaltshierarchie von 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 dessen Medieninhalte durchsuchen können, muss onGetRoot() einen BrowserRoot ungleich null zurückgeben. Dies ist eine Stamm-ID, die Ihre Inhaltshierarchie darstellt.

Damit Clients ohne Durchsuchen eine Verbindung zu Ihrer MediaSession herstellen können, 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 festlegen, wer eine Verbindung zu Ihrem MediaBrowserService herstellen darf. Eine Möglichkeit ist die Verwendung einer Zugriffssteuerungsliste (Access Control List, ACL), die angibt, welche Verbindungen zulässig sind, oder alternativ aufzählt, welche Verbindungen unzulässig sind. Ein Beispiel für die Implementierung einer ACL, die bestimmte Verbindungen zulässt, findest du in der Klasse PackageValidator in der Beispiel-App Universal Android Music Player.

Je nachdem, von welcher Art von Client die Abfrage stammt, sollten Sie unterschiedliche Contenthierarchien angeben. Android Auto schränkt insbesondere die Interaktion der Nutzer mit Audio-Apps ein. Weitere Informationen finden Sie unter Audio bei der automatischen Wiedergabe abspielen. Sie können den clientPackageName zum Zeitpunkt der Verbindung prüfen, um den Clienttyp zu bestimmen, und je nach Client (oder rootHints, falls vorhanden) ein anderes BrowserRoot zurückgeben.

Inhalte mit onLoadChildren() kommunizieren

Nachdem der Client eine Verbindung hergestellt hat, kann er die Inhaltshierarchie durchlaufen, indem er MediaBrowserCompat.subscribe() wiederholt aufruft, um eine lokale Darstellung der UI 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, bei dem es sich um ein intransparentes Token handelt. Wenn ein Kunde ein Untermenü öffnen oder ein Element abspielen möchte, wird die ID übergeben. 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 : MediaItem-Objekte, die von MediaBrowserService bereitgestellt werden, sollten keine Symbol-Bitmaps enthalten. Verwenden Sie stattdessen Uri. Rufen Sie dazu setIconUri() auf, wenn Sie die MediaDescription für jedes Element erstellen.

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

Lebenszyklus des Medienbrowserdienstes

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 und/oder gebunden werden. In all diesen Bundesstaaten ist er voll funktionsfähig und kann die vorgesehene Arbeit ausführen. Der Unterschied ist, wie lange der Dienst bestehen bleibt. Ein gebundener Dienst wird erst gelöscht, wenn alle gebundenen Clients die Bindung aufgehoben haben. Ein gestarteter Dienst kann explizit beendet und gelöscht werden (vorausgesetzt, er ist nicht mehr an Clients gebunden).

Wenn eine MediaBrowser, die in einer anderen Aktivität ausgeführt wird, eine Verbindung zu einer MediaBrowserService herstellt, wird die Aktivität an den Dienst gebunden, wodurch der Dienst gebunden, aber nicht gestartet wird. Dieses Standardverhalten ist in die MediaBrowserServiceCompat-Klasse integriert.

Ein Dienst, der nur gebunden (und nicht gestartet) ist, wird gelöscht, wenn die Bindung aller Clients aufgehoben wird. Wenn Ihre UI-Aktivität zu diesem Zeitpunkt getrennt wird, wird der Dienst gelöscht. Wenn du noch keine Musik abgespielt hast, stellt dies kein Problem dar. Zu Beginn der Wiedergabe erwartet der Nutzer jedoch wahrscheinlich, dass er die Wiedergabe auch nach einem App-Wechsel fortsetzen wird. Sie möchten den Spieler nicht zerstören, wenn Sie die Verknüpfung der Benutzeroberfläche aufheben, um mit einer anderen App zu arbeiten.

Aus diesem Grund müssen Sie startService() aufrufen, damit der Dienst gestartet wird, wenn die Wiedergabe beginnt. Ein gestarteter Dienst muss explizit beendet werden, unabhängig davon, ob er gebunden ist oder nicht. Dadurch wird sichergestellt, dass Ihr Player auch dann weiter ausgeführt wird, wenn die steuernde UI-Aktivität die Verknüpfung aufhebt.

Rufen Sie zum Beenden eines gestarteten Dienstes Context.stopService() oder stopSelf() auf. Das System stoppt und löscht den Dienst so schnell wie möglich. Wenn jedoch ein oder mehrere Clients immer noch an den Dienst gebunden sind, wird der Aufruf zum Beenden des Dienstes verzögert, bis alle seine Clients die Bindung aufgehoben haben.

Der Lebenszyklus des MediaBrowserService wird durch die Art der Erstellung, die Anzahl der an ihn gebundenen Clients und die Aufrufe gesteuert, die es von Mediensitzungs-Callbacks erhält. Zusammenfassung:

  • Der Dienst wird erstellt, wenn er als Reaktion auf eine Medienschaltfläche oder wenn eine Aktivität an ihn gebunden wird (nachdem eine Verbindung über seine MediaBrowser hergestellt wurde).
  • Der onPlay()-Callback der Mediensitzung sollte Code enthalten, der startService() aufruft. Dadurch wird sichergestellt, dass der Dienst auch dann gestartet und weiter ausgeführt wird, wenn die Bindung aller MediaBrowser-UI-Aktivitäten, die an ihn gebunden sind, aufgehoben werden.
  • Der onStop()-Callback sollte stopSelf() aufrufen. Wenn der Dienst gestartet wurde, wird er dadurch beendet. Außerdem wird der Dienst gelöscht, wenn keine Aktivitäten damit verbunden sind. Andernfalls bleibt der Dienst gebunden, bis die Bindung aller Aktivitäten aufgehoben wird. (Wenn ein nachfolgender startService()-Aufruf empfangen wird, bevor der Dienst gelöscht wird, wird die ausstehende Haltestelle abgebrochen.)

Das folgende Flussdiagramm zeigt, wie der Lebenszyklus eines Dienstes verwaltet wird. Der Variablenzähler verfolgt die Anzahl der gebundenen Clients:

Dienstlebenszyklus

MediaStyle-Benachrichtigungen mit einem Dienst im Vordergrund verwenden

Wenn ein Dienst wiedergegeben wird, sollte er im Vordergrund ausgeführt werden. Dadurch wird dem System mitgeteilt, dass der Dienst eine nützliche Funktion ausführt, und sollte nicht beendet werden, wenn dem System nur noch wenig Arbeitsspeicher zur Verfügung steht. Bei einem Dienst im Vordergrund muss eine Benachrichtigung angezeigt werden, damit der Nutzer darüber informiert wird und die Benachrichtigung bei Bedarf steuern kann. Der onPlay()-Callback sollte den Dienst in den Vordergrund stellen. (Beachten Sie, dass dies eine besondere Bedeutung von „Vordergrund“ ist. Während Android den Dienst im Vordergrund für die Prozessverwaltung betrachtet, wird der Player für den Nutzer im Hintergrund wiedergegeben, während eine andere App im Vordergrund auf dem Bildschirm zu sehen ist.

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

Erstellen Sie die Benachrichtigung und zeigen Sie sie an, wenn der Spieler mit dem Spielen beginnt. Dies erfolgt am besten in der Methode MediaSessionCompat.Callback.onPlay().

Im folgenden Beispiel wird NotificationCompat.MediaStyle verwendet, das für Medien-Apps entwickelt wurde. Darin erfahren Sie, wie Sie eine Benachrichtigung mit Metadaten- und Transportsteuerungen erstellen. Mit der praktischen Methode getController() können Sie einen Mediencontroller direkt aus Ihrer 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());

Wenn Sie MediaStyle-Benachrichtigungen verwenden, sollten Sie das Verhalten der folgenden NotificationCompat-Einstellungen beachten:

  • Wenn Sie setContentIntent() verwenden, wird der Dienst automatisch gestartet, wenn auf die Benachrichtigung geklickt wird. Dies ist eine praktische Funktion.
  • In einer „nicht vertrauenswürdigen“ Situation wie dem Sperrbildschirm ist die Standardsichtbarkeit für Benachrichtigungsinhalte VISIBILITY_PRIVATE. Sie möchten wahrscheinlich die Steuerelemente für Verkehrsmittel auf dem Sperrbildschirm sehen, dann ist VISIBILITY_PUBLIC die richtige Wahl.
  • Seien Sie beim Festlegen der Hintergrundfarbe vorsichtig. In einer gewöhnlichen Benachrichtigung ab Android-Version 5.0 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 Gehen Sie vorsichtig zu den Augen 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 mit Ihrer Sitzung zu verknüpfen. Dadurch können Drittanbieter-Apps und Companion-Geräte auf die Sitzung zugreifen und sie steuern.
  • Mit setShowActionsInCompactView() können Sie bis zu 3 Aktionen hinzufügen, die in der contentView in Standardgröße der Benachrichtigung angezeigt werden sollen. (Hier ist die Pause-Schaltfläche angegeben.)
  • Unter Android 5.0 (API-Level 21) und höher kannst du eine Benachrichtigung wegwischen, um den Player zu beenden, sobald der Dienst nicht mehr im Vordergrund ausgeführt wird. Dies ist in früheren Versionen nicht möglich. Damit Nutzer die Benachrichtigung entfernen und die Wiedergabe vor Android 5.0 (API-Level 21) beenden können, kannst du oben rechts in der Benachrichtigung eine Schaltfläche zum Abbrechen hinzufügen. Rufe dazu setShowCancelButton(true) und setCancelButtonIntent() auf.

Wenn Sie die Schaltflächen zum Pausieren und Abbrechen hinzufügen, benötigen Sie einen PendingIntent, der an die Wiedergabeaktion angehängt werden soll. Die Methode MediaButtonReceiver.buildMediaButtonPendingIntent() wandelt eine WiedergabeState-Aktion in einen PendingIntent um.