Advanced Contact Syncing in Android with Kotlin: A Step-by-Step Guide

Let's break down the process of syncing phone contacts with an Android app using Kotlin.

Syncing phone contacts with your Android app can enhance the user experience by seamlessly integrating their contact lists. 

In this blog, we'll walk you through how to sync phone contacts using Kotlin, the preferred language for modern Android development.

android-contact-sync-kotlin
Photo by Melinda Gimpel on Unsplash


Why Sync Contacts?

Syncing contacts allows your app to:
  • Personalize User Experience: Offer tailored services by accessing user contacts.
  • Enhance Functionality: Enable features like quick messaging or calling from within the app.

Step 1: Set Up Permissions

First, you'll need to request permissions to access the contacts. Add these permissions to your AndroidManifest.xml

<uses-permission android:name="android.permission.READ_CONTACTS"></uses-permission>

If you're targeting Android 6.0 (API level 23) or higher, you'll need to request permissions at runtime as well. Here’s how you can do it:

// Check if the permission is already granted
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) 
    != PackageManager.PERMISSION_GRANTED) {
    
    // Request the permission
    ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_CONTACTS), REQUEST_CODE_READ_CONTACTS)
}

Step 2: Request Permission Result

Override onRequestPermissionsResult to handle the user's response

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    
    if (requestCode == REQUEST_CODE_READ_CONTACTS) {
        if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // Permission granted, proceed with contact syncing
            syncContacts()
        } else {
            // Permission denied, show a message to the user
            Toast.makeText(this, "Permission denied to read contacts", Toast.LENGTH_SHORT).show()
        }
    }
}

Step 3: Access and Sync Contacts

Now, let’s write the code to read and sync contacts. We’ll use the ContentResolver to query contacts from the phone’s contact database.

private fun syncContacts() {
    val contactsList = mutableListOf<String>()

    // Access the Contacts content provider
    val cursor = contentResolver.query(
        ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
        null, null, null, null
    )

    cursor?.use {
        // Check if cursor contains data
        val nameIndex = it.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)
        val numberIndex = it.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)
        
        while (it.moveToNext()) {
            val name = it.getString(nameIndex)
            val number = it.getString(numberIndex)
            contactsList.add("Name: $name, Number: $number")
        }
    }

    // Now you have the contacts list, you can use it as needed
    displayContacts(contactsList)
}

private fun displayContacts(contacts: List<String>) {
    // Display the contacts in a TextView, Log, or any UI element
    contacts.forEach {
        Log.d("ContactSync", it)
    }
}
  

Step 4: Fetch Additional Details (Optional)

If you want to fetch more details, you'll need to query additional ContentProvider URIs. Here’s how you might include more URIs:

private fun syncContacts() {
    val contactsList = mutableListOf<ContactDTO>()

    // Build query columns name array.
    val PROJECTION_DETAILS = arrayOf(
        ContactsContract.Data.RAW_CONTACT_ID,
        ContactsContract.Data.CONTACT_ID,
        ContactsContract.Data.MIMETYPE,
        ContactsContract.Data.DATA1, // number
        ContactsContract.Data.DATA2, // first name
        ContactsContract.Data.DATA3, // last name
        ContactsContract.Data.DATA5 // middle name
    )

    // Access the Contacts content provider
    // Query data table and return related contact data.
    val cursor = contentResolver.query(
        ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
        PROJECTION_DETAILS,
        null,
        null,
        null
    )

    val contactMap = mutableMapOf<Long, ContactDTO>()
    var contactRequestDTO: ContactDTO? = null

    if (cursor != null && cursor.count > 0) {
        cursor.moveToFirst()
        do {
            val contactId = cursor.getLongOrNull(cursor.getColumnIndex(ContactsContract.Data.CONTACT_ID)) ?: 0L

            // create a contactDTO based on CONTACT_ID to prevent duplicate contact for linked social media accounts, e.g. whatsapp
            contactDTO = if (contactMap[contactId] == null) {
                val newContact = ContactDTO()
                contactMap[contactId] = newContact
                newContact
            } else {
                contactMap[contactId]
            }

            // First get mimetype column value.
            val mimeType =
                cursor.getStringOrNull(cursor.getColumnIndex(ContactsContract.Data.MIMETYPE))

            val dataValueList = getColumnValueByMimetype(cursor, mimeType.orEmpty())
            dataValueList.forEach { (key, value) ->
                when (key) {
                	// if any contact has multiple mobile numbers
                    ContactsContract.CommonDataKinds.Phone.NUMBER -> {
                        if (!value.isNullOrBlankExt()) {
                            contactRequestDTO?.mobileNumberList?.add(value.orEmpty())
                        }
                    }

                    ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME -> {
                        contactRequestDTO?.firstName = value
                    }

                    ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME -> {
                        contactRequestDTO?.lastName = value
                    }

                    ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME -> {
                        contactRequestDTO?.middleName = value
                    }
                }
            }
        } while (cursor.moveToNext())

        contactsList.addAll(contactMap.values)
    }
    cursor?.close()

    // Now you have the contacts list, you can use it as needed
    displayContacts(contactsList)
}
 

