二十七、探索联系人 API

在第 25 章和第 26 章中,我们讨论了内容供应器和他们的近亲,加载者。我们列出了通过内容提供者抽象公开数据的好处。在内容提供者抽象中,数据被公开为一系列 URL。这些数据 URL 可用于读取、查询、更新、插入和删除。这些 URL 及其对应的光标成为该内容提供者的 API。

Contacts API 就是这样一个用于处理联系人数据的内容提供者 API。Android 中的联系人保存在一个数据库中,并通过一个内容供应器公开,该供应器的权威来源于

content://com.android.contacts

Android SDK 使用一组植根于 Java 包的 Java 接口和类来记录各种 URL 及其返回的数据

android.provider.ContactsContract

您将看到许多父上下文为 ContactsContract 的类;这些在查询、读取、更新和向内容数据库插入联系人时非常有用。使用联系人 API 的主要文档可在 Android 网站上获得,网址为

[https://developer.android.com/guide/topics/providers/contacts-provider.html](https://developer.android.com/guide/topics/providers/contacts-provider.html)

主 API 入口点 ContactsContract 被恰当地命名,因为该类定义了联系人的客户端与联系人数据库的提供者和保护者之间的契约。

本章对这个契约进行了相当详细的探讨,但并没有涵盖每一个细节。Contacts API 很大,触角很远。然而,当您使用 Contacts API 时,需要几周的研究才能意识到它的底层结构很简单。这是我们想贡献最多的地方,在阅读本章的时间里解释这些基础知识。

Android 4.0 扩展了联系人的概念,加入了用户资料,类似于社交网络中的用户资料。用户配置文件是代表设备所有者的专用联系人。大多数基于接触的一般概念保持不变。我们将介绍如何扩展 Contacts API 来支持用户配置文件。

了解账户

Android 中的所有联系人都在一个帐户的上下文中工作。什么是账户?嗯,举个例子,如果你的电子邮件是通过谷歌发的,那么你就有了一个谷歌账户。如果你把自己设置成脸书的用户,你就拥有了脸书的账户。您可以通过设备上的“帐户与同步”设置选项来设置这些帐户。请参阅 Android 用户指南,了解有关帐户以及如何设置帐户的更多详细信息。

您管理的联系人与特定帐户相关联。一个帐户拥有它的一组联系人,或者说一个帐户是一个联系人的父代。帐户由两个字符串标识:帐户名和帐户类型。在谷歌的情况下,你的账户名是你在 Gmail 的电子邮件用户名,你的账户类型是 com.google 。帐户类型在设备中必须是唯一的。您的帐户名称在该帐户类型中是唯一的。帐户类型和帐户名称一起构成了一个帐户,只有在帐户形成后,才能为该帐户插入一组联系人。

枚举帐户

Contacts API 主要处理存在于各种帐户中的联系人。创建帐户的机制不在 Contacts API 的范围之内,所以解释编写自己的帐户提供程序的能力以及如何在这些帐户中同步联系人不在本章的讨论范围之内。你可以理解并受益于这一章,而不必深入到如何建立账户的细节中。但是,当您想要添加联系人或联系人列表时,您确实需要知道设备上存在哪些帐户。您可以使用清单 27-1 中的代码来枚举帐户及其属性(帐户名和类型)。清单 27-1 中的代码列出了给定上下文变量(如活动)的账户名称和类型。

清单 27-1 。显示帐户列表的代码

public void listAccounts(Context ctx) {
    AccountManager am = AccountManager.get(ctx);
    Account[] accounts = am.getAccounts();
    for(Account ac: accounts) {
        String account_name=ac.name;
        String account_type = ac.type;
        Log.d("accountInfo", account_name + ":" + account_type);
    }
}

要运行清单 27-1 中的代码,清单文件需要使用清单 27-2 中的行请求许可。

清单 27-2 。读取帐户的权限

<uses-permission android:name="android.permission.GET_ACCOUNTS"/>

来自清单 27-1 的代码将打印出如下内容:

Your-email-at-gmail:com.google

这假设您只配置了一个帐户(Google)。如果您有多个帐户,所有这些帐户将以类似的方式列出。

使用设备上的“联系人”应用,您可以添加、编辑和删除任何现有帐户的联系人。

了解联系人

客户拥有的联系人称为原始联系人。原始联系人有一组可变的数据元素(例如,电子邮件地址、电话号码、姓名和邮政地址)。Android 通过只列出一次任何似乎匹配的原始联系人来呈现原始联系人的聚合视图。这些汇总的联系人构成了您在打开“联系人”应用时看到的一组联系人。

我们现在将研究联系人及其相关数据是如何存储在各种表中的。理解这些联系人表及其相关视图是理解联系人 API 的关键。

检查联系人 SQLite 数据库

理解和检查联系人数据库表的一种方法是从设备或模拟器下载联系人数据库,并使用 SQLite explorer 工具之一打开它。

要下载联系人数据库,请使用图 30-17 所示的文件资源管理器,并导航到仿真器上的以下目录:

/data/data/com.android.providers.contacts/databases

根据版本的不同,数据库文件名可能会略有不同,但应该称为 contacts.db 、 contacts2.db 或类似的名称。在 4.0 中,联系人提供程序使用一个结构相似但独立的数据库文件,名为 profile.db ,用于保存与个人资料相关的联系人。

了解原始联系人

您在联系人应用中看到的联系人称为聚合联系人。在每个聚集的联系人下面是一组称为原始联系人的联系人。聚合联系人是一组相似的原始联系人的视图。

属于一个帐户的一组联系人称为原始联系人。每个原始联系人指向该帐户上下文中一个人的详细信息。这与聚合联系人相反,聚合联系人跨越帐户边界,并作为一个整体属于设备。帐户与其原始联系人集之间的这种关系在原始联系人表中维护。清单 27-3 显示了联系人数据库中原始联系人表的结构。

清单 27-3 。原始联系表定义

CREATE TABLE raw_contacts
(_id INTEGER PRIMARY KEY AUTOINCREMENT,
is_restricted        INTEGER DEFAULT 0,
account_name         STRING DEFAULT NULL,
account_type         STRING DEFAULT NULL,
sourceid             TEXT,
version              INTEGER NOT NULL DEFAULT 1,
dirty                INTEGER NOT NULL DEFAULT 0,
deleted              INTEGER NOT NULL DEFAULT 0,
contact_id           INTEGER REFERENCES contacts(_id),
aggregation_mode     INTEGER NOT NULL DEFAULT 0,
aggregation_needed   INTEGER NOT NULL DEFAULT 1,
custom_ringtone      TEXT
send_to_voicemail    INTEGER NOT NULL DEFAULT 0,
times_contacted      INTEGER NOT NULL DEFAULT 0,
last_time_contacted  INTEGER,
starred              INTEGER NOT NULL DEFAULT 0,
display_name         TEXT,
display_name_alt     TEXT,
display_name_source  INTEGER NOT NULL DEFAULT 0,
phonetic_name        TEXT,
phonetic_name_style  TEXT,
sort_key             TEXT COLLATE PHONEBOOK,
sort_key_alt         TEXT COLLATE PHONEBOOK,
name_verified        INTEGER NOT NULL DEFAULT 0,
contact_in_visible_group  INTEGER NOT NULL DEFAULT 0,
sync1 TEXT, sync2         TEXT, sync3 TEXT, sync4 TEXT )

与大多数 Android 表一样,原始联系人表具有唯一标识原始联系人的 _ID 列。该字段的 account_name 和 account_type 一起识别该联系人(具体地说,原始联系人)所属的账户。 sourceid 字段指示如何在帐户中唯一标识该原始联系人。

字段 contact_id 指的是该原始联系人所属的聚合联系人。聚合联系人指向一个或多个相似的联系人,这些联系人实质上是在多个帐户中设置的同一个人。

字段显示名称指向联系人的显示名称。这主要是一个只读字段。它是由触发器根据添加到该原始联系人的数据表(将在下一小节中介绍)中的数据行设置的。

帐户使用同步字段来同步设备和服务器端帐户(如 Google mail)之间的联系人。

尽管我们使用了 SQLite 工具来探索这些领域,但是发现这些领域的方法不止一种。推荐的方法是遵循在 ContactsContract API 中声明的类定义。要浏览属于原始联系人的列,可以查看 ContactsContract 的类文档。原始联系人。

这种方法有优点也有缺点。一个显著的优势是,您可以了解 Android SDK 发布和认可的领域。可以在不改变公共接口的情况下添加或删除数据库列。因此,如果您直接使用数据库列,它们可能存在,也可能不存在。相反,如果您对这些列使用公共定义,那么您在两个版本之间是安全的。

然而,一个缺点是,类文档中有许多其他的常数散布在列名中;我们有点迷失在搞清楚什么是什么的过程中。这些众多的类定义给人一种 API 很复杂的印象,而实际上,Contacts API 的 80%的类文档都是为这些列定义常量,并为访问这些行定义 URIs。

当我们在后面的小节中练习 Contacts API 时,我们将使用基于类文档的常量,而不是直接的列名。但是,我们认为直接浏览表是帮助您理解 Contacts API 的最快方法。

接下来让我们讨论一下与联系人相关的数据(如电子邮件和电话号码)是如何存储的。

了解联系人数据表

从原始联系人表定义可以看出,原始联系人(从虎头蛇尾的意义上来说)只是一个 ID,表示它属于哪个帐户。与联系人相关的数据不在原始联系人表中,而是保存在数据表中。每个数据元素,比如电子邮件和电话号码,都作为单独的行存储在数据表中,由原始联系人 ID 绑定。数据表的定义如清单 27-4 所示,包含 16 个通用列,可以存储任何类型的数据元素,如电子邮件。

清单 27-4 。接触数据表定义

CREATE TABLE data
(_id              INTEGER PRIMARY KEY AUTOINCREMENT,
package_id        INTEGER REFERENCES package(_id),
mimetype_id       INTEGER REFERENCES mimetype(_id) NOT NULL,
raw_contact_id    INTEGER REFERENCES raw_contacts(_id) NOT NULL,
is_primary        INTEGER NOT NULL DEFAULT 0,
is_super_primary  INTEGER NOT NULL DEFAULT 0,
data_version      INTEGER NOT NULL DEFAULT 0,
data1 TEXT,data2 TEXT,data3 TEXT,data4 TEXT,data5 TEXT,
data6 TEXT,data7 TEXT,data8 TEXT,data9 TEXT,data10 TEXT,
data11 TEXT,data12 TEXT,data13 TEXT,data14 TEXT,data15 TEXT,
data_sync1 TEXT, data_sync2 TEXT, data_sync3 TEXT, data_sync4 TEXT )

raw_contact_id 指向该数据行所属的原始联系人。 mimetype_id 指向 MIME 类型条目,指示在清单 27-4 中的联系人数据类型中标识的类型之一。列 data1 到 data15 是通用的基于字符串的表,可以根据 MIME 类型存储任何必要的内容。同步字段支持联系人同步。解析 MIME 类型 id 的表格在清单 27-5 中。

清单 27-5 。联系人 MIME 类型查找表定义

CREATE TABLE mimetypes
(_id INTEGER PRIMARY KEY AUTOINCREMENT,
mimetype TEXT NOT NULL)

与原始 contacts 表一样,您可以通过 ContactsContract 的 helper 类文档来发现数据表列。数据。虽然您可以从这个类定义中找出列,但是您不会知道从数据 1 到数据 15 的每个通用列中存储了什么。要了解这一点,您需要查看名称空间 ContactsContract 下许多类的类定义。常用数据种类。

这些类别的一些示例如下:

  • 联系人联系人。CommonDataKinds.Email
  • 联系人联系人。CommonDataKinds.Phone

事实上,您将看到每个预定义 MIME 类型都有一个类。这些类如下:邮件、事件、群组成员、身份、 Im 、昵称、备注、组织、电话、照片、关系、 SipAddress 、结构名称最终, CommonDataKinds 类所做的就是指出哪些通用数据字段( data1 到 data15 )正在使用以及用途。

了解汇总联系人

最终,联系人及其相关数据明确地存储在原始联系人表和数据表中。另一方面,聚合联系是启发式的,可能是不明确的。

当多个帐户之间有相同的联系人时,您可能希望看到一个姓名,而不是看到相同或相似的姓名在每个帐户中重复出现一次。Android 通过将联系人聚集到一个只读视图中来解决这个问题。Android 将这些聚集的联系人存储在一个名为 contacts 的表中。Android 在原始联系人表和数据表上使用许多触发器来填充或更改这个聚合联系人表。

在解释聚合背后的逻辑之前,让我们给你看一下联系表的定义(见清单 27-6 )。

清单 27-6 。聚合联系人表定义

CREATE TABLE contacts
(_id                  INTEGER PRIMARY KEY AUTOINCREMENT,
name_raw_contact_id   INTEGER REFERENCES raw_contacts(_id),
photo_id              INTEGER REFERENCES data(_id),
custom_ringtone       TEXT,
send_to_voicemail     INTEGER NOT NULL DEFAULT 0,
times_contacted       INTEGER NOT NULL DEFAULT 0,
last_time_contacted   INTEGER,
starred               INTEGER NOT NULL DEFAULT 0,
in_visible_group      INTEGER NOT NULL DEFAULT 1,
has_phone_number      INTEGER NOT NULL DEFAULT 0,
lookup                TEXT,
status_update_id      INTEGER REFERENCES data(_id),
single_is_restricted  INTEGER NOT NULL DEFAULT 0)

没有客户端直接更新该表。当添加一个原始联系人及其详细信息时,Android 会搜索其他原始联系人,以查看是否有类似的原始联系人。如果有,它将使用该原始联系人的聚合联系人 ID 作为新的原始联系人的聚合联系人 ID。聚合联系人表中没有条目。如果没有找到,它将创建一个聚合联系人,并将该聚合联系人用作该原始联系人的联系人 ID。

Android 使用以下算法来确定哪些原始联系人是相似的:

  1. 这两个原始联系人具有匹配的姓名。
  2. 名称中的单词是相同的,但顺序不同:“first last”或“first,last”或“last,first”
  3. 姓名的较短版本匹配,例如“Bob”代表“Robert”
  4. 如果其中一个原始联系人只有名字或姓氏,这将触发对其他属性的搜索,如电话号码或电子邮件,如果其他属性匹配,该联系人将被聚合。
  5. 如果其中一个原始联系人完全丢失了姓名,这也将触发对其他属性的搜索,如步骤 4 所示。

因为这些规则是启发式的,所以一些联系人可能会被无意地聚集。在这种情况下,客户端应用需要提供一种机制来分离联系人。如果你参考 Android 用户指南,你会看到默认的联系人应用允许你分离无意中合并的联系人。

您还可以通过在插入原始联系人时设置聚合模式来阻止聚合。聚集模式如清单 27-7 所示。

清单 27-7 。聚合模式常数

AGGREGATION_MODE_DEFAULT
AGGREGATION_MODE_DISABLED
AGGREGATION_MODE_SUSPENDED

第一种选择是显而易见的;这就是聚合的工作方式。

第二个选项( disabled )将这个原始联系人排除在聚合之外。即使它已经被聚合,Android 也会将其从聚合中取出,并为该原始联系人分配一个新的聚合联系人 id。

第三个选项( suspended )表示即使联系人的属性可能改变,这将使其不能聚合到该批联系人中,也应该保持与该聚合联系人的联系。

最后一点引出了聚合联系人的可变维度。假设您有一个包含名字和姓氏的唯一原始联系人。现在,它不匹配任何其他原始联系人,因此这个唯一的原始联系人获得它自己的聚合联系人分配。聚合的联系人 ID 将存储在原始联系人表中,与原始联系人行相对应。

但是,您可以更改这个原始联系人的姓氏,使其与另一组聚合的联系人相匹配。在这种情况下,Android 将从这个聚合联系人中删除原始联系人,并将其移动到另一个,自己放弃这个单个聚合联系人。在这种情况下,聚合联系人的 ID 完全被放弃,因为它在将来不会匹配任何内容,因为它只是一个没有底层原始联系人的 ID。

所以聚集接触是不稳定的。随着时间的推移,保持这个聚集的联系人 ID 没有重要的价值。

Android 通过在聚合联系人表中提供一个名为 lookup 的字段来缓解这种困境。此查找字段是帐户和该帐户中每个原始联系人的唯一 ID 的聚合(串联)。该信息被进一步编码,以便可以作为 URL 参数传递,从而检索最新的聚合联系人 ID。Android 查看查找关键字,并查看该查找关键字有哪些基础的原始联系人 id。然后,它使用最佳算法返回一个合适的(或者可能是新的)聚合联系人 ID。

当我们明确地检查联系人数据库时,让我们考虑几个有用的与联系人相关的数据库视图。

浏览视图 _ 联系人

这些视图的第一个是视图 _ 联系人 。虽然有一个保存聚合联系人的表(contacts 表),但是 API 并不直接公开 contacts 表。相反,它使用 view_contacts 作为读取聚合联系人的目标。当您基于 URI ContactsContract 进行查询时。Contacts.CONTENT_URI ,返回的列基于这个视图 view_contacts 。视图 _ 联系人视图的定义如清单 27-8 所示。

清单 27-8 。读取聚合联系人的视图

CREATE VIEW view_contacts AS

SELECT contacts._id AS _id,
contacts.custom_ringtone                AS custom_ringtone,
name_raw_contact.display_name_source    AS display_name_source,
name_raw_contact.display_name           AS display_name,
name_raw_contact.display_name_alt       AS display_name_alt,
name_raw_contact.phonetic_name          AS phonetic_name,
name_raw_contact.phonetic_name_style    AS phonetic_name_style,
name_raw_contact.sort_key               AS sort_key,
name_raw_contact.sort_key_alt           AS sort_key_alt,
name_raw_contact.contact_in_visible_group AS in_visible_group,
has_phone_number,
lookup,
photo_id,
contacts.last_time_contacted           AS last_time_contacted,
contacts.send_to_voicemail             AS send_to_voicemail,
contacts.starred                       AS starred,
contacts.times_contacted               AS times_contacted, status_update_id

FROM contacts JOIN raw_contacts AS name_raw_contact
ON(name_raw_contact_id=name_raw_contact._id)

请注意,视图 _contacts 视图根据聚合的联系人 ID 将 contacts 表与原始 contact 表组合在一起。

探索联系人 _ 实体 _ 视图

另一个有用的视图是 contact _ entities _ view,它将原始的 contacts 表与数据表结合在一起。这个视图允许我们一次检索给定原始联系人的所有数据元素,甚至是属于同一个聚合联系人的多个原始联系人的数据元素。清单 27-9 给出了基于联系实体的视图的定义。

清单 27-9 。联系人实体视图

CREATE VIEW contact_entities_view AS

SELECT raw_contacts.account_name    AS account_name,
raw_contacts.account_type           AS account_type,
raw_contacts.sourceid               AS sourceid,
raw_contacts.version                AS version,
raw_contacts.dirty                  AS dirty,
raw_contacts.deleted                AS deleted,
raw_contacts.name_verified          AS name_verified,
package                             AS res_package,
contact_id,
raw_contacts.sync1                  AS sync1,
raw_contacts.sync2                  AS sync2,
raw_contacts.sync3                  AS sync3,
raw_contacts.sync4                  AS sync4,
mimetype, data1, data2, data3, data4, data5, data6, data7, data8,
data9, data10, data11, data12, data13, data14, data15,
data_sync1, data_sync2, data_sync3, data_sync4,

raw_contacts._id                    AS _id,

is_primary, is_super_primary,
data_version,
data._id                            AS data_id,
raw_contacts.starred                AS starred,
raw_contacts.is_restricted          AS is_restricted,
groups.sourceid                     AS group_sourceid

FROM raw_contacts LEFT OUTER JOIN data
   ON (data.raw_contact_id=raw_contacts._id)
LEFT OUTER JOIN packages
  ON (data.package_id=packages._id)
LEFT OUTER JOIN mimetypes
  ON (data.mimetype_id=mimetypes._id)
LEFT OUTER JOIN groups
  ON (mimetypes.mimetype='vnd.android.cursor.item/group_membership'
    AND groups._id=data.data1)

访问该视图所需的 URIs 在 类 ContactsContract 中可用。RawContactsEntity 。

使用联系人 API

到目前为止,我们已经通过研究 Contacts API 的表和视图探索了它背后的基本思想。我们现在将展示一些可用于探索联系人的代码片段。这些片段摘自为支持本章而开发的示例应用。尽管这些片段来自示例应用,但它们足以帮助理解 Contacts API 是如何工作的。您可以使用本章末尾的项目下载 URL 下载完整的示例程序。

探索帐户

我们将通过编写一个可以打印出帐户列表的程序来开始我们的练习。我们已经给出了获取帐户列表所需的代码片段。考虑清单 27-10 中的类 AccountsFunctionTester。

清单 27-10 。 AccountsFunctionTester 打印可用账户

//Java class: AccountsFunctionTester.java
//Menu to invoke this: Accounts
//BaseTester is a supporting base class holding the parent activity
// and some reused common variables. See the source code if you are more curious.
public class AccountsFunctionTester extends BaseTester {
    private static String tag = "tc>";

    //IReportBack is a simple logging interface that writes log messages
    //to the main activity and also to the log.
    public AccountsFunctionTester(Context ctx, IReportBack target) {
        super(ctx, target);
    }
    public void testAccounts() {
        AccountManager am = AccountManager.get(this.mContext);
        Account[] accounts = am.getAccounts();
        for(Account ac: accounts) {
            String acname=ac.name;
            String actype = ac.type;
            this.mReportTo.reportBack(tag,acname + ":" + actype);
        }
    }
}

注意在我们展示和探索使用联系人所需的 Java 代码时,您会看到在展示的源代码中重复使用了三个变量:

mContext :一个指向活动的变量

mReportTo :一个实现日志接口的变量(ireport back—你可以在可下载的项目中看到这个 Java 文件),它可以用来将消息记录到本章使用的测试活动中

Utils :封装了非常简单的实用方法的静态类

我们选择不在这里列出这些类,因为它们会分散您对 Contacts API 核心功能的理解。您可以在可下载的项目中检查这些类。

本章中的所有代码都使用针对内容提供者的非托管查询。这是通过调用 Activity.getContentResolver()来完成的。查询()。这是因为我们只是读取数据并立即打印出结果。如果你的目标是使用 UI(通过活动或片段)作为显示你的联系人的目标,那么请阅读第 27 章关于加载器的内容。加载器显示了显示来自任何内容提供者的光标的正确方式。

当您运行您可以为本章下载的示例程序时,您将看到一个主活动,它带有许多菜单选项。菜单选项“帐户”将打印设备上可用的帐户列表。

探索聚合联系人

让我们看看如何通过代码片段探索聚合联系人。若要读取联系人,您需要在清单文件中请求以下权限:

android.permission.READ_CONTACTS

由于我们测试的功能涉及内容提供者、URIs 和光标,让我们来看看清单 27-11 中的一些有用的代码片段。(这些代码片段可以在 utils.java 的或者从本章的可下载项目中的 BaseTester 派生的一些基类中获得。)

清单 27-11 。给定一个 URI 和一个 where 子句,获取一个游标

//Utils.java
//Retrieve a column from a cursor
public static String getColumnValue(Cursor cc, String cname) {
   int i = cc.getColumnIndex(cname);
   return cc.getString(i);
}
//See what columns are there  in a cursor
protected static String getCursorColumnNames(Cursor c) {
   int count = c.getColumnCount();
   StringBuffer cnamesBuffer = new StringBuffer();
    for (int i=0;i<count;i++) {
       String cname = c.getColumnName(i);
       cnamesBuffer.append(cname).append(';');
    }
    return cnamesBuffer.toString();
}

//From URIFunctionTester.java, baseclass of some of the other testers
//Given a URI and a where clause return a cursor
protected Cursor getACursor(Uri uri,String clause) {
   Activity a = (Activity)this.mContext; //mContext coming from BaseTester
   return a.getContentResolver().query(uri, null, clause, null, null);
}

在本节中,我们主要探索由聚合联系人 URIs 返回的游标。由产生的联系人光标返回的每一行将有许多字段。对于我们的例子,我们对所有的领域都不感兴趣,只对少数领域感兴趣。您可以将其抽象为另一个名为 AggregatedContact 的类。清单 27-12 展示了这个类。

清单 27-12 。聚合联系人的几个字段的对象定义

//AggregatedContact.java
public class AggregatedContact {
    public String id;
    public String lookupUri;
    public String lookupKey;
    public String displayName;
    public void fillinFrom(Cursor c) {
        id = Utils.getColumnValue(c,"_ID");
        lookupKey = Utils.getColumnValue(c,ContactsContract.Contacts.LOOKUP_KEY);
        lookupUri = ContactsContract.Contacts.CONTENT_LOOKUP_URI + "/" + lookupKey;
        displayName = Utils.getColumnValue(c,ContactsContract.Contacts.DISPLAY_NAME);
    }
}

在清单 27-12 中,我们使用光标来加载我们感兴趣的字段。

获取聚集联系人光标

清单 27-13 展示了如何检索一个聚集联系人集合的光标。

清单 27-13 。获取所有聚合联系人的光标

//Get a cursor of all contacts. Specify the where clause as null to indicate all rows.
//Java class: AggregatedContactFunctionTester.java
//Menu item to invoke: Contacts Cursor
private Cursor getContacts() {
    Uri uri = ContactsContract.Contacts.CONTENT_URI;
    //Specify ascending or descending way to sort names
    String sortOrder = ContactsContract.Contacts.DISPLAY_NAME
                         + " COLLATE LOCALIZED ASC";
    Activity a = (Activity)this.mContext; //Local variable pointing to an activity
    return a.getContentResolver().query(uri, null, null, null, sortOrder);
}

用于读取所有联系人的 URI 是 contacts contact。联系人.内容 _URI 。您可以将这个 URI 传递给 q uery() 函数来检索光标。您可以传递 null 作为列投影来接收所有列。虽然在实践中不建议这样做,但在我们的例子中,这样做是有意义的,因为我们想知道它返回的所有列。我们还使用联系人的显示名称作为排序顺序。再次注意我们是如何使用 ContactContract 的。联系人获取联系人的列名,显示 名称。如果你要从这个光标打印字段名,你会看到返回的字段,如清单 27-14 所示。根据版本的不同,顺序可能会有所不同,并且可能会添加更多的列。显式指定查询子句的投影是一种好的做法;这样,您的代码将跨版本工作。

清单 27-14 。汇总联系人内容 URI 光标列

times_contacted; contact_status; custom_ringtone; has_phone_number; phonetic_name;
phonetic_name_style; contact_status_label; lookup; contact_status_icon; last_time_contacted;
display_name; sort_key_alt; in_visible_group; _id; starred; sort_key; display_name_alt;
contact_presence; display_name_source; contact_status_res_package; contact_status_ts;
photo_id; send_to_voicemail;

读取汇总的联系人详细信息

现在我们已经研究了联系人内容 URI 中可用的列,让我们挑选几列,看看有哪些联系人行可用。我们对联系人光标的以下几列感兴趣:显示名称、查找关键字和查找 URI。我们之所以考虑这些字段,是因为我们希望根据本章理论部分的内容来了解查找关键字和查找关键字 URI 的情况。具体来说,我们感兴趣的是启动查找 URI,看看它返回什么类型的游标。

清单 27-15 中的函数 listContacts() 获取一个联系人光标,并为光标的每一行打印这三列。请注意,这个清单来自一个类,该类包含一个名为 mContext 的局部变量来指示活动,还包含一个名为 mReportTo 的局部变量来记录活动的任何消息。

清单 27-15 。打印汇总联系人的查找关键字

//Java class: AggregatedContactFunctionTester.java
//Menu item to invoke: Contacts
public void listContacts() {
    Cursor c = null;
    try {
        c = getContacts();
        int i = c.getColumnCount();
        this.mReportTo.reportBack(tag, "Number of columns:" + i);
        this.printLookupKeys(c);
    }
   finally { if (c!= null) c.close(); }
}
private void printLookupKeys(Cursor c) {
    for(c.moveToFirst();!c.isAfterLast();c.moveToNext()) {
        String name=this.getContactName(c);
        String lookupKey = this.getLookupKey(c);
        String luri = this.getLookupUri(lookupKey);
        this.mReportTo.reportBack(tag, name + ":" + lookupKey); //log
        this.mReportTo.reportBack(tag, name + ":" + luri); //log
    }
}
private String getLookupKey(Cursor cc) {
    int lookupkeyIndex = cc.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY);
    return cc.getString(lookupkeyIndex);
}
private String getContactName(Cursor cc){
    return Utils.getColumnValue(cc,ContactsContract.Contacts.DISPLAY_NAME);
}
private String getLookupUri(String lookupkey) {
    String luri = ContactsContract.Contacts.CONTENT_LOOKUP_URI + "/" + lookupkey;
    return luri;
}

