联系人提供程序

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

本指南介绍以下内容:

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

本指南假设您了解 Android 内容提供程序的基础知识。要了解有关 Android 内容提供程序的更多信息,请阅读内容提供程序基础知识指南。

联系人提供程序组织

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

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

这三个表通常以其合同类的名称来指代。这些类定义了表使用的内容 URI、列名和列值的常量。

ContactsContract.Contacts
基于原始联系人行的聚合,表示不同人员的行。
ContactsContract.RawContacts
包含人员数据摘要的行,特定于用户帐户和类型。
ContactsContract.Data
包含原始联系人详细信息的行,例如电子邮件地址或电话号码。

ContactsContract 中合同类表示的其他表是辅助表,联系人提供程序使用这些表来管理其操作或支持设备联系人或电话应用中的特定功能。

原始联系人

原始联系人表示来自单个帐户类型和帐户名称的人员数据。由于联系人提供程序允许将多个在线服务作为人员数据的来源,因此联系人提供程序允许针对同一人使用多个原始联系人。多个原始联系人还允许用户组合来自同一帐户类型的多个帐户的人员数据。

原始联系人的大部分数据并非存储在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。始终使用您拥有或控制的域标识符限定您的帐户类型。这将确保您的帐户类型是唯一的。 提供联系人数据的帐户类型通常具有关联的同步适配器,该适配器与联系人提供程序同步。
已删除 原始联系人的“已删除”标记。 此标记允许联系人提供程序在内部维护行,直到同步适配器能够从其服务器删除行,然后最终从存储库删除行。

说明

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

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

    例如,如果您希望您的应用维护域为 com.example.dataservice 的基于 Web 的服务的联系人数据,并且用户对您服务的帐户为 [email protected],则用户必须首先添加帐户“类型”(com.example.dataservice)和帐户“名称”([email protected]),然后您的应用才能添加原始联系人行。您可以在文档中向用户解释此要求,也可以提示用户添加类型和名称,或两者都添加。帐户类型和帐户名称将在下一节中详细介绍。

原始联系人数据的来源

要了解原始联系人的工作原理,请考虑在她的设备上定义了以下三个用户帐户的用户“Emily Dickinson”

此用户已在“帐户”设置中为所有这三个帐户启用了“同步联系人”。

假设 Emily Dickinson 打开一个浏览器窗口,以 [email protected] 登录 Gmail,打开联系人,然后添加“Thomas Higginson”。稍后,她以 [email protected] 登录 Gmail 并向“Thomas Higginson”发送电子邮件,这会自动将其添加为联系人。她还在 Twitter 上关注“colonel_tom”(Thomas Higginson 的 Twitter ID)。

联系人提供程序为此工作创建了三个原始联系人

  1. [email protected] 关联的“Thomas Higginson”的原始联系人。用户帐户类型为 Google。
  2. [email protected] 关联的第二个“Thomas Higginson”的原始联系人。用户帐户类型也是 Google。即使名称与之前的名称相同,也存在第二个原始联系人,因为该联系人是为不同的用户帐户添加的。
  3. 与“belle_of_amherst”关联的第三个“Thomas Higginson”的原始联系人。用户帐户类型为 Twitter。

数据

如前所述,原始联系人的数据存储在 ContactsContract.Data 行中,该行链接到原始联系人的 _ID 值。这允许单个原始联系人具有相同类型数据的多个实例,例如电子邮件地址或电话号码。例如,如果 [email protected] 的“Thomas Higginson”(与 Google 帐户 [email protected] 关联的 Thomas Higginson 的原始联系人行)的家庭电子邮件地址为 [email protected],工作电子邮件地址为 [email protected],则联系人提供程序会存储这两个电子邮件地址行并将它们都链接到原始联系人。

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

描述性列名

一些描述性列名的示例:

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

通用列名

有 15 个名为 DATA1DATA15 的通用列通常可用,另外还有四个通用列 SYNC1SYNC4 只能由同步适配器使用。无论行包含哪种类型的数据,通用列名常量始终有效。

