Android API Guide for Media Apps(四)—— 构建媒体浏览器服务(Building a Media Browser Service)

构建媒体浏览器服务(Building a Media Browser Service)

你的应用必须在清单文件中使用一个intent-filter声明MediaBrowserService 。你可以选择自己的服务名字,在下面的例子中,它叫“MediaPlaybackService”。

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

Note:推荐使用media-compat support library的MediaBrowserServiceCompat来实现MediaBrowserService 。本文的MediaBrowserService始终引用MediaBrowserServiceCompat。

初始化媒体会话(Initialize the media session)

当service收到到生命周期回调方法onCreate()的回调时,它应该实现这些步骤:

  • 创建并初始化媒体会话。

  • 设置媒体会话回调。

  • 设置MediaButtonReceiver

  • 设置媒体会话标记(media session token)

下面onCreate()的示例代码展示这些步骤:

public class MediaPlaybackService extends MediaBrowserServiceCompat {
  private MediaSessionCompat mMediaSession;
  private PlaybackStateCompat.Builder mStateBuilder;

  @Override
  public void onCreate() {
    super.onCreate();

    // Create a MediaSessionCompat
    mMediaSession = new MediaSessionCompat(context, LOG_TAG);

    // Enable callbacks from MediaButtons and TransportControls
    mMediaSession.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
    mStateBuilder = new PlaybackStateCompat.Builder()
                    .setActions(
                        PlaybackStateCompat.ACTION_PLAY |
                        PlaybackStateCompat.ACTION_PLAY_PAUSE);
    mMediaSession.setState(mStateBuilder.build());

    // MySessionCallback() has methods that handle callbacks from a media controller
    mMediaSession.setCallback(new MySessionCallback());

    // For Android 5.0 (API version 21) or greater
    // To enable restarting an inactive session in the background,
    // You must create a pending intent and setMediaButtonReceiver.
    Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);

    mediaButtonIntent.setClass(context, MediaPlaybackService.class);
    PendingIntent mbrIntent =
      PendingIntent.getService(context, 0, mediaButtonIntent, 0);

    mMediaSession.setMediaButtonReceiver(mbrIntent);

    // Set the session's token so that client activities can communicate with it.
    setSessionToken(mMediaSession.getToken());
  }
}

当你的会话在后台处于非活动状态时,你可能需要使用媒体按钮来开启它。在Android 5.0(API 21)或更高版本,你必须如同sample code一样创建一个PendingIntent 和一个MediaButtonReceiver 。参看 Using media buttons to restart an inactive media session获取更多关于媒体按钮的信息。

管理客户端的连接(Manage client connections)

MediaBrowserService 有两个方法处理客户端连接:onGetRoot()控制访问服务,onLoadChildren()为客户端提供一个构建和显示MediaBrowserService内容层次结构的菜单。

使用onGetRoot()控制客户端的连接(Controlling client connections with onGetRoot())

onGetRoot()方法返回内容层次结构的根节点。如果方法返回null,则表示连接失败。

为了让所有用户连接你的服务和浏览媒体内容,onGetRoot()应该返回一个不为null且有一个根id的BrowserRoot。如果只是让用户连接而不需要浏览内容,则要返回一个根ID为null的非空BrowserRoot。

通常onGetRoot()代码的实现如下:

@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, so clients can use onLoadChildren() to retrieve the content hierarchy
      return new BrowserRoot(MY_MEDIA_ROOT_ID, null);
    }
   else {
      // Returns an empty root, so clients can connect, but no content browsing possible
      return new BrowserRoot(null, null);
    }
}

在一些情况,你可能想实现一个白名单/黑名单来控制连接,如白名单,参看 Universal Android Music Player示例应用的 PackageValidator类。

Note:你应该考虑根据用户的类型来做查询并提供不同的内容层次结构。尤其是,Android Auto限制了用户与音频应用的交互。获取更多信息,参看 Playing Audio for Auto。你可以查看clientPackageName 并在连接期间决定客户的类型,然后根据用户(或任何rootHints)返回一个不同的BrowserRoot 。

使用onLoadChildren()进行内容通信(Communicating content with onLoadChildren())

客户端连接后,它可以通过重复调用MediaBrowserCompat.subscribe() 遍历内容层次结构来构建一个本地的UI界面。subscribe() 方法发送一个onLoadChildren()回调到service中,然后返回一个MediaBrowser.MediaItem 对象列表。

每个MediaItem 都有唯一的ID字符串,它其实是一个隐式的token。当客户想打开子菜单或播放一个item时,它就将ID传入。然后service使用适当的菜单节点和内容item来响应关联的ID。

onLoadChildren()的简单实现一般如下:

@Override
public void onLoadChildren(final String parentMediaId,
    final Result<List<MediaItem>> result) {

  //  Browsing not allowed
  if (parentMediaId == null) {
   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);
}

Note:通过MediaBrowserService 传递的MediaItem 对象不应该包含icon位图。当你为每个item构建一个 MediaDescription时通过调用 setIconUri()使用Uri替代。

如何实现onLoadChildren(),参看 MediaBrowserService和 Universal Android Music Player的应用示例。

媒体浏览器服务的生命周期(The media browser service lifecycle)

Android service的表现行为取决于它是否通过普通启动或绑定到一个或多个客户端。在创建一个服务后,它可以通过普通开启、绑定或两者结合。在这些状态中,它都能完整地实现所有它所需要实现的工作。不同的地方就是服务存活的时间。绑定的服务在客户端解绑之前是不会被销毁的。普通开启的服务可以显式地停止并销毁(假设被显式销毁的服务没有再绑定到其它用户上)。

