Implementing a Broadcast Blacklist Framework in AOSP.

Implementing a Broadcast Blacklist Framework in AOSP.

We have reached the tenth stage of our journey into the deep, hidden and intricate world of Android OS.

In this article we will explore how Android handles BroadcastReceivers, key components that allow apps to respond to system events (like boot completion, incoming SMS, or network connectivity changes) or app-specific events.

The management of these components is handled by ActivityManagerService (AMS), one of Android’s core system services responsible for the lifecycle of activities (1.), services, and, of course, BroadcastReceivers.

In the first part of this article, we will take a deep dive into how AMS handles the registration of BroadcastReceivers and the delivery of broadcast intents to registered receivers.

The second part is where things get really interesting, we will take on a challenging task: implementing a restriction layer to control broadcast delivery. We'll build a blacklist mechanism that prevents apps like Netflix or YouTube from receiving system boot-up events.??

Finally, we’ll wrap up this article with a lesser-discussed topic: ordered broadcast intents and the role of the sendOrderedBroadcast function.


?? Why is managing a blacklist for BroadcastReceivers useful?

If you take a look at the APKs of some Play Store apps, you’ll notice that many of them register the "android.intent.action.BOOT_COMPLETED" intent in their manifest. This means they automatically start as soon as the operating system is ready.

However, this can be a problem for embedded devices. For example, apps like Netflix and YouTube, launching at boot, can interfere with the multi-user implementation we developed in a previous article (2.). That’s why creating a dynamic blacklist for broadcast receivers is essential to prevent unwanted app behavior.


Alright, let’s begin our deep dive into how AMS manages BroadcastReceivers!


How the AMS manages BroadcastReceivers in Android.

The process of delivering a?Broadcast Intent?in Android is a complex mechanism involving several system components. This process is primarily managed by the?ContextImpl, ActivityManagerService and?BroadcastQueue?classes.

In this section we will explore in detail how the AMS manages?BroadcastReceivers, starting from their registration, moving on to sending a broadcast, and finally to the delivery and execution of the receiver's code. We will refer to the Android 12 codebase to understand the software flows and the methods involved.


1. Registering a BroadcastReceiver.

Before a?BroadcastReceiver?can receive events, it must be registered. There are two ways to do this:?statically?(via the?AndroidManifest.xml?file) or?dynamically?(via the?registerReceiver() a ?method of Context).

When an app calls?Context.registerReceiver(), the request is handled internally by the?ContextImpl?class, which acts as the implementation of the app's context. This method delegates the operation to the AMS via the?registerReceiver()?method.

This is the Registration flow:

  1. The app calls?Context.registerReceiver().
  2. The call is handled by?ContextImpl.registerReceiverInternal().
  3. The AMS receives the request and stores the receiver in a list of registered receivers.

This is a simplified extract of Registration Flow code:

// from ContextImpl.java

@Override
public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
    return registerReceiverInternal(receiver, getUserId(), filter, null, null, 0);
}

private Intent registerReceiverInternal(BroadcastReceiver receiver, int userId,   
                                          IntentFilter filter, String broadcastPermission, 
                                          Handler scheduler, int flags) {
        // Delegate the request to the ActivityManagerService
        return ActivityManager.getService().registerReceiverWithFeature(
                              mMainThread.getApplicationThread(), mBasePackageName,  
                              receiver, filter, broadcastPermission, userId, flags);
}        
// from ActivityManagerService.java

final HashMap<IBinder, ReceiverList> mRegisteredReceivers = new HashMap<>();

public Intent registerReceiverWithFeature(IApplicationThread caller, 
                   String callerPackage, IIntentReceiver receiver, IntentFilter filter, 
                   String permission, int userId, int flags) {
        // Store the receiver in the list of registered receivers
        mRegisteredReceivers.put(receiver.asBinder(), receiver);
        // ...
}        

This way, the AMS keeps track of all dynamically registered receivers from applications.


Here is the Registration Flow Sequence Diagram:



2. Sending a Broadcast.

When an app sends a broadcast intent (for example, using? Context.sendBroadcast()), the AMS steps in to determine which receivers are interested in the event and to manage its delivery.