DATA1 列已建立索引。联系人提供程序始终使用此列作为提供程序预期将成为查询最频繁目标的数据。例如,在电子邮件行中,此列包含实际的电子邮件地址。

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

特定于类型的列名

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

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

注意:不要使用具有提供程序预定义 MIME 类型之一的行将您自己的自定义数据添加到 ContactsContract.Data 表中。如果您这样做,您可能会丢失数据或导致提供程序出现故障。例如,您不应添加具有 MIME 类型 Email.CONTENT_ITEM_TYPE 的行,该行在 DATA1 列中包含用户名而不是电子邮件地址。如果您对行使用您自己的自定义 MIME 类型,则您可以自由定义您自己的特定于类型的列名并根据需要使用这些列。

图 2 显示了描述性列和数据列如何在 ContactsContract.Data 行中出现,以及特定于类型的列名如何“覆盖”通用列名

How type-specific column names map to generic column names

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

特定于类型的列名类

表 2 列出了最常用的特定于类型的列名类

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

映射类 数据类型 说明
ContactsContract.CommonDataKinds.StructuredName 与该数据行关联的原始联系人的名称数据。 原始联系人只有一个这样的行。
ContactsContract.CommonDataKinds.Photo 与该数据行关联的原始联系人的主要照片。 原始联系人只有一个这样的行。
ContactsContract.CommonDataKinds.Email 与该数据行关联的原始联系人的电子邮件地址。 原始联系人可以有多个电子邮件地址。
ContactsContract.CommonDataKinds.StructuredPostal 与该数据行关联的原始联系人的邮政地址。 原始联系人可以有多个邮政地址。
ContactsContract.CommonDataKinds.GroupMembership 将原始联系人链接到联系人提供程序中的组之一的标识符。 组是帐户类型和帐户名称的可选功能。它们将在联系人组部分中详细介绍。

联系人

联系人提供程序组合所有帐户类型和帐户名称中的原始联系人行以形成一个联系人。这有助于显示和修改用户为一个人收集的所有数据。联系人提供程序管理新联系人行的创建以及原始联系人与现有联系人行的聚合。应用程序和同步适配器都不允许添加联系人,并且联系人行中的一些列是只读的。

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

联系人提供程序会响应添加与任何现有联系人都不匹配的新原始联系人而创建新的联系人。如果现有原始联系人的数据发生变化,以至于它不再与之前附加到的联系人匹配,提供程序也会执行此操作。如果应用程序或同步适配器创建的新原始联系人确实与现有联系人匹配,则将新原始联系人聚合到现有联系人。

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

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

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

Contacts provider main tables

图 3. 联系人、原始联系人以及详细信息表的关系。

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

在上述情况下,系统会定期清除写入这些数据字段的任何值。

用于设置上述数据字段的 API 也已过时。

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

如果您的应用正在访问或更新这些字段或 API,请使用替代方法。例如,您可以使用 私有内容提供程序 或存储在您的应用或后端系统中的其他数据来满足某些用例。

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

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

同步适配器中的数据

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

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

帐户类型
标识用户已在其存储数据的服务。大多数情况下,用户必须向服务进行身份验证。例如,Google 联系人是一种帐户类型,由代码 google.com 标识。此值对应于 AccountManager 使用的帐户类型。
帐户名称
标识帐户类型特定帐户或登录名。Google 联系人帐户与 Google 帐户相同,Google 帐户使用电子邮件地址作为帐户名称。其他服务可能使用单字用户名或数字 ID。

帐户类型不必唯一。用户可以配置多个 Google 联系人帐户并将它们的数据下载到通讯录提供程序;如果用户为个人帐户名称设置了一组个人联系人,而为工作设置了另一组联系人,则可能会发生这种情况。帐户名称通常是唯一的。它们一起标识通讯录提供程序和外部服务之间的特定数据流。

如果您想将服务的數據傳輸到聯絡人提供者,您需要編寫您自己的同步适配器。這在聯絡人提供者同步适配器 部分中有更詳細的說明。

