Lesson 06 / 15
Broadcast Receivers
Measures how a broadcast receiver is registered in the manifest file and at run time, that an implicit system broadcast does not reach a declared receiver, and that the same receiver class gets different fulfillments by three paths.
Published Updated
In this lesson
- Tells apart the two ways a broadcast receiver is registered, in the manifest file and at run time
- Measures that an implicit system broadcast does not reach a declared receiver while one on the exemption list does
- Shows that fulfillment changes with the lifetime of a run-time registration
- Verifies that a broadcast reaches the receiver class by three separate paths with two different fulfillments
Contents
A service runs by the app’s own request. The system calls the app with its own events: the screen turned on, the device restarted, the charger was plugged in, the network changed. These events are sent to the system as a broadcast, and the app components that are interested receive it. The app component that receives a broadcast is a broadcast receiver (BroadcastReceiver). A broadcast receiver has no UI, and its only job is to do a short piece of work in the onReceive callback.
The question of the lesson: when a broadcast is requested, that is, when a receiver wants to receive a certain broadcast, how does the platform fulfill the request? The answer depends on the way the receiver was registered with the platform.
// Fulfillment.kt — classifies how the platform fulfills a request
sealed interface Outcome {
data class Given(val value: String) : Outcome
data class Waiting(val on: String) : Outcome
data class Refused(val reason: String) : Outcome
}
class Ask(val label: String, val wanted: String, val outcome: Outcome) {
val fulfillment: String = when (outcome) {
is Outcome.Waiting -> "pending"
is Outcome.Refused -> "unmet"
is Outcome.Given -> if (outcome.value == wanted) "full" else "substitute"
}
fun report() {
val detail = when (outcome) {
is Outcome.Given -> "given=${outcome.value}"
is Outcome.Waiting -> "waiting-on=${outcome.on}"
is Outcome.Refused -> "reason=${outcome.reason}"
}
println("$label: wanted=$wanted | $detail -> $fulfillment")
}
}
fun tally(asks: List<Ask>) {
for (name in listOf("full", "substitute", "pending", "unmet")) {
println("$name: ${asks.count { it.fulfillment == name }}")
}
}
Two Ways to Register
A broadcast receiver is registered in two ways.
- In the manifest file: the receiver is declared with a
<receiver>element, and an intent filter says which broadcast it wants. The platform can set up the receiver even when the app is not running. - At run time: code registers the receiver with
registerReceiverand removes it withunregisterReceiver. The receiver gets broadcasts only as long as it is registered.
<application> <receiver android:name=".ScreenReceiver" android:exported="false"> <intent-filter> <action android:name="android.intent.action.SCREEN_ON" /> </intent-filter> </receiver> </application>
// Platform code: registering the same receiver at run time
import android.content.IntentFilter
val filter = IntentFilter("android.intent.action.SCREEN_ON")
registerReceiver(ScreenReceiver(), filter)
The two ways used to give the same result. In today’s platform the difference comes from whether the broadcast is implicit or explicit. An implicit broadcast does not target a particular app; it is sent to everyone, and system events such as the screen turning on are like this. An explicit broadcast carries the package name of an app and goes only to that app. From a certain API level on, the platform has restricted receivers declared in the manifest file from receiving implicit system broadcasts; broadcasts on an exemption list are free of this restriction. Receivers registered at run time are not restricted.
The model sets up this rule in a single helper file. The inputs are the name of the broadcast, the way of registration, whether the broadcast is an implicit system broadcast, whether it was sent with a package name, and the exemption list. The list and the level are model inputs.
// Delivery.kt — sets up how a broadcast reaches the receiver class
fun deliver(
action: String,
way: String,
systemImplicit: Boolean,
explicitPackage: Boolean,
exempt: Set<String>,
): Outcome = when {
way == "runtime" -> Outcome.Given(action)
explicitPackage -> Outcome.Given(action)
systemImplicit && action !in exempt -> Outcome.Refused("implicit-broadcast-limit")
else -> Outcome.Given(action)
}
A Broadcast That Reaches and One That Does Not Reach a Declared Receiver
Three receivers want three broadcasts. The screen turning on is an implicit system broadcast and is not on the exemption list. The device booting is also an implicit system broadcast, but it is on the exemption list. In the third, the screen broadcast is this time sent to a receiver registered at run time.
// Main.kt — implicit system broadcast and the way of registration
fun main() {
val exempt = setOf("BOOT_COMPLETED")
val asks = listOf(
Ask("manifest-implicit", "SCREEN_ON", deliver("SCREEN_ON", "manifest", true, false, exempt)),
Ask("manifest-exempt", "BOOT_COMPLETED", deliver("BOOT_COMPLETED", "manifest", true, false, exempt)),
Ask("runtime-implicit", "SCREEN_ON", deliver("SCREEN_ON", "runtime", true, false, exempt)),
)
for (ask in asks) {
ask.report()
}
tally(asks)
}
manifest-implicit: wanted=SCREEN_ON | reason=implicit-broadcast-limit -> unmet manifest-exempt: wanted=BOOT_COMPLETED | given=BOOT_COMPLETED -> full runtime-implicit: wanted=SCREEN_ON | given=SCREEN_ON -> full full: 2 substitute: 0 pending: 0 unmet: 1
The screen broadcast did not reach the receiver in the manifest file: unmet. By the same path, the device boot on the exemption list did reach: full. The screen broadcast reached the receiver registered at run time: full. The fulfillment of this lesson is unmet: declaring a receiver in the manifest file does not guarantee that an implicit system broadcast will reach it.
AC1. The exemption list and the API level at which the restriction starts are model inputs. Which broadcasts are on the list and how it changes between versions were not measured.
The Lifetime of a Run-Time Registration
A receiver that gets broadcasts through run-time registration gets them as long as the registration exists. If the activity that made the registration is destroyed and the receiver is not removed, there is a leak; if it is removed, the receiver is no longer a receiver. A registration in the manifest file, on the other hand, depends on the life of the app.
// Main.kt — lifetime of a run-time registration
fun deliverRegistered(action: String, way: String, active: Boolean): Outcome =
if (way == "runtime" && !active) Outcome.Refused("not-registered") else Outcome.Given(action)
fun main() {
val asks = listOf(
Ask("runtime-registered", "SCREEN_ON", deliverRegistered("SCREEN_ON", "runtime", active = true)),
Ask("runtime-unregistered", "SCREEN_ON", deliverRegistered("SCREEN_ON", "runtime", active = false)),
Ask("manifest-always", "BOOT_COMPLETED", deliverRegistered("BOOT_COMPLETED", "manifest", active = false)),
)
for (ask in asks) {
ask.report()
}
tally(asks)
}
runtime-registered: wanted=SCREEN_ON | given=SCREEN_ON -> full runtime-unregistered: wanted=SCREEN_ON | reason=not-registered -> unmet manifest-always: wanted=BOOT_COMPLETED | given=BOOT_COMPLETED -> full full: 2 substitute: 0 pending: 0 unmet: 1
The registered receiver received the broadcast: full. The receiver whose registration was removed did not: unmet. For a receiver in the manifest file the active value is meaningless; the registration is valid as long as the app exists. The difference between the two unmet lines again lies in the reason: in the first, implicit-broadcast-limit is a rule of the platform; in the second, not-registered is the work of the code itself.
AC2. The model takes the active value as an input. In which lifecycle callback the receiver should be removed was not measured in this lesson.
Ordered Broadcast and Aborting
Broadcasts are sent in two forms. In a normal broadcast, the platform delivers the broadcast to all receivers together; there is no order among the receivers and one does not affect another. In an ordered broadcast, the platform delivers the broadcast to the receivers one by one, in order of priority. Each receiver receives the broadcast and may add a result, or may abort the broadcast and stop the chain from reaching the ones that follow. The priority is given in the filter with the android:priority attribute, or on the IntentFilter in a run-time registration.
// Platform code: sending an ordered broadcast and stopping the chain
sendOrderedBroadcast(Intent("app.action.CHECK"), null)
// inside the receiver, to stop the chain
abortBroadcast()
In the measurement three receivers are lined up in order of priority. What is wanted is for every receiver to receive the broadcast. In an ordered broadcast, if the second receiver aborts, the third is never reached. In a normal broadcast the abort call has no effect.
// Main.kt — ordered broadcast and aborting
fun receive(index: Int, ordered: Boolean, abortAt: Int?): Outcome =
if (ordered && abortAt != null && index > abortAt) Outcome.Refused("broadcast-aborted")
else Outcome.Given("received")
fun main() {
val asks = mutableListOf<Ask>()
for (ordered in listOf(true, false)) {
val kind = if (ordered) "ordered" else "normal"
for (index in 0..2) {
asks.add(Ask("$kind-receiver-${index + 1}", "received", receive(index, ordered, abortAt = 1)))
}
}
for (ask in asks) {
ask.report()
}
tally(asks)
}
ordered-receiver-1: wanted=received | given=received -> full ordered-receiver-2: wanted=received | given=received -> full ordered-receiver-3: wanted=received | reason=broadcast-aborted -> unmet normal-receiver-1: wanted=received | given=received -> full normal-receiver-2: wanted=received | given=received -> full normal-receiver-3: wanted=received | given=received -> full full: 5 substitute: 0 pending: 0 unmet: 1
In the ordered broadcast the receiver that aborted received the broadcast: full; the receiver after it did not: unmet. In the normal broadcast the same abort changed nothing: all three receivers received it. The lesson to draw is not to assume that one receiver depends on another. In an ordered broadcast a receiver in the middle of the chain can stop the broadcast; this receiver may not be in your own app. Some system broadcasts cannot be aborted, and some broadcasts are sent only in the normal form.
AC3. The model takes the abort only as an index. Receivers adding results, the order of receivers with the same priority, and which system broadcasts are sent as ordered were not measured.
How Long the Receiver Can Run
As an app component, a broadcast receiver is very short-lived: it exists only while the onReceive method runs. When the method returns, the platform no longer counts the receiver as active and may end the process that hosts it. The method runs on the main thread, and if it takes long the platform raises a not-responding error. For this reason, doing long work inside onReceive is not solved only by opening a thread; the moment the receiver returns, the process loses priority.
// Platform code: scheduling the work before onReceive returns
override fun onReceive(context: Context, intent: Intent) {
val pending = goAsync()
thread {
doWork(intent)
pending.finish()
}
}
The measurement takes the duration of the work and the manner in which it is done as inputs. The limit in the model is ten seconds; this is an input, not a fixed claim of the platform. What is wanted is completed. Short work finishes directly. If long work is done inside onReceive, the time is exceeded. If long work is handed off to scheduled work, the receiver returns quickly and the work is done later: a result that stands in for the wanted one, not the wanted one itself. Work that waits for the network waits until the network arrives.
// Main.kt — duration of work inside onReceive
fun runIn(onReceive: Boolean, seconds: Int, handOff: Boolean, needsNetwork: Boolean, limit: Int): Outcome = when {
needsNetwork -> Outcome.Waiting("network")
handOff -> Outcome.Given("job-scheduled")
onReceive && seconds > limit -> Outcome.Refused("time-exceeded")
else -> Outcome.Given("completed")
}
fun main() {
val limit = 10
val asks = listOf(
Ask("short-work", "completed", runIn(true, 3, handOff = false, needsNetwork = false, limit = limit)),
Ask("long-work", "completed", runIn(true, 15, handOff = false, needsNetwork = false, limit = limit)),
Ask("long-work-handed-off", "completed", runIn(true, 15, handOff = true, needsNetwork = false, limit = limit)),
Ask("network-work", "completed", runIn(true, 3, handOff = false, needsNetwork = true, limit = limit)),
)
for (ask in asks) {
ask.report()
}
tally(asks)
}
short-work: wanted=completed | given=completed -> full long-work: wanted=completed | reason=time-exceeded -> unmet long-work-handed-off: wanted=completed | given=job-scheduled -> substitute network-work: wanted=completed | waiting-on=network -> pending full: 1 substitute: 1 pending: 1 unmet: 1
Four jobs received four fulfillments. The short work finished under the limit: full. The long work stayed unmet when it was done inside the receiver. Handing the long work off to scheduled work gave a substitute: the work was not completed inside onReceive, but its completion was guaranteed. This is often the right solution. Work that waits for the network is not tried directly either; it stays pending and carries its condition. This distinction is the receiver counterpart of the distinction drawn earlier for scheduled jobs.
AC4. Ten seconds is an input chosen by the model. That the real duration changes with the type of broadcast and with whether the app is in the foreground, and the exact limit of the time obtained with goAsync, were not measured.
The Bounding Measurement
The same broadcast, the same receiver class: three paths. The first is an implicit broadcast to a receiver declared in the manifest file, and it stays unmet. The second is registering the same receiver class at run time, and it is fulfilled in full. The third is sending the broadcast explicitly, with the package name of the app: it reaches the receiver in the manifest file too.
// Main.kt — the same broadcast, the same receiver class, three paths
fun main() {
val exempt = setOf("BOOT_COMPLETED")
val asks = listOf(
Ask("manifest-implicit", "SCREEN_ON", deliver("SCREEN_ON", "manifest", true, false, exempt)),
Ask("runtime-implicit", "SCREEN_ON", deliver("SCREEN_ON", "runtime", true, false, exempt)),
Ask("manifest-explicit", "SCREEN_ON", deliver("SCREEN_ON", "manifest", true, true, exempt)),
)
for (ask in asks) {
ask.report()
}
tally(asks)
println(asks.map { it.fulfillment }.distinct().sorted())
}
manifest-implicit: wanted=SCREEN_ON | reason=implicit-broadcast-limit -> unmet runtime-implicit: wanted=SCREEN_ON | given=SCREEN_ON -> full manifest-explicit: wanted=SCREEN_ON | given=SCREEN_ON -> full full: 2 substitute: 0 pending: 0 unmet: 1 [full, unmet]
Of the three paths, two are full and one is unmet. The last line again shows the view of the rule: the fulfillments of the three requests are only two different values, and which path corresponds to which cannot be read from the category; the path is in the label field. Whether the receiver runs depends on the path by which the broadcast came to it, not on the broadcast itself.
AC5. The model counts an explicit broadcast as any broadcast sent with a package name. In reality the path of an explicit broadcast also depends on the target component being declared and exported; this was not measured in this lesson.
Summary
- This lesson’s criterion: fulfillment. A broadcast receiver is registered in the manifest file or at run time, and these two ways receive different fulfillments. This lesson’s fulfillment: unmet; an implicit system broadcast does not reach a declared receiver.
- An implicit system broadcast does not reach a declared receiver: unmet. A broadcast on the exemption list and a run-time registration were fulfilled in full: 2 full and 1 unmet.
- When a run-time registration is removed, the receiver gets no broadcasts; the lifetime of the registration is the responsibility of the code.
- An explicit broadcast reaches the receiver in the manifest file too.
- The fulfillment category does not carry the path: two unmet results can come from different reasons.
Next Step
A broadcast receiver reports an event; it carries no data. To share the data itself with another app, the next lesson, Content Providers, measures how a content URI is mapped and how a query without permission is fulfilled.
To keep your progress and take notes, Log in
My notes
Log in to take notes.