Android Contacts Provider Tutorial

Photo by Ozgur Kara on Unsplash
Photo by Ozgur Kara on Unsplash
Contacts Provider is an Android built-in content provider. It manages contacts in the system. Android’s Contacts app also accesses contact information through the contacts provider.

Contacts Provider is an Android built-in content provider. It manages contacts in the system. Android’s Contacts app also accesses contact information through the contacts provider. This article will introduce how to use the contacts provider to access the contact information in the system.

The complete code for this chapter can be found in and .

Overview

Contacts Provider mainly manages three tables:

The hierarchical relationship of these three tables is as follows:

Contacts Provider table structure.
Contacts Provider table structure from Google Developers .

When we add a new contact, we actually create a new row in the RawContacts table. Then, the detailed information of this contact, such as last name, first name, email, phone number, each information will be created a new row in the Data table. Therefore, RawContacts and Data tables are a one-to-many relationship.

In Android, we can add multiple accounts, for example, one account is waynestalk@gmail.com, and the other account is hello@gmail.com. Therefore the RawContacts table has the following two fields to indicate its source:

  • ACCOUNT_NAME : account name, such as waynestalk@gmail.com.
  • ACCOUNT_TYPE : account type, such as Google account is com.google.

So in fact, when we access the contact list, we mostly access the RawContacts table. What is the use of the Contacts table? Conceptually, each row in the Contacts table represents a person. However, the same person may have multiple contact information. When the user adds a contact data to the RawContacts table, the Contacts Provider will determine whether the new contact data can be associated with a person in the Contacts table. If so, the Contacts table will associate them. If not, it will create a new row in the Contacts table. Therefore, Contacts and RawContacts tables are a one-to-many relationship.

So what is the basis for ContactsProvider to determine whether the new added contact information can be associated with a person in the Contacts table? There are some rough introductions on the official website , but the rules are not clearly stated. In addition, this article has done some analysis, which may help you understand. So in fact, we can only read the Contacts table, but not add or modify it. In addition, when synchronizing the contacts of all accounts, this associated action will also occur.

Retrieving Contacts

First of all, we must first add READ_CONSTACTS permission in AndroidManifest.xml, as follows.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.READ_CONTACTS" />
</manifest>

Then, in the activity, request the READ_CONSTACTS permission.

ActivityCompat.requestPermissions(
    this,
    arrayOf(Manifest.permission.READ_CONTACTS),
    PERMISSION_REQUEST_CODE
)

In the following code, we use ContentResolver.query() to get the contact list from RawContacts table. Its parameters are described as follows:

  • uri: Specifies to access the RowContacts table.
  • projection: Specifies to access the _ID and DISPLAY_NAME_PRIMARY columns in the RowContacts table.
  • selection: Same as WHERE of SELECT.
  • selectionArgs: Replaces the ? in the selection parameter.
  • sortOrder: Same as ORDER BY of SELECT.

So, using query() is very similar to SQL. The difference is that in SQL, we specify the name of the table, but in query(), we specify the content URI.

You can find more columns in ContactsContract.RawContacts.

class RawContactListViewModel : ViewModel() {
    private val projection = arrayOf(
        ContactsContract.RawContacts._ID,
        ContactsContract.RawContacts.DISPLAY_NAME_PRIMARY,
    )

    val rawContacts: MutableLiveData<List<RawAccount>> = MutableLiveData()

    fun loadRawContacts(contentResolver: ContentResolver, pattern: String? = null) {
        viewModelScope.launch(Dispatchers.IO) {
            val cursor = contentResolver.query(
                ContactsContract.RawContacts.CONTENT_URI,
                projection,
                pattern?.let { "${ContactsContract.RawContacts.DISPLAY_NAME_PRIMARY} LIKE ?" },
                pattern?.let { arrayOf("%$it%") },
                "${ContactsContract.RawContacts.ACCOUNT_NAME} ASC",
            ) ?: return@launch

            if (cursor.count == 0) {
                cursor.close()
                rawContacts.postValue(emptyList())
                return@launch
            }

            val idIndex = cursor.getColumnIndex(ContactsContract.RawContacts._ID)
            val nameIndex = cursor.getColumnIndex(ContactsContract.RawContacts.DISPLAY_NAME_PRIMARY)

            val list = mutableListOf<RawAccount>()
            while (cursor.moveToNext()) {
                val id = cursor.getLong(idIndex)
                val name = cursor.getString(nameIndex)
                list.add(RawAccount(id, name))
            }

            cursor.close()
            rawContacts.postValue(list)
        }
    }
}

