targetSdkを31に上げた瞬間にクラッシュする
Play Storeの期限に合わせてtargetSdkVersionを31に更新してビルドをプッシュすると、数分以内にクラッシュレポートが殺到します:
java.lang.IllegalArgumentException: Targeting S+ (version 31 and above) requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating a PendingIntent.
at android.app.PendingIntent.checkFlags(PendingIntent.java:375)
at android.app.PendingIntent.getBroadcast(PendingIntent.java:642)
at com.yourapp.notifications.NotificationHelper.buildIntent(NotificationHelper.java:88)
FLAG_IMMUTABLEまたはFLAG_MUTABLEを含まないPendingIntentの呼び出しは、Android 12でハードクラッシュするようになりました。警告も、非推奨の猶予期間もなく、ランタイムで純粋なIllegalArgumentExceptionが発生します。
問題のある呼び出しをすべて見つける
コードに触れる前に、影響範囲を把握しましょう。プロジェクト全体でPendingIntentのファクトリメソッドを検索します:
# Android Studio: Edit → Find → Find in Files
PendingIntent.getActivity(
PendingIntent.getBroadcast(
PendingIntent.getService(
PendingIntent.getForegroundService(
またはターミナルから:
grep -rn "PendingIntent\.get" app/src/ --include="*.java" --include="*.kt"
すべての一致箇所が対象候補です。どれかが安全だと思い込まないでください。変更可能性フラグが欠けていれば、API 31以降でクラッシュします。
どちらのフラグを使うべきかを理解する
やみくもに修正する前に、ルールを把握しましょう:
FLAG_IMMUTABLEを使用するのは95%のケース — 通知、ディープリンク、スケジュール済みの処理、メディアセッションコールバック。PendingIntentに埋め込まれたIntentは作成後に変更されません。FLAG_MUTABLEを使用するのは、システムがIntentに書き戻す必要がある場合のみ — 具体的には正確なアラーム用のcreatePendingIntentを使ったAlarmManager、またはPendingIntentを返信スロットとして使う場合(例:RemoteInputを使ったインライン通知返信)です。
迷った場合はFLAG_IMMUTABLEから始めましょう。何か壊れた場合(アラームが発火しない、返信アクションが失敗するなど)、その特定のものだけFLAG_MUTABLEに切り替えてください。
修正方法
Java — 修正前と修正後
// BEFORE — Android 12でクラッシュする
PendingIntent pi = PendingIntent.getBroadcast(
context,
requestCode,
intent,
PendingIntent.FLAG_UPDATE_CURRENT // 変更可能性フラグが欠けている
);
// AFTER — 正しい書き方
PendingIntent pi = PendingIntent.getBroadcast(
context,
requestCode,
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
Kotlin — 修正前と修正後
// BEFORE
val pi = PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
// AFTER
val pi = PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
compatヘルパーを使う(新規コードに推奨)
API 23未満をサポートしていてよりクリーンなコードを求めるなら、AndroidX Core 1.8+のPendingIntentCompatを使いましょう:
// build.gradle
implementation "androidx.core:core:1.8.0"
// Kotlin
val pi = PendingIntentCompat.getBroadcast(
context,
requestCode,
intent,
PendingIntent.FLAG_UPDATE_CURRENT,
false // isMutable = false → API 23以降でFLAG_IMMUTABLE
)
バージョンチェックを内部で処理してくれるため、if (Build.VERSION.SDK_INT >= 23)のガードを各所に散らばらせる必要がありません。
常にこの修正が必要な典型的な箇所
// 通知アクションボタン
val actionIntent = PendingIntent.getBroadcast(
context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// AlarmManagerの正確なアラーム(システムがelapsedRealtimeを書き込むためFLAG_MUTABLEが必要)
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerAtMillis,
PendingIntent.getBroadcast(
context, alarmId, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
)
)
// 通知からのService起動
val serviceIntent = PendingIntent.getForegroundService(
context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
サードパーティライブラリもクラッシュする場合
クラッシュのスタックトレースが、制御できないライブラリ — 古いバージョンのFirebase Messaging、WorkManager、または通知ライブラリ — を指していることがあります。まずライブラリのバージョンを確認しましょう:
# 依存関係レポートを実行
./gradlew app:dependencies | grep -E "firebase-messaging|work-runtime|androidx.core"
Android 12での既知の安全な最低バージョン:
firebase-messaging: 22.0.0以上androidx.work:work-runtime: 2.7.0以上androidx.core:core: 1.7.0以上com.google.android.gms:play-services-location: 19.0.0以上
まずこれらをアップグレードしましょう。ライブラリにおけるAndroid 12のPendingIntentクラッシュのほとんどは2021年末頃にパッチ済みです。
修正を確認する
コンパイルしてリリースするだけで済ませないでください。実際のAPI 31以降のデバイスまたはエミュレーターでテストしましょう:
# コマンドラインでAPI 31のAVDを作成
avdmanager create avd -n test31 -k "system-images;android-31;google_apis;x86_64"
# エミュレーターを起動
emulator -avd test31
# インストールして特定の例外のlogcatを監視
adb logcat | grep -E "IllegalArgumentException|PendingIntent"
PendingIntentを使用するすべての機能を一通り確認しましょう:すべての通知アクションをタップし、スケジュールされたアラームをトリガーし、ディープリンクをテストします。logcatがクリーンなままで機能が正常に動作すれば完了です。
また、UIテストも実行しましょう:
./gradlew connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.yourapp.NotificationTest
深夜2時の緊急対応から学んだこと
このクラッシュが厄介なのは、targetSdkVersionを変更するまで完全に見えないからです。もっと早く検出できた方法が3つあります:
- Lintルール — Android StudioにはAGP 7.0以降このチェック用のlintが含まれています。targetSdkを上げる前に
./gradlew lintを実行してUnspecifiedImmutableFlagの警告を確認しましょう。 - リリース前にAPI 31エミュレーターでテストする — 後から考えると当然ですが、古いOSを搭載した物理デバイスだけでテストするチームも多くいます。
- バージョンアップ前にgrepする —
grep -rn "PendingIntent.get" app/src/に5分かけることで、深夜3時間の作業を節約できます。
フラグの要件は任意ではありません。Android 12は、アプリがPendingIntentをIntent Redirectionによって悪用可能な状態のままにしてしまうことを防ぐため、OS レベルでこれを強制しています。API 31以降をターゲットにする際は、必須の衛生管理として扱いましょう。

