Search code examples
androidandroid-contactsandroid-cursorandroid-cursorloader

Increase speed of fetching contacts with cursor


In my app I'm fetching all contacts with cursor.

private static final String[] PROJECTION = new String[]{
        PHONE_CONTACT_ID,
        DISPLAY_NAME,
        TIMESTAMP,
        HAS_PHONE_NUMBER};

CursorLoader cursorLoader = new CursorLoader(ResUtils.getInstance().getContext(),
            ContactsContract.Contacts.CONTENT_URI, PROJECTION, null, null,
            "UPPER(" + ContactsContract.Contacts.DISPLAY_NAME + ")ASC");
    Cursor cursor = cursorLoader.loadInBackground();
    // Loop for every contact in the phone
    if (cursor != null && cursor.getCount() > 0) {
        while (cursor.moveToNext()) {
            ArrayList<String> phoneNumbers = new ArrayList<>();
            String contact_id = cursor.getString(cursor.getColumnIndex(PHONE_CONTACT_ID));
            String   name = cursor.getString(cursor.getColumnIndex(DISPLAY_NAME));
            String  timeStamp = cursor.getString(cursor.getColumnIndex(TIMESTAMP));
            cursorLoader = new CursorLoader( ResUtils.getInstance().getContext(), ContactsContract.CommonDataKinds.Email.CONTENT_URI,
                    new String[]{EMAIL},ContactsContract.CommonDataKinds.Email.CONTACT_ID + " = ?", new String[]{contact_id}, null);
            Cursor emailCur = cursorLoader.loadInBackground();
            while (emailCur.moveToNext()) {
                String email = emailCur.getString(emailCur.getColumnIndex(EMAIL));
                if (TmlyUtils.isValidEmail(email)) {
                    phoneNumbers.add(ContactUtils.MAIL_TAG + email);
                }
            }
            emailCur.close();
            if (Integer.parseInt(cursor.getString(cursor.getColumnIndex(HAS_PHONE_NUMBER))) > 0) {
                cursorLoader = new CursorLoader( ResUtils.getInstance().getContext(),   ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
                        new String[]{NUMBER},ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?", new String[]{contact_id}, null);
                Cursor phones = cursorLoader.loadInBackground();
                while (phones.moveToNext()) {
                    String phoneNumber = phones.getString(phones.getColumnIndex(NUMBER));
                    phoneNumber = isValidMobileNumber(phoneNumber);
                    if (!phoneNumber.isEmpty() && !phoneNumbers.contains(ContactUtils.UPN_TAG + phoneNumber)) {
                        phoneNumbers.add(ContactUtils.UPN_TAG + phoneNumber);
                    }
                }
                phones.close();
            }
        }
        cursor.close();
    }

The code works fine but when you have thousands of contacts the app freeze during several seconds.

I'm using three cursor, the first one allow me to get all contacts in my phone

CursorLoader cursorLoader = new CursorLoader(ResUtils.getInstance().getContext(),
        ContactsContract.Contacts.CONTENT_URI, PROJECTION, null, null,
        "UPPER(" + ContactsContract.Contacts.DISPLAY_NAME + ")ASC");
Cursor cursor = cursorLoader.loadInBackground();

The second one loop for every email adress

 cursorLoader = new CursorLoader( ResUtils.getInstance().getContext(), ContactsContract.CommonDataKinds.Email.CONTENT_URI,
                new String[]{EMAIL},ContactsContract.CommonDataKinds.Email.CONTACT_ID + " = ?", new String[]{contact_id}, null);
        Cursor emailCur = cursorLoader.loadInBackground();

The third one loop for every phone

cursorLoader = new CursorLoader( ResUtils.getInstance().getContext(),   ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
                    new String[]{NUMBER},ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?", new String[]{contact_id}, null);
            Cursor phones = cursorLoader.loadInBackground();

I need to use the second and the third one If I'm not using it I can't get all phones and email for every contacts, because one contact can have several phones and email address

I try to use CursorLoader for speed up, but it's not sufficient, is it possible to get rid of the second and the third cursor ?

EDIT : I forgot to say that all the query is already inside a Retrofit call


Solution

  • This is a very common issue when working with Android ContactsContract APIs, you're basically doing one big query to get all contacts, and then a ton of little queries to get phones and emails.

    So on a phone with 1000 contacts, you may have to do 2001 queries to get all info.

    Instead, you can have one query to get all the info you need, you just need to skip the Contacts table and query directly on the Data table which contains all the info you need.

    I didn't quite understand what you're doing with contact_id, name, and those MAIL_TAG/UPN_TAG's, so I'll just print to the log the info you'd need and you'll need to sort that into the java objects that make sense to your app.

    String[] projection = {Data.CONTACT_ID, Data.DISPLAY_NAME, Data.MIMETYPE, Data.DATA1};
    String selection = Data.MIMETYPE + " IN ('" + Phone.CONTENT_ITEM_TYPE + "', '" + Email.CONTENT_ITEM_TYPE + "')";
    Cursor cur = cr.query(Data.CONTENT_URI, projection, selection, null, null);
    
    while (cur.moveToNext()) {
        long id = cur.getLong(0); // contact-id
        String name = cur.getString(1); // contact name
        String mime = cur.getString(2); // type: email / phone
        String data = cur.getString(3); // the actual info, e.g. +1-212-555-1234
    
        switch (mime) {
            case Phone.CONTENT_ITEM_TYPE: 
                Log.i("Contacts", "got a phone: " + id + ", " + name + ", " + data);
                break;
            case Email.CONTENT_ITEM_TYPE: 
                Log.i("Contacts", "got an email: " + id + ", " + name + ", " + data);
                break;
        }
    }
    cur.close();
    

    Also, as @IliaKuzmin said, you should run this on a background thread, not the UI thread.