联系人提供程序

联系人提供程序是一个功能强大且灵活的 Android 组件,用于管理设备上有关人员数据的中央存储库。联系人提供程序是您在设备的通讯录应用中看到的数据的来源,您也可以在自己的应用中访问其数据,并在设备和在线服务之间传输数据。该提供程序可容纳各种数据源,并尝试为每个人管理尽可能多的数据,因此其组织结构非常复杂。因此,提供程序的 API 包含一组丰富的协定类和接口,可同时协助数据检索和修改。

本指南介绍了以下内容:

  • 基本提供程序结构。
  • 如何从提供程序检索数据。
  • 如何修改提供程序中的数据。
  • 如何编写同步适配器,以便将数据从服务器同步到联系人提供程序。

本指南假定您了解 Android content provider 的基础知识。如需详细了解 Android content provider,请参阅 content provider 基础知识指南。

联系人提供程序组织

联系人提供程序是一个 Android content provider 组件。它会维护有关人员的三种类型的数据,每种类型的数据都对应于提供程序提供的一个表,如图 1 所示:

图 1. 联系人提供程序表结构。

这三个表通常通过其协定类的名称来引用。这些类为表使用的内容 URI、列名称和列值定义了常量:

ContactsContract.Contacts 个表
根据原始联系人行的汇总,代表不同人员的行。
ContactsContract.RawContacts 个表
包含个人数据摘要的行,特定于用户账号和类型。
ContactsContract.Data 个表
包含原始联系人详细信息(例如电子邮件地址或电话号码)的行。

ContactsContract 中由协定类表示的其他表是联系人提供程序用于管理其 IT 运维或支持设备联系人或电话应用中特定功能的辅助表。

原始联系人

原始联系人表示来自单一账号类型和账号名称的用户数据。由于联系人提供程序允许将多个在线服务作为某位联系人的数据来源,因此联系人提供程序允许同一联系人有多个原始联系人。 多个原始联系人还允许用户合并来自同一账号类型的多个账号中的联系人数据。

原始联系人的大部分数据不存储在 ContactsContract.RawContacts 表中。而是存储在 ContactsContract.Data 表中的一行或多行中。每个数据行都有一个 Data.RAW_CONTACT_ID 列,其中包含其父级 ContactsContract.RawContacts 行的 RawContacts._ID 值。

重要的原始联系人列

表 1 列出了 ContactsContract.RawContacts 表中的重要列。请阅读表格后面的说明:

表 1. 重要的原始联系人列。

列名 使用 备注
ACCOUNT_NAME 作为此原始联系人来源的账号类型的账号名称。 例如,Google 账号的账号名称是设备所有者的 Gmail 邮箱之一。如需了解详情,请参阅 ACCOUNT_TYPE 的下一个条目。 此名称的格式因账号类型而异。不一定是电子邮件地址。
ACCOUNT_TYPE 此原始联系人的来源账号类型。例如,Google 账号的账号类型为 com.google。请始终使用您拥有或控制的网域的网域标识符来限定您的账号类型。这样可以确保您的账号类型是唯一的。 提供联系人数据的账号类型通常具有一个关联的同步适配器,该适配器会与联系人提供程序同步。
DELETED 原始联系人的“已删除”标志。 此标志允许联系人提供程序在内部维护该行,直到同步适配器能够从其服务器中删除该行,然后最终从代码库中删除该行。

备注

以下是关于 ContactsContract.RawContacts 表的重要说明:

  • 原始联系人的姓名不存储在 ContactsContract.RawContacts 中其所在的行内。而是存储在 ContactsContract.Data 表的 ContactsContract.CommonDataKinds.StructuredName 行中。在 ContactsContract.Data 表中,原始联系人只有一行此类型的数据。
  • 注意:如需在原始联系人行中使用您自己的账号数据,必须先向 AccountManager 注册该数据。为此,请提示用户将账号类型和账号名称添加到账号列表中。如果您不这样做,联系人提供程序会自动删除您的原始联系人行。

    例如,如果您希望应用为网域为 com.example.dataservice 的基于 Web 的服务维护联系人数据,并且用户在您的服务中的账号为 becky.sharp@dataservice.example.com,则用户必须先添加账号“类型”(com.example.dataservice) 和账号“名称”(becky.smart@dataservice.example.com),然后您的应用才能添加原始联系人行。您可以在文档中向用户说明此要求,也可以提示用户添加类型和/或名称。下一部分将更详细地介绍账号类型和账号名称。

原始联系人数据来源

为了解原始联系人的工作原理,我们以用户“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)。

联系人提供程序会因此创建三个原始联系人:

  1. 与 emily.dickinson@gmail.com 关联的“Thomas Higginson”原始联系人。 用户账号类型为 Google。
  2. 与 emilyd@gmail.com 关联的“Thomas Higginson”的第二个原始联系人。 用户账号类型也是 Google。即使姓名与之前的姓名相同,也会有第二个原始联系人,因为该联系人是为其他用户账号添加的。
  3. 与“belle_of_amherst”关联的“Thomas Higginson”的第三个原始联系人。用户账号类型为 Twitter。

数据

如前所述,原始联系人的数据存储在 ContactsContract.Data 行中,该行与原始联系人的 _ID 值相关联。这样,单个原始联系人便可拥有多个相同类型的数据实例,例如电子邮件地址或电话号码。例如,如果 emilyd@gmail.com 的“Thomas Higginson”(与 Google 账号 emilyd@gmail.com 关联的 Thomas Higginson 的原始联系人行)具有家庭邮箱 thigg@gmail.com 和工作邮箱 thomas.higginson@gmail.com,则联系人提供程序会存储这两个邮箱行,并将它们都关联到原始联系人。

请注意,此表中存储了不同类型的数据。显示名称、电话号码、电子邮件、邮政地址、照片和网站详细信息行都位于 ContactsContract.Data 表中。为了帮助管理此问题,ContactsContract.Data 表中的某些列具有描述性名称,而其他列则具有通用名称。无论行中的数据类型如何,描述性名称列的内容都具有相同的含义,而通用名称列的内容则根据数据类型而具有不同的含义。

描述性列名称

以下是一些描述性列名称示例:

RAW_CONTACT_ID
此数据的原始联系人的 _ID 列的值。
MIMETYPE
此行中存储的数据类型,表示为自定义 MIME 类型。联系人提供程序使用 ContactsContract.CommonDataKinds 的子类中定义的 MIME 类型。这些 MIME 类型是开源的,可供任何与联系人提供程序搭配使用的应用或同步适配器使用。
IS_PRIMARY
如果一个原始联系人可以有多个此类数据行,则 IS_PRIMARY 列会标记包含该类型主要数据的数据行。例如,如果用户长按某个联系人的电话号码并选择设为默认,则包含该号码的 ContactsContract.Data 行会将其 IS_PRIMARY 列设置为非零值。

通用列名称

有 15 个名为 DATA1 到 DATA15 的通用列可供使用,另外还有 4 个通用列 SYNC1 到 SYNC4 仅供同步适配器使用。无论行包含的数据类型如何,通用列名称常量始终有效。

DATA1 列已编入索引。联系人提供程序始终将此列用于其预计最常作为查询目标的数据。例如,在电子邮件行中,此列包含实际的电子邮件地址。

按照惯例,DATA15 列预留用于存储二进制大型对象 (BLOB) 数据,例如照片缩略图。

类型专用列名称

为便于处理特定类型行的列,联系人提供程序还提供了在 ContactsContract.CommonDataKinds 的子类中定义的特定于类型的列名称常量。这些常量只是为同一列名称指定了不同的常量名称,这有助于您访问特定类型的行中的数据。

