Os controles de mídia no Android estão localizados perto das Configurações rápidas. As sessões de vários apps são organizadas em um carrossel deslizante. O carrossel lista as sessões nesta ordem:
- streams reproduzidos localmente no smartphone;
- streams remotos, como aqueles detectados em dispositivos externos ou sessões de transmissão;
- sessões retomáveis anteriores, na ordem em que foram reproduzidas pela última vez.
A partir do Android 13 (nível 33 da API), para garantir que os usuários possam acessar um conjunto
de controles de mídia para apps que reproduzem mídia, os botões de ação nos controles de mídia
são derivados do estado Player
.
Dessa forma, você pode apresentar um conjunto consistente de controles de mídia e uma experiência de controle de mídia mais refinada em todos os dispositivos.
A Figura 1 mostra um exemplo de como eles são exibidos em um smartphone e em um tablet, respectivamente.
O sistema exibe até cinco botões de ação com base no estado Player
, conforme
descrito na tabela a seguir. No modo compacto, apenas os três primeiros slots de ação
são exibidos. Isso está alinhado à maneira como os controles de mídia são renderizados em outras
plataformas Android, como o Auto, o Google Assistente e o Wear OS.
Espaço | Critérios | Ação |
---|---|---|
1 |
playWhenReady
é falso ou o estado de
reprodução atual é STATE_ENDED .
|
Reproduzir |
playWhenReady é verdadeiro e o estado de reprodução atual é STATE_BUFFERING .
|
Ícone de carregamento | |
playWhenReady é verdadeiro e o estado de reprodução atual é STATE_READY . |
Pausar | |
2 | O comando do player COMMAND_SEEK_TO_PREVIOUS ou COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM está disponível. |
Anterior |
Nenhum comando do player COMMAND_SEEK_TO_PREVIOUS ou COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM está disponível, e um comando personalizado do layout personalizado que ainda não foi colocado está disponível para preencher o slot. |
Personalizado | |
Os extras da sessão incluem um valor booleano true para a chave EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV . |
Vazio | |
3 | O comando do player COMMAND_SEEK_TO_NEXT ou COMMAND_SEEK_TO_NEXT_MEDIA_ITEM está disponível. |
Próxima |
Nenhum comando do player COMMAND_SEEK_TO_NEXT ou COMMAND_SEEK_TO_NEXT_MEDIA_ITEM está disponível, e um comando personalizado do layout personalizado que ainda não foi colocado está disponível para preencher o slot. |
Personalizado | |
Os extras da sessão incluem um valor booleano true para a chave EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT . |
Vazio | |
4 | Um comando personalizado do layout personalizado que ainda não foi colocado está disponível para preencher o slot. | Personalizado |
5 | Um comando personalizado do layout personalizado que ainda não foi colocado está disponível para preencher o slot. | Personalizado |
Os comandos personalizados são colocados na ordem em que foram adicionados ao layout personalizado.
Personalizar botões de comando
Para personalizar os controles de mídia do sistema com o Jetpack Media3,
é possível definir o layout personalizado da sessão e os comandos disponíveis de
controladores de acordo com a implementação de um MediaSessionService
:
Em
onCreate()
, crie umaMediaSession
e defina o layout personalizado dos botões de comando.Em
MediaSession.Callback.onConnect()
, autorize os controladores definindo os comandos disponíveis, incluindo comandos personalizados, noConnectionResult
.Em
MediaSession.Callback.onCustomCommand()
, responda ao comando personalizado selecionado pelo usuário.
Kotlin
class PlaybackService : MediaSessionService() { private val customCommandFavorites = SessionCommand(ACTION_FAVORITES, Bundle.EMPTY) private var mediaSession: MediaSession? = null override fun onCreate() { super.onCreate() val favoriteButton = CommandButton.Builder() .setDisplayName("Save to favorites") .setIconResId(R.drawable.favorite_icon) .setSessionCommand(customCommandFavorites) .build() val player = ExoPlayer.Builder(this).build() // Build the session with a custom layout. mediaSession = MediaSession.Builder(this, player) .setCallback(MyCallback()) .setCustomLayout(ImmutableList.of(favoriteButton)) .build() } private inner class MyCallback : MediaSession.Callback { override fun onConnect( session: MediaSession, controller: MediaSession.ControllerInfo ): ConnectionResult { // Set available player and session commands. return AcceptedResultBuilder(session) .setAvailablePlayerCommands( ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon() .remove(COMMAND_SEEK_TO_NEXT) .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) .remove(COMMAND_SEEK_TO_PREVIOUS) .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) .build() ) .setAvailableSessionCommands( ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon() .add(customCommandFavorites) .build() ) .build() } override fun onCustomCommand( session: MediaSession, controller: MediaSession.ControllerInfo, customCommand: SessionCommand, args: Bundle ): ListenableFuture{ if (customCommand.customAction == ACTION_FAVORITES) { // Do custom logic here saveToFavorites(session.player.currentMediaItem) return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) } return super.onCustomCommand(session, controller, customCommand, args) } } }
Java
public class PlaybackService extends MediaSessionService { private static final SessionCommand CUSTOM_COMMAND_FAVORITES = new SessionCommand("ACTION_FAVORITES", Bundle.EMPTY); @Nullable private MediaSession mediaSession; public void onCreate() { super.onCreate(); CommandButton favoriteButton = new CommandButton.Builder() .setDisplayName("Save to favorites") .setIconResId(R.drawable.favorite_icon) .setSessionCommand(CUSTOM_COMMAND_FAVORITES) .build(); Player player = new ExoPlayer.Builder(this).build(); // Build the session with a custom layout. mediaSession = new MediaSession.Builder(this, player) .setCallback(new MyCallback()) .setCustomLayout(ImmutableList.of(favoriteButton)) .build(); } private static class MyCallback implements MediaSession.Callback { @Override public ConnectionResult onConnect( MediaSession session, MediaSession.ControllerInfo controller) { // Set available player and session commands. return new AcceptedResultBuilder(session) .setAvailablePlayerCommands( ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon() .remove(COMMAND_SEEK_TO_NEXT) .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) .remove(COMMAND_SEEK_TO_PREVIOUS) .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) .build()) .setAvailableSessionCommands( ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon() .add(CUSTOM_COMMAND_FAVORITES) .build()) .build(); } public ListenableFutureonCustomCommand( MediaSession session, MediaSession.ControllerInfo controller, SessionCommand customCommand, Bundle args) { if (customCommand.customAction.equals(CUSTOM_COMMAND_FAVORITES.customAction)) { // Do custom logic here saveToFavorites(session.getPlayer().getCurrentMediaItem()); return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS)); } return MediaSession.Callback.super.onCustomCommand( session, controller, customCommand, args); } } }
Para saber mais sobre como configurar o MediaSession
para que clientes como o
sistema possam se conectar ao seu app de mídia, consulte
Conceder controle a outros clientes.
Com o Jetpack Media3, quando você implementa um MediaSession
, o PlaybackState
é atualizado automaticamente com o player de mídia. Da mesma forma, quando você
implementa um MediaSessionService
, a biblioteca publica automaticamente uma
notificação
MediaStyle
para você e a mantém atualizada.
Responder a botões de ação
Quando um usuário toca em um botão de ação nos controles de mídia do sistema, o
MediaController
do sistema envia um comando de reprodução para o MediaSession
. O
MediaSession
delega esses comandos ao jogador. Os comandos
definidos na interface Player
do Media3 são processados automaticamente pela sessão
de mídia.
Consulte Adicionar comandos personalizados para saber como responder a um comando personalizado.
Comportamento anterior ao Android 13
Para compatibilidade com versões anteriores, a IU do sistema continua fornecendo um layout alternativo
que usa ações de notificação para apps que não são atualizados para o Android 13
ou que não incluem informações PlaybackState
. Os botões de ação são
derivados da lista Notification.Action
anexada à notificação
MediaStyle
. O sistema mostra até cinco ações na ordem em que elas
foram adicionadas. No modo compacto, até três botões são mostrados, determinados pelos
valores transmitidos para setShowActionsInCompactView()
.
As ações personalizadas são posicionadas na ordem em que foram adicionadas ao
PlaybackState
.
O exemplo de código a seguir ilustra como adicionar ações à notificação MediaStyle :
Kotlin
import androidx.core.app.NotificationCompat import androidx.media3.session.MediaStyleNotificationHelper var notification = NotificationCompat.Builder(context, CHANNEL_ID) // Show controls on lock screen even when user hides sensitive content. .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setSmallIcon(R.drawable.ic_stat_player) // Add media control buttons that invoke intents in your media service .addAction(R.drawable.ic_prev, "Previous", prevPendingIntent) // #0 .addAction(R.drawable.ic_pause, "Pause", pausePendingIntent) // #1 .addAction(R.drawable.ic_next, "Next", nextPendingIntent) // #2 // Apply the media style template .setStyle(MediaStyleNotificationHelper.MediaStyle(mediaSession) .setShowActionsInCompactView(1 /* #1: pause button */)) .setContentTitle("Wonderful music") .setContentText("My Awesome Band") .setLargeIcon(albumArtBitmap) .build()
Java
import androidx.core.app.NotificationCompat; import androidx.media3.session.MediaStyleNotificationHelper; NotificationCompat.Builder notification = new NotificationCompat.Builder(context, CHANNEL_ID) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setSmallIcon(R.drawable.ic_stat_player) .addAction(R.drawable.ic_prev, "Previous", prevPendingIntent) .addAction(R.drawable.ic_pause, "Pause", pausePendingIntent) .addAction(R.drawable.ic_next, "Next", nextPendingIntent) .setStyle(new MediaStyleNotificationHelper.MediaStyle(mediaSession) .setShowActionsInCompactView(1 /* #1: pause button */)) .setContentTitle("Wonderful music") .setContentText("My Awesome Band") .setLargeIcon(albumArtBitmap) .build();
Suporte à retomada de mídia
A retomada de mídia permite que os usuários reiniciem sessões anteriores do carrossel sem precisar iniciar o app. Quando a reprodução começa, o usuário interage com os controles de mídia da maneira habitual.
O recurso de retomada da reprodução pode ser ativado e desativado usando o app Configurações, nas opções Som > Mídia. O usuário também pode acessar as Configurações tocando no ícone de engrenagem que aparece depois de deslizar no carrossel aberto.
O Media3 oferece APIs para facilitar o suporte à retomada de mídia. Consulte a documentação Retomada de reprodução com o Media3 para saber como implementar esse recurso.
Como usar as APIs de mídia legadas
Esta seção explica como fazer a integração com os controles de mídia do sistema usando as APIs MediaCompat legadas.
O sistema recupera as seguintes informações do MediaMetadata
do MediaSession
e as exibe quando estão disponíveis:
METADATA_KEY_ALBUM_ART_URI
METADATA_KEY_TITLE
METADATA_KEY_DISPLAY_TITLE
METADATA_KEY_ARTIST
METADATA_KEY_DURATION
: se a duração não for definida, a barra de busca não vai mostrar o progresso.
Para garantir uma notificação de controle de mídia válida e precisa,
defina o valor dos metadados METADATA_KEY_TITLE
ou METADATA_KEY_DISPLAY_TITLE
para o título da mídia que está sendo reproduzida.
O player de mídia mostra o tempo decorrido da mídia em reprodução no momento com uma barra de busca mapeada para o MediaSession
PlaybackState
.
O player de mídia mostra o progresso da mídia em reprodução no momento, além de
uma barra de busca mapeada para o MediaSession
PlaybackState
. A barra de busca
permite que os usuários mudem a posição e mostra o tempo decorrido do item
de mídia. Para que a barra de busca seja ativada, é necessário implementar
PlaybackState.Builder#setActions
e incluir ACTION_SEEK_TO
.
Espaço | Ação | Critérios |
---|---|---|
1 | Reproduzir |
O estado atual do PlaybackState é um dos seguintes:
|
Ícone de carregamento |
O estado atual do PlaybackState é um dos seguintes:
|
|
Pausar | O estado atual do PlaybackState não está listado acima. |
|
2 | Anterior | As ações PlaybackState incluem ACTION_SKIP_TO_PREVIOUS . |
Personalizado | As ações PlaybackState não incluem ACTION_SKIP_TO_PREVIOUS e as ações personalizadas PlaybackState incluem uma ação personalizada que ainda não foi posicionada. |
|
Vazio | Os PlaybackState extras incluem um valor booleano true para a chave SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV . |
|
3 | Próxima | As ações PlaybackState incluem ACTION_SKIP_TO_NEXT . |
Personalizado | As ações PlaybackState não incluem ACTION_SKIP_TO_NEXT e as ações personalizadas PlaybackState incluem uma ação personalizada que ainda não foi posicionada. |
|
Vazio | Os PlaybackState extras incluem um valor booleano true para a chave SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT . |
|
4 | Personalizado | As ações personalizadas PlaybackState incluem uma ação personalizada que ainda não foi posicionada. |
5 | Personalizado | As ações personalizadas PlaybackState incluem uma ação personalizada que ainda não foi posicionada. |
Adicionar ações padrão
Os exemplos de código a seguir ilustram como adicionar ações padrão e
personalizadas de PlaybackState
.
Para tocar, pausar, anterior e próxima, defina essas ações no
PlaybackState
da sessão de mídia.
Kotlin
val session = MediaSessionCompat(context, TAG) val playbackStateBuilder = PlaybackStateCompat.Builder() val style = NotificationCompat.MediaStyle() // For this example, the media is currently paused: val state = PlaybackStateCompat.STATE_PAUSED val position = 0L val playbackSpeed = 1f playbackStateBuilder.setState(state, position, playbackSpeed) // And the user can play, skip to next or previous, and seek val stateActions = PlaybackStateCompat.ACTION_PLAY or PlaybackStateCompat.ACTION_PLAY_PAUSE or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or PlaybackStateCompat.ACTION_SKIP_TO_NEXT or PlaybackStateCompat.ACTION_SEEK_TO // adding the seek action enables seeking with the seekbar playbackStateBuilder.setActions(stateActions) // ... do more setup here ... session.setPlaybackState(playbackStateBuilder.build()) style.setMediaSession(session.sessionToken) notificationBuilder.setStyle(style)
Java
MediaSessionCompat session = new MediaSessionCompat(context, TAG); PlaybackStateCompat.Builder playbackStateBuilder = new PlaybackStateCompat.Builder(); NotificationCompat.MediaStyle style = new NotificationCompat.MediaStyle(); // For this example, the media is currently paused: int state = PlaybackStateCompat.STATE_PAUSED; long position = 0L; float playbackSpeed = 1f; playbackStateBuilder.setState(state, position, playbackSpeed); // And the user can play, skip to next or previous, and seek long stateActions = PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SEEK_TO; // adding this enables the seekbar thumb playbackStateBuilder.setActions(stateActions); // ... do more setup here ... session.setPlaybackState(playbackStateBuilder.build()); style.setMediaSession(session.getSessionToken()); notificationBuilder.setStyle(style);
Se você não quiser botões nos slots anteriores ou seguintes, não adicione
ACTION_SKIP_TO_PREVIOUS
ou ACTION_SKIP_TO_NEXT
. Em vez disso, adicione extras à
sessão:
Kotlin
session.setExtras(Bundle().apply { putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true) putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true) })
Java
Bundle extras = new Bundle(); extras.putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true); extras.putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true); session.setExtras(extras);
Adicionar ações personalizadas
Para outras ações que você quer mostrar nos controles de mídia, crie um
PlaybackStateCompat.CustomAction
e adicione ao PlaybackState
. Essas ações são mostradas na
ordem em que foram adicionadas.
Kotlin
val customAction = PlaybackStateCompat.CustomAction.Builder( "com.example.MY_CUSTOM_ACTION", // action ID "Custom Action", // title - used as content description for the button R.drawable.ic_custom_action ).build() playbackStateBuilder.addCustomAction(customAction)
Java
PlaybackStateCompat.CustomAction customAction = new PlaybackStateCompat.CustomAction.Builder( "com.example.MY_CUSTOM_ACTION", // action ID "Custom Action", // title - used as content description for the button R.drawable.ic_custom_action ).build(); playbackStateBuilder.addCustomAction(customAction);
Como responder a ações de PlaybackState
Quando um usuário toca em um botão, o SystemUI usa
MediaController.TransportControls
para enviar um comando de volta ao MediaSession
. É necessário registrar um callback
que possa responder corretamente a esses eventos.
Kotlin
val callback = object: MediaSession.Callback() { override fun onPlay() { // start playback } override fun onPause() { // pause playback } override fun onSkipToPrevious() { // skip to previous } override fun onSkipToNext() { // skip to next } override fun onSeekTo(pos: Long) { // jump to position in track } override fun onCustomAction(action: String, extras: Bundle?) { when (action) { CUSTOM_ACTION_1 -> doCustomAction1(extras) CUSTOM_ACTION_2 -> doCustomAction2(extras) else -> { Log.w(TAG, "Unknown custom action $action") } } } } session.setCallback(callback)
Java
MediaSession.Callback callback = new MediaSession.Callback() { @Override public void onPlay() { // start playback } @Override public void onPause() { // pause playback } @Override public void onSkipToPrevious() { // skip to previous } @Override public void onSkipToNext() { // skip to next } @Override public void onSeekTo(long pos) { // jump to position in track } @Override public void onCustomAction(String action, Bundle extras) { if (action.equals(CUSTOM_ACTION_1)) { doCustomAction1(extras); } else if (action.equals(CUSTOM_ACTION_2)) { doCustomAction2(extras); } else { Log.w(TAG, "Unknown custom action " + action); } } };
Retomada de mídia
Para que o app do player apareça na área de configurações rápidas, você precisa criar uma notificação MediaStyle
com um token MediaSession
válido.
Para mostrar o título da notificação do MediaStyle, use
NotificationBuilder.setContentTitle()
.
Para exibir o ícone de marca do player de mídia, use NotificationBuilder.setSmallIcon()
.
Para compatibilidade com a retomada de reprodução, os apps precisam implementar um MediaBrowserService
e um MediaSession
. O MediaSession
precisa implementar o callback onPlay()
.
Implementação de MediaBrowserService
Depois que o dispositivo for inicializado, o sistema procurará os cinco apps de mídia mais recentes e fornecerá controles que podem ser usados para reiniciar a reprodução de cada app.
O sistema tentará entrar em contato com o MediaBrowserService
com uma conexão da SystemUI. Seu app precisa permitir essas conexões. Caso contrário, não será possível retomar a reprodução.
As conexões da SystemUI podem ser identificadas e verificadas usando o nome do pacote com.android.systemui
e a assinatura. A SystemUI tem a assinatura da plataforma. Um exemplo de como verificar a assinatura da plataforma pode ser encontrado no app UAMP.
Para compatibilidade com a retomada de reprodução, seu MediaBrowserService
precisa implementar estes comportamentos:
onGetRoot()
precisa retornar uma raiz não nula rapidamente. Outra lógica complexa precisa ser tratada emonLoadChildren()
Quando
onLoadChildren()
é chamado no ID de mídia raiz, o resultado precisa conter um filho FLAG_PLAYABLE.MediaBrowserService
precisa retornar o item de mídia reproduzido mais recentemente quando receber uma consulta EXTRA_RECENT. O valor retornado precisa ser um item de mídia real em vez de uma função genérica.MediaBrowserService
precisa fornecer uma MediaDescription apropriada com um título e uma legenda não vazios. Ele também precisa definir um URI de ícone ou um bitmap de ícone.
Os exemplos de código a seguir ilustram como implementar onGetRoot()
.
Kotlin
override fun onGetRoot( clientPackageName: String, clientUid: Int, rootHints: Bundle? ): BrowserRoot? { ... // Verify that the specified package is SystemUI. You'll need to write your // own logic to do this. if (isSystem(clientPackageName, clientUid)) { rootHints?.let { if (it.getBoolean(BrowserRoot.EXTRA_RECENT)) { // Return a tree with a single playable media item for resumption. val extras = Bundle().apply { putBoolean(BrowserRoot.EXTRA_RECENT, true) } return BrowserRoot(MY_RECENTS_ROOT_ID, extras) } } // You can return your normal tree if the EXTRA_RECENT flag is not present. return BrowserRoot(MY_MEDIA_ROOT_ID, null) } // Return an empty tree to disallow browsing. return BrowserRoot(MY_EMPTY_ROOT_ID, null)
Java
@Override public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) { ... // Verify that the specified package is SystemUI. You'll need to write your // own logic to do this. if (isSystem(clientPackageName, clientUid)) { if (rootHints != null) { if (rootHints.getBoolean(BrowserRoot.EXTRA_RECENT)) { // Return a tree with a single playable media item for resumption. Bundle extras = new Bundle(); extras.putBoolean(BrowserRoot.EXTRA_RECENT, true); return new BrowserRoot(MY_RECENTS_ROOT_ID, extras); } } // You can return your normal tree if the EXTRA_RECENT flag is not present. return new BrowserRoot(MY_MEDIA_ROOT_ID, null); } // Return an empty tree to disallow browsing. return new BrowserRoot(MY_EMPTY_ROOT_ID, null); }