This is the Sending flow:

  1. The app calls?Context.sendBroadcast().
  2. The call is handled by?ContextImpl.sendBroadcast(), which in turn invokes the AMS's?broadcastIntent()?method.
  3. The AMS analyzes the?Intent?and matches it against the?IntentFilters of registered receivers.
  4. If a receiver matches the intent, the AMS enqueues the broadcast for delivery.

This is a simplified extract of Sending Flow code:

// from ContextImpl.java

@Override
    public void sendBroadcast(Intent intent) {
            // ...
            ActivityManager.getService().broadcastIntentWithFeature(
                        mMainThread.getApplicationThread(), getAttributionTag(), 
                        intent, intent.resolveTypeIfNeeded(getContentResolver()), null, 
                        Activity.RESULT_OK, null, null, null, null, AppOpsManager.OP_NONE, 
                        null, false, false, getUserId());

    }        
// from ActivityManagerService.java

public final int broadcastIntentWithFeature(IApplicationThread caller, String 
                                 callingFeatureId, Intent intent, String resolvedType, 
                                 IIntentReceiver resultTo, int resultCode, String resultData, 
                                 Bundle resultExtras, String[] requiredPermissions, String[] 
                                 excludedPermissions, int appOp, Bundle bOptions,
                                 boolean serialized, boolean sticky, int userId) {
             // ...
            final ProcessRecord callerApp = getRecordForAppLOSP(caller);
            final int callingPid = Binder.getCallingPid();
            final int callingUid = Binder.getCallingUid();

            final long origId = Binder.clearCallingIdentity();
            return broadcastIntentLocked(callerApp, 
                                         callerApp != null ? callerApp.info.packageName : null,  
                                         callingFeatureId, intent, resolvedType, resultTo, resultCode, 
                                         resultData, resultExtras, requiredPermissions, 
                                         excludedPermissions, appOp, bOptions, serialized,
                                         sticky, callingPid, callingUid, callingUid, callingPid, userId);
}

final int broadcastIntentLocked(ProcessRecord callerApp, String callerPackage,
                                 @Nullable String callerFeatureId, Intent intent,                   
                                 String resolvedType, IIntentReceiver resultTo, 
                                 int resultCode, String resultData, Bundle resultExtras, String[] 
                                 requiredPermissions, String[] excludedPermissions, int appOp, 
                                 Bundle bOptions, boolean ordered, boolean sticky, int callingPid, 
                                 int callingUid, int realCallingUid, int realCallingPid, int userId,
                                 boolean allowBackgroundActivityStarts, 
                                 @Nullable IBinder backgroundActivityStartsToken,
                                 @Nullable int[] broadcastAllowList)  {
            // Find receivers matching the intent
            List<ResolveInfo> receivers = collectReceiverComponents(intent,   
                                                              resolvedType, userId);
             
             // Enqueue the broadcast for delivery: we'll see it in the next snippet
             // ...
}

        


Here is the Sending Flow Sequence Diagram:



3. Delivering the Broadcast.

Once the AMS has determined which receivers should receive the broadcast, the next step is delivery. This process is managed by the?BroadcastQueue?class, which handles enqueuing and delivering broadcasts in an orderly manner.

This is the Delivery flow:

  1. The AMS creates a?BroadcastRecord?object to represent the broadcast.
  2. The broadcast is added to a queue (BroadcastQueue) by calling the method enqueueParallelBroadcastLocked of BroadcastQueue class.
  3. The AMS schedules the delivery of the broadcast to the corresponding receivers by calling the method scheduleBroadcastsLockedof BroadcastQueue class

This is a simplified extract of Delivering Flow code:

// ActivityManagerService.java

final int broadcastIntentLocked(ProcessRecord callerApp, String callerPackage,
                                 @Nullable String callerFeatureId, Intent intent,                   
                                 String resolvedType, IIntentReceiver resultTo, 
                                 int resultCode, String resultData, Bundle resultExtras, String[] 
                                 requiredPermissions, String[] excludedPermissions, int appOp, 
                                 Bundle bOptions, boolean ordered, boolean sticky, int callingPid, 
                                 int callingUid, int realCallingUid, int realCallingPid, int userId,
                                 boolean allowBackgroundActivityStarts, 
                                 @Nullable IBinder backgroundActivityStartsToken,
                                 @Nullable int[] broadcastAllowList)  {
            // Find receivers matching the intent
            List<ResolveInfo> receivers = collectReceiverComponents(intent,   
                                                              resolvedType, userId);
             
             // Enqueue the broadcast for delivery
             // Create a BroadcastRecord for the broadcast
             BroadcastRecord r = new BroadcastRecord(intent, receivers, ...);
             // Add the broadcast to the queue
             mBroadcastQueue.enqueueParallelBroadcastLocked(r);
              // Schedule the delivery
             mBroadcastQueue.scheduleBroadcastsLocked();
}
        

