to top

Contacts Provider (原文链接)

翻译:汉尼拔萝卜(https://github.com/gaojian3301)

Contacts Provider 是一个功能强大并且灵活的 Android 组件,它被用来管理手机联系人的数据库。 Contacts Provider 是手机联系人应用的数据来源,我们可以在自己的应用中获取到它并使它在手机和 服务器之间传输。Contacts Provider 包含了很广泛的数据源并且尝试为每个联系人管理尽可能多的数据, 这导致了它的结构异常复杂。也正因为这个原因,它的 API 包含了一系列大量的契约类和接口来协助 数据的查询和修改。

本篇指南描述了以下内容:

  • 基本的 provider 架构.
  • 如何从 provider 中查询数据.
  • 如何修改 provider 中的数据.
  • 如何写一个 sync adapter 来讲 server 端的数据同步到 Contacts Provider.

本篇文章假设读者了解 Android 的 content provider 组件。想了解更多的 content provider 组件, 请参考 Content Provider Basics 这一篇文章。 Sample Sync Adapter 这个示例应用使用了 sync adapter 来同步 Contatcs Provider 和 Google Web Services 应用的数据。

Contacts Provider Organization

Contacts Provider 是一个 content provider 组件。它维护每个联系人的三种数据类型,每一种类型都 对应着 provider 提供的一张表,如图1所示:

Figure 1. Contacts Provider 表的结构.

这三张表通常与他们的契约类的名字关联。这些契约类定义了这些表所使用的 URIs、列名、以及列的值:

ContactsContract.Contacts table
每条记录代表了不同的联系人,是基于 raw contact 数据的组合。
ContactsContract.RawContacts table
每一条记录表示特定账户名和账户类型下一个联系人数据的摘要。
ContactsContract.Data table
每一条记录包含了 raw contact 的具体信息,比如email地址或者电话号码。

ContactsContract 中提供的其他表都是一些辅助表,被 Contacts Provider 用来管理它的操作,也用来为手机的Contacts应用和telephony应用提供一些特殊的功能实现。

Raw contacts

一条 raw contact 记录表示一个特定账户类型和账户名下的一个联系人的数据。由于 Contacts Provider 允许一个联系人的数据来自多个服务器,因此 Contacts Provider允许多个 raw contact 组成一个联系人。 也可以将同一个账户类型下不同账户名的 raw contact 进行组合。

一条 raw contact 的大部分数据没有存储在 ContactsContract.RawContacts 中。而是存储在一行或者多行 ContactsContract.Data 的记录中。 每一行 data 都拥有名为 Data.RAW_CONTACT_ID 的列,这一列的值是来自 ContactsContract.RawContacts 中的 RawContacts._ID 列的值.

Important raw contact columns

table 1中列出了 ContactsContract.RawContacts 表中一些重要的列名。注意阅读表格后面的注意事项:

Table 1. Important raw contact columns.

Column name Use Notes
ACCOUNT_NAME account type 下的 account name 是raw contact 的来源。举个例子,Google 账户的 account name 就是手机使用的 Gmail 地址。阅读 ACCOUNT_TYPE 来获取更多信息。 account name 的格式跟 account type 有关,并不一定都是邮箱地址。
ACCOUNT_TYPE account type 是 raw contact 数据的来源。比如说,Google 账户的 account type 是 com.google. 在定义 account type 的时候,应该使用自己的域名,这可以确保 account type 独一无二。 account type 提供联系人的数据,通常它也会与一个 sync adapter 关联起来,sync adapter 是用来同步 Content Provider 的。
DELETED 标志一个 raw contact 是否被删除。 “DELETED”的存在允许 Contacts Provider 内部保持着 raw contact 直到 sync adapter 在服务器端删除这一个 raw contact,然后才会彻底从手机的数据库中删除。

Notes

以下是关于 ContactsContract.RawContacts 表的一些注意事项:

  • raw contact 的名字没有存储在 ContactsContract.RawContacts 表中。而是存储在 ContactsContract.Data 表中类型为 ContactsContract.CommonDataKinds.StructuredName 的一条记录,一条 raw contact 在 ContactsContract.Data 中只有一条这种类型的数据。
  • 注意: 如果需要使用自己定义的 account 数据,首先需要使用 AccountManager 来注册。应该提示用户将 account type 和对应的 account name 添加到账户列表中。如果没有这一步操作,Contacts Provider 将会自行删除你的 raw contact 数据。

    举例说明,如果你需要维护域名为 com.example.dataservice ,用户名为 becky.sharp@dataservice.example.com 的联系人。那么在添加 raw contact 之前,首先需要将 accout type (com.example.dataservice) 和 account name (becky.smart@dataservice.example.com) 添加到手机的账户列表中。你可以在文档中解释这一需求,也可以在程序中提示用户去添加 type 和 name,或者两者都实现。 Account type 和 account name 在下面的章节会更加详细介绍。

Sources of raw contacts data

为了更好的理解 raw contacts 是如何工作的,假设 Emily Dickinson 在她的设备上有下面3个账户:

  • emily.dickinson@gmail.com
  • emilyd@gmail.com
  • Twitter account "belle_of_amherst"

打开 Accounts 设置中3个账户的 Sync Contacts

现在 Emily Dickinson 打开浏览器,登录 emily.dickinson@gmail.com,然后打开联系人应用,新增一个名为 "Thomas Higginson" 的联系人。 稍后,她登录 emilyd@gmail.com 并且向 "Thomas Higginson" 发送一封邮件,发现 "Thomas Higginson" 可以自动识别。Emily Dickinson 发现 她在 Twitter 上关注了 "colonel_tom"(Thomas Higginson 的Twitter名)。

Contacts Provider 在上述操作中新建了3个 raw contacts:

  1. 第一个 raw contact 账户名为 emily.dickinson@gmail.com,账户类型是 Google。
  2. 第二个 raw contact 账户名为 emilyd@gmail.com,账户类型也是 Google。可以创建姓名相同的 raw contacts,是因为他们所属的账户不同。
  3. 第三个 raw contact 账户名为 "belle_of_amherst",账户类型是 Twitter。

Data

前面有提到 raw contact 的数据存储在 ContactsContract.Data 表中,每一条记录都仪 raw contact 的 _ID 值作为外键。这允许每一个 raw contact 可以拥有几个同一类型数据,比如邮箱地址和电话号码。举个例子,emilyd@gmail.com 账户下的联系人"Thomas Higginson"(即"Thomas Higginson"的 raw contact 是来源于 Google 账户 emilyd@gmail.com)拥有一个类型是 home 的邮箱为thigg@gmail.com,还有一个类型是 work 的邮箱为 thomas.higginson@gmail.com,Contacts Provider 将会把这两个邮箱地址和 raw contact 关联起来。

不同类型的数据存储在这一张表里的每一行中。Display name, phone number, email, postal address, photo, 以及 website 都可以在 ContactsContract.Data 中找到。为了更好的管理这张表, ContactsContract.Data 中定义了一些具有描述性的列名和一些通用性的列名。描述性名称的列在各种数据记录中表达的意思是一样的,然而通用性名称的列在各种数据类型的记录中则表达的是不一样的意思。(by汉泥巴萝卜:这边直接翻译过来的有些难以理解,所谓描述性质的列就是那些列的值跟数据类型没有关系,始终表达的是同一种意思。而通用性的列则跟数据类型有关,表达不同的意思。)

Descriptive column names

下面是一些描述性质的列:

RAW_CONTACT_ID
它对应着 raw contact 中的_ID 列。
MIMETYPE
这一条数据的类型,使用 MIME 类型来表达。Contacts Provider使用的 MIME 类型都定义在 ContactsContract.CommonDataKinds中。这些 MIME 类型都是开源的,可以在应用程序和同步适配器中使用。
IS_PRIMARY
如果一种类型的数据可以存储多条记录,IS_PRIMARY 标志这一条记录是这种类型数据的默认数据。举个例子,长按联系人的一个号码,并且在弹出的菜单中选择 Set default,那么包含这个号码的 ContactsContract.Data 记录的 IS_PRIMARY 列将会被设为一个非0的值。

Generic column names

DATA1DATA15 总共有15个通用的列,另外从 SYNC1SYNC4 有4个通用的列是在同步适配器中使用的。不管数据类型是什么,通用列名总是工作的(by汉泥巴萝卜:这一句不太理解)。

DATA1 这一列是被加了索引的。Contacts Provider 通常使用这一列来存储那些经常需要检索的值。比如说存储邮箱地址的一条记录中,真实的地址就存储在这一列里面。

习惯上,DATA15 列用来存储一些比较大的二进制文件(BLOB),比如说缩略图。

Type-specific column names

为了更好的使用特定类型记录中的列,Contacts Provider 在 ContactsContract.CommonDataKinds定义了一些特定列名的常量。这些常量为相同的列名定义了不同的名称,这会有助于你更加方便地获取特定类型记录的数据。

举个例子,ContactsContract.CommonDataKinds.Email 这个类为 ContactsContract.Data 中数据类型为 Email.CONTENT_ITEM_TYPE 的记录定义了一些特定的列名。这个类中包含了常量 ADDRESS 来表示邮箱地址那一列。 ADDRESS 的真实值是 "data1",与通用列名一样。

Caution: 不要将与 MIME 类型不匹配的数据添加到 ContactsContract.Data 表中。如果你这么做,可能会丢失这条记录或者造成 provider 故障。举个例子,如果一条记录的数据类型是 Email.CONTENT_ITEM_TYPE,它的DATA1 中存储的却是姓名而不是邮箱地址。如果你定了自己的 MIME 类型,那你可以自由定义它的列名并且可以任意地使用它。

Figure 2 展示了描述性的列在 ContactsContract.Data 表中的列是如何表示的,并且可以看出特定类型的列是如何覆盖 ContactsContract.Data 表中通用性列的。

How type-specific column names map to generic column names

Figure 2. Type-specific column names and generic column names.

Type-specific column name classes

表格2中列出的是最常用的特定类型的一些类:

Table 2. Type-specific column name classes

Mapping class Type of data Notes
ContactsContract.CommonDataKinds.StructuredName 存储的数据是 raw contact 的姓名。 一个 raw contact 仅有一个这种类型的记录。
ContactsContract.CommonDataKinds.Photo 存储的数据是 raw contact 的头像。 一个 raw contact 仅可以有一个这种类型的记录。
ContactsContract.CommonDataKinds.Email 存储的是 raw contact 的邮箱地址。 一个 raw contact 可以有多个这种类型的记录。
ContactsContract.CommonDataKinds.StructuredPostal 存储的数据是 raw contact 的通信地址。 一个 raw contact 可以有多个这种类型的记录。
ContactsContract.CommonDataKinds.GroupMembership 它的值在 Contacts Provider 中表示的是 raw contact所属的群组。 群组是一个账户的可选功能。将会在 Contact groups 这一节中详细讲解。

Contacts

Contacts Provider 可以将各个账户下的 raw contacts 组合起来形成一个 contact。这方便显示和管理用户为某一个人搜集的所有信息。Contacts Provider 管理着 raw copntacts 的创建,以及将新建的 raw contact 与其他的进行组合。不管是应用程序还是同步适配器都不允许增加 contact(只能增加 raw contact),并且 contact 表中有些字段是只读的。

Note: 如果你尝试通过 insert() 操作来往 contacts 表中添加记录,那么你将会得到 UnsupportedOperationException 异常。如果去更新一个只读的列,那么这个操作将被忽略。

如果我们新增一个 raw contact,它没有被链接到现有的联系人上,那么 Contacts Provider 将会创建一个联系人。如果我们将 join 起来的联系人拆分开来,Contacts Provider 也同样会创建新的联系人。如果应用程序或者同步适配器创建了一个raw contact,并且它被链接到现有的联系人,那么Contacts Provider 将会把新建的 raw contact 与目标联系人组合起来。

Contacts Provider 通过 ContactsContract.RawContacts 表中的 _ID 列将 contacts 表中的数据和 raw contacts 表中的数据链接起来。ContactsContract.RawContacts 表中 CONTACT_ID 的值就是 contacts 表中的 _ID,说明这条 raw contact 记录属于这个联系人。

ContactsContract.Contacts 表中还有 LOOKUP_KEY 这一列,它是 contact 记录的永久链接。因为 contacts 表格是由 Contacts Provider 自动管理的,在组合和同步时有可能会改变 contact 据录的 _ID 列的值。即使发生了这样的情况,CONTENT_LOOKUP_URILOOKUP_KEY组合起来依旧能够指向这一条 contact 记录,因此我们可以使用 LOOKUP_KEY 这一列来维持“收藏”的联系人。这一列有自己的格式,与 _ID 没有任何关系。

Figure 3 shows how the three main tables relate to each other. by汉尼拔萝卜:通常我们联系人应用中看到的联系人对用的就是 contacts 表中的一条记录。一个 contact 可以拥有多个 raw contacts,这时候点击这个联系人的编辑界面,就会发现可以同时编辑多个 raw contacts.这边所谓的组合,也就是 raw contacts 的 contact_id,只是让这几个 raw contacts 在界面上组合起来显示,本身的数据还是完全独立的。

Contacts provider main tables

Figure 3. Contacts, Raw Contacts, and Details table relationships.

Data From Sync Adapters

用户联系人显示的不仅仅是手机上创建的数据,还有通过 同步适配器 从Web服务同步到Contacts Provider 中的数据。同步适配器 自动管理着手机端和服务端的数据传输。同步适配器有系统管理着在后台运行,它会调用 ContentResolver 的方法来管理联系人数据。

在 Android 中,一个同步适配器对应的 web 服务被一个账户类型标识。每一个同步适配器只能对应一个账户类型,但是可以支持这种账户类型下的多个账户名。账户类型和账户名在 Sources of raw contacts data 中有简要描述。下面提供了更多的信息,并且描述账户类型和账户名是如何关联到同步适配器和 web 服务的。

账户类型
用来标识一个存储数据的服务器。大多时候,用户会使用这个服务来进行验证。比如 google 联系人的账户类型是 google.com.这个值与 AccountManager 注册是账户类型一致。
账户名
某个账户类型下的特定账户。Google 联系人的账户类型都是一样的,但是账户名不一样,可以用邮箱地址作为账户名,当然也使用字母或者数字作为账户名。

账户类型不一定是唯一的,用户可以配置多个 Google 联系人账户来下载数据;然而,同一个账户类型下的账户名一定是唯一的,账户类型和账户名共同定义一个单独的账户。

如果你想要实现设备和服务器的数据传输,那么需要实现自己的同步适配器,这将会在 Contacts Provider Sync Adapters 中详细描述。

Figure 4 shows how the Contacts Provider fits into the flow of data about people. In the box marked "sync adapters," each adapter is labeled by its account type.

Flow of data about people

Figure 4. The Contacts Provider flow of data.

Required Permissions

想要访问 Contacts Provider 的用用必须要有下面的权限:

读取权限
READ_CONTACTS, specified in AndroidManifest.xml with the <uses-permission> element as <uses-permission android:name="android.permission.READ_CONTACTS">.
修改权限
WRITE_CONTACTS, specified in AndroidManifest.xml with the <uses-permission> element as <uses-permission android:name="android.permission.WRITE_CONTACTS">.

定义了这些权限并不能够访问 profile(也就是个人信息) 数据。The User Profile 中将会讨论 profile。

联系人数据是私人并且敏感的信息。用户并不希望应用程序去搜集联系人数据。如果想要访问联系人数据的原因不够明显,那么用户会给你的应用很低的评分或者直接拒绝安装你的应用。

The User Profile

ContactsContract.Contacts 中有一行单独的记录来表示设备用户的信息。其实就相当于个人名片一个意思。系统可能会有多个用户 raw contacts(by汉尼拔萝卜:这边应该是Android 4.2 中加入的多用户功能,但是我在手头上 5.1 和 6.0 的手机已经没有了这个功能,所以一般情况下,用户 contact row 就只对应一个 raw contact row),每一个 raw contact 与普通联系人的数据结构一样,可以有多个 data 记录。访问 profile 需要的常量被定义在 ContactsContract.Profile 这个类中。

访问用户信息需要额外的权限。除了 READ_CONTACTSWRITE_CONTACTS 之外,访问用户信息还需要添加 READ_PROFILEWRITE_PROFILE 这两个权限。

要知道用户的个人信息是非常隐私的。READ_PROFILE 这个权限允许你访问个人信息,所以一定要在你应用的描述中告诉用户为什么需要获取这一权限。

在我们使用 ContentResolver.query() 来查询用户个人信息时,将查询的 URI设置为 CONTENT_URI,并且不要设置任何过滤条件(by汉尼拔萝卜:因为这个记录只有一条)。同样也可以使用这个 URI 去查询用户的 raw contact 和 data 记录。示例如下:

// Sets the columns to retrieve for the user profile
mProjection = new String[]
    {
        Profile._ID,
        Profile.DISPLAY_NAME_PRIMARY,
        Profile.LOOKUP_KEY,
        Profile.PHOTO_THUMBNAIL_URI
    };

// Retrieves the profile from the Contacts Provider
mProfileCursor =
        getContentResolver().query(
                Profile.CONTENT_URI,
                mProjection ,
                null,
                null,
                null);

Note: 如果你查询到多条 contact 数据记录,那么你可以使用 IS_USER_PROFILE 这一列的值来判断哪一条记录是个人信息。当某条记录是用户个人信息时,它的值是“1”.

Contacts Provider Metadata

Contacts Provider 对数据库中数据的状态改变都有记录。这些元数据(by汉尼拔萝卜:也就是描述数据的数据,它本身并不包含联系人的任何信息,只是用来描述联系人数据的状态)存在于各种地方,包括 Raw Contacts表、Data表、Contacts表以及ContactsContract.Settings 表和ContactsContract.SyncState表。下面的表格给出了这些元数据的作用:

Table 3. Metadata in the Contacts Provider

Table Column Values Meaning
ContactsContract.RawContacts DIRTY "0" - 上次同步过后就再也没有发生过变化 标志这一个 raw contact 记录是否需要与web服务进行同步。当我们在应用中更新联系人信息时,Contacts Provider 会自动修改这一列。

当同步适配器在修改 raw contact 记录或者 data 记录时,我们需要将 CALLER_IS_SYNCADAPTER 这个参数附加到 URI中(by汉尼拔萝卜:默认值为 false,需要设置为 true,就像这样写 appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")),这样做可以阻止 Contacts Provider 去修改 dirty 的值。否则的话,当同步适配器将web服务端的数据同步到设备时,手机上并没有修改动作,但是 dirty 还是变为了1,这样的行为就不合理了。

"1" - 上次同步后发生变化,需要再次发起同步
ContactsContract.RawContacts VERSION raw contact 数据的版本 当与这条记录相关的数据发生变化时,Contacts Provider 会自动增加这一列的值。
ContactsContract.Data DATA_VERSION data 数据的版本号 当这一行 data 数据发生变化时,Contacts Provider 会自动增加它的值。
ContactsContract.RawContacts SOURCE_ID 用来表示这一个 raw contact 是在哪个账户中创建的字符串。 当同步适配器创建一个 raw conatct 时,这一列应该设置一个独一无二的值。当 Android 应用程序创建 raw contact,这一列应该置空。

source id 必须得是独一无二的,并且同步时是恒定的:

  • Unique: 每个账户下的一个 raw contact 都拥有它自己的 source id. 如果做不到这一点,你的联系人应用可能会出现问题。要说名的是一个账户类型下的两个 raw contacts 可以拥有相同的 source id. 比如 emily.dickinson@gmail.com 账户下的联系人 "Thomas Higginson" 和 emilyd@gmail.com 账户下的联系人 "Thomas Higginson".(by汉尼拔萝卜:一个账户的确定需要账户类型和账户名称,账户名称不同,表示了两个账户)
  • Stable: raw contact 的 source id 在服务端应该是恒定不变的。举个例子来说,如果用户从设置中清除了 Contacts Storage 的数据,然后重新同步服务端的数据,那么之前 raw contact 的 source id 是不会发生变化的。如果不满足这个结果,那么 shortcuts(by汉尼拔萝卜:应该指的是我们添加在桌面上某个联系人widget,单击它可以直接跳转到这个联系人的详情)就会出现问题。
ContactsContract.Groups GROUP_VISIBLE "0" - 属于这个群组的联系人不应该在应用中显示 用户使用这一列的值来隐藏特定群组中的联系人
"1" - 属于这个群组的联系人应该在应用中显示
ContactsContract.Settings UNGROUPED_VISIBLE "0" - 某个账户下不属于任何群组的联系人不应该在应用中显示 默认情况下,如果一个联系人的 raw contacts 不属于任何群组(一个 raw contact 所属的群组由 ContactsContract.Data 表中数据类型为 ContactsContract.CommonDataKinds.GroupMembership 的数据表示 ),这个联系人是不应该在应用中显示的。通过设置 ContactsContract.Settings 表中每个账户对应的这个标志,可以强制显示出不属于任何群组的联系人。比如说某个服务器中的联系人并没有群组功能,那么我们可以通过设置这个 flag 来显示出这个服务器中的联系人。
"1" - 某个账户下不属于任何群组的联系人应该在应用中显示
ContactsContract.SyncState (all) 这张表用来存储同步适配器的元数据 使用这张表来存储同步的状态和同步相关的数据

Contacts Provider Access

这一节将描述如何访问 Contacts Provider 提供的数据,重点是下面几点:

  • Entity queries.
  • Batch modification.
  • Retrieval and modification with intents.
  • Data integrity.

关于同步适配器所做的修改将在 Contacts Provider Sync Adapters中进一步讨论。

Querying entities

因为 Contacts Provider 中的表都是有层次组织的,在查询一行数据时,也应该将它的“子行”查询出来。举个例子来说,为了显示出一个联系人的信息,我们需要获取到一个 ContactsContract.Contacts 下所有的 ContactsContract.RawContacts 数据,还需要将每一个 ContactsContract.RawContacts 关联的 ContactsContract.CommonDataKinds.Email 数据都查询出来。为了方便地完成这样的操作,Contacts Provider 提供了 entity 结构,它可以将数据库中的表连接起来。

entity 就像是一张从父表和子表中选出一些列重新组合的一张表。当查询 entity 时,只能根据entity中的列来定制查询结果和查询条件。查询结果的 Cursor 里包含了子表中被查询到的记录。举例来说,如果去查询 ContactsContract.Contacts.Entity 来获取某个联系人的姓名,并且要得到这个姓名对应的联系人的所有 raw contacts 中所有的 ContactsContract.CommonDataKinds.Email 数据记录,那么得到的 Cursor 将会包含所有的 ContactsContract.CommonDataKinds.Email 数据记录。

entity 的使用非常方便,通过查询它我们可以一次就得到一个联系人的所有数据。不许要先去查询父表,得到 ID 后,再用这个 ID 去子表中查询。同时,Contacts Provider 还可以保证查询道德数据是内部一致的,也就是说,在一次查询过程中,数据不会发生变化。

Note: entity 通常不会包含父表和子表中所有的列,如果使用的列并不在 entity 中时,程序就会抛出Exception.

下面的代码片段展示了如何获取一个联系人对应的所有 raw contacts 记录。这段代码来自于拥有"main" 和 "detail"这两个 acticvity的应用程序(by汉尼拔萝卜:跟源码中 PeopleActivity 和 QuickContactActivity 一样,一个用来显示联系人列表,一个用来显示联系人详情)。“main”界面显示的是联系人列表,当用户选择其中一个时,将会把所选联系人的 ID 传给 “detail”.然后“detail”去查询 ContactsContract.Contacts.Entity 来显示所有与该ID关联的 raw contacts 的 data 数据。

下面代码截选自“detail”:

...
    /*
     * Appends the entity path to the URI. In the case of the Contacts Provider, the
     * expected URI is content://com.google.contacts/#/entity (# is the ID value).
     */
    mContactUri = Uri.withAppendedPath(
            mContactUri,
            ContactsContract.Contacts.Entity.CONTENT_DIRECTORY);

    // Initializes the loader identified by LOADER_ID.
    getLoaderManager().initLoader(
            LOADER_ID,  // The identifier of the loader to initialize
            null,       // Arguments for the loader (in this case, none)
            this);      // The context of the activity

    // Creates a new cursor adapter to attach to the list view
    mCursorAdapter = new SimpleCursorAdapter(
            this,                        // the context of the activity
            R.layout.detail_list_item,   // the view item containing the detail widgets
            mCursor,                     // the backing cursor
            mFromColumns,                // the columns in the cursor that provide the data
            mToViews,                    // the views in the view item that display the data
            0);                          // flags

    // Sets the ListView's backing adapter.
    mRawContactList.setAdapter(mCursorAdapter);
...
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {

    /*
     * Sets the columns to retrieve.
     * RAW_CONTACT_ID is included to identify the raw contact associated with the data row.
     * DATA1 contains the first column in the data row (usually the most important one).
     * MIMETYPE indicates the type of data in the data row.
     */
    String[] projection =
        {
            ContactsContract.Contacts.Entity.RAW_CONTACT_ID,
            ContactsContract.Contacts.Entity.DATA1,
            ContactsContract.Contacts.Entity.MIMETYPE
        };

    /*
     * Sorts the retrieved cursor by raw contact id, to keep all data rows for a single raw
     * contact collated together.
     */
    String sortOrder =
            ContactsContract.Contacts.Entity.RAW_CONTACT_ID +
            " ASC";

    /*
     * Returns a new CursorLoader. The arguments are similar to
     * ContentResolver.query(), except for the Context argument, which supplies the location of
     * the ContentResolver to use.
     */
    return new CursorLoader(
            getApplicationContext(),  // The activity's context
            mContactUri,              // The entity content URI for a single contact
            projection,               // The columns to retrieve
            null,                     // Retrieve all the raw contacts and their data rows.
            null,                     //
            sortOrder);               // Sort by the raw contact ID.
}

当 load 完成数据加载后,LoaderManager 会调用 onLoadFinished() 方法。这个方法中的一个参数就是查询的 Cursor 结果。可以从该 Cursor 中得到数据用来显示或者其他用途。

Batch modification

当我们在 Contacts Provider 中执行新增、修改、删除操作时,应该尽可能地使用“批处理”,通过创建一个 ContentProviderOperation 对象的 ArrayList 集合然后调用 applyBatch() 方法。因为 Contacts Provider 会把一个 applyBatch() 中的所有操作当成一个事务来处理,你的修改将不会让数据库处于一个不一致的状态中。批处理还方便同时插入 raw contact 和它对应的 data 数据。

Note: 像要去修改单个 raw contact 记录,最好是通过发送 intent 来让联系人应用去修改,而不是直接用代码进行修改。这将会在 Retrieval and modification with intents 章节中详细讲述。

Yield points(by汉尼拔萝卜:这边我翻译为挂起点)

批量处理中有很多的操作,这又可能会阻塞其他进程的运行,这是很差的用户体验。为了改善这种情况,应该尽可能地将所要做的操作分散到多个集合中,同时也为了避免阻塞系统运行,应该为 ContentProviderOperation 设置一个或多个yield points. 通过让 ContentProviderOperationisYieldAllowed() 的返回值置为 true. 当Contacts Provider 遇到一个挂起点时,它会停止工作来让其他进程得以运行,并且会关闭当前事务。当Contacts Provider 再次工作时,它将会开启一个新的事务来进行 ArrayList 集合中的下一个操作。

挂起点会导致调用一次 applyBatch() 时开启多个事务。因此,我们应该为某一行记录所有操作的最后一个操作设置一个挂起点。比如说,在我们插入 raw contact 和它相关的 data 数据时,我们应该为最后一步操作设置一个挂起点。

挂起点是原子操作,也就是说两个挂起点之间的所有操作要么成功要么失败。如果没有设置挂起点,原子操作将会是每一步具体的操作。使用挂起点,可以阻止系统性能的下降,还可以确保一些列的操作集合具有原子性。

Modification back references

当我们使用 ContentProviderOperation 来插入新的 raw contacts 和与它相关的 data 数据时,必须要将 data 数据的 raw contact's _ID 这一列关联到 raw contact 的RAW_CONTACT_ID 列上。然而,这个值在创建 ContentProviderOperation 对象时还没有,因为插入 raw contact 的操作还没有完成。为了解决这个问题,ContentProviderOperation.Builder 类提供了withValueBackReference()这个方法,它允许基于上一条操作的结果来插入或者修改数据。

withValueBackReference() 方法有两个参数:

key
键值对的键。这个值应该是表中的列名。
previousResult
applyBatch() 的结果存储在一个索引从0开始的 ContentProviderResult 对象数组中。执行批处理操作时,指向结果存储在一个中间数组中。previousResult 的值就是数组中的一个索引,它以key值来进行存取。这允许我们在插入一个 raw contact 记录时得到插入的 _ID 值,接下来在插入 data 记录时使用。

结果数组在调用 applyBatch() 时就被创建完成,它的大小等于 ContentProviderOperationArrayList 数组的大小。数组初始值都是null,当我们想要获取一个还没有被执行操作的向后引用时,withValueBackReference() 方法将会抛出 异常

下面的代码将展示如果使用批处理来插入 raw contact 和 data 记录。它包含了建立挂起点和使用向后引用。这段代码是 createContacEntry() 方法的扩展,而这个方法来自于 Contact Manager 示例应用中 ContactAdder类。

第一段代码从UI中得到联系人数据,这个时候,用户已经选择好了需要将 raw contact 添加到哪个账户下面(by汉尼拔萝卜:也就是源码中的ContactEditorActivity).

// Creates a contact entry from the current UI values, using the currently-selected account.
protected void createContactEntry() {
    /*
     * Gets values from the UI
     */
    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());

接下来的代码展示了如何创建一个插入到ContactsContract.RawContacts表中 raw contact 记录的操作:

    /*
     * Prepares the batch operation for inserting a new raw contact and its data. Even if
     * the Contacts Provider does not have any data for this person, you can't add a Contact,
     * only a raw contact. The Contacts Provider will then add a Contact automatically.
     */

     // Creates a new array of ContentProviderOperation objects.
    ArrayList<ContentProviderOperation> ops =
            new ArrayList<ContentProviderOperation>();

    /*
     * Creates a new raw contact with its account type (server type) and account name
     * (user's account). Remember that the display name is not stored in this row, but in a
     * StructuredName data row. No other data is required.
     */
    ContentProviderOperation.Builder op =
            ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
            .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, mSelectedAccount.getType())
            .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, mSelectedAccount.getName());

    // Builds the operation and adds it to the array of operations
    ops.add(op.build());

