Créer un service de navigateur multimédia

Votre application doit déclarer le MediaBrowserService avec un filtre d'intent dans son fichier manifeste. Vous pouvez choisir votre propre nom de service. Dans l'exemple suivant, le nom de service choisi est MediaPlaybackService.

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

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() suivant 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 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 la valeur nulle, la connexion est refusée.

Pour autoriser les clients à se connecter à votre service et à 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 autoriser les clients à se connecter à votre MediaSession sans parcourir le contenu, onGetRoot() doit toujours renvoyer un BrowserRoot non nul, mais l'ID racine doit représenter une hiérarchie de contenu vide.

Une implémentation typique 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 pouvez contrôler qui peut se connecter à votre MediaBrowserService. Pour ce faire, vous pouvez utiliser une liste de contrôle d'accès (ACL) qui spécifie les connexions autorisées ou, à l'inverse, celles qui doivent être interdites. Pour obtenir un exemple d'implémentation d'une LCA qui autorise 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. En particulier, Android Auto limite la façon dont les utilisateurs interagissent avec les applications audio. Pour en savoir plus, consultez la section Lire de l'audio pour Auto. Vous pouvez examiner 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 des rootHints, le cas échéant).

Communiquer du contenu avec onLoadChildren()

Une fois le client connecté, il peut parcourir 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'MediaBrowser.MediaItem objets.

Chaque MediaItem possède une chaîne d'ID 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 responsable de l'association de 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 fournis par le MediaBrowserService ne doivent pas contenir de bitmaps d'icônes. 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 dépend de s'il est démarré ou lié à un ou plusieurs clients. Une fois un 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 est conçu. La différence réside dans la durée de vie 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 arrêté et détruit explicitement (en supposant qu'il n'est plus lié à aucun client).

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

Un service qui n'est que lié (et non démarré) est détruit lorsque tous ses clients sont dissociés. Si votre activité d'UI 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 à continuer à écouter même après avoir changé d'application. Vous ne voulez pas détruire le lecteur lorsque vous dissociez l'UI pour travailler avec une autre application.

Pour cette raison, vous devez vous assurer que le service est démarré lorsqu'il commence à lire en appelant startService(). Un service démarré doit être arrêté explicitement, qu'il soit lié ou non. Cela garantit que votre lecteur continue de fonctionner même si l'activité d'UI de contrôle 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 à arrêter le service est retardé jusqu'à ce que tous ses clients soient dissociés.

Le cycle de vie du MediaBrowserService est contrôlé par la façon dont il est créé, le nombre de clients qui y sont liés et les appels qu'il reçoit 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é y est liée (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 d'UI MediaBrowser 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é. De plus, 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 la destruction du service, l'arrêt en attente est annulé.)

L'organigramme suivant montre comment le cycle de vie d'un service est géré. Le compteur de variables suit le 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 spéciale de "premier plan". Bien qu'Android considère le service au premier plan à des fins de gestion des processus, pour l'utilisateur, le lecteur est en cours de lecture 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, idéalement avec 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 lecteur commence la lecture. Le meilleur endroit pour cela est la méthode MediaSessionCompat.Callback.onPlay().

L'exemple ci-dessous utilise NotificationCompat.MediaStyle, qui est conçu pour les applications multimédias. Il montre comment créer une notification qui affiche des métadonnées et des 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, tenez compte du comportement de ces paramètres NotificationCompat :

  • Lorsque vous utilisez setContentIntent(), votre service démarre automatiquement lorsque vous cliquez sur la notification, ce qui est une fonctionnalité pratique.
  • Dans une situation "non fiable" comme 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. Vous devez donc utiliser VISIBILITY_PUBLIC.
  • Soyez prudent lorsque vous définissez la couleur d'arrière-plan. Dans une notification ordinaire sous 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 de la notification. Testez votre couleur d'arrière-plan. Choisissez des couleurs douces pour les yeux et évitez les couleurs extrêmement 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 la contentView de taille standard de la notification. (Ici, le bouton de pause est spécifié.)
  • Sous Android 5.0 (niveau d'API 21) et versions ultérieures, vous pouvez balayer une notification pour arrêter le lecteur une fois que le service ne s'exécute plus au premier plan. Vous ne pouvez pas le faire 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 avez besoin d'un PendingIntent à associer à l'action de lecture. La méthode MediaButtonReceiver.buildMediaButtonPendingIntent() permet de convertir une action PlaybackState en PendingIntent.

Activer la navigation multimédia AVRCP

En plus des applications personnalisées telles qu'Android Auto, la couche Bluetooth du système agit également comme client de votre MediaBrowserService pour faciliter la navigation à distance sans fil dans le catalogue (AVRCP).

Sous Android 16 et Android 17, la plate-forme exige que les applications n'utilisant pas Media3 exposent une activité spécifique avec un filtre d'intent pour être validées pour la navigation.

Ajoutez ce filtre d'intent spécifique à une activité exportée dans votre AndroidManifest.xml. Notez que CATEGORY_DEFAULT est intentionnellement omis pour empêcher votre application d'apparaître dans les menus génériques "Ouvrir avec" pour les fichiers audio locaux :

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