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:
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:
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:
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:
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
// // 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).
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:
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:
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.