AOSP Framework & Internals
6 min read

BroadcastReceiver

Learn how Android's publish-subscribe mechanism allows apps to respond to system-wide events.

The Polling Problem and the System-Wide Pulse

Apps operate in an unpredictable environment. The battery drains, network connections drop, and users toggle airplane mode. If every app had to poll the operating system continuously to detect these changes, your battery would die in hours. We need a way for the OS to announce state changes, and for interested apps to listen quietly without wasting CPU cycles.

The solution is the BroadcastReceiver. This component acts as an entry point into your application, responding directly to system or application events. It relies on a publish-subscribe model. You tell the system which specific intent actions your app cares about. When a matching event occurs, the OS creates an instance of your receiver class and executes its onReceive() method.

To start listening, you must register the receiver. You can do this dynamically in your code or statically in your manifest file. By decoupling senders from receivers, this architecture prevents apps from waking up unnecessarily. But decoupling creates a new challenge: how do these isolated components actually find each other without breaking security?

Routing the Message Through AMS

If one app could broadcast an event directly to another app, the system would have a massive security hole. Malicious apps could spam others or intercept sensitive data.

We need a central authority to route messages safely.

This authority is the ActivityManagerService (AMS). AMS acts as the central clearinghouse for all broadcasts. It maintains a massive internal dictionary of intent filters called the BroadcastQueue. When an event fires, AMS checks the queue to see exactly who asked to listen. The service then verifies permissions for both the sender and the receiver.

The sequence diagram below shows this routing process in action. It illustrates how the central AMS component intercepts a broadcast before fanning it out. Look for the permission check step that happens before the message reaches the destination apps.

As the diagram shows, the sender gets an immediate asynchronous return. AMS then takes over the heavy lifting of scheduling the execution on the main UI thread of each target application. This true isolation makes the system highly flexible and secure. Yet this routing architecture only works if AMS knows exactly who is listening.

Registration Mechanics and the Boot Storm

Knowing how broadcasts travel is useless if your app cannot receive them. The system needs a way to connect your receiver code to the internal AMS queues. You accomplish this through dynamic or static registration.

Dynamic registration happens at runtime using Context.registerReceiver(). A dynamic receiver only lives as long as the component that registered it. If you register it in an Activity's onResume() method, you must unregister it in onPause(). This ties the listening lifecycle directly to the user's active context.

BroadcastReceiver airplaneModeReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        boolean isOn = intent.getBooleanExtra("state", false);
        Log.d("Broadcast", "Airplane mode changed: " + isOn);
    }
};

IntentFilter filter = new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED);
context.registerReceiver(airplaneModeReceiver, filter);

Static registration tells the system about your receiver at install time via AndroidManifest.xml. Historically, if a static broadcast fired, the OS would spawn the app process even if it was completely dead. This caused the infamous "Boot Storm", where starting a device would wake up dozens of apps simultaneously and crush the CPU. Modern Android now severely restricts which intents can use static registration to prevent this massive battery drain. Even with safe registration limits, the system still needs a way to control the order of execution.

Controlling Broadcast Delivery Order

Some system messages are far more urgent than others. If a malicious app tries to intercept a banking SMS, the system must prioritize a legitimate security app first. Android solves this conflict with different delivery strategies.

The default strategy is the normal broadcast. Normal broadcasts are completely asynchronous. All registered receivers execute simultaneously in an undefined order. This approach is fast and highly efficient for the system. A major tradeoff is that a receiver cannot cancel the broadcast or pass data to the next receiver in line.

Ordered broadcasts solve this by delivering the intent to one receiver at a time. The android:priority attribute in the intent filter determines the exact sequence. A high-priority receiver like a spam blocker can intercept the broadcast, process it, and call abortBroadcast().

This instantly kills the message, preventing lower-priority receivers from ever seeing the event.

Common Mistake: Engineers used to rely on "Sticky Broadcasts", which remained in memory indefinitely. You could register a receiver long after a battery event fired and still get the last known state. These caused huge memory leaks and security vulnerabilities. Google has strictly deprecated them, so you should never use sticky broadcasts in modern development.

Properly configuring your delivery order prevents many race conditions. When bugs do arise, you must look directly at the system queues to see what went wrong.

Inspecting the Queue State

Platform engineers need to know if the broadcast system is backlogged. A single stuck receiver on the main thread will delay all subsequent broadcasts in that specific queue. You can inspect the live state of the AMS broadcast queues using the dumpsys utility.

The dumpsys activity broadcasts command dumps historical dispatch times, active receivers, and the current state of both the foreground and background queues. Run this check when diagnosing a system that feels sluggish or an app that is missing events.

adb shell dumpsys activity broadcasts

Engineers often forget that dumpsys outputs a massive amount of text. You should pipe the result through grep to filter for your specific application package. While debugging helps fix missed events, some problems stem from fundamentally misusing the receiver component itself.

The Limits of Immediate Execution

Your app finally receives a broadcast, but the required response takes ten seconds to run. The onReceive() method executes entirely on the main UI thread. If your reaction requires downloading a file or processing a database, you cannot do it here. Android will trigger an Application Not Responding error if you block the thread for more than a few seconds.

BroadcastReceivers excel at brief, transient reactions to state changes. They keep the OS efficient by decoupling senders from listeners. When you need to do heavy lifting, you need a component specifically designed for long-running background work. That requirement introduces a new challenge that Android solves with a completely different architecture.