androidnativecontactsandroid-contact-mimetype

How do I get the telephone numbers of a Contact retrieved by clicking on a custom field in the native Contacts app in Android?


The situation: I've added a custom action to a contact in Android following the instructions in this question and the related sample app on GitHub. When pressed I want to dial that contact in my application. I am able to successfully retrieve the Contact in the Activity that is opened when the custom action is pressed.

I execute this:

Cursor cursor = context.getContentResolver().query(data, null, null, null, null);
if (cursor != null) {
    newIntent = true;
    contact = LocalContactAsync.getContacts(cursor, context.getContentResolver()).get(0);
    cursor.close();
} 

And the data I retrieve from Android is:

content://com.android.contacts/data/2849

Notice the number 2849 at the end, this is not the native ID of the contact. The native ID of the contact is 459. I am able to successfully retrieve the contact executing this query, the following data returns:

cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts._ID); 

-returns '2849'

cur.getString(cur.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)) ; 

-returns 'sample samplee' which is correct

But although this is true:

cursor.getInt(cur.getColumnIndex(
            ContactsContract.Contacts.HAS_PHONE_NUMBER)) > 0)

The following function returns an empty cursor:

Cursor pCur = cr.query(
                ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
                null,
                ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?",
                new String[]{id}, null); 

-id = 2849 in this case but if I fill in 459 I retrieve the right amount of telephone numbers

The real contact has 3 numbers so it should return 3 numbers.

How am I able to fix this?

Edited:

This is how I retrieve numbers, to be clear: I get the correct name, but the following query returns null while the contact has numbers.

    ArrayList<Number> numbers = new ArrayList<>();
    if (cur.getInt(cur.getColumnIndex(
            ContactsContract.Contacts.HAS_PHONE_NUMBER)) > 0) {
        Cursor pCur = cr.query(
                ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
                null,
                ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?",
                new String[]{id}, null);
        while (pCur.moveToNext()) {
            numbers.add(new nl.coffeeit.clearvoxnexxt.objects.dto.Number(pCur.getString(pCur.getColumnIndex(
                    ContactsContract.CommonDataKinds.Phone.NUMBER))));
        }
        pCur.close();
    }
    return numbers;

Please note that I do not request an intent, I receive it through a custom action that is added to a native contact, like Viber and WhatsApp do:

custom action android contact

Full code LocalContacts Async:

private static final String TAG = "LocalContactAsync";
private static List<Contact> contacts;

private Context context;
private boolean refreshOtherFragments;
private boolean renew;    

private synchronized List<Contact> getContacts(Context context) {
    if (!renew && contacts != null) {
        return contacts;
    }
    ContentResolver cr = context.getContentResolver();
    Cursor cur = cr.query(ContactsContract.Contacts.CONTENT_URI,
            null, null, null, null);

    if (cur != null && cur.getCount() > 0) {
        contacts = getContacts(cur, cr);
        cur.close();
        return contacts;
    }
    if (cur != null) {
        cur.close();
    }
    return new ArrayList<>();
}

public static List<Contact> getContacts(Cursor cur, ContentResolver cr) {
    List<Contact> contacts = new ArrayList<>();
    while (cur.moveToNext()) {
        String id =  getId(cur);
        String name = getName(cur);
        ArrayList<Number> numbers = getNumbers(cur, cr, id);
        if (name != null) {
            contacts.add(new Contact(id, name, numbers));
        }
    }
    Log.d(TAG, "amount of contacts" + contacts.size());
    return contacts;
}

private static ArrayList<Number> getNumbers(Cursor cur, ContentResolver cr, String id) {
    ArrayList<Number> numbers = new ArrayList<>();
    if (cur.getInt(cur.getColumnIndex(
            ContactsContract.Contacts.HAS_PHONE_NUMBER)) > 0) {
        Cursor pCur = cr.query(
                ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
                null,
                ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?",
                new String[]{id}, null);
        while (pCur.moveToNext()) {
            numbers.add(getNumber(pCur));
        }
        pCur.close();
    }
    return numbers;
}

