【最后一篇API译文】Android开发-API指南- Contacts Provider

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

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 记录:

  1. 第一条 Raw Contact 记录对应“Thomas Higginson”,关联账户是 emily.dickinson@gmail.com。 账户类型为 Google。
  2. 第二条 Raw Contact 记录对应“Thomas Higginson”,关联账户是 emilyd@gmail.com。 账户类型也是 Google。由于对应的用户帐户不同,尽管第二条记录的名称与前一条的完全相同,还是成为了第二个 Raw Contact。
  3. 第三条 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个,名字分别为 DATA1DATA15。 另外还有4个通用字段是 Sync Adapter 专用的,命名为 SYNC1SYNC4。 通用字段名称随时可用,与数据行的类型无关。

DATA1 字段自带索引。 Contacts Provider 始终会使用这一字段,将其视为查询语句中最常用的数据所在。 比如,在 Email 数据行中,本字段就存放了实际的 Email 地址。

按照惯例,DATA15 是保留字段,用于存放二进制大对象(BLOB)数据,比如缩略图。

固定类型的字段名称

为了简化对某些特定类型数据的操作,Contacts Provider 也支持一些固定类型的字段名称,这些常量在 ContactsContract.CommonDataKinds 的子类中定义。通过这些常量,就可以方便地访问同名的数据字段。

例如, ContactsContract.CommonDataKinds.Email 类中就为 MIME 类型为 Email.CONTENT_ITEM_TYPEContactsContract.Data 数据行定义了一些字段。该类中包含了 Email 地址字段 ADDRESSADDRESS 的值实际就是通用字段名称“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.comAccountManager 将利用这个值来识别相应的账户类型。
账户名称
标识了某种账户类型的一个账户或登录名。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_CONTACTSWRITE_CONTACTS 权限之外,读写用户个人资料还分别需要 READ_PROFILEWRITE_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.SettingsContactsContract.SyncState 等。每种元数据的作用如下所示:

表 3.Contacts Provider 中的元数据

表名字段含义
ContactsContract.RawContacts DIRTY “0”表示自前一次同步以来没有变化 标记本机 Raw Contact 记录已被修改过并需要同步到服务器端去。 当 Android 应用程序做出修改后,Contacts Provider 会自动设置该字段值。

修改 Raw Contact 或 Data 表的 Sync Adapter 应该确保在 Content URI 后面添加 CALLER_IS_SYNCADAPTER 字符串。这样会阻止 Provider 把该条记录标记为 DIRTY 。 不然的话, Sync Adapter 修改也会被视为本地修改,并发送至服务器端,但其实这种修改正是由服务器端发起的。

“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 必须唯一,并在同步过程中保持不变:

  • 唯一性:每个账户对应的 Raw Contact 记录都必须拥有唯一的 SOURCE_ID。 如果做不到这一点,“联系人”应用就会出错。 请注意,账户类型相同的 Raw Contact 记录可以拥有一样的 SOURCE_ID。 比如,账户 emily.dickinson@gmail.com 对应的 Raw Contact 为“Thomas Higginson”, 它的 SOURCE_ID 就可以和 emilyd@gmail.com 对应的 Raw Contact 记录“Thomas Higginson”相同。
  • 稳定性:SOURCE_ID 是 Raw Contact 位于在线服务端的持久性数据。 比如:如果用户在应用程序设置中清除了联系人信息,然后又进行了一次同步, 那么恢复回来的 Raw Contact 记录仍然会保持原来的 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 数据行时使用乐观并发控制,请按以下步骤执行:

  1. 读取数据时,同时取回 Raw Contact 的 VERSION 字段。
  2. 根据不同的强制约束条件,用 newAssertQuery(Uri) 方法创建一个合适的 ContentProviderOperation.Builder 对象。对于 Content URI 而言,使用 RawContacts.CONTENT_URI ,并附带 Raw Contact ID 即可。
  3. 调用 ContentProviderOperation.Builder 对象的 withValue() 方法,把 VERSION 字段与前面取回的版本号进行对比。
  4. 再调用此 ContentProviderOperation.Builder 对象的 withExpectedCount() 方法,确保本次比较只涉及一条记录。
  5. 调用 build() 方法创建 ContentProviderOperation 对象,并把它作为第一个成员加入列表 ArrayList 中,这个列表是要传给 applyBatch() 的。
  6. 提交批处理事务。

