According to the documentation on Android 14 behaviour changes, context-registered receivers should use a flag to indicate whether or not the receiver should be exported. However there is an exception listed for receivers that only receive system broadcasts:
If your app is registering a receiver only for system broadcasts through Context#registerReceiver methods, such as Context#registerReceiver(), then it shouldn't specify a flag when registering the receiver.
(emphasis added)
I have a fragment which registers a receiver for broadcasts of ACTION_DOWNLOAD_COMPLETE
. Here is the relevant code inside the fragment's onCreateView
:
if (getActivity()!=null)
getActivity().registerReceiver(onDownloadComplete,new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
Here, onDownloadComplete
is a class field of type BroadcastReceiver
which is created in the field initializer (i.e. at construction time):
private BroadcastReceiver onDownloadComplete = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// do stuff
}
};
I was under the impression that this receiver is one which only listens to system broadcasts (and therefore the flag should NOT be specified according to the above mentioned exception), because:
broadcast_actions.txt
in the Android SDKACTION_DOWNLOAD_COMPLETE
is equal to android.intent.action.DOWNLOAD_COMPLETE
android.intent.action.DOWNLOAD_COMPLETE
does appear in the broadcast_actions.txt
for Android 14However, the app still crashes when entering that fragment on Android 14, giving an error message about the lack of export flag.
Looking at the documentation, I reached the conclusion that I shouldn't specify the flag because it fell within the exception, but it seems that is not the case. What have I misunderstood from the documentation and can somebody explain what the correct interpretation of this exception is?
Okay, I believe I've figured this out and it's down to misleading documentation.
TL;DR: In order to determine the allowed "system broadcasts" (more accurately "protected broadcasts") for context-registered receivers which don't need export flags, we need to consult the AndroidManifest.xml
in the core/res
directory of the relevant platform source code, and look for the <protected-broadcast>
elements, rather than looking at broadcast_actions.txt
referred to by the documentation.
As I mentioned in my question, the Broadcasts overview document refers the reader to BROADCAST_ACTIONS.TXT
for a list of so-called "system broadcasts". It defines the system broadcasts as (paraphrasing) "broadcasts automatically sent by the system when various system events occur, such as when the system switches in and out of airplane mode. System broadcasts are sent to all apps that are subscribed to receive the event."
This may well be true within that meaning of the term "system broadcasts", however it's not the same meaning as used within the Android 14 behavior changes documentation. It's rather misleading that this document links to the broadcasts overview one for more information on what it calls "system broadcasts".
In the Android source code handling this new Android 14 behaviour change, the term "system broadcast" is not used - instead it uses the term "protected broadcast" which is more accurate. While "system broadcasts" are defined above as those which the system sends, "protected broadcasts" are those which only the system can send. There is an associated <protected-broadcast>
manifest element which registers a broadcast action and tells the OS that only system-level apps can send broadcasts of this action. There is little to no documentation about this - most probably because only a tiny minority of people are developing system-level apps - so I've had to delve into the source code to solve this problem.
When Context.registerReceiver
is called, it goes through a chain of method calls inside the context, eventually ending up at registerReceiverInternal
. This then calls ActivityManager.getService().registerReceiverWithFeature(...)
The return type of ActivityManager.getService()
is IActivityManager
, but confusingly ActivityManager
doesn't implement IActivityManager
, it's actually ActivityManagerService
which implements this. Inside this beast of a class (20k+ lines) we see the exception:
if (!onlyProtectedBroadcasts) {
if (receiver == null && !explicitExportStateDefined) {
// sticky broadcast, no flag specified (flag isn't required)
flags |= Context.RECEIVER_EXPORTED;
} else if (requireExplicitFlagForDynamicReceivers && !explicitExportStateDefined) {
throw new SecurityException(
callerPackage + ": One of RECEIVER_EXPORTED or "
+ "RECEIVER_NOT_EXPORTED should be specified when a receiver "
+ "isn't being registered exclusively for system broadcasts");
// Assume default behavior-- flag check is not enforced
} else if (!requireExplicitFlagForDynamicReceivers && (
(flags & Context.RECEIVER_NOT_EXPORTED) == 0)) {
// Change is not enabled, assume exported unless otherwise specified.
flags |= Context.RECEIVER_EXPORTED;
}
} else if ((flags & Context.RECEIVER_NOT_EXPORTED) == 0) {
flags |= Context.RECEIVER_EXPORTED;
}
We can easily see from the calling code that receiver!=null
. explicitExportStateDefined
is a flag to say whether the export flag was explicitly passed (which it wasn't as per my question), and requireExplicitFlagForDynamicReceivers
effectively just boils down to a check that the app is targeting Android 14 or above. So just the onlyProtectedBroadcasts
flag is left.
Looking up in the same class we can see that onlyProtectedBroadcasts
is determined by calling AppGlobals.getPackageManager().isProtectedBroadcast(action)
on each action, and the overall value is true only if this method returns true for every action. Similarly to before, AppGlobals.getPackageManager
returns an object of type IPackageManager
but the implementation is actually PackageManagerService
. It's using a package-protected field mProtectedBroadcasts
to determine isProtectedBroadcast
.
At this point I got a bit stuck, but I found that InstallPackageHelper
is populating mProtectedBroadcasts
based on protectedBroadcasts
within the package (method commitPackageSettings
), and that the protectedBroadcasts
property is set in PackageParser
method parseBaseApkCommon
, based on the aforementioned <protected-broadcast>
elements of the base APK, which are referred to here as com.android.internal.R.styleable.AndroidManifestProtectedBroadcast
.
These can be found in the AndroidManifest.xml
for that platform version. For example, looking at Android 14, it is quite clear that comparing protected broadcast actions in AndroidManifest.xml
and broadcast actions in broadcast_actions.txt
, there are some discrepancies - in particular android.intent.action.DOWNLOAD_COMPLETE
mentioned in my question is in the latter list but not the former.
I tried out registering broadcast receivers for various actions without export flags and it works as I expected based on the above.
In Kotlin I wrote this helper function:
private fun registerDummyReceiver(context: Context, action: String): Boolean {
val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// do nothing
}
}
val intentFilter = IntentFilter(action)
try {
context.registerReceiver(broadcastReceiver, intentFilter)
return true
} catch (ex: SecurityException) {
return false
}
}
Then, calling it:
registerDummyReceiver(context, Intent.ACTION_BOOT_COMPLETED) // returns true
registerDummyReceiver(context, DownloadManager.ACTION_DOWNLOAD_COMPLETE) // returns false
This is what we now expect, as the first is a protected broadcast, while the second, while a system broadcast, is not a protected broadcast.
isProtectedBroadcast
To be doubly sure, I wanted to actually check the return value of isProtectedBroadcast
. Unfortunately, Google implemented restrictions on this "non-SDK method" so it can't be called on apps targeting API 26+. So I created a new dummy app targeting API 25 and wrote the following code:
private fun isProtectedBroadcast(action: String): Boolean {
val appGlobalsClass = Class.forName("android.app.AppGlobals")
val getPackageManagerMethod = appGlobalsClass.getDeclaredMethod("getPackageManager")
val packageManager = getPackageManagerMethod.invoke(null)
val packageManagerClass = packageManager.javaClass
val isProtectedBroadcastMethod = packageManagerClass.getDeclaredMethod("isProtectedBroadcast", String::class.java)
return isProtectedBroadcastMethod.invoke(packageManager, action) as Boolean
}
Then, calling it:
isProtectedBroadcast(Intent.ACTION_BOOT_COMPLETED) // returns true
isProtectedBroadcast(DownloadManager.ACTION_DOWNLOAD_COMPLETE) // returns false
Again, this confirms what was said above.