Como criar um serviço de navegador de mídia

O app precisa declarar o MediaBrowserService com um filtro de intent no manifesto. Você pode escolher um nome de serviço próprio. No exemplo a seguir, é "MediaPlaybackService".

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

Observação : a implementação recomendada de MediaBrowserService é MediaBrowserServiceCompat. que é definido na Biblioteca de Suporte media-compat. Nesta página, o termo "MediaBrowserService" se refere a uma instância de MediaBrowserServiceCompat.

Inicializar a sessão de mídia

Quando o serviço recebe o método de callback do ciclo de vida de onCreate(), ele executa estas etapas:

O código onCreate() abaixo demonstra essas etapas.

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

Gerenciar conexões de clientes

Um MediaBrowserService tem dois métodos que gerenciam conexões de cliente: onGetRoot() controla o acesso ao serviço, e onLoadChildren() fornece a capacidade de um cliente criar e exibir um menu da hierarquia de conteúdo do MediaBrowserService.

Como controlar as conexões do cliente com onGetRoot()

O método onGetRoot() retorna o nó raiz da hierarquia de conteúdo. Se o método retornar nulo, a conexão será recusada.

Para permitir que os clientes se conectem ao seu serviço e naveguem pelo conteúdo de mídia, onGetRoot() precisa retornar um BrowserRoot não nulo, que é um ID raiz que representa a hierarquia de conteúdo.

Para permitir que os clientes se conectem à MediaSession sem navegar, onGetRoot() ainda precisa retornar um BrowserRoot não nulo, mas o ID raiz precisa representar uma hierarquia de conteúdo vazia.

Uma implementação típica de onGetRoot() pode ter esta aparência:

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

Em alguns casos, é recomendável controlar quem pode se conectar ao MediaBrowserService. Uma maneira é usar uma lista de controle de acesso (ACL, na sigla em inglês) que especifica quais conexões são permitidas ou que enumera as conexões proibidas. Para ver um exemplo de como implementar uma ACL que permita conexões específicas, consulte a classe PackageValidator no app de exemplo Universal Android Music Player (link em inglês).

Considere fornecer diferentes hierarquias de conteúdo, dependendo do tipo de cliente que está fazendo a consulta. Especificamente, o Android Auto limita a forma como os usuários interagem com apps de áudio. Para ver mais informações, consulte Como reproduzir áudio para o modo automático. Você pode analisar o clientPackageName no momento da conexão para determinar o tipo de cliente e retornar um BrowserRoot diferente, dependendo dele (ou rootHints, se houver).

Como comunicar conteúdo com onLoadChildren()

Depois que o cliente se conecta, ele pode atravessar a hierarquia de conteúdo fazendo chamadas repetidas para MediaBrowserCompat.subscribe() para criar uma representação local da IU. O método subscribe() envia o callback onLoadChildren() ao serviço, que retorna uma lista de objetos MediaBrowser.MediaItem.

Cada MediaItem tem uma string de ID exclusiva, que é um token opaco. Quando um cliente quer abrir um submenu ou reproduzir um item, ele passa o código. O serviço é responsável por associar o código ao nó do menu ou ao item de conteúdo apropriado.

Uma implementação simples de onLoadChildren() teria esta aparência:

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

Observação : os objetos MediaItem entregues pelo MediaBrowserService não podem conter bitmaps de ícone. Em vez disso, use um Uri chamando setIconUri() ao criar a MediaDescription para cada item.

Para ver um exemplo de como implementar onLoadChildren(), veja o app de amostra Universal Android Music Player.

O ciclo de vida do serviço do navegador de mídia

O comportamento de um serviço para Android depende de ser iniciado ou estar vinculado a um ou mais clientes. Depois de ser criado, um serviço pode ser iniciado, vinculado ou ambos. Em todos esses estados, ele é totalmente funcional e pode executar o trabalho para que foi projetado. A diferença é o tempo de existência do serviço. Um serviço vinculado não é destruído até que todos os seus clientes se desvinculem. Um serviço iniciado pode ser explicitamente interrompido e destruído (supondo que ele não esteja mais vinculado a nenhum cliente).

Quando um MediaBrowser em execução em outra atividade se conecta a um MediaBrowserService, ele vincula a atividade ao serviço, tornando-o vinculado (mas não iniciado). Esse comportamento padrão é integrado à classe MediaBrowserServiceCompat.

Um serviço que só é vinculado (e não iniciado) é destruído quando todos os clientes se desvinculam. Se a atividade da IU é desconectada nesse momento, o serviço é destruído. Isso não é um problema, caso você ainda não tenha tocado nenhuma música. No entanto, quando a reprodução começa, o usuário provavelmente espera continuar ouvindo mesmo depois de trocar de app. Você não quer destruir o player ao desvincular a IU para funcionar com outro app.

Por esse motivo, chame startService() para ter certeza de que o serviço será iniciado quando começar a ser reproduzido. Um serviço iniciado precisa ser explicitamente interrompido, esteja ou não vinculado. Isso garante que o player continue a funcionar mesmo que a atividade de interface de controle seja desvinculada.

