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.