接下来的代码在 data 表中新增了 display name、phone、email 记录。

创建的每个 operation builder 对象都使用 withValueBackReference() 来获取 RAW_CONTACT_ID. 这个引用指向 ContentProviderResult 中的第一个结果,也就是插入 raw contact 时返回的 _ID 值。最终,每一个data记录都能够通过 RAW_CONTACT_ID 链接到他们所属的 raw contact 记录上去。

创建插入邮箱地址的 ContentProviderOperation.Builder 对象时,调用了 withYieldAllowed(),设置了一个挂起点:

    // Creates the display name for the new raw contact, as a StructuredName data row.
    op =
            ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
            /*
             * withValueBackReference sets the value of the first argument to the value of
             * the ContentProviderResult indexed by the second argument. In this particular
             * call, the raw contact ID column of the StructuredName data row is set to the
             * value of the result returned by the first operation, which is the one that
             * actually adds the raw contact row.
             */
            .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)

            // Sets the data row's MIME type to StructuredName
            .withValue(ContactsContract.Data.MIMETYPE,
                    ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)

            // Sets the data row's display name to the name in the UI.
            .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name);

    // Builds the operation and adds it to the array of operations
    ops.add(op.build());

    // Inserts the specified phone number and type as a Phone data row
    op =
            ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
            /*
             * Sets the value of the raw contact id column to the new raw contact ID returned
             * by the first operation in the batch.
             */
            .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)

            // Sets the data row's MIME type to Phone
            .withValue(ContactsContract.Data.MIMETYPE,
                    ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)

            // Sets the phone number and type
            .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, phone)
            .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, phoneType);

    // Builds the operation and adds it to the array of operations
    ops.add(op.build());

    // Inserts the specified email and type as a Phone data row
    op =
            ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
            /*
             * Sets the value of the raw contact id column to the new raw contact ID returned
             * by the first operation in the batch.
             */
            .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)

            // Sets the data row's MIME type to Email
            .withValue(ContactsContract.Data.MIMETYPE,
                    ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)

            // Sets the email address and type
            .withValue(ContactsContract.CommonDataKinds.Email.ADDRESS, email)
            .withValue(ContactsContract.CommonDataKinds.Email.TYPE, emailType);

    /*
     * Demonstrates a yield point. At the end of this insert, the batch operation's thread
     * will yield priority to other threads. Use after every set of operations that affect a
     * single contact, to avoid degrading performance.
     */
    op.withYieldAllowed(true);

    // Builds the operation and adds it to the array of operations
    ops.add(op.build());