Retrieving Contact Details

All contact details are stored in ContactsContract.Data. Each raw only stores one type of data. You can know what kind of data this is by ContactsContract.DataColumns.MIMETYPE column. In the following code, we specify to retrieve all the email data of a RawContact in the selection parameter. We can get the mime type string of email from ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE.

You can find more data types in ContactsContract.CommonDataKinds .

class ContactViewModel : ViewModel() {
    var rawContactId: Long = 0

    val name: MutableLiveData<String?> = MutableLiveData()
    val emailList: MutableLiveData<List<ContactEmail>> = MutableLiveData()

    val result: MutableLiveData<Result<String>> = MutableLiveData()

    private fun loadEmail(contentResolver: ContentResolver): List<ContactEmail> {
        val cursor = contentResolver.query(
            ContactsContract.Data.CONTENT_URI,
            arrayOf(
                ContactsContract.CommonDataKinds.Email._ID, 
                ContactsContract.CommonDataKinds.Email.ADDRESS, 
                ContactsContract.CommonDataKinds.Email.TYPE
            ),
            "${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.Data.MIMETYPE} = '${ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE}'",
            arrayOf("$rawContactId"),
            "${ContactsContract.CommonDataKinds.Email.TYPE} ASC"
        ) ?: return emptyList()

        if (cursor.count == 0) {
            cursor.close()
            return emptyList()
        }

        val idIndex = cursor.getColumnIndex(ContactsContract.Data._ID)
        val emailAddressIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS)
        val emailTypeIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.TYPE)

        val list = mutableListOf<ContactEmail>()
        while (cursor.moveToNext()) {
            val id = cursor.getLong(idIndex)
            val emailAddress = cursor.getString(emailAddressIndex)
            val emailType = cursor.getInt(emailTypeIndex)
            list.add(ContactEmail(id, emailAddress, emailType))
        }

        cursor.close()
        return list
    }
}

Editing Contact Details

We can also modify the contact details. First, we need to add WRITE_CONTACTS permission in AndroidManifest.xml.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.WRITE_CONTACTS" />
</manifest>

Then, in the activity, request the WRITE_CONTACTS permission.

ActivityCompat.requestPermissions(
    this,
    arrayOf(Manifest.permission.WRITE_CONTACTS),
    PERMISSION_REQUEST_CODE
)

Adding Contact Details

In the following code, we add an email data for a RawContact.

class ContactViewModel : ViewModel() {
    var rawContactId: Long = 0

    val result: MutableLiveData<Result<String>> = MutableLiveData()

    fun addEmail(contentResolver: ContentResolver, email: String, type: Int) {
        viewModelScope.launch(Dispatchers.IO) {
            try {
                val values = ContentValues()
                values.put(Data.RAW_CONTACT_ID, rawContactId)
                values.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE)
                values.put(Email.TYPE, type)
                values.put(Email.ADDRESS, email)
                val uri = contentResolver.insert(Data.CONTENT_URI, values)

                result.postValue(
                    if (uri != null) Result.success(uri.toString())
                    else Result.failure(Exception("Error on inserting email"))
                )
            } catch (e: Exception) {
                result.postValue(Result.failure(e))
            }
        }
    }
}

Modifying Contact Details

In the following code, we modify an email data for a RawContact.

class ContactViewModel : ViewModel() {
    var rawContactId: Long = 0

    val result: MutableLiveData<Result<String>> = MutableLiveData()

    fun saveEmail(contentResolver: ContentResolver, email: String, type: Int) {
        viewModelScope.launch(Dispatchers.IO) {
            try {
                val contactEmail = emailList.value?.find { it.type == type } ?: return@launch

                val values = ContentValues()
                values.put(Email.ADDRESS, email)
                val count = contentResolver.update(
                    Data.CONTENT_URI,
                    values,
                    "${Data._ID} = ?",
                    arrayOf("${contactEmail.id}")
                )

                result.postValue(
                    if (count == 1) Result.success("Updated email")
                    else Result.failure(Exception("Error on updating email"))
                )
            } catch (e: Exception) {
                result.postValue(Result.failure(e))
            }
        }
    }
}