/** Return data column value by mimetype column value.
 *  Because for each mimetype there has not only one related value,
 *  So the return map with key for that field, each string for one column value.
 **/
private fun getColumnValueByMimetype(
    cursor: Cursor,
    mimeType: String
): MutableMap<String, String?> {
    val params: MutableMap<String, String?> = HashMap()
    when (mimeType) {
        ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE -> {
            // Phone.NUMBER == data1
            val phoneNumber = cursor.getStringOrNull(
                cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)
            )
            params[ContactsContract.CommonDataKinds.Phone.NUMBER] = phoneNumber
        }

        ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> {
            // StructuredName.GIVEN_NAME == DATA2
            val givenName = cursor.getStringOrNull(
                cursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME)
            )
            // StructuredName.FAMILY_NAME == DATA3
            val familyName = cursor.getStringOrNull(
                cursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME)
            )

            // StructuredName.MIDDLE_NAME == DATA5
            val middleName = cursor.getStringOrNull(
                cursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME)
            )
            params[ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME] = givenName
            params[ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME] = middleName
            params[ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME] = familyName
        }
    }

    return params
}
 
By extending your contact sync functionality, you can gather more comprehensive information from the user’s contact list, such as email addresses, contact photos, and addresses. This can significantly enrich the features of your app, providing a more personalized and engaging user experience.

Additional Note: Sync Contacts in a Background Thread Using ViewModel

To enhance performance and ensure a smooth user experience, it's crucial to perform intensive operations like syncing contacts on a background thread. This avoids blocking the main UI thread and keeps your app responsive. One effective way to achieve this is by using ViewModel and LiveData from Android’s Architecture Components.

class ContactViewModel(application: Application) : AndroidViewModel(application) {
    
    private val _contactsLiveData = MutableLiveData<List<ContactDTO>>()
    val contactsLiveData: LiveData<List<ContactDTO>> get() = _contactsLiveData

    fun fetchContacts() {
        viewModelScope.launch(Dispatchers.IO) {
            val contactsList = fetchContacts()
            withContext(Dispatchers.Main) {
                _contactsLiveData.value = contactsList
            }
        }
    }

    private fun syncContacts(): List<ContactDTO> {
    	....
    }
}

Step 5: Handle Data Safely

Be sure to handle user data responsibly: 
  • Follow Privacy Guidelines: Ensure you only access and use data for intended purposes. 
  • Handle Data Securely: Store and transmit data securely if needed.

Conclusion

Syncing contacts in your Android app with Kotlin involves requesting permissions, accessing the contact database, and handling data responsibly. With these steps, you can integrate phone contacts into your app, enhancing its functionality and user experience.

Feel free to tweak the example code to suit your app’s needs.

Thanks for reading this article. Hope you would have liked it!. Please share and subscribe to my blog to support.

Pragnesh Ghoda

A forward-thinking developer offering more than 8 years of experience building, integrating, and supporting android applications for mobile and tablet devices on the Android platform. Talks about #kotlin and #android

Post a Comment

Please let us know about any concerns or query.

Previous Post Next Post

Contact Form