最后一段代码展示了如何调用 applyBatch() 来新增一个 raw contact 和与它相关的 data 记录。

    // Ask the Contacts Provider to create a new contact
    Log.d(TAG,"Selected account: " + mSelectedAccount.getName() + " (" +
            mSelectedAccount.getType() + ")");
    Log.d(TAG,"Creating contact: " + name);

    /*
     * Applies the array of ContentProviderOperation objects in batch. The results are
     * discarded.
     */
    try {

            getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
    } catch (Exception e) {

            // Display a warning
            Context ctx = getApplicationContext();

            CharSequence txt = getString(R.string.contactCreationFailure);
            int duration = Toast.LENGTH_SHORT;
            Toast toast = Toast.makeText(ctx, txt, duration);
            toast.show();

            // Log exception
            Log.e(TAG, "Exception encountered while inserting contact: " + e);
    }
}

批处理允许我们实现乐观并发控制,它可以让我们在执行修改事务时不去锁定数据库。要是用这个方法,我们先执行事务然后检查是否有其他修改发生。如果检测到不一致的变化,应该回滚事务在重新执行。

乐观并发控制在手机上非常有用,因为只有一个用户,极少发生对一个数据库的同时访问。因为锁没有被使用,所以没有时间浪费在加锁和等待锁的操作上。

