AndroidアプリからTVを電源ONして、しかも入力切替をしたい

この記事は[Android Advent Calendar 2019](https://qiita.com/advent-calendar/2019/android)の14日目の記事です。

## はじめに
自社でAndroid端末を開発していてかつ、その端末がTVにつながるようなサービスでAndroidアプリを作っているという、ニッチ中のニッチみたいな人たちに向けた記事になります。
Android7.1.1で動作の確認をしています。

## 実現したいこと
自社で独自のAndroid端末を製造していて、かつ、その端末がTVに接続するような端末の場合、結構な確率で下記のようなことがやりたくなります。

1. **Android端末からSW的にTVの電源をつけること**
2. **Android端末からSW的にTVの入力切替でその端末の入力に切り替えること**

昔々のTVと映像入力機器がコンポジット端子(黃赤白の3端子)で接続されていた頃には無理でしたが、最近のスタンダードである、HDMI端子では標準規格として上記のようなことが実現できるようになってきています。

## HDMI CEC
HDMI規格には、実は、映像と音声信号以外にも信号を送ることができるように規格が作られています。その中にHDMIケーブルで接続された機器同士を連動して動作させるためのCEC（Consumer Electronics Control）信号があります。

CECという名称は一般的にはあまり表に出ることは有りませんが、この規格をベースに各社が独自拡張したものが一般的には広く認知されています。

| メーカー       |      名称 | 
|:-----------------|------------------|
| パナソニック             |ビエラリンク | 
| シャープ          |AQUOSファミリンク |  
| ソニー            |ブラビアリンク|  
|東芝            |  レグザリンク |   
| 三菱電機|リアリンク|

この規格を使用することで、TVにHDMI接続された映像機器からTVの電源ONと入力切替を実現することができます。

さらに、CEC機能を用いることで、HDMIケーブルで接続された入力機器から、TV電源のONなどの基本的な制御に始まり、音量の上げ下げや、TV側のコントロールメニューを出すなどのかなり拡張した動作までできる場合があります。

また、機器同士と言っている通り、必ずTVが受信側である必要がなく、TVから接続された映像機器等へ信号を流すことができます。
TVのリモコンでBlu-rayレコーダーが操作できたりしますが、あれはBlu-rayレコーダーがTVリモコンの赤外線受信機能持っているわけではなく、TVからリモコンの入力をCEC機能を用いて受け取るという方法で実現されています。

HDMI CECを用いることで可能な挙動一覧についてはこちらのページにまとまっています。
[CEC-O-MATIC](http://www.cec-o-matic.com/)


## AndroidでのHDMI CECサポート状況
Androidが動くSTB(Set Top Box)がで始めたAndroid4.xの頃からCECを使う事ができるSTBはいくつか有りました。
しかし、この頃のAndroidでのCECサポート状況は無いと行っても良いような状況でそれぞれのメーカーが独自で実装しているような状態でした。

潮目が変わったのは、Android TVが出たAndroid5からです。~~まるでBluetoothみたいですね~~
下記の比較表のように標準のコードにHDMIをコントロールするコードが入りました。
![HDMI_Control_Service.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45441/0d2aa4fb-bb21-9fa9-b8fa-d4e7d0558174.png)
from https://source.android.com/devices/tv/hdmi-cec

これにより、フレームワークのAPIをコールすることでCEC信号をAndroidアプリから接続した機器へ流せるようになりました。

## 実際に動かしてみる
### 前準備
今回使用するAPIはHide APIのため、そのままでは使用することができません。
そこで、アプリを動かしたいAndroidバージョンのAOSPソースコードをフルビルドすることで手に入るframework.jarが必要になります。
ビルドするなり、有志が公開しているビルド済みを貰うなりし、入手します。
実際の入手方法については割愛します。

### framework.jarを使えるようにする
framework.jarが手に入ったら、```app```フォルダの下に```framework```フォルダを作り、その中に手に入れたframework.jarを入れます。
<img width="241" alt="スクリーンショット 2019-12-14 21.38.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45441/b3d4e019-db09-0652-b784-73deae854b84.png">

このまま通常のライブラリと同様にimplementationしても使うことができますが、そうしてしますと、ビルド後のApkファイルにframework.jarが含まれることになってしまいます。

ビルド方法にもよりますが、framework.jarは20M~のファイルサイズのため、Apkの肥大化やビルド時間の悪影響がひどいので、コンパイル時に参照するだけにします。

```xml:build.gradle
dependencies {

    implementation fileTree(dir: 'libs', include: ['*.jar'])
    compileOnly fileTree(dir: 'framework', includes: ['*.jar'])
    ~
}
```
build.gradleのdependenciesでの指定方法をcompileOnlyにすることで、ビルド時の依存関係解決はできるが生成Apkにはframework.jarを含まない形でビルドできます。

### CEC Permissionの追加
アプリからCEC関係APIを使用する場合にはパーミッション宣言を必要とします。
```AndroidManifest.xml```に追加します。

```xml:AndroidManifest.xml
    <uses-permission android:name="android.permission.HDMI_CEC" />
```

### 正しく準備が行えているか確認する
上記の用意が終わっている場合には、下記のクラスがアプリのソースコードから使用できる様になっているはずです。
アプリのソースコードで下記のクラスをimportし、確認します。

```
android.hardware.hdmi.HdmiControlManager
```
```
android.hardware.hdmi.HdmiPlaybackClient
```
```
android.hardware.hdmi.IHdmiControlService
```

もしimportできない場合には、どこかがおかしいので再度チェックします。
よくあるのはビルドしたframework.jarがフルビルドになっておらず必要クラスが含まれていない場合なので、framework.jar内を検索するなどで確認します。

jar内のクラスの検索などは```jar tf [jar file]```コマンドを使います。

```
➜  framework git:(develop) ✗ jar tf framework.jar 
META-INF/
META-INF/MANIFEST.MF
android/
android/permissionpresenterservice/
android/permissionpresenterservice/RuntimePermissionPresenterService$1.class

・・・

android/hardware/hdmi/IHdmiControlService$Stub$Proxy.class
android/hardware/hdmi/IHdmiControlService.class
android/hardware/hdmi/HdmiRecordSources$AnalogueServiceSource.class
android/hardware/hdmi/HdmiControlManager$HotplugEventListener.class
android/hardware/hdmi/HdmiPlaybackClient$2.class
android/hardware/hdmi/HdmiTvClient$1.class
android/hardware/hdmi/HdmiClient$1.class

・・・

```

### コードからHDMI CECを用いてTVの電源ONと入力切り替えを行う

AndroidアプリからTVを操作するには下記の手順を踏みます。
1. ```HdmiControlService```のインスタンスを取得
2. 1で取得した```HdmiControlService```インスタンスを用いて```HdmiControlManager```のインスタンスを取得
3. 2で取得した```HdmiControlManager```インスタンスに対して、行いたい操作をインプット

上記を踏まえ**コードからHDMI CECを用いてTVの電源ONと入力切り替えを行う**コードは下記の用になります。


```java:Sample.kt
    private fun setTvOn() {
        val hdmiControlService: IHdmiControlService =
                IHdmiControlService.Stub.asInterface(ServiceManager.getService(Context.HDMI_CONTROL_SERVICE))
                        ?: return
        val hdmiControlManager = HdmiControlManager(hdmiControlService)
        val hdmiPlaybackClient: HdmiPlaybackClient = hdmiControlManager.playbackClient ?: return

        hdmiPlaybackClient.oneTouchPlay { Log.d(TAG, "success Tv On") }
    }
```

上記のメソッドを呼び出すことで、AndroidアプリからTVをつけることができます。

### 注意点
基本的にPermissionやAPIの観点等々から、Systemアプリでの実行が基本になると思います。
このコードが含まれるAPKは、Platform署名を付けて```/system/priv-app```以下にインストールするが良いと思います。
またこのコードを走らせると、何も問題ない場合TVの状態によらずTVの電源が入り入力が切り替わります。
ユーザーが意図した状態では問題有りませんが、意図せずにこのような挙動をさせるとユーザーに非常に不信感を与えるので、この機能を走らせるタイミングのUXについては慎重に検討する必要があります。

**下記は参考資料です**
## AndroidでHDMI CECを使用する際の落とし穴
Androidフレームワーク自体でサポートされたとはいえ、下記のダイアグラムを見ていただければわかるのですが、Androidフレームワーク上には抽象化されたAPIの口のみ用意されており、その下の実際のドライバ層や、それとAndroidフレームワークへのつなぎ込みはデバイスメーカーが行う必要があります。

![HDMI_Control_Service_Flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45441/dbe5b315-c025-dd6f-bd65-de2581c3a394.png)
from https://source.android.com/devices/tv/hdmi-cec

これは、カメラなどのHWを使う機能全般に言えることですね。

また、Androidに限った話ではないのですが、TV自体のHDMI CECの実装に大きな課題があります。
まず、日本以外のメーカー製TVのCECサポート率が低いです。
CECはHDMIに求められる必須機能ではなく、そこまで使用される機能でもないため、メーカーとしては不要な機能を削りコストダウンに努めるのは当然なので、仕方のないところではあります。
また、日本メーカーでも、オリオンや、DXアンテナといったメーカーのTVもサポートしていなことが多めです。

逆にわざわざCEC＋αの機能を作り、独自のブランド名をつけている日本メーカーにしても、時々明らかにバグっぽい挙動をすることが有り、デバッグに非常に苦労をさせられることがあります。

さらに、TV側がCECに対応している場合でもユーザーが設定でCEC連動設定をoffにしている場合も当然ながらCEC系の機能を動かすことはできません。
このCEC連動系の設定場所がTVメーカー・機種によりまちまちで、更に設定項目の名称もバラバラ、更に初期値も統一感が無いという非常にひどい状況なので、この機能をサービスの絶対必須条件にするのは辞めたほうが良いです。
~~正直統一規格なんだからそこら辺も統一しろよというお気持ち~~