图 4 显示了通讯录提供程序如何适应有关人员的数据流。在标有“同步适配器”的框中,每个适配器都用其帐户类型标记。

Flow of data about people

图 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_CONTACTSWRITE_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。例如,帐户[email protected]的原始联系人“Thomas Higginson”允许与帐户[email protected]的原始联系人“Thomas Higginson”具有相同的源 ID。
  • 稳定性:源 ID 是在线服务中原始联系人数据的永久组成部分。例如,如果用户从应用设置中清除联系人存储并重新同步,则恢复的原始联系人应与之前具有相同的源 ID。如果不强制执行此操作,快捷方式将停止工作。
ContactsContract.Groups GROUP_VISIBLE “0” - 此组中的联系人不应在 Android 应用程序 UI 中可见。 此列用于与允许用户隐藏某些组中联系人的服务器兼容。
“1” - 允许此组中的联系人显示在应用程序 UI 中。
ContactsContract.Settings UNGROUPED_VISIBLE “0” - 对于此帐户和帐户类型,不属于任何组的联系人对 Android 应用程序 UI 是不可见的。 默认情况下,如果联系人的任何原始联系人都不属于任何组,则该联系人不可见(原始联系人的组成员资格由ContactsContract.CommonDataKinds.GroupMembership表中的一行或多行ContactsContract.Data指示)。通过在帐户类型和帐户的ContactsContract.Settings表行中设置此标志,您可以强制没有分组的联系人可见。此标志的一种用途是显示来自不使用组的服务器的联系人。
“1” - 对于此帐户和帐户类型,不属于任何组的联系人对应用程序 UI 是可见的。
ContactsContract.SyncState (全部) 使用此表存储同步适配器的元数据。 使用此表,您可以将同步状态和其他与同步相关的数据持久地存储在设备上。

联系人提供程序访问

本节描述访问联系人提供程序数据的指南,重点介绍以下内容:

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

从同步适配器进行修改的内容也在联系人提供程序同步适配器部分中更详细地介绍。

查询实体

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

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

实体简化了查询。使用实体,您可以一次性检索联系人的所有联系人数据,而无需先查询父表以获取 ID,然后使用该 ID 查询子表。此外,联系人提供程序在一个事务中处理对实体的查询,这确保检索到的数据在内部一致。

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

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

此代码段取自“详细信息”活动。

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获取数据以显示或进一步处理它。

批量修改

尽可能地,您应该通过创建ArrayListContentProviderOperation对象并调用applyBatch(),以“批量模式”在联系人提供程序中插入、更新和删除数据。由于联系人提供程序在一个事务中执行applyBatch()中的所有操作,因此您的修改将永远不会使联系人存储库处于不一致状态。批量修改还有助于同时插入原始联系人及其详细信息数据。

**注意:**要修改单个原始联系人,请考虑向设备的联系人应用程序发送意图,而不是在您的应用程序中处理修改。这将在使用意图进行检索和修改部分中更详细地介绍。

让步点

包含大量操作的批量修改可能会阻塞其他进程,从而导致整体用户体验不佳。为了尽可能少地组织您要执行的所有修改,并同时防止它们阻塞系统,您应该为一个或多个操作设置**让步点**。让步点是一个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() 时,会创建整个结果数组,其大小等于您提供的 ArrayListContentProviderOperation 对象的数量。但是,结果数组中的所有元素都设置为 null,如果您尝试对尚未应用的操作的结果进行反向引用,withValueBackReference() 会抛出 Exception 异常。

以下代码片段演示了如何批量插入新的原始联系人及其数据。它们包含建立屈服点和使用反向引用的代码。

第一个代码片段从UI检索联系人数据。此时,用户已选择应为其添加新原始联系人的帐户。

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
    }

使用意图进行检索和修改

