Android Contacts Provider 教學

Photo by Ozgur Kara on Unsplash
Photo by Ozgur Kara on Unsplash
Contacts Provider 是一個 Android 內建的 content provider。它管理系統中的聯絡人資料。Android 的聯絡人 app 也是透過 Contacts Provider 存取聯絡人資料。

Contacts Provider 是一個 Android 內建的 content provider。它管理系統中的聯絡人資料。Android 的 Contacts app 也是透過 contacts provider 存取聯絡人資料。本文章將介紹如何利用 contacts provider 存取系統中的聯絡人資料。

完整程式碼可以在 下載。

概覽

Contacts Provider 主要管理三個 tables:

這三個 tables 的階層關係如下圖:

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

當我們在新增一個聯絡人時,實際上就是在 RawContacts table 中新建一筆。然後,這個聯絡人的詳細資料,如 last name、first name、email、電話號碼,每一個資料都會在 Data table 中新建一筆。所以,RawContacts 和 Data tables 是一個一對多的對應關係。

在 Android 中,我們可以新增多個帳號,如一個帳號是 waynestalk@gmail.com,另一個帳號是 hello@gmail.com。因此 RawContacts table 有以下兩個欄位來表明它的來源:

  • ACCOUNT_NAME:帳號名稱,如 waynestalk@gmail.com。
  • ACCOUNT_TYPE:帳號型態,如 Google account 是 com.google。

所以實際上我們在存取聯絡人列表時,大多是存取 RawContacts table。那 Contacts table 是做什麼用的呢?概念上,Contacts table 裡的每一筆資料代表的是一個人。而,同一個人可能會有多筆的聯絡人資料。當使用者在新增一個聯絡人資料到 RawContacts table 時,Contacts Provider 會判斷這個新的聯絡人資料是否可以關聯到 Contacts table 裡的某一個人。如果可以,那 Contacts table 就會將它們關聯起來。如果不行的話,則它會在 Contacts table 裡建立一筆新的資料。所以,Contacts 和 RawContacts tables 是一個一對多的對應關係。

那 ContactsProvider 是根據什麼來判斷新增的聯絡人資料是否可以關聯到 Contacts table 裡的某一個人呢?在官網中有做一些粗略的介紹,但並沒有明確指出規則。另外,這篇文章有做一些分析,或許有幫助你了解。所以實際上,我們只能對 Contacts table 做讀取,而不能對它做新增或修改。另外,在同步所有帳號的聯絡人時,這個關聯的動作也會發生。

取得 Contacts 列表

首先,我們必須要先在 AndroidManifest.xml 中加上 READ_CONSTACTS permission,如下。

<?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>

然後,在 activity 中,請求 READ_CONSTACTS permission。

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

以下的程式碼中,我們用 ContentResolver.query() 從 RawContacts table 取得聯絡人列表。其各個參數說明如下:

  • uri:指定要存取 RowContacts table。
  • projection:指定要存取 RowContacts table 的 _ID 和 DISPLAY_NAME_PRIMARY 欄位。
  • selection:同 SELECT 的 WHERE。
  • selectionArgs:取代 selection 參數中的 ?。
  • sortOrder:同 SELECT 的 ORDER BY。

所以,使用 query() 的方式非常類似於 SQL。比較不同是在 SQL 中,我們是指定 table 的名稱,而在 query() 中,則是指定 content URI。

你可以在 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)
        }
    }
}

取得 Contact 詳情

所有的 contact 詳情都儲存在 ContactsContract.Data 中。每一筆只會儲存一種資料。你可以透過 ContactsContract.DataColumns.MIMETYPE 欄位知道這一筆是什麼資料。下方程式碼中,我們在 selection 參數中,指定我們想要取得的 RawContact 的所有 email 資料。我們可由 ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE 取得 email 的 mime type 字串。

你可以在 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
    }
}

編輯 Contact 詳情

我們也可以修改 contact 詳情。首先,我們要先在 AndroidManifest.xml 中加上 WRITE_CONTACTS permission。

<?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>

然後,在 activity 中,請求 WRITE_CONTACTS permission。

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

新增 Contact 詳情

以下程式碼中,我們對一個 RawContact 新增一筆 email 資料。

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))
            }
        }
    }
}

修改 Contact 詳情

以下程式碼中,我們修改一個 RawContact 的一筆 email 資料。

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))
            }
        }
    }
}

刪除 Contact 詳情

以下程式碼中,我們刪除一個 RawContact 的一筆 email 資料。

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))
            }
        }
    }
}

使用 Intent 新增和修改 Contacts

至目前為止,我們都是直接新增或修改 Data table。但,我們可以看出,其實 Data table 的資料種類相當的多。所以,如果我們要完整地支援所有的資料種類,這並不簡單。另外一種比較推薦的方式是透過 Android 的 Contacts app 來新增和修改 Contacts 資料。只是這樣的話,我們就無法自定 UI 畫面。

新增 Contacts

用 ContactsContract.Intents.Insert.ACTION 新增一個 Intent。然後,在 intent 中設定我們要新增的資料欄位。

你可以在 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)

修改 Contacts

在修改時,比較不一樣的是 content URI。我們要呼叫 ContactsContract.Contacts.getLookupUri() 並傳入 ContactsContract.Contacts._IDContactsContract.ContactsColumns.LOOKUP_KEY 來取得指定 Contact 的 URI。這兩個參數值可以在 Contacts table 中取得,而不是 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)

結語

新增和修改聯絡人資料並不是那麼地容易,比較推薦使用 Intent 來新增和修改。所幸的是,大部分的情況話,我們應該只是需要取得聯絡人資料而已。本文章中只粗略地介紹如何取得聯絡人的 email。它還有很多不同的資料種類,有待讀者自行發掘。

參考

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

You May Also Like
Photo by Hans-Jurgen Mager on Unsplash
Read More

Kotlin Coroutine 教學

Kotlin 的 coroutine 是用來取代 thread。它不會阻塞 thread,而且還可以被取消。Coroutine core 會幫你管理 thread 的數量,讓你不需要自行管理,這也可以避免不小心建立過多的 thread。
Read More