AndroidのHDMI-CEC電源制御はsideloadだとSecurityExceptionで落ちる
※本記事にはアフィリエイトリンクを含む場合があります。内容は広告の有無に影響されません。
結論
HdmiControlManagerのCEC制御系APIはsignature|privileged権限が必要なため、通常のsideloadアプリから呼び出すとほとんどの機種でSecurityExceptionになり実質no-opになる。
結論
AndroidのHdmiControlManagerが持つHDMI-CEC制御系API(standby/wakeup等)はsignature|privileged権限を要求するため、sideloadアプリから呼び出すとほとんどの機種でSecurityExceptionになり実質no-opになる。 キミテラスのサイネージ端末(Device Ownerキオスク化したGoogle TV)は、夜間の電源OFFを本物のHDMI-CEC standbyで実現したかったが、この制約により本命の手段をCECではなくDevicePolicyManager.lockNow()に置き換えた。
症状
キミテラスのサイネージ端末は、校内の掲示板用に設置したGoogle TVをDevice Ownerでキオスク化し、夜間は画面を消して朝また点ける運用にしている。最初に検討したのは、HDMI-CEC経由でTV本体を本物のstandbyに落とす方法だった。実装したコードのコメントには、この手段がそもそも機能しない理由がそのまま書かれている。
// CecHelper.kt
/**
* HDMI-CEC で TV を本物の Standby / WakeUp に持っていくベストエフォート試行。
*
* 注意:
* - HdmiControlManager の制御系 API は signature|privileged 権限が必要なため、
* 通常の sideload アプリでは通常 ぐ拒否される。
* - 例外で動く機種があれば儲けもの、ぐらいの位置付け。
* - 失敗しても黒画面オーバーレイで「見た目 OFF」は維持される。
*
* 実装は API 30 以降の `HdmiControlManager#setSystemAudioMode`/`HdmiControlManager.OneTouchPlay`
* 等を試したいところだが、ほとんどの場合 SecurityException で落ちる。
* 現時点では「リフレクション + フォールバック」で吸収する。
*/
実際のtryStandbyは、サービスの有無をログに残すだけで、制御コマンドは一切発行していない。
fun tryStandby(context: Context) {
runCatching {
val svc = context.getSystemService("hdmi_control")
if (svc != null) {
Log.i(TAG, "hdmi_control service available: ${svc.javaClass.name}")
// ベンダー実装次第。一般 API では制御コマンドの直接呼出は無理。
} else {
Log.i(TAG, "hdmi_control service not available")
}
}.onFailure {
Log.d(TAG, "tryStandby skipped: ${it.message}")
}
}
getSystemService("hdmi_control")自体はnullにならずサービスを返す。だが、そこから先の「standbyにする」「wakeupする」という制御コマンドを呼ぶ段になると拒否される。
原因
AndroidのHdmiControlManagerは、TVの電源やオーディオ経路をHDMI-CEC経由で操作する公開APIを持つが、その制御系メソッドはsignatureまたはprivilegedの保護レベルを持つ権限を要求する。これはシステム証明書で署名されたアプリか、ベンダーのprivilegedパーティションに置かれたアプリにしか付与されない権限で、サードパーティのsideloadアプリが満たす手段がない。
ここで混同しやすいのが、サイネージ端末がすでにDevicePolicyManagerのDevice Ownerになっている点だ。Device Ownerは「端末全体を管理する」強い権限だが、これはdpm(デバイス管理)の権限体系であって、HdmiControlManagerが要求するsignature|privileged権限とは別の階層にある。Device Ownerになっても、CECの制御系APIが要求する署名・配置の条件は満たされない。コメントが「例外で動く機種があれば儲けもの」と書いているのは、ごく一部の機種でOEMがCECハンドラの権限チェックを緩めている場合を想定した保険的な表現であり、設計上の前提は「基本的には拒否される」側にある。
直し方
CECが本命にならないと分かった時点で、夜間の画面OFFはPowerControllerの設計コメントが明記する別の手段に倒した。深いスリープに落とすとポーリングが止まりリモートwakeも届かず復帰不能になるため、端末本体は24時間起こしたままにし、画面(バックライト)だけをDevice OwnerのlockNow()で消す。
// PowerController.kt
/**
* 設計判断(2026-06-08):
* - 端末を *深いスリープ* に落とすと、ポーリングが止まりリモート wake も届かず **復帰不能** になる
* (ユーザー確認済み)。よって端末本体は 24h 起こしたまま、**画面(バックライト)だけ** を消す。
* - 画面 OFF の本命は DevicePolicyManager.lockNow()(Device Owner / アクティブな端末管理者なら可)。
* これは画面ロック=バックライト OFF を起こすが、CPU/サービス/ポーリングは生きたまま。
* - HDMI-CEC(CecHelper)は privileged 権限が要りほぼ no-op なので、あくまで *補助* として併用。
* - BlackScreenActivity(黒オーバーレイ)は lockNow が効かない機種向けの *見た目* フォールバック。
*/
実装ではlockNow()を本命として呼び、CECは「効けば儲けもの」の補助試行として同じ呼び出しの中に残した。
fun screenOff(context: Context) {
val locked = runCatching {
if (canLock(context)) {
dpm(context)?.lockNow()
Log.i(TAG, "screenOff: lockNow() issued")
true
} else {
Log.w(TAG, "screenOff: not device owner / admin -> lockNow skipped")
false
}
}.onFailure { Log.w(TAG, "screenOff: lockNow failed: ${it.message}") }
.getOrDefault(false)
// 補助: HDMI-CEC standby(多くの機種で no-op だが効けば儲けもの)
runCatching { CecHelper.tryStandby(context) }
if (!locked) {
Log.i(TAG, "screenOff: relying on BlackScreenActivity overlay fallback")
}
}
lockNow()が失敗する(Device Ownerでも管理者でもない)場合に備え、BlackScreenActivityの黒画面オーバーレイを見た目だけのフォールバックとして残している。3段構え(lockNow → CEC補助 → 黒画面オーバーレイ)にすることで、どの機種でも「何らかの形でOFFに見える」状態を外さないようにした。
再発防止
このコードに自動テストは無い。再発防止の実体は、PowerController.ktの設計判断コメント(2026-06-08)とCecHelper側の注意書きそのものが果たしている。「CECはsignature|privileged権限が要るためほぼno-op」「本命はlockNow」という結論をコードの先頭コメントに残したことで、後から読む人間(自分を含む)が同じ検証をやり直してCEC経由の制御コマンド実装を書き足す、という手戻りを防いでいる。すべての呼び出しがrunCatchingで包まれているのも、Device Ownerでない端末や非対応機種でも例外で落ちずに次のフォールバックへ流れるようにするための設計で、権限が無い環境を前提にした実装になっている。
よくある質問
Q1なぜHDMI-CEC経由のTV電源制御がSecurityExceptionで落ちるのですか?
HdmiControlManagerのsetSystemAudioModeやOneTouchPlayなどの制御系APIはsignature|privileged権限を要求する。sideloadアプリは通常の署名・権限しか持たないため、呼び出すとほとんどの機種でSecurityExceptionになる。
Q2Device Ownerにすれば回避できますか?
できない。Device Ownerはdpm(DevicePolicyManager)が提供するデバイス管理権限であり、HdmiControlManagerが要求するsignature|privileged権限とは別の権限体系。dpm.lockNow()は使えるようになるが、CEC制御系APIの権限不足は解消されない。
Q3CECが使えないなら、夜間の画面OFFはどう実現しましたか?
DevicePolicyManager.lockNow()で画面ロック(バックライトOFF)だけを起こし、端末本体は24時間起こしたままにした。深いスリープに落とすとポーリングが止まりリモートwakeも届かず復帰不能になるため。CECは効けば儲けものという位置づけの補助試行として併用している。
Q4HdmiControlManagerのサービス自体は取得できるのですか?
取得できる。context.getSystemService("hdmi_control")はnullを返さずサービスオブジェクトを返す。問題はその先で、standby/wakeupといった制御系メソッドを呼ぶ段になるとsignature|privileged権限が無いため拒否される。
確認した環境
- Android(Device Owner キオスク化した Google TV 端末 / tv-ble-bridge)
- 2026-06-08 設計判断、2026-06-14 コミット e81029c7 で実装
この記事の根拠
- Kotlinファイル 1〜18行目コミット e81029c
- Kotlinファイル 23〜37行目コミット e81029c
- Kotlinファイル 10〜21行目コミット e81029c
- Kotlinファイル 57〜76行目コミット e81029c
本文の主張は、上の記録に書かれていることだけです。運用しているリポジトリは非公開のため リンクは張れませんが、どのファイルの何行目を、どのコミット時点で見て書いたかは 記事ごとに残しています。推測で書いた箇所はありません。