【Android TV / Fire TV 📺】テレビの電源ON/OFF、入力切替を検知する🔌

# はじめに
この記事では、Nexus PlayerやFire TVなどのAndroid OS(またはFire OS)を搭載したAndroid TV対応端末使用時における、TV端末の電源ON/OFF、入力切替を検知する方法を紹介します。

## 用語定義
似たような用語が多く出てくるので、わかりやすいよう、本記事では以下のように定義して使用させていただきます🙇‍♂️

| 用語 | 概要 |
| :--: | :--: |
| 外部端末 | Nexus Player/Fire TVなどの付属のHDMIをテレビ端末に接続する端末 |
| 内蔵端末 | BRAVIAなどのAndroid OSを内蔵するテレビ端末 |
| TV端末 | Android OSの内蔵有無に関わらない全てのテレビ端末やディスプレイ端末 |
| オーディオ端末 | 外付けスピーカーなどの付属のHDMIをテレビ端末に接続する端末 |
| アプリ | 開発対象のAndroid TVアプリケーション |

## 背景
Nexus PlayerやFire TVなどを使って、TV端末で映像コンテンツを楽しんでいる方も多いと思います。
みなさん、これらの端末ってどうやって使ってますか？
見終わったら、TV端末のリモコンぽちーってやってませんか？

そうなんです！実は、TV端末の電源を消しただけでは外部端末の電源が供給されている限り、外部端末は起動しっぱなしなんですよ！😨
これじゃあ、実際にユーザがいつアプリの使用をやめたかっていうのが分からないですよね...

