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, 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 um conjunto de controle de mídia em vários dispositivos.
A Figura 1 mostra um exemplo dessa aparência em um smartphone e um tablet, respectivamente.
O sistema mostra até cinco botões de ação com base no estado Player
.
descritos na tabela a seguir. No modo compacto, apenas as três primeiras ações
slots serão exibidos. Isso se alinha com a forma como os controles de mídia são renderizados em outros
Plataformas Android, como Auto, Assistente e Wear OS.
Espaço | Critérios | Ação |
---|---|---|
1 |
playWhenReady
é falsa ou a reprodução atual
estado é 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 |
Os comandos do player COMMAND_SEEK_TO_PREVIOUS e COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM não estão disponíveis, e um comando personalizado do layout personalizado que ainda não foi colocado está disponível para preencher o espaço. |
Personalizado | |
(ainda não compatível com Media3) PlaybackState extras 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 |
Os comandos do player COMMAND_SEEK_TO_NEXT e COMMAND_SEEK_TO_NEXT_MEDIA_ITEM não estão disponíveis, e um comando personalizado do layout personalizado que ainda não foi colocado está disponível para preencher o espaço. |
Personalizado | |
(ainda não compatível com Media3) PlaybackState extras 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 espaço. | Personalizado |
5 | Um comando personalizado do layout personalizado que ainda não foi colocado está disponível para preencher o espaço. | Personalizado |
Os comandos personalizados são colocados na ordem em que foram adicionados ao um layout personalizado.
Personalizar botões de comando
Para personalizar os controles de mídia do sistema com o Jetpack Media3, faça o seguinte:
você pode definir o layout personalizado da sessão e os comandos disponíveis de
controladores adequadamente ao implementar um MediaSessionService
:
Em
onCreate()
, crie umMediaSession
e defina o layout personalizado de botões de comando.No
MediaSession.Callback.onConnect()
, autorizar os controladores definindo os comandos disponíveis, incluindo comandos personalizados, emConnectionResult
.No
MediaSession.Callback.onCustomCommand()
, respondem ao comando personalizado que é 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 MediaSession
para que clientes como o
sistema pode se conectar ao seu app de música, consulte
Conceder controle a outros clientes.
Com o Jetpack Media3, quando você implementa um MediaSession
, seu PlaybackState
são atualizados automaticamente com o player de mídia. Da mesma forma, quando você
implementar um MediaSessionService
, a biblioteca publica automaticamente uma
Notificação do MediaStyle
para você e o mantém atualizado.
Botões de resposta
Quando um usuário toca em um botão de ação nos controles de mídia do sistema, o
O MediaController
envia um comando de reprodução ao MediaSession
. A
Em seguida, o MediaSession
delega esses comandos para o player. Comandos
definido no Player
do Media3
são gerenciadas automaticamente pela
sessão.
Consulte Adicionar comandos personalizados. para receber orientações sobre como responder a um comando personalizado.
Comportamento anterior ao Android 13
Para compatibilidade com versões anteriores, a interface do sistema continua a oferecer 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 incluam informações de PlaybackState
. Os botões de ação são
derivado da lista de Notification.Action
anexada ao MediaStyle
notificação. O sistema exibe até cinco ações na ordem em que elas
foram adicionados. No modo compacto, até três botões são mostrados, determinados pelo
passados 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 ao MediaStyle notificação :
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 para retomada de mídia
Ao retomar a mídia, os usuários podem reiniciar as 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 normal.
O recurso de retomada da reprodução pode ser ativado e desativado usando o aplicativo Configurações, na seção Som > Opções de mídia. O usuário também pode acessar as Configurações tocando no ícone de engrenagem que aparece depois de deslizar sobre o carrossel expandido.
A Media3 oferece APIs para facilitar o suporte à retomada de mídia. Consulte a Retomada de reprodução com Media3 documentação para orientações sobre 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 mostrar progresso)
Para garantir que você tenha uma notificação de controle de mídia válida e precisa,
Defina o valor de METADATA_KEY_TITLE
ou METADATA_KEY_DISPLAY_TITLE
.
metadados ao título da mídia em reprodução.
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, junto com
uma barra de busca mapeada para o PlaybackState
do MediaSession
. Barra de busca
permite que os usuários alterem a posição e exibe o tempo decorrido da mídia
do item de linha. Para que a barra de busca seja ativada, você deve implementar
PlaybackState.Builder#setActions
e inclua ACTION_SEEK_TO
.
Espaço | Ação | Critérios |
---|---|---|
1 | Reproduzir |
O estado atual da 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 os padrões PlaybackState
e
ações personalizadas.
Para reproduzir, pausar, voltar e avançar, defina essas ações em
o PlaybackState
para a 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 não quiser nenhum botão nos espaços anterior ou seguinte, não adicione
ACTION_SKIP_TO_PREVIOUS
ou ACTION_SKIP_TO_NEXT
e, em vez disso, adicionar 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ê queira mostrar nos controles de mídia, é possível criar um
PlaybackStateCompat.CustomAction
e adicioná-lo ao PlaybackState
. Essas ações são mostradas
ordem em que foram adicionados.
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 PlaybackState
Quando um usuário toca em um botão, o SystemUI usa
MediaController.TransportControls
para enviar um comando de volta ao MediaSession
. É preciso registrar um callback
que podem responder adequadamente 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 exibir o título da notificação 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
. Seu 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); }