アプリでは、マニフェスト内の intent-filter で MediaBrowserService を宣言する必要があります。サービス名には任意の名前を付けることができます。次の例では、サービス名として MediaPlaybackService を選択しています。
<service android:name=".MediaPlaybackService">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
メディア セッションの初期化
サービスで 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() と、クライアントで MediaBrowserService のコンテンツ階層のメニューを作成および表示できるようにする 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 hierarchy // 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 hierarchy // so onLoadChildren returns nothing. This disables the ability to browse for content. return new BrowserRoot(MY_EMPTY_MEDIA_ROOT_ID, null); } }
MediaBrowserService に接続できるユーザーを制御したい場合があります。1 つの方法として、許可する接続を指定するアクセス制御リスト(ACL)を使用する方法があります。また、禁止する接続を列挙することもできます。特定の接続を許可する ACL を実装する方法の例については、PackageValidator クラスを
Universal Android Music Player サンプルアプリでご覧ください。
クエリを実行しているクライアントのタイプに応じて、異なるコンテンツ階層を提供することを検討してください。特に、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); }
注: MediaItem MediaBrowserService で配信されるオブジェクトには、アイコン ビットマップを含めるべきではありません。代わりに Uri を使用するには、各アイテムの MediaDescription を作成するときに
setIconUri()
を呼び出します。
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 に変換できます。
AVRCP メディア ブラウジングを有効にする
Android Auto などのカスタムアプリに加えて、システムの Bluetooth レイヤも MediaBrowserService のクライアントとして機能し、ワイヤレス リモート カタログ ブラウジング(AVRCP)を容易にします。
Android 16 と Android 17 では、Media3 を使用していないアプリは、ブラウジング用に検証されるインテント フィルタを使用して特定のアクティビティを公開する必要があります。
AndroidManifest.xml のエクスポートされたアクティビティに、この特定のインテント フィルタを追加します。CATEGORY_DEFAULT は、ローカル オーディオ ファイルの汎用的な [アプリで開く] メニューにアプリが表示されないように、意図的に省略されています。
<activity
android:name=".BluetoothValidationActivity"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay"
android:excludeFromRecents="true"
android:noHistory="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="content" />
<data android:host="media" />
<!-- Specific path check used by Bluetooth stack for validation -->
<data android:pathPrefix="/internal/audio/media/" />
<data android:mimeType="audio/*" />
</intent-filter>
</activity>