問題の概要
アプリを targetSdkVersion 34 (Android 14) にアップグレードすると、予期せぬトラブルに見舞われることがあります。動的に BroadcastReceiver を登録しようとした瞬間にアプリがクラッシュする可能性があります。これは通常、アプリの起動時や特定の機能に遷移した際に発生します。スタックトレースは以下のようになります:
FATAL EXCEPTION: main
Process: com.example.myapp, PID: 12345
java.lang.SecurityException: One of RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED should be specified when a receiver isn't being registered exclusively for system broadcasts
セキュリティ上の理由
Googleは「ブロードキャスト・ハイジャック」を防ぐためにアプリのセキュリティを強化しています。以前は、コード内で登録されたレシーバー(マニフェストではなく)は、設定が曖昧な部分がありました。デバイス上の他のアプリがそれらにブロードキャストを送信できるかどうか、システムによって厳密に強制されていませんでした。
API 34以降では、これを明示する必要があります。レシーバーを exported(他のアプリからアクセス可能)にするか、not exported(自分のアプリ専用)にするかを宣言しなければなりません。このフラグを省略し、レシーバーが ACTION_AIRPLANE_MODE_CHANGED のような特定のシステムイベント以外をリッスンしている場合、Androidはデータを保護するためにプロセスを強制終了します。
修正ステップ
1. 登録処理の箇所を特定する
コードベースから registerReceiver() を使用しているすべての箇所を検索します。3番目の引数であるフラグが欠落している「古い書き方」を探してください:
// Android 14デバイスではクラッシュします
registerReceiver(myReceiver, intentFilter);
2. 適切なフラグを選択して適用する
90%のケースでは、レシーバーはアプリ内部の通信にのみ使用されます。その場合は RECEIVER_NOT_EXPORTED を使用してください。これにより、アプリ内部のロジックを他のインストール済みアプリケーションから隠蔽できます。
Kotlin:
val filter = IntentFilter("com.example.UPDATE_UI")
val flags = Context.RECEIVER_NOT_EXPORTED
registerReceiver(myReceiver, filter, flags)
Java:
IntentFilter filter = new IntentFilter("com.example.UPDATE_UI");
int flags = Context.RECEIVER_NOT_EXPORTED;
registerReceiver(myReceiver, filter, flags);
3. 後方互換性の維持
これらのフラグはAPI 33で導入されました。Android 12デバイスで Context.RECEIVER_NOT_EXPORTED を直接使用すると、アプリは NoSuchFieldError でクラッシュします。
最もクリーンな解決策は、AndroidXライブラリの ContextCompat を使用することです。これはバージョンチェックを自動的に行い、Android 5.0から14までのすべてのバージョンでアプリがスムーズに動作することを保証します。
// 推奨される最新のアプローチ
ContextCompat.registerReceiver(
context,
myReceiver,
new IntentFilter("com.example.UPDATE_UI"),
ContextCompat.RECEIVER_NOT_EXPORTED
);
システムブロードキャストの例外
Intent.ACTION_POWER_CONNECTED のようなシステム専用のブロードキャストを登録する場合、フラグは厳密には必須ではありません。しかし、例外に頼るべきではありません。フラグを明示的に追加することで、将来の変化に対応しやすくなり、チームメンバーにとっても読みやすいコードになります。
検証チェックリスト
- **API 34へのデプロイ:** 実機のAndroid 14デバイスまたはエミュレータでテストします。
- **レシーバーのトリガー:** `registerReceiver` の行が実際に実行されることを確認します。
- **ADBテスト:** `RECEIVER_NOT_EXPORTED` を使用した場合は、ターミナルからトリガーを試みてください: **`adb shell am broadcast -a com.example.UPDATE_UI`。 セキュリティが正しく機能していれば、レシーバーは起動しない**はずです。
トラブルシューティングのヒント
- **サードパーティ製SDK:** 自分のコードに問題がないのにクラッシュが続く場合は、依存関係を確認してください。古いバージョンの分析や広告SDKは、古い登録方法を使用していることがよくあります。それらを最新バージョンに更新してください。
- **レガシーコード:** まだ `LocalBroadcastManager` を使用している場合、このクラッシュは発生しません。しかし、Googleは何年も前にこれを非推奨にしました。そのロジックを **Kotlin Flows** や **LiveData** に移行する絶好の機会です。

