Get specific contact information from URI returned from Intent.ACTION_PICK

Karl Giesing picture Karl Giesing · Dec 28, 2012 · Viewed 22.4k times · Source

I am writing an Android app that has a data type that represents a person (specifically, the parent or guardian of a child). I'd like to be able to "import" the relevant data fields from the Contacts database in the Android device. (This should be optional; that is, it will not be a requirement that the parent/guardian is already in the Contacts database, nor will the Contacts database be updated if they add new parents/guardians.)

So far, I have written code to start a new Intent to choose the specific Contact (using Intent.ACTION_PICK). I then get a URI that represents a specific Contact in the database.

Unfortunately, I don't know what the next step is. It seems like this should be the simplest thing in the world to do, but apparently not. I've read through the documentation on the Android developer website, and I've looked through more than one Android book. No joy.

The specific information I'd like to get, is:

  1. The contact's name (first and last separately if possible)

  2. The contact's (primary) email address

  3. The contact's cell phone number

I imagine that this should be possible by querying using the ContentResolver, but I have no idea how to do this with the URI returned from the Intent. Most of the documentation assumes you have the Contact ID, not the Contact's URI. Also, I have no idea what kind of fields I can put into the projection for the query, assuming that this is even the right way to do what I want.

Here is my starting code:

// In a button's onClick event handler:
Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
startActivityForResult(intent, PICK_CONTACT);

// In onActivityResult:
if (resultCode == RESULT_OK) {
    if (requestCode == PICK_CONTACT) {
        contactURI = data.getData();
        // NOW WHAT?
    }
}

Answer

Karl Giesing picture Karl Giesing · Dec 29, 2012

Okay, after lots of digging, I found what I believe to be the answers. The solutions I found differ according to which Android API level you're using. However, they're not pretty at all, so if there are better solutions, I'd love to know.

In any case, the first step is to get the ID of the Contact, by doing a query on the URI returned from Intent.ACTION_PICK. While we're here, we should also get the display name, and the string representing whether the contact has a phone number or not. (We won't need them for the modern solution, but we will need them for the legacy solution.)

String id, name, phone, hasPhone;
int idx;
Cursor cursor = getContentResolver().query(contactUri, null, null, null, null);
if (cursor.moveToFirst()) {
    idx = cursor.getColumnIndex(ContactsContract.Contacts._ID);
    id = cursor.getString(idx);

    idx = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME);
    name = cursor.getString(idx);

    idx = cursor.getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER);
    hasPhone = cursor.getString(idx);
}

For the record, the columns returned from this URI are most of the fields represented by constants in the ContactsContract.Profile class (including constants inherited from other interfaces). Not included are PHOTO_FILE_ID, PHOTO_THUMBNAIL_URI, or PHOTO_URI (but PHOTO_ID is included).

Now that we have the ID, we need to get the relevant data. The first (and simplest) solution is to query an Entity. Entity queries retrieve all of the contacts data for a contact or raw contact at once. Each row represents a single Raw Contact, accessed using the constants in ContactsContract.Contacts.Entity. Usually you'll only be concerned with RAW_CONTACT_ID, DATA1, and MIMETYPE. However, if you want the first and last names separately, the Name MIME type holds the first name in DATA2 and the last name in DATA3.

You load up the variables by matching the MIMETYPE column with ContactsContract.CommonDataKinds constants; for example, the email MIME type is in ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE.

// Build the Entity URI.
Uri.Builder b = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, id).buildUpon();
b.appendPath(ContactsContract.Contacts.Entity.CONTENT_DIRECTORY);
URI contactUri = b.build();

// Create the projection (SQL fields) and sort order.
String[] projection = {
        ContactsContract.Contacts.Entity.RAW_CONTACT_ID,
        ContactsContract.Contacts.Entity.DATA1,
        ContactsContract.Contacts.Entity.MIMETYPE };
String sortOrder = ContactsContract.Contacts.Entity.RAW_CONTACT_ID + " ASC";
cursor = getContentResolver().query(contactUri, projection, null, null, sortOrder);

String mime;
int mimeIdx = cursor.getColumnIndex(ContactsContract.Contacts.Entity.MIMETYPE);
int dataIdx = cursor.getColumnIndex(ContactsContract.Contacts.Entity.DATA1);
if (cursor.moveToFirst()) {
    do {
        mime = cursor.getString(mimeIdx);
        if (mime.equalsIgnoreCase(ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)) {
            email = cursor.getString(dataIdx);
        }
        if (mime.equalsIgnoreCase(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)) {
            phone = cursor.getString(dataIdx);
        }
        // ...etc.
    } while (cursor.moveToNext());
}

Unfortunately, Entities were not introduced unti API 11 (Android 3.0, Honeycomb), which means this code is incompatible with roughly 65% of the Android devices in the marketplace (as of this writing). If you try it, you will get an IllegalArgumentException from the URI.

The second solution is to build a query string, and make one query for each data type you want to use:

// Get phone number - if they have one
if ("1".equalsIgnoreCase(hasPhone)) {
    cursor = getContentResolver().query(
            ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
            null,
            ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = "+ id, 
            null, null);
    if (cursor.moveToFirst()) {
        colIdx = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
        phone = cursor.getString(colIdx);
    }
    cursor.close();
}

// Get email address
cursor = getContentResolver().query(
        ContactsContract.CommonDataKinds.Email.CONTENT_URI,
        null,
        ContactsContract.CommonDataKinds.Email.CONTACT_ID + " = " + id,
        null, null);
if (cursor.moveToFirst()) {
    colIdx = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS);
    email = cursor.getString(colIdx);
}
cursor.close();

// ...etc.

Obviously, this way will result in a lot of separate database queries, so it's not recommended for efficiency reasons.

The solution I've come up with is to try the version that uses Entity queries, catch the IllegalArgumentException, and put the legacy code inside the catch block:

try {
    // Build the Entity URI.
    Uri.Builder b = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, id).buildUpon();
    b.appendPath(ContactsContract.Contacts.Entity.CONTENT_DIRECTORY);
        // ...etc...
} catch (IllegalArgumentException e) {
    // Get phone number - if they have one
    if ("1".equalsIgnoreCase(hasPhone)) {   
        // ...etc...
} finally {
    // If you want to display the info in GUI objects, put the code here
}

I hope this helps someone. And, again, if there are better ways to do this, I'm all ears.