联系人提供程序是一种强大而灵活的 Android 组件,用于管理设备上有关人员的中央数据存储区。联系人提供程序是设备“通讯录”应用中所显示数据的来源,您还可以在自己的应用中访问其数据,并在设备和在线服务之间传输数据。该提供商可处理各种数据源,并尽可能管理每个人的更多数据,因此其组织结构较为复杂。因此,提供方的 API 包含一组广泛的合同类和接口,可帮助您检索和修改数据。
本指南介绍了以下内容:
- 基本提供程序结构。
- 如何从提供程序检索数据。
- 如何修改提供程序中的数据。
- 如何编写同步适配器,以将数据从服务器同步到联系人提供程序。
本指南假定您了解 Android 内容提供程序的基础知识。如需详细了解 Android 内容提供程序,请阅读 内容提供程序基础知识指南。
联系人提供程序结构
联系人提供程序是一种 Android content provider 组件。它会维护有关个人的三种类型的数据,每种数据都对应于提供商提供的表格,如图 1 所示:

图 1. 联系人提供程序表结构。
这三个表通常以其协定类的名称来指代。这些类定义了表所使用的内容 URI、列名称和列值的常量:
-
ContactsContract.Contacts
表 - 基于原始联系人行的汇总,表示不同人员的行。
-
ContactsContract.RawContacts
表 - 包含个人数据摘要的行,特定于用户账号和数据类型。
-
ContactsContract.Data
表 - 包含原始联系人详细信息(例如电子邮件地址或电话号码)的行。
ContactsContract
中由协定类表示的其他表是辅助表,联系人提供程序使用这些表来管理其操作或支持设备联系人或电话应用中的特定功能。
原始联系人
原始联系人表示来自单个账号类型和账号名称的个人数据。由于联系人提供程序允许将多个在线服务作为个人的数据源,因此联系人提供程序允许同一人拥有多个原始联系人。 多个原始联系人还允许用户合并来自多个相同账号类型账号的个人数据。
原始联系人的大部分数据不会存储在 ContactsContract.RawContacts
表中。而是存储在 ContactsContract.Data
表中的一个或多个行中。每个数据行都有一个列 Data.RAW_CONTACT_ID
,其中包含其父级 ContactsContract.RawContacts
行的 RawContacts._ID
值。
重要的原始联系人列
ContactsContract.RawContacts
表中的重要列如表 1 所示。请阅读表格后面的注释:
表 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@gmail.com
的身份登录 Gmail,打开“通讯录”,然后添加“托马斯·希金森”。之后,她以 emilyd@gmail.com
的身份登录 Gmail,并向“Thomas Higginson”发送电子邮件,系统会自动将对方添加为联系人。她还在 Twitter 上关注了“colonel_tom”(托马斯·希金森的 Twitter ID)。
联系人提供程序会创建三个原始联系人,作为此工作的成果:
-
与
emily.dickinson@gmail.com
关联的“Thomas Higginson”的原始联系人。 用户账号类型为 Google。 -
与
emilyd@gmail.com
关联的“Thomas Higginson”的第二个原始联系人。 用户账号类型也是 Google。即使名称与之前的名称相同,也会有第二个原始联系人,因为该人员是为其他用户账号添加的。 - 与“belle_of_amherst”关联的“Thomas Higginson”的第三个原始联系人。用户账号类型为 Twitter。
数据
如前所述,原始联系人的数据存储在与原始联系人的 _ID
值相关联的 ContactsContract.Data
行中。这样,单个原始联系人就可以拥有同一类型数据的多个实例,例如电子邮件地址或电话号码。例如,如果 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
列将联系人行与其原始联系人行相关联。原始联系人表的 CONTACT_ID
列(ContactsContract.RawContacts
)包含与每个原始联系人行关联的联系人行的 _ID
值。
ContactsContract.Contacts
表格还包含 LOOKUP_KEY
列,该列是联系人行的“永久”链接。由于联系人提供程序会自动维护联系人,因此它可能会根据聚合或同步操作更改联系人行的 _ID
值。即使发生这种情况,内容 URI CONTENT_LOOKUP_URI
与联系人的 LOOKUP_KEY
结合使用仍会指向相应联系人行,因此您可以使用 LOOKUP_KEY
来维护指向“收藏”联系人等的链接。此列具有自己的格式,与 _ID
列的格式无关。
图 3 显示了这三个主要表之间的关系。

