Search code examples
androidkotlinandroid-contactsquery-performance

How to improve performance when query many data on ALL contacts from Android ContactsContract.Contacts?


OBJECTIVE

I want to get for each contacts in the user phone the following data

StructuredName.GIVEN_NAME|Phone.NUMBER|Email.DATA|StructuredPostal.CITY

I'm pretty sure I query the ContactsContract.Data table with a pure SQL query but the there is no clear documentation on how to do it. It seems that you can inject SQL in the contentResolver.query but it does not seem to be sustainable.

PROBLEM

My code hereafter works perfectly but is very slow.

Basically,

  1. I get the IDs of ALL contacts from ContactsContract.Contacts and LOOP through it and for each,
  2. SELECT naming data on CommonDataKinds.StructuredName,
  3. SELECT and LOOP phone data on CommonDataKinds.Phone,
  4. SELECT and LOOP email data on CommonDataKinds.Email,
  5. SELECT and LOOP address data on CommonDataKinds.StructuredPostal

However, the many loops are obviously counterproductive in term of performance.

With a 1000 contacts, it makes around 3000 queries.

CODE

// CREATE Content resolver
val resolver: ContentResolver = contentResolver
val cursor = resolver.query(
        ContactsContract.Contacts.CONTENT_URI,
        arrayOf(
                ContactsContract.Contacts._ID
        ),
        null,
        null,
        null
)

if ( cursor != null && cursor.count > 0) {
    // PROGRESSBAR Process
    myProgressBar?.progress = 0
    myProgressBarCircleText?.text = getString(R.string.processing_contacts)
    myProgressBar?.visibility = View.VISIBLE
    myProgressBarCircle?.visibility = View.VISIBLE
    myProgressBarCircleText?.visibility = View.VISIBLE



    // PUT BASIC REQUIRED INFO
    val jsonAllContacts = JSONObject()
    jsonAllContacts.put("source", "2")



    // EXECUTE CODE on another thread to prevent blocking UI
    Thread(Runnable {
        var cursorPosition = 0
        var currentProgress: Int


        Log.e("JSON", "cursor.count: ${cursor.count}")


        // CODE TO EXEC LOOP
        while (cursor.moveToNext()) {

            // Increment cursor for progressBar
            cursorPosition += 1
            currentProgress = ((cursorPosition.toFloat() / cursor.count.toFloat()) * 100).toInt()


            // INIT of jsonObjects
            val jsonEmail = JSONObject()
            val jsonPhone = JSONObject()
            val jsonAddress = JSONObject()
            val jsonCurrentContact = JSONObject()



            /**
             * NAME DETAILS
             */
            val contactID = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts._ID))



            val nameCur = contentResolver.query(
                    ContactsContract.Data.CONTENT_URI,
                    arrayOf(
                            ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME,
                            ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME,
                            ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME
                    ),
                    ContactsContract.Data.CONTACT_ID + " = ?" + " AND " + ContactsContract.Data.MIMETYPE + " = ?",
                    arrayOf(
                            contactID,
                            ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE
                    ),
                    null
            )
            var givenName = ""
            var familyName: String
            var middleName: String
            var fullName = ""

            if ( nameCur != null ) {
                while (nameCur.moveToNext()) {

                    givenName = nameCur.getString(nameCur.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME)) ?: ""
                    middleName = nameCur.getString(nameCur.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME)) ?: ""
                    familyName = nameCur.getString(nameCur.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME)) ?: ""


                    fullName = if ( middleName != "" && (middleName != familyName) ) {
                        "$middleName $familyName"
                    } else {
                        familyName
                    }
                }

                jsonCurrentContact.put("given", givenName)
                jsonCurrentContact.put("family", fullName)
            }
            nameCur?.close()


            /**
             * PHONE NUMBER
             */
            val phoneCur = contentResolver.query(
                    ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
                    arrayOf(
                            ContactsContract.CommonDataKinds.Phone.TYPE,
                            ContactsContract.CommonDataKinds.Phone.LABEL,
                            ContactsContract.CommonDataKinds.Phone.NUMBER
                    ),
                    ContactsContract.CommonDataKinds.Phone.CONTACT_ID + "=?",
                    arrayOf( contactID ),
                    null
            )

            if ( phoneCur != null && phoneCur.count > 0 ) {
                while (phoneCur.moveToNext()) {
                    val phoneNumType = phoneCur.getString( phoneCur.getColumnIndex(ContactsContract.CommonDataKinds.Phone.TYPE) ) ?: ""
                    val phoneNumLabel = phoneCur.getString( phoneCur.getColumnIndex(ContactsContract.CommonDataKinds.Phone.LABEL) ) ?: ""
                    var label: String
                    val phoneNumber = phoneCur.getString( phoneCur.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER) ).replace(" ", "") ?: ""

                    //Log.e("JSON", "JSON phoneNum: $phoneNumLabel $phoneNumber")


                    // TRY to get label info
                    label = if ( phoneNumType == "" ) {
                        phoneNumLabel
                    } else {
                        phoneNumType
                    }

                    jsonPhone.put("label", label)
                    jsonPhone.put("number", phoneNumber)
                    jsonCurrentContact.accumulate("phone", jsonPhone)

                }


            }
            phoneCur?.close()


            /**
             * EMAIL
             */
            val emailCur = contentResolver.query(
                    ContactsContract.CommonDataKinds.Email.CONTENT_URI,
                    arrayOf(
                            ContactsContract.CommonDataKinds.Email.LABEL,
                            ContactsContract.CommonDataKinds.Email.DATA
                    ),
                    ContactsContract.CommonDataKinds.Email.CONTACT_ID + "=?",
                    arrayOf(contactID),
                    null
            )
            if ( emailCur != null ) {

                while (emailCur.moveToNext()) {
                    val emailLabel = emailCur.getString(emailCur.getColumnIndex(ContactsContract.CommonDataKinds.Email.LABEL)) ?: ""
                    val email = emailCur.getString(emailCur.getColumnIndex(ContactsContract.CommonDataKinds.Email.DATA)) ?: ""

                    jsonEmail.put("label", emailLabel)
                    jsonEmail.put("email", email)
                    jsonCurrentContact.accumulate("email", jsonEmail)

                }

            }
            emailCur?.close()




            /**
             * ADDRESS
             */
            var street: String
            var city: String
            var postalCode: String
            var state: String
            var country: String
            var label: String
            val addressCur = contentResolver.query(
                    ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_URI,
                    arrayOf(
                            ContactsContract.CommonDataKinds.StructuredPostal.TYPE,
                            ContactsContract.CommonDataKinds.StructuredPostal.STREET,
                            ContactsContract.CommonDataKinds.StructuredPostal.CITY,
                            ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE,
                            ContactsContract.CommonDataKinds.StructuredPostal.REGION,
                            ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY
                    ),
                    ContactsContract.CommonDataKinds.StructuredPostal.CONTACT_ID + "=" + contactID,
                    null,
                    null
            )

            if ( addressCur != null ) {

                while (addressCur.moveToNext()) {
                    label         = addressCur.getString(addressCur.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.TYPE)) ?: ""
                    street          = addressCur.getString(addressCur.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.STREET)) ?: ""
                    city            = addressCur.getString(addressCur.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.CITY)) ?: ""
                    postalCode      = addressCur.getString(addressCur.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE)) ?: ""
                    state           = addressCur.getString(addressCur.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.REGION)) ?: ""
                    country         = addressCur.getString(addressCur.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY)) ?: ""

                    jsonAddress.put("label", label)
                    jsonAddress.put("street", street)
                    jsonAddress.put("city", city)
                    jsonAddress.put("postalcode", postalCode)
                    jsonAddress.put("state", state)
                    jsonAddress.put("country", country)
                    jsonCurrentContact.accumulate("address", jsonAddress)

                }

            }
            addressCur?.close()


            Log.e("", "jsonCurrentContact: $jsonCurrentContact")



            // PUT the current JSON object info into an array
            jsonAllContacts.accumulate("contacts", jsonCurrentContact)
        }
        cursor.close()

    }).start()

} else {
    cursor?.close()
}        