向设备的联系人应用程序发送意图允许您间接访问联系人提供程序。该意图启动设备的联系人应用程序UI,用户可以在其中执行与联系人相关的操作。使用这种类型的访问,用户可以:

  • 从列表中选择联系人,并将其返回到您的应用程序以进行进一步处理。
  • 编辑现有联系人的数据。
  • 为其任何帐户插入新的原始联系人。
  • 删除联系人或联系人数据。

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

当您使用意图通过设备的联系人应用程序访问联系人提供程序时,您不必编写自己的UI或访问提供程序的代码。您也不必请求读取或写入提供程序的权限。设备的联系人应用程序可以将联系人的读取权限委托给您,并且由于您正在通过另一个应用程序修改提供程序,因此您不必拥有写入权限。

在“内容提供程序基础知识”指南的“通过意图访问数据”部分中,详细介绍了发送意图访问提供程序的常规流程。表4总结了您可以用于可用任务的操作、MIME类型和数据值,而您可以与 putExtra() 一起使用的额外值列在 ContactsContract.Intents.Insert 的参考文档中。

表4. 联系人提供程序意图。

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

调用 startActivityForResult(),它返回所选行的内容URI。URI的形式是表的Content URI,附加了行的LOOKUP_ID。设备的联系人应用程序将读取和写入权限委托给此内容URI,以延长您的活动时间。有关更多详细信息,请参阅内容提供程序基础知识指南。

插入新的原始联系人 Insert.ACTION N/A RawContacts.CONTENT_TYPE,一组原始联系人的MIME类型。 显示设备联系人应用程序的“添加联系人”屏幕。显示您添加到意图的额外值。如果使用 startActivityForResult() 发送,则新添加的原始联系人的内容URI将通过“数据”字段中的Intent参数传递回您的活动的onActivityResult()回调方法。要获取该值,请调用 getData()
编辑联系人 ACTION_EDIT CONTENT_LOOKUP_URI 用于联系人。编辑器活动将允许用户编辑与此联系人关联的任何数据。 Contacts.CONTENT_ITEM_TYPE,单个联系人。 在联系人应用程序中显示“编辑联系人”屏幕。显示您添加到意图的额外值。当用户单击“完成”保存编辑时,您的活动将返回前台。
显示一个也可以添加数据的选择器。 ACTION_INSERT_OR_EDIT N/A CONTENT_ITEM_TYPE 此意图始终显示联系人应用程序的选择器屏幕。用户可以选择一个联系人进行编辑,或添加一个新的联系人。将显示编辑或添加屏幕,具体取决于用户的选择,以及您在意图中传递的额外数据。如果您的应用程序显示联系人数据(如电子邮件或电话号码),请使用此意图允许用户将数据添加到现有联系人。

注意:无需在此意图的额外数据中发送名称值,因为用户总是选择现有名称或添加新名称。此外,如果您发送一个名称,并且用户选择进行编辑,则联系人应用程序将显示您发送的名称,覆盖先前的值。如果用户没有注意到这一点并保存了编辑,则旧值将丢失。

设备的联系人应用程序不允许您使用意图删除原始联系人或其任何数据。相反,要删除原始联系人,请使用 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.RawContacts 表中没有 ContactsContract.CommonDataKinds.StructuredName 行的 ContactsContract.RawContacts 行可能会在聚合过程中导致问题。
始终将新的 ContactsContract.Data 行链接到其父 ContactsContract.RawContacts 行。
未链接到 ContactsContract.RawContactsContactsContract.Data 行在设备的联系人应用程序中不可见,并且可能会导致同步适配器出现问题。
仅更改您拥有的那些原始联系人的数据。
请记住,联系人提供程序通常管理来自多种不同帐户类型/在线服务的的数据。您需要确保您的应用程序仅修改或删除属于您的行的 数据,并且仅插入您控制的帐户类型和名称的数据。
始终使用 ContactsContract 及其子类中定义的常量,用于权限、内容 URI、URI 路径、列名称、MIME 类型和 TYPE 值。
使用这些常量可以帮助您避免错误。如果任何常量已弃用,您还会收到编译器警告。

自定义数据行

