Android Internals · System-Level Deep Dive

Broadcast
Receiver

Android's publish-subscribe backbone. A system-wide event bus where the kernel, framework, and apps broadcast signals — and any authorized listener can react, regardless of whether it's running. Understand the design from the inside out.

1→N
Delivery Model
One sender, unlimited receivers. No point-to-point coupling.
10s
Receiver Time Limit
onReceive() must finish in 10 seconds or ANR is triggered by the system.
API 26
Background Limits
Oreo restricted implicit broadcast receivers in manifests — a major design shift.
01 — The Problem Space

Why Does the OS Need a Broadcast System?

Android is a multi-process, multi-application OS. At any moment, dozens of apps and system services exist in separate Linux processes, with strict UID-level isolation. They cannot call each other's methods, read each other's memory, or subscribe to each other's events directly.

Yet real devices generate a constant stream of events — power connected, network changed, SMS received, time zone updated, screen turned off. Every app that cares needs to know. Without a system-level pub/sub mechanism, each app would need to poll indefinitely — burning CPU, draining battery, and producing race conditions.

"Broadcast Receiver is the OS saying: I have an event. I don't care who's listening, or whether they're running. If you registered interest, you'll hear it — and you'll hear it exactly once."

🔌

System Events

Hardware and OS state changes — battery low, boot completed, airplane mode toggled — need to reach all interested apps without the OS knowing who they are in advance.

📦

App-to-App Messaging

Apps need loose coupling. Sending an Intent broadcast lets an app signal an event without knowing — or caring — which other apps handle it.

💤

Dead-Process Delivery

The receiver doesn't need to be running. The system can start the receiver's process on demand — making it possible to react to events even after a force-stop.

02 — Taxonomy

Four Kinds of
Broadcasts

Android has evolved the broadcast system across many API versions. Today there are four distinct types, each with different delivery guarantees, ordering, and security implications.

Type
Behavior
Use Case
Order
Normal
Delivered asynchronously to all receivers simultaneously. No ordering guarantee. Receivers cannot abort.
General event notification (battery changed, connectivity changed).
Unordered
Ordered
Delivered one receiver at a time, in priority order. Each receiver can modify the result or abort the chain entirely.
SMS interception, custom URL scheme handling, security policy enforcement.
Priority
Sticky
Last Intent is cached by the system. New receivers immediately receive the last value even after the broadcast is sent. Deprecated API 21+.
Battery state (ACTION_BATTERY_CHANGED) — any late subscriber still gets current state.
Immediate
Local
Stays within the same process. Uses LocalBroadcastManager (now deprecated) or in-process event bus. Never crosses IPC boundary.
In-app event bus. Fragment ↔ Activity messaging.
In-process

Implicit vs. Explicit

Beyond type, broadcasts are also either implicit (addressed to an action string, any matching receiver can respond) or explicit (addressed directly to a component by class name or package). Since API 26, most implicit broadcasts cannot be received by manifest-registered receivers — apps must use runtime-registered receivers or explicit broadcasts instead.

System vs. App Broadcasts

System broadcasts (like android.intent.action.BOOT_COMPLETED) are sent by the OS with signature-level or special permissions. App broadcasts are custom actions defined by developers. System broadcasts are protected — third-party apps cannot spoof them because sendBroadcast() from a non-system process using a protected action is rejected by ActivityManagerService.

03 — Delivery Pipeline

How a Broadcast Travels
Through the System

When your app calls sendBroadcast(intent), that call enters a deep pipeline involving ActivityManagerService, Binder IPC, PackageManager lookups, and process lifecycle management. Here's every step.

📱 Caller Your App
IPC Binder
🧠 System AMS
📋 Registry PMS Lookup
🔀 Queue BroadcastQueue
📬 Dispatch Receivers
1

sendBroadcast() → Binder → ActivityManagerService

Your call goes through ContextImpl.sendBroadcast(), which immediately delegates via Binder IPC to ActivityManagerService (AMS) running in the system_server process. Your process is unblocked immediately — the call is fire-and-forget. AMS handles everything from here.

2

AMS resolves receivers via PackageManager

AMS calls PackageManagerService.queryBroadcastReceivers(intent) to find all manifest-registered receivers matching the Intent's action, category, and data. It also checks its in-memory list of runtime-registered receivers (from registerReceiver() calls). Permission filters and background restrictions are applied here.

3

BroadcastQueue enqueues the record

AMS has two BroadcastQueues: a "foreground" queue (for broadcasts with Intent.FLAG_RECEIVER_FOREGROUND) and a "background" queue. The broadcast record is enqueued to the appropriate queue. Normal broadcasts go background; system critical ones go foreground with lower latency budget.

⚠ Two Queues, Different SLAs

Foreground broadcasts must be processed within ~10 seconds per receiver. Background broadcasts have a 60-second timeout. If your onReceive() blocks for too long, AMS declares an ANR — even for background receivers.

4

Process spawning (if receiver's process isn't running)

For manifest-registered receivers, AMS checks if the receiver's process is alive. If not, it calls startProcessLocked() — forking from Zygote, initializing the Application class, and then delivering the broadcast. This means a broadcast can cold-start a dead app in milliseconds just to run your 10-line onReceive().