例如,ContactsContract.CommonDataKinds.Email 类为 MIME 类型为 Email.CONTENT_ITEM_TYPE 的 ContactsContract.Data 行定义了特定于类型的列名称常量。该类包含电子邮件地址列的常量 ADDRESS。ADDRESS 的实际值为“data1”,与列的通用名称相同。

注意:请勿使用具有提供程序预定义 MIME 类型的行将您自己的自定义数据添加到 ContactsContract.Data 表中。否则,您可能会丢失数据或导致提供程序发生故障。例如,您不应添加 MIME 类型为 Email.CONTENT_ITEM_TYPE 且在 DATA1 列中包含用户名(而非电子邮件地址)的行。如果您为行使用自己的自定义 MIME 类型,则可以自由定义自己的类型专用列名称,并随意使用这些列。

图 2 显示了描述性列和数据列在 ContactsContract.Data 行中的显示方式,以及特定于类型的列名称如何“叠加”通用列名称

特定于类型的列名称如何映射到通用列名称

图 2. 特定于类型的列名称和通用列名称。

特定于类型的列名称类

表 2 列出了最常用的类型专用列名称类:

表 2. 特定于类型的列名称类

映射类 数据类型 备注
ContactsContract.CommonDataKinds.StructuredName 与此数据行关联的原始联系人的姓名数据。 原始联系人只有其中一行。
ContactsContract.CommonDataKinds.Photo 与此数据行关联的原始联系人的主照片。 原始联系人只有其中一行。
ContactsContract.CommonDataKinds.Email 与此数据行关联的原始联系人的邮箱。 一个原始联系人可以有多个电子邮件地址。
ContactsContract.CommonDataKinds.StructuredPostal 与此数据行关联的原始联系人的邮政地址。 一个原始联系人可以有多个邮政地址。
ContactsContract.CommonDataKinds.GroupMembership 一个标识符,用于将原始联系人与联系人提供程序中的某个群组相关联。 群组是账号类型和账号名称的可选功能。联系人群组部分对此进行了更详细的介绍。

通讯录

联系人提供程序会将所有账号类型和账号名称中的原始联系人行合并,以形成一个联系人。这样可以更轻松地显示和修改用户为某人收集的所有数据。联系人提供程序负责管理新联系人行的创建,以及原始联系人与现有联系人行的汇总。应用和同步适配器都不能添加联系人,并且联系人行中的某些列是只读的。

注意:如果您尝试使用 insert() 向联系人提供程序添加联系人,则会收到 UnsupportedOperationException 异常。如果您尝试更新列为“只读”的列,系统会忽略该更新。

联系人提供程序会创建新联系人,以响应添加与任何现有联系人都不匹配的新原始联系人。如果现有原始联系人的数据发生变化,导致其不再与之前关联的联系人匹配,提供程序也会执行此操作。如果应用或同步适配器创建的新原始联系人与现有联系人匹配,则新原始联系人会汇总到现有联系人中。

联系人提供程序通过 Contacts 表中联系人行的 _ID 列将联系人行与其原始联系人行关联起来。原始联系人表 ContactsContract.RawContacts 的 CONTACT_ID 列包含与每个原始联系人行关联的联系人行的 _ID 值。

ContactsContract.Contacts 表还包含 LOOKUP_KEY 列,该列是联系人行的“永久”链接。由于联系人提供程序会自动维护联系人,因此它可能会更改联系人行的 _ID 值,以响应汇总或同步。即使发生这种情况,内容 URI CONTENT_LOOKUP_URI 与联系人的 LOOKUP_KEY 组合后仍会指向联系人行,因此您可以使用 LOOKUP_KEY 来维护指向“收藏”联系人的链接,等等。此列有自己的格式,与 _ID 列的格式无关。

图 3 展示了三个主表之间的关系。

联系人提供程序主表

图 3. Contacts、原始联系人 和 Details 表之间的关系。

注意: 如果您将应用发布到 Google Play 商店,或者您的应用在搭载 Android 10(API 级别 29)或更高版本的设备上运行,请注意,有一组有限的通讯录数据字段和方法已过时。

在上述情况下,系统会定期清除写入以下数据字段的所有值:

用于设置上述数据字段的 API 也已废弃:

此外,以下字段不再返回常用联系人。请注意,其中某些字段仅在联系人属于特定数据类型时才会影响联系人的排名。

如果您的应用正在访问或更新这些字段或 API,请使用替代方法。例如,您可以使用私有 content provider 或应用/后端系统中存储的其他数据来实现某些用例。

如需验证应用的功能是否不受此变更的影响,您可以手动清除这些数据字段。为此,请在搭载 Android 4.1(API 级别 16)或更高版本的设备上运行以下 ADB 命令:

adb shell content delete \
--uri content://com.android.contacts/contacts/delete_usage

来自同步适配器的数据

用户直接将联系人数据输入到设备中,但数据也会通过同步适配器从 Web 服务流入联系人提供程序,同步适配器会自动在设备和服务之间传输数据。同步适配器在系统的控制下在后台运行,并调用 ContentResolver 方法来管理数据。

在 Android 中,同步适配器使用的网络服务由账号类型标识。 每个同步适配器都适用于一种账号类型,但它可以支持该类型的多个账号名称。原始联系人数据来源部分简要介绍了账号类型和账号名称。以下定义提供了更多详细信息,并介绍了账号类型和名称与同步适配器及服务之间的关系。

账号类型
标识用户在其中存储了数据的服务。大多数情况下,用户必须向服务进行身份验证。例如,Google 通讯录是一种账号类型,由代码 google.com 标识。此值对应于 AccountManager 使用的账号类型。
账号名称
标识特定账号或账号类型的登录信息。Google 通讯录账号与 Google 账号相同,其账号名称为邮箱。 其他服务可能会使用单字用户名或数字 ID。

账号类型不必是唯一的。用户可以配置多个 Google 通讯录账号,并将其数据下载到联系人提供程序;如果用户有一组个人联系人(对应个人账号名称)和另一组工作联系人,就可能会发生这种情况。账号名称通常是唯一的。它们共同标识联系人提供程序与外部服务之间的特定数据流。

如果您想将服务的数据传输到联系人提供程序,则需要编写自己的同步适配器。联系人提供程序同步适配器部分对此进行了更详细的说明。

图 4 显示了联系人提供程序如何融入人员数据流。在标有“同步适配器”的框中,每个适配器都按其账号类型进行标记。

人员数据流

图 4. 联系人提供程序的数据流。

所需权限

想要访问联系人提供程序的应用必须请求以下权限:

针对一个或多个表的读取权限
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">。

这些权限不适用于用户个人资料数据。用户画像及其所需权限将在下一部分用户画像中讨论。

请注意,用户的联系人数据属于个人敏感数据。用户关心自己的隐私,因此不希望应用收集有关他们或其联系人的数据。 如果您需要获取访问其通讯录数据的权限,但原因不明显,他们可能会给您的应用低评分,或者直接拒绝安装。

用户个人资料

ContactsContract.Contacts 表只有一行,其中包含设备用户的个人资料数据。此数据描述的是设备的 user,而不是用户的某个联系人。对于使用个人资料的每个系统,个人资料联系人行都会链接到原始联系人行。 每个个人资料原始联系人行都可以有多个数据行。用于访问用户个人资料的常量在 ContactsContract.Profile 类中提供。

访问用户个人资料需要特殊权限。除了读取和写入所需的 READ_CONTACTS 和 WRITE_CONTACTS 权限之外,访问用户个人资料还需要 android.Manifest.permission#READ_PROFILE 和 android.Manifest.permission#WRITE_PROFILE 权限,分别用于读取和写入访问。