为了使用乐观并发控制,我们在更新 ContactsContract.RawContacts 表中的数据时,要遵循以下步骤:

  1. 在获取其他数据时,一并将 VERSION 获取出来
  2. 通过 newAssertQuery(Uri) 方法来创建一个 ContentProviderOperation.Builder 对象。将 raw contact's _ID 附在 RawContacts.CONTENT_URI 后面来形成 URI参数。
  3. 调用 ContentProviderOperation.Builder 对象的 withValue() 方法,将得到的 VERSION 的值添加进来。
  4. 调用 ContentProviderOperation.BuilderwithExpectedCount() 方法来保证这个断言只检测一条数据记录。
  5. 调用 build() 创建 ContentProviderOperation 对象,并将这个对象加入到需要通过 applyBatch() 执行的 ArrayList 数组中去。
  6. 执行批处理。

如果在读取和修改某条记录的过程中,这条记录被其他程序修改了,那么这个断言 ContentProviderOperation 将会失败,事务将会回滚。此时你可以尝试重新执行或者采取其他措施。

下面的代码展示了通过 CursorLoader 完成一个 raw contact 数据查询后,如何创建一个断言 ContentProviderOperation

/*
 * The application uses CursorLoader to query the raw contacts table. The system calls this method
 * when the load is finished.
 */
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {

    // Gets the raw contact's _ID and VERSION values
    mRawContactID = cursor.getLong(cursor.getColumnIndex(BaseColumns._ID));
    mVersion = cursor.getInt(cursor.getColumnIndex(SyncColumns.VERSION));
}