探索查找 URI 光标

既然我们知道了如何为给定的聚合联系人提取查找 URIs,那么让我们来看看我们可以用查找 URI 做些什么。

清单 27-16 中的函数 listlookupricolumns()将从所有联系人列表中取出第一个联系人,然后为该联系人制定一个查找 URI,并启动 URI,通过打印该光标的列名来查看它返回哪种光标。

清单 27-16 。探索查找 URI 光标

//Class: AggregatedContactFunctionTester.java, Menu item to invoke: Single Contact Cursor
public void listLookupUriColumns() {
    Cursor c = null;
    try {
        c = getContacts();
        String firstContactLookupUri = getFirstLookupUri(c);
        printLookupUriColumns(firstContactLookupUri);
    }
    finally { if (c!= null) c.close(); }
}
private String getFirstLookupUri(Cursor c) {
    c.moveToFirst();
    if (c.isAfterLast()) {
        Log.d(tag,"No rows to get the first contact");
        return null;
    }
    String lookupKey = this.getLookupKey(c);
    return  this.getLookupUri(lookupKey);
}
public void printLookupUriColumns(String lookupuri) {
    Cursor c = null;
    try {
        c = getASingleContact(lookupuri);
        int i = c.getColumnCount();
        this.mReportTo.reportBack(tag, "Number of columns:" + i);
        int j = c.getCount();
        this.mReportTo.reportBack(tag, "Number of rows:" + j);
        this.printCursorColumnNames(c);
    }
    finally { if (c!=null)c.close(); }
}
// Use the lookup uri, retrieve a single aggregated contact
private Cursor getASingleContact(String lookupUri) {
    Activity a = (Activity)this.mContext;
    return a.getContentResolver().query(Uri.parse(lookupUri), null, null, null, null);
}