图 3. “联系人”“原始联系人”和“详细信息”表关系。
注意: 如果您将应用发布到 Google Play 商店,或者您的应用在搭载 Android 10(API 级别 29)或更高版本的设备上运行,请注意,一组有限的联系人数据字段和方法已过时。
在上述条件下,系统会定期清除写入这些数据字段的所有值:
-
ContactsContract.ContactOptionsColumns.LAST_TIME_CONTACTED
-
ContactsContract.ContactOptionsColumns.TIMES_CONTACTED
-
ContactsContract.DataUsageStatColumns.LAST_TIME_USED
-
ContactsContract.DataUsageStatColumns.TIMES_USED
用于设置上述数据字段的 API 也已过时:
此外,以下字段不再返回常用联系人。请注意,只有当联系人属于特定数据类型时,其中一些字段才会影响联系人的排名。
-
ContactsContract.Contacts.CONTENT_FREQUENT_URI
-
ContactsContract.Contacts.CONTENT_STREQUENT_URI
-
ContactsContract.Contacts.CONTENT_STREQUENT_FILTER_URI
-
CONTENT_FILTER_URI
(仅影响电子邮件、电话、可拨打和可联系数据种类) -
ENTERPRISE_CONTENT_FILTER_URI
(仅影响电子邮件、电话和可拨打的数据种类)
如果您的应用正在访问或更新这些字段或 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 账号相同,其账号名称为电子邮件地址。 其他服务可能会使用单字用户名或数字 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. 联系人提供程序中的元数据
表格 | Column | 值 | 含义 |
---|---|---|---|
ContactsContract.RawContacts |
DIRTY |
“0”表示自上次同步以来未发生变化。 |
标记在设备上发生更改且必须同步回服务器的原始联系人。当 Android 应用更新某一行时,通讯录提供程序会自动设置该值。
修改原始联系人或数据表的同步适配器应始终将字符串 |
“1” - 自上次同步以来已发生更改,需要同步回服务器。 | |||
ContactsContract.RawContacts |
VERSION |
此行的版本号。 | 每当相应行或其相关数据发生更改时,联系人提供程序都会自动递增此值。 |
ContactsContract.Data |
DATA_VERSION |
此行的版本号。 | 每当数据行发生更改时,联系人提供程序都会自动递增此值。 |
ContactsContract.RawContacts |
SOURCE_ID |
一个字符串值,用于唯一标识相应原始联系人所属的账号。 |
当同步适配器创建新的原始联系人时,此列应设置为服务器为该原始联系人分配的唯一 ID。当 Android 应用创建新的原始联系人时,该应用应将此列留空。这会向同步适配器发出信号,表明它应在服务器上创建新的原始联系人,并获取 SOURCE_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
显示与所选联系人关联的所有原始联系人中的所有数据行。
此代码段取自“详情”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
获取数据,以显示数据或进一步处理数据。
批量修改
在可能的情况下,您应通过创建 ArrayList
的 ContentProviderOperation
对象并调用 applyBatch()
,在“批量模式”下插入、更新和删除“联系人提供程序”中的数据。由于联系人提供程序在单个事务中执行 applyBatch()
中的所有操作,因此您的修改绝不会使联系人存储库处于不一致的状态。批量修改还可同时插入原始联系人及其详细数据。
注意:如需修改单个原始联系人,请考虑向设备的“通讯录”应用发送 intent,而不是在您的应用中处理修改。使用 intent 进行检索和修改部分更详细地介绍了如何执行此操作。
挂起点
包含大量操作的批量修改可能会阻塞其他进程,从而导致整体用户体验不佳。为了尽可能以最少的单独列表来整理您要执行的所有修改,同时防止这些修改阻塞系统,您应为一个或多个操作设置让步点。收益点是一个 ContentProviderOperation
对象,其 isYieldAllowed()
值设置为 true
。当联系人提供程序遇到让步点时,它会暂停工作,让其他进程运行,并关闭当前事务。当提供程序再次启动时,它会继续执行 ArrayList
中的下一个操作,并启动新事务。
收益点确实会导致每次对 applyBatch()
的调用产生多笔交易。因此,您应为一组相关行的最后一次操作设置一个让步点。
例如,您应为一组添加原始联系人行及其关联数据行的操作中的最后一个操作设置一个让步点,或者为一组与单个联系人相关的行的最后一个操作设置一个让步点。
产出点也是原子操作的单位。两个 yield 点之间的所有访问将作为一个单元成功或失败。如果您未设置任何让步点,则最小的原子操作是整个操作批次。如果您使用让步点,则可以防止操作降低系统性能,同时确保部分操作是原子性的。
修改向后引用
当您要插入新的原始联系人行及其关联的数据行作为一组 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()
,用于设置 yield 点:
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
行时使用乐观并发控制,请按以下步骤操作:
-
检索原始联系人的
VERSION
列以及您检索的其他数据。 -
使用
newAssertQuery(Uri)
方法创建一个适合强制执行限制的ContentProviderOperation.Builder
对象。对于内容 URI,请使用RawContacts.CONTENT_URI
,并在其后附加原始联系人的_ID
。 -
对于
ContentProviderOperation.Builder
对象,请调用withValue()
将VERSION
列与您刚刚检索的版本号进行比较。 -
对于相同的
ContentProviderOperation.Builder
,请调用withExpectedCount()
以确保此断言仅测试一行。 -
调用
build()
以创建ContentProviderOperation
对象,然后将此对象添加为传递给applyBatch()
的ArrayList
中的第一个对象。 - 应用批处理交易。
如果在您读取原始联系人行与尝试修改该行之间,原始联系人行被另一项操作更新,则“断言”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 通过设备的“通讯录”应用访问“通讯录提供程序”时,无需编写自己的界面或代码来访问该提供程序。您也不必请求读取或写入提供程序的权限。设备的“通讯录”应用可以向您委托联系人的读取权限,并且由于您是通过另一个应用修改提供程序,因此您无需具有写入权限。
如需详细了解向提供程序发送 intent 以访问提供程序的一般流程,请参阅
内容提供程序基础知识指南中的“通过 intent 进行数据访问”部分。表 4 总结了您可用于可用任务的操作、MIME 类型和数据值,而您可用于 putExtra()
的 extra 值则列在 ContactsContract.Intents.Insert
的参考文档中:
表 4. 联系人提供程序 intent。
任务 | 操作 | 数据 | MIME 类型 | 备注 |
---|---|---|---|---|
从列表中选择联系人 | ACTION_PICK |
以下值之一:
|
未使用 |
显示原始联系人列表或原始联系人中的数据列表,具体取决于您提供的内容 URI 类型。
调用 |
插入新的原始联系人 | Insert.ACTION |
不适用 |
RawContacts.CONTENT_TYPE ,一组原始联系人的 MIME 类型。
|
显示设备的“通讯录”应用的添加联系人界面。您添加到 intent 中的 extra 值会显示出来。如果使用 startActivityForResult() 发送,则新添加的原始联系人的内容 URI 会在 Intent 实参的“data”字段中传递回 activity 的 onActivityResult() 回调方法。如需获取该值,请调用 getData() 。
|
修改联系人 | ACTION_EDIT |
联系人的 CONTENT_LOOKUP_URI 。编辑者活动将允许用户修改与相应联系人相关联的任何数据。
|
Contacts.CONTENT_ITEM_TYPE ,单个联系人。 |
在“通讯录”应用中显示“修改联系人”界面。系统会显示您添加到 intent 中的 extra 值。当用户点击完成以保存编辑内容时,您的 activity 会返回到前台。 |
显示一个还可以添加数据的选择器。 | ACTION_INSERT_OR_EDIT |
N/A |
CONTENT_ITEM_TYPE
|
此 intent 始终会显示“通讯录”应用的 picker 界面。用户可以选择修改联系人,也可以添加新联系人。系统会显示修改界面或添加界面,具体取决于用户的选择,并显示您在 intent 中传递的 extra 数据。如果您的应用显示电子邮件地址或电话号码等联系人数据,请使用此 intent 允许用户将数据添加到现有联系人。
contact,
注意:无需在此 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.CommonDataKinds.StructuredName
行的ContactsContract.RawContacts
行可能会在汇总期间导致问题。 -
始终将新的
ContactsContract.Data
行与其父级ContactsContract.RawContacts
行相关联。 -
未与
ContactsContract.RawContacts
关联的ContactsContract.Data
行不会显示在设备的联系人应用中,并且可能会导致同步适配器出现问题。 - 仅更改您拥有的原始联系人的数据。
- 请注意,联系人提供程序通常会管理来自多种不同账号类型/在线服务的数据。您需要确保应用仅修改或删除属于您的行的数据,并且仅插入您控制的账号类型和名称的数据。
-
对于授权、内容 URI、URI 路径、列名称、MIME 类型和
TYPE
值,请始终使用ContactsContract
及其子类中定义的常量。 - 使用这些常量有助于避免错误。如果任何常量被弃用,您还会收到编译器警告。
自定义数据行
通过创建和使用您自己的自定义 MIME 类型,您可以在 ContactsContract.Data
表中插入、修改、删除和检索您自己的数据行。您的行只能使用 ContactsContract.DataColumns
中定义的列,不过您可以将自己的特定于类型的列名映射到默认列名。在设备的“通讯录”应用中,系统会显示您行的数据,但无法修改或删除这些数据,用户也无法添加其他数据。如需允许用户修改自定义数据行,您必须在自己的应用中提供编辑器 activity。
如需显示自定义数据,请提供一个 contacts.xml
文件,其中包含 <ContactsAccountType>
元素及其一个或多个 <ContactsDataKind>
子元素。有关详情,请参阅 <ContactsDataKind> element
部分。
如需详细了解自定义 MIME 类型,请阅读 创建内容提供器指南。
联系人提供程序同步适配器
联系人提供程序专门用于处理设备与在线服务之间的联系人数据同步。这样,用户就可以将现有数据下载到新设备,并将现有数据上传到新账号。 同步还可确保用户随时都能获取最新数据,无论添加和更改操作的来源是什么。同步的另一个优点是,即使设备未连接到网络,也能使用联系人数据。
虽然您可以通过多种方式实现同步,但 Android 系统提供了一个插件同步框架,可自动执行以下任务:
- 正在检查网络可用性。
- 根据用户偏好设置安排和执行同步。
- 重新开始已停止的同步。
如需使用此框架,您需要提供同步适配器插件。每个同步适配器都专用于某个服务和内容提供程序,但可以处理同一服务的多个账号名称。该框架还允许同一服务和提供程序使用多个同步适配器。
同步适配器类和文件
您可以将同步适配器实现为 AbstractThreadedSyncAdapter
的子类,并将其作为 Android 应用的一部分进行安装。系统会从应用清单中的元素以及清单指向的特殊 XML 文件中了解同步适配器。该 XML 文件定义了在线服务的账号类型和内容提供程序的授权,这两者共同唯一标识了适配器。在用户为同步适配器的账号类型添加账号并为同步适配器同步的内容提供程序启用同步之前,同步适配器不会变为有效状态。此时,系统开始管理适配器,并在必要时调用该适配器,以在内容提供程序和服务器之间进行同步。
注意:使用账号类型作为同步适配器标识的一部分,可让系统检测并分组来自同一组织但访问不同服务的同步适配器。例如,Google 在线服务的同步适配器都具有相同的账号类型 com.google
。当用户向设备添加 Google 账号时,系统会一并列出所有已安装的 Google 服务同步适配器;列出的每个同步适配器都会与设备上不同的内容提供程序同步。
由于大多数服务都要求用户先验证自己的身份,然后才能访问数据,因此 Android 系统提供了一个身份验证框架,该框架与同步适配器框架类似,并且通常与同步适配器框架结合使用。身份验证框架使用 AbstractAccountAuthenticator
的子类(即插件身份验证器)。身份验证器通过以下步骤验证用户身份:
- 收集用户的姓名、密码或类似信息(用户的凭据)。
- 将凭据发送到服务
- 检查服务的回复。
如果服务接受凭据,身份验证器可以存储凭据以供日后使用。借助插件身份验证器框架,AccountManager
可以提供对身份验证器支持并选择公开的任何身份验证令牌(例如 OAuth2 身份验证令牌)的访问权限。
虽然不需要进行身份验证,但大多数联系人服务都会使用身份验证。 不过,您无需使用 Android 身份验证框架即可进行身份验证。
同步适配器实现
如需为联系人提供程序实现同步适配器,首先要创建一个包含以下内容的 Android 应用:
-
一种
Service
组件,用于响应来自系统的绑定到同步适配器的请求。 -
当系统想要运行同步时,它会调用服务的
onBind()
方法来获取同步适配器的IBinder
。这样,系统就可以对适配器的方法进行跨进程调用。 -
实际的同步适配器,实现为
AbstractThreadedSyncAdapter
的具体子类。 -
此类负责从服务器下载数据、从设备上传数据以及解决冲突。适配器的主要工作是在方法
onPerformSync()
中完成的。此类必须实例化为单例。 -
Application
的子类。 -
此类充当同步适配器单例的工厂。使用
onCreate()
方法实例化同步适配器,并提供一个静态“getter”方法,以将单例返回给同步适配器服务的onBind()
方法。 -
可选:一种
Service
组件,用于响应来自系统的用户身份验证请求。 -
AccountManager
启动此服务以开始身份验证流程。服务的onCreate()
方法会实例化一个身份验证器对象。当系统想要对应用的同步适配器进行用户账号身份验证时,它会调用服务的onBind()
方法来获取身份验证器的IBinder
。这允许系统对身份验证器的方法进行跨进程调用。 -
可选:
AbstractAccountAuthenticator
的具体子类,用于处理身份验证请求。 -
此类提供
AccountManager
调用以使用服务器对用户凭据进行身份验证的方法。身份验证流程的详细信息因所用的服务器技术而异。您应参阅服务器软件的文档,详细了解身份验证。 - 用于向系统定义同步适配器和身份验证器的 XML 文件。
-
前面介绍的同步适配器和身份验证器服务组件在应用清单的
<service>
元素中定义。这些元素包含<meta-data>
子元素,这些子元素可向系统提供特定数据:
社交流数据
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 文件。
与联系人提供程序定期同步流项与其他同步操作相同。如需详细了解同步,请参阅联系人提供程序同步适配器部分。注册通知和邀请联系人将在接下来的两部分中介绍。
通过注册处理社交网络查看
如需注册同步适配器,以便在用户查看由您的同步适配器管理的联系人时接收通知,请执行以下操作:
-
在项目的
res/xml/
目录中创建一个名为contacts.xml
的文件。如果您已有此文件,则可以跳过此步骤。 -
在此文件中,添加元素
<ContactsAccountType xmlns:android="http://schemas.android.com/apk/res/android">
。 如果此元素已存在,您可以跳过此步骤。 -
如需注册一项服务,以便在用户打开设备通讯录应用中某个联系人的详情页面时收到通知,请向元素添加属性
viewContactNotifyService="serviceclass"
,其中serviceclass
是应从设备通讯录应用接收 intent 的服务的完全限定类名。对于通知程序服务,请使用扩展IntentService
的类,以允许该服务接收 intent。传入 intent 中的数据包含用户点击的原始联系人的内容 URI。从通知程序服务中,您可以绑定到同步适配器,然后调用该适配器来更新原始联系人的数据。
如需注册在用户点击信息流项或照片或两者时要调用的 activity,请执行以下操作:
-
在项目的
res/xml/
目录中创建一个名为contacts.xml
的文件。如果您已有此文件,则可以跳过此步骤。 -
在此文件中,添加元素
<ContactsAccountType xmlns:android="http://schemas.android.com/apk/res/android">
。 如果此元素已存在,您可以跳过此步骤。 -
如需注册某个 activity 以处理用户点击设备联系人应用中的流项的操作,请向元素添加
viewStreamItemActivity="activityclass"
属性,其中activityclass
是应接收来自设备联系人应用的 intent 的 activity 的完全限定类名。 -
如需注册某个 activity 以处理用户在设备“通讯录”应用中点击信息流照片的操作,请向元素添加属性
viewStreamItemPhotoActivity="activityclass"
,其中activityclass
是应接收来自设备“通讯录”应用的 intent 的 activity 的完全限定类名。
<ContactsAccountType> 元素部分更详细地介绍了 <ContactsAccountType>
元素。
传入的 intent 包含用户点击的商品或照片的内容 URI。 如需为文字内容和照片分别设置不同的 activity,请在同一文件中使用这两个属性。
与您的社交网络服务进行交互
用户无需离开设备的“通讯录”应用,即可邀请联系人加入您的社交网站。您可以让设备的“通讯录”应用发送 intent,以邀请相应联系人参加您的某项活动。如需设置此模式,请执行以下操作:
-
在项目的
res/xml/
目录中创建一个名为contacts.xml
的文件。如果您已有此文件,则可以跳过此步骤。 -
在此文件中,添加元素
<ContactsAccountType xmlns:android="http://schemas.android.com/apk/res/android">
。 如果此元素已存在,您可以跳过此步骤。 -
添加以下属性:
inviteContactActivity="activityclass"
-
inviteContactActionLabel="@string/invite_action_label"
activityclass
值是应接收 intent 的 activity 的完全限定类名。invite_action_label
值是一个文本字符串,显示在设备通讯录应用的添加关联菜单中。
注意: 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 组件和界面标签,以便用户邀请其某个联系人加入社交网络、在用户的某个社交网络信息流更新时通知用户,等等。
请注意,android:
的属性不需要属性前缀 android:
。
<ContactsAccountType>
属性:
inviteContactActivity
- 应用中某个 activity 的完全限定类名,当用户从设备的“通讯录”应用中选择添加连接时,系统会启动该 activity。
inviteContactActionLabel
-
在添加连接菜单中,为
inviteContactActivity
中指定的 activity 显示的文本字符串。 例如,您可以使用字符串“Follow in my network”。您可以使用字符串资源标识符来表示此标签。 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 drawable 资源。使用此属性向用户表明数据来自您的服务。
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 表中,社交信息流照片部分对此表进行了更详细的介绍。