Deleting Contact Details

In the following code, we delete an email data from a RawContact.

class ContactViewModel : ViewModel() {
    var rawContactId: Long = 0

    val result: MutableLiveData<Result<String>> = MutableLiveData()

    fun removeEmail(contentResolver: ContentResolver, type: Int) {
        viewModelScope.launch(Dispatchers.IO) {
            try {
                val email = emailList.value?.find { it.type == type } ?: return@launch
                val count = contentResolver.delete(
                    Data.CONTENT_URI,
                    "${Data._ID} = ?",
                    arrayOf("${email.id}")
                )

                result.postValue(
                    if (count == 1) Result.success("Removed email")
                    else Result.failure(Exception("Error on removing email"))
                )
            } catch (e: Exception) {
                result.postValue(Result.failure(e))
            }
        }
    }
}

Using Intent to Add and Modify Contacts

So far, we have directly added or modified the Data table. However, we can see that there are actually quite a lot of data types in the Data table. So, if we want to fully support all data types, it’s not easy. Another recommended way is to add and modify Contacts data through the Android Contacts app. But then, we cannot customize the UI.

Adding Contacts

Create an Intent with ContactsContract.Intents.Insert.ACTION. Then, set the data we want to add in the intent.

You can find more columns in ContactsContract.Intents.Insert.

fun addContact(): Intent {
    return Intent(ContactsContract.Intents.Insert.ACTION).apply {
        type = ContactsContract.RawContacts.CONTENT_TYPE

        // Sets the special extended data for navigation
        putExtra("finishActivityOnSaveCompleted", true)

        // Insert an email address
        putExtra(ContactsContract.Intents.Insert.EMAIL, "waynestalk@gmail.com")
        putExtra(
            ContactsContract.Intents.Insert.EMAIL_TYPE,
            ContactsContract.CommonDataKinds.Email.TYPE_WORK
        )

        // Insert a phone number
        putExtra(ContactsContract.Intents.Insert.PHONE, "123456789")
        putExtra(
            ContactsContract.Intents.Insert.PHONE_TYPE,
            ContactsContract.CommonDataKinds.Phone.TYPE_WORK
        )
    }
}

val intent = addContact()
startActivity(intent)

Modifying Contacts

When modifying, what is different is the content URI. We need to call ContactsContract.Contacts.getLookupUri() and pass in ContactsContract.Contacts._ID and  ContactsContract.ContactsColumns.LOOKUP_KEY to get the URI for the specified Contact. These two parameter ​​can be obtained in the Contacts table instead of the RawContacts table.

fun editContact(account: Account): Intent {
    return Intent(Intent.ACTION_EDIT).apply {
        val contactUri = ContactsContract.Contacts.getLookupUri(account.id, account.lookupKey)
        setDataAndType(contactUri, ContactsContract.Contacts.CONTENT_ITEM_TYPE)

        // Sets the special extended data for navigation
        putExtra("finishActivityOnSaveCompleted", true)

        // Insert an email address
        putExtra(ContactsContract.Intents.Insert.EMAIL, "waynestalk@gmail.com")
        putExtra(
            ContactsContract.Intents.Insert.EMAIL_TYPE,
            ContactsContract.CommonDataKinds.Email.TYPE_WORK
        )

        // Insert a phone number
        putExtra(ContactsContract.Intents.Insert.PHONE, "123456789")
        putExtra(
            ContactsContract.Intents.Insert.PHONE_TYPE,
            ContactsContract.CommonDataKinds.Phone.TYPE_WORK
        )
    }
}

val intent = editContact(account)
startActivity(intent)

Conclusion

Adding and modifying contact data is not so easy, it is recommended to use Intent to add and modify. Fortunately, in most cases, we just need to get the contact details. This article only briefly introduces how to retrieve a contact’s email. It also has many different types of data, which are left for readers to discover them.

References

Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like