Créer un service de navigateur multimédia

Votre application doit déclarer 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éfinie dans la bibliothèque Support Media-compat. Sur cette page, le terme "MediaBrowserService" fait référence à une instance de 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 dispose de deux méthodes qui gèrent les connexions client : onGetRoot() contrôle l'accès au service, et onLoadChildren() permet à un client de créer et d'afficher un menu de la hiérarchie de contenu du MediaBrowserService.

Contrôler les connexions client avec onGetRoot()

La méthode onGetRoot() renvoie le nœud racine de la hiérarchie de contenu. Si la méthode 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, qui est un ID racine représentant votre hiérarchie de contenu.

Pour permettre aux clients de se connecter à votre MediaSession sans naviguer, onGetRoot() doit toujours renvoyer un BrowserRoot non nul, mais l'ID racine doit représenter 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 souhaiterez peut-être contrôler qui peut se connecter à votre MediaBrowserService. Vous pouvez par exemple utiliser une liste de contrôle d'accès (LCA) qui spécifie les connexions autorisées ou qui énumère les connexions à interdire. Pour obtenir un exemple d'implémentation d'une LCA autorisant des connexions spécifiques, consultez la classe PackageValidator dans l'application exemple Universal Android Music Player.

Vous devez envisager de fournir différentes hiérarchies de contenu en fonction du type de client qui effectue la requête. Plus spécifiquement, Android Auto limite la façon dont les utilisateurs interagissent avec les applications audio. Pour en savoir plus, consultez la section Lecture audio en mode automatique. Vous pouvez consulter le clientPackageName au moment de la connexion pour déterminer le type de client et renvoyer un BrowserRoot différent en fonction du client (ou de l'rootHints, le cas échéant).

Communiquer du contenu avec onLoadChildren()

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

Chaque élément 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 doit associer l'identifiant 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 obtenir un exemple d'implémentation de 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 qu'un service est créé, il peut être démarré, lié ou les deux. Dans tous ces états, l'appareil est entièrement fonctionnel et peut effectuer les tâches pour lesquelles il a été conçu. La différence réside dans la durée de validité 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é à aucun client).

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

Un service qui n'est lié (et non démarré) est détruit lorsque tous ses clients sont dissociés. Si votre activité d'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. Toutefois, lorsque la lecture commence, l'utilisateur s'attend probablement à ce qu'il continue d'écouter même après avoir changé d'application. Il est préférable d'éviter de 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 commence à être lu en appelant startService(). Un service démarré doit être explicitement arrêté, qu'il soit lié ou non. Cela garantit que votre lecteur continue de fonctionner même si l'activité de contrôle de l'interface utilisateur est dissociée.

Pour arrêter un service démarré, appelez Context.stopService() ou stopSelf(). Le système s'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 la MediaBrowserService est contrôlé par la manière 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é lui est liée (après la connexion via son MediaBrowser).
  • Le rappel onPlay() de la session multimédia doit inclure un 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 lui sont associées sont dissociées.
  • Le rappel onStop() doit appeler stopSelf(). Si le service a démarré, il est arrêté. En outre, le service est détruit si aucune activité n'y est lié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 des services

Utiliser des 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 afin 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". Alors qu'Android considère le service au premier plan pour la gestion des processus, pour l'utilisateur, le lecteur joue 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 dans l'idéal une ou plusieurs commandes de transport. La notification doit également inclure des informations utiles provenant des métadonnées de la session.

Compilez et affichez la notification lorsque le joueur commence à jouer. Pour ce faire, nous vous recommandons de suivre cette méthode dans la méthode MediaSessionCompat.Callback.onPlay().

L'exemple ci-dessous utilise NotificationCompat.MediaStyle, conçu pour les applications multimédias. Elle montre 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 à partir de 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());

Lorsque vous utilisez des notifications MediaStyle, soyez conscient du comportement des paramètres NotificationCompat suivants:

  • Lorsque vous utilisez setContentIntent(), votre service démarre automatiquement lorsque l'utilisateur clique sur la notification, ce qui est une fonctionnalité pratique.
  • Dans une situation "non fiable" telle que l'écran de verrouillage, la visibilité par défaut du contenu des notifications est VISIBILITY_PRIVATE. Vous souhaitez probablement voir les commandes de transport sur l'écran de verrouillage. VISIBILITY_PUBLIC est donc la solution.
  • Soyez prudent lorsque vous définissez la couleur d'arrière-plan. Dans une notification ordinaire sur Android 5.0 ou version ultérieure, la couleur n'est appliquée qu'à l'arrière-plan de la petite icône d'application. Toutefois, pour les notifications MediaStyle antérieures à Android 7.0, la couleur est utilisée pour l'ensemble de l'arrière-plan des notifications. Testez la couleur d'arrière-plan. Prenez soin de vos yeux et évitez les couleurs très vives ou fluorescentes.

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

  • Utilisez setMediaSession() pour associer la notification à votre session. Cela permet aux applications tierces et aux appareils associés d'accéder à la session et de la contrôler.
  • Utilisez setShowActionsInCompactView() pour ajouter jusqu'à trois actions à afficher dans l'élément contentView de taille standard de la notification. Le bouton de mise en veille est ici spécifié.
  • À partir d'Android 5.0 (niveau d'API 21), vous pouvez faire disparaître une notification afin d'arrêter le lecteur une fois que le service ne s'exécute plus au premier plan. Ce n'est pas possible dans les versions antérieures. Pour permettre aux utilisateurs de supprimer la notification et d'arrêter la lecture avant Android 5.0 (niveau d'API 21), vous pouvez ajouter un bouton d'annulation en haut à droite de la notification en appelant setShowCancelButton(true) et setCancelButtonIntent().

Lorsque vous ajoutez les boutons de pause et d'annulation, vous devez associer un PendingIntent à l'action de lecture. La méthode MediaButtonReceiver.buildMediaButtonPendingIntent() convertit une action PlaybackState en PendingIntent.