Understanding Broadcast Receivers in Android

Broadcast Receiver is one of the component in Android that enable apps to listen for and respond to broadcast messages from other apps or the system itself. Think of them as listeners waiting for specific events to occur. Apps can respond to system-wide events like changes in battery level, network connectivity, and incoming SMS messages by using Broadcast Receivers. The broadcast message is nothing but an Intent . The action string of this Intent identifies the event that occurred (e.g, android.intent.action.AIRPLANE_MODE indicates that Airplane mode is toggled). The intent may also include additional information bundled into its extra field. For example, the airplane mode intent includes a boolean extra that indicates whether or not Airplane Mode is on.

Receiving Broadcast

Apps can receive broadcasts in two ways: Manifest-declared receivers and Context-registered receivers.

Manifest-declared receivers (Static Broadcast Receiver)

//Receiver App class SmsReceiver: BroadcastReceiver()  override fun onReceive(context: Context?, intent: Intent?)  if(intent?.action == "android.provider.Telephony.SMS_RECEIVED")  // it's best practice to verify intent action before performing any operation Log.i("ReceiverApp", "SMS Received") > > > 
Enter fullscreen mode

Exit fullscreen mode

  android:name="android.permission.RECEIVE_SMS"/> .. ..  android:name=".SmsReceiver" android:exported="true">  android:name="android.provider.Telephony.SMS_RECEIVED" />   .. .. 
Enter fullscreen mode

Exit fullscreen mode

To enable receiver to receive events from outside of our app, set android:exported to true , else set it to false if want to receive locally.

Behind the scenes: The system package manager registers the receiver when the app is installed. The receiver then becomes a separate entry point into our app which means that the system can start the app and deliver the broadcast if the app is not currently running.

Beginning with Android 8.0 (API level 26), we cannot use the manifest to declare a receiver for most implicit broadcasts (broadcasts that don’t target our app specifically). Check this list of Broadcast that can use manifest declared Receivers. However we can always use context-registered receivers.

Context-registered receivers (Dynamic Broadcast Receiver)

Context-registered receivers receive broadcasts as long as their registering context is valid. For an example, if we register within an activity context, we receive broadcasts as long as the activity is not destroyed. If we register with the application context, we receive broadcasts as long as the app is running.

To implement context-registered broadcast, remove receiver from manifest file and instead register it inside the activity:

//Receiver App private val smsReceiver = SmsReceiver() override fun onCreate(savedInstanceState: Bundle?)  super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) registerReceiver(smsReceiver, IntentFilter("android.provider.Telephony.SMS_RECEIVED") ) > override fun onDestroy()  super.onDestroy() unregisterReceiver(smsReceiver) > 
Enter fullscreen mode

Exit fullscreen mode

Custom Broadcasts

Till now we have discussed about system broadcast only, now we will see how we can create and send our own custom broadcast.

Sending broadcasts

Any application can send the broadcast using sendBroadcast(Intent) .

//Sender App // To send broadcase from any application, specify the custom action to intent and sendBroadcast val intent = Intent("TEST_CUSTOM_ACTION") intent.putExtra("data", "Some custom data") sendBroadcast(intent) 
Enter fullscreen mode

Exit fullscreen mode

In this way all the applications that are listening to TEST_CUSTOM_ACTION intent will receive the broadcast asynchronously.

We can also limit a broadcast to a particular app by calling setPackage(String) on the intent. In this way broadcast will be send to only single app with mentioned package name.

//Sender App val intent = Intent("TEST_CUSTOM_ACTION") intent.putExtra("data", "Some custom data") intent.setPackage("com.example.receiverapp") sendBroadcast(intent) 
Enter fullscreen mode

Exit fullscreen mode

Receiving Broadcasts

To receive broadcast we need to use context-registered receiver (From Android 8.0, we cannot use manifest-declared receiver for our custom broadcast)

//Receiver App private val customReceiver = CustomReceiver() override fun onCreate(savedInstanceState: Bundle?)  super.onCreate(savedInstanceState) Log.i("ReceiverApp", "activity created") setContentView(R.layout.activity_main) registerReceiver(customReceiver, IntentFilter("TEST_CUSTOM_ACTION"), RECEIVER_EXPORTED ) > override fun onDestroy()  super.onDestroy() Log.i("ReceiverApp", "activity destroyed") unregisterReceiver(customReceiver) > 
Enter fullscreen mode

