Processing Ordered Broadcasts

[This post is by Bruno Albuquerque, an engineer who works in Google’s office in Belo Horizonte, Brazil. —Tim Bray]

One of the things that I find most interesting and powerful about Android is the concept of broadcasts and their use through the BroadcastReceiver class (from now on, we will call implementations of this class “receivers”). As this document is about a very specific usage scenario for broadcasts, I will not go into detail about how they work in general, so I recommend reading the documentation about them in the Android developer site. For the purpose of this document, it is enough to know that broadcasts are generated whenever something interesting happens in the system (connectivity changes, for example) and you can register to be notified whenever one (or more) of those broadcasts are generated.

While developing Right Number, I noticed that some developers who create receivers for ordered broadcasts do not seem to be fully aware of what is the correct way to do it. This suggests that the documentation could be improved; in any case, things often still work(although it is mostly by chance than anything else).

Non-ordered vs. Ordered Broadcasts

In non-ordered mode, broadcasts are sent to all interested receivers “at the same time”. This basically means that one receiver can not interfere in any way with what other receivers will do neither can it prevent other receivers from being executed. One example of such broadcast is the ACTION_BATTERY_LOW one.

In ordered mode, broadcasts are sent to each receiver in order (controlled by the android:priority attribute for the intent-filter element in the manifest file that is related to your receiver) and one receiver is able to abort the broadcast so that receivers with a lower priority would not receive it (thus never execute). An example of this type of broadcast (and one that will be discussing in this document) is the ACTION_NEW_OUTGOING_CALL one.

Ordered Broadcast Usage

As mentioned earlier in this document, the ACTION_NEW_OUTGOING_CALL broadcast is an ordered one. This broadcast is sent whenever the user tries to initiate a phone call. There are several reasons that one would want to be notified about this, but we will focus on only 2:

  • To be able to reject an outgoing call;

  • To be able to rewrite the number before it is dialed.

In the first case, an app may want to control what numbers can be dialed or what time of the day numbers can be dialed. Right Number does what is described in the second case so it can be sure that a number is always dialed correctly no matter where in the world you are.

A naive BroadcastReceiver implementation would be something like this (note that you should associate this receiver with the ACTION_NEW_OUTGOING_CALL broadcast in the manifest file for your application):

public class CallReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// Original phone number is in the EXTRA_PHONE_NUMBER Intent extra.
String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);

if (shouldCancel(phoneNumber)) {
// Cancel our call.
setResultData(null);
} else {
// Use rewritten number as the result data.
setResultData(reformatNumber(phoneNumber));
}
}

The receiver either cancels the broadcast (and the call) or reformats the number to be dialed. If this is the only receiver that is active for the ACTION_NEW_OUTGOING_CALL broadcast, this will work exactly as expected. The problem arrises when you have, for example, a receiver that runs before the one above (has a higher priority) and that also changes the number as instead of looking at previous results of other receivers, we are just using the original (unmodified) number!

Doing It Right

With the above in mind, here is how the code should have been written in the first place:

public class CallReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// Try to read the phone number from previous receivers.
String phoneNumber = getResultData();

if (phoneNumber == null) {
// We could not find any previous data. Use the original phone number in this case.
phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
}

if (shouldCancel(phoneNumber)) {
// Cancel our call.
setResultData(null);
} else {
// Use rewritten number as the result data.
setResultData(reformatNumber(phoneNumber));
}
}

We first check if we have any previous result data (which would be generated by a receiver with a higher priority) and only if we can not find it we use the phone number in the EXTRA_PHONE_NUMBER intent extra.

How Big Is The Problem?

We have actually observed phones with a priority 0 receiver for the NEW_OUTGOING_CALL intent installed out of the box (this will be the last one that is called after all others) that completely ignores previous result data which means that, in effect, they disable any useful processing of ACTION_NEW_OUTGOING_CALL (other than canceling the call, which would still work). The only workaround for this is to also run your receiver at priority 0, which works due to particularities of running 2 receivers at the same priority but, by doing that, you break one of the few explicit rules for processing outgoing calls:

“For consistency, any receiver whose purpose is to prohibit phone calls should have a priority of 0, to ensure it will see the final phone number to be dialed. Any receiver whose purpose is to rewrite phone numbers to be called should have a positive priority. Negative priorities are reserved for the system for this broadcast; using them may cause problems.”

Conclusion

There are programs out there that do not play well with others. Urge any developers of such programs to read this post and fix their code. This will make Android better for both developers and users.

Notes About Priorities

  • For the NEW_OUTGOING_CALL intent, priority 0 should only be used by receivers that want to reject calls. This is so it can see changes from other receivers before deciding to reject the call or not.

  • Receivers that have the same priority will also be executed in order, but the order in this case is undefined.

  • Use non-negative priorities only. Negative ones are valid but will result in weird behavior most of the time.