The Problem
Upgrading your app to targetSdkVersion 34 (Android 14) often comes with a frustrating surprise. Your app might crash the exact moment it tries to register a BroadcastReceiver dynamically. This usually happens during app startup or when navigating to a specific feature. The stack trace will look like this:
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
The Security Rationale
Google is tightening app security to prevent "broadcast hijacking." Previously, receivers registered in your code (not in the Manifest) were somewhat ambiguous. The system didn't strictly enforce whether other apps on the device could send broadcasts to them.
Starting with API 34, you must be explicit. You have to state if your receiver is exported (accessible to other apps) or not exported (private to your app). If you skip this flag and your receiver listens for anything other than specific system events like ACTION_AIRPLANE_MODE_CHANGED, Android kills the process to protect your data.
Step-by-Step Fix
1. Locate Your Registration Calls
Search your codebase for every instance of registerReceiver(). You are looking for the "old way" of doing things, which lacks the third flag argument:
// This will now crash on Android 14 devices
registerReceiver(myReceiver, intentFilter);
2. Choose and Apply the Correct Flag
In 90% of cases, your receiver is only used for internal app communication. For these, use RECEIVER_NOT_EXPORTED. It keeps your app's internal logic hidden from other installed applications.
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. Maintain Backward Compatibility
These flags were introduced in API 33. If you use Context.RECEIVER_NOT_EXPORTED directly on an Android 12 device, your app will crash with a NoSuchFieldError.
The cleanest solution is to use ContextCompat from the AndroidX library. It handles the version checks for you, ensuring your app runs smoothly on everything from Android 5.0 to 14.
// The recommended modern approach
ContextCompat.registerReceiver(
context,
myReceiver,
new IntentFilter("com.example.UPDATE_UI"),
ContextCompat.RECEIVER_NOT_EXPORTED
);
System Broadcast Exceptions
If you are registering for a system-only broadcast—like Intent.ACTION_POWER_CONNECTED—the flag isn't strictly mandatory. However, don't rely on the exception. Adding the flag explicitly makes your code future-proof and easier for your teammates to read.
Verification Checklist
- **Deploy to API 34:** Test on a physical Android 14 device or an emulator.
- **Trigger the Receiver:** Ensure the `registerReceiver` line actually executes.
- **ADB Testing:** If you used `RECEIVER_NOT_EXPORTED`, try triggering it from your terminal: **`adb shell am broadcast -a com.example.UPDATE_UI`. The receiver should not** fire if the security is working correctly.
Troubleshooting Tips
- **Third-Party SDKs:** If your code looks fine but the crash persists, check your dependencies. Older versions of analytics or ad SDKs often use outdated registration methods. Update them to their latest versions.
- **Legacy Code:** If you are still using `LocalBroadcastManager`, you won't see this crash. However, Google deprecated it years ago. It is a great time to migrate that logic to **Kotlin Flows** or **LiveData**.