Exit fullscreen mode

RECEIVER_EXPORTED flag needs to be add for custom broadcast, it indicates that other apps can send the broadcast to our app. If we do not add this flag, android will throw the below exception:

java.lang.SecurityException: com.example.receiverapp: One of RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED should be specified when a receiver isn’t being registered exclusively for system broadcasts

Below is out CustomReceiver class that receives the broadcast

//Receiver App class CustomReceiver: BroadcastReceiver()  override fun onReceive(context: Context?, intent: Intent?)  if(intent?.action == "TEST_CUSTOM_ACTION")  val value = intent.extras?.getString("data") Log.i("ReceiverApp", "Custom Received: $value") > > > 
Enter fullscreen mode

Exit fullscreen mode

Restricting broadcasts with permissions

Permissions allow us to restrict broadcasts to the set of apps that hold certain permissions.

Suppose we want to send broadcast to app that have internet permission, we can specify a permission parameter.

//Sender App val intent = Intent("TEST_CUSTOM_ACTION") intent.putExtra("data", "Some custom data") sendBroadcast(intent, Manifest.permission.INTERNET) 
Enter fullscreen mode

Exit fullscreen mode

Only receivers who have requested that permission with the tag in their manifest (and subsequently been granted the permission if it is dangerous) can receive the broadcast.

To receive the broadcast, declare permission in manifest file

  android:name="android.permission.INTERNET"/> 
Enter fullscreen mode

Exit fullscreen mode

//Receiver App registerReceiver(customReceiver, IntentFilter("TEST_CUSTOM_ACTION"), Manifest.permission.INTERNET, null, RECEIVER_EXPORTED ) 
Enter fullscreen mode

Exit fullscreen mode

Additionally we can define our own permission as well for custom broadcast (Check Custom permission)

Ordered Broadcast

There is one more way to send broadcast apart from sendBroadcast(Intent) method, i.e., sendOrderedBroadcast(Intent, String)

It sends broadcasts to one receiver at a time. Say we have multiple receivers listening to our custom action, when we send broadcast using this method, only one receiver at a time will get the onReceive() callback and other will get only when previous function completely executes. The order receivers run in can be controlled with the android:priority attribute of the matching intent-filter; receivers with the same priority will be run in an arbitrary order.

//Sender App val intent = Intent("TEST_CUSTOM_ACTION") intent.putExtra("data", "Some custom data") sendOrderedBroadcast(intent, null) //Receiver App registerReceiver(customReceiver, IntentFilter("TEST_CUSTOM_ACTION").apply  priority = SYSTEM_HIGH_PRIORITY >, RECEIVER_EXPORTED ) 
Enter fullscreen mode

Exit fullscreen mode

Local Broadcast

In case we want to communicate inside our own application only, we can use LocalBroadcastManager class provided by android.

LocalBroadcastManager is a helper class to register for and send broadcasts of Intents to local objects within your process.

This has a number of advantages over sending global broadcasts with Context.sendBroadcast :

// Reciever App (Same app will send and receive broadcast) private val localReceiver = LocalReceiver() override fun onCreate(savedInstanceState: Bundle?)  super.onCreate(savedInstanceState) Log.i("ReceiverApp", "activity created") setContentView(R.layout.activity_main) findViewByIdButton>(R.id.btn).setOnClickListener  //sending broadcast val intent = Intent("TEST_CUSTOM_ACTION_LOCAL") LocalBroadcastManager.getInstance(this@MainActivity).sendBroadcast(intent) > //registering local broadcast LocalBroadcastManager.getInstance(this).registerReceiver(localReceiver, IntentFilter("TEST_CUSTOM_ACTION_LOCAL") ) > override fun onDestroy()  super.onDestroy() Log.i("ReceiverApp", "activity destroyed") //unregistering local broadcast LocalBroadcastManager.getInstance(this).unregisterReceiver(localReceiver) >