Sửa lỗi "Targeting S+ requires FLAG_IMMUTABLE or FLAG_MUTABLE" trên Android 12

intermediate📱 Android2026-06-03| Android 12 (API 31+), Java/Kotlin, minSdk hoặc targetSdk đã nâng lên 31

Error Message

java.lang.IllegalArgumentException: Targeting S+ (version 31 and above) requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating a PendingIntent.
#android 12#pendingintent#migration

Ứng dụng crash ngay khi nâng targetSdk lên 31

Bạn cập nhật targetSdkVersion lên 31 để đáp ứng deadline của Play Store, đẩy bản build lên, và chỉ vài phút sau báo cáo crash ào ạt đổ về:

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)

Mọi lời gọi PendingIntent không có FLAG_IMMUTABLE hoặc FLAG_MUTABLE giờ đây sẽ hard-crash trên Android 12. Không có cảnh báo, không có thời gian ân hạn deprecated — chỉ là IllegalArgumentException thuần túy lúc runtime.

Tìm tất cả các lời gọi có vấn đề

Trước khi đụng vào code, hãy xác định phạm vi ảnh hưởng. Tìm kiếm toàn bộ project để tìm các factory method của PendingIntent:

# Trong Android Studio: Edit → Find → Find in Files
PendingIntent.getActivity(
PendingIntent.getBroadcast(
PendingIntent.getService(
PendingIntent.getForegroundService(

Hoặc từ terminal:

grep -rn "PendingIntent\.get" app/src/ --include="*.java" --include="*.kt"

Từng kết quả tìm được đều là ứng viên cần xem xét. Đừng giả định bất kỳ chỗ nào là an toàn — nếu thiếu cờ mutability, chúng sẽ crash trên API 31+.

Hiểu nên dùng cờ nào

Trước khi sửa bừa bãi, hãy nắm rõ quy tắc:

  • Dùng FLAG_IMMUTABLE trong 95% trường hợp — thông báo, deep link, scheduled work, media session callback. Intent được nhúng trong PendingIntent sẽ không bị thay đổi sau khi tạo.
  • Dùng FLAG_MUTABLE chỉ khi hệ thống cần ghi ngược lại vào Intent — cụ thể là AlarmManager với createPendingIntent cho exact alarm, hoặc khi dùng PendingIntent làm reply slot (ví dụ: inline notification reply qua RemoteInput).

Khi không chắc, hãy bắt đầu với FLAG_IMMUTABLE. Nếu có gì đó bị hỏng (alarm không kích hoạt, reply action thất bại), hãy chuyển chỗ đó cụ thể sang FLAG_MUTABLE.

Cách sửa

Java — trước và sau

// TRƯỚC — crash trên Android 12
PendingIntent pi = PendingIntent.getBroadcast(
    context,
    requestCode,
    intent,
    PendingIntent.FLAG_UPDATE_CURRENT  // thiếu cờ mutability
);

// SAU — đúng cách
PendingIntent pi = PendingIntent.getBroadcast(
    context,
    requestCode,
    intent,
    PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);

Kotlin — trước và sau

// TRƯỚC
val pi = PendingIntent.getActivity(
    context,
    0,
    intent,
    PendingIntent.FLAG_UPDATE_CURRENT
)

// SAU
val pi = PendingIntent.getActivity(
    context,
    0,
    intent,
    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)

Dùng compat helper (khuyến nghị cho code mới)

Nếu bạn hỗ trợ API < 23 và muốn code gọn hơn, hãy dùng PendingIntentCompat từ 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 trên API 23+
)

Cách này xử lý kiểm tra version nội bộ nên bạn không cần rải if (Build.VERSION.SDK_INT >= 23) khắp nơi.

Những chỗ thường gặp luôn cần sửa

// Nút action trong notification
val actionIntent = PendingIntent.getBroadcast(
    context, 0, intent,
    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)

// AlarmManager exact alarm (cần FLAG_MUTABLE để hệ thống điền elapsedRealtime)
alarmManager.setExactAndAllowWhileIdle(
    AlarmManager.RTC_WAKEUP,
    triggerAtMillis,
    PendingIntent.getBroadcast(
        context, alarmId, intent,
        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
    )
)

// Service từ notification
val serviceIntent = PendingIntent.getForegroundService(
    context, 0, intent,
    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)

Thư viện bên thứ ba cũng bị crash

Đôi khi stack trace của crash trỏ vào một thư viện bạn không kiểm soát được — phiên bản cũ của Firebase Messaging, WorkManager, hoặc thư viện notification nào đó. Hãy kiểm tra phiên bản thư viện trước:

# Chạy báo cáo dependency
./gradlew app:dependencies | grep -E "firebase-messaging|work-runtime|androidx.core"

Phiên bản tối thiểu an toàn đã biết cho 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+

Hãy nâng các thư viện này trước. Hầu hết crash PendingIntent trên Android 12 trong các thư viện đã được vá vào cuối năm 2021.

Kiểm tra lại sau khi sửa

Đừng chỉ compile rồi ship. Hãy test trên thiết bị thật API 31+ hoặc emulator:

# Tạo AVD với API 31 qua command line
avdmanager create avd -n test31 -k "system-images;android-31;google_apis;x86_64"

# Khởi động emulator
emulator -avd test31

# Cài app và theo dõi logcat để tìm exception cụ thể
adb logcat | grep -E "IllegalArgumentException|PendingIntent"

Thử qua từng tính năng có dùng PendingIntent: nhấn tất cả nút action trong notification, kích hoạt scheduled alarm, test deep link. Nếu logcat không có gì và các tính năng hoạt động bình thường, bạn đã xong.

Cũng hãy chạy UI test với:

./gradlew connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.yourapp.NotificationTest

Bài học từ cuộc khủng hoảng lúc 2 giờ sáng

Crash này đặc biệt khó chịu vì hoàn toàn vô hình cho đến khi bạn flip targetSdkVersion. Ba điều có thể phát hiện sớm hơn:

  • Lint rule — Android Studio có lint check cho vấn đề này từ AGP 7.0. Chạy ./gradlew lint và tìm cảnh báo UnspecifiedImmutableFlag trước khi nâng targetSdk.
  • Test trên emulator API 31 trước khi release — nghe có vẻ hiển nhiên sau khi xảy ra, nhưng nhiều team chỉ test trên thiết bị vật lý của mình đang chạy OS cũ hơn.
  • Grep trước khi nâng targetSdk — năm phút với grep -rn "PendingIntent.get" app/src/ có thể tiết kiệm ba tiếng đồng hồ vào lúc nửa đêm.

Yêu cầu về cờ này không phải tùy chọn. Android 12 thực thi nó ở tầng OS để ngăn các ứng dụng vô tình để lộ PendingIntent có thể bị khai thác qua Intent redirection. Hãy coi đây là hygiene bắt buộc khi target API 31+.

Related Error Notes