7

Background

Contacts on the address book can have an account data that's attached to them. Each app can have an account, and then add its own information for the contact.

Apps such as Telegram, WhatsApp, Viber,... - all create an account that adds information and/or actions to contacts.

Here's an example of a contact that has both WhatsApp and Viber accounts for it:

enter image description here

The problem

I'm trying to figure out how to fetch all contacts that have a specified account.

Since WhatsApp is the most popular that I know of, my tests focus on it.

My problem is that some users claim what I did barely returns contacts, and some claim it doesn't show even a single one. It seems to usually work, and in my case it always worked, but something is probably not good on the code.

What I've tried

I got to make the next code, which to me seems to work, getting a map of phone-to-contact-info, of all WhatsApp contacts.

The idea is to get all possible information of WhatsApp contacts, vs all basic contacts data, and merge those that match the same lookup-key.

I tried to use a better query of joining, but I failed. Maybe it is possible too, and might be more efficient.

Here's the code:

/**
 * returns a map of lookup-key to contact-info, of all WhatsApp contacts
 */
@NonNull
public HashMap<String, ContactInfo> getAllWhatsAppPhones(Context context) {
    ContentResolver cr = context.getContentResolver();
    final HashMap<String, ContactInfo> phoneToContactInfoMap = new HashMap<>();
    final HashMap<String, String> whatsAppLookupKeyToPhoneMap = new HashMap<>();
    final String phoneMimeType = Phone.CONTENT_ITEM_TYPE;
    final Cursor whatsAppCursor;
    whatsAppCursor = cr.query(Data.CONTENT_URI,
            new String[]{Phone.NUMBER, Phone.LOOKUP_KEY},
            Phone.MIMETYPE + " = ?", new String[]{WhatsAppStuff.WHATS_APP_MIME_TYPE}, null);
    if (whatsAppCursor == null)
        return phoneToContactInfoMap;
    Cursor contactCursor = cr.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
            //null,
            new String[]{
                    Contacts.LOOKUP_KEY, Contacts._ID, Contacts.PHOTO_THUMBNAIL_URI,
                    ContactsContract.Contacts.DISPLAY_NAME,                        //        ContactsContract.CommonDataKinds.Phone.NUMBER,
            },
            "(" + Phone.MIMETYPE + " IS NULL OR " + Phone.MIMETYPE + " = '" + phoneMimeType + "') AND ("
                    + ContactsContract.RawContacts.ACCOUNT_TYPE + " = 'com.google' OR " + ContactsContract.RawContacts.ACCOUNT_TYPE + " IS NULL)",
            null, null);
    if (contactCursor == null) {
        whatsAppCursor.close();
        return phoneToContactInfoMap;
    }
    int progress = 0;
    final int phoneNumberIdx = whatsAppCursor.getColumnIndex(Phone.NUMBER);
    final int lookupKeyIdx = whatsAppCursor.getColumnIndex(Phone.LOOKUP_KEY);
    while (whatsAppCursor.moveToNext()) {
        final String phoneNumberValue = whatsAppCursor.getString(phoneNumberIdx);
        final int endIndex = phoneNumberValue.indexOf("@");
        if (endIndex < 0)
            continue;
        String lookupKey = whatsAppCursor.getString(lookupKeyIdx);
        final String phone = phoneNumberValue.substring(0, endIndex);
        if (!phone.isEmpty() && StringUtil.isAllDigits(phone)) {
            //Log.d("AppLog", "whatsApp phone:" + phone + " " + lookupKey);
            whatsAppLookupKeyToPhoneMap.put(lookupKey, phone);
        }
        if (markedToCancel != null && markedToCancel.get()) {
            whatsAppCursor.close();
            contactCursor.close();
            return phoneToContactInfoMap;
        }
        if (progressListener != null)
            progressListener.onProgressUpdate(progress++, maxProgress);
    }
    whatsAppCursor.close();
    if (whatsAppLookupKeyToPhoneMap.isEmpty())
        return phoneToContactInfoMap;
    //Log.d("AppLog", "getting info about whatsapp contacts");
    final int idColIdx = contactCursor.getColumnIndex(Contacts._ID);
    final int displayNameColIdx = contactCursor.getColumnIndex(Contacts.DISPLAY_NAME);
    final int lookupKeyColIdx = contactCursor.getColumnIndex(Contacts.LOOKUP_KEY);
    final int photoColIdx = contactCursor.getColumnIndex(Contacts.PHOTO_THUMBNAIL_URI);

    while (contactCursor.moveToNext()) {
        String lookupKey = contactCursor.getString(lookupKeyColIdx);
        String phoneNumber = whatsAppLookupKeyToPhoneMap.get(lookupKey);
        if (phoneNumber == null)
            continue;
        ContactInfo contactInfo = new ContactInfo();
        contactInfo.lookupKey = lookupKey;
        contactInfo.displayName = contactCursor.getString(displayNameColIdx);
        contactInfo.photoThumbUriStr = contactCursor.getString(photoColIdx);
        contactInfo.whatsAppPhoneNumber = phoneNumber;
        contactInfo.contactId = contactCursor.getLong(idColIdx);
        phoneToContactInfoMap.put(phoneNumber, contactInfo);
        if (markedToCancel != null && markedToCancel.get()) {
            contactCursor.close();
            return phoneToContactInfoMap;
        }
        if (progressListener != null)
            progressListener.onProgressUpdate(progress++, maxProgress);
    }
    contactCursor.close();
    return phoneToContactInfoMap;
}

