Créer un service de navigateur multimédia

Votre application doit déclarer l'MediaBrowserService avec un filtre d'intent dans son fichier manifeste. Vous pouvez choisir votre propre nom de service, dans l'exemple suivant, il s'agit de "MediaPlaybackService".

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

Remarque : L'implémentation recommandée de MediaBrowserService est MediaBrowserServiceCompat. qui est défini dans bibliothèque Support Media-compat. Sur cette page, le terme "MediaBrowserService" fait référence à une instance sur MediaBrowserServiceCompat.

Initialiser la session multimédia

Lorsque le service reçoit la méthode de rappel de cycle de vie onCreate(), il doit effectuer les étapes suivantes:

Le code onCreate() ci-dessous illustre ces étapes:

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

Gérer les connexions client

Un MediaBrowserService comporte deux méthodes pour gérer les connexions client: Commandes onGetRoot() l'accès au service ; onLoadChildren() permet à un client de créer et d'afficher un menu de la hiérarchie de contenu de MediaBrowserService.

Contrôler les connexions client avec onGetRoot()

La méthode onGetRoot() renvoie le nœud racine de la hiérarchie de contenu. Si le renvoie une valeur nulle, la connexion est refusée.

Pour permettre aux clients de se connecter à votre service et de parcourir son contenu multimédia, onGetRoot() doit renvoyer un BrowserRoot non nul, c'est-à-dire un identifiant racine qui représente votre hiérarchie de contenu.

Pour permettre aux clients de se connecter à votre MediaSession sans naviguer, onGetRoot() doit quand même renvoyer une valeur BrowserRoot non nulle, mais l'ID racine doit représenter une une hiérarchie de contenu vide.

Une implémentation type de onGetRoot() peut se présenter comme suit:

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

Dans certains cas, vous voudrez peut-être contrôler qui peut se connecter sur votre MediaBrowserService. Une méthode consiste à utiliser une liste de contrôle d'accès (LCA) spécifiant les connexions autorisées, ou énumérant les connexions qui doivent être interdites. Pour un exemple de la façon d'implémenter une LCA qui autorise des connexions spécifiques, consultez PackageValidator de la classe Universal Android Music Player (Lecteur de musique universel Android) application exemple.

Vous devriez envisager de proposer différentes hiérarchies de contenu en fonction le type de client à l’origine de la requête. En particulier, Android Auto limite les utilisateurs interagissent avec les applications audio. Pour en savoir plus, consultez la section Lecture audio pour Automatique. Toi peut examiner clientPackageName au moment de la connexion pour déterminer le client et renvoient un BrowserRoot différent en fonction du client (ou rootHints le cas échéant).

Communication de contenu avec onLoadChildren()

Une fois le client connecté, il peut balayer la hiérarchie de contenu en effectuant des appels répétés à MediaBrowserCompat.subscribe() pour créer une représentation locale de l'UI. La méthode subscribe() envoie le rappel onLoadChildren() au service, qui renvoie une liste d'objets MediaBrowser.MediaItem.

Chaque MediaItem possède une chaîne d'identifiant unique, qui est un jeton opaque. Lorsqu'un client souhaite ouvrir un sous-menu ou lire un élément, il transmet l'ID. Votre service est chargé d'associer l'ID au nœud de menu ou à l'élément de contenu approprié.

Une implémentation simple de onLoadChildren() peut se présenter comme suit:

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

Remarque : Les objets MediaItem diffusés par MediaBrowserService ne doivent pas contenir de bitmaps d'icône. Utilisez plutôt un Uri en appelant setIconUri() lorsque vous créez le MediaDescription pour chaque élément.

Pour découvrir comment implémenter onLoadChildren(), consultez l'application exemple Universal Android Music Player.

Cycle de vie du service de navigateur multimédia

Le comportement d'un service Android varie selon qu'il est démarré ou lié à un ou plusieurs clients. Une fois le service créé, il peut être démarré, lié ou les deux. Dans tous ces états, il est entièrement fonctionnel et peut effectuer le travail pour lequel il a été conçu. La différence réside dans la durée d'existence du service. Un service lié n'est pas détruit tant que tous ses clients liés ne sont pas dissociés. Un service démarré peut être explicitement arrêté et détruit (en supposant qu'il n'est plus lié à des clients).