事实证明,它只是返回一个游标(如清单 27-14 中的)与清单 27-13 中的中的聚合联系人游标在列上是相同的,除了它只有一行指向查找关键字的联系人。另请注意,我们使用了以下代码 URI 定义:

ContactsContract.Contacts.CONTENT_LOOKUP_URI

从对联系人查找 URIs 的讨论中可以看出,每个查找 URI 都代表一个已连接的原始联系人标识的集合。既然如此,您可能希望查找 URI 返回一系列匹配的原始联系人。然而,清单 27-16 中的测试表明,它返回的不是原始联系人的光标,而是联系人的光标。

注意基于联系人查找 URI 的查找返回汇总联系人,而不是原始联系人。

另一个趣闻是,基于查找 URI 的聚集联系人的查找过程不是线性的或精确的。这意味着 Android 不会寻找查找键的精确匹配。相反,Android 将查找关键字解析为其组成的原始联系人,然后找到与大多数原始联系人记录匹配的聚合联系人 id,并返回该聚合联系人记录。

这样做的一个后果是,没有公共机制可以从查找键转到它的原始联系人。相反,您必须找到该查找关键字的联系人 ID,然后为该联系人 ID 生成一个原始联系人 URI,以检索相应的原始联系人。

下面是另一个代码片段,显示了从游标返回的对象,而不是一组列。清单 27-17 中的代码将第一个聚集的联系人作为 一个 对象返回。