...

// Sets up a Uri for the assert operation
Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, mRawContactID);

// Creates a builder for the assert operation
ContentProviderOperation.Builder assertOp = ContentProviderOperation.netAssertQuery(rawContactUri);

// Adds the assertions to the assert operation: checks the version and count of rows tested
assertOp.withValue(SyncColumns.VERSION, mVersion);
assertOp.withExpectedCount(1);

// Creates an ArrayList to hold the ContentProviderOperation objects
ArrayList ops = new ArrayList<ContentProviderOperationg>;

ops.add(assertOp.build());

// You would add the rest of your batch operations to "ops" here

...

// Applies the batch. If the assert fails, an Exception is thrown
try
    {
        ContentProviderResult[] results =
                getContentResolver().applyBatch(AUTHORITY, ops);

    } catch (OperationApplicationException e) {

        // Actions you want to take if the assert operation fails go here
    }

Retrieval and modification with intents

发送特定的 intent 可以通过设备的联系人应用间接接触 Contacts Provider.这些 intent 会启动联系人应用界面,用户可以在这些界面中完成联系人相关的工作。通过这种方式,用户可以:

  • 通过列表选择一个联系人在自己的应用中作处理
  • 编辑一个联系人
  • 增加某个账户下的 raw contact 数据
  • 删除联系人或者删除联系人的一些数据

如果用户想要增加或者修改联系人数据,可以将这些数据搜集起来作为 intent 的一部分。

当使用 intents 通过联系人应用来访问 Contacts Provider 时,不需要实现自己的 UI 代码和访问 provider 的代码。也不需要添加对 provider 的读写权限。联系人应用会将 provider 的读取权限委托给你,同时,是通过联系人应用来修改 provider,所以也不许要在自己的应用中添加写权限。

通过发送 intent 来访问 provider 的流程在 Content Provider Basics 文章中的 "Data access via intents."节有详细阐述。可以处理的 action, MIME 类型, 还有 data 值都在表格4中列出,当然额外的值你可以通过 putExtra() 来添加,他们在 ContactsContract.Intents.Insert 中被一一列出:

Table 4. Contacts Provider Intents.

Task Action Data MIME type Notes
从列表中选择一个联系人 ACTION_PICK One of: Not used 显示联系人列表或者联系人数据的列表,取决于设置的 URI 类型.

调用 startActivityForResult(),可以得到你所选择的 URI.这个URI的格式是将这一列的 LOOKUP_ID 附到表的 URI 后面。联系人应用在 activity 的生命周期中将读写权限委托给 URI. 详情请参考 Content Provider Basics.

新增 raw contact Insert.ACTION N/A RawContacts.CONTENT_TYPE, MIME type for a set of raw contacts. 调出联系人应用的新增联系人界面,附在 intent 中的数据将会显示出来。如果调用 startActivityForResult() 来新增联系人,完成后返回将调用 onActivityResult() 方法,这个方法有一个 Intent 参数,可以通过调用Intent参数的 getData() 方法来获取返回值。
编辑联系人 ACTION_EDIT 联系人的 CONTENT_LOOKUP_URI. 编辑界面允许用户去修改所有和这个联系人相关的数据。 Contacts.CONTENT_ITEM_TYPE, a single contact. 调出联系人应用的编辑界面,添加在 intent 中的数据可以显示出来。当点击 完成 来保存修改后,你的 activity 就会被显示出来。
Display a picker that can also add data. ACTION_INSERT_OR_EDIT N/A CONTENT_ITEM_TYPE 这个 intent 会调出联系人应用的选择界面,用户可以选择一个人来编辑,也可一新增一个联系人。(by汉尼拔罗比:原文有缺失,这边我用自己的语言来描述一下。 比如我们从通话记录那边,长按一个电话号码,选择将它添加到联系人。这时候使用的就是这个 Intent,我们可以将这个号码存给一个已经存在的联系人,也可以新建一个联系人来存这个号码。 就是所谓的 edit or insert).

