25. 04. 2018
Počínaje Lollipopem je broadcast PHONE_STATE (viz. ACTION_PHONE_STATE_CHANGED) pro příchozí hovory posílán dvakrát. Důvody jsou skryté v temných zákoutích operačního systému a v hlavách Google developerů.
Proč se broadcast volá dvakrát?
Ponoříme-li se hlouběji do probému a pokusíme se najít zdroj duplicity, můžeme poměrně rychle vystopovat její původ v TelephonyRegistry. Pro Android 5.0 by měl callstack vypadat nějak takto:
- TelephonyRegistry.broadcastCallStateChanged(): 1196
- TelephonyRegistry.notifyCallStateForSubscriber(): 539
- DefaultPhoneNotifier.notifyPhoneState(Phone sender): 67
Pojďme se podívat co v této části kódu páni inženýři schovali. V kódu je plno FIXME, TODO a „to be unhidden“ komentářů a je velmi špatně dokumentovaný. I přesto se dá odhadnout co bylo cílem. Celé to vypadá jako nedokončená snaha o podporu více SIM karet přímo v jádru systému, tedy bez nutnosti úprav od výrobců telefonů.
Extra parametr „subscription“
Možná jste si všimli, že BroadcastReciever dostane navíc jeden nedokumentovaný Extra parametr pojmenovaný „subscription“. Jeho definici najdeme v PhoneConstants.SUBSCRIPTION_KEY, kterážto konstanta je pro vývojáře v rámci aktuálního SDK nedostupná. Budeme potřebovat zjistit, jakých hodnot může parametr „subscription“ nabývat a co tyto hodnoty znamenají.
Když si projdeme callstack popsaný výše, je vidět že jeho hodnoty se nastavují v metodě DefaultPhoneNotifier.notifyPhoneState(), jako výstup z Phone.getSubId(). Dle mých pozorování můžeme dostat:
- Reálné subscription ID (něco jako SIM ID)
- SubscriptionManager.DEFAULT_SUB_ID (něco jako nedefinovaná SIM)
Bohužel SubscriptionManager.DEFAULT_SUB_ID také není v aktuálním SDK dostupné pro vývojáře (možná pomocí reflexe?). Co je ale horší, že jeho hodnota se mění napříč verzemi operačního systému:
- 5.0 – DEFAULT_SUB_ID = Long.MAX_VALUE
- 5.1 – DEFAULT_SUBSCRIPTION_ID = Integer.MAX_VALUE
Pěkný binec!
Zbavujeme se druhého broadcastu
Konečně jsme u řešení. Naštěstí je poměrně přímočaré. Použijeme subscription
Extra a vyfiltrujeme broadcast s výchozím subscription ID. Dle mého názoru nejlepší způsob jak toho docílit je:
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! } }
Musíme načíst subscription
jako obecný objekt, protože je v extras uložený jako long
for Android 5.0 a int
pro Android 5.1.
Změny od Android 6
Metoda uvedená výše stále funguje. Od Android 6 subscription
extra není již připojeno k PHONE_STATE broadcastům. Existuje nový broadcast s SUBSCRIPTION_PHONE_STATE action kde je subscription
extra připojen.