Contacts Provider 是一個 Android 內建的 content provider。它管理系統中的聯絡人資料。Android 的 Contacts app 也是透過 contacts provider 存取聯絡人資料。本文章將介紹如何利用 contacts provider 存取系統中的聯絡人資料。
Table of Contents
概覽
Contacts Provider 主要管理三個 tables:
- ContactsContract.Contacts:每一筆代表一個人的總覽,基於多筆 RowContacts 資料。
- ContactsContract.RawContacts:每一筆代表一個聯絡人,基於 user account 和 type。
- ContactsContract.Data:每一筆包含一個聯絡人的一種資料,如 email 地址或電話號碼。
這三個 tables 的階層關係如下圖:

當我們在新增一個聯絡人時,實際上就是在 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._ID 和 ContactsContract.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。它還有很多不同的資料種類,有待讀者自行發掘。
參考
- Contacts Provider, Google Developers.
- Ali Chousein, Android contacts: CONTACT_ID vs RAW_CONTACT_ID.