ということで、何かいい方法はないかなーと調べた結果を紹介します🙋‍♀️
実は、AmazonのDeveloperサイトに[Fire TV上のHDMIイベントの処理](https://developer.amazon.com/ja/docs/fire-tv/handling-hdmi-events.html)という項目で、説明がされています。
しかし、このドキュメントだけだと分からない様々なユースケースがあるので、今回はその検証結果についてもまとめておきます。

# 使用するAPIについて
`BroadcastReceiver`を使用して、取得できるイベントの一つに [`ACTION_HDMI_AUDIO_PLUG`](https://developer.android.com/reference/android/media/AudioManager.html#ACTION_HDMI_AUDIO_PLUG) というものが存在します。
このイベントは `AudioManager` クラスに定義されており、映像や音声を**出力する**HDMIの接続状態を通知するイベントです。
ここで、出力を強調している訳は、本記事を読み進めていくと理解できると思います。
この `ACTION_HDMI_AUDIO_PLUG` というイベントは、物理的にHDMIコードを接続/切断する場合だけでなく、TV端末の電源をON/OFFにした場合などにも通知されます。
これを利用して、TV端末の電源ON/OFF、入力切替を検知します。

## 出力HDMIの接続/切断状態について
`ACTION_HDMI_AUDIO_PLUG` を利用して、出力HDMIの接続状態変化を通知することは分かったと思います。
では、出力HDMIの接続状態変化が起こった時の状態を取得する方法を説明します。
`AudioManager` クラスの中には [`EXTRA_AUDIO_PLUG_STATE`](https://developer.android.com/reference/android/media/AudioManager.html#EXTRA_AUDIO_PLUG_STATE)というものが存在します。`ACTION_HDMI_AUDIO_PLUG` イベントが飛んできた時に、この `EXTRA_AUDIO_PLUG_STATE` の値を `Intent` から取得することができます。
`EXTRA_AUDIO_PLUG_STATE` の値は、出力HDMI接続時に `1` となり、出力HDMI切断時に `0` となります。
つまり、`TV端末の電源がONになった時`や`入力切替によって、別の入力からアプリを起動している入力に切り替えられた時`には `1` になります。
逆に、`TV端末の電源がOFFになった時`や`入力切替によって、アプリを起動している入力から別の入力に切り替えられた時`には `0`になります。

# 実装
説明はこのくらいにして、実装を見ていきましょう！

https://github.com/ronnnnn/HDMIPlugStateSample
こちらにサンプルアプリを作成したので、ぜひ参考にしてください🙏

## `BroadcastReceiver` について
`ACTION_HDMI_AUDIO_PLUG` イベントを処理するには、`BroadcastReceiver` を継承したクラスを作成します。
そして、`BroadcastReceiver#onReceiver` をOverrideして、その中にイベントを受け取った時にやりたいことを実装していきます。
この辺は基本的な `BroadcastReceiver` による `Intent` アクションのハンドリング方法と同じですね🙆‍♀️

```kotlin:HDMIPlugStateBroadcastReceiver.kt
class HDMIPlugStateBroadcastReceiver : BroadcastReceiver() {

    companion object {
        private const val PLUGGED_IN: Int = 1
        private const val UNPLUGGED: Int = 0
    }

    override fun onReceive(context: Context?, intent: Intent?) {

        // ここで ACTION_HDMI_AUDIO_PLUG イベントを処理する

        intent ?: return
        if (intent.action == AudioManager.ACTION_HDMI_AUDIO_PLUG) {
            val plugState = intent.getIntExtra(AudioManager.EXTRA_AUDIO_PLUG_STATE, -1) // こいつが現在の接続状態
            val address = intent.getStringExtra("address")
            val state = intent.getIntExtra("state", -1)
            val portName = intent.getStringExtra("portName")

            when (plugState) {
                UNPLUGGED -> {
                    Timber.d(StringBuilder(" \n")
                            .append("---------------  HDMI is unplugged  ---------------\n")
                            .append("address is ${address}\n")
                            .append("state is ${state}\n")
                            .append("port name is ${portName}\n")
                            .append("---------------------------------------------------")
                            .toString()
                    )
                }

                PLUGGED_IN -> {
                    Timber.d(StringBuilder(" \n")
                            .append("---------------  HDMI is plugged in ---------------\n")
                            .append("address is ${address}\n")
                            .append("state is ${state}\n")
                            .append("port name is ${portName}\n")
                            .append("---------------------------------------------------")
                            .toString()
                    )
                }

                else -> Timber.e("invalid ACTION_HDMI_AUDIO_PLUG value")
            }
        }
    }
}
```

ここで注目したいのが、 `Intent` を経由して取得できる値についてです。
`EXTRA_AUDIO_PLUG_STATE` の値を取得できるのは先で説明しましたが、このイベントの `Intent` には、これ以外にも　`"address"`、`"state"`、`"portName"` というkeyの値が付随してくるようです。
私の環境では `"state"` の値は `EXTRA_AUDIO_PLUG_STATE` の値と同じで、`"state"`、`"portName"` の値は空でした。ドキュメントにもこれらのkeyについての説明を見つけられなかったので、何かわかる方がいましたら教えてください🙋‍♀️

## `BroadcastReceiver` の登録について
[ドキュメントにも記載されています](https://developer.android.com/reference/android/media/AudioManager.html#ACTION_HDMI_AUDIO_PLUG)が、今回説明している `ACTION_HDMI_AUDIO_PLUG` のイベントは、`Context#registerReceiver` を使用して `BroadcastReceiver` を継承したクラスを登録する必要があります。
**`AndroidManifest.xml` にこのクラスを登録してもイベントを受け取ることができません**ので、注意してください。

今回は、`Application` クラスを継承したクラスで登録、解除を行っています。

```App.kt
class App : Application() {

    private val hdmiPlugStateBroadcastReceiver: HDMIPlugStateBroadcastReceiver =
            HDMIPlugStateBroadcastReceiver()

    override fun onCreate() {
        super.onCreate()

        initializeReceiver()
    }

    override fun onTerminate() {
        terminateReceiver()

        super.onTerminate()
    }

    private fun initializeReceiver() {
        registerReceiver(
                hdmiPlugStateBroadcastReceiver,
                IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG)
        )
    }

    private fun terminateReceiver() {
        unregisterReceiver(hdmiPlugStateBroadcastReceiver)
    }
}
```

`Context#registerReceiver` の第二引数に `ACTION_HDMI_AUDIO_PLUG` の `IntentFilter` を渡すことをお忘れなく！

# `EXTRA_AUDIO_PLUG_STATE` の値について
これまで説明してきた、`EXTRA_AUDIO_PLUG_STATE` の値が、どういった場合に変化するかを検証したので、下にまとめておきます。
他にもこのパターンの時はどうなんだ🤔などありましたら、教えてください！

↓の値が取得できるのは基本的に、**外部端末の電源が入っており(コンセント電源供給)、アプリが起動していることが前提**です。

| パターン | `EXTRA_AUDIO_PLUG_STATE` の値 | 備考 |
| :-- | :--: | :-- |
| 外部端末付属のHDMIをTV端末から切断 | `0` | テレビ端末でないディスプレイ端末での検証では `0` のみが取れたが、BRAVIAでの検証では `0 -> 1 -> 0` を確認 |
| 外部端末付属のHDMIをTV端末に接続 | `1` |  |
| TV端末の入力切替で別の入力へ遷移 | `0` |  |
| TV端末の入力切替で別の入力から移動 | `1` |  |
| TV端末の電源off (TV端末の主電源on) | `0` |  |
| TV端末の電源off (TV端末の主電源も同時にoff) | - | アプリが強制終了されるため何も取れない |
| TV端末の電源off (外部端末の電源も同時にoff) | - | TV端末のUSBポートを電源供給源としている場合には、TV端末の電源offと同時にアプリも強制終了されるため何も取れない |
| TV端末の電源on | `1` |  |
| アプリ起動中に外部端末の電源off->電源on | `1` | 外部端末の再起動時にアプリが裏で起動され、 `1` のイベントが発火する (FireTVのみでしか検証してない) |
| ターゲットとする外部端末が刺さっているHDMIポート以外のHDMIポートへのHDMI接続/切断 | - | 別の外部端末がアプリを所有し、起動していてもイベントは発火されない |
| 外部端末でアプリの起動 | `1` | ただし、アプリが起動中の場合はイベントは発火されない |
| オーディオ端末の接続・切断 | - |  |

# おまけ
今回はNexus PlayerやFire TVなどのTV端末に接続して使用するAndroid TV端末の場合を説明してきました。
ですが、BRAVIAなどのAndroid OS内蔵端末はどうなんだ🤔と思った方もいるかと思います。
私が行った検証の限りだと、内蔵端末の場合は、**基本的に**外部端末のように出力HDMIを持たないため、TV端末のHDMIポートにHDMIが接続/切断されても `ACTION_HDMI_AUDIO_PLUG` イベントは発火されません。

ただし、**オーディオ端末を使用して、内蔵端末の音声をHDMI経由で出力している場合**には、オーディオ端末のHDMI接続/切断により `ACTION_HDMI_AUDIO_PLUG` イベントが発火されてしまいます😭

そのため、内蔵端末において、TV端末の電源ON/OFF、入力切替を検知するには、`Application` や `Activity` などのライフサイクルを利用するのがいいかと考えています。

ちょっとニッチな内容だったかもしれませんが、最後までお読みいただきありがとうございます！
何かありましたら、ぜひコメントください🙏

# 参考
- https://developer.android.com/reference/android/media/AudioManager
- https://developer.amazon.com/ja/docs/fire-tv/handling-hdmi-events.html