Android Broadcast Receiver Tutorial

Photo by Roman Kraft on Unsplash
Photo by Roman Kraft on Unsplash
The Android Broadcast Receiver component allows an app to receive messages from the Android system or other apps, and to send messages to other components of the app itself, or to other apps. It is similar to the publish-subscribe design pattern.

The Android Broadcast Receiver component allows an app to receive messages from the Android system or other apps, and to send messages to other components of the app itself, or to other apps. It is similar to the publish-subscribe design pattern. This article will describe how to use Broadcast Receiver.

The complete code for this chapter can be found in .

Overview

When a system event occurs, Android system automatically broadcasts the event to all apps that subscribe to the event. For example, the system broadcasts ACTION_AIRPLANE_MODE_CHANGED event when the user turns on or off Airplane Mode.

Apps can also broadcast events to all apps subscribe to the event. Of course, the app itself can also subscribe to the events it broadcasts.

Receiving Broadcasts

There are two ways to receive broadcasts, one is to register manifest-declared receivers, and the other is to register context-declared receivers.

Manifest-Declared Receivers

Manifest-declared receivers refer to the receivers registered in AndroidManifest.xml. Set the class subscribing the event in <receiver/>, and specify the event to be subscribed to in <intent-filter/>. If the event source is the system or other apps, you must also set android:exported="true".

In the following example, we create LocaleChangedBroadcastReceiver inheriting BroadcastReceiver and override onReceive(). If the event includes extra data, we can get it from the intent.

package com.waynestalk.broadcastreceiverexample

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log

class LocaleChangedBroadcastReceiver : BroadcastReceiver() {
    companion object {
        private const val TAG = "LocaleChangedBroadcast"
    }

    override fun onReceive(context: Context, intent: Intent) {
        Log.d(TAG, "Received: locale changed => ${Thread.currentThread()}")
    }
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.AndroidBroadcastReceiverExample"
        tools:targetApi="31">

        <receiver
            android:name=".LocaleChangedBroadcastReceiver"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.LOCALE_CHANGED" />
            </intent-filter>
        </receiver>
    </application>

</manifest>

Android provides many system events, please refer to Intent constants. Beginning with Android 8 (API level 26), apps cannot receive implicit broadcasts through manifest-declared receivers. Implicit broadcasts are those broadcasts that don’t specify your app as the target. For example, system events are implicit broadcasts. So you cannot use manifest-declared receivers to receive system events, except for a few implicit broadcasts.

Manifest-declared receivers do not receive events when the app is never executed by the user or is force stopped.

As shown in the first figure below, if the app is running, in the background, or swiped out from the background by the user, the Force stop button can be pressed in the App info. This means that the app is launched. Then, the manifest-declared receivers of the app can receive events.

App is launched.
App is launched.

If the user presses Force stop to stop the app, the app’s manifest-declared receivers will no longer receive events. When the system starts, if the user has never executed the app, the app is also stopped. In some brands of Android system, when the user swipe out the app from the background, the system will force stop the app.

App is not launched, or force-stopped.
App is not launched, or force-stopped.

Context-Declared Receivers

Context-declared receivers refer to the receivers registered in Android components through Context.registerReceiver() or ContextCompat.registerReceiver().

In the following example, we register AirplaneModeBroadcastReceiver in MainActivity.onResume(). Because it subscribes to the event from the system, you must set ContextCompat.RECEIVER_EXPORTED flag. Finally, we also need to call Context.unregisterReceiver() in MainActivity.onPause( ) to unregister to prevent registering the event multiple times.

package com.waynestalk.broadcastreceiverexample

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast

class AirplaneModeBroadcastReceiver : BroadcastReceiver() {
    companion object {
        private const val TAG = "AirplaneModeBroadcast"
    }

    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action != Intent.ACTION_AIRPLANE_MODE_CHANGED) return

        val isOn = intent.getBooleanExtra("state", false)
        val message = "Airplane mode is ${if (isOn) "on" else "off"}."
        Toast.makeText(context, message, Toast.LENGTH_LONG).show()
    }
}
package com.waynestalk.broadcastreceiverexample

