22. 10. 2018
Starting with the Android Lollipop mobile operating system, PHONE_STATE broadcast (aka ACTION_PHONE_STATE_CHANGED) is fired twice for incoming calls. Reasons are hidden in deep corners of the operating system and in brains of Google developers.
Why is broadcast called twice?
When you try to dig deeper and find a source of this duplicity you can find very quickly it comes from TelephonyRegistry. I guess, for Android 5.0 the call stack could look like this:
- TelephonyRegistry.broadcastCallStateChanged(): 1196
- TelephonyRegistry.notifyCallStateForSubscriber(): 539
- DefaultPhoneNotifier.notifyPhoneState(Phone sender): 67
Let’s take your torch and try to go through this code. It’s full of FIXME, TODO and “to be unhidden” comments, very poorly documented. However, man can guess the main point of this whole stuff. It all looks like unfinished effort to support more SIM slots without any requirements for vendor-specific hacks.
Extra called “subscription”
You have probably noticed, that your BroadcastReciever is called with one more undocumented Extra parameter “subscription”. It is defined in PhoneConstants.SUBSCRIPTION_KEY which is unavailable for developers in current SDK’s. For our purposes, it is important to identify what its values could be and why.
By simply going through the call stack mentioned above, you could see that its value originates from DefaultPhoneNotifier.notifyPhoneState() method, obtained from Phone.getSubId(). According to my observations this could be:
- Real subscription ID (something like SIM ID)
- SubscriptionManager.DEFAULT_SUB_ID (like undefined SIM)
The SubscriptionManager.DEFAULT_SUB_ID is also unavailable for developers in current SDK’s (maybe by reflection?). What’s even worse is that value and name differ across system versions:
- 5.0 – DEFAULT_SUB_ID = Long.MAX_VALUE
- 5.1 – DEFAULT_SUBSCRIPTION_ID = Integer.MAX_VALUE
This is a complete mess!
Avoid the second broadcast
Let’s look at the solution. Luckily, it is very simple to do. We can use subscription
Extra and filter out broadcasts with default subscription id. In my opinion, the best way to achieve it is to do something like this:
public void onReceive(Context context, Intent intent) { Object obj = intent.getExtras().get("subscription"); long subId; if(obj == null) { subId = Long.MIN_VALUE; // subscription not in extras } else { subId = Long.valueOf(obj.toString()); // subscription is long or int } if(subId < Integer.MAX_VALUE) { // hurray, this is called only once on all operating system versions! } }
We need to load subscription
as the general object because it is long
for Android 5.0 and int
for Android 5.1.
Changes in Android 6
The short version is the method above still works for Android 6+. Starting with Android 6 subscription
extra is no longer attached to PHONE_STATE broadcasts. There is new broadcast with SUBSCRIPTION_PHONE_STATE action where subscription
extra is attached.
If you are into Android, do not hesitate and contact us at [email protected]. We tend to look for Android dev (iOS as well), but there are also chances for a project manager or a tester.