The BroadcastQueue class manages the queue of broadcasts and ensures they are delivered efficiently. Here’s a snippet:

// from BroadcastQueue.java

final ArrayList<BroadcastRecord> mParallelBroadcasts = new ArrayList<>();
final BroadcastHandler mHandler;

private final class BroadcastHandler extends Handler {
        public BroadcastHandler(Looper looper) {
            super(looper, null, true);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case BROADCAST_INTENT_MSG: {
                    processNextBroadcast(true);
                } break;
                // ...
            }
        }
    }

public void enqueueParallelBroadcastLocked(BroadcastRecord r) {
        mParallelBroadcasts.add(r);
    }

public void scheduleBroadcastsLocked() {
          // ...
         mHandler.sendMessage(mHandler.obtainMessage(BROADCAST_INTENT_MSG,                                                                                          
                                                          this));
    }

    private void processNextBroadcast(boolean fromMsg) {
        synchronized (mService) {
            processNextBroadcastLocked(fromMsg, false);
        }
    }

   final void processNextBroadcastLocked(boolean fromMsg, boolean skipOomAdj) {
        BroadcastRecord r;

       while (mParallelBroadcasts.size() > 0) {
            r = mParallelBroadcasts.remove(0);
            final int N = r.receivers.size();
            for (int i=0; i<N; i++) {
                Object target = r.receivers.get(i);
                deliverToRegisteredReceiverLocked(r, (BroadcastFilter) target, false, i);
            }
        }
}        

Below is a brief explanation of the methods in the BroadcastQueue class in Android, along with the role of the BroadcastHandler:

  1. enqueueParallelBroadcastLocked: Adds a broadcast to the queue.
  2. scheduleBroadcastsLocked: Schedules the delivery of broadcasts by sending a message to the BroadcastHandler.
  3. processNextBroadcast: Initiates the processing of the next broadcast.
  4. processNextBroadcastLocked: Handles the delivery of the next broadcast.
  5. deliverToRegisteredReceiverLocked: Delivers the broadcast to a specific receiver.
  6. BroadcastHandler: Manages the asynchronous delivery of broadcasts and ensures proper scheduling.

Here is the Delivering Flow Sequence Diagram:



4. Executing the BroadcastReceiver.

The final phase is the execution of the?BroadcastReceiver's code. The AMS invokes the receiver's?onReceive()?method, which runs on the main thread.

Execution Flow

  1. The scheduleBroadcastsLocked method is called to initiate the delivery of broadcasts. This method sends a message to the BroadcastHandler to start processing the next broadcast in the queue.
  2. The BroadcastHandler receives the message and calls the processNextBroadcast method to handle the delivery of the next broadcast. This method is the entry point for processing the next broadcast in the queue.
  3. The processNextBroadcast method calls processNextBroadcastLocked, which handles the actual delivery of the broadcast.
  4. The deliverToRegisteredReceiverLocked method is responsible for calling the performReceiveLocked()?method.
  5. The receiver's?performReceive()?method is called.
  6. The receiver's?onReceive()?method is executed.

// // BroadcastQueue.java

    private void deliverToRegisteredReceiverLocked(BroadcastRecord r,
                                   BroadcastFilter filter, boolean ordered, int index)  {
     // ...
    // Invoke the receiver's performReceive() method
    performReceiveLocked(filter.receiverList.app, filter.receiverList.receiver,
                        new Intent(r.intent), r.resultCode, r.resultData,
                        r.resultExtras, r.ordered, r.initialSticky, r.userId);
}