请注意,您应将用户的个人资料视为敏感信息。android.Manifest.permission#READ_PROFILE 权限允许您访问设备用户的个人身份数据。请务必在应用说明中告知用户您需要用户个人资料访问权限的原因。

如需检索包含用户个人资料的联系人行,请调用 ContentResolver.query()。将内容 URI 设置为 CONTENT_URI,并且不提供任何选择条件。您还可以将此内容 URI 用作基本 URI,以检索个人资料的原生联系人或数据。例如,以下代码段会检索该个人资料的数据:

Kotlin

// Sets the columns to retrieve for the user profile
projection = arrayOf(
        ContactsContract.Profile._ID,
        ContactsContract.Profile.DISPLAY_NAME_PRIMARY,
        ContactsContract.Profile.LOOKUP_KEY,
        ContactsContract.Profile.PHOTO_THUMBNAIL_URI
)

// Retrieves the profile from the Contacts Provider
profileCursor = contentResolver.query(
        ContactsContract.Profile.CONTENT_URI,
        projection,
        null,
        null,
        null
)

Java

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

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

注意:如果您检索多个联系人行,并且想要确定其中一行是否为用户个人资料,请测试该行的 IS_USER_PROFILE 列。如果联系人是用户个人资料,则此列会设为“1”。

联系人提供程序元数据

联系人提供程序会管理用于跟踪代码库中联系人数据状态的数据。此代码库元数据存储在各种位置,包括原始联系人、数据和联系人表行、ContactsContract.Settings 表和 ContactsContract.SyncState 表。下表显示了其中每个元数据的影响:

表 3. 联系人提供程序中的元数据

表 列 值 含义
ContactsContract.RawContacts DIRTY “0”- 自上次同步以来未发生变化。 标记在设备上发生更改且必须同步回服务器的原始联系人。当 Android 应用更新某一行时,联系人提供程序会自动设置该值。

修改原始联系人或数据表的同步适配器应始终将字符串 CALLER_IS_SYNCADAPTER 附加到其使用的内容 URI。这可以防止提供程序将行标记为脏行。 否则,同步适配器修改看起来像是本地修改,并会发送到服务器,即使服务器是修改的来源也是如此。

“1”- 自上次同步以来已更改,需要同步回服务器。
ContactsContract.RawContacts VERSION 此行的版本号。 每当行或其相关数据发生更改时,联系人提供程序都会自动递增此值。
ContactsContract.Data DATA_VERSION 此行的版本号。 每当数据行发生更改时,联系人提供程序都会自动递增此值。
ContactsContract.RawContacts SOURCE_ID 一个字符串值,用于在创建此原始联系人的账号中唯一标识此原始联系人。 当同步适配器创建新的原始联系人时,此列应设置为原始联系人的服务器唯一 ID。当 Android 应用创建新的原始联系人时,应用应将此列留空。这会向同步适配器发出信号,指示其应在服务器上创建新的原始联系人,并获取 SOURCE_ID 的值。

具体而言,对于每种账号类型,来源 ID 必须是唯一的,并且在多次同步之间应保持稳定:

  • 唯一:账号的每个原始联系人都必须有自己的来源 ID。如果您不强制执行此操作,则会导致通讯录应用出现问题。 请注意,同一账号类型的两个原始联系人可能具有相同的来源 ID。例如,账号 emily.dickinson@gmail.com 的原始联系人“Thomas Higginson”可以与账号 emilyd@gmail.com 的原始联系人“Thomas Higginson”具有相同的来源 ID。
  • 稳定:来源 ID 是原始联系人的在线服务数据中永久存在的一部分。例如,如果用户从“应用”设置中清除通讯录存储空间并重新同步,则恢复的原始联系人应具有与之前相同的来源 ID。如果您不强制执行此操作,快捷方式将停止运行。
ContactsContract.Groups GROUP_VISIBLE “0”- 此群组中的联系人不应显示在 Android 应用界面中。 此列用于与允许用户隐藏特定群组中的联系人的服务器兼容。
“1”- 允许此群组中的联系人显示在应用界面中。
ContactsContract.Settings UNGROUPED_VISIBLE “0”- 对于此账号和账号类型,不属于任何群组的联系人对 Android 应用界面不可见。 默认情况下,如果联系人的原始联系人都不属于某个群组,则该联系人不可见(原始联系人的群组成员资格由 ContactsContract.Data 表中的一个或多个 ContactsContract.CommonDataKinds.GroupMembership 行指示)。 通过在 ContactsContract.Settings 表中为账号类型和账号设置此标志,您可以强制显示没有群组的联系人。 此标志的一个用途是显示不使用群组的服务器中的联系人。
“1”- 对于此账号和账号类型,不属于任何群组的联系人对应用界面可见。
ContactsContract.SyncState (全部) 使用此表存储同步适配器的元数据。 借助此表,您可以在设备上永久存储同步状态和其他与同步相关的数据。

联系人提供程序访问权限

本部分介绍了从联系人提供程序访问数据的准则,重点关注以下方面:

  • 实体查询。
  • 批量修改。
  • 使用 intent 进行检索和修改。
  • 数据完整性。

联系人提供程序同步适配器部分也更详细地介绍了如何通过同步适配器进行修改。

查询实体

由于联系人提供程序表按层次结构进行组织,因此检索一行以及与其关联的所有“子”行通常很有用。例如,如需显示某个人的所有信息,您可能需要检索单个 ContactsContract.Contacts 行的所有 ContactsContract.RawContacts 行,或单个 ContactsContract.RawContacts 行的所有 ContactsContract.CommonDataKinds.Email 行。为方便起见,联系人提供程序提供了实体构造,其作用类似于表之间的数据库联接。

实体就像一个表,由父表及其子表中的选定列组成。 查询实体时,您需要根据实体中可用的列提供投影和搜索条件。结果是一个 Cursor,其中包含与检索到的每个子表行对应的一行。例如,如果您查询 ContactsContract.Contacts.Entity 以获取联系人姓名以及该姓名对应的所有原始联系人的所有 ContactsContract.CommonDataKinds.Email 行,则会返回一个 Cursor,其中每个 ContactsContract.CommonDataKinds.Email 行对应一行。

实体可简化查询。使用实体,您可以一次性检索联系人或原始联系人的所有联系人数据,而不必先查询父表以获取 ID,然后再使用该 ID 查询子表。此外,联系人提供程序会在单个事务中处理针对实体的查询,从而确保检索到的数据在内部保持一致。

注意:实体通常不会包含父表和子表的所有列。如果您尝试使用不在实体的列名称常量列表中的列名称,则会收到 Exception。

以下代码段展示了如何检索联系人的所有原始联系人行。该代码段是一个较大应用的一部分,该应用有两个 activity,即“main”和“detail”。主 activity 会显示联系人行列表;当用户选择其中一行时,该 activity 会将其 ID 发送到详细信息 activity。详细信息 activity 使用 ContactsContract.Contacts.Entity 显示与所选联系人关联的所有原始联系人的所有数据行。

此代码段取自“detail”activity:

Kotlin

...
    /*
     * 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).
     */
    contactUri = Uri.withAppendedPath(
            contactUri,
            ContactsContract.Contacts.Entity.CONTENT_DIRECTORY
    )

    // Initializes the loader identified by LOADER_ID.
    loaderManager.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
    cursorAdapter = SimpleCursorAdapter(
            this,                       // the context of the activity
            R.layout.detail_list_item,  // the view item containing the detail widgets
            mCursor,                    // the backing cursor
            fromColumns,               // the columns in the cursor that provide the data
            toViews,                   // the views in the view item that display the data
            0)                          // flags

    // Sets the ListView's backing adapter.
    rawContactList.adapter = cursorAdapter
