Создание службы медиабраузера

Ваше приложение должно объявить MediaBrowserService с фильтром намерений в своем манифесте. Вы можете выбрать собственное имя службы; в следующем примере это «MediaPlaybackService».

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

Примечание. Рекомендуемая реализация MediaBrowserServiceMediaBrowserServiceCompat . который определен в библиотеке поддержки медиа-совместимости . На этой странице термин «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 отслеживает количество привязанных клиентов:

Service Lifecycle

Использование уведомлений 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.