void performReceiveLocked(ProcessRecord app, IIntentReceiver receiver,
            Intent intent, int resultCode, String data, Bundle extras,
            boolean ordered, boolean sticky, int sendingUser) {
             // Invoke the receiver's performReceive() method
             receiver.performReceive(intent, resultCode, data, extras, ordered,
                    sticky, sendingUser);
}        
// IIntentReceiver.java (implemented by the receiver)

public void performReceive(Intent intent, int resultCode, String data, Bundle extras,
        boolean ordered, boolean sticky, int sendingUser) {
        // Invoke the receiver's onReceive() method
        receiver.onReceive(context, intent);
}        


Here is the Execution Flow Sequence Diagram:



?? In this deep dive into how Android internally handles broadcasts, we left out one important aspect. To keep things simpler, we focused only on parallel broadcasts.

However, there’s another type: ordered broadcasts, and their handling is quite different.

The key difference between these two types of broadcast intents is that Parallel broadcasts are delivered to all receivers at the same time, with no specific order or dependency while Ordered broadcasts are delivered one at a time, in sequence, and each receiver has the ability to modify or even abort the transmission before passing it to the next one.

In the ContextImpl class the sendBroadcast method sends intents in parallel mode, instead the sendOrderedBroadcast method sends them in ordered mode.

In this article, we won’t explore how AMS manages ordered broadcast intents internally. However, in the final section, we’ll see a practical application of ordered broadcasts in action!


Great! We've reached the most exciting part of this article. Let's roll up our sleeves and figure out how we can implement a blacklist for BroadcastReceivers! ??


Implementing a Broadcast Blacklist Framework in AOSP.

In Android, apps can register to receive system broadcasts, such as android.intent.action.BOOT_COMPLETED and android.permission.BLUETOOTH_CONNECT.

When these broadcasts are received, apps like Netflix and YouTube can start in the background, even if they were not previously running. While this behavior is generally expected, it can create issues in specific scenarios, such as real multi-user implementations (2.) where unnecessary app startups should be avoided.

To prevent certain applications from receiving specific system broadcasts, we need to design an additional restriction layer (a specialized BroadcastReceivers blacklist for each app) that integrates with AMS in the AOSP (Android Open Source Project). Managing this blacklist ensures that restricted apps do not receive certain broadcast intents.

?? Our solution hooks into the broadcastIntentLocked method inside AMS. As we saw in the first part of this article, every time we send broadcast intents whether in parallel mode (using sendBroadcast) or in ordered mode (using sendOrderedbroadcast) they always go through the broadcastIntentLocked method in AMS.

This new restriction mechanism is based on the BroadcastBlacklist class, which manages a list of restricted broadcasts for specific apps. It filters out blacklisted receivers before they can receive any broadcasts.


Here are the steps of the system boot-up flow with the additional restriction layer on BroadcastReceivers. This layer takes action when broadcast intents are sent, whether in parallel mode (sendBroadcast) or ordered mode (sendOrderedBroadcast).

  1. System Boot: AMS initializes a BroadcastBlacklist instance.
  2. Blacklist Parsing: The framework reads a predefined configuration file containing restricted (package name, broadcast action) pairs.
  3. Broadcast Filtering: Each time a broadcast is sent, the filterReceivers method of BroadcastBlacklist checks if any receiver should be removed from the list.
  4. Modified Broadcast Dispatching: Only allowed receivers get the broadcast, ensuring blacklisted apps do not receive restricted intents.


Here is an example:

below we see the sequence diagram for the android.intent.action.BOOT_COMPLETED system broadcast notification (3.),(4.),(5.). As you can see, the process includes an additional layer that prevents broadcast intents from reaching apps that are listed in the blacklist.



This solution requires modifications inside the Activity Manager Service (AMS) and an additional class to manage the blacklist. These are the Implementation Details:

1. Modifying AMS to Integrate Broadcast Blacklist

Inside ActivityManagerService.java, we modify the broadcastIntentLocked method to apply the blacklist filter:

final void broadcastIntentLocked(...) {
    // Existing logic for resolving broadcast receivers
    List<ResolveInfo> receivers = resolveBroadcastReceivers(intent, userId);
  
    // Apply the blacklist filter
    receivers = mBroadcastBlacklist.filterReceivers(receivers, intent);
  
    // Continue with the standard broadcast dispatch logic
    if (!receivers.isEmpty()) {
        for (ResolveInfo receiver : receivers) {
            deliverBroadcastToReceiver(receiver, intent);
        }
    }
}        


