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

After setting up the basic structure of a contacts database lookup function, this time we’ll look at all the parameters which are there to customize the lookup! Beside context and target URI, the CursorLoader constructor accepts the following properties:

  • Projection defines which columns of the detected data sets (in our case, phone contacts) to return (therefore this is an array parameter)
  • Selection and selection arguments define the detailed search criteria to be performed
  • Sort order is quite self-explanatory: this one defines how to sort the results before returning them

Note that all these are optional, in contrast to context and uri, although the constructor method’s signature does not reveal this: To not specify one of the optional parameters, we are allowed to pass null. For example,

  • if passing null as projection, all fields of the detected phone contact items will be returned,
  • if passing null as selection and selection arguments, all available phone contact items will be returned (This does not instantly make sense – why starting a lookup in the first place if requesting a full list anyway? We’ll look at an potential use case in the upcoming blog post!)
  • if passing null the results will be returned in arbitrary order.

In our case, we are looking for the name of a certain contact that is assigned a given phone number. Translating that requirement to the parameters, this means:

  • It is sufficient to retrieve the contact’s name (since all we want to do is read this out aloud), so we specify only the ContactsContract.Contacts.InterfaceConsts.DisplayName as projection (we still need to wrap that into a single-item array to meet the constructor’s signature).
  • Since we hope to get only one result item, we omit specifying a custom sort order.
  • The selection parameter needs more considerations: Basically, it can be compared to an SQL query’s WHERE clause – we specify columns in the phone contacts data set (in our case only one, the phone number column), and actual values (in our case, the incoming calling number), and
    let the database compare them. Speaking about SQL: We can even make use of the common LIKE keyword for string comparison. However, phone numbers can be typed in different ways: With or without leading + signs and zeros, with intermediate blank spaces and hyphens, etc. This means the probability that the incoming phone number and the stored phone number of any contact will exactly match is quite small.
    For these cases, Android provides the concept of normalized phone numbers: We can use the PhoneNumberUtils.NormalizeNumber method to remove superfluous information, such as blanks and hyphens, from the incoming phone number. In addition, we also need to make sure that it is compared to each stored contact’s normalized number, so our selection query looks as follows:
var selection = $"{ContactsContract.CommonDataKinds.Phone.NormalizedNumber} LIKE ?";

The actual value(s) to be compared against are not specified directly in the selection clause, instead ? signs are used as placeholder, and the value(s) need to be passed in a separate array parameter:

var normalizedNumber = PhoneNumberUtils.NormalizeNumber(number);
var selectionArgs = new string[] { normalizedNumber };

Finally, after running the lookup and while looping through the results, the projection parameter is re-used to access certain columns of the each result set.

The complete contact lookup helper method looks like (new lines highlighted):

static IEnumerable<string> FindCallerName(Context context, string number)
{
    var uri = ContactsContract.CommonDataKinds.Phone.ContentUri;
     
	string[] projection = {
		ContactsContract.Contacts.InterfaceConsts.DisplayName,
	};
	var selection = $"{ContactsContract.CommonDataKinds.Phone.NormalizedNumber} LIKE ?";
	var normalizedNumber = PhoneNumberUtils.NormalizeNumber(number);
	var selectionArgs = new string[] { normalizedNumber };
	
    var loader = new CursorLoader(context, uri, projection, selection, selectionArgs, null);
    var cursor = (ICursor) loader.LoadInBackground();
     
    if (cursor.MoveToFirst())
    {
        do
        {
			var callerName = cursor.GetString(cursor.GetColumnIndex(projection[0]));
			yield return callerName;
        } while (cursor.MoveToNext());
    }
}

When trying to run this helper method against the contacts stored on your phone, e.g. by calling it directly and passing a known phone number as string to it, you might or might not get results – why is that? Well, as mentioned before, phone numbers can be written in a variety of ways – the normalization approach we use in the code snippets removes superfluous characters such as blanks and hyphens, but there are other mismatches it cannot compensate for.

For example, many people store phone numbers including leading zero characters, but without leading country codes. In contrast, the incoming phone number detected by our phonecall receiver will always include country code with a leading + sign. These two variants will never match, even when normalizing them. In the upcoming blog post, I’ll present an alternative contact lookup strategy that addresses this issue.