清单 27-17 。代码测试聚合联系人

//Java class: AggregatedContactFunctionTester.java
protected AggregatedContact getFirstContact() {
    Cursor c=null;
    try {
        c = getContacts(); c.moveToFirst();
        if (c.isAfterLast()) {
            Log.d(tag,"No contacts");
            return null;
        }
        AggregatedContact firstcontact = new AggregatedContact();
        firstcontact.fillinFrom(c);
        return firstcontact;
    }
    finally { if (c!=null) c.close(); }
}

探索原始联系人

清单 27-18 中,文件【RawContact.java 从原始联系人表光标中捕获了几个重要字段。(与本章中的所有其他代码片段一样,这个文件可以在本章的可下载项目中找到。)

清单 27-18 。源代码为 RawContact.javaT8】

//Class: RawContact.java
public class RawContact  {
    public String rawContactId;
    public String aggregatedContactId;
    public String accountName;
    public String accountType;
    public String displayName;

    public void fillinFrom(Cursor c) {
        rawContactId = Utils.getColumnValue(c,"_ID");
        accountName = Utils.getColumnValue(c,ContactsContract.RawContacts.ACCOUNT_NAME);
        accountType = Utils.getColumnValue(c,ContactsContract.RawContacts.ACCOUNT_TYPE);
        aggregatedContactId = Utils.getColumnValue(c,
                                        ContactsContract.RawContacts.CONTACT_ID);
        displayName = Utils.getColumnValue(c,"display_name");
    }
    public String toString() { //..prints the public fields. See the download project for details }
}//eof-class

显示原始联系人光标

与聚合联系人 URIs 一样,让我们首先检查原始联系人 URI 的性质及其返回的内容。原始联系人 URI 的签名定义如下:

ContactsContract.RawContacts.CONTENT_URI

清单 27-19 中的函数 showRawContactsCursor()打印原始联系人 URI 的光标列。

清单 27-19 。浏览原始联系人光标

//Java class: RawContactFunctionTester.java; Menu item: Raw Contacts Cursor
public void showRawContactsCursor() {
    Cursor c = null;
    try {
        c = this.getACursor(ContactsContract.RawContacts.CONTENT_URI,null);
        this.printCursorColumnNames(c);
    }
    finally { if (c!=null) c.close(); }
}

清单 27-19 中的代码将显示原始接触光标具有清单 27-20中所示的字段(该列表似乎因设备不同而有所不同)。

清单 27-20 。原始联系人光标字段

times_contacted; phonetic_name; phonetic_name_style; contact_id;version; last_time_contacted;
aggregation_mode; _id; name_verified; display_name_source; dirty; send_to_voicemail; account_type; custom_ringtone; sync4;sync3;sync2;sync1; deleted; account_name; display_name;
sort_key_alt; starred; sort_key; display_name_alt; sourceid;

查看原始联系人光标返回的数据

清单 27-21 显示了方法 showAllRawContacts() ,它打印原始联系人光标中的所有行。

清单 27-21 。显示原始联系人

//Java class: RawContactFunctionTester.java; Menu item: All Raw Contacts
public void showAllRawContacts(){
    Cursor c = null;
    try {
        c = this.getACursor(getRawContactsUri(), null);
        this.printRawContacts(c);
    }
    finally { if (c!=null) c.close(); }
}
private void printRawContacts(Cursor c) {
    for(c.moveToFirst();!c.isAfterLast();c.moveToNext()) {
        RawContact rc = new RawContact();
        rc.fillinFrom(c);
        this.mReportTo.reportBack(tag, rc.toString()); //log
    }
}

用一组对应的聚集联系人约束原始联系人

使用清单 27-20 中的光标列,让我们看看是否可以细化我们的查询,以检索给定聚合联系人 ID 的联系人。清单 27-22 中的代码将查找第一个聚合联系人,然后发出一个带有 where 子句的原始联系人 URI,该子句为 contact_id 列指定一个值。

清单 27-22 。获取聚合联系人的原始联系人

//Java class: RawContactFunctionTester.java; Menu item: Raw Contacts
public void showRawContactsForFirstAggregatedContact(){
    AggregatedContact ac = getFirstContact();
    Cursor c = null;
    try {
        c = this.getACursor(getRawContactsUri(), getClause(ac.id));
        this.printRawContacts(c);
    }
    finally { if (c!=null) c.close(); }
}
private String getClause(String contactId) {
    return "contact_id = " + contactId;
}

探索原始联系人数据

因为属于原始联系人的数据行包含许多字段,所以我们创建了一个名为 ContactData.java 的 Java 类,如清单 27-23 所示,来捕获联系人数据的代表性集合,而不是所有字段。

清单 27-23 。源代码为 ContactData.javaT8】

//ContactData.java
public class ContactData {
    public String rawContactId;
    public String aggregatedContactId;
    public String dataId;
    public String accountName;
    public String accountType;
    public String mimetype;
    public String data1;