5

onReceive() is called on the main thread

AMS delivers the Intent via Binder to the receiver's process. The ActivityThread (which runs your app's main thread message loop) receives the RECEIVER message and invokes BroadcastReceiver.onReceive(context, intent) synchronously on the main thread. Once this method returns, AMS considers the receiver done — and can kill the process.

Kotlin — BroadcastReceiver with goAsync()
@Deprecated("Use WorkManager or JobScheduler for background work")
class MyReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        // onReceive() runs on main thread. DO NOT block here.
        // If you need async work: use goAsync() to extend the window.

        val pendingResult = goAsync() // tells AMS "not done yet"

        CoroutineScope(Dispatchers.IO).launch {
            try {
                // Do work on a background thread — still within ~10s budget
                processIntentData(intent)
            } finally {
                pendingResult.finish() // MUST call — releases AMS hold
            }
        }
    }
}

// MANIFEST registration (static) — survives app death
<receiver android:name=".MyReceiver" android:exported="false">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
    </intent-filter>
</receiver>

// RUNTIME registration (dynamic) — tied to component lifecycle
val filter = IntentFilter(Intent.ACTION_BATTERY_LOW)
val receiver = MyReceiver()
ContextCompat.registerReceiver(
    context, receiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED
)
// Always unregister to prevent leaks:
unregisterReceiver(receiver)
04 — Ordered Delivery

Ordered Broadcasts:
A Chain of Responsibility

Ordered broadcasts implement the Chain of Responsibility pattern at the OS level. AMS dispatches the Intent to receivers one at a time, sorted by android:priority (range: -1000 to 1000, default 0).

Each receiver can:

  • READ the result data set by previous receivers via getResultData()
  • WRITE modified result back with setResultData() — passed to the next receiver
  • ABORT the chain entirely with abortBroadcast() — downstream receivers never see it

This is exactly how SMS apps intercept messages: the SMS provider sends an ordered broadcast; your SMS app registers at high priority (999), reads the PDU, possibly shows a custom notification, and aborts so the default handler doesn't also fire.

Kotlin — Ordered broadcast sender & receiver
// SENDER: ordered broadcast with initial result
sendOrderedBroadcast(
    intent,
    "com.example.RECEIVE_PERMISSION",
    finalReceiver,  // called last always
    null,           // handler
    Activity.RESULT_OK,
    "initial data",
    null
)

// RECEIVER: high-priority interceptor
class SmsInterceptor : BroadcastReceiver() {
    override fun onReceive(ctx: Context, intent: Intent) {
        val prev = resultData  // from higher-priority receiver
        
        if (shouldBlock(intent)) {
            abortBroadcast() // ← kills the chain here
            return
        }

        // Modify and pass down
        setResultData("modified: $prev")
    }
}

<!-- Manifest: priority 999 = very early in chain -->
<receiver android:name=".SmsInterceptor">
    <intent-filter android:priority="999">
        <action android:name="android.provider.Telephony.SMS_RECEIVED"/>
    </intent-filter>
</receiver>
05 — Lifecycle & Threading

The Most Misunderstood
Lifecycle in Android

BroadcastReceiver's lifecycle is unique — it begins when onReceive() is called and ends when it returns. That's it. This short window has profound implications for what you can and cannot do.

⏱️

10-Second Hard Limit

AMS sets a watchdog timer per receiver. Exceed 10 seconds on the main thread and the system force-kills your process with an ANR. There are no exceptions. This is kernel-level enforcement, not a suggestion.

🧵

Always Main Thread

onReceive() is always called on the main thread, regardless of which thread called sendBroadcast(). You cannot bind a service, do network I/O, or block. Even room queries block the main thread if called here.

🪄

goAsync() Trick

Call goAsync() to get a PendingResult token. This extends the ANR window slightly and lets you hand off work to another thread — but you must call pendingResult.finish() when done or the process hangs.

🔬 Why the Main Thread? The ActivityThread Message Loop

Android apps have a single-threaded event loop running on the main thread, implemented as a Looper/Handler message queue inside ActivityThread. When AMS sends a broadcast to your process via Binder, the Binder thread posts a RECEIVER message to this queue. The main thread's Looper dequeues it and calls your onReceive(). This is the same queue that handles touch events, frame renders, and Activity lifecycle — which is exactly why blocking it for more than a few milliseconds causes visible jank, and blocking for 10+ seconds triggers ANR.

Registration: Static vs. Dynamic — When to Use Each

Aspect Static (Manifest) Dynamic (Runtime)
Survives app death ✓ Yes — system starts process ✗ No — dies with component
Implicit broadcasts (API 26+) ✗ Mostly blocked ✓ Allowed
Active only when app running ✗ Always active ✓ Controlled lifecycle
Battery/memory cost ⚠ Higher — wakes process Lower — already running
Can receive in background ✓ Yes (exceptions apply) ✗ Only if process alive
BOOT_COMPLETED ✓ Required ✗ Not possible
06 — Security Model

Who Can Hear,
Who Can Speak