2. Creating the BroadcastBlacklist class

The blacklist is implemented as a map that links each app's package name to a list of intents that the app registers but that we want to block from reaching it.

It plays its role when called inside the broadcastIntentLocked method of AMS. This method is triggered every time a broadcast intent is sent, making it the perfect place to block the broadcast from reaching the receivers of apps listed in the blacklist.

public class BroadcastBlacklist {
    private final Map<String, Set<String>> blacklist = new HashMap<>();
 
    public BroadcastBlacklist() {
        loadBlacklistFromFile();
    }

    private void loadBlacklistFromFile() {
        // Load blacklist entries from a configuration file
        // Example format:   
        // com.google.android.youtube:android.intent.action.BOOT_COMPLETED
    }

    public List<ResolveInfo> filterReceivers(List<ResolveInfo> receivers, Intent intent) {
        List<ResolveInfo> filteredList = new ArrayList<>();
        for (ResolveInfo receiver : receivers) {
            String packageName = receiver.activityInfo.packageName;
            if (!isBlacklisted(packageName, intent.getAction())) {
                filteredList.add(receiver);
            }
        }
        return filteredList;
    }

    private boolean isBlacklisted(String packageName, String action) {
        return blacklist.containsKey(packageName) && 
        blacklist.get(packageName).contains(action);
    }
}        

3. Defining the Blacklist Configuration File

The blacklist configuration file could be a simple text or XML file located in the device storage memory, here is a fragment of this file:

android.intent.action.LOCALE_CHANGED:com.google.android.youtube
android.intent.action.BOOT_COMPLETED:com.google.android.youtube
android.intent.action.BOOT_COMPLETED:com.netflix.mediaclient
android.intent.action.TIMEZONE_CHANGED:com.netflix.mediaclient
android.intent.action.TIME_SET:com.netflix.mediaclient
android.net.conn.CONNECTIVITY_CHANGE:com.netflix.mediaclient
android.intent.action.BOOT_COMPLETED:com.disney.disneyplus
android.intent.action.TIMEZONE_CHANGED:com.disney.disneyplus
android.intent.action.TIME_SET:com.disney.disneyplus
android.net.conn.CONNECTIVITY_CHANGE:com.disney.disneyplus
android.intent.action.BOOT_COMPLETED:tv.pluto.android
android.intent.action.BOOT_COMPLETED:tunein.player
android.intent.action.TIMEZONE_CHANGED:tunein.player
android.intent.action.TIME_SET:tunein.player
android.net.conn.CONNECTIVITY_CHANGE:tunein.player
android.net.wifi.WIFI_STATE_CHANGED:tunein.player        


At system boot the BroadcastBlacklist object reads this file and loads the rules into memory.

In conclusion, we can say that this solution provides a lightweight and efficient way to block certain applications from receiving unwanted broadcasts without modifying their APKs. By integrating directly into AMS, we ensure that restricted broadcasts are removed before reaching the application layer.



Sending the Ordered Broadcast.

Let's wrap up this article with a practical example of using BroadcastReceivers in ordered mode. We’ve seen that the function:

sendOrderedBroadcast(Intent intent, String receiverPermission)        

is used to send an ordered broadcast, meaning the message is delivered to multiple BroadcastReceivers in order of priority.

Unlike sendBroadcast, which delivers the message to all receivers simultaneously, sendOrderedBroadcast sends it one receiver at a time, allowing:

  • Receivers to modify the broadcast data before passing it to the next one.
  • Receivers to stop the propagation by calling abortBroadcast().
  • The system to respect a priority order, set in the manifest or at runtime.

Now, let’s create a practical example with three BroadcastReceiver components, where one of them stops the broadcast from reaching the next receivers.

We’ll define three receivers (Receiver1, Receiver2, and Receiver3), each with a different priority and even in separate apps. In the AndroidManifest.xml file of each app, we’ll register its receiver and assign it a priority using the android:priority attribute.

Here are the code snippets for the first app (App1). Receiver1 has the highest priority (3), so it receives the message first and changes the data received:

public class Receiver1 extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        String data = getResultData(); // Ottiene i dati attuali del broadcast
        Log.d("Receiver1", "Received: " + data);
        
        // Modify data before passing it to the next receiver
        setResultData("Modified by Receiver1");
    }        
<!-- registration in manifest -–>
<receiver android:name=".Receiver1" android:exported="false">
    <intent-filter android:priority="3">
        <action android:name="com.example.MY_BROADCAST" />
    </intent-filter>
</receiver>        


Here are the code snippets for the second app (App2). Receiver2 has a priority of 2 (medium), so it receives the message after Receiver1 and blocks the propagation.

Receiver2 (Priorità Media, Blocca il Broadcast)
public class Receiver2 extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        String data = getResultData();
        Log.d("Receiver2", "Received: " + data);
        
        // Block broadcast, so Receiver3 will NOT receive it
        abortBroadcast();
    }
}        
<!-- registration in manifest -–>
<receiver android:name=".Receiver2" android:exported="false">
    <intent-filter android:priority="2">
        <action android:name="com.example.MY_BROADCAST" />
    </intent-filter>
</receiver>        


Here are the code snippets for the third app (App3). Receiver3 has a priority of 1 (low), but it will not receive the broadcast because Receiver2 has stopped it.

public class Receiver3 extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        String data = getResultData();
        Log.d("Receiver3", "Received: " + data);
    }
}        
<!-- registration in manifest -–>
<receiver android:name=".Receiver3" android:exported="false">
    <intent-filter android:priority="1">
        <action android:name="com.example.MY_BROADCAST" />
    </intent-filter>
</receiver>        


From another app, we send the broadcast using sendOrderedBroadcast:

sendOrderedBroadcast(intent, null, 
    new BroadcastReceiver() { // final receiver is opztional
        @Override
        public void onReceive(Context context, Intent intent) {
            Log.d("FinalReceiver", "Broadcast completed with data: " + getResultData());
        }
    }, 
    null,  // Handler (not used)
    0,     // Initial result code
    "Initial data",  // Initial broadcast data
    null   // Initial Bundle
);        

Here’s what happens:

  • The broadcast starts with the initial data: "Initial Data".
  • Receiver1 receives it, prints the message, and modifies it to "Modified by Receiver1".
  • Receiver2 receives the updated message, prints it, and stops the broadcast.
  • Receiver3 does NOT receive it because the broadcast was interrupted.
  • The final (optional) BroadcastReceiver receives the final data only if the broadcast reaches the end.

And this is the expected log output:

Receiver1: Received: Initial Data  
Receiver2: Received: Modified by Receiver1  
FinalReceiver: Broadcast completed with data: Modified by Receiver1.        

Personally, I’ve only needed to use ordered intents once, but they can definitely make sense in certain situations.

In particular, sendOrderedBroadcast can be useful when:

The order of receivers matters (e.g., for security or processing chains).

You want receivers to modify the data before passing it to the next one.

You need a receiver to have the ability to stop the broadcast.

You want a final receiver to collect the final data.



That's all for this topic, this article ends here.

In the next episode we will continue our journey into the deep, hidden and intricate world of Android OS.

I remind you my newsletter "Sw Design & Clean Architecture": https://lnkd.in/eUzYBuEX where you can find my previous articles and where you can register, if you have not already done, so you will be notified when I publish new articles.

Thanks for reading my article, and I hope you have found the topic useful,

Feel free to leave any feedback.

Your feedback is very appreciated.

Thanks again.

Stefano


References:

1. S.Santilli: Unlocking the Secrets of Android Activity Lifecycle: What Really Happens Behind the Scenes.

2. S.Santilli: Mastering ActivityManagerService: Unlocking the Hidden Potential of Android for Advanced Features Like Magic Multi-User Support.

3. S.Santilli: From Android Basics to Binder Mastery: A Journey Through IPC Mechanisms.

4. S.Santilli: A Deep Dive into the Android Boot Process: Step-by-Step Breakdown.

5. S.Santilli: Android Boot Process Part 2: From Zygote to SystemServer, SystemUI, and Launcher Initialization.

要查看或添加评论,请登录

Stefano Santilli的更多文章