メディア ブラウザ サービスの構築

アプリでは、マニフェスト内の 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() は、クライアントが MediaBrowserService のコンテンツ階層のメニューを作成して表示する機能を提供します。

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 の実装方法の例については、Universal Android Music Player サンプルアプリの PackageValidator クラスをご覧ください。

クエリを実行しているクライアントの種類に応じて、異なるコンテンツ階層の指定を検討する必要があります。特に、Android 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 オブジェクトには、アイコンのビットマップを含めることはできません。代わりに Uri を使用します。そのためには、各アイテムの MediaDescription を作成するときに setIconUri() を呼び出します。

onLoadChildren() を実装する方法の例については、Universal Android Music Player サンプルアプリをご覧ください。

メディア ブラウザ サービスのライフサイクル

Android サービスの動作は、「開始」されたのか、1 つ以上のクライアントに「バインド」されたのかによって異なります。サービスを作成したら、それを開始することも、バインドすることも、その両方を行うことも可能です。いずれの状態でも、サービスは完全に機能し、設計されたタスクの実施が可能です。違いは、サービスの存続期間です。バインドされたサービスは、すべてのバインド相手のクライアントからバインド解除されるまで破棄されません。開始されたサービスは、明示的に停止と破棄を行えます(どのクライアントにもバインドされていない場合)。

別のアクティビティで実行されている MediaBrowser から MediaBrowserService への接続がなされると、そのアクティビティがサービスにバインドされ、サービスがバインドされた(開始はされていない)状態になります。このデフォルトの動作は、MediaBrowserServiceCompat クラスに組み込まれています。

すべてのクライアントでバインドが解除されると、バインドのみされた(開始はされていない)状態のサービスは破棄されます。この時点で UI アクティビティが切断されると、サービスは破棄されます。これは、まだ音楽が再生されていない場合は問題ではありません。ただし、再生が開始されていたら、おそらくユーザーはアプリ切り替え後も聴き続けられることを期待しています。よって、別のアプリでの作業のために UI のバインドを解除しても、プレーヤーは破棄しないほうがよいでしょう。

このため、startService() を呼び出して、再生開始時にサービスが開始されるようにする必要があります。開始されたサービスは、バインドされているかどうかにかかわらず、明示的に停止する必要があります。これにより、制御 UI アクティビティのバインドが解除されても、プレーヤーは実行し続けることができます。

開始状態のサービスを停止するには、Context.stopService() または stopSelf() を呼び出します。これにより、サービスはシステムによってできるだけ早く停止され、破棄されます。ただし、まだサービスにバインドしているクライアントがある場合は、そのすべてのバインドが解除されるまで、サービス停止の呼び出しは遅延します。

MediaBrowserService のライフサイクルは、その作成方法、バインドされているクライアントの数、メディア セッション コールバックから受ける呼び出しによって制御されます。まとめ

  • サービスは、メディアボタンへの応答として開始されたとき、またはアクティビティに(MediaBrowser を介しての接続後に)バインドされたときに作成されます。
  • メディア セッションの onPlay() コールバックには、startService() を呼び出すコードを含める必要があります。これにより、サービスが開始状態となり、すべての UI MediaBrowser アクティビティとのバインドが解除された場合でも実行が継続されます。
  • 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 に変換する作業を行います。