How Media API works in Android

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`に放り込んで、それぞれのアクションを順にブロードキャストしてやれば、音楽アプリがそれに反応して再生がコントロールされます。

```java:MediaButtonEventSender.java

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`の使い方については[この記事](http://techbooster.jpn.org/tag/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`](https://developer.android.com/intl/ja/reference/android/media/RemoteController.html)という新しい仕組みが導入されました。これは、今までの`RemoteControlDisplay`を置き換えるもので、かつ幾つか`RemoteControlClient`への対話もできるインタフェースが提供されます。

## `RemoteController.OnClientUpdatedListener`

いままでの`RemoteControlDisplay`に変わるものは、[`RemoteController.OnClientUpdatedListener`](https://developer.android.com/intl/ja/reference/android/media/RemoteController.OnClientUpdateListener.html)です。ほぼおなじ機能を実現するためのコールバックメソッドが定義されており、このリスナの実装を`RemoteController`のコンストラクタに渡します。

## `NotificationListener`と共に

`RemoteController`は、それ単体では動作しません。これは、先に上げた`RemoteController.OnClientUpdatedListener`の実装を`NotificationListenerService`に持たせなければならないことに由来しています。

実際、以下のようなクラスでのみ`RemoteController`はうまく動作します。

```java:MyNotificationListenerService.java

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