...
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
    /*
     * 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.
     */
    val projection: Array<String> = arrayOf(
            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.
     */
    val 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 CursorLoader(
            applicationContext, // The activity's context
            contactUri,        // 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.
    )
}

Java

...
    /*
     * 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).
     */
    contactUri = Uri.withAppendedPath(
            contactUri,
            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
    cursorAdapter = new SimpleCursorAdapter(
            this,                        // the context of the activity
            R.layout.detail_list_item,   // the view item containing the detail widgets
            mCursor,                     // the backing cursor
            fromColumns,                // the columns in the cursor that provide the data
            toViews,                    // the views in the view item that display the data
            0);                          // flags

    // Sets the ListView's backing adapter.
    rawContactList.setAdapter(cursorAdapter);
...
@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
            contactUri,              // 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.
}

加载完成后,LoaderManager 会调用 onLoadFinished() 的回调。此方法的其中一个传入参数是包含查询结果的 Cursor。在您自己的应用中,您可以从此 Cursor 获取数据,以显示数据或进一步处理数据。

批量修改

应尽可能在联系人提供程序中以“批处理模式”插入、更新和删除数据,方法是创建 ContentProviderOperation 对象的 ArrayList 并调用 applyBatch()。由于联系人提供程序会在单个事务中执行 applyBatch() 中的所有操作,因此您的修改绝不会使联系人代码库处于不一致的状态。批量修改还有助于同时插入原始联系人及其详细数据。

注意:如需修改单个原始联系人,请考虑向设备的通讯录应用发送 intent,而不是在您的应用中处理修改。使用 intent 检索和修改部分对此进行了更详细的介绍。

退让点

包含大量操作的批量修改可能会阻止其他进程,从而导致整体用户体验不佳。为了将您要执行的所有修改整理到尽可能少的单独列表中,同时防止这些修改阻塞系统,您应为一个或多个操作设置退让点。退让点是一个 ContentProviderOperation 对象,其 isYieldAllowed() 值设置为 true。当联系人提供程序遇到退让点时,它会暂停其工作以让其他进程运行,并关闭当前事务。当提供程序再次启动时,它会继续执行 ArrayList 中的下一个操作,并启动一个新事务。

退让点确实会导致每次调用 applyBatch() 时产生多个事务。因此,您应为一组相关行的最后一个操作设置退让点。例如,您应为一组操作中的最后一个操作设置退让点,该操作会添加原始联系人行及其关联的数据行,或者为与单个联系人相关的一组行设置退让点。

退让点也是原子操作的单位。两个退让点之间的所有访问要么作为一个整体成功,要么作为一个整体失败。如果您未设置任何退让点,则最小原子操作就是整批操作。如果您使用退让点,则可以防止操作降低系统性能,同时确保一部分操作是原子操作。

修改反向引用

当您将新的原始联系人行及其关联的数据行作为一组 ContentProviderOperation 对象插入时,必须通过插入原始联系人的 _ID 值作为 RAW_CONTACT_ID 值,将数据行链接到原始联系人行。不过,在为数据行创建 ContentProviderOperation 时,此值不可用,因为您尚未为原始联系人行应用 ContentProviderOperation。为了解决此问题,ContentProviderOperation.Builder 类提供了 withValueBackReference() 方法。 此方法允许您使用上一个操作的结果插入或修改列。

withValueBackReference() 方法有两个参数:

key
键值对的键。此参数的值应为您要修改的表中的列名称。
previousResult
applyBatch() 中 ContentProviderResult 对象数组中值的从 0 开始的索引。应用批量操作时,每个操作的结果都会存储在中间结果数组中。previousResult 值是其中一个结果的索引,该结果会与 key 值一起检索和存储。这样,您就可以插入新的原始联系人记录并取回其 _ID 值,然后在添加 ContactsContract.Data 行时对该值进行“反向引用”。

当您首次调用 applyBatch() 时,系统会创建整个结果数组,其大小等于您提供的 ContentProviderOperation 对象的 ArrayList 的大小。不过,结果数组中的所有元素都会设置为 null,如果您尝试对尚未应用的操作的结果进行反向引用,withValueBackReference() 会抛出 Exception。

以下代码段展示了如何批量插入新的原始联系人和数据。它们包含用于建立退让点和使用反向引用的代码。

第一个代码段会从界面中检索联系人数据。此时,用户已选择要为其添加新原始联系人的账号。

Kotlin

// Creates a contact entry from the current UI values, using the currently-selected account.
private fun createContactEntry() {
    /*
     * Gets values from the UI
     */
    val name = contactNameEditText.text.toString()
    val phone = contactPhoneEditText.text.toString()
    val email = contactEmailEditText.text.toString()

    val phoneType: String = contactPhoneTypes[mContactPhoneTypeSpinner.selectedItemPosition]

    val emailType: String = contactEmailTypes[mContactEmailTypeSpinner.selectedItemPosition]

Java

// Creates a contact entry from the current UI values, using the currently-selected account.
protected void createContactEntry() {
    /*
     * Gets values from the UI
     */
    String name = contactNameEditText.getText().toString();
    String phone = contactPhoneEditText.getText().toString();
    String email = contactEmailEditText.getText().toString();

    int phoneType = contactPhoneTypes.get(
            contactPhoneTypeSpinner.getSelectedItemPosition());

    int emailType = contactEmailTypes.get(
            contactEmailTypeSpinner.getSelectedItemPosition());

下一个代码段会创建一个操作,用于将原始联系人行插入到 ContactsContract.RawContacts 表中:

Kotlin

    /*
     * 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.
    val ops = arrayListOf<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.
     */
    var op: ContentProviderOperation.Builder =
            ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
                    .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, selectedAccount.name)
                    .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, selectedAccount.type)

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