The question

As I wrote, the above code only usually works.

How come it only usually works? What's missing to fix it?

Should I use Contacts.getLookupUri instead of lookup key? If so, how should I change the code above to use it instead?

I tried to use a URI instead of a lookup-key, but then it didn't find any of them inside the normal contacts.

android developer
  • 114,585
  • 152
  • 739
  • 1,270

1 Answers1

4

The main issue I see that can explain why users won't see results from your code, is that you're assuming all the contacts are stored on a Google account.

While this is the default behavior in some devices, it's not the default on all devices, also, users can freely change their contacts storage to any other location (yahoo contacts, MS exchange, phone-only (unsynced), etc.)

Having that said, if your only requirement is to

fetch all contacts that have a specified account.

I think that's a much better alternative then your 2 queries (one of which runs over all contacts, not just the required ones):

// Uri to query contacts that have a RawContact in the desired account
final Uri.Builder builder = Contacts.CONTENT_URI.buildUpon();
builder.appendQueryParameter(RawContacts.ACCOUNT_NAME, whatsappAccountName);
builder.appendQueryParameter(RawContacts.ACCOUNT_TYPE, whatsappAccountType);
Uri uri = builder.build();

String[] projection = new String[]{ Contacts.LOOKUP_KEY, Contacts._ID Contacts.DISPLAY_NAME }; // add more if needed

// boo-yaa!
Cursor cur = cr.query(uri, projection, null, null, null);
marmor
  • 27,641
  • 11
  • 107
  • 150
  • First, I'm pretty sure what I did is checking on both device and Google. Second, the users insisted that the contacts are stored on Google account. Since I need information of the contact that is connected to WhatsApp account, I need the extra query, just as shown on the contact details screen on the screenshot I've provided. If there is a better way to fetch all general information of the contact (name, phone numbers, photos,...) including way to update contact, and not just that it has a WhatsApp account, please let me know. – android developer Aug 16 '17 at 08:23
  • well, you have 2 queries, one to get all lookupKeys from all whatsapp contacts, the other goes over all contacts in Google, the second one is going over much more contacts then needed. the query i present will limit to only required contacts, if needed you can then fetch the phone-number and other details you might be missing, in another smaller query. but this will save you the long query i believe. – marmor Aug 16 '17 at 08:29
  • 1
    regarding google/device contacts, (a) there are many other sources for contacts, not just google/device (b) device contacts doesn't mean the account_type is NULL, see: https://stackoverflow.com/a/44802016/819355 – marmor Aug 16 '17 at 08:32
  • So how should I change the code, to support getting all of the basic information, including a way to modify the contact (like putting a photo) ? Is it possible in a single query ? Should I avoid the account specifying of non-whatsapp ? Should I add all types of device accounts that you've pointed to on the link? – android developer Aug 16 '17 at 09:11
  • `including a way to modify the contact` while iterating through the contacts cursor, you can update it if needed, just like you would do in your code i believe. `Should I avoid the account` yes, i don't see a need to specify specific account-type, why not check them all. – marmor Aug 16 '17 at 14:43
  • `support getting all of the basic information` sounds funny, but i would get all the `Contacts._ID` from the above query, and then query over the `Data` table with selection `Data.CONTACT_ID IN idsList`, if idsList is > 500, you might need to split it to batches – marmor Aug 16 '17 at 14:45
  • But if I find more than one contact info (of more than one account), how would I know which of them has the needed information? For example, there might be one of account1 that has a photo (or any other field), and one of account2 that doesn't have a photo. Not only that, but if I modify one that I've found, I think it doesn't neccasary mean that it will succeed or even when it does succeed, that the user will see it in the address book app... – android developer Aug 16 '17 at 15:23
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/152073/discussion-between-marmor-and-android-developer). – marmor Aug 16 '17 at 15:25
  • Plus, if I remove checking of accounts, this will also include the one of WhatsApp, no? And I don't think updating photo there changes the way it looks on the contacts app – android developer Aug 16 '17 at 16:39
  • I don't get it. I thought there is a main account that all others merge into it, that is the correct one that I should modify in case I'm making a contacts app myself. You say it's a whole pile of accounts that make a contact? If so, how could I know to which one of them to modify the data (or query for photos and other info) ? To all? To specific ones? – android developer Aug 16 '17 at 16:41
  • I also don't see Viber account contacts using the new query (of "contactCursor ", that has no selection anymore) . Only WhatsApp and Google... How could it be? – android developer Aug 16 '17 at 16:53