Элементы управления мультимедиа в Android находятся рядом с быстрыми настройками. Сеансы из нескольких приложений представлены в виде карусели, которую можно пролистывать. В карусели сеансы перечислены в следующем порядке:
- Трансляции, воспроизводимые на телефоне.
- удаленные потоки, например обнаруженные на внешних устройствах или в сеансах трансляции;
- Предыдущие сеансы, которые можно возобновить, в порядке их последнего воспроизведения.
Начиная с Android 13 (уровень API 33), чтобы пользователи могли получить доступ к расширенному набору элементов управления мультимедиа для приложений, воспроизводящих медиаконтент, командные кнопки в элементах управления мультимедиа создаются на основе состояния Player.
Это позволяет обеспечить единообразие элементов управления мультимедиа и улучшить взаимодействие с ними на разных устройствах.
На рисунке 1 показано, как это выглядит на телефоне и планшете.
Система показывает до пяти командных кнопок в зависимости от состояния Player, как описано в таблице ниже. В компактном режиме отображаются только первые три слота действий. Это соответствует тому, как элементы управления мультимедиа отображаются на других платформах Android, таких как Auto, Assistant и Wear OS.
| Слот | Критерии | Действие |
|---|---|---|
| 1 |
playWhenReady имеет значение false или текущее
состояние воспроизведения – STATE_ENDED.
|
Воспроизвести |
playWhenReady имеет значение true, а текущее
состояние воспроизведения – STATE_BUFFERING.
|
Индикатор выполнения | |
playWhenReady имеет значение true, а текущее
состояние воспроизведения – STATE_READY.
|
Приостановить | |
| 2 |
В настройках кнопок мультимедиа есть настраиваемая кнопка для CommandButton.SLOT_BACK.
|
Свой вариант |
Доступна команда проигрывателя
COMMAND_SEEK_TO_PREVIOUS или
COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM.
|
Назад | |
| Недоступна ни настраиваемая кнопка, ни одна из перечисленных команд. | Пусто | |
| 3 |
В настройках кнопок управления медиаконтентом есть настраиваемая кнопка для CommandButton.SLOT_FORWARD.
|
Свой вариант |
Доступна команда проигрывателя
COMMAND_SEEK_TO_NEXT или
COMMAND_SEEK_TO_NEXT_MEDIA_ITEM.
|
Далее | |
| Недоступна ни настраиваемая кнопка, ни одна из перечисленных команд. | Пусто | |
| 4 |
В настройках кнопок мультимедиа есть специальная кнопка для CommandButton.SLOT_OVERFLOW, которая ещё не размещена.
|
Свой вариант |
| 5 |
В настройках кнопок мультимедиа есть специальная кнопка для CommandButton.SLOT_OVERFLOW, которая ещё не размещена.
|
Свой вариант |
Кнопки "Ещё" размещаются в том порядке, в котором они были добавлены в настройки кнопок мультимедиа.
Настроить кнопки команд
Чтобы настроить системные элементы управления мультимедиа с помощью Jetpack Media3, вы можете задать настройки кнопок мультимедиа для сеанса и доступные команды контроллеров:
Создайте
MediaSessionи задайте настройки кнопок мультимедиа для специальных кнопок команд.В
MediaSession.Callback.onConnect()авторизуйте контроллеры, определив доступные для них команды, в том числе пользовательские, вConnectionResult.В
MediaSession.Callback.onCustomCommand()ответьте на специальную команду, выбранную пользователем.
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(CommandButton.ICON_HEART_UNFILLED) .setDisplayName("Save to favorites") .setSessionCommand(customCommandFavorites) .build() val player = ExoPlayer.Builder(this).build() // Build the session with a custom layout. mediaSession = MediaSession.Builder(this, player) .setCallback(MyCallback()) .setMediaButtonPreferences(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) .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(CommandButton.ICON_HEART_UNFILLED) .setDisplayName("Save to favorites") .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()) .setMediaButtonPreferences(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) .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); } } }
Чтобы узнать, как настроить MediaSession, чтобы клиенты, такие как система, могли подключаться к вашему мультимедийному приложению, прочитайте статью Предоставление управления другим клиентам.
Если вы реализуете MediaSession с помощью Jetpack Media3, PlaybackState будет автоматически синхронизироваться с медиапроигрывателем. Аналогично, когда вы реализуете MediaSessionService, библиотека автоматически публикует для вас уведомление MediaStyle и поддерживает его в актуальном состоянии.
Как использовать командные кнопки
Когда пользователь нажимает командную кнопку в системных элементах управления мультимедиа, системный объект MediaController отправляет команду воспроизведения в ваш объект MediaSession. Затем MediaSession делегирует эти команды проигрывателю. Команды, определенные в интерфейсе Player Media3, обрабатываются сеансом мультимедиа автоматически.
Инструкции о том, как отвечать на специальные команды, приведены в разделе Добавление специальных команд.
Поддержка возобновления воспроизведения медиаконтента
Возобновление воспроизведения позволяет пользователям перезапускать предыдущие сеансы из карусели, не запуская приложение. Когда воспроизведение начинается, пользователь взаимодействует с элементами управления мультимедиа обычным образом.
Включить или отключить возобновление воспроизведения можно в настройках устройства в разделе Звук > Медиа. Пользователь также может перейти в настройки, нажав на значок шестеренки, который появляется после пролистывания развернутой карусели.
Media3 предлагает API, которые упрощают поддержку возобновления воспроизведения. Инструкции по реализации этой функции можно найти в документации Возобновление воспроизведения с помощью Media3.
Использование устаревших API для медиаконтента
В этом разделе объясняется, как интегрировать приложение с системными элементами управления мультимедиа, используя устаревшие API MediaCompat.
Система извлекает из MediaMetadata MediaSession следующую информацию и показывает ее, если она доступна:
METADATA_KEY_ALBUM_ART_URIMETADATA_KEY_TITLEMETADATA_KEY_DISPLAY_TITLEMETADATA_KEY_ARTISTMETADATA_KEY_DURATION(Если продолжительность не задана, панель перемотки не показывает прогресс.)
Чтобы уведомление об элементах управления мультимедиа было действительным и точным, задайте для метаданных METADATA_KEY_TITLE или METADATA_KEY_DISPLAY_TITLE значение, соответствующее названию воспроизводимого контента.
Проигрыватель показывает время, прошедшее с начала воспроизведения, и панель перемотки, которая сопоставлена с MediaSession
PlaybackState.
Проигрыватель показывает ход воспроизведения текущего медиаконтента, а также панель перемотки, которая сопоставлена с MediaSession PlaybackState. Панель перемотки позволяет пользователям менять позицию воспроизведения и показывает, сколько времени прошло с начала воспроизведения мультимедийного объекта. Чтобы включить панель перемотки, необходимо реализовать PlaybackState.Builder#setActions и добавить ACTION_SEEK_TO.
| Слот | Действие | Критерии |
|---|---|---|
| 1 | Воспроизвести |
Текущее состояние PlaybackState может быть одним из следующих:
|
| Индикатор выполнения |
Текущее состояние PlaybackState может быть одним из следующих:
|
|
| Приостановить | Текущее состояние PlaybackState не соответствует ни одному из перечисленных выше. |
|
| 2 | Назад | PlaybackState действий, в том числе ACTION_SKIP_TO_PREVIOUS. |
| Свой вариант | PlaybackState действий не включают ACTION_SKIP_TO_PREVIOUS, а PlaybackState настраиваемых действий включают настраиваемое действие, которое ещё не размещено. |
|
| Пусто | PlaybackState extras включает логическое значение true для ключа SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV. |
|
| 3 | Далее | PlaybackState действий, в том числе ACTION_SKIP_TO_NEXT. |
| Свой вариант | PlaybackState действий не включают ACTION_SKIP_TO_NEXT, а PlaybackState настраиваемых действий включают настраиваемое действие, которое ещё не размещено. |
|
| Пусто | PlaybackState extras включает логическое значение true для ключа SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT. |
|
| 4 | Свой вариант | PlaybackState настраиваемое действие включает настраиваемое действие, которое ещё не было размещено. |
| 5 | Свой вариант | PlaybackState настраиваемое действие включает настраиваемое действие, которое ещё не было размещено. |
Добавление стандартных действий
В примерах кода ниже показано, как добавить стандартные и настраиваемые действия PlaybackState.
Для воспроизведения, паузы, перехода к предыдущему и следующему треку задайте эти действия в PlaybackState для сеанса воспроизведения.
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);
Если вы не хотите добавлять кнопки в слоты для предыдущего и следующего треков, не добавляйте ACTION_SKIP_TO_PREVIOUS и ACTION_SKIP_TO_NEXT. Вместо этого добавьте в сеанс дополнительные кнопки:
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);
Добавить настраиваемые действия
Для других действий, которые вы хотите показывать в элементах управления мультимедиа, можно создать PlaybackStateCompat.CustomAction и добавить его в PlaybackState. Эти действия показаны в порядке их добавления.
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);
Реагирование на действия PlaybackState
Когда пользователь нажимает кнопку, SystemUI использует MediaController.TransportControls, чтобы отправить команду обратно в MediaSession. Вам нужно зарегистрировать обратный вызов, который будет правильно реагировать на эти события.
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); } } };
Возобновление воспроизведения медиаконтента
Чтобы ваше приложение появилось в быстрых настройках, создайте уведомление MediaStyle с действительным токеном MediaSession.
Чтобы показать заголовок уведомления MediaStyle, используйте NotificationBuilder.setContentTitle().
Чтобы показать значок бренда в медиапроигрывателе, используйте NotificationBuilder.setSmallIcon().
Чтобы поддерживать возобновление воспроизведения, приложения должны реализовывать MediaBrowserService и MediaSession. Ваш класс MediaSession должен реализовывать обратный вызов onPlay().
Реализация MediaBrowserService
После загрузки устройства система ищет пять последних использованных мультимедийных приложений и предоставляет элементы управления, которые можно использовать для перезапуска воспроизведения из каждого приложения.
Система пытается связаться с вашим устройством MediaBrowserService, используя подключение из SystemUI. Ваше приложение должно разрешать такие подключения, иначе оно не сможет поддерживать возобновление воспроизведения.
Подключения из SystemUI можно идентифицировать и проверить с помощью названия пакета com.android.systemui и подписи. SystemUI подписан подписью платформы. Пример проверки подписи платформы можно найти в приложении UAMP.
Чтобы поддерживать возобновление воспроизведения, ваше приложение MediaBrowserService должно выполнять следующие действия:
onGetRoot()должен быстро возвращать ненулевой корень. Другие сложные логические операции должны обрабатываться вonLoadChildren().Когда для корневого идентификатора мультимедийного объекта вызывается метод
onLoadChildren(), результат должен содержать дочерний элемент FLAG_PLAYABLE.MediaBrowserServiceдолжен возвращать последний воспроизведенный медиаобъект, когда получает запрос EXTRA_RECENT. Возвращаемое значение должно быть фактическим мультимедийным объектом, а не общей функцией.MediaBrowserServiceдолжен предоставлять подходящий объект MediaDescription с непустыми полями title и subtitle. Также необходимо задать URI значка или растровое изображение значка.
В примерах кода ниже показано, как реализовать 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); }
Поведение в версиях до Android 13
Для обратной совместимости системный интерфейс по-прежнему предоставляет альтернативный макет, в котором используются действия уведомлений для приложений, не обновленных до Android 13 или не содержащих информацию PlaybackState. Командные кнопки создаются на основе списка Notification.Action, прикрепленного к уведомлению MediaStyle. Система показывает до пяти действий в том порядке, в котором они были добавлены. В компактном режиме показывается до трех кнопок, которые определяются значениями, переданными в setShowActionsInCompactView().
Настраиваемые действия размещаются в том порядке, в котором они были добавлены в PlaybackState.
В следующем примере кода показано, как добавить действия в уведомление 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) // 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(new MediaStyleNotificationHelper.MediaStyle(mediaSession) .setShowActionsInCompactView(1 /* #1: pause button */)) .setContentTitle("Wonderful music") .setContentText("My Awesome Band") .setLargeIcon(albumArtBitmap) .build();