如果在读写某 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 的数据列表。

调用 startActivityForResult() ,返回选中行的 Content URI。 URI 的格式是数据表的 URI 加数据行的LOOKUP_ID 。 在调用者 Activity 的存活期间,系统“联系人”应用会授予其对该 URI 的读写权限。 详情请参阅文档 Content Provider 基础

插入新 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 验证用户身份的步骤如下:

  1. 记下用户名、密码或类似信息(用户证书);
  2. 向服务发送用户认证信息;
  3. 检查服务返回结果。

如果服务接受了用户信息,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> 子元素,用于向系统报告相应的数据信息:
  • Sync Adapter 服务的 <meta-data> 元素指向了 XML 文件 res/xml/syncadapter.xml。 该文件依次定义了要与 Contacts Provider 同步的远程服务 URI 及服务的账户类型。
  • 可选: Authenticator 的 <meta-data> 元素指向了 XML 文件 res/xml/authenticator.xml。 该文件依次定义了 Authenticator 支持的账户类型、认证过程中用于显示界面的 UI 资源。 这里的账户类型必须与上述 Sync Adapter 账户类型相一致。

社交流数据

ContactsContract.StreamItemsContactsContract.StreamItemPhotos 表管理着来自社交网络的数据。 可以编写一个 Sync Adapter,把来自个人社交网络圈的数据添加到这两张表中去,或者从表中读取社交数据并显示出来。 通过这种方式,可以把自己的社交网络后台服务和前台应用,与 Android 的社交网络用户体验集成在一起。

社交流文字

社交流数据项必须与某个 Raw Contact 关联。 RAW_CONTACT_ID 即为 Raw Contact 的 _ID 值。 Raw Contact 的账户类型和账户名称也会保存在社交流数据记录中。

社交流数据保存在以下字段中:

ACCOUNT_TYPE
必填项。 本条社交流数据的 Raw Contact 账户类型。在插入数据时必须设置。
ACCOUNT_NAME
必填项。 本条社交流数据的 Raw Contact 账户名称。在插入数据时必须设置。
ID 字段
必填项。 插入社交流数据时,必须插入以下 ID 字段:
COMMENTS
可选项。保存概要信息,前缀于本条社交流数据显示。
TEXT
社交流数据项的标题,可以是由数据源发送过来的,也可以是说明如何生成本条数据的信息。 该字段可以包含任意格式的数据,可嵌入能被 fromHtml() 解析的图片资源。Provider 可能会截断或略去超长的文本,但会尽量避免在语言标记(tag)中间截断。
TIMESTAMP
文本字符串,表示本条社交流数据的插入或修改时间,单位是 Epoch 纪元(译者注:1970-01-01 00:00:00 UTC)以来的毫秒数。 应用程序插入或修改本条数据时,Contacts Provider 会自动更新该字段。

为了能醒目地显示社交流数据,会用到 RES_ICONRES_LABELRES_PACKAGE ,这些都代表着应用程序的资源。

ContactsContract.StreamItems 表还包含 SYNC1SYNC4 字段,用于 Sync Adapter 间的互斥同步。

社交流图片

ContactsContract.StreamItemPhotos 表存放着社交流数据项关联的图片信息。并通过 STREAM_ITEM_ID 字段与 ContactsContract.StreamItems表的 _ID 字段关联。图片的引用方式保存在以下字段中:

