Android には、音楽や動画などのメディアコンテンツをコントロールしたり、それらのメタデータを取り扱うための API が用意されています。特に、自分のアプリケーション以外の場所(ロックスクリーンや通知など)から、音楽や動画などのコンテンツをコントロールしたり、データを表示したりする API は、IceCreamSandwitch 以後に徐々に発展してきています。今日では Android Auto を視野にいれた MediaBrowser という仕組みで、メディアコンテンツを保持するアプリケーションとそれをコントロールするアプリケーションを分離するフレームワークが確立されました。
この記事では、ICS で登場した仕組みから、Marshmallow になった現在の仕組みを順を追って見ていき、どのように動いているのかを追っていきます。
暗黒の時代: Intent を使ったメディアコントロール
Intent
には様々なアクションが存在します。このうち、リモコン(音量の調整とクリックによる再生コントロールができるもの)の付いているヘッドフォンやヘッドセットの各種リモコン操作は、Intent.ACTION_MEDIA_BUTTON
としてブロードキャストされます。どのキーを押したかは、extra に KeyEvent
オブジェクトを見ることで判別可能です。
ブロードキャスト Intent を投げる
このブロードキャストには特に制約はないため、リモコン操作の時と同じブロードキャスト Intent を自分で生成して、自分でブロードキャストを送信するという方法が取れます。
メディアコントロールのKeyEvent
KeyEvent
には以下のようなメディアコントロールのためのキーコードが定義されています。
-
KEYCODE_MEDIA_AUDIO_TRACK
: オーディオトラックの変更。主に動画における副音声に切り替えるためのもの。 -
KEYCODE_MEDIA_CLOSE
: CD/DVDトレーを閉じる。 -
KEYCODE_MEDIA_EJECT
: CD/DVDトレーを開く。 -
KEYCODE_MEDIA_FAST_FORWARD
: 早送り -
KEYCODE_MEDIA_NEXT
: 次の曲・トラックへ送る -
KEYCODE_MEDIA_PAUSE
: 一時停止 -
KEYCODE_MEDIA_PLAY
: 再生 -
KEYCODE_MEDIA_PLAY_PAUSE
: 再生と一時停止のトグル -
KEYCODE_MEDIA_PREVIOUS
: 前の曲・トラックへ送る -
KEYCODE_MEDIA_RECORD
: 録音 -
KEYCODE_MEDIA_REWIND
: 巻き戻し -
KEYCODE_MEDIA_SKIP_BACKWARD
: ? -
KEYCODE_MEDIA_SKIP_FORWARD
: ? -
KEYCODE_MEDIA_STEP_BACKWARD
: 1フレーム戻る -
KEYCODE_MEDIA_STEP_FORWARD
: 1フレーム進む -
KEYCODE_MEDIA_STOP
: 停止 -
KEYCODE_MEDIA_TOP_MENU
: トップメニューへ戻る
必ずしも全てのアプリがこれらに対応しているわけではないことに留意する必要はありますが、少なくとも、これらのキーコードを、KeyEvent.ACTION_DOWN
とKeyEvent.ACTION_UP
と共にIntent
に放り込んで、それぞれのアクションを順にブロードキャストしてやれば、音楽アプリがそれに反応して再生がコントロールされます。
public class MediaButtonEventSender {
public void send(Context context, int keycode) {
Intent down = new Intent(Intent.ACTION_MEDIA_BUTTON);
down.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, keycode);
context.sendBroadcast(down);
Intent up = new Intent(Intent.ACTION_MEDIA_BUTTON);
up.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, keycode);
context.sendBroadcast(up);
}
}
実のところ、この方法は現在でも活用することができ、かつ最も単純で手頃に使える手法でもあります。
さて、これでコントロールは出来るようになりますが、メタデータはどのように取得したらよいでしょうか。
実は、音楽アプリはブロードキャストによってメタデータを通知してくれます。ただしこれは必ずしも実装しなければならないものでもなく、かつフォーマットが決まっているものでもないため、アプリごとにまったく異なるブロードキャストが飛んできますし、そもそも飛ばさないアプリも存在します。
領主の時代: RemoteControlClient の登場
IceCreamSandwich になり、システムが持つ標準のロックスクリーン上でメディアコントロールが出来るようになりました。メディアコンテンツを持つアプリは、RemoteControlClient
をAudioManager
に登録することで、ロックスクリーン上でのメディアコントロールを受けとることができます。
RemoteControlClient
の使い方についてはこの記事が詳しいので割愛します。
RemoteControlDisplay
ところで、ロックスクリーンでのメディアコントロールの UI はどのように作られているのでしょう。
これは、RemoteControlDisplay
という別のクラスが居ます。プロセス間通信のため aidl も定義されています。
このRemoteControlDisplay
は、文字通りメディアコントロールの表示のためのインタフェースを持っています。ICS から JB、JB_MR1、JB_MR2 と次の時代が訪れるまでの間に数回インタフェースに変更がありますが、概ね以下のようなメソッドを持っています。
-
setCurrentClientId
: 現在再生を行っているアプリが通知されます -
setEnabled
: メディアコントロールが有効かどうか通知されます -
setPlaybackState
: 再生の状態が通知されます。これを受けて、再生・一時停止の表示の切り替えが実現されます -
setTransportControlFlags
: メディアコントロールについて、実装されているものが通知されます。早送りが未実装の場合、早送りの UI は無効化されるべきということになります -
setTransportControlInfo
: 同上 -
setMetadata
: メディアのメタデータが通知されます -
setArtwork
: アルバムアートなどの画像が通知されます -
setAllMetadata
: アートワークを含めた全てのメタデータが通知されます
このRemoteControlDisplay
のメソッドは全てコールバックとして呼ばれるものです。コールバックメソッドが呼ばれたら、Handler
を経由して様々な UI への変更を加える事になります。そしてコールバックですので、AudioManager
に登録してメソッドをよんでもらう必要があります。ただし、RemoteControlDisplay
を登録するメソッドは@hide
なため、リフレクションによってアクセスする必要があります(かつRemoteControlDisplay
も通常では見えないようになっているため、自分で aidl を宣言して使えるようにしておく必要もあります)。
城主の時代: RemoteController
Kitkat になり、RemoteController
という新しい仕組みが導入されました。これは、今までのRemoteControlDisplay
を置き換えるもので、かつ幾つかRemoteControlClient
への対話もできるインタフェースが提供されます。
RemoteController.OnClientUpdatedListener
いままでのRemoteControlDisplay
に変わるものは、RemoteController.OnClientUpdatedListener
です。ほぼおなじ機能を実現するためのコールバックメソッドが定義されており、このリスナの実装をRemoteController
のコンストラクタに渡します。
NotificationListener
と共に
RemoteController
は、それ単体では動作しません。これは、先に上げたRemoteController.OnClientUpdatedListener
の実装をNotificationListenerService
に持たせなければならないことに由来しています。
実際、以下のようなクラスでのみRemoteController
はうまく動作します。
public class MyNotificationListenerService extends NotificationListenerService implements RemoteController.OnClientUpdateListener {
private AudioManager mAudioManager;
private RemoteController mRc;
@Override
public void onCreate() {
super.onCreate();
mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
mRc = new RemoteController(getApplicationContext(), this);
mAudioManager.registerRemoteController(mRc);
}
}
RemoteController.OnClientUpdateListener
がNotificationListenerService
ではない場合、AudioManager
への登録の時点で失敗し、コールバックが受け取れません(registerRemoteController
はfalse
を返します)。
また、NotificationListenerService
は JB_MR2 からのクラスですが、RemoteController
は Kitkat からのクラスであるという点にも注意すべきです。
帝王の時代: MediaSession と MediaBrowserService
Lollipop の登場とともに、ハンドセット端末だけではない様々な端末で Android が動作するようになりました。これにともなって、メディアコントロールも進化し、ハンドセット端末を母艦として、Android Wear や Android Auto からコントロールできるようになりました。
世界の中心はMediaSession
この為、様々なデバイスからメディアコントロールを実現する仕組みが導入されました。
この仕組の中心にあるものがMediaSession
です。
メディアコンテンツを持つアプリはそれぞれにMediaSession
を持ちます。そして、そのコントロールを要求する他のアプリに対し、MediaSession.Token
を払い出すことによって許可を出します。
払いだされたMediaSession.Token
はMediaController
に渡され、トークンが有効である間他のアプリからメディアコントロールができるようになります。
MediaBrowserService
とMediaBrowser
Android Auto では更に、メディアコンテンツをブラウジングする仕組みも構築されており、これがMediaBrowserService
とMediaBrowser
です。
メディアコンテンツを持つアプリはMediaBrowserService
を実装し、持っているコンテンツに関する情報を返します。
Android Auto はMediaBrowser
を用いて、MediaBrowserService
を持つアプリを探索しサービスにバインドします。バインド後は提供された情報を元に Android Auto の画面を構築していくことになります。
MediaBrowserService
の探索自体は、仕組みさえわかっていれば誰でも出来るため、バインド時にバインドしてきたアプリが正しいアプリかどうかを検証する必要があります。これはアプリの署名とパッケージ名を用いて検証するため、署名とパッケージ名の情報をリソースとして持っておく必要があります。逆に、この署名とパッケージ名情報を追加することによって、Android Auto 以外の通常のアプリへも同じ機能を提供することが出来るようになります。
MediaBrowser
にはMediaSession.Token
も渡されるため、Android Auto 上ではコンテンツのブラウジングとコントロールが同時に実現されます。
新しい通知スタイル
また、通知のスタイルにNotification.MediaStyle
が Lollipop から登場しました。これも原理的にはMediaSession.Token
をNotification
のextraに含めることで、システムに対しメディアコントロールを許可することになります。Notification.MediaStyle
によって、通知に表示される UI も標準化されました。これによって、ICS で導入されたロックスクリーンでの音楽コントロールが廃止され、通知によって置き換えられたことになります。
まとめ
4.x のサポート: 「おい、その先は地獄だぞ」
API のほとんどが非公開であるがゆえに、いくつかなデバイスで改造が施されています。
概ね、インタフェースにメソッドが追加されていることが多いですが、AOSP の実装そのままでは追加されたメソッドを実装していないことになり、エラーとなってクラッシュしますので、Crashlytics 等で常に怪しげなエラーを追い続ける必要があります。
5.x 以後の世界
狙いは Android Auto にありますが、仕組み上は様々なアプリでも恩恵をうけることができます。RemoteController
は deprecated になりましたが内部では MediaSession
を使用した実装に置き換えられています。4.x 以前の API が公開され、標準化されたものが一通り揃ったのが 5.x 以後の世界です。はやく minSdk を 5.x までもってきたいですね。
参考文献・資料
https://speakerdeck.com/keithyokoma/android-media-hacks
https://speakerdeck.com/keithyokoma/deep-inside-android-hacks
https://github.com/DrBreen/RemoteController