Broadcasts cross process boundaries — which makes them a potential attack surface. Android applies layered permission enforcement on both the sender and receiver sides. Understanding both is critical to writing secure apps.

01

Sender-Side Permission

Pass a permission string as the second argument to sendBroadcast(intent, permission). Only receivers that have declared that permission in their manifest will receive the broadcast. Prevents eavesdropping by unprivileged apps.

02

Receiver-Side Permission

Declare android:permission on your <receiver> tag. Only senders holding that permission can deliver to you. Prevents spoofing — an untrusted app cannot impersonate the system by sending a broadcast to your protected receiver.

03

android:exported="false"

Required since API 33. Receivers with intent-filter must explicitly declare exported. Setting it to false means only your own app (or apps signed with the same certificate) can send broadcasts to this receiver — the system won't route any external intent to it.

04

Protected System Broadcasts

Actions like android.intent.action.BOOT_COMPLETED are declared in the platform manifest with protectionLevel="signature". Only the system (UID 1000) can send them. AMS verifies the caller's UID before allowing protected action broadcasts — non-system processes that try to send them get a SecurityException.

05

Background Launch Restrictions (API 26+)

Since Oreo, most implicit broadcasts cannot be received by manifest-registered receivers. Apps in the background cannot start Activities from broadcasts. These restrictions are enforced by AMS during dispatch — matching receivers in stopped packages are skipped entirely unless the broadcast carries FLAG_INCLUDE_STOPPED_PACKAGES.

Kotlin — Secure broadcast patterns
// ✓ CORRECT: Send with permission + explicit package
val intent = Intent("com.example.MY_ACTION").apply {
    setPackage("com.trusted.app")         // explicit = no broadcast hijack
    addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
}
sendBroadcast(intent, "com.example.RECEIVE_PERMISSION")

// ✓ CORRECT: Receive securely — require sender permission
ContextCompat.registerReceiver(
    context,
    myReceiver,
    IntentFilter("com.example.MY_ACTION"),
    "com.example.SEND_PERMISSION",     // sender must hold this
    null,
    ContextCompat.RECEIVER_NOT_EXPORTED  // not visible to other apps
)

// ✗ WRONG: No permission, no package — any app can send/receive
sendBroadcast(Intent("com.example.MY_ACTION"))
registerReceiver(myReceiver, IntentFilter("com.example.MY_ACTION"))
07 — Modern Android & Alternatives

When to Use Broadcasts
in 2024 and Beyond

"Since API 26, Broadcasts have become a last resort for background work. The right question is not 'how do I use a BroadcastReceiver?' but 'do I actually need one?'"

When Broadcast Receivers Are Still Right

  • Responding to system events: BOOT_COMPLETED, ACTION_POWER_CONNECTED, ACTION_HEADSET_PLUG
  • Receiving SMS/MMS or telephony events (requires manifest receiver)
  • Inter-app signaling where you control both sender and receiver
  • Alarm triggers from AlarmManager that need to wake the device
  • MediaSession control commands and notification actions

Modern Alternatives

  • USEWorkManager — for deferrable background work triggered by constraints (network, charging)
  • USEKotlin Flow / SharedFlow — for in-app reactive event streams instead of LocalBroadcastManager
  • USEFCM Push Notifications — to wake an app from the server side without polling
  • USEJobScheduler / Exact Alarms — for time-based triggers with battery awareness
  • USELiveData / StateFlow — for in-process reactive UI state, replacing ordered result passing
08 — Design Philosophy

Why Android Chose
This Design

Decoupling Over Coupling

The sender of a broadcast doesn't know who receives it. The system bridges the gap — making it possible for third-party apps to react to events that the original developer never anticipated. This is the Open-Closed Principle applied to an entire OS.

Fire-and-Forget for Battery

The alternative to broadcasts is polling — apps waking periodically to check if something changed. Broadcast push delivery is orders of magnitude more efficient: the receiver's process only runs when there's actual work to do, then dies immediately. The 10-second hard limit enforces this discipline.

Intent as Universal Message

Using Intent as the message format was a deliberate unification. The same data structure that launches Activities and starts Services also carries broadcast data. Any Intent extra, action string, or URI that works in one context works in another — the broadcast system gets type-checked, extensible messaging for free.

The Trade-offs Android Made

What they gained: A universal, decoupled, process-agnostic pub/sub system that works across the entire OS — from kernel-level power events to app-defined custom actions. No shared memory, no direct coupling, no polling.

What it cost them: Uncontrolled broadcast receivers could wake hundreds of processes simultaneously (broadcast storms). Every major Android version since Lollipop has progressively restricted what manifest receivers can do — culminating in Oreo's API 26 restrictions that killed most implicit background receivers.

The lesson: The original design was too permissive. The flexibility that made broadcasts powerful also made them a battery drain vector. The API 26 restrictions are Android admitting that background wakeups must be gated — but rather than removing broadcasts, they refined them, leaving the mechanism intact while removing the most abusive usage patterns.

The fact that Broadcasts still exist and are still the only correct tool for system events is a testament to the soundness of the original design: the problem was implementation abuse, not the model itself.