Note: 没有必要将姓名附加到intent中,因为用户只能选择一个已经存在的联系人或者新建一个。并且当用户选择编辑一个联系人时,intent中附加的姓名将会覆盖这个联系人中原来的姓名。如果用户没注意到这一点,原来的姓名就丢失了。

可以注意到,并没有提供删除联系人的 intent, 可以通过 ContentResolver.delete() 或者 ContentProviderOperation.newDelete() 这两个方法来删除 raw contact 数据。

下面的代码展示了如何构造并发送一个 intent,来新增一个联系人:

// Gets values from the UI
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();

// Creates a new intent for sending to the device's contacts application
Intent insertIntent = new Intent(ContactsContract.Intents.Insert.ACTION);

// Sets the MIME type to the one expected by the insertion activity
insertIntent.setType(ContactsContract.RawContacts.CONTENT_TYPE);

// Sets the new contact name
insertIntent.putExtra(ContactsContract.Intents.Insert.NAME, name);

// Sets the new company and job title
insertIntent.putExtra(ContactsContract.Intents.Insert.COMPANY, company);
insertIntent.putExtra(ContactsContract.Intents.Insert.JOB_TITLE, jobtitle);

/*
 * Demonstrates adding data rows as an array list associated with the DATA key
 */

// Defines an array list to contain the ContentValues objects for each row
ArrayList<ContentValues> contactData = new ArrayList<ContentValues>();


/*
 * Defines the raw contact row
 */

// Sets up the row as a ContentValues object
ContentValues rawContactRow = new ContentValues();

// Adds the account type and name to the row
rawContactRow.put(ContactsContract.RawContacts.ACCOUNT_TYPE, mSelectedAccount.getType());
rawContactRow.put(ContactsContract.RawContacts.ACCOUNT_NAME, mSelectedAccount.getName());

// Adds the row to the array
contactData.add(rawContactRow);

/*
 * Sets up the phone number data row
 */

// Sets up the row as a ContentValues object
ContentValues phoneRow = new ContentValues();

// Specifies the MIME type for this data row (all data rows must be marked by their type)
phoneRow.put(
        ContactsContract.Data.MIMETYPE,
        ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE
);

// Adds the phone number and its type to the row
phoneRow.put(ContactsContract.CommonDataKinds.Phone.NUMBER, phone);

// Adds the row to the array
contactData.add(phoneRow);

/*
 * Sets up the email data row
 */

// Sets up the row as a ContentValues object
ContentValues emailRow = new ContentValues();

// Specifies the MIME type for this data row (all data rows must be marked by their type)
emailRow.put(
        ContactsContract.Data.MIMETYPE,
        ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE
);

// Adds the email address and its type to the row
emailRow.put(ContactsContract.CommonDataKinds.Email.ADDRESS, email);

// Adds the row to the array
contactData.add(emailRow);

/*
 * Adds the array to the intent's extras. It must be a parcelable object in order to
 * travel between processes. The device's contacts app expects its key to be
 * Intents.Insert.DATA
 */
insertIntent.putParcelableArrayListExtra(ContactsContract.Intents.Insert.DATA, contactData);

// Send out the intent to start the device's contacts app in its add contact activity.
startActivity(insertIntent);

Data integrity

由于联系人数据是非常重要并且敏感的数据,所以用户期望这些数据是正确的,Contacts Provider 为了保证数据的完整性定义了一些规则。当用户修改联系人数据时,你要确保所做的操作都能够满足这些规则。下面是几个比较重要的规则:

要给每一个 ContactsContract.RawContacts 记录都添加 ContactsContract.CommonDataKinds.StructuredName 数据。
一个没有 ContactsContract.CommonDataKinds.StructuredName 数据的 ContactsContract.RawContacts 记录在与其他 raw contact 进行组合时可能会遇到问题。
Always link new ContactsContract.Data rows to their parent ContactsContract.RawContacts row.
每一条 ContactsContract.Data 记录都必须要链接到一条 ContactsContract.RawContacts 记录上去。一个没有链接到 ContactsContract.RawContacts 记录上的 ContactsContract.Data 记录将永远不会显示在联系人应用中,并且可能会影响同步适配器的工作。
仅修改属于你自己 raw contact 的数据
Contacts Provider 通常管理着几个账户类型的数据。你需要确保你的应用值修改属于你自己的数据,只向自己的账户下新增数据。
开发时总是使用 ContactsContract 类中定义的常量来表示authorities、content URIs、URI paths、MIME、以及 TYPE 值。
使用这些常量可以帮助你避免一些错误的发生。编译器将会对那些废除的常量给出警告。

Custom data rows

通过定义自己的 MIME 类型,我们可以向 ContactsContract.Data 表中添加自己的数据类型。尽管你可以为你的数据类型定义一些特别的列名(比如phone、Email),但是还是只能使用定义在 ContactsContract.DataColumns 中的列。在联系人应用中,用户只能显示出你定义的数据类型,但是无法编辑、删除、增加它,所以需要在你自己的应用中实现这样一个界面来完成这些操作。

为了显示自定义的数据类型,需要提供一个 contacts.xml 文件,这个文件中得有一个 <ContactsAccountType> 元素和它的子元素 <ContactsDataKind>.这将在 <ContactsDataKind> element 中详细描述。

需要了解更多关于自定义 MIME 类型的知识,请参考 Creating a Content Provider.

Contacts Provider Sync Adapters

Contacts Provider 为了处理设备和服务器之间的同步做了特别的设计。允许用户从服务器下载数据,并且也可以上传数据。同步使得设备端总能有最新的数据,不管数据源发生什么变化。同步的另一个好处是即使没有联网,设备还是能够使用服务器上存在的数据(当然是同步号的数据)。

尽管你可以有很多办法来实现同步,Android 提供了一个插件式的同步框架,它可以自动完成下面的工作:

  • 检查网络是否可用
  • 按照用户偏好,周期性地执行同步
  • 重新启动停止的同步操作

想要使用这个框架,应该实现一个同步适配器插件。每一个同步适配器都对应一个服务器和一个 provider,但是每一个服务器下可以有多个账户名。这个框架下也允许有一个服务器和 provider 对应多个同步适配器。

Sync adapter classes and files

通过继承 AbstractThreadedSyncAdapter 来实现同步适配器,并将它作为应用程序的一部分。系统从你应用的 manifest 文件指向的一个特殊的 XML 文件来了解同步适配器。这个XML文件定义了账户类型和 provider 的 authority,这两部分组合起来就可以标识一个同步适配器。只有当用户添加这个账户类型的账户并且启用同步功能时,这个同步适配器才会被激活。激活后,系统开始管理同步适配器,当设备端和服务端需要同步数据时,同步适配器就开始工作。

Note: 将账户类型当作同步适配器标识的一部分有助于系统去检测和分类同一账户类型下不同账户的适配器。举个例子,google 所有服务器都用有相同的账户类型 com.google.当用户在设备中添加一个 google 账户时,所有 google 服务的同步适配器都被归纳到一起;每一个同步适配器对应着设备上一个 provider.