PHOTO 字段(BLOB类型)。
图片的二进制数据,Provider 对其进行了缩放以便保存和显示。 此字段是为了保证向后兼容性才保留的,以前的 Contacts Provider 会使用这个字段保存图片。 但是在现在的版本中,不应再使用这个字段保存图片了。 而应使用 PHOTO_FILE_IDPHOTO_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.StreamItemsContactsContract.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。步骤如下:

  1. 在项目的 res/xml/ 目录中,创建名为 contacts.xml 的文件. 如果该文件已存在,可以跳过此步。
  2. 在此文件中加入 <ContactsAccountType xmlns:android="http://schemas.android.com/apk/res/android"> 元素。如果此元素已存在,则跳过此步。
  3. 如果需要注册服务,以便在用户打开系统“联系人”应用中的联系人详情页时,能收到通知, 请在上述 XML 元素中添加 viewContactNotifyService="serviceclass" 属性, serviceclass 是服务类的完全限定格式名称,该服务用于接收来自系统“联系人”应用的 Intent。 对于通知(notifier)服务,可以使用 IntentService 的子类,以便服务接收到 Intent。 接收到的 Intent 中包含了用户所选 Raw Contact 的 Content URI。 可以绑定该通知服务,并调用 Sync Adapter 更新该 Raw Contact 的数据。

如果需要在用户点击某个社交数据或图片时,调用某个 Activity,请按以下步骤注册 Activity:

  1. 在项目的 res/xml/ 目录下,创建名为 contacts.xml 的文件. 如果该文件已存在,可以跳过此步。
  2. 在此文件中加入 <ContactsAccountType xmlns:android="http://schemas.android.com/apk/res/android"> 元素。如果此元素已存在,则跳过此步。
  3. 如果需要注册 Activity,以便当用户点击系统“联系人”应用中的社交数据项时,能进行后续处理, 请在上述 XML 元素中添加 viewStreamItemActivity="activityclass" 属性, activityclass 是 Activity 类的完全限定格式名称, 该 Activity 将用于接收来自系统“联系人”应用的 Intent。
  4. 如果需要注册 Activity,以便当用户点击系统“联系人”应用中的社交图片时,能进行后续处理, 请在上述 XML 元素中添加 viewStreamItemPhotoActivity="activityclass" 属性, activityclass i是 Activity 类的完全限定格式名称, 该 Activity 将用于接收来自系统“联系人”应用的 Intent。

关于 <ContactsAccountType> 元素的更多细节,将在 <ContactsAccountType> 元素 一节中介绍。

接收到的 Intent 将包含用户点击的社交流数据项或图片的 Content URI。 如果需要对文本数据和图片分为两个 Activity 进行处理,可以在一个文件中同时使用两个属性。

与自建社交网络服务进行交互

不需要离开系统的“联系人”应用,用户就可以邀请某个联系人加入自建的社交网站。 只要让系统“联系人”应用向自建 Activity 发送一个邀请 Intent 即可。 请按以下步骤操作:

  1. 在项目的 res/xml/ 目录下,创建名为 contacts.xml 的文件. 如果该文件已存在,可以跳过此步。
  2. 在此文件中加入 <ContactsAccountType xmlns:android="http://schemas.android.com/apk/res/android"> 元素。如果此元素已存在,则跳过此步。
  3. 添加以下属性:
    • 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.PhotoContactsContract.RawContacts.DisplayPhoto 的参考文档包含了读取图片信息的示例。 系统未提供读取 Raw Contact 主缩略图的助手类,但可以查询 ContactsContract.Data 表来找到 Raw Contact 的主图片记录,查询条件为 Raw Contact 的 _IDPhoto.CONTENT_ITEM_TYPEIS_PRIMARY 字段。

社交流数据本身也可能包含图片。这些图片都保存在 ContactsContract.StreamItemPhotos 表中,社交流图片一节对该表进行了详细介绍。

posted on 2015-12-10 22:37  呆呆大虾  阅读(3127)  评论(2编辑  收藏  举报

导航