Para interromper um serviço iniciado, chame Context.stopService() ou stopSelf(). O sistema interrompe e destrói o serviço o mais rápido possível. No entanto, se um ou mais clientes ainda estiverem vinculados, a chamada para interromper o serviço será adiada até que todos os clientes se desvinculem.

O ciclo de vida de MediaBrowserService é controlado pela forma como foi criado, pelo número de clientes vinculados a ele e pelas chamadas recebidas de callbacks de sessão de mídia. Para resumir:

  • O serviço é criado quando é iniciado em resposta a um botão de mídia ou quando uma atividade é vinculada a ele, depois de se conectar via MediaBrowser.
  • O callback da sessão de mídia onPlay() precisa incluir o código que chama startService(). Isso garante que o serviço seja iniciado e continue em execução, mesmo quando todas as atividades MediaBrowser da IU que estão vinculadas a ele forem desvinculadas.
  • O callback onStop() precisa chamar stopSelf(). Se o serviço tiver sido iniciado, ele o interrompe. Além disso, o serviço será destruído se não houver atividades vinculadas a ele. Caso contrário, o serviço permanecerá vinculado até que todas as atividades dele sejam desvinculadas. Se uma chamada startService() subsequente for recebida antes de o serviço ser destruído, a parada pendente será cancelada.

O fluxograma a seguir demonstra como o ciclo de vida de um serviço é gerenciado. O contador variável rastreia o número de clientes vinculados:

Ciclo de vida do serviço

Como usar as notificações do MediaStyle com um serviço em primeiro plano

Quando um serviço está sendo reproduzido, ele precisa estar sendo executado em primeiro plano. Isso permite que o sistema saiba que o serviço está desempenhando uma função útil e não pode ser interrompido se o sistema estiver com pouca memória. Um serviço em primeiro plano precisa exibir uma notificação para que o usuário saiba sobre ele e tenha a opção de controlá-lo. O callback onPlay() precisa colocar o serviço em primeiro plano. Observe que o significado de "primeiro plano" aqui é especial. Embora o Android considere o serviço em primeiro plano para fins de gerenciamento do processo, para o usuário, o player está tocando em segundo plano enquanto algum outro app está visível em "primeiro plano" na tela.

Quando um serviço é executado em primeiro plano, ele precisa exibir uma notificação, de preferência com um ou mais controles de transporte. A notificação também precisa incluir informações úteis dos metadados da sessão.

Crie e exiba a notificação quando o player começar a tocar. O melhor lugar para fazer isso é dentro do método MediaSessionCompat.Callback.onPlay().

O exemplo abaixo usa o NotificationCompat.MediaStyle, projetado para apps de música. Ele mostra como criar uma notificação que exiba metadados e controles de transporte. O método de conveniência getController() permite que você crie um controlador de mídia diretamente da sua sessão de mí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());

Ao usar notificações do MediaStyle, esteja ciente do comportamento destas configurações da NotificationCompat:

  • Ao usar setContentIntent(), seu serviço é iniciado automaticamente quando a notificação é clicada, um recurso útil.
  • Em uma situação "não confiável", como a tela de bloqueio, a visibilidade padrão para o conteúdo das notificações é VISIBILITY_PRIVATE. É provável que você queira ver os controles de transporte na tela de bloqueio, então VISIBILITY_PUBLIC é a melhor opção.
  • Cuidado ao definir a cor do plano de fundo. Em uma notificação comum no Android versão 5.0 ou mais recente, a cor é aplicada apenas ao plano de fundo do ícone pequeno do app. Mas, para notificações do MediaStyle anteriores ao Android 7.0, a cor é usada em todo o plano de fundo da notificação. Teste a cor do plano de fundo. Suavize os olhos e evite cores extremamente brilhantes ou fluorescentes.

Essas configurações só estão disponíveis quando você usa NotificationCompat.MediaStyle:

  • Use setMediaSession() para associar a notificação à sua sessão. Isso permite que apps de terceiros e dispositivos complementares acessem e controlem a sessão.
  • Use setShowActionsInCompactView() para adicionar até três ações que serão exibidas na contentView de tamanho padrão da notificação. (Aqui o botão de pausa é especificado.)
  • No Android 5.0 (nível 21 da API) e versões mais recentes, é possível deslizar uma notificação para interromper o jogador quando o serviço não estiver mais sendo executado em primeiro plano. Não é possível fazer isso em versões anteriores. Para permitir que os usuários removam a notificação e interrompam a reprodução antes do Android 5.0 (API de nível 21), você pode adicionar um botão de cancelamento no canto superior direito da notificação chamando setShowCancelButton(true) e setCancelButtonIntent().

Ao adicionar os botões de pausa e cancelamento, você precisará de uma PendingIntent para anexar à ação de reprodução. O método MediaButtonReceiver.buildMediaButtonPendingIntent() faz o trabalho de converter uma ação PlaybackState em PendingIntent.