大多数服务器在用户获取数据时都需要进行身份验证,Android 系统提供了一个与同步适配器类似的框架,也就是认证框架,它常常与同步适配器框架结合使用。可以继承 AbstractAccountAuthenticator 来实现这个认证框架,它通过下面步骤完成用户的身份认证:

  1. 获取用户的姓名,密码以及类似的信息(用户证书
  2. 将获取的信息发送给服务器
  3. 解析服务器的返回值

If the service accepts the credentials, the authenticator can store the credentials for later use. Because of the plug-in authenticator framework, the AccountManager can provide access to any authtokens an authenticator supports and chooses to expose, such as OAuth2 authtokens(by汉尼拔萝卜:这一句我不太懂,这边就不翻译了).

尽管身份认证不是必须的,但是大多数联系人服务器都在使用。然而,你也不一定非要使用 Android 提供的框架来完成身份认证操作。

Sync adapter implementation

要 Contacts Provider 实现同步适配器,应用程序中需要包含下面的组建:

一个 Service 用来响应系统绑定同步适配器的需求。
当系统想要执行同步时,它会调用这个 service 的 onBind()方法来得到一个同步适配器的 IBinder 对象,这样的实现方法可以允许系统跨进程调用这个适配器的方法。

In the Sample Sync Adapter sample app, the class name of this service is com.example.android.samplesync.syncadapter.SyncService.

通过继承 AbstractThreadedSyncAdapter 实现同步适配器类的编写。
这个类去完成从服务端下载数据、将本地数据上传、并且解决冲突的工作。这个适配器最主要的工作都在 onPerformSync() 方法中完成,并且这个类一定要设计成单例模式。

In the Sample Sync Adapter sample app, the sync adapter is defined in the class com.example.android.samplesync.syncadapter.SyncAdapter.

Application的一个子类。
这个类的功能相当于同步适配器类的工厂类,使用它的 onCreate() 方法来初始化同步适配器类,并且提供一个静态的"getter"方法来返回适配器的实例,"getter" 将在同步适配器的服务的 onBind() 方法中使用。
Optional: 用来响应系统进行用户认证的 Service.
AccountManager 启动这个服务来进行身份认证操作。这个 service的 onCreate() 方法中初始化一个认证对象。当系统想要进行认证操作时,调用 onBind() 方法来得到一个关于这个对象的 IBinder,这样做允许系统进行跨进程调用认证器的方法。

In the Sample Sync Adapter sample app, the class name of this service is com.example.android.samplesync.authenticator.AuthenticationService.

Optional:AbstractAccountAuthenticator 的一个子类,用来处理身份认证的结果。
AccountManager调用这个类中的方法来认证用户信息。认证过程的细节特别多,所以在进行身份认证之前应该仔细查阅服务器提供的文档。

In the Sample Sync Adapter sample app, the authenticator is defined in the class com.example.android.samplesync.authenticator.Authenticator.

定义同步适配器和身份验证的 XML 文件。
同步适配器服务和身份认证服务被定义在 manifest 文件中的 <service> 元素下。这些元素包含了 <meta-data> 子元素,这些子元素像系统提供了这些 XML 文件:
  • 同步适配器的 <meta-data> 指向了res/xml/syncadapter.xml,这个文件中定义了需要同步的 Contacts Provider 的 Authority,以及账户类型。
  • Optional: 身份认证服务的 <meta-data> 指向 res/xml/authenticator.xml,这个文件定义了可以通过认证的账户类型,以及认证过程中显示的 UI 图片。这边的账户类型一定要和同步适配器中的账户类型一样。

Social Stream Data(by汉尼拔萝卜:关于social这一篇,我在开发中没有接触过,如果觉得有不准确的地方,尽量去参考原文内容)

ContactsContract.StreamItems 表和 ContactsContract.StreamItemPhotos 表管理着来自与社交网络的数据。可以编写同步适配器来将网络上的数据添加到这两张表中,也可以将表中的数据显示在自己的应用中。这个功能可以将社交网络服务和应用集成到Android的社交网络体验中来。

Social stream text

社交数据总是与一个 raw contact 相关,通过 RAW_CONTACT_ID 与 raw contact 的 _ID列的链接,将社交记录链接到对应的 raw contact 记录上。账户类型和账户名也存储在社交数据记录中。

在下面的列中存储社交数据:

ACCOUNT_TYPE
Required. 这条 raw contact 记录的账户类型,当插入你社交数据时,一定要添加这个值。
ACCOUNT_NAME
Required. 这条 raw contact 记录的账户名,当插入你社交数据时,一定要添加这个值。
标识符列
Required. 当插入社交数据时,这些列的值一定要有:
COMMENTS
可选的. 可以用来显示在这个流数据的最开始的简要信息。
TEXT
这条记录的文本内容,要么是这条记录需要发送的内容,要么是来描述生成这条记录原因的内容。这个内容可以是任意格式的文本。
TIMESTAMP
用来记录这条社交数据插入或者修改的时间,以毫秒格式的数据呈现。插入数据或者修改数据时,要自行维护这一列,Contacts Provider 不会自动维护这一列的内容。

在你的应用中,应该使用 RES_ICON, RES_LABEL, 和 RES_PACKAGE 来表示列名。

ContactsContract.StreamItems 表中还存储有 SYNC1SYNC4 列的内容供同步适配器使用。

Social stream photos

ContactsContract.StreamItemPhotos 表中存储的头像都与每一条社交记录关联在一起。这张表的 STREAM_ITEM_ID 列链接到 ContactsContract.StreamItems 表的 _ID 列。照片引用被存储在这些列中:

PHOTO column (a BLOB).
照片的二进制表示,provider 为了方便存储和显示重新定义了它的大小。这一列是向后兼容的,然而在当前版本不应该使用这一列来存储照片。应该使用 PHOTO_FILE_ID 或者 PHOTO_URI 列来存储照片。这一列现在用来存储照片的缩略图。
PHOTO_FILE_ID
raw contact 的照片数字标识,将这一列的值附在 DisplayPhoto.CONTENT_URI 后面就可以得到一个指向照片的 URI,通过code>openAssetFileDescriptor() 就可以得到照片文件。
PHOTO_URI
一个指向这一列数据使用的照片的 URI,可以直接通过 openAssetFileDescriptor() 来获取到照片文件

Using the social stream tables

这些表格和 Contacts Provider 中的其他表格完全一样,除了以下几点:

  • 访问这些表格需要额外的权限,读权限是 READ_SOCIAL_STREAM,写权限是 code>WRITE_SOCIAL_STREAM.
  • ContactsContract.StreamItems 表中,为每一个 raw contact 存储的记录是有限制的,一旦达到这个限制值,Contacts Provider 将会删除那些 TIMESTAMP值比较老的列。想得到这个限制值,可以查询 CONTENT_LIMIT_URI 这个 URI,查询时其他参数都设为 null.得到的 Cursor 只包含一条记录,这条记录也只有一列,这一列就是MAX_ITEMS.

ContactsContract.StreamItems.StreamItemPhotos 类中定义了一张 ContactsContract.StreamItemPhotos 的子表来存储照片数据。

Social stream interactions

这叫数据被 Contacts Provider 管理着,它和联系人应用一起提供了非常有用的方法来将社交系统和联系人结合起来。可以使用下面几个功能:

  • 使用同步适配器来同步社交网络服务和 Contacts Provider,我们可以将最近使用的 activity 信息存储在 ContactsContract.StreamItems 表和 ContactsContract.StreamItemPhotos 表中已备下次使用。
  • 除了常规的同步外,当用户选择查看一个联系人时还可以触发同步适配器来获取更多的信息。着允许用户获取到高分辨率的头像以及最近经常使用的信息。
  • 当用户查看一个联系人时,可以注册一个 notification 来提示用户从服务端更新这个联系人最新的信息。与一次完整的同步相比,这样做更快,并且占用的带宽更少。
  • 用户可以通过联系人应用来向社交网络中增加联系人。可以实现一个“邀请”功能,要实现这个功能需要一个新增社交网络联系人的 activity,提供联系人应用的 XML 文件,以及Contacts Provider.

常规同步社交数据的操作与普通同步一样,需要了解更多同步相关的知识,参考 Contacts Provider Sync Adapters.注册 notification 和 邀请联系人在下面两节介绍。

Registering to handle social networking views

为了让你的同步适配器能够在用户查看联系人时接受到 notification 需要这样做:

  1. 在你项目的 res/xml/ 目录下创建 contacts.xml 文件。如果有这个文件了,跳过这步。
  2. 在这个文件中,增加一个 <ContactsAccountType xmlns:android="http://schemas.android.com/apk/res/android"> 元素,如果已经存在,跳过这一步。
  3. 为这个元素添加 viewContactNotifyService="serviceclass" 属性,serviceclass 的内容是服务的完全修饰名。通过继承 IntentService 来实现这个服务,这个服务接受的 Intent 中含有用户点击的联系人的 URI,你可以绑定这个服务然后调用同步适配器来更新数据。

当用户点击社交数据或者头像时,注册这样一个 activity:

  1. 在你项目的 res/xml/ 目录下创建 contacts.xml 文件。如果有这个文件了,跳过这步。
  2. 在这个文件中,增加一个 <ContactsAccountType xmlns:android="http://schemas.android.com/apk/res/android"> 元素,如果已经存在,跳过这一步。
  3. 要注册处理点击联系人社交数据产生的Intent的activity,为这个元素添加 viewStreamItemActivity="activityclass" 属性,activityclass 是这个 activity 的全修饰名称。
  4. 要注册处理点击联系人社交头像时产生的Intent的 activity,为这个元素添加 viewStreamItemPhotoActivity="activityclass"属性,activityclass 是这个 activity 的全修饰名称。

<ContactsAccountType> 元素将在 <ContactsAccountType> element 详细阐述.

传过来的 intent 中含有社交文本数据或者社交头像数据,为了区分开来,将这两个属性定义在同一个文件中。

Interacting with your social networking service

用户不必离开联系人应用就可以将联系人邀请到社交网络上。可以通过联系人应用应用发送 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 菜单的字符串。

Note: ContactsSource is a deprecated tag name for ContactsAccountType.

contacts.xml reference

contacts.xml 文件包含了控制同步适配器、联系人应用、Contacts Provider之间互动的 XML 元素。这些元素在下面章节中描述.

<ContactsAccountType> element

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

contained in:

res/xml/contacts.xml

can contain:

<ContactsDataKind>

Description:

描述 Android 组件和 UI 标签,它允许用户邀请联系人加入到他们的社交网络中去,当社交数据更新时通知用户,以及其他的功能。

<ContactsAccountType> 的属性中 android: 前缀不是必须要有的.

Attributes:

inviteContactActivity
当用户选择联系人中 Add connection 菜单时,需要调用出来的 activity 全修饰名称。
inviteContactActionLabel
用来显示在 inviteContactActivity 中和 Add connection 菜单上的字符串。举个例子,可以将字符串定义为"Follow in my network",也可以使用一个字符串资源来表示这个标签。
viewContactNotifyService
当用户查看联系人时,你的应用中用来接收 notification 的 service. 这个 notification 是联系人应用发出来的,它允许我们将一些数据密集型的操作推迟到需要的时候再去获取。举个例子,你的应用可以通过获取和显示联系人高清头像和最近常用的社交数据来响应这个 notification. 这个功能在 Social stream interactions 章节中描写得更加详细。你可以在 SampleSyncAdapter 这个示例应用中的 NotifierService.java 文件中看到使用 notification 的示例。
viewGroupActivity
可以显示联系人群组信息的 activity,当用户点击联系人的群组标签时,这个 activity 应该显示出来。
viewGroupActionLabel
联系人应用显示的UI标签,这个标签允许用户在你的应用中查看群组。

举个例子,如果你安装 Google+ 并且将它与你的联系人应用同步,你可以在联系人的应用的 Groups 界面里看到 Google+的社交圈子。如果你点击它,会看到 Google+中的联系人以群组的形式呈现出来。在显示界面顶端,会有 Google+的图标,如果点击它,会跳转到 Google+应用。联系人通过 viewGroupActivity 来完成这些操作,使用 Google+ 的图标当做 viewGroupActionLabel.

A string resource identifier is allowed for this attribute.

viewStreamItemActivity
当用户点击一个 raw contact 的社交文本时,需要启动的 activity 全修饰名称。
viewStreamItemPhotoActivity
当用户点击一个 raw contact 的社交头像时,需要启动的 activity 全修饰名称。

<ContactsDataKind> element

<ContactsDataKind> 元素控制着你自定义的类型在联系人应用 UI中的显示,它有以下内容:

<ContactsDataKind
        android:mimeType="MIMEtype"
        android:icon="icon_resources"
        android:summaryColumn="column_name"
        android:detailColumn="column_name">

contained in:

<ContactsAccountType>

Description:

使用这个元素可以使联系人应用在详情界面显示出自定义的数据类型。<ContactsAccountType> 元素下的,每一个子元素豆代表着同步适配器往 ContactsContract.Data 表中新增的数据类型。每自定义一个 MIME 类型,就需要添加一个 <ContactsDataKind> 元素。如果你只是新增一个 MIME 类型,但是并不想它显示出来,那么可以不用添加这个元素。

Attributes:

android:mimeType
给 data 记录的定义的 MIME 类型,举个例子,ContactsContract.Data 这个类型可以用来说明这条 data 记录存储的是联系人上一次出现的地点。
android:icon
在联系人详情界面,显示在自定义 MIME类型数据旁边的图像资源,可以使用它来说明这个数据来源于网络。
android:summaryColumn
用来描述从这个 data 记录获取到的两个值中的第一个,这个值显示在联系人详情中该数据类型的第一行,是用来描述这个类型数据的,不一定非要有。
android:detailColumn
用来描述从这个 data 记录获取到的两个值中的第二个,这个值显示在联系人详情中该数据类型的第二行。

Additional Contacts Provider Features

除了前面章节中提到的功能外,Contacts Proider 还提供一些其他有用的功能:

  • 联系人群组
  • 联系人头像

Contact groups

Contacts Provider 能够通过群组功能将联系人进行分类。如果一个账户类型的下的联系人需要实现群组功能,同步适配器应该同步与群组有关的数据。如果用户新增一个联系人和群组,并且将这个联系人添加到群组中去,那么同步适配器应该在 ContactsContract.Groups 表中新增一条记录。一个raw contact 的群组数据存储在 ContactsContract.Data 表中数据类型为 ContactsContract.CommonDataKinds.GroupMembership 的记录中。

如果你的同步适配器需要往 Contacts Provider 中新增 raw contact 数据,并且没有实现群组功能,那么你就需要让 provider 将这些联系人显示出来。在我们创建这个账户时,需要将 code>ContactsContract.Settings 表中对应记录的 Settings.UNGROUPED_VISIBLE 这一列值设为1(前面有讲过了)。这样做后,就算没有群组功能的账户,联系人也可以显示出来了。

Contact photos

ContactsContract.Data表中,头像数据的存储类型是Photo.CONTENT_ITEM_TYPE.与其他数据一样,头像数据记录的 CONTACT_ID 列链接到 raw contact 的_ID列。ContactsContract.Contacts.Photo类定义了 ContactsContract.Contacts 表中带有头像数据的子表,它的内容是联系人的默认头像,也就是这个联系人默认 raw contact 记录的默认头像(by汉尼拔萝卜:一个联系人对应多个 raw contact,一个 raw contact 可以有多个头像数据,其中一个是 primary 数据)。类似的,ContactsContract.RawContacts.DisplayPhoto 类定义了 ContactsContract.RawContacts 表中带有头像数据的子表,它的内容是 raw contact 记录的默认头像。

ContactsContract.Contacts.PhotoContactsContract.RawContacts.DisplayPhoto 这两篇文章包含了获取头像数据的示例。没方便的类来获取 raw contact 的头像数据,但是我们可以通过 raw contact's _IDPhoto.CONTENT_ITEM_TYPEIS_PRIMARY 这三个少选条件来查询 ContactsContract.Data 表以获取这个 raw conatct 的默认头像。

一个联系人的社交数据可能也含有头像,他们存储在 ContactsContract.StreamItemPhotos 表中,这一点在 Social stream photos 中有讲解过了。