Understanding Android Broadcast Receivers

Romman SabbirRomman Sabbir
6 min read

What is a Broadcast Receiver?

A Broadcast Receiver in Android is a component that lets your app listen for system-wide or app-specific messages. These messages, called "intents," indicate that an event has happened. For example, your app can listen for system messages like the device booting up or a low battery warning, as well as custom messages from other apps.

How Does It Work?

When an event happens, the Android system creates a broadcast intent that multiple apps can receive. The broadcast can be:

  • System Broadcasts: Sent by the Android system (e.g., ACTION_BOOT_COMPLETED, ACTION_BATTERY_LOW).

  • Custom Broadcasts: Sent by your app or other apps.

The Broadcast Receiver acts as a link between the system or other apps and your app, letting it respond to events without always running in the background.

Let's understand how it works

The Broadcast Receiver mechanism in Android uses the Binder IPC framework to send and receive messages between processes.

  • A bit about IPC:

IPC stands for inter-process communication. It describes how different Android components talk to each other.

  1. Intents are messages that components can send and receive. They are used to pass data between processes. With intents, you can start services or activities, invoke broadcast receivers, and more.

  2. Bundles are data containers passed through intents. They are like serialized objects but faster on Android. You can read a bundle from an intent using the getExtras() method.

  3. Binders allow activities and services to get a reference to another service. This lets you not only send messages to services but also call methods on them directly.

  • How broadcast works:

Broadcast Intent Creation:

  • When a broadcast is sent, an Intent object holds the action and any additional data. The Intent specifies what kind of broadcast is being sent and may include extra information as key-value pairs.

Broadcast Queue:

  • The intent is placed into a queue managed by the system's ActivityManagerService (AMS). The AMS is responsible for handling and managing the lifecycle of activities, services, and broadcasts.

Identifying Receivers:

  • The system identifies all registered receivers that match the intent’s action and other filters (e.g., category, data scheme). These receivers can be either statically registered in the AndroidManifest.xml file or dynamically registered at runtime using Context.registerReceiver().

Dispatching:

  • The onReceive() method of each matching receiver is called. For receivers registered in the application's manifest, the onReceive() method runs on the main thread of the application. For dynamically registered receivers, the onReceive() method runs on the main thread of the process where the receiver was registered.

Internal Flow

Sending a Broadcast:

The app or system uses Context.sendBroadcast() or Context.sendOrderedBroadcast() to send a broadcast intent.

val intent = Intent("com.example.SAMPLE_ACTION")
sendBroadcast(intent)

Queuing in AMS:

The ActivityManagerService (AMS) receives the broadcast request and enqueues it for processing.

// Pseudo-code
public void broadcastIntent(Intent intent) {
    BroadcastRecord r = new BroadcastRecord(intent);
    mBroadcastQueue.enqueue(r);
}

Finding Registered Receivers:

The system looks up all the receivers registered for the broadcast action.

// Pseudo-code
List<BroadcastReceiver> matchingReceivers = findReceivers(intent);

Delivering the Broadcast:

The intent is sent to the onReceive() method of each matching receiver on the main thread.

// Pseudo-code
for (BroadcastReceiver receiver : matchingReceivers) {
    handler.post(() -> receiver.onReceive(context, intent));
}

Simplified Code Snippets

// Pseudo-code for Broadcast Record
class BroadcastRecord {
    Intent intent;
    List<BroadcastReceiver> receivers;

    BroadcastRecord(Intent intent) {
        this.intent = intent;
        this.receivers = new ArrayList<>();
    }
}

// Pseudo-code for Broadcast Queue
class BroadcastQueue {
    Queue<BroadcastRecord> queue = new LinkedList<>();

    void enqueue(BroadcastRecord record) {
        queue.add(record);
    }

    void processNext() {
        BroadcastRecord record = queue.poll();
        for (BroadcastReceiver receiver : record.receivers) {
            handler.post(() -> receiver.onReceive(context, record.intent));
        }
    }
}