import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.waynestalk.broadcastreceiverexample.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    private val broadcastReceiver = AirplaneModeBroadcastReceiver()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }

    override fun onResume() {
        super.onResume()

        val filter = IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED)
        val flags = ContextCompat.RECEIVER_EXPORTED
        ContextCompat.registerReceiver(this, broadcastReceiver, filter, flags)
    }

    override fun onPause() {
        super.onPause()

        unregisterReceiver(broadcastReceiver)
    }
}

Lifecycle

When the system broadcasts an event to the app, the BroadcastReceiver in the app that subscribes to the event will be created, and the system will execute BroadcastReceiver.onReceive() in the main thread of the process. When onReceive() finishes, BroadcastReceiver is no longer active. The system will create a new BroadcastReceiver every time to process a new event, so when onReceive() is executed, the lifecycle of the BroadcastReceiver is over.

When onReceive() is executed, its hosting process will be considered as a foreground process by the system. So even if the system encounters a low memory situation, it will not kill the app. But when onReceive() finishes, if the app is a background process, the system may kill the process.

So in onReceive(), we should not create a long running thread. Because when onReceive() finishes, the system may kill the process and the spawned threads as well.

We should also not perform long running operations in onReceive(), because onReceive() is performed in the main thread. When the user is interacting with the app, and onReceive() happens to be executed to handle an event, if onReceive() spends too much time processing the event in the main thread, it is not good for the user experience.

If you must perform long running operations in onReceive(), you can use JobScheduler and JobService, or BroadcastReceiver.goAsync(). goAsync() tells the system that it needs more time to process the event (up to 10 seconds). The onReceive() in the following example first calls goAsync(), and then performs the long running operation in the coroutine, so that the main thread will not be blocked. Call PendingResult.finish() to inform the system that processing is over.

class MyBroadcastReceiver : BroadcastReceiver() {
    private val scope = CoroutineScope(SupervisorJob())

    override fun onReceive(context: Context, intent: Intent) {
        val pendingResult: PendingResult = goAsync()

        scope.launch(Dispatchers.Default) {
            // ...

            // Must call finish() so the BroadcastReceiver can be recycled
            pending.finish()
        }
    }
}

Sending Broadcasts

Apps can call sendBroadcast(Intent, String) to broadcast events to themselves and other apps.

As in the example below, the EchoBroadcastReceiver subscribes to com.waynestalk.echo event. We register it in AndroidManifest.xml. In MainActivity, create an intent and set the event to com.waynestalk.echo. Beginning with Android 8 (API level 26), the app cannot receive implicit broadcasts through manifest-declared receivers, so we set the receiver’s application package in Intent.package, which is the target of this event. Because it broadcasts to itself, we set to its application package.

package com.waynestalk.broadcastreceiverexample

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast

class EchoBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action != "com.waynestalk.echo") return

        val message = intent.getStringExtra("message")
        Toast.makeText(context, "Received: $message", Toast.LENGTH_LONG).show()
    }
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.AndroidBroadcastReceiverExample"
        tools:targetApi="31">

        <receiver
            android:name=".EchoBroadcastReceiver"
            android:exported="true">
            <intent-filter>
                <action android:name="com.waynestalk.echo" />
            </intent-filter>
        </receiver>
    </application>

</manifest>
package com.waynestalk.broadcastreceiverexample

import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.waynestalk.broadcastreceiverexample.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.echoButton.setOnClickListener {
            val intent = Intent().apply {
                action = "com.waynestalk.echo"
                `package` = "com.waynestalk.broadcastreceiverexample"
                putExtra("message", "Hello Android")
            }
            sendBroadcast(intent)
        }
    }
}

Conclusion

System events allow apps to react appropriately, such as an app subscribes to network connection changes. If it is disconnected, the app can display a message asking the user to turn on the network connection. And the app does not need to pull the connection state all the time, but subscribes system events to know whether the network state has changed.

Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like