アプリでは、マニフェスト内の intent-filter で MediaBrowserService
を宣言する必要があります。サービス名は独自に指定できます。次の例では「MediaPlaybackService」です。
<service android:name=".MediaPlaybackService">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
注: MediaBrowserService
の推奨の実装は MediaBrowserServiceCompat
です。これは、media-compat サポート ライブラリで定義されています。このページ全体を通して、「MediaBrowserService」という用語は MediaBrowserServiceCompat
のインスタンスを指します。
メディア セッションの初期化
サービスで onCreate()
ライフサイクル コールバック メソッドを受信したら、以下の手順を行います。
- メディア セッションを作成して初期化します。
- メディア セッション コールバックを設定します。
- メディア セッション トークンを設定します。
次の onCreate()
コードは、この手順を示しています。
Kotlin
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) } } }
Java
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
には、クライアント接続を処理する 2 つのメソッドがあります。このサービスへのアクセスを制御する onGetRoot()
と、クライアントで のコンテンツ階層のメニューを作成および表示できるようにする onLoadChildren()
です。
onGetRoot()
を使用したクライアント接続の制御
onGetRoot()
メソッドからは、コンテンツ階層のルートノードを返します。メソッドから null を返すと、接続は拒否されます。
クライアントからサービスに接続してメディア コンテンツをブラウジングできるようにするには、onGetRoot()で null 以外の BrowserRoot(コンテンツ階層を表すルート ID)を返す必要があります。
クライアントから MediaSession に接続でき、ブラウジングはできないようにするには、onGetRoot()で null 以外の BrowserRoot を返す必要がありますが、ルート ID は空のコンテンツ階層を表すようにします。
onGetRoot()
の典型的な実装は次のようになります。
Kotlin
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 hierachy // so onLoadChildren returns nothing. This disables the ability to browse for content. MediaBrowserServiceCompat.BrowserRoot(MY_EMPTY_MEDIA_ROOT_ID, null) } }
Java
@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 hierachy // so onLoadChildren returns nothing. This disables the ability to browse for content. return new BrowserRoot(MY_EMPTY_MEDIA_ROOT_ID, null); } }
必要に応じて、接続を制御するためにホワイトリスト(またはブラックリスト)スキームを実装します。ホワイトリストの例については、Universal Android Music Player サンプルアプリの PackageValidator クラスをご覧ください。
クエリ元のクライアントのタイプに応じて異なるコンテンツ階層を提供することを検討します。特に、Android Auto では、ユーザーがオーディオ アプリを操作する方法が制限されます。詳細については、Auto 用の音声の再生をご覧ください。接続時に clientPackageName
を調べてクライアントのタイプを判別することで、クライアント(または、ある場合は rootHints
)に応じて異なる BrowserRoot
を返すことができます。
onLoadChildren()
を使用したコンテンツとの通信
接続後のクライアントでは、MediaBrowserCompat.subscribe()
を繰り返し呼び出して UI のローカル表現を作成することにより、コンテンツ階層を走査できます。この subscribe()
メソッドからは onLoadChildren()
コールバックがサービスに送信され、MediaBrowser.MediaItem
オブジェクトのリストが返されます。
各 MediaItem には、不透明なトークンである固有の ID 文字列があります。クライアントがサブメニューを開いたり、アイテムを再生したりする際には、その ID が渡されます。サービスは、ID を適切なメニューノードまたはコンテンツ アイテムに関連付ける役割を担います。
onLoadChildren()
の簡単な実装例を以下に示します。
Kotlin
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) }
Java
@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); }
注: MediaBrowserService で配信される MediaItem
オブジェクトには、アイコン ビットマップを含めるべきではありません。各アイテムの MediaDescription
を作成するときに setIconUri()
を呼び出して、代わりに Uri
を使用します。
onLoadChildren()
を実装する方法の例については、Universal Android Music Player サンプルアプリをご覧ください。
メディア ブラウザ サービスのライフサイクル
Android サービスの動作は、「開始」されたのか、1 つ以上のクライアントに「バインド」されたのかによって異なります。サービスを作成したら、それを開始することも、バインドすることも、その両方を行うことも可能です。いずれの状態でも、サービスは完全に機能し、設計されたタスクの実施が可能です。違いは、サービスの存続期間です。バインドされたサービスは、すべてのバインド相手のクライアントからバインド解除されるまで破棄されません。開始されたサービスは、明示的に停止と破棄を行えます(どのクライアントにもバインドされていない場合)。
別のアクティビティで実行されている MediaBrowser
から MediaBrowserService
への接続がなされると、そのアクティビティがサービスにバインドされ、サービスがバインドされた(開始はされていない)状態になります。このデフォルトの動作は、MediaBrowserServiceCompat
クラスに組み込まれています。
すべてのクライアントでバインドが解除されると、バインドのみされた(開始はされていない)状態のサービスは破棄されます。この時点で UI アクティビティが切断されると、サービスは破棄されます。これは、まだ音楽が再生されていない場合は問題ではありません。ただし、再生が開始されていたら、おそらくユーザーはアプリ切り替え後も聴き続けられることを期待しています。よって、別のアプリでの作業のために UI のバインドを解除しても、プレーヤーは破棄しないほうがよいでしょう。
そのためには、サービスで再生を開始するときに、startService()
を呼び出してサービスを開始状態にする必要があります。開始状態のサービスは、バインドされているかどうかにかかわらず、明示的に指示しない限り停止しません。そのため、制御元の UI アクティビティでバインドが解除されても、プレーヤーは継続して実行されます。
開始状態のサービスを停止するには、Context.stopService()
または stopSelf()
を呼び出します。これにより、サービスはシステムによってできるだけ早く停止され、破棄されます。ただし、まだサービスにバインドしているクライアントがある場合は、そのすべてのバインドが解除されるまで、サービス停止の呼び出しは遅延します。
MediaBrowserService
のライフサイクルは、その作成方法、バインドされているクライアントの数、メディア セッション コールバックから受ける呼び出しによって制御されます。以下にまとめます。
- サービスは、メディアボタンへの応答として開始されたとき、またはアクティビティに(
MediaBrowser
を介しての接続後に)バインドされたときに作成されます。 - メディア セッションの
onPlay()
コールバックには、startService()
を呼び出すコードを含める必要があります。これにより、サービスが開始状態となり、すべての UIMediaBrowser
アクティビティとのバインドが解除された場合でも実行が継続されます。 onStop()
コールバックではstopSelf()
を呼び出します。開始状態のサービスは、これにより停止します。また、バインドされているアクティビティがない場合、サービスは破棄されます。それ以外の場合は、サービスはすべてのアクティビティのバインドが解除されるまでバインド状態のままになります(サービスが破棄される前に次のstartService()
呼び出しを受けると、保留中の停止はキャンセルされます)。
次のフローチャートは、サービスのライフサイクルがどのように管理されるかを示しています。変数 counter は、バインドされたクライアントの数を表します。
フォアグラウンド サービスによる MediaStyle 通知の使用
再生中のサービスは、フォアグラウンドで実行されるようにします。これにより、そのサービスが有用な機能を実行中であり、利用できるメモリが少ない場合でも強制終了すべきではないことを、システムに対して通知できます。フォアグラウンド サービスでは、ユーザーが状況を把握し、必要に応じて制御できるよう、通知を表示する必要があります。onPlay()
コールバックでは、サービスをフォアグラウンドに配置するようにします(ここでは「フォアグラウンド」を特別な意味で使用しています。ユーザーから見ると、再生はバックグラウンドのプレーヤーで行われており、「フォアグラウンド」にあるのは別のアプリですが、Android では、プロセス管理の観点から、このサービスはフォアグラウンドにあると見なされます)。
サービスがフォアグラウンドで実行される場合、(理想的には 1 つ以上のトランスポート コントロールを備えた)通知を表示する必要があります。通知には、セッションのメタデータからの有用な情報も含めるようにします。
通知を作成して表示するのは、プレーヤーで再生を開始するときです。これを行うのに最適な場所は、MediaSessionCompat.Callback.onPlay()
メソッド内です。
下記の例では、メディアアプリ用に設計された NotificationCompat.MediaStyle
を使用して、メタデータとトランスポート コントロールを表示する通知の作成方法を示しています。getController()
という便利なメソッドを使用すると、メディア セッションからメディア コントローラを直接作成できます。
Kotlin
// 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())
Java
// 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 以降の通常の通知では、この色は小アプリアイコンの背景にのみ適用されます。これに対し、Android 7.0 より前の MediaStyle 通知では、この色は通知の背景全体に適用されます。背景色はテストするようにしてください。目にやさしい色を使用し、極端に明るい色や蛍光色は避けてください。
以下の設定は、NotificationCompat.MediaStyle を使用する場合にのみ利用可能です。
setMediaSession()
を使用すると、通知をセッションに関連付けできます。これにより、サードパーティ製アプリやコンパニオン デバイスから、セッションに対するアクセスと制御ができるようになります。setShowActionsInCompactView()
を使用すると、通知の標準サイズの contentView に、最大 3 つのアクションを追加して表示できます(ここでは一時停止ボタンを指定しています)。- Android 5.0(API レベル 21)以降では、サービスがフォアグラウンドで実行されなくなったら、通知をスワイプしてプレーヤーを停止できます。これは、以前のバージョンではできません。Android 5.0(API レベル 21)より前のバージョンで、ユーザーが通知を削除して再生を停止できるようにするには、
setShowCancelButton(true)
とsetCancelButtonIntent()
を呼び出すことで、通知の右上隅にキャンセル ボタンを追加します。
一時停止ボタンやキャンセル ボタンを追加する場合は、再生アクションにアタッチするために PendingIntent が必要です。MediaButtonReceiver.buildMediaButtonPendingIntent()
メソッドにより、PlaybackState アクションを PendingIntent に変換できます。