通过创建和使用您自己的自定义 MIME 类型,您可以在 ContactsContract.Data 表中插入、编辑、删除和检索您自己的数据行。您的行仅限于使用 ContactsContract.DataColumns 中定义的列,尽管您可以将您自己的类型特定列名称映射到默认列名称。在设备的联系人应用程序中,会显示您的行的 数据,但无法编辑或删除,用户也无法添加其他数据。要允许用户修改您的自定义数据行,您必须在您自己的应用程序中提供一个编辑器活动。

要显示您的自定义数据,请提供一个包含 <ContactsAccountType> 元素及其一个或多个 <ContactsDataKind> 子元素的 contacts.xml 文件。这在<ContactsDataKind> 元素部分中有更详细的说明。

要了解有关自定义 MIME 类型的更多信息,请阅读创建内容提供程序指南。

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

联系人提供程序专门设计用于处理设备和在线服务之间联系人数据的**同步**。这允许用户将现有数据下载到新设备并将现有数据上传到新帐户。同步还确保用户拥有最新的数据,无论添加和更改的来源如何。同步的另一个优点是即使设备未连接到网络,它也能使联系人数据可用。

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

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

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

同步适配器类和文件

您可以将同步适配器实现为 AbstractThreadedSyncAdapter 的子类,并将其安装为 Android 应用程序的一部分。系统从应用程序清单中的元素和清单指向的特殊 XML 文件中了解同步适配器。XML 文件定义了在线服务的帐户类型和内容提供程序的权限,这两者一起唯一地标识适配器。在用户为同步适配器的帐户类型添加帐户并为同步适配器与其同步的内容提供程序启用同步之前,同步适配器不会处于活动状态。此时,系统开始管理适配器,并在需要时调用它以在内容提供程序和服务器之间进行同步。

注意:使用帐户类型作为同步适配器标识的一部分,允许系统检测和组合访问同一组织的不同服务的同步适配器。例如,Google 在线服务的同步适配器都具有相同的帐户类型 com.google。当用户向其设备添加 Google 帐户时,所有安装的 Google 服务同步适配器都将一起列出;列出的每个同步适配器都与设备上的不同内容提供程序同步。

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

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

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

虽然不需要身份验证,但大多数联系人服务都使用它。但是,您不需要使用 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`。反过来,此文件指定此身份验证器支持的帐户类型,以及在身份验证过程中出现的 UI 资源。此元素中指定的帐户类型必须与为同步适配器指定的帐户类型相同。

社交流数据

android.provider.ContactsContract.StreamItemsandroid.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_ICONandroid.provider.ContactsContract.StreamItemsColumns#RES_LABELandroid.provider.ContactsContract.StreamItemsColumns#RES_PACKAGE 来链接到应用程序中的资源。

android.provider.ContactsContract.StreamItems 表还包含专供同步适配器使用的列 android.provider.ContactsContract.StreamItemsColumns#SYNC1android.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_IDandroid.provider.ContactsContract.StreamItemPhotosColumns#PHOTO_URI(两者都在以下几点中进行了描述)在一个文件中存储照片。此列现在包含照片的缩略图,可供读取。
android.provider.ContactsContract.StreamItemPhotosColumns#PHOTO_FILE_ID
原始联系人的照片的数字标识符。将此值附加到常量`DisplayPhoto.CONTENT_URI` 以获取指向单个照片文件的 Content URI,然后调用`openAssetFileDescriptor()` 以获取照片文件的句柄。
android.provider.ContactsContract.StreamItemPhotosColumns#PHOTO_URI
直接指向此行表示的照片的照片文件的 Content 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`。查询返回一个包含单行的游标,其中包含单个列android.provider.ContactsContract.StreamItems#MAX_ITEMS

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

社交流交互

