Reacting to incoming phone calls in Xamarin.Android #2

As an alternative to the PhoneStateListener presented in the previous blog post, another approach towards identifying incoming phone calls on Xamarin.Android is the use of a BroadcastReceiver. Android BroadcastReceivers also listen to certain notifications (either within an application or system-wide) and invoke custom actions when such a notification is triggered. However, in contrast to services which must explcitly be started and continuously run in the background, BroadcastReceivers need to be registered once, and start their actions automatically whenever the monitored notification triggers.

BroadcastReceivers at a glance:

Let’s take a deeper look at this one: Custom receivers must be derived from the abstract Android.Content.BroadcastReceiver class , override the OnReceive method (this is where your custom action magic goes), and be flagged with the [BroadcastReceiver] attribute:

[BroadcastReceiver(Enabled = true, Exported = true)]
public class PhonecallReceiver : BroadcastReceiver, TextToSpeech.IOnInitListener
{
	public override void OnReceive(Context context, Intent intent)
	{
		// our custom business logic to be executed on a new phone call will come here
	}
}

The above-mentioned [BroadcastReceiver] attribute requires two parameters: Enabled is pretty self-explanatory, while Exported needs some explanation: It specifies whether the receiver can listen to notification coming from outside the application or not. If listening to system-wide state changes such as phone calls, this might sound useless (system-wide notification will always originate outside your application, obviously), but as I mentioned before it’s possible to use BroadcastReceivers as app-internal messaging channel, in which case not exporting makes sense. If this parameter is not set, the BroadcastReceiver listens to a system-wide state change, then it will automatically be set to true by the Android system, however it does not hurt to specify it explicitly.

Reacting to intents:

Speaking about system-wide state changes: How do instruct Android in which notifications our receiver is interested? This where intent filters come in: By binding an intent filter to our BroadcastReceiver, we tell it to react to exactly that intent being broadcasted by the Android OS. Android provides a variety of predefined intents, and obviously each app adds custom ones, but let me cut that short and tell you that we are interested in "android.intent.action.PHONE_STATE" – which is, fortunately, defined as constant in the Xamarin TelephonyManager.ActionPhoneStateChanged.

When registering BroadcastReceivers from an activity (which we’ll do in a while, for testing our solution), the targeted intent filter must be passed as parameter – we will see that in detail. However, in a real-life app it is not necessary to first run an activity in order to register a BroadcastReceiver, instead it will be registered automatically if correctly specified in the app manifest. This means we have to specify the intent filter somehow, and since we don’t want to edit the manifest manually we rely on the [IntentFilter] attribute which does that for us:

[BroadcastReceiver(Enabled = true, Exported = true)]
[IntentFilter(new[] { TelephonyManager.ActionPhoneStateChanged })]

public class PhonecallReceiver : BroadcastReceiver, TextToSpeech.IOnInitListener
{
	// ...

(Note that the [IntentFilter] attribute accepts an array of string parameters, although we only pass one, since it is possible for a BroadcastReceiver to listen to multiple intents.)

Registering a simple test setup:

To be able to run, test, and debug our project in a simple way, we’ll still start an activity and register the BroadcastReceiver from there. Remember the service registration presented in the previous blog post? The registration of a receiver looks somewhat similar. First, we need the user to grant the android.permission.READ_PHONE_STATE permission. When this is done, we simply use the RegisterReceiver method:

public class MainActivity : AppCompatActivity
{
    // ...
 
    protected override void OnCreate(Bundle savedInstanceState)
    {
        // ...
         
        var permissions = new string[]
        {
            Manifest.Permission.ReadPhoneState
        };
        ActivityCompat.RequestPermissions(this, permissions, 123);
    }
}
 
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
{
    base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
     
    if (requestCode == 123 && grantResults.Length > 0 && grantResults[0] == Permission.Granted)
    {
        RegisterReceiver(new PhonecallReceiver(), new IntentFilter(TelephonyManager.ActionPhoneStateChanged));
    }
}

As mentioned before, the RegisterReceiver method also expects the intent filter to listen for to be passed as parameter.

Retrieving additional information:

If you add a breakpoint inside the PhonecallReceiver’s OnReceive method and start the app, and either call yourself (incoming call) or somebody else (outgoing call), you should be able to watch our receiver be activated. Now we’re missing three types of additional information: What type of change did happen in the phone system (phone call, voicemail, message, etc.)? In case of a phone call, what exactly did happen (incoming call / outgoing call / returning to idle state after finishing a call)? And in case of an incoming call, what is the caller’s phone number?

The general state is encoded in the intent’s Action property: We are only interested in notifications concerning the TelephonyManager.ActionPhoneStateChanged action.

In addition, phone call details and incoming number are passed together with the intent, we just need to explicitly ask for them using the intent’s GetStringExtra method. All three steps combines look as follows:

public override void OnReceive(Context context, Intent intent)
{
	if (intent.Action == TelephonyManager.ActionPhoneStateChanged)
	{
		var state = intent.GetStringExtra(TelephonyManager.ExtraState);
		if (state == TelephonyManager.ExtraStateRinging)
		{
			var number = intent.GetStringExtra(TelephonyManager.ExtraIncomingNumber);
			// do something with this number...
		}
	}
}

Note that the phone state changed notification is triggered pretty often, it might be worth some extra effort to introduce a private string _previousState field (or similar), to save the incoming state and only continue execution (check incoming number etc.) if the state has changed, meaning that an incoming call is detected after the phone had been idle previously.

Comparison:

I hope you were able to follow both sample projects, and could identify incoming calls including the caller’s phone number both ways. Which way is better suited for our goal of continuously reacting to each and every incoming phone call?

Well, the service that registers our custom implementation of PhoneStateListener needs to be started at some point (probably again triggered by some BroadcastReceiver that listenes to system startup), and must run in the background continuously. In contrast, when relying on a BroadcastReceiver we can let the OS take care of the whole listening and triggering logic. So, both approaches have their appropriate use cases, but for our app I’d suggest to go with the BroadcastReceiver approach.

By the way, since I was speaking about continuous execution and the checking of incoming phone numbers: In the next blog post, I’ll discuss how to match phone numbers and contact names!