Search code examples
androidkotlinandroid-contacts

Android, Contacts Contract: store and retrieve custom data


In an attempt to learn how androids ContacsContract works, I tried to add a custom entry to a contact. A whole day of reading docs, tutorials and watching youtube videos later, I am still not really any further in achieving this task.

The closest I got is this document https://developer.android.com/reference/kotlin/android/provider/ContactsContract.Data Stating

For example, if you add a data row for "favorite song" to a raw contact owned by a Google account, it will not get synced to the server, because the Google sync adapter does not know how to handle this data kind. Thus new data kinds are typically introduced along with new account types, i.e. new sync adapters.

They wrote what I am trying, but unfortunately did not provide a solution for how to achieve this task. It would be really kind, if someone would provide a simple example for adding a data row for a favorite song to contacts and retrieving it via code.

__

What I got by myself:

A Method for fetching basic contact information:

import android.content.ContentResolver
import android.database.Cursor
import android.provider.ContactsContract

fun fetchContacts(resolver: ContentResolver) : MutableList<ItemContact> {
    var cols = listOf<String>(
        ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
        ContactsContract.CommonDataKinds.Phone.NUMBER,
        ContactsContract.CommonDataKinds.Phone.CONTACT_ID,
        ContactsContract.CommonDataKinds.Phone._ID,
    ).toTypedArray()
    var cursor : Cursor? = resolver.query(
            ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
            cols, null, null,
            ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME
    )
    var contactsList : MutableList<ItemContact> 
            = emptyList<ItemContact>().toMutableList()
    if (cursor != null && cursor.count > 0) {
        while(cursor.moveToNext()){
            contactsList.add(ItemContact(
                name = cursor.getString(0),
                number = cursor.getString(1),
                contact_id = cursor.getString(2),
            ))
        }
    }
    return contactsList
}

And its corresponding data class

data class ItemContact (
    val name: String,
    val number: String,
    val contact_id: String,
)

From my current understanding I need to add a new RawContact, representing my App, to a given Contact and add the favorite song as single data entry, while creating it. This RawContact should be able to connect to the Contact with the retrieved contact_id. And then I need to check, if a RawContact representing my App exists for a Contact, if so, I will be able to retrieve the stored song, else I leave a placeholder text in the UI, that the Song still needs to be choosen. Somehow this involves a custom Mimetype, but I am still not sure, what this is and how to create one.


Solution

  • First, let's recap the way ContactsContract DB is organized:

    1. Contacts table - contains one row per contact, but hardly any information
    2. RawContacts table - can have multiple rows, each assigned to a single contact-ID (from the previous table), contains a logical group of multiple Data rows usually for a single SyncProvider such as Google.
    3. Data table - contains the actual data of a RawContact, each row has a MIMETYPE column which states what kind of data this row is about (phone, email, name, etc.) + 15 data columns to hold the information itself.

    There are also pseudo tables, such as ContactsContract.CommonDataKinds.Phone which you are querying in your code, which basically queries over the Data table but with a specific MIMETYPE value, for example Phone.CONTENT_ITEM_TYPE.

    If you want to implement your own SyncProvider you usually create your own RawContact row, add it to an existing Contact_ID and add Data rows with your new RAW_CONTACT_ID.

    Then the People/Contacts apps on the device, when they want to present data about a certain contact, will get a list of all the RawContacts for it (including yours), and then get all the Data for all those RawContacts.

    If you only want to add a little piece of custom data to an existing contact like "favorite song" to a contact, you don't have to create a new RawContact for that, instead you can create a new Data row and attach it to the existing RawContact with a custom MIMETYPE, but keep in mind your item will not get synced to Google's servers, however it can still be used on the device locally.

    I assume you're looking after the second option, so here's a sample code (not tested):

    // you need to get a RawContact ID of the contact you want to add info to
    fun addFavoriteSong(context: Context, rawContactId: Long) {
        val resolver = context.contentResolver
        val ops = ArrayList<ContentProviderOperation>()
        
        ops.add(ContentProviderOperation.newInsert(Data.CONTENT_URI, true)
                    .withValue(Data.RAW_CONTACT_ID, rawContactId)
                    .withValue(Data.MIMETYPE, "vnd.android.cursor.item/vnd.com.example.favorite_song")
                    .withValue(Data.DATA1, "Paranoid Android")
                    .withValue(Data.DATA2, "Radiohead")
                    .withValue(Data.DATA3, "OK Computer")
                    .build())
    
        try {
            val results = resolver.applyBatch(ContactsContract.AUTHORITY, ops)
            if (results.isEmpty())
                return
        } catch (e: Exception) {
            e.printStackTrace()
        }
    
        Log.i("Songs Added", "success!");
    }
    

    Then to query for that information, along with other info such as name and phone:

    fun fetchContacts(resolver: ContentResolver) : MutableCollection<ItemContact> {
        var cols = arrayOf(
            Data.CONTACT_ID,
            Data.MIMETYPE,
            Data.DISPLAY_NAME,
            Phone.NUMBER,
            Data.DATA1,
            Data.DATA2,
            Data.DATA3,
        )
        
        // get only rows of MIMETYPE phone and your new custom MIMETYPE
        var selection = Data.MIMETYPE + " IN (" + Phone.CONTENT_ITEM_TYPE + ", " + "vnd.android.cursor.item/vnd.com.example.favorite_song" + ")"
    
        var cursor : Cursor? = resolver.query(Data.CONTENT_URI, cols, null, null, Data.CONTACT_ID)
        val map = hashMapOf<Long, ItemContact>()
        
        while(cursor != null && cursor.moveToNext()) {
            val contactId = cursor.getLong(0)
            val mimetype = cursor.getString(1)
    
            // gets the existing ItemContact from the map or if not found, puts an empty one in the map
            val contact = map.getOrPut(contactId) { ItemContact() }
    
            with(contact) {
                if (mimetype == Phone.CONTENT_ITEM_TYPE) {
                    contact_id = contactId
                    name = cursor.getString(2)
                    number = cursor.getString(3)
                } else {
                    contact_id = contactId
                    song = cursor.getString(4)
                    band = cursor.getString(5)
                    album = cursor.getString(6)
                }
            }
        }
        return map.values
    }