联系人提供程序管理的社交流数据与设备的联系人应用程序结合使用,提供了一种将您的社交网络系统与现有联系人连接的强大方法。提供以下功能:

  • 通过使用同步适配器将您的社交网络服务同步到联系人提供程序,您可以检索用户的联系人的最新活动,并将其存储在android.provider.ContactsContract.StreamItemsandroid.provider.ContactsContract.StreamItemPhotos 表中以供日后使用。
  • 除了定期同步外,您还可以触发同步适配器在用户选择查看联系人时检索其他数据。这允许您的同步适配器为联系人检索高分辨率照片和最新的流项目。
  • 通过向设备的联系人应用程序和联系人提供程序注册通知,您可以在查看联系人时接收意图,并在此处从您的服务更新联系人的状态。这种方法可能比使用同步适配器进行完全同步更快,并且带宽使用更少。
  • 用户在查看设备联系人应用程序中的联系人时,可以将联系人添加到您的社交网络服务。您可以通过“邀请联系人”功能启用此功能,您可以通过一个活动(将现有联系人添加到您的网络)和一个 XML 文件(为设备的联系人应用程序和联系人提供程序提供应用程序的详细信息)的组合来启用此功能。

使用联系人提供程序定期同步流项目与其他同步相同。要了解有关同步的更多信息,请参阅联系人提供程序同步适配器 部分。注册通知和邀请联系人将在接下来的两节中介绍。

注册以处理社交网络视图

要注册您的同步适配器以在用户查看由您的同步适配器管理的联系人时接收通知

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

要注册一个活动,以便在用户单击流项目或照片或两者兼而有之的时候调用

  1. 在项目的`res/xml/` 目录中创建一个名为`contacts.xml` 的文件。如果您已经拥有此文件,则可以跳过此步骤。
  2. 在此文件中,添加元素`<ContactsAccountType xmlns:android="http://schemas.android.com/apk/res/android">`。如果此元素已存在,则可以跳过此步骤。
  3. 要注册您的其中一个活动来处理用户在设备联系人应用程序中单击流项目,请向该元素添加属性`viewStreamItemActivity="activityclass"`,其中`activityclass` 是应从设备联系人应用程序接收意图的活动的完全限定类名。
  4. 要注册一个活动来处理用户点击设备联系人应用程序中流照片的情况,请向元素添加属性viewStreamItemPhotoActivity="activityclass",其中activityclass是应接收来自设备联系人应用程序 intent 的活动的完全限定类名。

<ContactsAccountType> 元素在<ContactsAccountType> 元素 部分中有更详细的描述。

传入的 intent 包含用户点击的项目或照片的内容 URI。要为文本项目和照片使用单独的活动,请在同一文件中使用这两个属性。

与您的社交网络服务交互

用户无需离开设备的联系人应用程序即可邀请联系人加入您的社交网络站点。您可以让设备的联系人应用程序发送一个 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 的活动的完全限定类名。invite_action_label 值是在设备联系人应用程序的“添加联系人”菜单中显示的文本字符串。

注意: ContactsSourceContactsAccountType 的已弃用标签名称。

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 组件和 UI 标签,允许用户邀请其联系人之一加入社交网络,在用户的社交网络流更新时通知用户,等等。

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

属性

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

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

viewStreamItemActivity
应用程序中活动的完全限定类名,当用户点击原始联系人的流项目时,设备的联系人应用程序将启动此活动。
viewStreamItemPhotoActivity
应用程序中活动的完全限定类名,当用户点击原始联系人的流项目中的照片时,设备的联系人应用程序将启动此活动。

<ContactsDataKind> 元素

<ContactsDataKind> 元素控制您的应用程序的自定义数据行在联系人应用程序的 UI 中的显示。它具有以下语法:

<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.PhotoContactsContract.RawContacts.DisplayPhoto 的参考文档包含检索照片信息的示例。没有方便的类可以检索原始联系人的主要缩略图,但是您可以向 ContactsContract.Data 表发送查询,根据原始联系人的 _IDPhoto.CONTENT_ITEM_TYPEIS_PRIMARY 列进行选择,以查找原始联系人的主要照片行。

某人的社交流数据也可能包含照片。这些存储在 android.provider.ContactsContract.StreamItemPhotos 表中,在社交流照片 部分中有更详细的描述。