Solution

  • Those 3000 queries you mentioned can be reduced to just one, and it should finish rather quickly.

    We'll take advantage of two things when improving the code:

    1. All the Data stored in the ContactsContract.CommonDataKinds.XXX tables is actually stored in a single big table called Data.
    2. There's an implicit join in ContactsContract that allows us to select columns from ContactsContract.Contacts when querying over Data

    To make the code simpler, I advise you to define a Contact object to store in memory the info we find for a single contact, and use a HashMap to map a contact-ID to a Contact object

    Read more about this here: https://developer.android.com/reference/android/provider/ContactsContract.Data.html

    Here's some code to get you started:

    Map<Long, Contact> contacts = new HashMap<>();
    
    // If you need item type / label, add Data.DATA2 & Data.DATA3 to the projection
    String[] projection = {Data.CONTACT_ID, Data.DISPLAY_NAME, Data.MIMETYPE, Data.DATA1};
    // Add more types to the selection if needed, e.g. StructuredName
    String selection = Data.MIMETYPE + " IN ('" + Phone.CONTENT_ITEM_TYPE + "', '" + Email.CONTENT_ITEM_TYPE + "', '" + StructuredPostal.CONTENT_ITEM_TYPE + "')"; 
    Cursor cur = cr.query(Data.CONTENT_URI, projection, selection, null, null);
    
    // Loop through the data
    while (cur.moveToNext()) {
        long id = cur.getLong(0);
        String name = cur.getString(1);
        String mime = cur.getString(2); // email / phone / postal
        String data = cur.getString(3); // the actual info, e.g. +1-212-555-1234
    
        // get the Contact class from the HashMap, or create a new one and add it to the Hash
        Contact contact;
        if (contacts.containsKey(id)) {
            contact = contacts.get(id);
        } else {
            contact = new Contact(id);
            contact.setDisplayName(name);
            // start with empty Sets for phones and emails
            // instead of HashSets you can use some object to retain more info about the data item (e.g. label)
            contact.setPhoneNumbers(new HashSet<>()); 
            contact.setEmails(new HashSet<>());
            contact.setAddresses(new HashSet<>());
            contacts.put(id, contact);
        } 
    
        switch (mime) {
            case Phone.CONTENT_ITEM_TYPE: 
                contact.getPhoneNumbers().add(data);
                break;
            case Email.CONTENT_ITEM_TYPE: 
                contact.getEmails().add(data);
                break;
            case StructuredPostal.CONTENT_ITEM_TYPE: 
                contact.getAddresses().add(data);
                break;
        }
    }
    cur.close();
    

    FOLLOW UP Great progress on performance! Now here are some small tweaks to take your new code even further:

    1. Try to avoid the sort, pass null for sort, and adjust your code to handle the data in whatever order it comes, SQLite sort can sometimes slow down queries significantly
    2. Reduce your projection to only fields you actually need, the more stuff you put on your projection the bigger the data size that needs to be shifted around between processes on the device, which leads to more chunks with fewer rows in each
    3. Don't getString on all fields in projection, if some fields are only used by the StructuredPostal read those only for StructuredPostal rows and not for each iteration.

    report in the comments what were you able to achieve with the above tips...