Java

    /*
     * 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, selectedAccount.getType())
            .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, selectedAccount.getName());

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

接下来,代码会为显示名称、电话和电子邮件行创建数据行。

每个操作构建器对象都使用 withValueBackReference() 来获取 RAW_CONTACT_ID。该引用指向第一个操作中的 ContentProviderResult 对象,该操作会添加原始联系人行并返回其新的 _ID 值。因此,每个数据行都会通过其 RAW_CONTACT_ID 自动关联到其所属的新 ContactsContract.RawContacts 行。

添加电子邮件行的 ContentProviderOperation.Builder 对象带有 withYieldAllowed() 标记,该标记会设置一个退让点:

Kotlin

    // 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())

Java

    // 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() 的调用,该调用会插入新的原始联系人和数据行。

Kotlin

    // Ask the Contacts Provider to create a new contact
    Log.d(TAG, "Selected account: ${mSelectedAccount.name} (${mSelectedAccount.type})")
    Log.d(TAG, "Creating contact: $name")

    /*
     * Applies the array of ContentProviderOperation objects in batch. The results are
     * discarded.
     */
    try {
        contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
    } catch (e: Exception) {
        // Display a warning
        val txt: String = getString(R.string.contactCreationFailure)
        Toast.makeText(applicationContext, txt, Toast.LENGTH_SHORT).show()

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

Java

    // Ask the Contacts Provider to create a new contact
    Log.d(TAG,"Selected account: " + selectedAccount.getName() + " (" +
            selectedAccount.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 对象。对于内容 URI,请使用 RawContacts.CONTENT_URI,并在其后附加原始联系人的 _ID。
  3. 对于 ContentProviderOperation.Builder 对象,调用 withValue() 以将 VERSION 列与您刚刚检索到的版本号进行比较。
  4. 对于同一 ContentProviderOperation.Builder,请调用 withExpectedCount(),以确保此断言仅测试一行。
  5. 调用 build() 以创建 ContentProviderOperation 对象,然后将此对象作为第一个对象添加到您传递给 applyBatch() 的 ArrayList 中。
  6. 应用批量事务。

如果您在读取原始联系人行和尝试修改该行之间通过其他操作更新了该行,则“断言”ContentProviderOperation 将失败,并且整批操作都将撤消。然后,您可以选择重试该批次或执行其他操作。

以下代码段演示了如何在使用 CursorLoader 查询单个原始联系人后创建“断言”ContentProviderOperation:

Kotlin

/*
 * The application uses CursorLoader to query the raw contacts table. The system calls this method
 * when the load is finished.
 */
override fun onLoadFinished(loader: Loader<Cursor>, cursor: Cursor) {
    // Gets the raw contact's _ID and VERSION values
    rawContactID = cursor.getLong(cursor.getColumnIndex(BaseColumns._ID))
    mVersion = cursor.getInt(cursor.getColumnIndex(SyncColumns.VERSION))
}

...

// Sets up a Uri for the assert operation
val rawContactUri: Uri = ContentUris.withAppendedId(
        ContactsContract.RawContacts.CONTENT_URI,
        rawContactID
)

// Creates a builder for the assert operation
val assertOp: ContentProviderOperation.Builder =
        ContentProviderOperation.newAssertQuery(rawContactUri).apply {
            // Adds the assertions to the assert operation: checks the version
            withValue(SyncColumns.VERSION, mVersion)

            // and count of rows tested
            withExpectedCount(1)
        }

// Creates an ArrayList to hold the ContentProviderOperation objects
val ops = arrayListOf<ContentProviderOperation>()

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 {
    val results: Array<ContentProviderResult> = contentResolver.applyBatch(AUTHORITY, ops)
} catch (e: OperationApplicationException) {
    // Actions you want to take if the assert operation fails go here
}

Java

/*
 * 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
    rawContactID = 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, rawContactID);

// Creates a builder for the assert operation
ContentProviderOperation.Builder assertOp = ContentProviderOperation.newAssertQuery(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<ContentProviderOperation>;

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
    }

使用 intent 进行检索和修改

通过向设备的通讯录应用发送 intent,您可以间接访问联系人提供程序。该 intent 会启动设备的通讯录应用界面,用户可以在其中执行与通讯录相关的工作。拥有此类访问权限的用户可以:

  • 从列表中选择一个联系人,并将其返回到您的应用中,以便进一步处理。
  • 修改现有联系人的数据。
  • 为其任何账号插入新的原始联系人。
  • 删除单个联系人或多位联系人的数据。

如果用户要插入或更新数据,您可以先收集数据,然后将其作为 intent 的一部分发送。

当您使用 intent 通过设备的通讯录应用访问联系人提供程序时,您无需编写自己的界面或代码来访问该提供程序。您也无需请求对提供程序进行读取或写入的权限。设备的通讯录应用可以向您委托对某个联系人的读取权限,并且由于您是通过其他应用对提供程序进行修改,因此您无需拥有写入权限。

Content Provider 基础知识指南中的“通过 intent 访问数据”部分详细介绍了发送 intent 以访问 provider 的一般流程。表 4 总结了您在执行可用任务时使用的操作、MIME 类型和数据值,而您可以与 putExtra() 搭配使用的 extra 值则列在 ContactsContract.Intents.Insert 的参考文档中:

表 4. 联系人提供程序 intent。

任务 操作 数据 MIME 类型 备注
从列表中选择联系人 ACTION_PICK 以下之一: 未使用 显示原始联系人列表或原始联系人中的数据列表,具体取决于您提供的内容 URI 类型。

调用 startActivityForResult(),该方法会返回所选行的内容 URI。URI 的形式是表的 content URI 加上行的 LOOKUP_ID。 在 activity 的生命周期内,设备的通讯录应用会将对此内容 URI 的读取和写入权限委托给您的应用。如需了解详情,请参阅 Content Provider 基础知识指南。

插入新的原始联系人 Insert.ACTION 不适用 RawContacts.CONTENT_TYPE,一组原始联系人的 MIME 类型。 显示设备的通讯录应用的添加联系人界面。系统会显示您添加到 intent 的 extra 值。如果使用 startActivityForResult() 发送,新添加的原始联系人的内容 URI 会在 Intent 参数的“data”字段中传回 activity 的 onActivityResult() 回调方法。如需获取该值,请调用 getData()。
修改联系人 ACTION_EDIT 联系人的 CONTENT_LOOKUP_URI。编辑器 activity 将允许用户修改与此联系人关联的任何数据。 Contacts.CONTENT_ITEM_TYPE,1 位联系人。 在通讯录应用中显示“修改联系人”界面。系统会显示您添加到 intent 的 extra 值。当用户点击完成以保存修改时,您的 activity 会返回到前台。
显示一个还可添加数据的选择器。 ACTION_INSERT_OR_EDIT N/A CONTENT_ITEM_TYPE 此 intent 始终会显示通讯录应用的选取器屏幕。用户可以选择要修改的联系人,也可以添加新联系人。系统会根据用户的选择显示修改或添加界面,并显示您在 intent 中传递的 extra 数据。如果您的应用显示电子邮件地址或电话号码等联系人数据,请使用此 intent 允许用户将这些数据添加到现有联系人。 联系人、

注意:无需在此 intent 的 extra 中发送名称值,因为用户始终会选择现有名称或添加新名称。此外,如果您发送了姓名,并且用户选择进行修改,则通讯录应用会显示您发送的姓名,并覆盖之前的值。如果用户未注意到这一点并保存了修改,则旧值会丢失。

设备的通讯录应用不允许您使用 intent 删除原始联系人或其任何数据。如需删除原始联系人,请改用 ContentResolver.delete() 或 ContentProviderOperation.newDelete()。

以下代码段展示了如何构建和发送用于插入新原始联系人和数据的 intent:

Kotlin

// Gets values from the UI
val name = contactNameEditText.text.toString()
val phone = contactPhoneEditText.text.toString()
val email = contactEmailEditText.text.toString()

val company = companyName.text.toString()
val jobtitle = jobTitle.text.toString()

/*
 * 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
val contactData = arrayListOf<ContentValues>()

/*
 * Defines the raw contact row
 */

// Sets up the row as a ContentValues object
val rawContactRow = ContentValues().apply {
    // Adds the account type and name to the row
    put(ContactsContract.RawContacts.ACCOUNT_TYPE, selectedAccount.type)
    put(ContactsContract.RawContacts.ACCOUNT_NAME, selectedAccount.name)
}

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

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

// Sets up the row as a ContentValues object
val phoneRow = ContentValues().apply {
    // Specifies the MIME type for this data row (all data rows must be marked by their type)
    put(ContactsContract.Data.MIMETYPE,ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)

    // Adds the phone number and its type to the row
    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
val emailRow = ContentValues().apply {
    // Specifies the MIME type for this data row (all data rows must be marked by their type)
    put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)

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

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

// Creates a new intent for sending to the device's contacts application
val insertIntent = Intent(ContactsContract.Intents.Insert.ACTION).apply {
    // Sets the MIME type to the one expected by the insertion activity
    type = ContactsContract.RawContacts.CONTENT_TYPE

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

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

    /*
    * 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
    */
    putParcelableArrayListExtra(ContactsContract.Intents.Insert.DATA, contactData)
}

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

Java

// Gets values from the UI
String name = contactNameEditText.getText().toString();
String phone = contactPhoneEditText.getText().toString();
String email = contactEmailEditText.getText().toString();

String company = companyName.getText().toString();
String jobtitle = jobTitle.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, selectedAccount.getType());
rawContactRow.put(ContactsContract.RawContacts.ACCOUNT_NAME, selectedAccount.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);

数据完整性

由于联系人代码库包含用户希望正确且最新的重要敏感数据,因此联系人提供程序具有明确定义的数据完整性规则。您有责任在修改联系人数据时遵守这些规则。重要规则如下:

每添加一个 ContactsContract.RawContacts 行,请务必添加一个 ContactsContract.CommonDataKinds.StructuredName 行。
如果 ContactsContract.Data 表中存在 ContactsContract.RawContacts 行,但没有 ContactsContract.CommonDataKinds.StructuredName 行,则可能会在汇总期间导致问题。
始终将新的 ContactsContract.Data 行与其父级 ContactsContract.RawContacts 行相关联。
未关联到 ContactsContract.RawContacts 的 ContactsContract.Data 行不会显示在设备的通讯录应用中,并且可能会导致同步适配器出现问题。
仅更改您拥有的原始联系人的数据。
请记住,联系人提供程序通常会管理来自多种不同账号类型/在线服务的数据。您需要确保应用仅修改或删除属于您的行的数据,并且仅插入具有您控制的账号类型和名称的数据。
请始终使用 ContactsContract 及其子类中定义的常量来表示授权机构、内容 URI、URI 路径、列名称、MIME 类型和 TYPE 值。
使用这些常量有助于避免错误。如果任何常量已弃用,您还会收到编译器警告通知。

自定义数据行

通过创建和使用您自己的自定义 MIME 类型,您可以在 ContactsContract.Data 表中插入、修改、删除和检索您自己的数据行。您的行只能使用 ContactsContract.DataColumns 中定义的列,但您可以将自己的特定于类型的列名称映射到默认列名称。在设备的通讯录应用中,系统会显示您的行的数据,但无法修改或删除这些数据,并且用户无法添加其他数据。如需允许用户修改您的自定义数据行,您必须在自己的应用中提供编辑器 activity。

如需显示自定义数据,请提供一个 contacts.xml 文件,其中包含一个 <ContactsAccountType> 元素及其一个或多个 <ContactsDataKind> 子元素。<ContactsDataKind> element 部分对此进行了更详细的说明。

如需详细了解自定义 MIME 类型,请参阅 创建 content provider 指南。

联系人提供程序同步适配器

联系人提供程序专为处理设备与在线服务之间的联系人数据同步而设计。这样,用户就可以将现有数据下载到新设备,并将现有数据上传到新账号。 同步还可确保用户拥有最新数据,无论添加和更改的来源如何。同步的另一个优势是,即使设备未连接到网络,也可以使用联系人数据。

虽然您可以通过多种方式实现同步,但 Android 系统提供了一个插件同步框架,该框架可自动执行以下任务:

  • 正在检查网络可用性。
  • 根据用户偏好设置安排和执行同步。
  • 正在重启已停止的同步。

如需使用此框架,您需要提供同步适配器插件。每个同步适配器对于服务和 content provider 都是唯一的,但可以处理同一服务的多个账号名称。该框架还允许同一服务和提供程序有多个同步适配器。

同步适配器类和文件

您需要将同步适配器实现为 AbstractThreadedSyncAdapter 的子类,并将其作为 Android 应用的一部分进行安装。系统会通过应用清单中的元素以及清单指向的特殊 XML 文件来了解同步适配器。XML 文件定义了在线服务的账号类型和 content provider 的授权,这两者共同唯一标识了适配器。只有在用户为同步适配器的账号类型添加账号,并为同步适配器与之同步的内容提供程序启用同步后,同步适配器才会变为活跃状态。此时,系统会开始管理适配器,并根据需要在内容提供程序和服务器之间进行同步时调用适配器。

注意:将账号类型用作同步适配器标识的一部分,可让系统检测并分组访问同一组织中不同服务的同步适配器。例如,Google 在线服务的同步适配器都具有相同的账号类型 com.google。当用户向其设备添加 Google 账号时,系统会一并列出所有已安装的 Google 服务同步适配器;列出的每个同步适配器都会与设备上的不同 content provider 同步。

由于大多数服务都要求用户先验证其身份,然后才能访问数据,因此 Android 系统提供了一个身份验证框架,该框架与同步适配器框架类似,并且通常与同步适配器框架结合使用。身份验证框架使用 AbstractAccountAuthenticator 的子类插件身份验证器。身份验证器通过以下步骤验证用户的身份:

  1. 收集用户的姓名、密码或类似信息(用户的凭据)。
  2. 将凭证发送到服务
  3. 检查服务的回复。

如果服务接受凭据,则身份验证器可以存储凭据以供日后使用。由于插件身份验证器框架,AccountManager 可以提供对身份验证器支持并选择公开的任何身份验证令牌(例如 OAuth2 身份验证令牌)的访问权限。

虽然身份验证并非必需,但大多数通讯录服务都会使用它。 不过,您不一定要使用 Android 身份验证框架进行身份验证。

同步适配器实现

如需为联系人提供程序实现同步适配器,您首先需要创建一个包含以下内容的 Android 应用:

一个 Service 组件,用于响应系统发出的绑定到同步适配器的请求。
当系统想要运行同步时,会调用服务的 onBind() 方法来获取同步适配器的 IBinder。这样,系统就可以对适配器的方法进行跨进程调用。
实际的同步适配器,作为 AbstractThreadedSyncAdapter 的具体子类实现。
此类负责从服务器下载数据、从设备上传数据以及解决冲突。适配器的主要工作在 onPerformSync() 方法中完成。此类必须实例化为单例。
Application 的子类。
此类充当同步适配器单例的工厂。使用 onCreate() 方法实例化同步适配器,并提供静态“getter”方法,以便将单例返回给同步适配器服务的 onBind() 方法。
可选:一个 Service 组件,用于响应系统发出的用户身份验证请求。
AccountManager 会启动此服务,以开始身份验证流程。服务的 onCreate() 方法会实例化一个身份验证器对象。当系统想要为应用的同步适配器对用户账号进行身份验证时,它会调用服务的 onBind() 方法来获取身份验证器的 IBinder。这样,系统就可以对身份验证器的方法进行跨进程调用。
可选:AbstractAccountAuthenticator 的具体子类,用于处理身份验证请求。
此类提供 AccountManager 调用的方法,用于通过服务器对用户的凭据进行身份验证。身份验证过程的细节因所使用的服务器技术而异。您应参阅服务器软件的文档,详细了解身份验证。
用于向系统定义同步适配器和身份验证器的 XML 文件。
前面介绍的同步适配器和身份验证器服务组件在应用清单中的 <service> 元素中定义。这些元素包含向系统提供特定数据的 <meta-data> 子元素:
  • 同步适配器服务的 <meta-data> 元素指向 XML 文件 res/xml/syncadapter.xml。反过来,此文件会为将与联系人提供程序同步的 Web 服务指定一个 URI,并为该 Web 服务指定一个账号类型。
  • 可选:身份验证器的 <meta-data> 元素指向 XML 文件 res/xml/authenticator.xml。反过来,此文件会指定此身份验证器支持的账号类型,以及在身份验证过程中显示的界面资源。此元素中指定的账号类型必须与为同步适配器指定的账号类型相同。

社交信息流数据

android.provider.ContactsContract.StreamItems 和 android.provider.ContactsContract.StreamItemPhotos 表管理来自社交网络的传入数据。您可以编写一个同步适配器,用于将您自己网络中的信息流数据添加到这些表中,也可以从这些表中读取信息流数据并将其显示在您自己的应用中,或者同时执行这两种操作。借助这些功能,您的社交网络服务和应用可以集成到 Android 的社交网络体验中。

社交媒体信息流文本

信息流内容始终与原始联系人相关联。android.provider.ContactsContract.StreamItemsColumns#RAW_CONTACT_ID 链接到原始联系人的 _ID 值。原始联系人的账号类型和账号名称也存储在信息流内容行中。

将数据流中的数据存储在以下列中:

android.provider.ContactsContract.StreamItemsColumns#ACCOUNT_TYPE
必需。与此信息流内容关联的原始联系人的用户账号类型。请务必在插入直播内容时设置此值。
android.provider.ContactsContract.StreamItemsColumns#ACCOUNT_NAME
必需。与此信息流内容关联的原始联系人的用户账号名称。请务必在插入直播活动时设置此值。
标识符列
必需。插入直播项时,您必须插入以下标识符列:
  • android.provider.ContactsContract.StreamItemsColumns#CONTACT_ID:与此信息流内容关联的联系人的 android.provider.BaseColumns#_ID 值。
  • android.provider.ContactsContract.StreamItemsColumns#CONTACT_LOOKUP_KEY:与此信息流内容关联的联系人的 android.provider.ContactsContract.ContactsColumns#LOOKUP_KEY 值。
  • android.provider.ContactsContract.StreamItemsColumns#RAW_CONTACT_ID:与此信息流内容关联的原始联系人的 android.provider.BaseColumns#_ID 值。
android.provider.ContactsContract.StreamItemsColumns#COMMENTS
可选。存储可在信息流条目开头显示的摘要信息。
android.provider.ContactsContract.StreamItemsColumns#TEXT
信息流条目的文本,可以是条目来源发布的内容,也可以是生成信息流条目的某些操作的说明。此列可以包含可由 fromHtml() 呈现的任何格式和嵌入式资源图片。提供程序可能会截断或省略较长的内容,但会尽量避免破坏标记。
android.provider.ContactsContract.StreamItemsColumns#TIMESTAMP
一个文本字符串,其中包含信息流内容插入或更新的时间,格式为自纪元以来的毫秒数。插入或更新信息流内容的应用程序负责维护此列;联系人提供程序不会自动维护此列。

如需显示动态项的身份信息,请使用 android.provider.ContactsContract.StreamItemsColumns#RES_ICON、android.provider.ContactsContract.StreamItemsColumns#RES_LABEL 和 android.provider.ContactsContract.StreamItemsColumns#RES_PACKAGE 链接到应用中的资源。

android.provider.ContactsContract.StreamItems 表还包含 android.provider.ContactsContract.StreamItemsColumns#SYNC1 到 android.provider.ContactsContract.StreamItemsColumns#SYNC4 列,供同步适配器专用。

社交媒体照片

android.provider.ContactsContract.StreamItemPhotos 表存储与动态消息项关联的照片。该表的 android.provider.ContactsContract.StreamItemPhotosColumns#STREAM_ITEM_ID 列会链接到 android.provider.ContactsContract.StreamItems 表的 _ID 列中的值。照片引用存储在表中的以下列中:

android.provider.ContactsContract.StreamItemPhotos#PHOTO 列(BLOB)。
照片的二进制表示形式,由提供商调整大小以进行存储和显示。 此列可向后兼容使用它来存储照片的旧版联系人提供程序。不过,在当前版本中,您不应使用此列来存储照片。而应使用 android.provider.ContactsContract.StreamItemPhotosColumns#PHOTO_FILE_ID 或 android.provider.ContactsContract.StreamItemPhotosColumns#PHOTO_URI(下文对这两者进行了说明)将照片存储在文件中。此列现在包含照片的缩略图,可供读取。
android.provider.ContactsContract.StreamItemPhotosColumns#PHOTO_FILE_ID
原始联系人照片的数字标识符。将此值附加到常量 DisplayPhoto.CONTENT_URI,以获取指向单个照片文件的内容 URI,然后调用 openAssetFileDescriptor() 以获取照片文件的句柄。
android.provider.ContactsContract.StreamItemPhotosColumns#PHOTO_URI
直接指向此行所表示的照片的照片文件的内容 URI。 使用此 URI 调用 openAssetFileDescriptor(),以获取照片文件的句柄。

使用社交媒体流表

这些表的工作方式与联系人提供程序中的其他主表相同,但有以下区别:

  • 这些表需要额外的访问权限。如需从中读取数据,您的应用必须具有 android.Manifest.permission#READ_SOCIAL_STREAM 权限。如需修改这些内容,您的应用必须具有 android.Manifest.permission#WRITE_SOCIAL_STREAM 权限。
  • 对于 android.provider.ContactsContract.StreamItems 表,为每个原始联系人存储的行数是有限的。达到此上限后,联系人提供程序会通过自动删除具有最旧 android.provider.ContactsContract.StreamItemsColumns#TIMESTAMP 的行,为新的信息流内容行腾出空间。如需获取此限制,请向内容 URI android.provider.ContactsContract.StreamItems#CONTENT_LIMIT_URI 发出查询。您可以将除内容 URI 之外的所有参数都设置为 null。查询会返回一个包含单行的 Cursor,其中包含单个列 android.provider.ContactsContract.StreamItems#MAX_ITEMS。

android.provider.ContactsContract.StreamItems.StreamItemPhotos 类定义了 android.provider.ContactsContract.StreamItemPhotos 的一个子表,其中包含单个信息流条目的照片行。

社交信息流互动

由联系人提供程序管理的社交信息流数据与设备的通讯录应用相结合,可提供一种将社交网络系统与现有联系人联系起来的强大方式。可指定下列地图项:

  • 通过使用同步适配器将社交网络服务同步到联系人提供程序,您可以检索用户联系人的近期活动,并将其存储在 android.provider.ContactsContract.StreamItems 和 android.provider.ContactsContract.StreamItemPhotos 表中,以供日后使用。
  • 除了定期同步之外,您还可以在用户选择要查看的联系人时触发同步适配器检索其他数据。这样,您的同步适配器就可以检索联系人的高分辨率照片和最新信息流内容。
  • 通过向设备的通讯录应用和联系人提供程序注册通知,您可以在查看联系人时接收 intent,并在此时通过您的服务更新联系人的状态。与使用同步适配器执行完全同步相比,此方法可能更快且使用的带宽更少。
  • 用户可以在设备的通讯录应用中查看联系人时,将联系人添加到您的社交网络服务。您可以通过“邀请联系人”功能启用此功能,该功能通过以下两项的组合启用:一项是将现有联系人添加到您的网络的 activity,以及一个向设备的通讯录应用和联系人提供程序提供应用详细信息的 XML 文件。

信息流内容与联系人提供程序的定期同步与其他同步相同。如需详细了解同步,请参阅联系人提供程序同步适配器部分。接下来的两部分将介绍如何注册通知和邀请联系人。

注册以处理社交网络视图

如需注册同步适配器,以便在用户查看由同步适配器管理的联系人时收到通知,请执行以下操作:

  1. 在项目的 res/xml/ 目录中创建一个名为 contacts.xml 的文件。如果您已有此文件,则可以跳过此步骤。
  2. 在此文件中,添加元素 <ContactsAccountType xmlns:android="http://schemas.android.com/apk/res/android">。 如果此元素已存在,您可以跳过此步骤。
  3. 如需注册在用户打开设备通讯录应用中的联系人详情页面时收到通知的服务,请向元素添加属性 viewContactNotifyService="serviceclass",其中 serviceclass 是应从设备通讯录应用接收 intent 的服务的完全限定类名。对于通知程序服务,请使用扩展 IntentService 的类,以允许该服务接收 intent。传入 intent 中的数据包含用户点击的原始联系人的内容 URI。通过通知器服务,您可以绑定到同步适配器,然后调用同步适配器来更新原始联系人的数据。

如需注册在用户点击信息流内容或照片(或两者)时调用的 activity,请执行以下操作:

  1. 在项目的 res/xml/ 目录中创建一个名为 contacts.xml 的文件。如果您已有此文件,则可以跳过此步骤。
  2. 在此文件中,添加元素 <ContactsAccountType xmlns:android="http://schemas.android.com/apk/res/android">。 如果此元素已存在,您可以跳过此步骤。
  3. 如需注册您的某个 activity 来处理用户在设备的通讯录应用中点击信息流条目的操作,请将属性 viewStreamItemActivity="activityclass" 添加到元素中,其中 activityclass 是应从设备的通讯录应用接收 intent 的 activity 的完全限定类名。
  4. 如需注册您的某个 activity 来处理用户在设备的通讯录应用中点击信息流照片的操作,请向该元素添加 viewStreamItemPhotoActivity="activityclass" 属性,其中 activityclass 是应从设备的通讯录应用接收 intent 的 activity 的完全限定类名。

<ContactsAccountType> 元素部分更详细地介绍了 <ContactsAccountType> 元素。

传入 intent 包含用户点击的商品或照片的内容 URI。 如需为文本项和照片设置单独的 activity,请在同一文件中使用这两个属性。

与您的社交网络服务互动

用户无需离开设备的通讯录应用即可邀请联系人加入您的社交网站。您可以让设备的通讯录应用发送一个 intent,以邀请联系人加入您的某个 activity。如需设置此模式,请执行以下操作:

  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 值是一个文本字符串,显示在设备通讯录应用的添加连接菜单中。

注意: ContactsSource 是 ContactsAccountType 的已弃用标记名称。

contacts.xml 参考文档

contacts.xml 文件包含一些 XML 元素,用于控制同步适配器和应用与通讯录应用和联系人提供程序的交互。下文将对这些元素进行描述。

<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 组件和界面标签,以便用户邀请其联系人加入社交网络、在社交网络信息流更新时通知用户,等等。

请注意,<ContactsAccountType> 的属性不需要属性前缀 android:。

属性:

inviteContactActivity
当用户从设备的通讯录应用中选择添加连接时,您希望在应用中激活的 activity 的完全限定类名。
inviteContactActionLabel
在添加连接菜单中为 inviteContactActivity 中指定的 activity 显示的文本字符串。 例如,您可以使用字符串“在我的网络中关注”。您可以为此标签使用字符串资源标识符。
viewContactNotifyService
应用中服务的完全限定类名称,当用户查看联系人时,该服务应收到 通知。此通知由设备的通讯录应用发送;它允许您的应用将数据密集型操作推迟到需要时再执行。例如,您的应用可以通过读取并显示联系人的高分辨率照片和最新社交信息流内容来响应此通知。社交信息流互动部分对此功能进行了更详细的说明。
viewGroupActivity
应用中可显示群组信息的 activity 的完全限定类名称。当用户在设备的通讯录应用中点击群组标签时,系统会显示此 activity 的界面。
viewGroupActionLabel
通讯录应用为界面控件显示的标签,该控件允许用户查看应用中的群组。

此属性允许使用字符串资源标识符。

viewStreamItemActivity
应用中某个 activity 的完全限定类名,当用户点击原始联系人的信息流内容时,设备的通讯录应用会启动该 activity。
viewStreamItemPhotoActivity
当用户点击原始联系人的信息流内容中的照片时,设备的通讯录应用会启动您应用中的某个 activity,此属性即为该 activity 的完全限定类名。

<ContactsDataKind> 元素

<ContactsDataKind> 元素控制应用的自定义数据行在通讯录应用界面中的显示。其语法如下:

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

包含在以下内容中:

<ContactsAccountType>

说明:

使用此元素可让通讯录应用将自定义数据行的内容显示为原始联系人详细信息的一部分。<ContactsAccountType> 的每个 <ContactsDataKind> 子元素都表示同步适配器添加到 ContactsContract.Data 表中的一种自定义数据行。为您使用的每种自定义 MIME 类型添加一个 <ContactsDataKind> 元素。如果您有不想显示数据的自定义数据行,则无需添加该元素。

属性:

android:mimeType
您在 ContactsContract.Data 表中为其中一种自定义数据行类型定义的自定义 MIME 类型。例如,值 vnd.android.cursor.item/vnd.example.locationstatus 可以是记录联系人最后已知位置的数据行的自定义 MIME 类型。
android:icon
Android 可绘制资源,通讯录应用会在您的数据旁边显示该资源。使用此图标向用户表明数据来自您的服务。
android:summaryColumn
从数据行中检索到的两个值中第一个值的列名称。该值会显示为此数据行条目的第一行。第一行旨在用作数据摘要,但这是可选的。另请参阅 android:detailColumn。
android:detailColumn
从数据行中检索到的两个值中第二个值的列名称。该值会显示为此数据行条目的第二行。另请参阅 android:summaryColumn。

其他联系人提供程序功能

除了前面几部分介绍的主要功能外,联系人提供程序还提供以下用于处理联系人数据的实用功能:

  • 联系人群组
  • 照片功能

联系人群组

联系人提供程序可以选择使用群组数据为相关联系人集合添加标签。如果与用户账号关联的服务器想要维护群组,则该账号类型的同步适配器应在联系人提供程序和服务器之间传输群组数据。当用户向服务器添加新联系人,然后将此联系人放入新群组时,同步适配器必须将新群组添加到 ContactsContract.Groups 表中。原始联系人所属的一个或多个群组存储在 ContactsContract.Data 表中,使用 ContactsContract.CommonDataKinds.GroupMembership MIME 类型。

如果您要设计一个同步适配器,用于将服务器中的原始联系人数据添加到联系人提供程序,并且您不使用群组,则需要告知提供程序让您的数据可见。在用户向设备添加账号时执行的代码中,更新联系人提供程序为该账号添加的 ContactsContract.Settings 行。在此行中,将 Settings.UNGROUPED_VISIBLE 列的值设置为 1。这样一来,联系人提供程序将始终显示您的联系人数据,即使您不使用群组也是如此。

联系人照片

ContactsContract.Data 表将照片存储为 MIME 类型为 Photo.CONTENT_ITEM_TYPE 的行。该行的 CONTACT_ID 列会关联到其所属原始联系人的 _ID 列。 ContactsContract.Contacts.Photo 类定义了 ContactsContract.Contacts 的子表,其中包含联系人主要照片的照片信息,该照片是联系人主要原始联系人的主要照片。同样,ContactsContract.RawContacts.DisplayPhoto 类定义了 ContactsContract.RawContacts 的一个子表,其中包含原始联系人主照片的照片信息。

ContactsContract.Contacts.Photo 和 ContactsContract.RawContacts.DisplayPhoto 的参考文档包含检索照片信息的示例。没有用于检索原始联系人的主缩略图的便捷类,但您可以向 ContactsContract.Data 表发送查询,选择原始联系人的 _ID、Photo.CONTENT_ITEM_TYPE 和 IS_PRIMARY 列,以查找原始联系人的主照片行。

个人的社交媒体信息流数据可能还包含照片。这些照片存储在 android.provider.ContactsContract.StreamItemPhotos 表中,社交信息流照片部分对此进行了更详细的介绍。