Search code examples
androidbroadcastreceiverandroid-14

What is the system broadcast exception to the Android 14 rule on receivers specifying export behaviour?


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:

  • According to the documentation on system broadcasts, system broadcasts are those defined in the broadcast_actions.txt in the Android SDK
  • The referenced action string ACTION_DOWNLOAD_COMPLETE is equal to android.intent.action.DOWNLOAD_COMPLETE
  • The action android.intent.action.DOWNLOAD_COMPLETE does appear in the broadcast_actions.txt for Android 14

However, 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?


Solution

  • 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.

    System vs protected broadcasts

    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.

    Looking in the source code

    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.

    Determining protected broadcasts

    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.

    Trying it out

    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.

    Checking the value of 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.