构建媒体浏览器服务(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。