Additional Notes:

  1. Threading and Performance:

    • The handler.post() method ensures that the onReceive() method of the receiver runs on the main thread, which is crucial for updating the UI and interacting with other components safely.
  2. Ordered Broadcasts:

    • For ordered broadcasts, the system processes each receiver sequentially, allowing each to consume or modify the intent before passing it to the next receiver.
  3. Priority and Permissions:

    • The system can prioritize receivers based on their declared priority in the manifest or when dynamically registered. Permissions can also restrict which applications can receive the broadcast.

Let's see some use cases and how to Broadcast and Receive

  • Registering Broadcast Receivers:

Broadcast Receivers can be registered in two ways:

  1. Static Registration (in the Manifest):

    • Suitable for receiving broadcasts even when the app is not running.

    • Limited to system-defined broadcasts in Android 8.0 (Oreo) and above.

<receiver android:name=".MyBroadcastReceiver">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>
  1. Dynamic Registration (in Code):

    • Useful for context-specific broadcasts.

    • The receiver is active only when the app is running.

val myReceiver = MyBroadcastReceiver()
val filter = IntentFilter(Intent.ACTION_BATTERY_LOW)
registerReceiver(myReceiver, filter)
  • How to send a broadcast:

      val intent = Intent("com.example.SAMPLE_ACTION")
      intent.putExtra("key", "value")
      sendBroadcast(intent)
      // or
      val intent = Intent("com.example.SAMPLE_ACTION")
      intent.putExtra("key", "value")
      sendOrderedBroadcast(intent, null)
    
  • Implementing a Broadcast Receiver:

class MyBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        when (intent?.action) {
            Intent.ACTION_BATTERY_LOW -> {
                Toast.makeText(context, "Battery is low!", Toast.LENGTH_SHORT).show()
            }
            Intent.ACTION_BOOT_COMPLETED -> {
                Toast.makeText(context, "Device just booted!", Toast.LENGTH_SHORT).show()
            }
        }
    }
}
  • Example: Monitoring Network Connectivity:

class ConnectivityReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        val activeNetwork = connectivityManager.activeNetworkInfo
        if (activeNetwork?.isConnected == true) {
            // Network is connected
            Toast.makeText(context, "Connected to the Internet", Toast.LENGTH_SHORT).show()
        } else {
            // Network is disconnected
            Toast.makeText(context, "No Internet connection", Toast.LENGTH_SHORT).show()
        }
    }
}

// Register the receiver dynamically
val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
val receiver = ConnectivityReceiver()
registerReceiver(receiver, filter)
  • Example: Responding to System Events:

class BootReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
            // Start a service or schedule a job
            Toast.makeText(context, "Device booted, starting service", Toast.LENGTH_SHORT).show()
            // context.startService(Intent(context, MyService::class.java))
        }
    }
}
  • Communication Between Components:

Broadcast Receivers can talk to other parts of the app using:

  • Intents: Send data to an activity or service.

  • LocalBroadcastManager: For communication within the app to avoid security risks of global broadcasts.

// Sending a local broadcast
val intent = Intent("com.example.SOME_ACTION")
intent.putExtra("data", "Hello from the receiver!")
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)

// Receiving the local broadcast
class LocalReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val data = intent.getStringExtra("data")
        Toast.makeText(context, "Received data: $data", Toast.LENGTH_SHORT).show()
    }
}

// Register the local receiver
val localReceiver = LocalReceiver()
val filter = IntentFilter("com.example.SOME_ACTION")
LocalBroadcastManager.getInstance(context).registerReceiver(localReceiver, filter)

Considerations and Best Practices

  • Limit Static Broadcast Receivers: To save battery, avoid static registration for broadcasts that happen often.
  • Unregister Dynamic Receivers: Always unregister dynamic receivers in onPause() or onDestroy() to avoid memory leaks.
  • Use LocalBroadcastManager: For in-app broadcasts, use LocalBroadcastManager to improve security and performance.

Conclusion

Broadcast Receivers are a powerful feature in Android, allowing your app to respond to various system and app-specific events. By understanding how they work and following best practices, you can create responsive and efficient Android applications. Happy coding!


That's it for today. Happy Coding...

3
Subscribe to my newsletter

Read articles from Romman Sabbir directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Romman Sabbir
Romman Sabbir

Senior Android Engineer from Bangladesh. Love to contribute in Open-Source. Indie Music Producer.