Find the contact name to a phone number on Android #3

Welcome to the third part of my contact name lookup tutorial for Xamarin Android – if you’ve come this far, you probably rebuilt and tested the solution presented in part #2. If so, you might have noticed that the lookup works for some phone numbers but not for others – what’s this about?

Well, as I mentioned shortly already, normalization only removes superfluous characters from a phone number but does not change its structure. This means, that phone numbers stored in Android’s contact app in local format (meaning with leading zero, but without country code) will never match successfully, because incoming phone numbers detected by our phonecall receiver always include country code prefixes.

For example, consider the following (fictional) german mobile phone number: (0)172 123-45-67

After passing it to the PhoneNumberUtils.NormalizeNumber() method, it will be simplified to: 01721234567

However, when a call from this number is detected, the phonecall receiver would deliver it in the following format: +491721234567

Both strings represent normalized phone numbers, but they do not match on a simple string comparison. Therefore, let’s have a look at an alternative lookup method, and afterwards discuss which one to use when.

Remember the basic structure for contact database lookups?

static IEnumerable<string> FindCallerName(Context context, string number)
{
    var uri = ContactsContract.CommonDataKinds.Phone.ContentUri;
     
    // TODO: specify selection details
     
    var loader = new CursorLoader(context, uri, projection, selection, selectionArgs, sortOrder);
    var cursor = (ICursor) loader.LoadInBackground();
     
    if (cursor.MoveToFirst())
    {
        do
        {
            // TODO: access data set and return name
        } while (cursor.MoveToNext());
    }
}

This time, the idea is to load all contact items (remember we can do so by not specifying a selection for the loader!) and do the comparison ourselves instead of asking the database to filter automatically. Obviously, this means we need to retrieve both each contact’s name and its stored phone number, otherwise the number will not be present later on for comparison:

static IEnumerable<string> FindCallerName(Context context, string number)
{
    var uri = ContactsContract.CommonDataKinds.Phone.ContentUri;
    
	string[] projection = {
		ContactsContract.Contacts.InterfaceConsts.DisplayName,
		ContactsContract.CommonDataKinds.Phone.Number,
	};
     
    var loader = new CursorLoader(context, uri, projection, null, null, null);
    var cursor = (ICursor) loader.LoadInBackground();
     
    if (cursor.MoveToFirst())
    {
        do
        {
            // TODO: access data set, do comparison and maybe return name
        } while (cursor.MoveToNext());
    }
}

Inside the iterator, we can now work with both the contact’s name and its phone number, and use some custom logic to handle the comparison of contact phone number to incoming phone number. Fortunately, Android provides us with the PhoneNumberUtils.Compare() method which takes two string representing phone numbers and decides (according to documentation) whether they’re identical enough for caller ID purposes. Voila, exactly what we need, so let’s implement it: Retrieve phone number, execute comparison, return contact name if the comparison was positive:

static IEnumerable<string> FindCallerName(Context context, string number)
{
    var uri = ContactsContract.CommonDataKinds.Phone.ContentUri;
    
	string[] projection = {
		ContactsContract.Contacts.InterfaceConsts.DisplayName,
		ContactsContract.CommonDataKinds.Phone.Number,
	};
     
    var loader = new CursorLoader(context, uri, projection, null, null, null);
    var cursor = (ICursor) loader.LoadInBackground();
     
    if (cursor.MoveToFirst())
    {
        do
        {
            var contactNumber = cursor.GetString(cursor.GetColumnIndex(projection[1]));
			if (PhoneNumberUtils.Compare(contactNumber, number))
			{
				var callerName = cursor.GetString(cursor.GetColumnIndex(projection[0]));
				yield return callerName;
			}
        } while (cursor.MoveToNext());
    }
}

Of course, handling things “manually” that were done by the database in our previous version will certainly have performance implications. Therefore, I decided to include both variants as two separate lookups methods and use them one after the other: Only if the normalized number lookup does not deliver any results, try the manual approach.