7.12.2015

Last Updated on

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:

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:

  1. Reálné subscription ID (něco jako SIM ID)
  2. 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.

Vladislav Skoumal
SKOUMAL CEO