Contacts Provider
今年加入了某字幕組,加之雜事頗多,許久未添新文了,慚愧之極。 在聽聞 Google 即將重返中國后,近日忽又發現官方網站正在放出 API 中文版,比如本文。當然不是大家所譯,但至少句子結構較通順,竊以為比 MSDN 中文版好些。雖有些生硬(比如將 Provider 譯為“提供者”,有趣得緊),但好在前無古人,也許 Google 自此便統一了自己的中文術語也未可知。能讓更多的國人精確領悟 Android 的精髓,肯定是好事,希望 Google 繼續堅持。
這事應該喜大普奔啊,怎么沒見報道啊?當年加入的翻譯組成了余黨,現在余黨也已消失了,雖開始便知是遲早的事,仍不免生出許多悲涼。本人以后將不再出這些代庖譯文了。
英文原文:http://developer.android.com/guide/topics/providers/contacts-provider.html
采集日期:2014-5-18
官方譯文:http://developer.android.com/intl/zh-cn/guide/topics/providers/contacts-provider.html
概覽
- Android 提供的聯系人信息數據庫。
- 與 Web 同步。
- 與社交流數據整合。
在本文中
- Contacts Provider 組織架構
- Raw Contact 表
- Data 表
- Contacts 表
- 來自 Sync Adapter 的數據
- 所需權限
- 用戶個人資料
- Contacts Provider 元數據定義
- 訪問 Contacts Provider
- Contacts Provider Sync Adapter
- 社交流數據
- Contacts Provider 的其他功能
關鍵類
ContactsContract.Contacts
ContactsContract.RawContacts
ContactsContract.Data
ContactsContract.StreamItems
相關示例
參閱
Contacts Provider 是一種強大而靈活的 Android 組件,它管理着當前設備中的聯系人信息數據庫。 Contacts Provider 是“聯系人”應用的數據源,其他應用也可以訪問其中的數據,並且可以在當前設備和在線服務之間傳輸數據。 Contacts Provider 可容納多種數據源,並且為了盡可能將人員信息全部管理起來,它的組織架構是比較復雜的。 因此,Contacts Provider 的 API 包含了大量的契約(contract)類和接口,為讀取和修改數據提供便利。
本文主要介紹了:
- Provider 基本架構。
- 如何從 Provider 讀取數據。
- 如何修改 Provider 中的數據。
- 如何編寫一個 Sync Adapter,實現服務器數據與 Contacts Provider 的同步。
本文假定讀者已對 Android 的 Content Provider 有了初步的了解。 有關 Content Provider 的更多信息,請參閱開發指南中的 Content Provider 基礎。 Sync Adapter 范例 中給出了一個應用實例,利用 Sync Adapter 在 Contacts Provider 和托管於 Google Web Service 內的一個示例應用之間進行數據傳輸。
Contacts Provider 組織架構
Contacts Provider 是一個Android Content Provider 組件。 它維護着三種人員信息相關的數據,每種數據對應着 Provider 中的一個表,如圖 1 所示:
圖 1. Contacts Provider 表結構。
這三個表通常通過它們的契約類名稱來引用。 這些類為下列數據表的 Content URI、字段名和字段值進行了對應常量的定義:
-
ContactsContract.Contacts
表 - 每行數據代表一個聯系人,這里已對 Raw Contact 數據記錄做過歸並。
-
ContactsContract.RawContacts
表 - 存放聯系人的摘要信息,根據“賬戶名稱+賬戶類型”來唯一確定人員。
-
ContactsContract.Data
表 - 存放 Raw Contact 的詳細信息,比如 Email 地址、電話號碼等。
ContactsContract
中還有其他一些輔助表,也是由契約類來代表的,都是 Contacts Provider 用來進行操作管理的,或是用於向“聯系人”或“電話”應用提供特定功能的。
RawContacts 表
一個 Raw Contact 代表由賬戶類型和賬戶名唯一確定的人員信息。 因為 Contacts Provider 允許同一聯系人使用多個在線服務作為數據源,所以它允許一個人對應多個 Raw Contact。 正因如此,一個用戶的數據可由相同賬戶類型的多個賬戶歸並生成。
Raw Contact 的大部分數據並不是保存在 ContactsContract.RawContacts
表中。而是存放於 ContactsContract.Data
表的一條或多條記錄中。每行數據都有一個 Data.RAW_CONTACT_ID
字段,存放着上一級 ContactsContract.RawContacts
的記錄 RawContacts._ID
。
RawContacts 的重要字段
表 1 中列出了ContactsContract.RawContacts
中的重要字段。請注意表中的注意事項。
表 1 RawContacts 的重要字段:
字段名稱 | 用途 | 注意事項 |
---|---|---|
ACCOUNT_NAME |
該 Raw Contact 數據來源的賬戶名稱,與賬戶類型呼應。 比如,Google 賬戶的名稱是個 Gmail 地址。詳情請參閱下面的 ACCOUNT_TYPE 。 |
賬戶名稱的格式根據賬戶類型而定。不一定是 Email 地址。 |
ACCOUNT_TYPE |
該 Raw Contact 數據來源的賬戶類型。比如,Google 賬戶的類型是 com.google 。 請務必使用擁有所有權或控制權的域名作為賬戶類型的修飾符,以保證賬戶類型的唯一性。 |
聯系人的賬戶類型通常與某個 Sync Adapter 關聯起來,以便與 Contacts Provider 同步數據。 |
DELETED |
該 Raw Contact 的“刪除”標志。 | 本標志允許 Contacts Provider 在內部暫時保留這條記錄,等到 Sync Adapter 完成服務器上的刪除操作后,再從數據庫中最終刪除這條記錄。 |
注意
以下是有關 ContactsContract.RawContacts
表的重要事項:
- Raw Contact 的姓名部分並未存放在
ContactsContract.RawContacts
的記錄中。而是保存在ContactsContract.Data
表中,類型為ContactsContract.CommonDataKinds.StructuredName
。每個 Raw Contact 在ContactsContract.Data
表中只存在一條該類型的數據。 - 注意:如果要在 Raw Contact 記錄中使用自己的賬戶信息,必須先用
AccountManager
進行注冊。只要讓用戶把賬戶類型和賬戶名稱加入賬戶列表即可。 如果未經注冊,Contacts Provider 將會自動刪除自行加入的 Raw Contact 數據行。比如,假設應用程序需要對一個基於 Web 服務的聯系人信息進行管理, 服務的域名是
com.example.dataservice
, 服務的賬戶類型是becky.sharp@dataservice.example.com
, 那么在添加 Raw Contact 記錄之前, 首先必須讓用戶添加賬戶“類型”(com.example.dataservice
)和賬戶“名稱”(becky.smart@dataservice.example.com
)。 可以在應用程序的文檔中向用戶說明這一要求,也可以在程序中提醒用戶添加類型和名稱。 關於賬戶類型和賬戶名稱的更多細節,將在下一節中介紹。
Raw Contact 的數據來源
為了加深對 Raw Contact 的理解,以下舉例說明,假定用戶“Emily Dickinson”在設備中擁有以下三個賬戶:
emily.dickinson@gmail.com
emilyd@gmail.com
- Twitter 賬號 "belle_of_amherst"
該用戶在系統設置項 賬戶 中對這三個賬號都啟用了 同步聯系人功能。
假設 Emily Dickinson 打開瀏覽器窗口,用 emily.dickinson@gmail.com
登錄 Gmail,並打開聯系人,添加“Thomas Higginson”。 然后,她又用 emilyd@gmail.com
登錄 Gmail 並發送郵件給“Thomas Higginson”,這會自動將其添加為聯系人。 同時,她還在 Twitter 上關注了“colonel_tom”(Thomas Higginson 的 Twitter ID)。
完成上述操作后,Contacts Provider 將會生成以下三條 Raw Contact 記錄:
- 第一條 Raw Contact 記錄對應“Thomas Higginson”,關聯賬戶是
emily.dickinson@gmail.com
。 賬戶類型為 Google。 - 第二條 Raw Contact 記錄對應“Thomas Higginson”,關聯賬戶是
emilyd@gmail.com
。 賬戶類型也是 Google。由於對應的用戶帳戶不同,盡管第二條記錄的名稱與前一條的完全相同,還是成為了第二個 Raw Contact。 - 第三條 Raw Contact 記錄對應“Thomas Higginson” ,關聯賬戶是“belle_of_amherst”。 賬戶類型是 Twitter。
Data 表
如前所述,Raw Contact 的數據存放在 ContactsContract.Data
中,並通過 Raw Contact 記錄的 _ID
關聯。 這樣每個 Raw Contact 對每種數據(如 Email 地址或電話號碼)都可以擁有多個實例。 例如,“Thomas Higginson”的 Raw Contact 記錄為 emilyd@gmail.com
(通過 Google 賬戶 emilyd@gmail.com
關聯), 她的家用 Email 地址是 thigg@gmail.com
,工作 Email 地址是 thomas.higginson@gmail.com
, Contacts Provider 就會存儲兩條 Email 記錄,且均通過 ID 與 Raw Contact 記錄關聯。
請注意,Data 表中存放着多種類型的數據。顯示姓名、電話號碼、Email、郵寄地址、照片和網站詳情等信息都可以在 ContactsContract.Data
表中找到。為了便於管理, ContactsContract.Data
表的有些字段名稱是描述性的,另一些則是通用名稱。 描述性名稱的字段與數據的類型無關,內容的含義與字段名稱相同。 通用名稱字段中的內容,則會根據不同的數據類型而具有不同的含義。
描述性的字段名稱
以下是一些描述性字段名稱的例子:
-
RAW_CONTACT_ID
-
對應 Raw Contact 的
_ID
。 -
MIMETYPE
-
本行數據的類型,以某種 MIME 類型的形式給出。 Contacts Provider 將會使用
ContactsContract.CommonDataKinds
中定義的 MIME 類型。這些 MIME 類型都是公開定義的,可以被應用程序或 Contacts Provider 對應的 Sync Adapter 直接使用。 -
IS_PRIMARY
-
如果 Raw Contact 的某一類聯系信息可能對應多條數據行,
IS_PRIMARY
字段就標識了哪一行數據是該類信息的主數據。 比如,如果用戶長按某聯系人的電話號碼,並選擇 設為默認號碼,那么包含該號碼的ContactsContract.Data
數據行的IS_PRIMARY
字段就會被置為非零值。
通用字段名稱
目前可用的通用字段有15個,名字分別為 DATA1
到 DATA15
。 另外還有4個通用字段是 Sync Adapter 專用的,命名為 SYNC1
到 SYNC4
。 通用字段名稱隨時可用,與數據行的類型無關。
DATA1
字段自帶索引。 Contacts Provider 始終會使用這一字段,將其視為查詢語句中最常用的數據所在。 比如,在 Email 數據行中,本字段就存放了實際的 Email 地址。
按照慣例,DATA15
是保留字段,用於存放二進制大對象(BLOB)數據,比如縮略圖。
固定類型的字段名稱
為了簡化對某些特定類型數據的操作,Contacts Provider 也支持一些固定類型的字段名稱,這些常量在 ContactsContract.CommonDataKinds
的子類中定義。通過這些常量,就可以方便地訪問同名的數據字段。
例如, ContactsContract.CommonDataKinds.Email
類中就為 MIME 類型為 Email.CONTENT_ITEM_TYPE
的 ContactsContract.Data
數據行定義了一些字段。該類中包含了 Email 地址字段 ADDRESS
。 ADDRESS
的值實際就是通用字段名稱“data1”。
注意: 不要套用 Provider 預定義的 MIME 類型在 ContactsContract.Data
表中添加自定義的數據。這可能會導致數據丟失或 Provider 的功能異常。 比如,請勿在 DATA1
字段中添加 MIME 類型為 Email.CONTENT_ITEM_TYPE
的用戶名數據,這里本應是存放 Email 地址的。 如果該行使用的是自定義的 MIME 類型,那就可以定義任意類型的字段名稱,並隨意使用各個字段。
圖 2 演示了描述性字段和 data 字段在 ContactsContract.Data
表中的位置,以及固定類型的字段名“覆蓋”通用字段名的情況。
圖 2 固定類型字段與通用字段名稱
定義了固定類型字段名稱的類
表 2 列出了大部分常用的固定類型字段名稱類:
Table 2. 固定類型字段名稱類
映射類 | 數據類型 | 注意事項 |
---|---|---|
ContactsContract.CommonDataKinds.StructuredName |
本條記錄相關 Raw Contact 的姓名。 | 每個聯系人只能有一條該記錄。 |
ContactsContract.CommonDataKinds.Photo |
本條記錄相關 Raw Contact 的主照片。 | 每個聯系人只能有一條該記錄。 |
ContactsContract.CommonDataKinds.Email |
本條記錄相關 Raw Contact 的 Email 地址。 | 每個聯系人可以有多個 Email 地址。 |
ContactsContract.CommonDataKinds.StructuredPostal |
本條記錄相關 Raw Contact 的郵寄地址。 | 每個聯系人可以有多個郵寄地址。 |
ContactsContract.CommonDataKinds.GroupMembership |
聯系人在 Contacts Provider 中所屬的組。 | 群組是賬戶類型與名稱之外的可選功能。詳情請參閱 聯系人群組。 |
Contacts 表
根據賬戶類型和賬戶名稱,Contacts Provider 將多條 Raw Contact 記錄歸並,成為一個聯系人。 當需要修改某聯系人相關的所有信息時,這會比較方便。 Contacts Provider 負責創建新的 Contacts 記錄,並會把多條 Raw Contact 記錄與已有的 Contacts 記錄關聯。 應用程序和 Sync Adapter 都無權添加 Contacts 記錄,而且其中的某些字段還是只讀的。
注意: 當試圖用 insert()
在 Contacts Provider 中添加聯系人記錄時,會觸發 UnsupportedOperationException
異常。而對“只讀”字段的修改將被會忽略。
當某條新 Raw Contact 記錄與已有的 Contacts 記錄均無法匹配時, Contacts Provider 會創建一條新的 Contacts 記錄。 在 Raw Contact 記錄被修改之后,如果不再匹配之前的 Contacts 記錄了,Contacts Provider 也會創建一條新的 Contacts 記錄。 如果應用程序或 Sync Adapter 新建的 Raw Contact 記錄確實與已有的 Contacts 記錄相匹配,則會建立關聯關系。
Contacts Provider 利用 Contacts
表的 _ID
字段將 Contacts 記錄與 Raw Contact 記錄關聯起來。 ContactsContract.RawContacts
表的 CONTACT_ID
字段中存放了相關 Contacts 記錄的 _ID
值。
Contacts
表中還包含着一個字段 LOOKUP_KEY
,這是每條記錄的“固定”標識。 因為 Contacts 表是由 Contacts Provider 自動維護的,在歸並或同步聯系人數據時, _ID
的值可能會被其修改。但是帶有 LOOKUP_KEY
的 URI CONTENT_LOOKUP_URI
仍然會指向原有的 Contact 記錄,因此可以用 LOOKUP_KEY
對聯系人完成“收藏”等操作。該字段有自己的格式,與 _ID
字段的格式無關。
圖 3 給出了三張表之間的關系。
圖 3. Contacts、Raw Contacts 和 Details 表的關系。
來自 Sync Adapter 的數據
Contacts Provider 中的聯系人信息可以是由用戶錄入的,也可以通過 Sync Adapter 從 Web 服務端插入, 這種服務端和設備之間的數據傳輸是自動完成的。 Sync Adapter 運行於后台,由系統通過 ContentResolver
進行管理。
在 Android 平台中,與 Sync Adapter 合作的 Web 服務端是由賬戶類型標識的。 每個 Sync Adapter 與一種賬戶類型關聯,但可以支持同一種類型下的多個賬戶名稱。 賬戶類型和賬戶名稱已在 Raw Contact 的數據來源 一節中進行過簡要介紹了。下面將介紹更多細節,說明賬戶類型和賬戶名稱是如何與 Sync Adapter 及后台服務相關聯的。
- 賬戶類型
-
標識了一種可供用戶存儲數據的服務。大多數情況下,用戶必須經過認證才能使用這些服務。 例如,Google Contacts 就是一種賬戶類型,並被標記為
google.com
。AccountManager
將利用這個值來識別相應的賬戶類型。 - 賬戶名稱
- 標識了某種賬戶類型的一個賬戶或登錄名。Google Contacts 賬戶就是 Google 賬戶,它是用一個 Email 地址作為賬戶名稱的。 其他服務可能會用一個單詞或數字 ID 作為用戶名。
賬戶類型不一定是唯一的。用戶可以配置多個 Google Contacts 賬戶,並把各自的數據下載到 Contacts Provider 中。 這可能是由於他有一些賬戶是私人使用的,而另一些賬戶則用於工作。 賬戶名稱通常是唯一的。它和賬戶類型合在一起,唯一標識了 Contacts Provider 和外部服務之間的一條數據鏈。
為了能將自有服務的數據傳遞給 Contacts Provider,需要編寫自己的 Sync Adapter。 更多細節將在 Contacts Provider Sync Adapter 一節中介紹。
圖 4 演示了 Contacts Provider 在聯系人數據流中所起的作用。 在標為“sync adapters”的虛線框中,每個適配器(Adapter)都標出了賬戶類型。
圖 4. Contacts Provider 數據流圖。
必要的權限
訪問 Contacts Provider 必須申請以下權限:
- 對數據表的讀取權限
-
READ_CONTACTS
,將AndroidManifest.xml
的<uses-permission>
元素設為<uses-permission android:name="android.permission.READ_CONTACTS">
。 - 對數據表的寫入權限
-
WRITE_CONTACTS
,將AndroidManifest.xml
的<uses-permission>
元素設為<uses-permission android:name="android.permission.WRITE_CONTACTS">
。
上述權限與用戶個人資料(Profile)信息無關。用戶個人資料及權限將在 用戶個人資料 一節中介紹。
請記住,聯系人信息具有私密性和敏感性。 用戶都會比較在意自己的隱私,所以肯定不希望應用程序收集本人或其他聯系人的信息。 如果沒有為訪問聯系人信息給出充分的理由,用戶可能會對該應用給出差評,或者直接拒絕安裝。
用戶個人資料
ContactsContract.Contacts
表中有一條記錄保存着當前用戶的個人資料。 這條數據描述的是設備用戶
,而不是聯系人。 這條 Contacts 數據與一條 Raw Contacts 的記錄關聯,每個帶有用戶個人資料的系統都會有一條。 每條 Raw Contacts 記錄可以對應有多條 Data 表的記錄。在 ContactsContract.Profile
類中給出了所有訪問用戶個人資料時要用到的常量定義。
訪問用戶個人資料需要特定的權限。除了讀寫聯系人時需要的 READ_CONTACTS
和 WRITE_CONTACTS
權限之外,讀寫用戶個人資料還分別需要 READ_PROFILE
和 WRITE_PROFILE
權限。
請記住,用戶個人資料屬於敏感數據。擁有 READ_PROFILE
權限可以訪問到用戶的個人身份信息。請務必在應用程序的描述信息里,說明申請用戶個人資料訪問權限的理由。
通過調用 ContentResolver.query()
方法,可以獲取包含用戶個人資料的 Contacts 記錄。請把這里的 Content URI 置為 CONTENT_URI
,且不需要給出任何查詢條件。 基於該 Content URI ,還可以獲取 Raw Contact 記錄及用戶個人資料。 例如,以下代碼段就實現了用戶個人資料的讀取:
// 設置所要讀取的用戶個人資料字段 mProjection = new String[] { Profile._ID, Profile.DISPLAY_NAME_PRIMARY, Profile.LOOKUP_KEY, Profile.PHOTO_THUMBNAIL_URI }; // 從 Contacts Provider 中讀取用戶個人資料 mProfileCursor = getContentResolver().query( Profile.CONTENT_URI, mProjection , null, null, null);
注意: 如果讀取到多條聯系人記錄,通過檢查 IS_USER_PROFILE
字段,可以確定哪一條是用戶個人資料數據。 如果該字段為 1 ,則表示該條記錄為用戶個人資料。
Contacts Provider 元數據
Contacts Provider 用數據庫管理着聯系人的數據。 這些元數據(Metadata)分別存放於幾張表中,包括 Raw Contacts、Data、Contacts、 ContactsContract.Settings
、 ContactsContract.SyncState
等。每種元數據的作用如下所示:
表 3.Contacts Provider 中的元數據
表名 | 字段 | 值 | 含義 |
---|---|---|---|
ContactsContract.RawContacts |
DIRTY |
“0”表示自前一次同步以來沒有變化 | 標記本機 Raw Contact 記錄已被修改過並需要同步到服務器端去。 當 Android 應用程序做出修改后,Contacts Provider 會自動設置該字段值。 修改 Raw Contact 或 Data 表的 Sync Adapter 應該確保在 Content URI 后面添加 |
“1”表示前一次同步之后發生了變化,需要將數據向服務器端同步。 | |||
ContactsContract.RawContacts |
VERSION |
本行數據的版本號。 | 只要本行數據或關聯記錄發生了變化, Contacts Provider 就會自動遞增該字段值。 |
ContactsContract.Data |
DATA_VERSION |
本行數據的版本號。 | 只要本條 Data 數據發生了變化,Contacts Provider 就會自動遞增該字段值。 |
ContactsContract.RawContacts |
SOURCE_ID |
字符串值,唯一標識了創建該條 Raw Contact 記錄的賬戶。 | 當 Sync Adapter 新建一條 Raw Contact 記錄時,本字段就應被置為服務器端給出的唯一 ID。 而當 Android 應用程序新建一條 Raw Contact 記錄時,應該將本字段保持為空。 這就意味着, Sync Adapter 應該先在服務器端創建一條 Raw Contact 記錄,並為 SOURCE_ID 獲取一個值。 有一點特別重要,每種賬戶類型的 SOURCE_ID 必須唯一,並在同步過程中保持不變:
|
ContactsContract.Groups |
GROUP_VISIBLE |
“0”表示本組聯系人不允許顯示在 Android 應用程序的界面中。 | 本字段是為了與某些服務器端保持兼容,這些服務端支持隱藏某組聯系人的功能。 |
“1”表示本組聯系人可由應用程序顯示。 | |||
ContactsContract.Settings |
UNGROUPED_VISIBLE |
“0”表示:如果不屬於任何組,那么本賬戶和賬戶類型的聯系人將不會顯示在 Android 應用程序界面中。 | 默認情況下,如果聯系人的所有“Raw Contact”均不屬於任何群組( Raw Contact 的分組關系由 ContactsContract.Data 表中的 ContactsContract.CommonDataKinds.GroupMembership 記錄來定義),那么這些聯系人是不可見的。通過設置 ContactsContract.Settings 表中的這個字段,可以強制顯示某賬戶類型及賬戶的未分組聯系人。 本標志的一種用途就是把服務器端未分組的聯系人顯示出來。 |
“1”表示:即使不屬於任何組,本賬戶和賬戶類型的聯系人也可以在 Android 應用程序界面中顯示。 | |||
ContactsContract.SyncState |
(所有字段) | 本表供 Sync Adapter 存放元數據。 | 本表可用於在本地持久保存同步狀態及其他相關數據。 |
訪問 Contacts Provider
本節介紹了 Contacts Provider 的訪問規則,重點包括:
- 實體查詢。
- 批量修改。
- 通過 Intent 讀取和修改數據。
- 數據完整性。
有關由 Sync Adapter 進行數據修改的更多細節,還將在 Contacts Provider Sync Adapter 一節中進行介紹。
查詢實體
因為 Contacts Provider 中的數據表是按照一定的層次結構組織在一起的,所以它非常適用於將一條記錄連同所有關聯“子”記錄一起讀取出來。 比如,為了顯示某人的所有信息,可能要讀取一條 ContactsContract.Contacts
記錄對應的所有 ContactsContract.RawContacts
記錄,或者是一條 ContactsContract.RawContacts
記錄對應的所有 ContactsContract.CommonDataKinds.Email
記錄。為了便於操作,Contacts Provider 提出了 實體(Entity)的概念,它類似於關聯了多張表的數據庫。
一個實體類似於一張表,它由某父表及其子表中的選定字段構成。 在對實體進行查詢時,需要根據實體的字段,給出字段映射關系(Projection)和查詢條件。 返回的結果是一個 Cursor
(游標),其中每個子表的每條記錄都對應着一條記錄。 比如,假設查詢了一個 ContactsContract.Contacts.Entity
,要得到某個聯系人姓名及該姓名下所有 Raw Contact 對應的所有 ContactsContract.CommonDataKinds.Email
記錄,則返回的結果中每一行都對應於一條 ContactsContract.CommonDataKinds.Email
記錄。
實體簡化了查詢操作。利用實體可以一次取回某個聯系人的所有信息,而不需要先查詢父表獲取 ID、再用 ID 查詢子表了。 而且 Contacts Provider 在處理實體查詢時將放入一個事務中來完成,確保了數據的一致性。
注意: 實體通常不會包含父表和子表的全部字段。如果試圖對不在實體字段常量列表中的字段進行操作,將會觸發異常
。
以下代碼段演示了讀取某聯系人的所有 Raw Contact 記錄。 這段代碼屬於一個擁有兩個 Activity “main”、“detail”的應用程序。 Activity “main”將顯示聯系人列表,當用戶選中一個聯系人時,Activity 將其 ID 發送給 Activity “detail”。 Activity “detail” 利用 ContactsContract.Contacts.Entity
顯示選中聯系人的所有 Raw Contact 數據。
這是 Activity “detail” 的部分代碼:
... /* * 在 URI 中添加實體路徑。 * 對於 Contacts Provider 而言, URI 應為 content://com.google.contacts/#/entity (# 代表 ID)。 */ mContactUri = Uri.withAppendedPath( mContactUri, ContactsContract.Contacts.Entity.CONTENT_DIRECTORY); // 初始化 loader 。 getLoaderManager().initLoader( LOADER_ID, // loader ID null, // loader 的參數(這里沒有) this); // Activity 的上下文 context // 新建 ListView 要綁定的 Cursor Adapter mCursorAdapter = new SimpleCursorAdapter( this, // Activity 的 context R.layout.detail_list_item, // 包含 detail widget 的 View 項 mCursor, // 處於后台的游標 mFromColumns, // 游標中的數據字段 mToViews, // 顯示數據用的 View 0); // 標志 // 設置 ListView 的后台 Adapter mRawContactList.setAdapter(mCursorAdapter); ... @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { /* * 設置要讀取的字段。 * RAW_CONTACT_ID 標識了當前數據行所屬的 Raw Contact * DATA1 為 Data 記錄中的第一個數據字段(通常存放最重要的數據)。 * MIMETYPE 指明了 Data 記錄的數據類型。 */ String[] projection = { ContactsContract.Contacts.Entity.RAW_CONTACT_ID, ContactsContract.Contacts.Entity.DATA1, ContactsContract.Contacts.Entity.MIMETYPE }; /* * 根據 Raw Contact 的 ID 對返回游標進行排序, * 以便讓同一個 Raw Contact 的所有 Data 記錄放在一起。 */ String sortOrder = ContactsContract.Contacts.Entity.RAW_CONTACT_ID + " ASC"; /* * 返回一個新的 CursorLoader。 * 調用參數與 ContentResolver.query() 類似,只是 Context 參數不同,那里是所用的 ContentResolver。 */ return new CursorLoader( getApplicationContext(), // Activity 的 Context mContactUri, // 聯系人的實體 Content URI projection, // 要返回的字段 null, // 讀取所有 Raw Contact 記錄及相關 Data 記錄 null, // sortOrder); // 按照 Raw Contact ID 排序 }
載入完成后, LoaderManager
將會調用 onLoadFinished()
。該方法的傳入參數中有一個帶有查詢結果的 Cursor
。然后應用程序就可以從該 Cursor
中讀取數據,用於顯示或其他用途。
批量修改
應該盡可能地以“批處理方式”(Batch)進行 Contacts Provider 的增刪改操作。 這通過創建由 ContentProviderOperation
組成的 ArrayList
,然后調用 applyBatch()
即可實現。因為 Contacts Provider 會把一個 applyBatch()
中的所有操作放入一個事務中完成,所以不會發生數據不一致的情況。 批量修改也讓一次完成 Raw Contact 和明細記錄的操作變得更為簡單。
注意: 如果要修改單條 Raw Contact 記錄,可以考慮向系統自帶的聯系人應用發送 Intent, 而不用在自己的程序中來完成。 詳情請參閱通過 Intent 讀寫數據。
Yield Point
如果批量修改所包含的操作很多,就會阻塞其他的進程,這樣用戶體驗就會很糟糕。 這時就需要把所有操作盡可能拆分為多個獨立的列表,並防止系統的阻塞,這可以通過設置Yield Point來實現。 Yield Point 是一種 ContentProviderOperation
對象,它的 isYieldAllowed()
設為 true
。 當 Contacts Provider 處理到 Yield Point 時,將會暫停操作讓其他進程運行,並關閉當前事務。 當再次啟動 Contacts Provider 時,它會繼續 ArrayList
中的操作,並啟動一個新事務。
Yield Point 使得每次 applyBatch()
調用時會建立多個事務。因此,應該把插入 Raw Contact 記錄和相關 Data 記錄放在一起,再設置一個 Yield Point。 或是把與一個聯系人相關的操作組合在一起,再設置一個 Yield Point。
Yield Point 也是一種原子操作單位。兩個 Yield Point 之間的操作要么全部成功,要么全部失敗。 如果沒有設置任何 Yield Point,則最小的原子操作單位就是整個批量任務。 通過 Yield Point 的使用,確實可以防止系統性能的下降,同時也把全部操作拆分為幾個原子操作組。
修改記錄時的向前引用(Back Reference)
在用一組 ContentProviderOperation
插入 Raw Contact 及相關 Data 記錄時, 必須把多條 Data 記錄與該 Raw Contact 進行關聯,這通過把 RAW_CONTACT_ID
字段值設為該 Raw Contact 的 _ID
即可。但是,在創建插入 Data 記錄的 ContentProviderOperation
時,該 ID 值還未就緒,因為這時候插入 Raw Contact 記錄的 ContentProviderOperation
還沒有提交呢。 為了解決這一問題,可以通過 ContentProviderOperation.Builder
類的 withValueBackReference()
方法。該方法允許利用前一次操作的結果插入或修改字段。
withValueBackReference()
方法的參數有兩個:
-
key
- 鍵-值對中的鍵(key)。本參數值應為要修改的字段名。
-
previousResult
-
數組索引,從0開始,該數組由
ContentProviderResult
對象組成,是由applyBatch()
生成的。 當執行批量操作時,每步操作的結果都被保存在一個中間結果數組中。previousResult
即為這些中間結果的索引,可通過key
進行讀寫。 這樣就可以先插入一條 Raw Contact 記錄並得到其_ID
值,在后續插入ContactsContract.Data
記錄時就可以“向前引用”(Back Reference)該值。中間結果數組是在第一次調用
applyBatch()
時創建的,數組大小等於由所需ContentProviderOperation
組成的ArrayList
大小。不過,該結果數組的所有元素都預置為null
,如果試圖向前引用一個不存在的終結結果,withValueBackReference()
將會拋出Exception
。
以下代碼段演示了批量插入 Raw Contact 及 Data 記錄的過程。 其中包含了建立 Yield Point 及使用向前引用的代碼。這段代碼是 ContactAdder
類的 createContacEntry()
方法的升級版,該類屬於 Contact Manager
例程的一部分。
第一段代碼將從界面中讀取聯系人信息。用戶這時應該已經選擇了要添加 Raw Contact 記錄的賬戶。
// 根據用戶界面中的信息,在當前選中賬戶中創建聯系人入口 protected void createContactEntry() { /* * 讀取界面中的數據 */ String name = mContactNameEditText.getText().toString(); String phone = mContactPhoneEditText.getText().toString(); String email = mContactEmailEditText.getText().toString(); int phoneType = mContactPhoneTypes.get( mContactPhoneTypeSpinner.getSelectedItemPosition()); int emailType = mContactEmailTypes.get( mContactEmailTypeSpinner.getSelectedItemPosition());
以下代碼創建了一個 Operation 對象,其在 ContactsContract.RawContacts
表中插入一條 Raw Contact 記錄。
/* * 准備插入 Raw Contact 記錄的批量操作。 * 即便 Contacts Provider 中不存在此聯系人的任何數據,也不允許直接添加 Contact 記錄, * 而只能添加一條 Raw Contact 記錄。 * Contacts Provider 會隨后自動生成一條 Contact 。 */ // 新建一個由 ContentProviderOperation 對象組成的隊列 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); /* * 創建指定賬戶類型(服務器類型)和賬戶名稱(用戶名)的 Raw Contact 記錄。 * 請注意,賬戶的顯示名稱並不保存在此記錄中,而是存於 StructuredName 記錄中。 * 其他數據可以不填。 */ ContentProviderOperation.Builder op = ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI) .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, mSelectedAccount.getType()) .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, mSelectedAccount.getName()); // 創建操作並加入到隊列中 ops.add(op.build());
接下來,創建顯示名稱、電話號碼和 Email 記錄。
每個 Builder 對象都通過 withValueBackReference()
獲得 RAW_CONTACT_ID
。 這個引用(Reference)指向第一步操作的結果對象 ContentProviderResult
,而第一步操作中添加了 Raw Contact 並返回新生成記錄的 _ID
。 這樣,每條記錄都通過自己的 RAW_CONTACT_ID
字段與所屬的新增 ContactsContract.RawContacts
記錄關聯起來。
ContentProviderOperation.Builder
添加 Email 記錄, withYieldAllowed()
標記表示設置一個事務提交點(yield point)。
// 為新 Raw Contact 記錄創建顯示名稱,即一條 StructuredName 記錄。 op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) /* * withValueBackReference 將第一個參數置為 ContentProviderResult 值, * ContentProviderResult 的索引值由第二個參數給出。 * 在本例中,StructuredName 的 Raw Contact ID 列設為第一步操作返回的結果值, * 這步操作也就是實際添加 Raw Contact 記錄的操作。 */ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) // 把本條記錄的 MIME 類型置為 StructuredName .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) // 將本條記錄的顯示名稱設置為用戶界面中顯示的名字 .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name); // 生成操作對象並添加到隊列中 ops.add(op.build()); // 插入電話號碼,記錄類型設置為 Phone 類型 op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) /* * 把 Raw Contact ID 字段置為第一步操作返回的 Raw Contact ID */ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) // 把 MIME 類型設為 Phone .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE) // 設置電話號碼和類型 .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, phone) .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, phoneType); // 生成操作對象並添加到隊列中 ops.add(op.build()); // 插入 Email 數據,類型設為 Email op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) /* * 把 Raw Contact ID 字段置為第一步操作返回的 Raw Contact ID */ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) // 把 MIME 類型設為 Email .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE) // 設置 Email 值和類型 .withValue(ContactsContract.CommonDataKinds.Email.ADDRESS, email) .withValue(ContactsContract.CommonDataKinds.Email.TYPE, emailType); /* * 演示事務提交點(yield point)。 * 這表示在本次插入操作完成后,批量操作線程將優先於其他線程執行。 * 每對一個聯系人完成一組操作后,請設置一個提交點,以避免(維持長事務帶來的)性能下降。 */ op.withYieldAllowed(true); // 生成操作並加入操作隊列 ops.add(op.build());
最后一段代碼演示了 applyBatch()
的調用,以便插入新 Raw Contact 及數據。
// 請求 Contacts Provider 新建一個聯系人 Log.d(TAG,"Selected account: " + mSelectedAccount.getName() + " (" + mSelectedAccount.getType() + ")"); Log.d(TAG,"Creating contact: " + name); /* * 批量提交 ContentProviderOperation 隊列。 * 忽略返回結果。 */ try { getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops); } catch (Exception e) { // 顯示警告 Context ctx = getApplicationContext(); CharSequence txt = getString(R.string.contactCreationFailure); int duration = Toast.LENGTH_SHORT; Toast toast = Toast.makeText(ctx, txt, duration); toast.show(); // 將異常記入日志 Log.e(TAG, "Exception encountered while inserting contact: " + e); } }
批量操作可以實現樂觀並發控制(optimistic concurrency control), 這種方式可以不必鎖定底層數據即可實現修改事務。 為了使用這種方式,在提交事務后,需要檢查可能同時發生的其他修改操作。 如果發現了修改沖突,需要回滾並重新提交。
樂觀並發控制在移動設備上非常有用,因為同時只會有一個用戶,同時訪問一塊數據的可能性很小。 因為沒有用到鎖定機制,就不需要浪費時間加鎖,也不需要等待其他事務解鎖了。
如果要在更新某條 ContactsContract.RawContacts
數據行時使用樂觀並發控制,請按以下步驟執行:
- 讀取數據時,同時取回 Raw Contact 的
VERSION
字段。 - 根據不同的強制約束條件,用
newAssertQuery(Uri)
方法創建一個合適的ContentProviderOperation.Builder
對象。對於 Content URI 而言,使用RawContacts.CONTENT_URI
,並附帶 Raw Contact ID 即可。 - 調用
ContentProviderOperation.Builder
對象的withValue()
方法,把VERSION
字段與前面取回的版本號進行對比。 - 再調用此
ContentProviderOperation.Builder
對象的withExpectedCount()
方法,確保本次比較只涉及一條記錄。 - 調用
build()
方法創建ContentProviderOperation
對象,並把它作為第一個成員加入列表ArrayList
中,這個列表是要傳給applyBatch()
的。 - 提交批處理事務。
如果在讀寫某 Raw Contact 記錄期間,其他操作也在更新此記錄,“斷言”(assert) ContentProviderOperation
將會失敗,全部批量操作都會撤銷。 后面可以再次提交或者執行其他操作。
以下代碼演示了,在用 CursorLoader
查詢到一條 Raw Contact 記錄后, 如何創建 ContentProviderOperation
“斷言”:
/* * 應用程序通過 CursorLoader 查詢 Raw Contacts 表。 * 系統將會在加載完成后調用此方法。 */ public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { // 讀取 Raw Contact _ID 和 VERSION 值 mRawContactID = cursor.getLong(cursor.getColumnIndex(BaseColumns._ID)); mVersion = cursor.getInt(cursor.getColumnIndex(SyncColumns.VERSION)); } ... // 為斷言操作建立 Uri Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, mRawContactID); // 創建斷言操作 ContentProviderOperation.Builder assertOp = ContentProviderOperation.netAssertQuery(rawContactUri); // 添加斷言:檢查版本號、涉及的記錄數 assertOp.withValue(SyncColumns.VERSION, mVersion); assertOp.withExpectedCount(1); // 創建 ArrayList 保存 ContentProviderOperation 對象 ArrayList ops = new ArrayList<ContentProviderOperationg>; ops.add(assertOp.build()); // 可往 ops 中添加其他批量操作 ... // 提交批處理操作。如果斷言失敗,將會拋出異常 try { ContentProviderResult[] results = getContentResolver().applyBatch(AUTHORITY, ops); } catch (OperationApplicationException e) { // 在這里完成斷言失敗時要執行的動作 }
通過 Intent 讀寫數據
通過向系統的 Contacts 應用發送 Intent,可以直接訪問 Contacts Provider. 該類 Intent 將打開 Contacts 應用的界面,用戶可以在里面進行一些聯系人信息的操作。 這種方式可以讓用戶進行:
- 從列表中選取聯系人並向調用方應用返回相關數據
- 編輯已有聯系人信息
- 在用戶賬號下新建 Raw Contact 記錄
- 刪除聯系人或相關數據
如果用戶需要插入或修改數據,可以先記錄這些信息並作為 Intent 的一部分發送出去。
在用 Intent 通過系統 Contacts 應用訪問 Contacts Provider 時, 不需要設計用戶界面,不需要編寫數據訪問代碼,也不必申請 Provider 讀寫權限。 Contacts 系統應用可以授予讀取聯系人的權限, 因為是通過其他應用程序修改 Provider 數據的,所以也不需要擁有寫入權限。
發送 Intent 來訪問 Provider 的通用步驟在 Content Provider 基礎 的“通過 Intent 訪問數據”一節中已有詳細介紹。 相關操作可用的 Action 的 MIME 類型,以及數據類型都在表4中列出, putExtra()
可用的附件數據都在參考文檔 ContactsContract.Intents.Insert
中給出。
表 4. Contacts Provider Intent
操作 | Action | 數據 | MIME 類型 | 備注 |
---|---|---|---|---|
選取聯系人 | ACTION_PICK |
|
不需要 | 根據給出的 Content URI 類型,顯示 Raw Contact 列表或某個 Raw Contact 的數據列表。 調用 |
插入新 Raw Contact 記錄 | Insert.ACTION |
N/A | RawContacts.CONTENT_TYPE ,表示一組 Raw Contact 。 |
顯示系統“聯系人”應用的新建聯系人窗口。 加入 Intent 中的附件數據將一起顯示出來。 如果是用 startActivityForResult() 發送的,新 Raw Contact 記錄的 Content URI 將會傳回給 Activity 的 onActivityResult() 方法,在 Intent 參數的“data”部分中。調用 getData() 即可讀取。 |
編輯聯系人 | ACTION_EDIT |
聯系人的 CONTENT_LOOKUP_URI 。用戶可以在編輯器窗口中修改聯系人相關的數據。 |
Contacts.CONTENT_ITEM_TYPE ,表示一個聯系人。 |
顯示 Contacts 應用中的“修改聯系人”窗口。加入 Intent 中的附件數據將會一並顯示出來。 用戶點擊保存按鈕保存數據時,調用者的 Activity 將會回到前台。 |
顯示可添加數據的選擇列表 | ACTION_INSERT_OR_EDIT |
N/A | CONTENT_ITEM_TYPE |
這個 Intent 總是顯示 Contacts 應用的選擇界面。 用戶可以選中某個聯系人進行編輯,也可以添加新的聯系人。 到底是顯示編輯還是添加界面,取決於用戶的選擇,以及用 Intent 附件顯示的信息。 如果調用方顯示的是聯系人的相關數據,比如 Email 或電話號碼,則可利用此 Intent 讓用戶為已有聯系人添加數據。 注意:在此類 Intent 附件中不需要發送姓名, 因為用戶要么是從已有姓名中選取一個,要么就是添加新用戶。 而且,假如發送了姓名且用戶選擇了編輯聯系人,則 Contacts 應用會顯示發送過去的姓名,之前的姓名會被覆蓋。 如果用戶沒注意到這一點,又進行了保存,則以前的姓名就丟失了。 |
系統“聯系人”應用不允許通過 Intent 刪除 Raw Contact 及相關數據。 要想刪除 Raw Contact 記錄,請使用 ContentResolver.delete()
或ContentProviderOperation.newDelete()
。
以下代碼演示了如何建立並發送一個插入 Raw Contact 及數據的 Intent:
// 讀取用戶界面中的數據 String name = mContactNameEditText.getText().toString(); String phone = mContactPhoneEditText.getText().toString(); String email = mContactEmailEditText.getText().toString(); String company = mCompanyName.getText().toString(); String jobtitle = mJobTitle.getText().toString(); // 新建一個發送給系統“聯系人”應用的 Intent Intent insertIntent = new Intent(ContactsContract.Intents.Insert.ACTION); // 把 MIME 類型置為所需的插入記錄 Activity insertIntent.setType(ContactsContract.RawContacts.CONTENT_TYPE); // 設置聯系人姓名 insertIntent.putExtra(ContactsContract.Intents.Insert.NAME, name); // 設置公司名稱和職位 insertIntent.putExtra(ContactsContract.Intents.Insert.COMPANY, company); insertIntent.putExtra(ContactsContract.Intents.Insert.JOB_TITLE, jobtitle); /* * 以列表方式添加各數據行,相互以 DATA 鍵關聯 */ // 定義 ContentValues 對象列表,每個對象對應一行數據 ArrayList<ContentValues> contactData = new ArrayList<ContentValues>(); /* * 定義 Raw Contact 行 */ // 新建 ContentValues 對象作為行數據 ContentValues rawContactRow = new ContentValues(); // 加入賬戶類型和名稱 rawContactRow.put(ContactsContract.RawContacts.ACCOUNT_TYPE, mSelectedAccount.getType()); rawContactRow.put(ContactsContract.RawContacts.ACCOUNT_NAME, mSelectedAccount.getName()); // 把此行添加到列表中 contactData.add(rawContactRow); /* * 建立電話號碼數據行 */ // 新建 ContentValues 對象作為行數據 ContentValues phoneRow = new ContentValues(); // 設定 MIME 類型(所有數據行都必須給定類型) phoneRow.put( ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE ); // 加入電話號碼及其類型數據 phoneRow.put(ContactsContract.CommonDataKinds.Phone.NUMBER, phone); // 把此行添加到列表中 contactData.add(phoneRow); /* * 建立 Email 數據行 */ // 新建 ContentValues 對象作為行數據 ContentValues emailRow = new ContentValues(); // 設定 MIME 類型(所有數據行都必須給定類型) emailRow.put( ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE ); // 加入 Email 及其類型數據 emailRow.put(ContactsContract.CommonDataKinds.Email.ADDRESS, email); // 把此行添加到列表中 contactData.add(emailRow); /* * 把上述列表添加到 Intent 的附件中。 * 這個列表必須是可序列化的(parcelable),以便能在進程間傳遞。 * 系統應用 Ccontacts 需要把 Intents.Insert.DATA 內容作為鍵值使用。 */ insertIntent.putParcelableArrayListExtra(ContactsContract.Intents.Insert.DATA, contactData); // 發送 Intent,啟動系統“聯系人”應用並打開新建聯系人 Activity。 startActivity(insertIntent);
數據完整性
因為聯系人數據比較重要和敏感,用戶希望能及時修正,所以 Contacts Provider 規定了一些保證數據完整性的規則。 在修改聯系人信息時,必須遵守這些規則。 下面給出一些比較重要的規則:
-
為每條新建
ContactsContract.RawContacts
記錄都添加一條ContactsContract.CommonDataKinds.StructuredName
記錄。 -
在歸並聯系人數據時,如果
ContactsContract.RawContacts
數據行沒有在ContactsContract.Data
表中存在對應的ContactsContract.CommonDataKinds.StructuredName
記錄,可能會引發問題。 -
一定要把
ContactsContract.Data
鏈接到上級ContactsContract.RawContacts
記錄中。 -
沒有鏈接到
ContactsContract.RawContacts
記錄的ContactsContract.Data
數據行不會在系統“聯系人”應用中顯示,並可能會在使用 Sync Adapter 時引發問題。 - 只修改自有 Raw Contact 的數據。
- 請記住,Contacts Provider 通常管理着很多不同賬戶類型/在線服務的數據。 必須確保只對自己的數據行進行修改或刪除操作,且只用自己的賬戶類型和戶名插入數據。
-
在指定 Authority、Content URI、URI Path、字段名、MIME 類型及
TYPE
時,確保使用ContactsContract
及其子類中定義的常量, - 使用這些常量有助於避免出錯。 還可以在常量過時后收到編譯器的警告信息。
自定義數據
通過創建並應用自定義的 MIME 類型,可以在 ContactsContract.Data
表中插入、編輯、刪除和讀取自定義數據。 雖然可以把自定義類型的字段名映射為默認字段名稱,自定義數據行只能使用 ContactsContract.DataColumns
中給定的字段。在系統“聯系人”應用中,自定義數據可以正常顯示,但無法編輯和刪除,用戶也不能加入其他信息。 如果要讓用戶能修改自定義數據行,必須自行提供編輯 Activity。
為了顯示自定義數據,需要提供一個 contacts.xml
文件,里面包含一個 <ContactsAccountType>
元素,其內部包含一個以上的 <ContactsDataKind>
子元素。 詳情請參閱 <ContactsDataKind>
元素 一節。
關於自定義 MIME 類型的更多細節,請參閱 創建 Content Provider。
Contacts Provider Sync Adapter
為了完成設備和在線服務之間的聯系人數據同步(synchronization),Contacts Provider 經過了特別設計。 這樣,用戶就可以把已有數據下載到新設備上,還可以把數據上傳到新建賬戶中去。 同步可以確保用戶手頭的設備擁有最新的數據,不管是由於添加還是修改引起的。 同步的另一個好處就是,在網絡斷開時設備也能使用聯系人信息。
雖然實現同步的方式可以有很多種,但 Android 系統提供了一種插件式(plug-in)的同步框架,可以自動完成以下工作:
- 檢查網絡是否可用;
- 根據用戶設置安排同步計划;
- 重啟已停止的同步。
使用這一框架時,需要給出 Sync Adapter 插件。 每個 Aync Adapter 都唯一對應一個服務和一個 Content Provider,但可以處理同一個服務里的多個賬戶名。 此框架還允許同一套服務和 Provider 使用多個 Sync Adapter。
Sync Adapter 類和文件
Sync Adapter 都實現為 AbstractThreadedSyncAdapter
的一個子類,並要作為 Android 應用的一部分進行安裝。 系統會從 Manifest 文件中獲取 Sync Adapter 的信息,並讀取由 Manifest 文件給出的 XML 文件。 這個 XML 文件定義了在線服務的賬戶類型,以及 Content Provider 的用戶認證信息,這些都是該 Adapter 的唯一標識。 Sync Adapter 一開始並不會運行,只有當用戶添加 Sync Adapter 中賬戶類型的賬戶,並開啟其對應 Content Provider 的同步時,它才會被激活。 這時,系統會負責管理 Adapter,適時調用它完成 Content Provider 和服務器之間的同步。
注意: 把賬戶類型作為 Sync Adapter 唯一標識的一部分,可以讓系統對它們進行分組,把訪問同一公司服務的 Sync Adapter 放在一起。 比如,Google 在線服務的 Sync Adapter 都具有相同的賬戶類型 com.google
。 當用戶在設備上添加 Google 賬戶時,所有已安裝的 Google 服務 Sync Adapter 都會顯示在一起; 每個 Sync Adapter 列出各自關聯的本地 Content Provider。
因為絕大多數服務都需要在訪問數據之前驗證用戶身份,Android 系統提供了一種與 Adapter Adapter 框架類似的,並與其協同工作的用戶認證框架。 這種認證框架使用了插件式的 Authenticator,它是 AbstractAccountAuthenticator
的一個子類。Authenticator 驗證用戶身份的步驟如下:
- 記下用戶名、密碼或類似信息(用戶證書);
- 向服務發送用戶認證信息;
- 檢查服務返回結果。
如果服務接受了用戶信息,Authenticator 可以保存這些信息以備將來使用。 因為認證框架是插件式的, AccountManager
能夠訪問所有可支持的令牌(authtoken),並選擇其公開性,比如 OAuth2 令牌。
雖然用戶認證過程不是必需環節,但大部分與“聯系人”相關的服務都會要求使用。 當然,這不一定非要用 Android 認證框架來完成。
實現 Sync Adapter
為了給 Contacts Provider 編寫一個 Sync Adapter,請先創建一個包含以下部分的 Android 應用程序:
-
Service
組件,響應系統發出的綁定 Sync Adapter 的請求。 -
系統開始同步時,會調用服務的
onBind()
方法來獲取一個 Sync Adapter 使用的IBinder
。這使得系統可以跨進程調用 Adapter 的方法。在Sync Adapter 范例 中,服務的類名為
com.example.android.samplesync.syncadapter.SyncService
。 -
Sync Adapter 實體,實現為
AbstractThreadedSyncAdapter
的實體類。 -
此類完成從服務器下載數據、上傳本機數據、協調沖突等工作。 Adapter 的主要工作在
onPerformSync()
方法中。此類必須實現為單實例。在 Sync Adapter 范例 中,Sync Adapter 定義在
com.example.android.samplesync.syncadapter.SyncAdapter
中。 -
Application
的子類。 -
此類當作 Sync Adapter 單實例的工廠類來使用。通過
onCreate()
方法實例化 Sync Adapter,並提供靜態方法“getter”用於向onBind()
方法返回該單實例。 service. -
可選: 響應系統發起的用戶認證請求的
Service
組件。 -
AccountManager
啟動此服務開始認證過程。該服務的onCreate()
方法實例化一個 Authenticator 對象。 當系統需要認證 Sync Adapter 所用的用戶賬戶時,將會調用其onBind()
方法來獲得一個IBinder
。 這樣系統就能跨進程調用 Authenticator 的方法了。在 Sync Adapter 范例 中,服務的類名為
com.example.android.samplesync.authenticator.AuthenticationService
。 -
可選:
AbstractAccountAuthenticator
的實體子類,用於處理認證請求。 -
此類提供由
AccountManager
調用的方法,用來與服務器端進行身份認證。 由於服務器端采用的技術不同,各種認證過程的細節差別很大。 關於用戶認證的更多內容,請參考服務器端所用軟件的文檔。在Sync Adapter范例 中,Authenticator 在
com.example.android.samplesync.authenticator.Authenticator
類中定義。 - 定義 Sync Adapter 和 Authenticator 用到的 XML 文件,用於向系統進行聲明。
-
前面介紹的 Sync Adapter 和 Authenticator 服務組件定義於 Manifest 文件的
<service>
元素中。這些元素包含了以下一些<meta-data>
子元素,用於向系統報告相應的數據信息:
社交流數據
ContactsContract.StreamItems
和 ContactsContract.StreamItemPhotos
表管理着來自社交網絡的數據。 可以編寫一個 Sync Adapter,把來自個人社交網絡圈的數據添加到這兩張表中去,或者從表中讀取社交數據並顯示出來。 通過這種方式,可以把自己的社交網絡后台服務和前台應用,與 Android 的社交網絡用戶體驗集成在一起。
社交流文字
社交流數據項必須與某個 Raw Contact 關聯。 RAW_CONTACT_ID
即為 Raw Contact 的 _ID
值。 Raw Contact 的賬戶類型和賬戶名稱也會保存在社交流數據記錄中。
社交流數據保存在以下字段中:
-
ACCOUNT_TYPE
- 必填項。 本條社交流數據的 Raw Contact 賬戶類型。在插入數據時必須設置。
-
ACCOUNT_NAME
- 必填項。 本條社交流數據的 Raw Contact 賬戶名稱。在插入數據時必須設置。
- ID 字段
-
必填項。 插入社交流數據時,必須插入以下 ID 字段:
CONTACT_ID
:本條數據關聯的聯系人_ID
。CONTACT_LOOKUP_KEY
:本條數據關聯的聯系人LOOKUP_KEY
。RAW_CONTACT_ID
:本條數據關聯的 Raw Contact_ID
。
-
COMMENTS
- 可選項。保存概要信息,前綴於本條社交流數據顯示。
-
TEXT
-
社交流數據項的標題,可以是由數據源發送過來的,也可以是說明如何生成本條數據的信息。 該字段可以包含任意格式的數據,可嵌入能被
fromHtml()
解析的圖片資源。Provider 可能會截斷或略去超長的文本,但會盡量避免在語言標記(tag)中間截斷。 -
TIMESTAMP
- 文本字符串,表示本條社交流數據的插入或修改時間,單位是 Epoch 紀元(譯者注:1970-01-01 00:00:00 UTC)以來的 毫秒數。 應用程序插入或修改本條數據時,Contacts Provider 會自動更新該字段。
為了能醒目地顯示社交流數據,會用到 RES_ICON
、 RES_LABEL
和 RES_PACKAGE
,這些都代表着應用程序的資源。
ContactsContract.StreamItems
表還包含 SYNC1
至 SYNC4
字段,用於 Sync Adapter 間的互斥同步。
社交流圖片
ContactsContract.StreamItemPhotos
表存放着社交流數據項關聯的圖片信息。並通過 STREAM_ITEM_ID
字段與 ContactsContract.StreamItems
表的 _ID
字段關聯。圖片的引用方式保存在以下字段中:
-
PHOTO
字段(BLOB類型)。 -
圖片的二進制數據,Provider 對其進行了縮放以便保存和顯示。 此字段是為了保證向后兼容性才保留的,以前的 Contacts Provider 會使用這個字段保存圖片。 但是在現在的版本中,不應再使用這個字段保存圖片了。 而應使用
PHOTO_FILE_ID
或PHOTO_URI
(都在下一節介紹)保存到文件中。 目前該字段用於存放圖片的縮略圖,以供讀取。 -
PHOTO_FILE_ID
-
Raw Contact 相關圖片的數字型 ID。 把此字段值附加在
DisplayPhoto.CONTENT_URI
常量之后,即為指向某個圖片文件的 Content URI,然后調用openAssetFileDescriptor()
即可獲得圖片文件的句柄。 -
PHOTO_URI
-
直接指向本條數據對應圖片文件的 Content URI。 用此 URI 調用
openAssetFileDescriptor()
可以獲得圖片文件的句柄。
社交流數據表的使用
上述表的使用方式與 Contacts Provider 中的其他主表基本相同,以下幾點除外:
- 這些表需要額外的訪問權限。 讀取時需要
READ_SOCIAL_STREAM
權限。修改時需要WRITE_SOCIAL_STREAM
權限。 - 對於每個 Raw Contact,
ContactsContract.StreamItems
表中對應的記錄數是有限制的。 如果到達上限,Contacts Provider 會自動刪除TIMESTAMP
最早的記錄,以便為新進的社交流數據騰出空間。 用 Content URI 為CONTENT_LIMIT_URI
進行數據庫查詢,可以獲取記錄數上限值,其他參數都置為null
即可。該查詢會返回包含一條記錄的 Cursor,且只有一個字段MAX_ITEMS
。
ContactsContract.StreamItems.StreamItemPhotos
類定義了 ContactsContract.StreamItemPhotos
的子表,里面存放着某條社交流數據相關的圖片數據記錄。
社交流交互
Contacts Provider 管理的社交流數據,連同系統“聯系人”應用一起, 可以將社交網絡系統與現有的聯系人連接起來,得以實現以下強大功能:
- 通過 Sync Adapter 將社交網絡服務與 Contacts Provider 同步數據, 可以讀取聯系人最近使用過的 Activity 並保存到
ContactsContract.StreamItems
和ContactsContract.StreamItemPhotos
表中,以備后用。 - 除了常規的同步之外,還可以在用戶點選並查看某個聯系人時,觸發自有 Sync Adapter 讀取附加信息。 可以讓 Sync Adapter 讀取高分辨率頭像圖片,以及此人最新的社交流數據。
- 通過在系統“聯系人”應用和 Contacts Provider 中注冊通知(notification), 可以在聯系人被查看時,或是后台服務在修改聯系人信息時,收到一個 Intent。 與用 Sync Adapter 進行完全同步相比,這種方式可能更為快捷,占用的帶寬也更小。
- 當用戶在系統“聯系人”應用中瀏覽時,可以把某個聯系人添加到自建社交網絡服務中去。 利用“邀請聯系人”功能即可完成,這里需要一個添加已有聯系人的 Activity 和一個 XML 文件, 該 XML 文件給出了系統“聯系人”應用和 Contacts Provider,以及自建應用的詳細信息。
Contacts Provider 與社交流數據的定期同步功能與其他同步是一樣的。 關於同步的更多細節,請參閱 Contacts Provider Sync Adapter。 下面兩節將介紹如何注冊通知和邀請聯系人。
注冊並處理社交網絡數據查看請求
當用戶查看由 Sync Adapter 管理的聯系人時,為了能接收到通知,需要注冊 Sync Adapter。步驟如下:
- 在項目的
res/xml/
目錄中,創建名為contacts.xml
的文件. 如果該文件已存在,可以跳過此步。 - 在此文件中加入
<ContactsAccountType xmlns:android="http://schemas.android.com/apk/res/android">
元素。如果此元素已存在,則跳過此步。 - 如果需要注冊服務,以便在用戶打開系統“聯系人”應用中的聯系人詳情頁時,能收到通知, 請在上述 XML 元素中添加
viewContactNotifyService="serviceclass"
屬性,serviceclass
是服務類的完全限定格式名稱,該服務用於接收來自系統“聯系人”應用的 Intent。 對於通知(notifier)服務,可以使用IntentService
的子類,以便服務接收到 Intent。 接收到的 Intent 中包含了用戶所選 Raw Contact 的 Content URI。 可以綁定該通知服務,並調用 Sync Adapter 更新該 Raw Contact 的數據。
如果需要在用戶點擊某個社交數據或圖片時,調用某個 Activity,請按以下步驟注冊 Activity:
- 在項目的
res/xml/
目錄下,創建名為contacts.xml
的文件. 如果該文件已存在,可以跳過此步。 - 在此文件中加入
<ContactsAccountType xmlns:android="http://schemas.android.com/apk/res/android">
元素。如果此元素已存在,則跳過此步。 - 如果需要注冊 Activity,以便當用戶點擊系統“聯系人”應用中的社交數據項時,能進行后續處理, 請在上述 XML 元素中添加
viewStreamItemActivity="activityclass"
屬性,activityclass
是 Activity 類的完全限定格式名稱, 該 Activity 將用於接收來自系統“聯系人”應用的 Intent。 - 如果需要注冊 Activity,以便當用戶點擊系統“聯系人”應用中的社交圖片時,能進行后續處理, 請在上述 XML 元素中添加
viewStreamItemPhotoActivity="activityclass"
屬性,activityclass
i是 Activity 類的完全限定格式名稱, 該 Activity 將用於接收來自系統“聯系人”應用的 Intent。
關於 <ContactsAccountType>
元素的更多細節,將在 <ContactsAccountType> 元素 一節中介紹。
接收到的 Intent 將包含用戶點擊的社交流數據項或圖片的 Content URI。 如果需要對文本數據和圖片分為兩個 Activity 進行處理,可以在一個文件中同時使用兩個屬性。
與自建社交網絡服務進行交互
不需要離開系統的“聯系人”應用,用戶就可以邀請某個聯系人加入自建的社交網站。 只要讓系統“聯系人”應用向自建 Activity 發送一個邀請 Intent 即可。 請按以下步驟操作:
- 在項目的
res/xml/
目錄下,創建名為contacts.xml
的文件. 如果該文件已存在,可以跳過此步。 - 在此文件中加入
<ContactsAccountType xmlns:android="http://schemas.android.com/apk/res/android">
元素。如果此元素已存在,則跳過此步。 - 添加以下屬性:
inviteContactActivity="activityclass"
inviteContactActionLabel="@string/invite_action_label"
activityclass
是接收 Intent 的 Activity 的完全限定類名。invite_action_label
是個字符串,將在系統“聯系人”應用的 Add Connection (譯者注:沒找到,難道是分享?)菜單中顯示。
注意: ContactsSource
是過時的標記,現在對應的是 ContactsAccountType
。
contacts.xml 參考
contacts.xml
文件包含了一些 XML 元素,用於控制自建 Sync Adapter、自建應用,與系統“聯系人”應用、Contacts Provider 之間的交互。 下面介紹這些元素:
<ContactsAccountType> 元素
<ContactsAccountType>
元素控制着自建應用與“聯系人”應用之間的交互。 語法如下:
<ContactsAccountType xmlns:android="http://schemas.android.com/apk/res/android" inviteContactActivity="activity_name" inviteContactActionLabel="invite_command_text" viewContactNotifyService="view_notify_service" viewGroupActivity="group_view_activity" viewGroupActionLabel="group_action_text" viewStreamItemActivity="viewstream_activity_name" viewStreamItemPhotoActivity="viewphotostream_activity_name">
包含於:
res/xml/contacts.xml
可包含:
<ContactsDataKind>
描述:
聲明 Android 部件和 UI 標簽,用戶可以邀請某個聯系人加入社交網站, 當此聯系人的社交流數據時發生變化時也將收到通知,如此等等。
注意,<ContactsAccountType>
屬性的前綴 android:
不是必需的。
屬性:
-
inviteContactActivity
- 自建應用中某個 Activity 的完全限定類名,當用戶在系統“聯系人”應用中選擇 Add connection 菜單時, 將打開此 Activity。
-
inviteContactActionLabel
-
顯示在
Add connection 菜單中的字符串,表示
inviteContactActivity
定義的 Activity。 比如可以用“Follow in my network”。 這里可以使用字符串資源 ID。 -
viewContactNotifyService
-
以完全限定格式命名的接收通知用的自建服務類,當用戶查看某個聯系人時,此服務會接收到通知。 該通知由系統“聯系人”應用發出,這樣自建應用就能盡可能地推遲比較耗費資源的數據操作,只有在實際需要時再執行數據讀寫任務。 例如,自建應用可以響應這一查看聯系人的通知,讀取並顯示該聯系人的高分辨率頭像和最新的社交流數據。 更多細節將在
社交流信息的交互 一節中介紹。 關於接收通知的服務實例,請查看
SampleSyncAdapter 例程中的
NotifierService.java
文件。 -
viewGroupActivity
- 以完全限定格式命名的自建應用中的一個 Activity,用於顯示群組信息。 用戶在系統“聯系人”應用中點擊該群組的標題后,該 Activity 就會顯示出來。
-
viewGroupActionLabel
-
作為“聯系人”應用中某個控件的標題來顯示,用戶可以點擊查看自建應用中的聯系人群組。
例如,假定本地已安裝了 Google+ 應用,且通過“聯系人”應用對 Google+ 數據進行了同步, 則在“聯系人”應用的群組(Groups)選項卡中,將會看到 Google+ “圈子”以群組的形式顯示出來。 如果點擊 Google+ 圈子,就能看到以群組的方式顯示的圈子人員。 Google+ 圖標會顯示在頂部,點擊圖標則會跳轉到 Google+ 應用中去。 “聯系人”應用就是通過
viewGroupActivity
來完成上述操作的,並把viewGroupActionLabel
的值賦為 Google+ 的圖標。這里可以使用字符串資源 ID。
-
viewStreamItemActivity
- 以完全限定格式命名的自建應用中的一個 Activity, 當用戶在“聯系人”應用中點擊某個 Raw Contact 的一條社交流數據時,將會調用該 Acitivity。
-
viewStreamItemPhotoActivity
- 以完全限定格式命名的自建應用中的一個 Activity, 當用戶在“聯系人”應用中點擊某個 Raw Contact 的圖片時,將會調用該 Acitivity。
<ContactsDataKind> 元素
<ContactsDataKind>
元素控制着自建應用的自定義數據行在“聯系人”應用中的顯示方式。 語法如下:
<ContactsDataKind android:mimeType="MIMEtype" android:icon="icon_resources" android:summaryColumn="column_name" android:detailColumn="column_name">
包含於:
<ContactsAccountType>
描述:
“聯系人”應用可以利用此元素,把自定義數據行作為 Raw Contact 的明細數據之一,一起顯示出來。 <ContactsAccountType>
的每個 <ContactsDataKind>
子元素, 代表一種自定義數據行類型,此條數據是由自建 Sync Adapter 向 ContactsContract.Data
表添加的。每個自定義 MIME 類型都需要添加一個 <ContactsDataKind>
元素。 如果自定義數據行的數據不需要顯示出來,則無需添加該元素。
屬性:
-
android:mimeType
-
自定義數據行的 MIME 類型,該行數據位於
ContactsContract.Data
表中。例如,記錄聯系人最新地理位置的數據行,就可用vnd.android.cursor.item/vnd.example.locationstatus
作為 MIME 類型。 -
android:icon
- 在“聯系人”應用中顯示於自定義詳情旁邊的圖標,以 Android Drawable 資源的形式給出。 標明該條數據來源於自建服務。
-
android:summaryColumn
-
從 Data 記錄中讀到的第一項數據(共有兩項)所在字段的名稱。 在表示該條數據的列表項上,該字段中的內容將會顯示在第一行。 第一行為可選項,用於顯示摘要信息。 請參閱
android:detailColumn
。 -
android:detailColumn
-
從 Data 記錄中讀到的第二項數據(共有兩項)所在字段的名稱。 在表示該條數據的列表項上,該字段中的內容將會顯示在第二行。 請參閱
android:summaryColumn
。
Contacts Provider 的其他功能
除上述主要功能外,Contacts Provider 還提供了以下用於處理聯系人數據的功能:
- 聯系人群組
- 圖片功能
聯系人群組
Contacts Provider 可以把某些聯系人標記為群組。 如果某個賬戶對應的服務器需要維護群組,該賬戶類型對應的 Sync Adapter 應負責 Contacts Provider 和服務器之間的群組信息傳遞工作。 當用戶向服務器添加一個新聯系人,並把他歸入一個新組時,Sync Adapter 必須將這個新組添加到 ContactsContract.Groups
表中。 Raw Contact 所屬的一個或多個組使用 ContactsContract.CommonDataKinds.GroupMembership
MIME 類型存儲在 ContactsContract.Data
表內。
如果自建 Sync Adapter 會將服務器中的 Raw Contact 數據添加到 Contacts Provider 中,且沒有用到群組, 那么需要告訴 Provider 顯示這部分數據。 請在處理添加賬戶操作的代碼中,修改賬戶對應的 ContactsContract.Settings
記錄,這條記錄是由 Contacts Provider 添加的。 把該行數據的 Settings.UNGROUPED_VISIBLE
字段值置為1即可。這樣,即便未用到群組功能,Contacts Provider 也會讓相關的聯系人數據保持可見。
聯系人圖片
圖片信息以記錄的形式保存在 ContactsContract.Data
表中,MIME 類型為 Photo.CONTENT_ITEM_TYPE
。每行數據通過 CONTACT_ID
字段與圖片所屬 Raw Contact 的 _ID
字段關聯。 ContactsContract.Contacts.Photo
類定義了一個 ContactsContract.Contacts
子表,其中存放了聯系人的主圖片信息,即該聯系人主 Raw Contact 的主圖片。同樣, ContactsContract.RawContacts.DisplayPhoto
類也定義了一個 ContactsContract.RawContacts
子表,其中存放了 Raw Contact 主圖片的信息。
ContactsContract.Contacts.Photo
和 ContactsContract.RawContacts.DisplayPhoto
的參考文檔包含了讀取圖片信息的示例。 系統未提供讀取 Raw Contact 主縮略圖的助手類,但可以查詢 ContactsContract.Data
表來找到 Raw Contact 的主圖片記錄,查詢條件為 Raw Contact 的 _ID
、 Photo.CONTENT_ITEM_TYPE
和 IS_PRIMARY
字段。
社交流數據本身也可能包含圖片。這些圖片都保存在 ContactsContract.StreamItemPhotos
表中,社交流圖片一節對該表進行了詳細介紹。