private static Number getNumber(Cursor pCur) {
    return new nl.coffeeit.clearvoxnexxt.objects.dto.Number(pCur.getString(pCur.getColumnIndex(
            ContactsContract.CommonDataKinds.Phone.NUMBER)));
}

private static String getId(Cursor cur) {
    return cur.getString(
            cur.getColumnIndex(ContactsContract.Contacts._ID));
}

private static String getName(Cursor cur) {
    return cur.getString(cur.getColumnIndex(
            ContactsContract.Contacts.DISPLAY_NAME));
}

Code for Number DTO:

public class Number implements Parcelable, Serializable {

    @SerializedName("number")
    @Expose
    public String number;
    @SerializedName("type")
    @Expose
    public String type = "";
    @SerializedName("inherited")
    @Expose
    public Boolean inherited = false;

    public Number(String number) {
        this.number = number;
    }

    protected Number(Parcel in) {
        number = in.readString();
        type = in.readString();
        byte inheritedVal = in.readByte();
        inherited = inheritedVal == 0x02 ? null : inheritedVal != 0x00;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(number);
        dest.writeString(type);
        if (inherited == null) {
            dest.writeByte((byte) (0x02));
        } else {
            dest.writeByte((byte) (inherited ? 0x01 : 0x00));
        }
    }

    @SuppressWarnings("unused")
    public static final Parcelable.Creator<Number> CREATOR = new Parcelable.Creator<Number>() {
        @Override
        public Number createFromParcel(Parcel in) {
            return new Number(in);
        }

        @Override
        public Number[] newArray(int size) {
            return new Number[size];
        }
    };

    public Number setNumber(String number) {
        this.number = number;
        return this;
    }
}

Solution

  • The first thing to notice is that a call to the contacts picker like this:

    Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
    

    will return a Uri like this:

    content://com.android.contacts/contacts/lookup/3163r328-4D2941473F314D2941473F3131/328
    

    The second to last path (3163r....) is the lookup key, while 328 is the NAME_RAW_ID.

    Compare this with the Intent you get from the sample application. This contains an Uri that looks like this:

    content://com.android.contacts/data/2849
    

    As you have said, calling the content resolver with this Uri is not sufficient to retrieve phone numbers, although it may be used to retrieve the name of the contact and the id. So we will use the incomplete Intent Uri to construct a new Lookup Uri that we can use to get the phone numbers.

    Let's add the following methods to your LocalContactAsync (I won't refactor anything you have done so far, I'll just add in the style you have used):

    public static Uri getLookupUri(Cursor cur) {
        return getContentUri(getLookupKey(cur), getNameRawId(cur));
    }
    
    private static String getLookupKey(Cursor cur)  {
        return cur.getString(
                cur.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY));
    }
    
    private static String getNameRawId(Cursor cur) {
        return cur.getString(cur.getColumnIndex(ContactsContract.Contacts.NAME_RAW_CONTACT_ID));
    }
    
    private static Uri getContentUri(String lookupKey, String nameRawId) {
        return new Uri.Builder()
                .scheme("content")
                .authority("com.android.contacts")
                .appendPath("contacts")
                .appendPath("lookup")
                .appendPath(lookupKey)
                .appendPath(nameRawId)
                .build();
    }
    

    Let's alter the ViewingActivity inside the sample application so that it actually retrieves the contact details. We can now do that with the following code inside onResume():

    @Override
    protected void onResume() {
        super.onResume();
        Uri uri = getIntent().getData();
        Cursor intentCursor = this.getContentResolver().query(uri, null, null, null, null);
        Contact contact = null;
        if (intentCursor != null) {
            intentCursor.moveToFirst();
            Uri lookupUri = LocalContactAsync.getLookupUri(intentCursor);
            Cursor lookupCursor = this.getContentResolver().query(lookupUri, null, null, null, null);
            contact = LocalContactAsync.getContacts(lookupCursor, this.getContentResolver()).get(0);
            intentCursor.close();
            lookupCursor.close();
        }
    }
    

    The contact will now contain the phone numbers as required.