    public void fillinFrom(Cursor c) {
        rawContactId = Utils.getColumnValue(c,"_ID");
        accountName = Utils.getColumnValue(c,ContactsContract.RawContacts.ACCOUNT_NAME);
        accountType = Utils.getColumnValue(c,ContactsContract.RawContacts.ACCOUNT_TYPE);
        aggregatedContactId =
               Utils.getColumnValue(c,ContactsContract.RawContacts.CONTACT_ID);
        mimetype = Utils.getColumnValue(c,ContactsContract.RawContactsEntity.MIMETYPE);
        data1 = Utils.getColumnValue(c,ContactsContract.RawContactsEntity.DATA1);
        dataId = Utils.getColumnValue(c,ContactsContract.RawContactsEntity.DATA_ID);
    }
    public String toString()   {//just a concatenation of fields for logging }
}

Android 使用一个名为 RawContactEntity 视图的视图来从原始联系人表和相应的数据表中检索数据,如本章“contact_entities_view”一节所述。访问这个视图的 URI 在清单 27-24 中。

清单 27-24 。原始实体含量 URI

ContactsContract.RawContactsEntity.CONTENT_URI

让我们看看如何使用这个 URI 来发现这个 URI 返回的字段名称:

//Java class: ContactDataFunctionTester.java; Menu item: Contact Entity Cursor
public void showRawContactsEntityCursor(){
    Cursor c = null;
    try {
        Uri uri = ContactsContract.RawContactsEntity.CONTENT_URI;
        c = this.getACursor(uri,null);
        this.printCursorColumnNames(c);
    }
    finally { if (c!=null) c.close(); }
}

清单 27-24 中的代码打印出清单 27-25 中所示的列列表。因此,清单 27-25 中的列是由原始联系人实体光标返回的列。根据供应商特定的实现,可能还有其他列。

清单 27-25 。联系人实体光标列

data_version; contact_id; version; data12;data11;data10; mimetype; res_package;
_id; data15;data14;data13; name_verified; is_restricted; is_super_primary; data_sync1;dirty;data_sync3;data_sync2; data_sync4;account_type;data1;sync4;sync3;
data4;sync2;data5;sync1; data2;data3;data8;data9; deleted; group_sourceid; data6;data7;
account_name; data_id; starred; sourceid; is_primary;

一旦知道了这组列,就可以通过制定适当的 where 子句来过滤这个游标的结果集。然而,您想要使用 ContactsContract Java 类来使用这些列名的定义。例如,在清单 27-26 中,我们检索与联系人 IDs 3、4 和 5 相关的数据元素。

清单 27-26 。显示来自 RawContactsEntity 的数据元素

//Java class: ContactDataFunctionTester.java; Menu item: Contact Data
public void showRawContactsData(){
    Cursor c = null;
    try {
        Uri uri = ContactsContract.RawContactsEntity.CONTENT_URI;
        c = this.getACursor(uri,"contact_id in (3,4,5)");
        this.printRawContactsData(c);
    }
    finally { if (c!=null) c.close(); }
}
protected void printRawContactsData(Cursor c) {
    for(c.moveToFirst();!c.isAfterLast();c.moveToNext()) {
        ContactData dataRecord = new ContactData();
        dataRecord.fillinFrom(c);
        this.mReportTo.reportBack(tag, dataRecord.toString());
    }
}

清单 27-26 中的代码将打印姓名、电子邮件地址和 MIME 类型,如清单 27-23 中的 ContactData 对象所定义的那样。

添加联系人及其详细信息

让我们来看一个添加联系人姓名、电子邮件和电话号码的代码片段。要写入联系人,您需要清单文件中的以下权限:

android.permission.WRITE_CONTACTS

清单 27-27 中的代码添加了一个原始联系人,然后为该联系人添加了两个数据行(姓名和电话号码)。

清单 27-27 。添加联系人

//Java class: AddContactFunctionTester.java; Menu item: Add Contact
public void addContact(){
    long rawContactId = insertRawContact();
    this.mReportTo.reportBack(tag, "RawcontactId:" + rawContactId);
    insertName(rawContactId);
    insertPhoneNumber(rawContactId);
    showRawContactsDataForRawContact(rawContactId);
}
private long insertRawContact(){
    ContentValues cv = new ContentValues();
    cv.put(RawContacts.ACCOUNT_TYPE, "com.google");
    cv.put(RawContacts.ACCOUNT_NAME, "--use your gmail id -- ");
    Uri rawContactUri =
        this.mContext.getContentResolver()
             .insert(RawContacts.CONTENT_URI, cv);
    long rawContactId = ContentUris.parseId(rawContactUri);
    return rawContactId;
}
private void insertName(long rawContactId) {
    ContentValues cv = new ContentValues();
    cv.put(Data.RAW_CONTACT_ID, rawContactId);
    cv.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
    cv.put(StructuredName.DISPLAY_NAME,"John Doe_" + rawContactId);
    this.mContext.getContentResolver().insert(Data.CONTENT_URI, cv);
}
private void insertPhoneNumber(long rawContactId) {
    ContentValues cv = new ContentValues();
    cv.put(Data.RAW_CONTACT_ID, rawContactId);
    cv.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
    cv.put(Phone.NUMBER,"123 123 " + rawContactId);
    cv.put(Phone.TYPE,Phone.TYPE_HOME);
    this.mContext.getContentResolver().insert(Data.CONTENT_URI, cv);
}
private void showRawContactsDataForRawContact(long rawContactId) {
    Cursor c = null;
    try {
        Uri uri = ContactsContract.RawContactsEntity.CONTENT_URI;
        c = this.getACursor(uri,"_id = " + rawContactId);
        this.printRawContactsData(c);
    }
    finally { if (c!=null) c.close(); }
}

清单 27-27 中的代码执行以下操作:

  1. 使用帐户的名称和类型为预定义帐户添加新的原始联系人,由方法 insertRawContact() 表示。注意它是如何使用 URI RawContact 的。内容 _URI 。
  2. 从步骤 1 中获取原始联系人 ID,并使用 insertName() 方法在数据表中插入姓名记录。注意它是如何使用 URI 数据的。内容 _URI 。
  3. 从步骤 1 中获取原始联系人 ID,并在数据表中使用 insertPhoneNumber() 方法插入一个电话号码记录。作为数据行,它使用数据。内容 _URI 为 URI。

清单 27-27 还展示了插入记录时使用的列别名。注意像电话这样的常量。键入和的电话。编号指向通用数据表的列名 data1 和 data2 。

控制联系人的聚合

更新或插入联系人的客户端不会显式更改联系人表。联系人表由查看原始联系人表和原始联系人数据表的触发器更新。

添加或更改的原始联系人反过来会影响 contacts 表中的聚合联系人。但是,您可能不希望聚合两个联系人。

通过在创建合同时设置聚合模式,可以控制原始联系人的聚合行为。从清单 27-20 中的原始联系表列可以看出,原始联系表包含一个名为 aggregation_mode 的字段。聚集模式的值在清单 27-7 中显示,并在“聚集联系人”一节中解释

您还可以通过向名为 agg_exceptions 的表中插入行来保持两个联系人始终分开。需要插入到这个表中的 URIs 在 Java 类 ContactsContract 中定义。聚合异常。 agg_exceptions 的表结构如清单 27-28 所示。

清单 27-28 。聚集例外表定义

CREATE TABLE agg_exceptions
(_id INTEGER PRIMARY KEY AUTOINCREMENT,
type INTEGER NOT NULL,
raw_contact_id1 INTEGER REFERENCES raw_contacts(_id),
raw_contact_id2 INTEGER REFERENCES raw_contacts(_id))

清单 27-28 中的类型列保存了清单 27-29 中的一个整数常量。

清单 27-29 。聚集例外表中的聚集类型

ContactsContract.AggregationExceptions.TYPE_KEEP_TOGETHER
ContactsContract.AggregationExceptions.TYPE_KEEP_SEPARATE
ContactsContract.AggregationExceptions.TYPE_AUTOMATIC

TYPE_KEEP_TOGETHER 表示这两个原始接触永远不应该分开。 TYPE_KEEP_SEPARATE 表示这些原始的联系永远不应该被连接。 TYPE_AUTOMATIC 表示使用默认算法来聚合联系人。

您将用来插入、读取和更新该表的 URI 被定义为

ContactsContract.AggregationExceptions.CONTENT_URI

Java 类 ContactsContract 中也提供了用于该表的字段定义的常量。聚合异常。

了解个人资料

API 14 中引入的个人资料类似于联系人,只是只有一个个人资料联系人。这就是你,在你的设备上。

然而,作为一个实现细节,与单个个人资料联系人相关的所有数据都保存在一个名为 profile.db 的单独数据库中。我们的研究表明,该数据库具有与联系人 2.db 相同的结构。这意味着您已经知道可用的相关表以及每个表的列。

作为单个联系人,聚合非常简单。被添加到个人简档中的每个原始联系人都被期望属于单个聚集联系人。如果不存在,则创建一个新的聚合联系人,并将其放在新的原始联系人中。如果存在,该联系人 ID 将用作原始联系人的聚合联系人 ID。

Android SDK 使用相同的基类 ContactsContract 来定义必要的 URIs,以读取/更新/删除/添加原始联系人到个人资料。这些 URIs 与它们的对应者相似,但是在它们的某个地方有一根弦【轮廓】。清单 27-30 展示了其中的一些 URIs。

清单 27-30 。4.0 中引入的基于配置文件的 URIs

//Relates to profile aggregated contact
ContactsContract.Profile.CONTENT_URI

//Relates to profile based raw contact
ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI

//Relates to profile based raw contact + profile based data table
ContactsContract.RawContactsEntity.PROFILE_CONTENT_URI

清单 27-30 显示了在处理聚集联系和原始联系时,我们有单独的 URIs。然而,对于数据表,没有相应的个人简介 URI。同样的数据 URI,数据。内容 _URI ,既适用于常规联系人数据,也适用于个人资料联系人数据。

还要注意,同一个内容供应器同时满足个人简档和常规联系人的需求。在内部,该内容供应器基于原始联系人 ID 知道数据 URI 属于简档数据还是常规联系人数据。

接下来让我们看看读取联系人数据并将其添加到个人资料中的代码片段。您将需要清单 27-31 中的权限来读写概要文件数据。

清单 27-31 。读取/写入个人资料数据的权限

<uses-permission android:name="android.permission.READ_PROFILE"/>
<uses-permission android:name="android.permission.WRITE_PROFILE"/>

读取档案原始联系人

让我们使用下面的 URI 来读取属于个人资料的原始联系人:

ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI

清单 27-32 显示了如何读取档案原始联系人条目。

清单 27-32 。显示所有简档原始联系人

//Java class: ProfileRawContactFunctionTester.java; Menu item: PRaw Contacts
//In the download this method is named showAllRawContacts
//It is expanded here for clarity.
public void showAllRawProfileContacts() {
    Cursor c = null;
    try {
        String whereClause = null;
        c = this.getACursor(ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI,
            whereClause);
        this.printRawContacts(c);
    }
    finally { if (c!=null) c.close(); }
}
//In the download this method is named printRawContacts
//It is expanded here for clarity.
private void printRawProfileContacts(Cursor c) {
    for(c.moveToFirst();!c.isAfterLast();c.moveToNext()) {
        RawContact rc = new RawContact();
        rc.fillinFrom(c);
        this.mReportTo.reportBack(tag, rc.toString());
    }
}

请注意,一旦我们检索到光标,它包含的数据将与我们之前为常规原始联系人定义的 RawContact 相匹配。

读取个人资料联系人数据

让我们使用下面的 URI 来读取属于个人配置文件的原始联系人的各种数据元素(比如电子邮件、MIME 类型等等):

ContactsContract.RawContactsEntity.PROFILE_CONTENT_URI

请注意我们是如何使用与常规联系人相似的视图的。 RawContactEntity 是原始联系人和属于该原始联系人的数据行之间的连接。我们将看到每个数据元素占一行,比如姓名、电子邮件、MIME 类型等等。

清单 27-33 显示了读取 profile 原始联系人条目的代码片段。

清单 27-33 。显示个人资料联系人的数据元素

//Java class: ProfileContactDataFunctionTester.java; Menu item: all p raw contacts
public void showProfileRawContactsData() {
    Cursor c = null;
    try {
        Uri uri = ContactsContract.RawContactsEntity.PROFILE_CONTENT_URI;
        String whereClause = null;
        c = this.getACursor(uri,whereClause);
        this.printProfileRawContactsData(c);
    }
    finally { if (c!=null) c.close(); }
}
protected void printProfileRawContactsData(Cursor c) {
    for(c.moveToFirst();!c.isAfterLast();c.moveToNext()) {
        ContactData dataRecord = new ContactData();
        dataRecord.fillinFrom(c);
        this.mReportTo.reportBack(tag, dataRecord.toString());
    }
}

请注意,一旦我们检索到光标,它包含的数据就与我们之前为常规原始联系人数据元素定义的 ContactData 对象(清单 27-23 )相匹配。

向个人档案添加数据

让我们使用以下 URI 将原始联系人添加到个人资料中:

ContactsContract.RawContactsEntity.PROFILE_CONTENT_URI

我们还将向该原始联系人添加一些数据元素,如电话号码和昵称,以便它们出现在设备上您个人资料的详细信息中。清单 27-34 显示了代码片段。

清单 27-34 。添加简档原始联系人

//Java class: AddProfileContactFunctionTester.java; Menu item: all p raw contacts
//In the source code you won't see the word "profile" in the following method names
//It is added here to add clarity as the whole class is not included
public void addProfileContact() {
    long rawContactId = insertProfileRawContact();
    this.mReportTo.reportBack(tag, "RawcontactId:" + rawContactId);
    insertProfileNickName(rawContactId);
    insertProfilePhoneNumber(rawContactId);
    showProfileRawContactsDataForRawContact(rawContactId);
}
private void insertProfileNickName(long rawContactId) {
    ContentValues cv = new ContentValues();
    cv.put(Data.RAW_CONTACT_ID, rawContactId);
    //cv.put(Data.IS_USER_PROFILE, "1");
    cv.put(Data.MIMETYPE, CommonDataKinds.Nickname.CONTENT_ITEM_TYPE);
    cv.put(CommonDataKinds.Nickname.NAME,"PJohn Nickname_" + rawContactId);
    this.mContext.getContentResolver().insert(Data.CONTENT_URI, cv);
}
private void insertProfilePhoneNumber(long rawContactId) {
    ContentValues cv = new ContentValues();
    cv.put(Data.RAW_CONTACT_ID, rawContactId);
    cv.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
    cv.put(Phone.NUMBER,"P123 123 " + rawContactId);
    cv.put(Phone.TYPE,Phone.TYPE_HOME);
    this.mContext.getContentResolver().insert(Data.CONTENT_URI, cv);
}
private long insertProfileRawContact() {
    ContentValues cv = new ContentValues();
    cv.put(RawContacts.ACCOUNT_TYPE, "com.google");
    cv.put(RawContacts.ACCOUNT_NAME, "--use your gmail id --");
    Uri rawContactUri =
        this.mContext.getContentResolver()
             .insert(ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI, cv);
    long rawContactId = ContentUris.parseId(rawContactUri);
    return rawContactId;
}
private void showProfileRawContactsDataForRawContact(long rawContactId) {
    Cursor c = null;
    try {
        Uri uri = ContactsContract.RawContactsEntity.PROFILE_CONTENT_URI;
        c = this.getACursor(uri,"_id = " + rawContactId);
        this.printRawContactsData(c);
    }
    finally { if (c!=null) c.close(); }
}

清单 27-34 中的代码与我们用来添加常规联系人及其详细信息的代码相似(清单 27-27 )。尽管我们使用了特定于概要文件的 URI 来添加原始联系人,但是我们使用了相同的数据。内容 _URI 添加单个数据元素。

注意清单 27-34 中的注释掉的代码:

//cv.put(Data.IS_USER_PROFILE, "1");

因为数据。CONTENT_URI 不特定于简档,底层内容供应器如何知道是将该数据插入常规原始联系人还是个人简档原始联系人?我们认为指定一个名为的列会对内容供应器有所帮助。显然不是。这个新列主要用于读取目的。如果在插入过程中指定此项,插入将会失败。唯一的结论是,内容供应器依靠原始联系人 ID 来查看该原始联系人是来自 profile.db 还是 contacts2.db 。

同步适配器的作用

到目前为止,我们主要讨论了如何操作设备上的联系人。然而,Android 上的帐户及其联系人与基于服务器的联系人密切相关。例如,如果你已经在安卓手机上创建了一个谷歌账户,谷歌账户将提取你的 Gmail 联系人,并使他们在设备上可用。为了做到这一点,Android 提供了一个同步框架,只要你编写一个符合标准的同步适配器,它就可以完成大部分的基础工作。Android 的同步框架负责网络可用性、可选认证和调度。

实现同步适配器包括通过扩展 SDK 类 AbstractThreadedSyncAdapter 来实现服务,并在方法 onperformatsync()中完成工作。该方法涉及的工作是从服务器加载数据,并使用本章中讨论的 Contacts API 更新联系人。然后,需要在设备上创建同步适配器资源文件(XML ),该文件将描述该服务如何与需要同步的帐户相关联。

除了这个基本的理解之外,由于篇幅的限制,我们在本书的这个版本中没有涉及同步 API。Android SDK 文档有一些文档和示例。

联系人的同步对删除设备上的联系人有影响。当您使用汇总联系人 URI 删除联系人时,将删除其所有对应的原始联系人以及每个原始联系人的数据元素。然而,Android 只会在设备上将它们标记为已删除,并期望后台同步实际上与服务器同步,然后从设备上永久删除联系人。这种删除的级联也发生在原始联系人级别,其中该原始联系人的相应数据元素被删除。

使用批处理操作优化 ContentProvider 更新

当在第 26 章中讨论内容提供者时,我们指出我们将在本章中讨论批处理操作。

重新思考一下在本章前面如何创建原始联系人及其关联的数据元素。请注意,我们需要向 contacts 提供者发送多个命令来插入一个原始联系人。首先,我们必须插入原始接触。然后使用该 ID 插入属于该原始联系人的多个数据元素。这些插入中的每一个都是独立发送给内容供应器的单独命令。

当我们顺序发送这多个命令时,有两个问题。第一个问题是内容提供者不知道它们属于单个提交单元。第二个问题是更新内容供应器数据库需要更长的时间,因为每个事务都是自己提交的。

这两个问题由可用于任何内容提供者(包括联系提供者)的批量更新 API 来解决。

批量更新内容供应器的想法

在批处理方法中,每个内容提供者更新操作都封装在一个名为“ContentProviderOperation”的对象中,还有 URI 和执行该操作所需的所有键/值对。然后将这些操作收集到一个列表对象中。然后告诉内容解析器同时将整批命令或命令列表发送给内容提供者。因为内容提供者知道这些命令是成批的,所以它会根据提示在最后或经常适当地应用事务。

如果一个操作指示事务可以在该操作结束时应用,那么到目前为止完成的操作将被提交。这允许您将许多行的长时间更新分成更小的子行集。您还可以在操作中指示要更新的列之一需要使用由索引的先前操作返回的键。我们现在将展示一些展示这些想法如何工作的样本代码。

清单 27-35 显示了一个创建列表对象来保存操作列表的例子。

清单 27-35 。用于内容供应器操作的容器

ArrayList<ContentProviderOperation> ops =  new ArrayList<ContentProviderOperation>();

现在让我们看看如何构造添加到清单 27-36 中的单个操作。

清单 27-36 。批处理 ContentProviderOperations

ContentProviderOperation.Builder op = ContentProviderOperation.newInsert(a content URI);
op.withValue(key, value);
//...more of these
ContentProviderOperation op1 = op.build();
ops.add(op1);

关键类是 ContentProviderOperation 及其对应的 Builder 对象。在这里的例子中,我们使用插入操作。对于其余的方法,请参见类参考。一旦我们有了一个构建器及其相关的内容 URI,我们告诉构建器添加一组与内容 URI 一起的键/值对。一旦添加完所有的键/值对,我们就从构建器中生成 ContentProviderOperation ,并将其添加到列表中。然后我们要求内容解析器使用清单 27-37 中的代码来应用批量操作。

清单 27-37 。使用内容解析来应用批量操作

activity.getContentResolver().applyBatch(contentProviderAuthority, ops);

清单 27-37 中,参数 contentProviderAuthority 是指向内容供应器的授权字符串,参数 ops 是应该批量应用于该内容供应器的操作列表。这是一个将一系列更新操作作为单个事务添加的示例。现在让我们看看如何向提供提交提示,以便可以在给定批处理的较小子集上完成提交操作。

批处理通过让步提交

将大量命令作为单个事务提交的一个问题是,这项工作可能会阻塞数据库上的其他操作。为了有助于这一点,也为了有助于在单个事务中提交太多的工作,您可以指示一个操作放弃。当内容提供者识别出某个操作的 yield 参数时,它会提交已完成的工作并暂停,以便让出其他流程来运行。

注意在清单 27-38 的代码中,一个操作是如何被设置为允许 yield 的。

清单 27-38 。在 ContentProviderOperation 中使用 Yield

ContentProviderOperation.Builder operationBuilder =
      ContentProviderOperation.newInsert(a content URI);
operationBuilder.withValue(key, value);
//...more of these key/value pairs when you have them
ContentProviderOperation op1 = operationBuilder.build();

//... Add More operations

//Mark the next operation as yield allowed
operationBuilder = ContentProviderOperation.newInsert(a content URI);
operationBuilder.withValue(key, value);
operationBuilder.withYieldAllowed(true); //it is ok to commit
ContentProviderOperation operationWithYield = operationBuilder.build();
ops.add(operationWithYield);

//... Add More operations and yield points as needed

//Finally apply the list of operations
activity.getContentResolver().apply(contentProviderAuthority, ops);

使用反向引用

对于上面的一个操作,你可以使用一个反向引用,如清单 27-39 所示。

清单 27-39 。在 ContentProviderOperation 中使用反向引用

//Take the key coming out of op1 and add it as the value
int indexOfTheOperationWhoseKeyYouNeed = 0;
op.withValueBackReference(mykey, indexOfTheOperationWhoseKeyYouNeed);

清单 27-39 中的代码要求内容提供者运行由列表索引 indexOfTheOperationWhoseKeyYouNeed 指示的操作,并获取其生成的主键,并将其用作在目标操作上设置的列的值。这就是如何从原始联系人获取插入,并使用其主键作为属于该原始联系人的数据项的键值。

乐观锁定

在乐观锁定中,您首先在不锁定底层存储库的情况下应用事务,并查看自从您知道它的值之后是否进行了任何更新。如果是,请取消交易并重试。

为了在批处理模式下实现这一点,API 提供了一种称为断言查询的操作。在这种类型的操作中,内容提供者进行查询,并比较检索到的光标的值,以获得计数或某些键的值。如果它们不匹配,它将回滚事务并引发一个中断代码流的异常。请看清单 27-40 中的代码演示。

清单 27-40 。通过 newAssertQuery 使用乐观锁定

try {
  //Read a raw contact for a particular raw contact id
  ContentProviderOperation.Builder assertOpBuilder =
              ContentProviderOperation.newAssertQuery(rawContactUri);
  //Make sure there is only one raw contact with that details
  assertOpBuilder.withExpectedCount(1);
  //Make sure the version column matches with you started with
  //If not throw an exception. We chose to compare the version number
  //column (field) in the raw contacts table to assert.
  assertOpBuilder.withValue(SyncColumns.VERSION, mVersion);
  //get this operation and add it to the operations list at the end
  //Apply the batch ...
  activityInstance.getContentResolver().applyBatch(...);
}
//for this or other exceptions
catch (OperationApplicationException e) {
  //The batch is already cancelled
  //Tell the user the update failed
  //Show the user the new details and repeat the process
}

重用联系人提供者用户界面

Android 中的联系人提供者功能还定义了一组意图,可用于重用联系人应用中可用的 UI。

有三种意图。联系提供者基于内容提供者 UI 应用中发生的事件触发一组意图。例如,当用户在联系人应用中点击联系人上的“邀请到网络”按钮时,触发 intent INVITE_CONTACT。一个应用可以注册这个事件并读取联系信息。

当联系人提供者充当您的定制活动的搜索提供者时,会用到另一组意图。使用此功能,您可以通过搜索建议在自定义应用中搜索联系人。

外部应用可以使用另一组意图来重用联系人应用提供的 UI。您可以使用这些意图从联系人列表、电话号码列表、地址列表或电子邮件列表中进行选择。您还可以使用这些意图来更新联系人,或者使用 Android 应用提供的 UI 来创建联系人。

这些意图记录在 ContactsContract 的类引用中。意图。

使用组功能

联系人 API 提供了清单 27-41 中的所示的契约来处理联系人的群组特性

清单 27-41 。集团联系合同

ContactsContract.Groups
ContactsContract.CommonDataKinds.GroupMembership

groups 表保存诸如组的名称、关于该组的注释以及成员的一些组级别计数。原始联系人所属的组保存在数据表中。

使用照片功能

您可以使用清单 27-42 中显示的类契约来探索联系人的照片相关信息。

清单 27-42 。联系照片合同

ContactsContract.Contacts.Photo
ContactsContract.RawContacts.DisplayPhoto

这些协定的类文档包含描述如何使用这些功能的示例代码。

参考

以下是本章所涵盖主题的附加资源:

摘要

在本章中,我们介绍了以下内容:联系人 API 的性质,探索联系人数据库,探索联系人 API URIs 及其光标,读取和添加联系人,聚合原始联系人,个人资料和联系人之间的关系,以及读取和添加联系人到个人资料。我们还简要介绍了批处理提供者操作,使用联系人提供者作为联系人的搜索提供者。