当运行在另外一个activity中的MediaBrowser连接MediaBrowserService时,它将activity与服务进行了绑定(而不是通过开启的方式)。默认的行为被构建到MediaBrowserServiceCompat 类中。

一个绑定的服务(不是通过普通的启动方式)只有在所有客户端解绑后才能被销毁,如果你的UI activity在这个时候断开连接,服务就被销毁。如果你还没有播放任何音乐,这就不是一个问题。但是,当你已经开始播放,用户可能希望一直能听到音乐即使切换了应用。当你解绑UI并使用另外一个应用工作时,你不应该销毁播放器。

由于这个原因,你需要确保服务被开启当它在调用startService()开始播放音乐时。一个普通开启的服务必须显式地停止,不管它是否被绑定。这才能确保你的播放器能持续实现播放,即使UI与你的activity解绑了。

为了停止被开启的服务,可以调用Context.stopServce()或stopSelf()。系统会尽可能快地停止和销毁服务。但是,如果一个或多个客户端仍然绑定着服务,调用停止服务将会延迟直到所有的客户端解绑。

MediaBrowserService的生命周期是由它的创建方式,客户端的数量以及它所接收的媒体会话的回调函数控制。总结:

  • 当通过响应媒体按钮来开始或一个activity的绑定时(通过它的MediaBrowser连接),服务就被创建。

  • 媒体会话onPlay()的回调应该包含调用startService()的代码。确保服务开启并持续运行,即使在所有的UI MediaBrowser activity从绑定状态变成解绑状态。

  • onStop()的回调应该调用stopSelf()。如有存在开启的服务,通过这个方法停止它。另外,如果没有activity绑定服务,那么服务会被销毁。否则,服务将一直处于绑定状态直到所有的activity解绑(如果在服务被销毁之前,有startService()的调用,那么待定的停止操作将被取消)。

下面的图文示例service生命周期的管理。变量计数器跟踪绑定的客户端数目:

这里写图片描述

通过前台服务使用MediaStyle提示(Using MediaStyle notification with a foreground service)

当service正在播放音乐时,它应该运行在前台。通知系统service正在实现一个有用的功能,如果系统在一个低内存的环境下也不应该被杀掉。一个前台服务必须显示一个notification以便用户知道关于服务的信息并选择性控制它。onPlay()的回调应该将服务置于前台。(请注意,”前台”是一个特殊的含义,对于Android系统,它认为“前台”服务的目的就是为了管理进程的;对于用户来说,“前台”就是一些其它的app在屏幕上可视化时界面,而播放器在后台正在播放)

当一个服务运行在前台,它必须显示一个notification,理论是需要一个或多个传送控件的。这个提示应该包含媒体会话元数据的信息。

在播放器开始播放时,构建并显示这个提示。处理这点最好的地方就是在MediaSessionCompat.Callback.onPlay() 的方法中。

下面的示例使用 NotificationCompat.MediaStyle来设计媒体app。它展示如何构建一个显示元数据和传送控件的提示。getController()方法可以让你直接在媒体会话中创建一个媒体控制器。

// 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);

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(this,
       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(this, R.color.primaryDark))

// Add a pause button
      .addAction(new NotificationCompat.Action(
          R.drawable.pause, getString(R.string.pause),
          MediaButtonReceiver.buildMediaButtonPendingIntent(this,
              PlaybackStateCompat.ACTION_PLAY_PAUSE)))

// Take advantage of MediaStyle features
    .setStyle(new NotificationCompat.MediaStyle()
      .setMediaSession(mediaSession.getSessionToken())
      .setShowActionsInCompactView(0)
// Add a cancel button
      .setShowCancelButton(true)
      .setCancelButtonIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(this,
          PlaybackStateCompat.ACTION_STOP));

// Display the notification and place the service in the foreground
startForeground(id, builder.build());

在使用MediaStyle提示时,注意NotificationCompat 表现行为的设置:

  • 当你使用setContentIntent(),service将在提示被点击的时候自动开启。一个非常便利的功能。

  • 在一个“不可信”状态如锁屏,默认可视化的notification内容是 VISIBILITY_PRIVATE 。你可能想查看锁屏时看到传输控件,所以 VISIBILITY_PUBLIC 是一个不错的方式。

  • 当你设置背景颜色时要注意。一个原始的notification在Android版本 5.0 或更高环境下,颜色只适用于小应用程序图标的背景。但是MediaStyle notifications在Android 7.0之前,颜色用于整个notification的背景。

  • 只有在使用NotificationCompat.MediaStyle时,下面的设置才可以用:

  • 使用 setMediaSession() 来关联让你的session关联notification。这个让第三方应用和衍生设备可以访问并控制你的session。

  • 使用 setShowActionsInCompactView() 添加3个事件来展示notification中的标准尺寸内容视图(这里有个特殊的暂停按钮)。

  • 在Android 5.0(API 级别21)或更高版本,一旦服务不再运行在前台,你可以清除notification来停止播放器。你不能在早期的版本这么做。为了让用户在Android 5.0之前(API 21)移除notification并停止播放,你可以在notification的右上角添加一个取消按钮并通过调用 setShowCancelButton(true) 和setCancelButtonIntent()。

当你添加暂停和取消按钮时,你需要一个PendingIntent添加到播放事件上。MediaButtonReceiver.buildMediaButtonPendingIntent() 可以将一个PlaybackState 事件转化为PendingIntent。

猜你喜欢

转载自blog.csdn.net/u014011112/article/details/54889467