Ваше приложение должно объявить MediaBrowserService
с фильтром намерений в своем манифесте. Вы можете выбрать собственное имя службы; в следующем примере это «MediaPlaybackService».
<service android:name=".MediaPlaybackService">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
Примечание. Рекомендуемая реализация MediaBrowserService
— MediaBrowserServiceCompat
. который определен в библиотеке поддержки медиа-совместимости . На этой странице термин «MediaBrowserService» относится к экземпляру MediaBrowserServiceCompat
.
Инициализировать медиа-сессию
Когда служба получает метод обратного вызова жизненного цикла onCreate()
она должна выполнить следующие шаги:
- Создайте и инициализируйте медиа-сессию
- Установите обратный вызов медиа-сеанса
- Установите токен медиа-сеанса
Код onCreate()
ниже демонстрирует эти шаги:
Котлин
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) } } }
Ява
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()); } }
Управление клиентскими подключениями
MediaBrowserService
имеет два метода, которые обрабатывают клиентские соединения: onGetRoot()
управляет доступом к службе, а onLoadChildren()
предоставляет клиенту возможность создавать и отображать меню иерархии контента MediaBrowserService
.
Управление клиентскими соединениями с помощью onGetRoot()
Метод onGetRoot()
возвращает корневой узел иерархии контента. Если метод возвращает значение null, в соединении отказано.
Чтобы клиенты могли подключаться к вашей службе и просматривать ее мультимедийный контент, onGetRoot() должен возвращать ненулевой BrowserRoot, который является корневым идентификатором, представляющим вашу иерархию контента.
Чтобы клиенты могли подключаться к вашему MediaSession без просмотра, onGetRoot() по-прежнему должен возвращать ненулевой BrowserRoot, но корневой идентификатор должен представлять пустую иерархию контента.
Типичная реализация onGetRoot()
может выглядеть так:
Котлин
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) } }
Ява
@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); } }
В некоторых случаях вам может потребоваться контролировать, кто может подключаться к вашему MediaBrowserService
. Один из способов — использовать список управления доступом (ACL), который определяет, какие соединения разрешены, или, альтернативно, перечисляет, какие соединения следует запретить. Пример реализации ACL, разрешающего определенные подключения, см. в классе PackageValidator в примере приложения Universal Android Music Player .
Вам следует рассмотреть возможность предоставления различных иерархий контента в зависимости от типа клиента, отправляющего запрос. В частности, Android Auto ограничивает взаимодействие пользователей с аудиоприложениями. Дополнительную информацию см. в разделе «Воспроизведение аудио в режиме Авто» . Вы можете просмотреть clientPackageName
во время подключения, чтобы определить тип клиента, и вернуть другой BrowserRoot
в зависимости от клиента (или rootHints
, если таковые имеются).
Передача контента с помощью onLoadChildren()
После подключения клиента он может перемещаться по иерархии контента, повторяя вызовы MediaBrowserCompat.subscribe()
для создания локального представления пользовательского интерфейса. Метод subscribe()
отправляет обратный вызов onLoadChildren()
в службу, которая возвращает список объектов MediaBrowser.MediaItem
.
Каждый MediaItem имеет уникальную строку идентификатора, которая является непрозрачным токеном. Когда клиент хочет открыть подменю или воспроизвести элемент, он передает идентификатор. Ваша служба несет ответственность за привязку идентификатора к соответствующему узлу меню или элементу контента.
Простая реализация onLoadChildren()
может выглядеть так:
Котлин
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) }
Ява
@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); }
Примечание. Объекты MediaItem
, доставляемые MediaBrowserService, не должны содержать растровые изображения значков. Вместо этого используйте Uri
, вызывая setIconUri()
при создании MediaDescription
для каждого элемента.
Пример реализации onLoadChildren()
см. в примере приложения Universal Android Music Player .
Жизненный цикл службы медиабраузера
Поведение службы Android зависит от того, запущена ли она или привязана к одному или нескольким клиентам. После создания службы ее можно запустить, привязать или и то, и другое. Во всех этих состояниях он полностью функционален и может выполнять работу, для которой предназначен. Разница в том, как долго будет существовать услуга. Привязанная служба не уничтожается до тех пор, пока все ее привязанные клиенты не отсоединятся. Запущенную службу можно явно остановить и уничтожить (при условии, что она больше не привязана ни к какому клиенту).
Когда MediaBrowser
, работающий в другом действии, подключается к MediaBrowserService
, он привязывает действие к службе, делая службу привязанной (но не запускаемой). Это поведение по умолчанию встроено в класс MediaBrowserServiceCompat
.
Служба, которая только привязана (но не запущена), уничтожается, когда все ее клиенты отвязываются. Если на этом этапе ваша активность пользовательского интерфейса отключится, служба будет уничтожена. Это не проблема, если вы еще не проигрывали музыку. Однако когда воспроизведение начинается, пользователь, вероятно, ожидает продолжения прослушивания даже после переключения приложений. Вы же не хотите уничтожать плеер, когда отвязываете пользовательский интерфейс для работы с другим приложением.
По этой причине вам необходимо быть уверенным, что служба запускается, когда она начинает воспроизводиться, путем вызова startService()
. Запущенная служба должна быть явно остановлена независимо от того, привязана она или нет. Это гарантирует, что ваш проигрыватель продолжит работать, даже если действие управляющего пользовательского интерфейса будет отменено.
Чтобы остановить запущенную службу, вызовите Context.stopService()
или stopSelf()
. Система останавливает и уничтожает сервис как можно скорее. Однако если один или несколько клиентов по-прежнему привязаны к службе, вызов остановки службы откладывается до тех пор, пока все ее клиенты не отсоединятся.
Жизненный цикл MediaBrowserService
контролируется способом его создания, количеством привязанных к нему клиентов и вызовами, которые он получает от обратных вызовов сеанса мультимедиа. Подводя итог:
- Служба создается, когда она запускается в ответ на медиа-кнопку или когда к ней привязывается действие (после подключения через
MediaBrowser
). - Обратный вызов
onPlay()
медиа-сеанса должен включать код, вызывающийstartService()
. Это гарантирует, что служба запустится и продолжит работу, даже если все привязанные к ней действия пользовательского интерфейсаMediaBrowser
отменяются. - Обратный вызов
onStop()
должен вызыватьstopSelf()
. Если служба была запущена, это останавливает ее. Кроме того, служба уничтожается, если с ней не связаны никакие действия. В противном случае служба остается привязанной до тех пор, пока все ее действия не будут отменены. (Если последующий вызовstartService()
получен до уничтожения службы, ожидающая остановка отменяется.)
Следующая блок-схема демонстрирует, как управляется жизненный цикл службы. Переменная counter отслеживает количество привязанных клиентов:
Использование уведомлений MediaStyle со службой переднего плана
Когда служба работает, она должна работать на переднем плане. Это позволяет системе узнать, что служба выполняет полезную функцию и ее не следует отключать, если в системе недостаточно памяти. Служба переднего плана должна отображать уведомление, чтобы пользователь знал об этом и мог при необходимости управлять им. Обратный вызов onPlay()
должен перевести службу на передний план. (Обратите внимание, что это особое значение слова «передний план». Хотя Android рассматривает службу на переднем плане для целей управления процессами, для пользователя плеер играет в фоновом режиме, в то время как какое-то другое приложение видно на «переднем плане» на экран.)
Когда служба работает на переднем плане, она должна отображать уведомление , в идеале с одним или несколькими элементами управления транспортом. Уведомление также должно включать полезную информацию из метаданных сеанса.
Создайте и отобразите уведомление, когда игрок начинает играть. Лучшее место для этого — внутри метода MediaSessionCompat.Callback.onPlay()
.
В приведенном ниже примере используется NotificationCompat.MediaStyle
, предназначенный для мультимедийных приложений. В нем показано, как создать уведомление, отображающее метаданные и элементы управления транспортом. Удобный метод getController()
позволяет вам создать медиа-контроллер непосредственно из вашего медиа-сеанса.
Котлин
// 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())
Ява
// 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());
При использовании уведомлений MediaStyle обратите внимание на поведение этих параметров NotificationCompat:
- Когда вы используете
setContentIntent()
, ваша служба запускается автоматически при нажатии на уведомление, что является удобной функцией. - В «ненадежной» ситуации, такой как экран блокировки, видимость содержимого уведомления по умолчанию —
VISIBILITY_PRIVATE
. Вероятно, вы хотите видеть элементы управления транспортом на экране блокировки, поэтомуVISIBILITY_PUBLIC
— это то, что вам нужно. - Будьте осторожны при установке цвета фона. В обычном уведомлении в Android версии 5.0 или новее цвет применяется только к фону небольшого значка приложения. Но для уведомлений MediaStyle до Android 7.0 цвет используется для всего фона уведомления. Проверьте цвет фона. Будьте осторожны с глазами и избегайте слишком ярких или флуоресцентных цветов.
Эти параметры доступны только при использовании NotificationCompat.MediaStyle:
- Используйте
setMediaSession()
, чтобы связать уведомление с вашим сеансом. Это позволяет сторонним приложениям и сопутствующим устройствам получать доступ к сеансу и управлять им. - Используйте
setShowActionsInCompactView()
чтобы добавить до трех действий, которые будут отображаться в ContentView стандартного размера уведомления. (Здесь указана кнопка паузы.) - В Android 5.0 (уровень API 21) и более поздних версиях вы можете смахнуть уведомление, чтобы остановить проигрыватель, как только служба перестанет работать на переднем плане. В более ранних версиях это сделать невозможно. Чтобы разрешить пользователям удалять уведомление и останавливать воспроизведение до Android 5.0 (уровень API 21), вы можете добавить кнопку отмены в правом верхнем углу уведомления, вызвав
setShowCancelButton(true)
иsetCancelButtonIntent()
.
Когда вы добавите кнопки паузы и отмены, вам понадобится PendingIntent для присоединения к действию воспроизведения. Метод MediaButtonReceiver.buildMediaButtonPendingIntent()
выполняет преобразование действия PlaybackState в PendingIntent.