It crashes the moment you bump targetSdk to 31
You update targetSdkVersion to 31 for the Play Store deadline, push a build, and within minutes crash reports flood in:
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)
Every PendingIntent call that doesn't include FLAG_IMMUTABLE or FLAG_MUTABLE now hard-crashes on Android 12. No warnings, no deprecation grace period โ pure IllegalArgumentException at runtime.
Finding every offending call
Before touching code, map out the blast radius. Search your entire project for PendingIntent factory methods:
# In Android Studio: Edit โ Find โ Find in Files
PendingIntent.getActivity(
PendingIntent.getBroadcast(
PendingIntent.getService(
PendingIntent.getForegroundService(
Or from terminal:
grep -rn "PendingIntent\.get" app/src/ --include="*.java" --include="*.kt"
Every single match is a candidate. Don't assume any are safe โ if they're missing the mutability flag, they crash on API 31+.
Understanding which flag to use
Before fixing blindly, know the rule:
- Use
FLAG_IMMUTABLEin 95% of cases โ notifications, deep links, scheduled work, media session callbacks. The PendingIntent's embedded Intent won't be modified after creation. - Use
FLAG_MUTABLEonly when the system must write back into the Intent โ specificallyAlarmManagerwithcreatePendingIntentfor exact alarms, or when you're usingPendingIntentas a reply slot (e.g., inline notification replies viaRemoteInput).
When unsure, start with FLAG_IMMUTABLE. If something breaks (alarm doesn't fire, reply action fails), switch that specific one to FLAG_MUTABLE.
The fix
Java โ before and after
// BEFORE โ crashes on Android 12
PendingIntent pi = PendingIntent.getBroadcast(
context,
requestCode,
intent,
PendingIntent.FLAG_UPDATE_CURRENT // missing mutability flag
);
// AFTER โ correct
PendingIntent pi = PendingIntent.getBroadcast(
context,
requestCode,
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
Kotlin โ before and after
// 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
)
Using the compat helper (recommended for new code)
If you support API < 23 and want cleaner code, use PendingIntentCompat from AndroidX Core 1.8+:
// build.gradle
implementation "androidx.core:core:1.8.0"
// Kotlin
val pi = PendingIntentCompat.getBroadcast(
context,
requestCode,
intent,
PendingIntent.FLAG_UPDATE_CURRENT,
false // isMutable = false โ FLAG_IMMUTABLE on API 23+
)
This handles the version check internally so you don't scatter if (Build.VERSION.SDK_INT >= 23) guards everywhere.
Common spots that always need this fix
// Notification action buttons
val actionIntent = PendingIntent.getBroadcast(
context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// AlarmManager exact alarm (needs FLAG_MUTABLE for system to fill in elapsedRealtime)
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerAtMillis,
PendingIntent.getBroadcast(
context, alarmId, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
)
)
// Service from notification
val serviceIntent = PendingIntent.getForegroundService(
context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
Third-party libraries crashing too
Sometimes the crash stack trace points into a library you don't control โ an old version of Firebase Messaging, WorkManager, or a notification library. Check the library version first:
# Run dependency report
./gradlew app:dependencies | grep -E "firebase-messaging|work-runtime|androidx.core"
Known safe minimum versions for 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+
Bump these first. Most Android 12 PendingIntent crashes in libraries were patched around late 2021.
Verifying the fix
Don't just compile and ship. Test on a real API 31+ device or emulator:
# Create AVD with API 31 via command line
avdmanager create avd -n test31 -k "system-images;android-31;google_apis;x86_64"
# Start emulator
emulator -avd test31
# Install and watch logcat for the specific exception
adb logcat | grep -E "IllegalArgumentException|PendingIntent"
Walk through every feature that uses PendingIntent: tap all notification actions, trigger scheduled alarms, test deep links. If logcat stays clean and features work, you're done.
Also run your UI tests with:
./gradlew connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.yourapp.NotificationTest
Lessons from the 2 AM scramble
This particular crash is annoying because it's completely invisible until you flip targetSdkVersion. Three things that would have caught it earlier:
- Lint rule โ Android Studio ships a lint check for this since AGP 7.0. Run
./gradlew lintand look forUnspecifiedImmutableFlagwarnings before bumping targetSdk. - Test on API 31 emulator before release โ obvious in hindsight, but many teams only test on their physical device running an older OS.
- Grep before bumping โ five minutes with
grep -rn "PendingIntent.get" app/src/saves three hours at midnight.
The flag requirement isn't optional. Android 12 enforces it at the OS level to prevent apps from accidentally leaving PendingIntents exploitable via Intent redirection. Treat it as mandatory hygiene when targeting API 31+.