Lorsqu'un MediaBrowser exécuté dans une autre activité se connecte à un MediaBrowserService, il lie l'activité au service, ce qui le lie au service (mais n'est pas démarré). Ce comportement par défaut est intégré à la classe MediaBrowserServiceCompat.

Un service qui est seulement lié (et non démarré) est détruit lorsque tous ses clients sont dissociés. Si l'activité de l'interface utilisateur se déconnecte à ce stade, le service est détruit. Ce n'est pas un problème si vous n'avez pas encore écouté de musique. Cependant, lorsque la lecture commence, l'utilisateur s'attend probablement à continuer d'écouter même après avoir changé d'application. Vous ne devez pas détruire le lecteur lorsque vous dissociez l'interface utilisateur pour qu'elle fonctionne avec une autre application.

Pour cette raison, vous devez vous assurer que le service est démarré lorsqu'il démarre en appelant startService(). A démarré doit être explicitement arrêté, qu'il soit lié ou non. Ce garantit que votre joueur continue de fonctionner même si l'UI de contrôle d'activité est dissociée.

Pour arrêter un service démarré, appelez Context.stopService() ou stopSelf(). Le système arrête et détruit le service dès que possible. Toutefois, si un ou plusieurs clients sont toujours liés au service, l'appel visant à l'arrêter est retardé jusqu'à ce que tous ses clients soient dissociés.

Le cycle de vie de l'MediaBrowserService est contrôlé par la façon dont il est créé, par le nombre de clients qui y sont liés et par les appels qu'il reçoit à partir des rappels de session multimédia. En résumé :

  • Le service est créé lorsqu'il est démarré en réponse à un bouton multimédia ou lorsqu'une activité s'y associe (après connexion via son MediaBrowser).
  • Le rappel onPlay() de la session multimédia doit inclure du code qui appelle startService(). Cela garantit que le service démarre et continue de s'exécuter, même lorsque toutes les activités MediaBrowser de l'UI qui y sont liées sont dissociées.
  • Le rappel onStop() doit appeler stopSelf(). Si le service a été démarré, il est arrêté. En outre, le service est détruit si aucune activité ne lui est associée. Sinon, le service reste lié jusqu'à ce que toutes ses activités soient dissociées. (Si un appel startService() ultérieur est reçu avant que le service ne soit détruit, l'arrêt en attente est annulé.)

L'organigramme suivant illustre la gestion du cycle de vie d'un service. Le compteur de variables effectue le suivi du nombre de clients liés:

Cycle de vie du service

Utiliser les notifications MediaStyle avec un service de premier plan

Lorsqu'un service est en cours de lecture, il doit s'exécuter au premier plan. Cela permet au système de savoir que le service exécute une fonction utile et ne doit pas être arrêté si le système manque de mémoire. Un service de premier plan doit afficher une notification pour que l'utilisateur en soit informé et puisse éventuellement le contrôler. Le rappel onPlay() doit placer le service au premier plan. Notez qu'il s'agit d'une signification particulière du terme "premier plan". Bien qu'Android considère le service au premier plan pour la gestion des processus, pour l'utilisateur, le lecteur lit en arrière-plan tandis qu'une autre application est visible au "premier plan". à l'écran.)

Lorsqu'un service s'exécute au premier plan, il doit afficher une notification, avec idéalement une ou plusieurs commandes de transport. La notification doit également inclure des informations utiles provenant des métadonnées de la session.

Créez et affichez la notification lorsque le joueur commence à jouer. Pour ce faire, nous vous recommandons d'utiliser la méthode MediaSessionCompat.Callback.onPlay().

L'exemple ci-dessous utilise la NotificationCompat.MediaStyle, conçu pour les applications multimédias. Il explique comment créer une notification qui affiche les métadonnées et les commandes de transport. La méthode pratique getController() vous permet de créer un contrôleur multimédia directement depuis votre session multimédia.

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

Tenez compte du comportement de ces notifications lorsque vous utilisez Paramètres NotificationCompat:

  • Lorsque vous utilisez setContentIntent(), votre service démarre automatiquement lorsque la notification sur laquelle l'utilisateur clique.
  • Dans un environnement « digne de confiance » situation comme l'écran de verrouillage, la visibilité par défaut du contenu des notifications est VISIBILITY_PRIVATE. Vous voudrez sans doute voir commandes de transport sur l'écran de verrouillage, VISIBILITY_PUBLIC est donc la meilleure option.
  • Soyez prudent lorsque vous définissez la couleur d'arrière-plan. Dans une notification ordinaire Android 5.0 ou version ultérieure, la couleur n'est appliquée qu'à l'arrière-plan petite icône d'application. Toutefois, pour les notifications MediaStyle antérieures à Android 7.0, la couleur est utilisé pour tout l'arrière-plan des notifications. Testez la couleur d'arrière-plan. Accéder doux pour les yeux et évitez les couleurs extrêmement vives ou fluorescentes.

Ces paramètres ne sont disponibles que lorsque vous utilisez NotificationCompat.MediaStyle:

  • Utiliser setMediaSession() pour associer la notification à votre session. Cela permet aux applications tierces et les appareils associés pour accéder à la session et la contrôler.
  • Utilisez setShowActionsInCompactView() pour ajouter jusqu'à trois actions à afficher dans l'élément contentView en taille standard de la notification. (Ici, le bouton de pause est spécifié.)
  • Sur Android 5.0 (niveau d'API 21) ou version ultérieure, vous pouvez faire glisser une notification pour l'arrêter une fois que le service n'est plus exécuté au premier plan. Ce que vous ne pouvez pas faire dans les versions précédentes. Pour autoriser les utilisateurs à supprimer la notification et à arrêter la lecture antérieures à Android 5.0 (niveau d'API 21), vous pouvez ajouter un bouton d'annulation dans l'angle supérieur droit notification en appelant setShowCancelButton(true) et setCancelButtonIntent().

Lorsque vous ajoutez les boutons "Pause" et "Cancel", vous avez besoin d'un PendingIntent pour associer à l'action de lecture. La méthode MediaButtonReceiver.buildMediaButtonPendingIntent() se charge de convertir une action PlaybackState en PendingIntent.