Android Developer - ContentProvider

部分同学可能无法访问https://developer.android.com,将里面的文档贴出来供更多人学习,原链接https://developer.android.com/guide/topics/providers/content-providers.html


内容提供程序

主题

  1. 内容提供程序基础知识
  2. 创建内容提供程序
  3. 日历提供程序
  4. 联系人提供程序

相关示例

  1. 联系人管理器应用
  2. “游标(联系人)”
  3. “游标(电话)”
  4. 示例同步适配器

内容提供程序管理对结构化数据集的访问。它们封装数据,并提供用于定义数据安全性的机制。 内容提供程序是连接一个进程中的数据与另一个进程中运行的代码的标准界面。

如果您想要访问内容提供程序中的数据,可以将应用的 Context 中的 ContentResolver 对象用作客户端来与提供程序通信。 ContentResolver 对象会与提供程序对象(即实现 ContentProvider 的类实例)通信。 提供程序对象从客户端接收数据请求,执行请求的操作并返回结果。

如果您不打算与其他应用共享数据,则无需开发自己的提供程序。 不过,您需要通过自己的提供程序在您自己的应用中提供自定义搜索建议。 如果您想将复杂的数据或文件从您的应用复制并粘贴到其他应用中,也需要创建您自己的提供程序。

Android 本身包括的内容提供程序可管理音频、视频、图像和个人联系信息等数据。 android.provider 软件包参考文档中列出了部分提供程序。 任何 Android 应用都可以访问这些提供程序,但会受到某些限制。

以下主题对内容提供程序做了更详尽的描述:

内容提供程序基础知识
如何访问内容提供程序中以表形式组织的数据。
创建内容提供程序
如何创建您自己的内容提供程序。
日历提供程序
如何访问作为 Android 平台一部分的日历提供程序。
联系人提供程序
如何访问作为 Android 平台一部分的联系人提供程序。


内容提供程序基础知识

本文内容

  1. 概览
    1. 访问提供程序
    2. 内容 URI
  2. 从提供程序检索数据
    1. 请求读取访问权限
    2. 构建查询
    3. 显示查询结果
    4. 从查询结果中获取数据
  3. 内容提供程序权限
  4. 插入、更新和删除数据
    1. 插入数据
    2. 更新数据
    3. 删除数据
  5. 提供程序数据类型
  6. 提供程序访问的替代形式
    1. 批量访问
    2. 通过 Intent 访问数据
  7. 协定类
  8. MIME 类型引用

关键类

  1. ContentProvider
  2. ContentResolver
  3. Cursor
  4. Uri

相关示例

  1. 游标(联系人)
  2. 游标(电话)

另请参阅

  1. 创建内容提供程序
  2. 日历提供程序

内容提供程序管理对中央数据存储区的访问。提供程序是 Android 应用的一部分,通常提供自己的 UI 来使用数据。 但是,内容提供程序主要旨在供其他应用使用,这些应用使用提供程序客户端对象来访问提供程序。 提供程序与提供程序客户端共同提供一致的标准数据界面,该界面还可处理跨进程通信并保护数据访问的安全性。

本主题介绍了以下基础知识:

  • 内容提供程序的工作方式。
  • 用于从内容提供程序检索数据的 API。
  • 用于在内容提供程序中插入、更新或删除数据的 API。
  • 其他有助于使用提供程序的 API 功能。

概览


内容提供程序以一个或多个表(与在关系型数据库中找到的表类似)的形式将数据呈现给外部应用。 行表示提供程序收集的某种数据类型的实例,行中的每个列表示为实例收集的每条数据。

例如,Android 平台的内置提供程序之一是用户字典,它会存储用户想要保存的非标准字词的拼写。 表 1 描述了数据在此提供程序表中的显示情况:

表 1. 用户字典示例表格。

字词 应用 id 频率 语言区域 _ID
mapreduce user1 100 en_US 1
precompiler user14 200 fr_FR 2
applet user2 225 fr_CA 3
const user1 255 pt_BR 4
int user5 100 en_UK 5

在表 1 中,每行表示可能无法在标准词典中找到的字词实例。 每列表示该字词的某些数据,如该字词首次出现时的语言区域。 列标题是存储在提供程序中的列名称。 要引用行的语言区域,需要引用其 locale 列。对于此提供程序,_ID 列充当由提供程序自动维护的“主键”列。

注:提供程序无需具有主键,也无需将 _ID 用作其主键的列名称(如果存在主键)。 但是,如果您要将来自提供程序的数据与 ListView 绑定,则其中一个列名称必须是 _ID。 显示查询结果部分详细说明了此要求。

访问提供程序

应用从具有 ContentResolver 客户端对象的内容提供程序访问数据。 此对象具有调用提供程序对象(ContentProvider 的某个具体子类的实例)中同名方法的方法。 ContentResolver 方法可提供持续存储的基本“CRUD”(创建、检索、更新和删除)功能。

客户端应用进程中的 ContentResolver 对象和拥有提供程序的应用中的 ContentProvider 对象可自动处理跨进程通信。 ContentProvider 还可充当其数据存储区和表格形式的数据外部显示之间的抽象层。

注:要访问提供程序,您的应用通常需要在其清单文件中请求特定权限。 内容提供程序权限部分详细介绍了此内容。

例如,要从用户字典提供程序中获取字词及其语言区域的列表,则需调用 ContentResolver.query()。 query() 方法会调用用户字典提供程序所定义的ContentProvider.query() 方法。 以下代码行显示了 ContentResolver.query() 调用:

// Queries the user dictionary and returns results
mCursor = getContentResolver().query(
    UserDictionary.Words.CONTENT_URI,   // The content URI of the words table
    mProjection,                        // The columns to return for each row
    mSelectionClause                    // Selection criteria
    mSelectionArgs,                     // Selection criteria
    mSortOrder);                        // The sort order for the returned rows

表 2 显示了 query(Uri,projection,selection,selectionArgs,sortOrder) 的参数如何匹配 SQL SELECT 语句:

表 2. Query() 与 SQL 查询对比。

query() 参数 SELECT 关键字/参数 说明
Uri FROM table_name Uri 映射至提供程序中名为 table_name 的表。
projection col,col,col,... projection 是应该为检索到的每个行包含的列的数组。
selection WHERE col = value selection 会指定选择行的条件。
selectionArgs (没有完全等效项。选择参数会替换选择子句中 ? 占位符。)
sortOrder ORDER BY col,col,... sortOrder 指定行在返回的 Cursor 中的显示顺序。

内容 URI

内容 URI 是用于在提供程序中标识数据的 URI。内容 URI 包括整个提供程序的符号名称(其授权)和一个指向表的名称(路径)。 当您调用客户端方法来访问提供程序中的表时,该表的内容 URI 将是其参数之一。

在前面的代码行中,常量 CONTENT_URI 包含用户字典的“字词”表的内容 URI。 ContentResolver 对象会分析出 URI 的授权,并通过将该授权与已知提供程序的系统表进行比较,来“解析”提供程序。 然后, ContentResolver 可以将查询参数分派给正确的提供程序。

ContentProvider 使用内容 URI 的路径部分来选择要访问的表。 提供程序通常会为其公开的每个表显示一条路径

在前面的代码行中,“字词”表的完整 URI 是:

content://user_dictionary/words

其中,user_dictionary 字符串是提供程序的授权,words 字符串是表的路径。 字符串 content://架构)始终显示,并将此标识为内容 URI。

许多提供程序都允许您通过将 ID 值追加到 URI 末尾来访问表中的单个行。 例如,要从用户字典中检索 _ID 为 4 的行,则可使用此内容 URI:

Uri singleUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);

在检索到一组行后想要更新或删除其中某一行时通常会用到 ID 值。

注:Uri 和 Uri.Builder 类包含根据字符串构建格式规范的 URI 对象的便利方法。 ContentUris 包含一些可以将 ID 值轻松追加到 URI 后的方法。 前面的代码段就是使用 withAppendedId() 将 ID 追加到 UserDictionary 内容 URI 后。

从提供程序检索数据


本节将以用户字典提供程序为例,介绍如何从提供程序中检索数据。

为了明确进行说明,本节中的代码段将在“UI 线程”上调用 ContentResolver.query()。但在实际代码中,您应该在单独线程上异步执行查询。 执行此操作的方式之一是使用 CursorLoader 类,加载器指南中对此有更为详细的介绍。 此外,前述代码行只是片段;它们不会显示整个应用。

要从提供程序中检索数据,请按照以下基本步骤执行操作:

  1. 请求对提供程序的读取访问权限。
  2. 定义将查询发送至提供程序的代码。

请求读取访问权限

要从提供程序检索数据,您的应需要具备对提供程序的“读取访问”权限。 您无法在运行时请求此权限;相反,您需要使用元素和提供程序定义的准确权限名称,在清单文件中指明您需要此权限。 在您的清单文件中指定此元素后,您将有效地为应用“请求”此权限。 用户安装您的应用时,会隐式授予允许此请求。

要找出您正在使用的提供程序的读取访问权限的准确名称,以及提供程序使用的其他访问权限的名称,请查看提供程序的文档。

内容提供程序权限部分详细介绍了权限在访问提供程序过程中的作用。

用户字典提供程序在其清单文件中定义了权限 android.permission.READ_USER_DICTIONARY,因此希望从提供程序中进行读取的应用必需请求此权限。

构建查询

从提供程序中检索数据的下一步是构建查询。第一个代码段定义某些用于访问用户字典提供程序的变量:


// A "projection" defines the columns that will be returned for each row
String[] mProjection =
{
    UserDictionary.Words._ID,    // Contract class constant for the _ID column name
    UserDictionary.Words.WORD,   // Contract class constant for the word column name
    UserDictionary.Words.LOCALE  // Contract class constant for the locale column name
};

// Defines a string to contain the selection clause
String mSelectionClause = null;

// Initializes an array to contain selection arguments
String[] mSelectionArgs = {""};

下一个代码段以用户字典提供程序为例,显示了如何使用 ContentResolver.query()。 提供程序客户端查询与 SQL 查询类似,并且包含一组要返回的列、一组选择条件和排序顺序。

查询应该返回的列集被称为投影(变量 mProjection)。

用于指定要检索的行的表达式分割为选择子句和选择参数。 选择子句是逻辑和布尔表达式、列名称和值(变量 mSelectionClause)的组合。 如果您指定了可替换参数 ? 而非值,则查询方法会从选择参数数组(变量 mSelectionArgs)中检索值。

在下一个代码段中,如果用户未输入字词,则选择子句将设置为 null,而且查询会返回提供程序中的所有字词。 如果用户输入了字词,选择子句将设置为 UserDictionary.Words.WORD + " = ?" 且选择参数数组的第一个元素将设置为用户输入的字词。

/*
 * This defines a one-element String array to contain the selection argument.
 */
String[] mSelectionArgs = {""};

// Gets a word from the UI
mSearchString = mSearchWord.getText().toString();

// Remember to insert code here to check for invalid or malicious input.

// If the word is the empty string, gets everything
if (TextUtils.isEmpty(mSearchString)) {
    // Setting the selection clause to null will return all words
    mSelectionClause = null;
    mSelectionArgs[0] = "";

} else {
    // Constructs a selection clause that matches the word that the user entered.
    mSelectionClause = UserDictionary.Words.WORD + " = ?";

    // Moves the user's input string to the selection arguments.
    mSelectionArgs[0] = mSearchString;

}

// Does a query against the table and returns a Cursor object
mCursor = getContentResolver().query(
    UserDictionary.Words.CONTENT_URI,  // The content URI of the words table
    mProjection,                       // The columns to return for each row
    mSelectionClause                   // Either null, or the word the user entered
    mSelectionArgs,                    // Either empty, or the string the user entered
    mSortOrder);                       // The sort order for the returned rows

// Some providers return null if an error occurs, others throw an exception
if (null == mCursor) {
    /*
     * Insert code here to handle the error. Be sure not to use the cursor! You may want to
     * call android.util.Log.e() to log this error.
     *
     */
// If the Cursor is empty, the provider found no matches
} else if (mCursor.getCount() < 1) {

    /*
     * Insert code here to notify the user that the search was unsuccessful. This isn't necessarily
     * an error. You may want to offer the user the option to insert a new row, or re-type the
     * search term.
     */

} else {
    // Insert code here to do something with the results

}

此查询与 SQL 语句相似:

SELECT _ID, word, locale FROM words WHERE word =  ORDER BY word ASC;

在此 SQL 语句中,会使用实际的列名称而非协定类常量。

防止恶意输入

如果内容提供程序管理的数据位于 SQL 数据库中,将不受信任的外部数据包括在原始 SQL 语句中可能会导致 SQL 注入。

考虑此选择子句:

// Constructs a selection clause by concatenating the user's input to the column name
String mSelectionClause =  "var = " + mUserInput;

如果您执行此操作,则会允许用户将恶意 SQL 串连到 SQL 语句上。 例如,用户可以为 mUserInput 输入“nothing; DROP TABLE *;”,这会生成选择子句 var = nothing; DROP TABLE *;。 由于选择子句是作为 SQL 语句处理,因此这可能会导致提供程序擦除基础 SQLite 数据库中的所有表(除非提供程序设置为可捕获 SQL 注入尝试)。

要避免此问题,可使用一个用于将 ? 作为可替换参数的选择子句以及一个单独的选择参数数组。 执行此操作时,用户输入直接受查询约束,而不解释为 SQL 语句的一部分。 由于用户输入未作为 SQL 处理,因此无法注入恶意 SQL。请使用此选择子句,而不要使用串连来包括用户输入:

// Constructs a selection clause with a replaceable parameter
String mSelectionClause =  "var = ?";

按如下所示设置选择参数数组:

// Defines an array to contain the selection arguments
String[] selectionArgs = {""};

按如下所示将值置于选择参数数组中:

// Sets the selection argument to the user's input
selectionArgs[0] = mUserInput;

一个用于将 ? 用作可替换参数的选择子句和一个选择参数数组是指定选择的首选方式,即使提供程序并未基于 SQL 数据库。

显示查询结果

ContentResolver.query() 客户端方法始终会返回符合以下条件的 Cursor:包含查询的投影为匹配查询选择条件的行指定的列。 Cursor 对象为其包含的行和列提供随机读取访问权限。 通过使用 Cursor 方法,您可以循环访问结果中的行、确定每个列的数据类型、从列中获取数据,并检查结果的其他属性。 某些 Cursor 实现会在提供程序的数据发生更改时自动更新对象和/或在 Cursor 更改时触发观察程序对象中的方法。

注:提供程序可能会根据发出查询的对象的性质来限制对列的访问。 例如,联系人提供程序会限定只有同步适配器才能访问某些列,因此不会将它们返回至 Activity 或服务。

如果没有与选择条件匹配的行,则提供程序会返回 Cursor.getCount() 为 0(空游标)的 Cursor 对象。

如果出现内部错误,查询结果将取决于具体的提供程序。它可能会选择返回 null,或引发 Exception

由于 Cursor 是行“列表”,因此显示 Cursor 内容的良好方式是通过 SimpleCursorAdapter 将其与 ListView 关联。

以下代码段将延续上一代码段的代码。它会创建一个包含由查询检索到的 Cursor 的 SimpleCursorAdapter 对象,并将此对象设置为 ListView 的适配器:

// Defines a list of columns to retrieve from the Cursor and load into an output row
String[] mWordListColumns =
{
    UserDictionary.Words.WORD,   // Contract class constant containing the word column name
    UserDictionary.Words.LOCALE  // Contract class constant containing the locale column name
};

// Defines a list of View IDs that will receive the Cursor columns for each row
int[] mWordListItems = { R.id.dictWord, R.id.locale};

// Creates a new SimpleCursorAdapter
mCursorAdapter = new SimpleCursorAdapter(
    getApplicationContext(),               // The application's Context object
    R.layout.wordlistrow,                  // A layout in XML for one row in the ListView
    mCursor,                               // The result from the query
    mWordListColumns,                      // A string array of column names in the cursor
    mWordListItems,                        // An integer array of view IDs in the row layout
    0);                                    // Flags (usually none are needed)

// Sets the adapter for the ListView
mWordList.setAdapter(mCursorAdapter);

注:要通过 Cursor 支持 ListView,游标必需包含名为 _ID 的列。 正因如此,前文显示的查询会为“字词”表检索 _ID 列,即使 ListView 未显示该列。 此限制也解释了为什么大多数提供程序的每个表都具有 _ID 列。

从查询结果中获取数据

您可以将查询结果用于其他任务,而不是仅显示它们。例如,您可以从用户字典中检索拼写,然后在其他提供程序中查找它们。 要执行此操作,您需要在 Cursor 中循环访问行:


// Determine the column index of the column named "word"
int index = mCursor.getColumnIndex(UserDictionary.Words.WORD);

/*
 * Only executes if the cursor is valid. The User Dictionary Provider returns null if
 * an internal error occurs. Other providers may throw an Exception instead of returning null.
 */

if (mCursor != null) {
    /*
     * Moves to the next row in the cursor. Before the first movement in the cursor, the
     * "row pointer" is -1, and if you try to retrieve data at that position you will get an
     * exception.
     */
    while (mCursor.moveToNext()) {

        // Gets the value from the column.
        newWord = mCursor.getString(index);

        // Insert code here to process the retrieved word.

        ...

        // end of while loop
    }
} else {

    // Insert code here to report an error if the cursor is null or the provider threw an exception.
}

Cursor 实现包含多个用于从对象中检索不同类型的数据的“获取”方法。 例如,上一个代码段使用 getString()。 它们还具有 getType() 方法,该方法会返回指示列的数据类型的值。

内容提供程序权限


提供程序的应用可以指定其他应用访问提供程序的数据所必需的权限。 这些权限可确保用户了解应用将尝试访问的数据。 根据提供程序的要求,其他应用会请求它们访问提供程序所需的权限。 最终用户会在安装应用时看到所请求的权限。

如果提供程序的应用未指定任何权限,则其他应用将无权访问提供程序的数据。 但是,无论指定权限为何,提供程序的应用中的组件始终具有完整的读取和写入访问权限。

如前所述,用户字典提供程序需要 android.permission.READ_USER_DICTIONARY 权限才能从中检索数据。 提供程序具有用于插入、更新或删除数据的单独 android.permission.WRITE_USER_DICTIONARY 权限。

要获取访问提供程序所需的权限,应用将通过其清单文件中的  元素来请求这些权限。Android 软件包管理器安装应用时,用户必须批准该应用请求的所有权限。 如果用户批准所有权限,软件包管理器将继续安装;如果用户未批准这些权限,软件包管理器将中止安装。

以下  元素会请求对用户字典提供程序的读取访问权限:

     android:name="android.permission.READ_USER_DICTIONARY">

安全与权限指南中详细介绍了权限对提供程序访问的影响。

插入、更新和删除数据


与从提供程序检索数据的方式相同,也可以通过提供程序客户端和提供程序 ContentProvider 之间的交互来修改数据。 您通过传递到 ContentProvider的对应方法的参数来调用 ContentResolver 方法。 提供程序和提供程序客户端会自动处理安全性和跨进程通信。

插入数据

要将数据插入提供程序,可调用 ContentResolver.insert() 方法。此方法会在提供程序中插入新行并为该行返回内容 URI。 此代码段显示如何将新字词插入用户字典提供程序:

// Defines a new Uri object that receives the result of the insertion
Uri mNewUri;

...

// Defines an object to contain the new values to insert
ContentValues mNewValues = new ContentValues();

/*
 * Sets the values of each column and inserts the word. The arguments to the "put"
 * method are "column name" and "value"
 */
mNewValues.put(UserDictionary.Words.APP_ID, "example.user");
mNewValues.put(UserDictionary.Words.LOCALE, "en_US");
mNewValues.put(UserDictionary.Words.WORD, "insert");
mNewValues.put(UserDictionary.Words.FREQUENCY, "100");

mNewUri = getContentResolver().insert(
    UserDictionary.Word.CONTENT_URI,   // the user dictionary content URI
    mNewValues                          // the values to insert
);

新行的数据会进入单个 ContentValues 对象中,该对象在形式上与单行游标类似。 此对象中的列不需要具有相同的数据类型,如果您不想指定值,则可以使用 ContentValues.putNull() 将列设置为 null

代码段不会添加 _ID 列,因为系统会自动维护此列。 提供程序会向添加的每个行分配唯一的 _ID 值。 通常,提供程序会将此值用作表的主键。

newUri 中返回的内容 URI 会按照以下格式标识新添加的行:

content://user_dictionary/words/

 是新行的 _ID 内容。 大多数提供程序都能自动检测这种格式的内容 URI,然后在该特定行上执行请求的操作。

要从返回的 Uri 中获取 _ID 的值,请调用 ContentUris.parseId()

更新数据

要更新行,请按照执行插入的方式使用具有更新值的 ContentValues 对象,并按照执行查询的方式使用选择条件。 您使用的客户端方法是ContentResolver.update()。您只需将值添加至您要更新的列的 ContentValues 对象。 如果您要清除列的内容,请将值设置为 null

以下代码段会将语言区域具有语言“en”的所有行的语言区域更改为 null。 返回值是已更新的行数:

// Defines an object to contain the updated values
ContentValues mUpdateValues = new ContentValues();

// Defines selection criteria for the rows you want to update
String mSelectionClause = UserDictionary.Words.LOCALE +  "LIKE ?";
String[] mSelectionArgs = {"en_%"};

// Defines a variable to contain the number of updated rows
int mRowsUpdated = 0;

...

/*
 * Sets the updated value and updates the selected words.
 */
mUpdateValues.putNull(UserDictionary.Words.LOCALE);

mRowsUpdated = getContentResolver().update(
    UserDictionary.Words.CONTENT_URI,   // the user dictionary content URI
    mUpdateValues                       // the columns to update
    mSelectionClause                    // the column to select on
    mSelectionArgs                      // the value to compare to
);

您还应该在调用 ContentResolver.update() 时检查用户输入。如需了解有关此内容的更多详情,请阅读防止恶意输入部分。

删除数据

删除行与检索行数据类似:为要删除的行指定选择条件,客户端方法会返回已删除的行数。 以下代码段会删除应用 ID 与“用户”匹配的行。该方法会返回已删除的行数。


// Defines selection criteria for the rows you want to delete
String mSelectionClause = UserDictionary.Words.APP_ID + " LIKE ?";
String[] mSelectionArgs = {"user"};

// Defines a variable to contain the number of rows deleted
int mRowsDeleted = 0;

...

// Deletes the words that match the selection criteria
mRowsDeleted = getContentResolver().delete(
    UserDictionary.Words.CONTENT_URI,   // the user dictionary content URI
    mSelectionClause                    // the column to select on
    mSelectionArgs                      // the value to compare to
);

您还应该在调用 ContentResolver.delete() 时检查用户输入。如需了解有关此内容的更多详情,请阅读防止恶意输入部分。

提供程序数据类型


内容提供程序可以提供多种不同的数据类型。用户字典提供程序仅提供文本,但提供程序也能提供以下格式:

  • 整型
  • 长整型(长)
  • 浮点型
  • 长浮点型(双倍)

提供程序经常使用的另一种数据类型是作为 64KB 字节的数组实施的二进制大型对象 (BLOB)。 您可以通过查看 Cursor 类“获取”方法看到可用数据类型。

提供程序文档中通常都列出了其每个列的数据类型。 用户字典提供程序的数据类型列在其协定类 UserDictionary.Words 参考文档中(协定类部分对协定类进行了介绍)。 您也可以通过调用 Cursor.getType() 来确定数据类型。

提供程序还会维护其定义的每个内容 URI 的 MIME(多用途互联网邮件扩展)数据类型信息。您可以使用 MIME 类型信息查明应用是否可以处理提供程序提供的数据,或根据 MIME 类型选择处理类型。 在使用包含复杂数据结构或文件的提供程序时,通常需要 MIME 类型。 例如,联系人提供程序中的 ContactsContract.Data 表会使用 MIME 类型来标记每行中存储的联系人数据类型。 要获取与内容 URI 对应的 MIME 类型,请调用ContentResolver.getType()

MIME 类型引用部分介绍了标准和自定义 MIME 类型的语法。

提供程序访问的替代形式


提供程序访问的三种替代形式在应用开发过程中十分重要:

  • 批量访问:您可以通过 ContentProviderOperation 类中的方法创建一批访问调用,然后通过 ContentResolver.applyBatch() 应用它们。
  • 异步查询:您应该在单独线程中执行查询。执行此操作的方式之一是使用 CursorLoader 对象。 加载器指南中的示例展示了如何执行此操作。
  • 通过 Intent 访问数据:尽管您无法直接向提供程序发送 Intent,但可以向提供程序的应用发送 Intent,后者通常具有修改提供程序数据的最佳配置。

下文将介绍通过 Intent 进行的批量访问和修改。

批量访问

批量访问提供程序适用于插入大量行,或通过同一方法调用在多个表中插入行,或者通常用于跨进程界限将一组操作作为事务处理(原子操作)执行。

要在“批量模式”下访问提供程序, 您可以创建 ContentProviderOperation 对象数组,然后使用 ContentResolver.applyBatch() 将其分派给内容提供程序。您需将内容提供程序的授权传递给此方法,而不是特定内容 URI。 这样可使数组中的每个 ContentProviderOperation 对象都能适用于其他表。 调用 ContentResolver.applyBatch() 会返回结果数组。

ContactsContract.RawContacts 协定类 的说明包括展示批量注入的代码段。 联系人管理器示例应用包含在其 ContactAdder.java 源文件中进行批量访问的示例。

通过 Intent 访问数据

Intent 可以提供对内容提供程序的间接访问。即使您的应用不具备访问权限,您也可以通过以下方式允许用户访问提供程序中的数据:从具有权限的应用中获取回结果 Intent,或者通过激活具有权限的应用,然后让用户在其中工作。

通过临时权限获取访问权限

即使您没有适当的访问权限,也可以通过以下方式访问内容提供程序中的数据:将 Intent 发送至具有权限的应用,然后接收回包含“URI”权限的结果 Intent。 这些是特定内容 URI 的权限,将持续至接收该权限的 Activity 结束。 具有永久权限的应用将通过在结果 Intent 中设置标志来授予临时权限:

  • 读取权限: FLAG_GRANT_READ_URI_PERMISSION
  • 写入权限: FLAG_GRANT_WRITE_URI_PERMISSION

注:这些标志不会为其授权包含在内容 URI 中的提供程序提供常规的读取或写入访问权限。 访问权限仅适用于 URI 本身。

提供程序使用  元素的 android:grantUriPermission 属性以及  元素的  子元素在其清单文件中定义内容 URI 的 URI 权限。安全与权限指南中“URI 权限”部分更加详细地说明了 URI 权限机制。

例如,即使您没有 READ_CONTACTS 权限,也可以在联系人提供程序中检索联系人的数据。您可能希望在向联系人发送电子生日祝福的应用中执行此操作。 您更愿意让用户控制应用所使用的联系人,而不是请求 READ_CONTACTS,让您能够访问用户的所有联系人及其信息。 要执行此操作,您需要使用以下进程:

  1. 您的应用会使用方法 startActivityForResult() 发送包含操作 ACTION_PICK 和“联系人”MIME 类型 CONTENT_ITEM_TYPE 的 Intent 对象。
  2. 由于此 Intent 与“联系人”应用的“选择” Activity 的 Intent 过滤器相匹配,因此 Activity 会显示在前台。
  3. 在选择 Activity 中,用户选择要更新的联系人。 发生此情况时,选择 Activity 会调用 setResult(resultcode, intent) 以设置用于返回至应用的 Intent。Intent 包含用户选择的联系人的内容 URI,以及“extra”标志 FLAG_GRANT_READ_URI_PERMISSION。这些标志会为您的应用授予读取内容 URI 所指向联系人数据的 URI 权限。 然后,选择 Activity 会调用 finish(), 将控制权交还给您的应用。
  4. 您的 Activity 会返回至前台,系统会调用您的 Activity 的 onActivityResult() 方法。此方法会收到“联系人”应用中选择 Activity 所创建的结果 Intent。
  5. 通过来自结果 Intent 的内容 URI,您可以读取来自联系人提供程序的联系人数据,即使您未在清单文件中请求对该提供程序的永久读取访问权限。 您可以获取联系人的生日信息或其电子邮件地址,然后发送电子祝福。

使用其他应用

允许用户修改您无权访问的数据的简单方法是激活具有权限的应用,让用户在其中执行工作。

例如,日历应用接受 ACTION_INSERT Intent 对象,这让您可以激活 应用的插入 UI。您可以在此 Intent(应用将使用该 Intent 来预先填充 UI)中传递“extra”数据。 由于定期事件具有复杂的语法,因此将事件插入日历提供程序的首选方式是激活具有 ACTION_INSERT 的日历应用,然后让用户在其中插入事件。

协定类


协定类定义帮助应用使用内容 URI、列名称、 Intent 操作以及内容提供程序的其他功能的常量。 协定类未自动包含在提供程序中;提供程序的开发者需要定义它们,然后使其可用于其他开发者。 Android 平台中包含的许多提供程序都在软件包 android.provider 中具有对应的协定类。

例如,用户字典提供程序具有包含内容 URI 和列名称常量的协定类 UserDictionary。 “字词”表的内容 URI 在常量 UserDictionary.Words.CONTENT_URI中定义。 UserDictionary.Words 类也包含列名称常量,本指南的示例代码段中就使用了该常量。 例如,查询投影可以定义为:

String[] mProjection =
{
    UserDictionary.Words._ID,
    UserDictionary.Words.WORD,
    UserDictionary.Words.LOCALE
};

联系人提供程序的 ContactsContract 也是一个协定类。 此类的参考文档包括示例代码段。其子类之一 ContactsContract.Intents.Insert 是包含 Intent 和 Intent 数据的协定类。

MIME 类型引用


内容提供程序可以返回标准 MIME 媒体类型和/或自定义 MIME 类型字符串。

MIME 类型具有格式

type/subtype

例如,众所周知的 MIME 类型 text/html 具有 text 类型和 html 子类型。如果提供程序为 URI 返回此类型,则意味着使用该 URI 的查询会返回包含 HTML 标记的文本。

自定义 MIME 类型字符串(也称为“特定于供应商”的 MIME 类型)具有更加复杂的类型子类型值。 类型值始终为

vnd.android.cursor.dir

(多行)或

vnd.android.cursor.item

(单行)。

子类型特定于提供程序。Android 内置提供程序通常具有简单的子类型。 例如,当“通讯录”应用为电话号码创建行时,它会在行中设置以下 MIME 类型:

vnd.android.cursor.item/phone_v2

请注意,子类型值只是 phone_v2

其他提供程序开发者可能会根据提供程序的授权和表名称创建自己的子类型模式。 例如,假设提供程序包含列车时刻表。 提供程序的授权是 com.example.trains,并包含表 Line1、Line2 和 Line3。在响应表 Line1 的内容 URI

content://com.example.trains/Line1

时,提供程序会返回 MIME 类型

vnd.android.cursor.dir/vnd.example.line1

在响应表 Line2 中第 5 行的内容 URI

content://com.example.trains/Line2/5

时,提供程序会返回 MIME 类型

vnd.android.cursor.item/vnd.example.line2

大多数内容提供程序都会为其使用的 MIME 类型定义协定类常量。例如,联系人提供程序协定类 ContactsContract.RawContacts 会为单个原始联系人行的 MIME 类型定义常量 CONTENT_ITEM_TYPE

内容 URI 部分介绍了单个行的内容 URI。


创建内容提供程序

本文内容

  1. 设计数据存储
  2. 设计内容 URI
  3. 实现 ContentProvider 类
    1. 必需方法
    2. 实现 query() 方法
    3. 实现 insert() 方法
    4. 实现 delete() 方法
    5. 实现 update() 方法
    6. 实现 onCreate() 方法
  4. 实现内容提供程序 MIME 类型
    1. 表的 MIME 类型
    2. 文件的 MIME 类型
  5. 实现协定类
  6. 实现内容提供程序权限
  7. 元素
  8. Intent 和数据访问

关键类

  1. ContentProvider
  2. Cursor
  3. Uri

相关示例

  1. 记事本示例应用

另请参阅

  1. 内容提供程序基础知识
  2. 日历提供程序

内容提供程序管理对中央数据存储区的访问。您将提供程序作为 Android 应用中的一个或多个类(连同清单文件中的元素)实现。 其中一个类会实现子类 ContentProvider,即您的提供程序与其他应用之间的接口。 尽管内容提供程序旨在向其他应用提供数据,但您的应用中必定有这样一些 Activity,它们允许用户查询和修改由提供程序管理的数据。

本主题的其余部分列出了开发内容提供程序的基本步骤和需要使用的 API。

着手开发前的准备工作


请在着手开发提供程序之前执行以下操作:

  1. 决定您是否需要内容提供程序。如果您想提供下列一项或多项功能,则需要开发内容提供程序:
    • 您想为其他应用提供复杂的数据或文件
    • 您想允许用户将复杂的数据从您的应用复制到其他应用中
    • 您想使用搜索框架提供自定义搜索建议

    如果完全是在您自己的应用中使用,则需要提供程序即可使用 SQLite 数据库。

  2. 如果您尚未完成此项操作,请阅读内容提供程序基础知识主题,了解有关提供程序的详情。

接下来,请按照以下步骤开发您的提供程序:

  1. 为您的数据设计原始存储。内容提供程序以两种方式提供数据:
    文件数据
    通常存储在文件中的数据,如照片、音频或视频。 将文件存储在您的应用的私有空间内。 您的提供程序可以应其他应用发出的文件请求提供文件句柄。
    “结构化”数据
    通常存储在数据库、数组或类似结构中的数据。 以兼容行列表的形式存储数据。行表示实体,如人员或库存项目。 列表示实体的某项数据,如人员的姓名或商品的价格。 此类数据通常存储在 SQLite 数据库中,但您可以使用任何类型的持久存储。 如需了解有关 Android 系统中提供的存储类型的更多信息,请参阅 设计数据存储部分。
  2. 定义 ContentProvider 类及其所需方法的具体实现。 此类是您的数据与 Android 系统其余部分之间的接口。 如需了解有关此类的详细信息,请参阅实现 ContentProvider 类部分。
  3. 定义提供程序的授权字符串、其内容 URI 以及列名称。如果您想让提供程序的应用处理 Intent,则还要定义 Intent 操作、Extra 数据以及标志。 此外,还要定义想要访问您的数据的应用必须具备的权限。 您应该考虑在一个单独的协定类中将所有这些值定义为常量;以后您可以将此类公开给其他开发者。 如需了解有关内容 URI 的详细信息,请参阅设计内容 URI 部分。 如需了解有关 Intent 的详细信息,请参阅 Intent 和数据访问部分。
  4. 添加其他可选部分,如示例数据或可以在提供程序与云数据之间同步数据的 AbstractThreadedSyncAdapter 实现。

设计数据存储


内容提供程序是用于访问以结构化格式保存的数据的接口。在您创建该接口之前,必须决定如何存储数据。 您可以按自己的喜好以任何形式存储数据,然后根据需要设计读写数据的接口。

以下是 Android 中提供的一些数据存储技术:

  • Android 系统包括一个 SQLite 数据库 API,Android 自己的提供程序使用它来存储面向表的数据。 SQLiteOpenHelper 类可帮助您创建数据库,SQLiteDatabase 类是用于访问数据库的基类。

    请记住,您不必使用数据库来实现存储区。提供程序在外部表现为一组表,与关系型数据库类似,但这并不是对提供程序内部实现的要求;

  • 为了存储文件数据,Android 提供了各种面向文件的 API。 如需了解有关文件存储的更多信息,请阅读数据存储主题。 如果您要设计提供媒体相关数据(如音乐或视频)的提供程序,则可开发一个合并了表数据和文件的提供程序。
  • 要想使用基于网络的数据,请使用 java.net 和 android.net 中的类。 您也可以将基于网络的数据与本地数据存储(如数据库)同步,然后以表或文件的形式提供数据。 示例同步适配器示例应用展示了这类同步。

数据设计考虑事项

以下是一些设计提供程序数据结构的技巧:

  • 表数据应始终具有一个“主键”列,提供程序将其作为与每行对应的唯一数字值加以维护。 您可以使用此值将该行链接到其他表中的相关行(将其用作“外键”)。 尽管您可以为此列使用任何名称 ,但使用 BaseColumns._ID 是最佳选择,因为将提供程序查询的结果链接到 ListView 的条件是,检索到的其中一个列的名称必须是 _ID
  • 如果您想提供位图图像或其他非常庞大的文件导向型数据,请将数据存储在一个文件中,然后间接提供这些数据,而不是直接将其存储在表中。 如果您执行了此操作,则需要告知提供程序的用户,他们需要使用 ContentResolver 文件方法来访问数据;
  • 使用二进制大型对象 (BLOB) 数据类型存储大小或结构会发生变化的数据。 例如,您可以使用 BLOB 列来存储协议缓冲区或 JSON 结构。

    您也可以使用 BLOB 来实现独立于架构的表。在这类表中,您需要以 BLOB 形式定义一个主键列、一个 MIME 类型列以及一个或多个通用列。 这些 BLOB 列中数据的含义通过 MIME 类型列中的值指示。 这样一来,您就可以在同一个表中存储不同类型的行。 举例来说,联系人提供程序的“数据”表 ContactsContract.Data 便是一个独立于架构的表。

设计内容 URI


内容 URI 是用于在提供程序中标识数据的 URI。内容 URI 包括整个提供程序的符号名称(其授权)和一个指向表或文件的名称(路径)。 可选 ID 部分指向表中的单个行。 ContentProvider 的每一个数据访问方法都将内容 URI 作为参数;您可以利用这一点确定要访问的表、行或文件。

内容提供程序基础知识主题中描述了内容 URI 的基础知识。

设计授权

提供程序通常具有单一授权,该授权充当其 Android 内部名称。为避免与其他提供程序发生冲突,您应该使用互联网网域所有权(反向)作为提供程序授权的基础。 由于此建议也适用于 Android 软件包名称,因此您可以将提供程序授权定义为包含该提供程序的软件包名称的扩展名。 例如,如果您的 Android 软件包名称为 com.example.,则应为提供程序提供 com.example..provider 授权。

设计路径结构

开发者通常通过追加指向单个表的路径来根据权限创建内容 URI。 例如,如果您有两个表:table1 和 table2,则可以通过合并上一示例中的权限来生成 内容 URI com.example..provider/table1 和 com.example..provider/table2。路径并不限定于单个段,也无需为每一级路径都创建一个表。

处理内容 URI ID

按照惯例,提供程序通过接受末尾具有行所对应 ID 值的内容 URI 来提供对表中单个行的访问。 同样按照惯例,提供程序会将该 ID 值与表的 _ID 列进行匹配,并对匹配的行执行请求的访问。

这一惯例为访问提供程序的应用的常见设计模式提供了便利。应用会对提供程序执行查询,并使用 CursorAdapter 以 ListView 显示生成的 Cursor。 定义 CursorAdapter 的条件是, Cursor 中的其中一个列必须是 _ID

用户随后从 UI 上显示的行中选取其中一行,以查看或修改数据。 应用会从支持 ListView 的 Cursor 中获取对应行,获取该行的 _ID 值,将其追加到内容 URI,然后向提供程序发送访问请求。 然后,提供程序便可对用户选取的特定行执行查询或修改。

内容 URI 模式

为帮助您选择对传入的内容 URI 执行的操作,提供程序 API 加入了实用类 UriMatcher,它会将内容 URI“模式”映射到整型值。 您可以在一个 switch 语句中使用这些整型值,为匹配特定模式的一个或多个内容 URI 选择所需操作。

内容 URI 模式使用通配符匹配内容 URI:

  • *:匹配由任意长度的任何有效字符组成的字符串
  • #:匹配由任意长度的数字字符组成的字符串

以设计和编码内容 URI 处理为例,假设一个具有授权 com.example.app.provider 的提供程序能识别以下指向表的内容 URI:

  • content://com.example.app.provider/table1:一个名为 table1 的表
  • content://com.example.app.provider/table2/dataset1:一个名为 dataset1 的表
  • content://com.example.app.provider/table2/dataset2:一个名为 dataset2 的表
  • content://com.example.app.provider/table3:一个名为 table3 的表

提供程序也能识别追加了行 ID 的内容 URI,例如,content://com.example.app.provider/table3/1 对应由 table3 中 1 标识的行的内容 URI。

可以使用以下内容 URI 模式:

content://com.example.app.provider/*
匹配提供程序中的任何内容 URI。
content://com.example.app.provider/table2/*
匹配表  dataset1 和表  dataset2 的内容 URI,但不匹配  table1 或  table3 的内容 URI。
content://com.example.app.provider/table3/#:匹配  table3 中单个行的内容 URI,如  content://com.example.app.provider/table3/6 对应由  6 标识的行的内容 URI。

以下代码段演示了 UriMatcher 中方法的工作方式。 此代码采用不同方式处理整个表的 URI 与单个行的 URI,它为表使用的内容 URI 模式是content:///,为单个行使用的内容 URI 模式则是 content:////

方法 addURI() 会将授权和路径映射到一个整型值。 方法 match() 会返回 URI 的整型值。switch 语句会在查询整个表与查询单个记录之间进行选择:

public class ExampleProvider extends ContentProvider {
...
    // Creates a UriMatcher object.
    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    static {
        /*
         * The calls to addURI() go here, for all of the content URI patterns that the provider
         * should recognize. For this snippet, only the calls for table 3 are shown.
         */

        /*
         * Sets the integer value for multiple rows in table 3 to 1. Notice that no wildcard is used
         * in the path
         */
        sUriMatcher.addURI("com.example.app.provider", "table3", 1);

        /*
         * Sets the code for a single row to 2. In this case, the "#" wildcard is
         * used. "content://com.example.app.provider/table3/3" matches, but
         * "content://com.example.app.provider/table3 doesn't.
         */
        sUriMatcher.addURI("com.example.app.provider", "table3/#", 2);
    }
...
    // Implements ContentProvider.query()
    public Cursor query(
        Uri uri,
        String[] projection,
        String selection,
        String[] selectionArgs,
        String sortOrder) {
...
        /*
         * Choose the table to query and a sort order based on the code returned for the incoming
         * URI. Here, too, only the statements for table 3 are shown.
         */
        switch (sUriMatcher.match(uri)) {


            // If the incoming URI was for all of table3
            case 1:

                if (TextUtils.isEmpty(sortOrder)) sortOrder = "_ID ASC";
                break;

            // If the incoming URI was for a single row
            case 2:

                /*
                 * Because this URI was for a single row, the _ID value part is
                 * present. Get the last path segment from the URI; this is the _ID value.
                 * Then, append the value to the WHERE clause for the query
                 */
                selection = selection + "_ID = " uri.getLastPathSegment();
                break;

            default:
            ...
                // If the URI is not recognized, you should do some error handling here.
        }
        // call the code to actually do the query
    }

另一个类 ContentUris 会提供一些工具方法,用于处理内容 URI 的 id 部分。Uri 类和 Uri.Builder 类包括一些工具方法,用于解析现有 Uri 对象和构建新对象。

实现 ContentProvider 类


ContentProvider 实例通过处理来自其他应用的请求来管理对结构化数据集的访问。 所有形式的访问最终都会调用 ContentResolver,后者接着调用ContentProvider 的具体方法来获取访问权限。

必需方法

抽象类 ContentProvider 定义了六个抽象方法,您必须将这些方法作为自己具体子类的一部分加以实现。 所有这些方法(onCreate() 除外)都由一个尝试访问您的内容提供程序的客户端应用调用:

query()
从您的提供程序检索数据。使用参数选择要查询的表、要返回的行和列以及结果的排序顺序。 将数据作为  Cursor 对象返回。
insert()
在您的提供程序中插入一个新行。使用参数选择目标表并获取要使用的列值。 返回新插入行的内容 URI。
update()
更新您提供程序中的现有行。使用参数选择要更新的表和行,并获取更新后的列值。 返回已更新的行数。
delete()
从您的提供程序中删除行。使用参数选择要删除的表和行。 返回已删除的行数。
getType()
返回内容 URI 对应的 MIME 类型。 实现内容提供程序 MIME 类型部分对此方法做了更详尽的描述。
onCreate()
初始化您的提供程序。Android 系统会在创建您的提供程序后立即调用此方法。 请注意, ContentResolver 对象尝试访问您的提供程序时,系统才会创建它。

请注意,这些方法的签名与同名的 ContentResolver 方法相同。

您在实现这些方法时应考虑以下事项:

  • 所有这些方法(onCreate() 除外)都可由多个线程同时调用,因此它们必须是线程安全方法。如需了解有关多个线程的更多信息,请参阅进程和线程主题;
  • 避免在 onCreate() 中执行长时间操作。将初始化任务推迟到实际需要时进行。 实现 onCreate() 方法部分对此做了更详尽的描述;
  • 尽管您必须实现这些方法,但您的代码只需返回要求的数据类型,无需执行任何其他操作。 例如,您可能想防止其他应用向某些表插入数据。 要实现此目的,您可以忽略 insert() 调用并返回 0。

实现 query() 方法

ContentProvider.query() 方法必须返回 Cursor 对象。如果失败,则会引发 Exception。 如果您使用 SQLite 数据库作为数据存储,则只需返回由 SQLiteDatabase 类的其中一个 query() 方法返回的 Cursor。 如果查询不匹配任何行,您应该返回一个 Cursor 实例(其 getCount() 方法返回 0)。只有当查询过程中出现内部错误时,您才应该返回 null

如果您不使用 SQLite 数据库作为数据存储,请使用 Cursor 的其中一个具体子类。 例如,在 MatrixCursor 类实现的游标中,每一行都是一个 Object 数组。 对于此类,请使用 addRow() 来添加新行。

请记住,Android 系统必须能够跨进程边界传播 Exception。 Android 可以为以下异常执行此操作,这些异常可能有助于处理查询错误:

  • IllegalArgumentException(您可以选择在提供程序收到无效的内容 URI 时引发此异常)
  • NullPointerException

实现 insert() 方法

insert() 方法会使用 ContentValues 参数中的值向相应表中添加新行。 如果 ContentValues 参数中未包含列名称,您可能想在您的提供程序代码或数据库架构中提供其默认值。

此方法应该返回新行的内容 URI。要想构建此方法,请使用 withAppendedId() 向表的内容 URI 追加新行的 _ID(或其他主键)值。

实现 delete() 方法

delete() 方法不需要从您的数据存储中实际删除行。 如果您将同步适配器与提供程序一起使用,应该考虑为已删除的行添加“删除”标志,而不是将行整个移除。 同步适配器可以检查是否存在已删除的行,并将它们从服务器中移除,然后再将它们从提供程序中删除。

实现 update() 方法

update() 方法采用 insert() 所使用的相同 ContentValues 参数,以及 delete() 和 ContentProvider.query() 所使用的相同 selection 和 selectionArgs 参数。 这样一来,您就可以在这些方法之间重复使用代码。

实现 onCreate() 方法

Android 系统会在启动提供程序时调用 onCreate()。您只应在此方法中执行运行快速的初始化任务,并将数据库创建和数据加载推迟到提供程序实际收到数据请求时进行。 如果您在 onCreate() 中执行长时间的任务,则会减慢提供程序的启动速度, 进而减慢提供程序对其他应用的响应速度。

例如,如果您使用 SQLite 数据库,可以在 ContentProvider.onCreate() 中创建一个新的 SQLiteOpenHelper 对象,然后在首次打开数据库时创建 SQL 表。 为简化这一过程,在您首次调用 getWritableDatabase() 时,它会自动调用 SQLiteOpenHelper.onCreate() 方法。

以下两个代码段展示了 ContentProvider.onCreate() 与 SQLiteOpenHelper.onCreate() 之间的交互。第一个代码段是 ContentProvider.onCreate() 的实现:

public class ExampleProvider extends ContentProvider

    /*
     * Defines a handle to the database helper object. The MainDatabaseHelper class is defined
     * in a following snippet.
     */
    private MainDatabaseHelper mOpenHelper;

    // Defines the database name
    private static final String DBNAME = "mydb";

    // Holds the database object
    private SQLiteDatabase db;

    public boolean onCreate() {

        /*
         * Creates a new helper object. This method always returns quickly.
         * Notice that the database itself isn't created or opened
         * until SQLiteOpenHelper.getWritableDatabase is called
         */
        mOpenHelper = new MainDatabaseHelper(
            getContext(),        // the application context
            DBNAME,              // the name of the database)
            null,                // uses the default SQLite cursor
            1                    // the version number
        );

        return true;
    }

    ...

    // Implements the provider's insert method
    public Cursor insert(Uri uri, ContentValues values) {
        // Insert code here to determine which table to open, handle error-checking, and so forth

        ...

        /*
         * Gets a writeable database. This will trigger its creation if it doesn't already exist.
         *
         */
        db = mOpenHelper.getWritableDatabase();
    }
}

下一个代码段是 SQLiteOpenHelper.onCreate() 的实现,其中包括一个帮助程序类:

...
// A string that defines the SQL statement for creating a table
private static final String SQL_CREATE_MAIN = "CREATE TABLE " +
    "main " +                       // Table's name
    "(" +                           // The columns in the table
    " _ID INTEGER PRIMARY KEY, " +
    " WORD TEXT"
    " FREQUENCY INTEGER " +
    " LOCALE TEXT )";
...
/**
 * Helper class that actually creates and manages the provider's underlying data repository.
 */
protected static final class MainDatabaseHelper extends SQLiteOpenHelper {

    /*
     * Instantiates an open helper for the provider's SQLite data repository
     * Do not do database creation and upgrade here.
     */
    MainDatabaseHelper(Context context) {
        super(context, DBNAME, null, 1);
    }

    /*
     * Creates the data repository. This is called when the provider attempts to open the
     * repository and SQLite reports that it doesn't exist.
     */
    public void onCreate(SQLiteDatabase db) {

        // Creates the main table
        db.execSQL(SQL_CREATE_MAIN);
    }
}

实现内容提供程序 MIME 类型


ContentProvider 类具有两个返回 MIME 类型的方法:

getType()
您必须为任何提供程序实现的必需方法之一。
getStreamTypes()
系统在您的提供程序提供文件时要求实现的方法。

表的 MIME 类型

getType() 方法会返回一个 MIME 格式的 String,后者描述内容 URI 参数返回的数据类型。Uri 参数可以是模式,而不是特定 URI;在这种情况下,您应该返回与匹配该模式的内容 URI 关联的数据类型。

对于文本、HTML 或 JPEG 等常见数据类型,getType() 应该为该数据返回标准 MIME 类型。 IANA MIME Media Types 网站上提供了这些标准类型的完整列表。

对于指向一个或多个表数据行的内容 URI,getType() 应该以 Android 供应商特有 MIME 格式返回 MIME 类型:

  • 类型部分:vnd
  • 子类型部分:
    • 如果 URI 模式用于单个行:android.cursor.item/
    • 如果 URI 模式用于多个行:android.cursor.dir/
  • 提供程序特有部分:vnd..

    您提供  和 。  值应具有全局唯一性, 值应在对应的 URI 模式中具有唯一性。 适合选择贵公司的名称或您的应用 Android 软件包名称的某个部分作为 。 适合选择 URI 关联表的标识字符串作为 

例如,如果提供程序的授权是 com.example.app.provider,并且它公开了一个名为 table1 的表,则 table1 中多个行的 MIME 类型是:

vnd.android.cursor.dir/vnd.com.example.provider.table1

对于 table1 的单个行,MIME 类型是:

vnd.android.cursor.item/vnd.com.example.provider.table1

文件的 MIME 类型

如果您的提供程序提供文件,请实现 getStreamTypes()。 该方法会为您的提供程序可以为给定内容 URI 返回的文件返回一个 MIME 类型 String 数组。 您应该通过 MIME 类型过滤器参数过滤您提供的 MIME 类型,以便只返回客户端想处理的那些 MIME 类型。

例如,假设提供程序以 .jpg.png 和 .gif 格式文件形式提供照片图像。 如果应用调用 ContentResolver.getStreamTypes() 时使用了过滤器字符串 image/*(任何是“图像”的内容),则 ContentProvider.getStreamTypes() 方法应返回数组:

{ "image/jpeg", "image/png", "image/gif"}

如果应用只对 .jpg 文件感兴趣,则可以在调用 ContentResolver.getStreamTypes() 时使用过滤器字符串 *\/jpegContentProvider.getStreamTypes() 应返回:

{"image/jpeg"}

如果您的提供程序未提供过滤器字符串中请求的任何 MIME 类型,则 getStreamTypes() 应返回 null

实现协定类


协定类是一种 public final 类,其中包含对 URI、列名称、MIME 类型以及其他与提供程序有关的元数据的常量定义。 该类可确保即使 URI、列名称等数据的实际值发生变化,也可以正确访问提供程序,从而在提供程序与其他应用之间建立协定。

协定类对开发者也有帮助,因为其常量通常采用助记名称,因此可以降低开发者为列名称或 URI 使用错误值的可能性。 由于它是一种类,因此可以包含 Javadoc 文档。 集成开发环境(如 Android Studio)可以根据协定类自动完成常量名称,并为常量显示 Javadoc。

开发者无法从您的应用访问协定类的类文件,但他们可以通过您提供的 .jar 文件将其静态编译到其应用内。

举例来说,ContactsContract 类及其嵌套类便属于协定类。

实现内容提供程序权限


安全与权限主题中详细描述了 Android 系统各个方面的权限和访问。 数据存储主题也描述了各类存储实行中的安全与权限。 其中的要点简述如下:

  • 默认情况下,存储在设备内部存储上的数据文件是您的应用和提供程序的私有数据文件;
  • 您创建的 SQLiteDatabase 数据库是您的应用和提供程序的私有数据库;
  • 默认情况下,您保存到外部存储的数据文件是公用可全局读取的数据文件。 您无法使用内容提供程序来限制对外部存储内文件的访问,因为其他应用可以使用其他 API 调用来对它们执行读取和写入操作;
  • 用于在您的设备的内部存储上打开或创建文件或 SQLite 数据库的方法调用可能会为所有其他应用同时授予读取和写入访问权限。 如果您将内部文件或数据库用作提供程序的存储区,并向其授予“可全局读取”或“可全局写入”访问权限,则您在清单文件中为提供程序设置的权限不会保护您的数据。 内部存储中文件和数据库的默认访问权限是“私有”,对于提供程序的存储区,您不应更改此权限。

如果您想使用内容提供程序权限来控制对数据的访问,则应将数据存储在内部文件、SQLite 数据库或“云”中(例如,远程服务器上),而且您应该保持文件和数据库为您的应用所私有。

实现权限

即使底层数据为私有数据,所有应用仍可从您的提供程序读取数据或向其写入数据,因为在默认情况下,您的提供程序未设置权限。 要想改变这种情况,请使用属性或  元素的子元素在您的清单文件中为您的提供程序设置权限。 您可以设置适用于整个提供程序、特定表甚至特定记录的权限,或者设置同时适用于这三者的权限。

您可以通过清单文件中的一个或多个  元素为您的提供程序定义权限。要使权限对您的提供程序具有唯一性,请为 android:name 属性使用 Java 风格作用域。 例如,将读取权限命名为 com.example.app.provider.permission.READ_PROVIDER

以下列表描述了提供程序权限的作用域,从适用于整个提供程序的权限开始,然后逐渐细化。 更细化的权限优先于作用域较大的权限:

统一读写提供程序级别权限
一个同时控制对整个提供程序读取和写入访问的权限,通过   元素的  android:permission 属性指定。
单独的读取和写入提供程序级别权限
针对整个提供程序的读取权限和写入权限。您可以通过   元素的  android:readPermission 属性和  android:writePermission 属性 指定它们。它们优先于  android:permission 所需的权限。
路径级别权限
针对提供程序中内容 URI 的读取、写入或读取/写入权限。您可以通过   元素的   子元素指定您想控制的每个 URI。 对于您指定的每个内容 URI,您都可以指定读取/写入权限、读取权限或写入权限,或同时指定所有三种权限。 读取权限和写入权限优先于读取/写入权限。 此外,路径级别权限优先于提供程序级别权限。
临时权限
一种权限级别,即使应用不具备通常需要的权限,该级别也能授予对应用的临时访问权限。 临时访问功能可减少应用需要在其清单文件中请求的权限数量。 当您启用临时权限时,只有持续访问您的所有数据的应用才需要“永久性”提供程序访问权限。

假设您需要实现电子邮件提供程序和应用的权限,如果您想允许外部图像查看器应用显示您的提供程序中的照片附件, 为了在不请求权限的情况下为图像查看器提供必要的访问权限,可以为照片的内容 URI 设置临时权限。 对您的电子邮件应用进行相应设计,使应用能够在用户想要显示照片时向图像查看器发送一个 Intent,其中包含照片的内容 URI 以及权限标志。 图像查看器可随后查询您的电子邮件提供程序以检索照片,即使查看器不具备对您提供程序的正常读取权限,也不受影响。

要想启用临时权限,请设置  元素的 android:grantUriPermissions 属性,或者向您的  元素添加一个或多个  子元素。如果您使用了临时权限,则每当您从提供程序中移除对某个内容 URI 的支持,并且该内容 URI 关联了临时权限时,都需要调用 Context.revokeUriPermission()

该属性的值决定可访问的提供程序范围。 如果该属性设置为 true,则系统会向整个提供程序授予临时权限,该权限将替代您的提供程序级别或路径级别权限所需的任何其他权限。

如果此标志设置为 false,则您必须向  元素添加  子元素。每个子元素都指定授予的临时权限所对应的一个或多个内容 URI。

要向应用授予临时访问权限,Intent 必须包含 FLAG_GRANT_READ_URI_PERMISSION 和/或 FLAG_GRANT_WRITE_URI_PERMISSION 标志。 它们通过 setFlags() 方法进行设置。

如果 android:grantUriPermissions 属性不存在,则假设其为 false

元素


与 Activity 和 Service 组件类似,必须使用  元素在清单文件中为其应用定义 ContentProvider 的子类。 Android 系统会从该元素获取以下信息:

授权 ( android:authorities)
用于在系统内标识整个提供程序的符号名称。 设计内容 URI 部分对此属性做了更详尽的描述。
提供程序类名 (  android:name )
实现  ContentProvider 的类。 实现 ContentProvider 类中对此类做了更详尽的描述。
权限
指定其他应用访问提供程序的数据所必须具备权限的属性:
  • android:grantUriPermssions:临时权限标志
  • android:permission:统一提供程序范围读取/写入权限
  • android:readPermission:提供程序范围读取权限
  • android:writePermission:提供程序范围写入权限

实现内容提供程序权限部分对权限及其对应属性做了更详尽的描述。

启动和控制属性
这些属性决定 Android 系统如何以及何时启动提供程序、提供程序的进程特性以及其他运行时设置:
  • android:enabled:允许系统启动提供程序的标志。
  • android:exported:允许其他应用使用此提供程序的标志。
  • android:initOrder:此提供程序相对于同一进程中其他提供程序的启动顺序。
  • android:multiProcess:允许系统在与调用客户端相同的进程中启动提供程序的标志。
  • android:process:应在其中运行提供程序的进程的名称。
  • android:syncable:指示提供程序的数据将与服务器上的数据同步的标志。

开发指南中针对  元素的主题提供了这些属性的完整资料。

信息属性
提供程序的可选图标和标签:
  • android:icon:包含提供程序图标的可绘制对象资源。 该图标出现在Settings > Apps > All 中应用列表内的提供程序标签旁;
  • android:label:描述提供程序和/或其数据的信息标签。 该标签出现在Settings > Apps > All中的应用列表内。

开发指南中针对  元素的主题提供了这些属性的完整资料。

Intent 和数据访问


应用可以通过 Intent 间接访问内容提供程序。 应用不会调用 ContentResolver 或 ContentProvider 的任何方法,它并不会直接提供,而是会发送一个启动某个 Activity 的 Intent,该 Activity 通常是提供程序自身应用的一部分。 目标 Activity 负责检索和显示其 UI 中的数据。 视 Intent 中的操作而定,目标 Activity 可能还会提示用户对提供程序的数据进行修改。 Intent 可能还包含目标 Activity 在 UI 中显示的“extra”数据;用户随后可以选择更改此数据,然后使用它来修改提供程序中的数据。

您可能想使用 Intent 访问权限来帮助确保数据完整性。您的提供程序可能依赖于根据严格定义的业务逻辑插入、更新和删除数据。 如果是这种情况,则允许其他应用直接修改您的数据可能会导致无效的数据。 如果您想让开发者使用 Intent 访问权限,请务必为其提供详尽的参考资料。 向他们解释为什么使用自身应用 UI 的 Intent 访问比尝试通过代码修改数据更好。

处理想要修改您的提供程序数据的传入 Intent 与处理其他 Intent 没有区别。 您可以通过阅读 Intent 和 Intent 过滤器主题了解有关 Intent 用法的更多信息。



日历提供程序

本文内容

  1. 基础知识
  2. 用户权限
  3. 日历表
    1. 查询日历
    2. 修改日历
    3. 插入日历
  4. 事件表
    1. 添加事件
    2. 更新事件
    3. 删除事件
  5. 参加者表
    1. 添加参加者
  6. 提醒表
    1. 添加提醒
  7. 实例表
    1. 查询实例表
  8. 日历 Intent
    1. 使用 Intent 插入事件
    2. 使用 Intent 编辑事件
    3. 使用 Intent 查看日历数据
  9. 同步适配器

关键类

  1. CalendarContract.Calendars
  2. CalendarContract.Events
  3. CalendarContract.Attendees
  4. CalendarContract.Reminders

日历提供程序是用户日历事件的存储区。您可以利用 Calendar Provider API 对日历、事件、参加者、提醒等执行查询、插入、更新和删除操作。

Calender Provider API 可供应用和同步适配器使用。规则因进行调用的程序类型而异。 本文主要侧重于介绍使用 Calendar Provider API 作为应用的情况。如需了解对各类同步适配器差异的阐述,请参阅同步适配器。

正常情况下,要想读取或写入日历数据,应用的清单文件必须包括用户权限中所述的适当权限。 为简化常见操作的执行,日历提供程序提供了一组 Intent,日历 Intent中对这些 Intent 做了说明。 这些 Intent 会将用户转到日历应用,执行插入事件、查看事件和编辑事件。 用户与日历应用交互,然后返回原来的应用。 因此,您的应用不需要请求权限,也不需要提供用于查看事件或创建事件的用户界面。

基础知识


内容提供程序存储数据并使其可供应用访问。 Android 平台提供的内容提供程序(包括日历提供程序)通常以一组基于关系型数据库模型的表格形式公开数据,在这个表格中,每一行都是一条记录,每一列都是特定类型和含义的数据。 应用和同步适配器可以通过 Calendar Provider API 获得对储存用户日历数据的数据库表的读取/写入权限。

每一个内容提供程序都会公开一个对其数据集进行唯一标识的公共 URI(包装成一个 Uri 对象)。 控制多个数据集(多个表)的内容提供程序会为每个数据集公开单独的 URI。 所有提供程序 URI 都以字符串“content://”开头。 这表示数据受内容提供程序的控制。 日历提供程序会为其每个类(表)定义 URI 常量。这些 URI 的格式为 .CONTENT_URI。例如,Events.CONTENT_URI

图 1 是对日历提供程序数据模型的图形化表示。它显示了将彼此链接在一起的主要表和字段。

Android Developer - ContentProvider_第1张图片

图 1. 日历提供程序数据模型。

用户可以有多个日历,可将不同类型的日历与不同类型的帐户(Google 日历、Exchange 等)关联。

CalendarContract 定义了日历和事件相关信息的数据模型。这些数据存储在以下所列的若干表中。

表(类) 说明

CalendarContract.Calendars

此表储存日历特定信息。 此表中的每一行都包含一个日历的详细信息,例如名称、颜色、同步信息等。
CalendarContract.Events 此表储存事件特定信息。 此表中的每一行都包含一个事件的信息 — 例如事件标题、地点、开始时间、结束时间等。 事件可一次性发生,也可多次重复发生。参加者、提醒和扩展属性存储在单独的表内。它们各自具有一个 EVENT_ID,用于引用 Events 表中的 _ID
CalendarContract.Instances 此表储存每个事件实例的开始时间和结束时间。 此表中的每一行都表示一个事件实例。 对于一次性事件,实例与事件为 1:1 映射。对于重复事件,会自动生成多个行,分别对应多个事件实例。
CalendarContract.Attendees 此表储存事件参加者(来宾)信息。 每一行都表示事件的一位来宾。 它指定来宾的类型以及事件的来宾出席响应。
CalendarContract.Reminders 此表储存提醒/通知数据。 每一行都表示事件的一个提醒。一个事件可以有多个提醒。 每个事件的最大提醒数量在 MAX_REMINDERS 中指定,后者由拥有给定日历的同步适配器设置。 提醒以事件发生前的分钟数形式指定,其具有一个可决定用户提醒方式的方法。

Calendar Provider API 以灵活、强大为设计宗旨。提供良好的最终用户体验以及保护日历及其数据的完整性也同样重要。 因此,请在使用该 API 时牢记以下要点:

  • 插入、更新和查看日历事件。要想直接从日历提供程序插入事件、修改事件以及读取事件,您需要具备相应权限。不过,如果您开发的并不是完备的日历应用或同步适配器,则无需请求这些权限。您可以改用 Android 的日历应用支持的 Intent 将读取操作和写入操作转到该应用执行。当您使用 Intent 时,您的应用会将用户转到日历应用,在一个预填充表单中执行所需操作。 完成操作后,用户将返回您的应用。通过将您的应用设计为通过日历执行常见操作,可以为用户提供一致、可靠的用户界面。 这是推荐您采用的方法。 如需了解详细信息,请参阅日历 Intent。

  • 同步适配器。同步适配器用于将用户设备上的日历数据与其他服务器或数据源同步。 在 CalendarContract.Calendars 和 CalendarContract.Events表中,预留了一些供同步适配器使用的列。提供程序和应用不应修改它们。 实际上,除非以同步适配器形式进行访问,否则它们处于不可见状态。 如需了解有关同步适配器的详细信息,请参阅同步适配器。

用户权限


如需读取日历数据,应用必须在其清单文件中加入 READ_CALENDAR 权限。文件中必须包括用于删除、插入或更新日历数据的 WRITE_CALENDAR 权限:

xml version="1.0" encoding="utf-8"?>
 xmlns:android="http://schemas.android.com/apk/res/android"...>
     android:minSdkVersion="14" />
     android:name="android.permission.READ_CALENDAR" />
     android:name="android.permission.WRITE_CALENDAR" />
    ...

日历表


CalendarContract.Calendars 表包含各日历的详细信息。 应用和同步适配器均可写入下列日历列。如需查看所支持字段的完整列表,请参阅 CalendarContract.Calendars 参考资料。

常量 说明
NAME 日历的名称。
CALENDAR_DISPLAY_NAME 该日历显示给用户时使用的名称。
VISIBLE 表示是否选择显示该日历的布尔值。值为 0 表示不应显示与该日历关联的事件。 值为 1 表示应该显示与该日历关联的事件。此值影响 CalendarContract.Instances 表中行的生成。
SYNC_EVENTS 一个布尔值,表示是否应同步日历并将其事件存储在设备上。 值为 0 表示不同步该日历,也不将其事件存储在设备上。值为 1 表示同步该日历的事件,并将其事件存储在设备上。

查询日历

以下示例说明了如何获取特定用户拥有的日历。 为了简便起见,在此示例中,查询操作显示在用户界面线程(“主线程”)中。 实际上,此操作应该在一个异步线程而非主线程中完成。 如需查看更详细的介绍,请参阅加载器。 如果您的目的不只是读取数据,还要修改数据,请参阅 AsyncQueryHandler

// Projection array. Creating indices for this array instead of doing
// dynamic lookups improves performance.
public static final String[] EVENT_PROJECTION = new String[] {
    Calendars._ID,                           // 0
    Calendars.ACCOUNT_NAME,                  // 1
    Calendars.CALENDAR_DISPLAY_NAME,         // 2
    Calendars.OWNER_ACCOUNT                  // 3
};

// The indices for the projection array above.
private static final int PROJECTION_ID_INDEX = 0;
private static final int PROJECTION_ACCOUNT_NAME_INDEX = 1;
private static final int PROJECTION_DISPLAY_NAME_INDEX = 2;
private static final int PROJECTION_OWNER_ACCOUNT_INDEX = 3;

在示例的下一部分,您需要构建查询。选定范围指定查询的条件。 在此示例中,查询寻找的是ACCOUNT_NAME 为“[email protected]”、ACCOUNT_TYPE 为“com.google”、OWNER_ACCOUNT为“[email protected]”的日历。如果您想查看用户查看过的所有日历,而不只是用户拥有的日历,请省略 OWNER_ACCOUNT。您可以利用查询返回的 Cursor 对象遍历数据库查询返回的结果集。 如需查看有关在内容提供程序中使用查询的详细介绍,请参阅内容提供程序。

// Run query
Cursor cur = null;
ContentResolver cr = getContentResolver();
Uri uri = Calendars.CONTENT_URI;
String selection = "((" + Calendars.ACCOUNT_NAME + " = ?) AND ("
                        + Calendars.ACCOUNT_TYPE + " = ?) AND ("
                        + Calendars.OWNER_ACCOUNT + " = ?))";
String[] selectionArgs = new String[] {"[email protected]", "com.google",
        "[email protected]"};
// Submit the query and get a Cursor object back.
cur = cr.query(uri, EVENT_PROJECTION, selection, selectionArgs, null);

以下后续部分使用游标单步调试结果集。它使用在示例开头设置的常量来返回每个字段的值。

// Use the cursor to step through the returned records
while (cur.moveToNext()) {
    long calID = 0;
    String displayName = null;
    String accountName = null;
    String ownerName = null;

    // Get the field values
    calID = cur.getLong(PROJECTION_ID_INDEX);
    displayName = cur.getString(PROJECTION_DISPLAY_NAME_INDEX);
    accountName = cur.getString(PROJECTION_ACCOUNT_NAME_INDEX);
    ownerName = cur.getString(PROJECTION_OWNER_ACCOUNT_INDEX);

    // Do something with the values...

   ...
}

修改日历

如需执行日历更新,您可以通过 URI 追加 ID (withAppendedId()) 或第一个选定项形式提供日历的 _ID。 选定范围应以 "_id=?" 开头,并且第一个selectionArg 应为事件的 _ID。您还可以通过在 URI 中编码 ID 来执行更新。 下例使用 (withAppendedId()) 方法更改日历的显示名称:

private static final String DEBUG_TAG = "MyActivity";
...
long calID = 2;
ContentValues values = new ContentValues();
// The new display name for the calendar
values.put(Calendars.CALENDAR_DISPLAY_NAME, "Trevor's Calendar");
Uri updateUri = ContentUris.withAppendedId(Calendars.CONTENT_URI, calID);
int rows = getContentResolver().update(updateUri, values, null, null);
Log.i(DEBUG_TAG, "Rows updated: " + rows);

插入日历

日历设计为主要由同步适配器进行管理,因此您只应以同步适配器形式插入新日历。 在大多数情况下,应用只能对日历进行一些表面更改,如更改显示名称。 如果应用需要创建本地日历,可以利用 ACCOUNT_TYPE_LOCAL 的 ACCOUNT_TYPE,通过以同步适配器形式执行日历插入来实现目的。ACCOUNT_TYPE_LOCAL 是一种特殊的帐户类型,用于未关联设备帐户的日历。 这种类型的日历不与服务器同步。如需了解对同步适配器的阐述,请参阅同步适配器。

事件表


CalendarContract.Events 表包含各事件的详细信息。要想添加、更新或删除事件,应用必须在其清单文件中加入 WRITE_CALENDAR 权限。

应用和同步适配器均可写入下列事件列。 如需查看所支持字段的完整列表,请参阅CalendarContract.Events 参考资料。

常量 说明
CALENDAR_ID 事件所属日历的 _ID
ORGANIZER 事件组织者(所有者)的电子邮件。
TITLE 事件的标题。
EVENT_LOCATION 事件的发生地点。
DESCRIPTION 事件的描述。
DTSTART 事件开始时间,以从公元纪年开始计算的协调世界时毫秒数表示。
DTEND 事件结束时间,以从公元纪年开始计算的协调世界时毫秒数表示。
EVENT_TIMEZONE 事件的时区。
EVENT_END_TIMEZONE 事件结束时间的时区。
DURATION RFC5545 格式的事件持续时间。例如,值为 "PT1H" 表示事件应持续一小时,值为 "P2W" 表示持续 2 周。
ALL_DAY 值为 1 表示此事件占用一整天(按照本地时区的定义)。 值为 0 表示它是常规事件,可在一天内的任何时间开始和结束。
RRULE 事件的重复发生规则格式。例如,"FREQ=WEEKLY;COUNT=10;WKST=SU"。 您可以在此处找到更多示例。
RDATE 事件的重复发生日期。 RDATE 与 RRULE 通常联合用于定义一组聚合重复实例。 如需查看更详细的介绍,请参阅 RFC5545 规范。
AVAILABILITY 将此事件视为忙碌时间还是可调度的空闲时间。
GUESTS_CAN_MODIFY 来宾是否可修改事件。
GUESTS_CAN_INVITE_OTHERS 来宾是否可邀请其他来宾。
GUESTS_CAN_SEE_GUESTS 来宾是否可查看参加者列表。

添加事件

当您的应用插入新事件时,我们建议您按照使用 Intent 插入事件中所述使用 INSERT Intent。不过,您可以在需要时直接插入事件。 本节描述如何执行此操作。

以下是插入新事件的规则:

  • 您必须加入 CALENDAR_ID 和 DTSTART
  • 您必须加入 EVENT_TIMEZONE。如需获取系统中已安装时区 ID 的列表,请使用 getAvailableIDs()。 请注意,如果您按使用 Intent 插入事件中所述通过 INSERT Intent 插入事件,则此规则不适用 — 在该情形下,系统会提供默认时区。
  • 对于非重复事件,您必须加入 DTEND
  • 对于重复事件,您必须加入 DURATION 以及 RRULE 或 RDATE。请注意,如果您按使用 Intent 插入事件中所述通过 INSERT Intent 插入事件,则此规则不适用 — 在该情形下,您可以将 RRULE 与 DTSTART 和 DTEND 结合使用,日历应用会自动将其转换为持续时间。

以下是一个插入事件的示例。为了简便起见,此操作是在 UI 线程内执行的。实际上,应该在异步线程中完成插入和更新,以便将操作移入后台线程。 如需了解详细信息,请参阅 AsyncQueryHandler

long calID = 3;
long startMillis = 0;
long endMillis = 0;
Calendar beginTime = Calendar.getInstance();
beginTime.set(2012, 9, 14, 7, 30);
startMillis = beginTime.getTimeInMillis();
Calendar endTime = Calendar.getInstance();
endTime.set(2012, 9, 14, 8, 45);
endMillis = endTime.getTimeInMillis();
...

ContentResolver cr = getContentResolver();
ContentValues values = new ContentValues();
values.put(Events.DTSTART, startMillis);
values.put(Events.DTEND, endMillis);
values.put(Events.TITLE, "Jazzercise");
values.put(Events.DESCRIPTION, "Group workout");
values.put(Events.CALENDAR_ID, calID);
values.put(Events.EVENT_TIMEZONE, "America/Los_Angeles");
Uri uri = cr.insert(Events.CONTENT_URI, values);

// get the event ID that is the last element in the Uri
long eventID = Long.parseLong(uri.getLastPathSegment());
//
// ... do something with event ID
//
//

注:请注意以上示例如何在事件创建后捕获事件 ID。 这是获取事件 ID 的最简单方法。您经常需要使用事件 ID 来执行其他日历操作 — 例如,向事件添加参加者或提醒。

更新事件

当您的应用想允许用户编辑事件时,我们建议您按照使用 Intent 编辑事件中所述使用 EDIT Intent。不过,您可以在需要时直接编辑事件。 如需执行事件更新,您可以通过 URI 追加 ID (withAppendedId()) 或第一个选定项形式提供事件的 _ID。选定范围应以 "_id=?" 开头,并且第一个 selectionArg 应为事件的 _ID。 您还可以使用不含 ID 的选定范围执行更新。 以下是一个更新事件的示例。 它使用 withAppendedId() 方法更改事件的标题:

private static final String DEBUG_TAG = "MyActivity";
...
long eventID = 188;
...
ContentResolver cr = getContentResolver();
ContentValues values = new ContentValues();
Uri updateUri = null;
// The new title for the event
values.put(Events.TITLE, "Kickboxing");
updateUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventID);
int rows = getContentResolver().update(updateUri, values, null, null);
Log.i(DEBUG_TAG, "Rows updated: " + rows);  

删除事件

您可以通过将事件 _ID 作为 URI 追加 ID 或通过使用标准选定范围来删除事件。如果您使用追加 ID,则将无法同时使用选定范围。共有两个版本的删除:应用删除和同步适配器删除。应用删除将 deleted 列设置为 1。此标志告知同步适配器该行已删除,并且应将此删除操作传播至服务器。 同步适配器删除会将事件连同其所有关联数据从数据库中移除。 以下是一个应用通过事件 _ID 删除事件的示例:

private static final String DEBUG_TAG = "MyActivity";
...
long eventID = 201;
...
ContentResolver cr = getContentResolver();
ContentValues values = new ContentValues();
Uri deleteUri = null;
deleteUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventID);
int rows = getContentResolver().delete(deleteUri, null, null);
Log.i(DEBUG_TAG, "Rows deleted: " + rows);

参加者表


CalendarContract.Attendees 表的每一行都表示事件的一位参加者或来宾。调用 query() 会返回一个参加者列表,其中包含具有给定 EVENT_ID 的事件的参加者。此 EVENT_ID 必须匹配特定事件的 _ID

下表列出了可写入的字段。 插入新参加者时,您必须加入除 ATTENDEE_NAME 之外的所有字段。

常量 说明
EVENT_ID 事件的 ID。
ATTENDEE_NAME 参加者的姓名。
ATTENDEE_EMAIL 参加者的电子邮件地址。
ATTENDEE_RELATIONSHIP

参加者与事件的关系。下列值之一:

  • RELATIONSHIP_ATTENDEE
  • RELATIONSHIP_NONE
  • RELATIONSHIP_ORGANIZER
  • RELATIONSHIP_PERFORMER
  • RELATIONSHIP_SPEAKER
ATTENDEE_TYPE

参加者的类型。下列值之一:

  • TYPE_REQUIRED
  • TYPE_OPTIONAL
ATTENDEE_STATUS

参加者的出席状态。下列值之一:

  • ATTENDEE_STATUS_ACCEPTED
  • ATTENDEE_STATUS_DECLINED
  • ATTENDEE_STATUS_INVITED
  • ATTENDEE_STATUS_NONE
  • ATTENDEE_STATUS_TENTATIVE

添加参加者

以下是一个为事件添加一位参加者的示例。请注意,EVENT_ID 是必填项:

long eventID = 202;
...
ContentResolver cr = getContentResolver();
ContentValues values = new ContentValues();
values.put(Attendees.ATTENDEE_NAME, "Trevor");
values.put(Attendees.ATTENDEE_EMAIL, "[email protected]");
values.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ATTENDEE);
values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_OPTIONAL);
values.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_INVITED);
values.put(Attendees.EVENT_ID, eventID);
Uri uri = cr.insert(Attendees.CONTENT_URI, values);

提醒表


CalendarContract.Reminders 表的每一行都表示事件的一个提醒。调用 query() 会返回一个提醒列表,其中包含具有给定 EVENT_ID 的事件的提醒。

下表列出了提醒的可写入字段。插入新提醒时,必须加入所有字段。 请注意,同步适配器指定它们在 CalendarContract.Calendars 表中支持的提醒类型。 详情请参阅 ALLOWED_REMINDERS

常量 说明
EVENT_ID 事件的 ID。
MINUTES 事件发生前的分钟数,应在达到该时间时发出提醒。
METHOD

服务器上设置的提醒方法。下列值之一:

  • METHOD_ALERT
  • METHOD_DEFAULT
  • METHOD_EMAIL
  • METHOD_SMS

添加提醒

下例显示如何为事件添加提醒。提醒在事件发生前 15 分钟发出。

long eventID = 221;
...
ContentResolver cr = getContentResolver();
ContentValues values = new ContentValues();
values.put(Reminders.MINUTES, 15);
values.put(Reminders.EVENT_ID, eventID);
values.put(Reminders.METHOD, Reminders.METHOD_ALERT);
Uri uri = cr.insert(Reminders.CONTENT_URI, values);

实例表


CalendarContract.Instances 表储存事件实例的开始时间和结束时间。 此表中的每一行都表示一个事件实例。 实例表无法写入,只提供查询事件实例的途径。

下表列出了一些您可以执行实例查询的字段。请注意,时区由 KEY_TIMEZONE_TYPE 和 KEY_TIMEZONE_INSTANCES 定义。

常量 说明
BEGIN 实例的开始时间,以协调世界时毫秒数表示。
END 实例的结束时间,以协调世界时毫秒数表示。
END_DAY 与日历时区相应的实例儒略历结束日。
END_MINUTE 从日历时区午夜开始计算的实例结束时间(分钟)。
EVENT_ID 该实例对应事件的 _ID
START_DAY 与日历时区相应的实例儒略历开始日。
START_MINUTE 从日历时区午夜开始计算的实例开始时间(分钟)。

查询实例表

如需查询实例表,您需要在 URI 中指定查询的时间范围。 在以下示例中,CalendarContract.Instances 通过其 CalendarContract.EventsColumns 接口实现获得对 TITLE 字段的访问权限。换言之,TITLE 是通过数据库视图,而不是通过查询原始 CalendarContract.Instances 表返回的。

private static final String DEBUG_TAG = "MyActivity";
public static final String[] INSTANCE_PROJECTION = new String[] {
    Instances.EVENT_ID,      // 0
    Instances.BEGIN,         // 1
    Instances.TITLE          // 2
  };

// The indices for the projection array above.
private static final int PROJECTION_ID_INDEX = 0;
private static final int PROJECTION_BEGIN_INDEX = 1;
private static final int PROJECTION_TITLE_INDEX = 2;
...

// Specify the date range you want to search for recurring
// event instances
Calendar beginTime = Calendar.getInstance();
beginTime.set(2011, 9, 23, 8, 0);
long startMillis = beginTime.getTimeInMillis();
Calendar endTime = Calendar.getInstance();
endTime.set(2011, 10, 24, 8, 0);
long endMillis = endTime.getTimeInMillis();

Cursor cur = null;
ContentResolver cr = getContentResolver();

// The ID of the recurring event whose instances you are searching
// for in the Instances table
String selection = Instances.EVENT_ID + " = ?";
String[] selectionArgs = new String[] {"207"};

// Construct the query with the desired date range.
Uri.Builder builder = Instances.CONTENT_URI.buildUpon();
ContentUris.appendId(builder, startMillis);
ContentUris.appendId(builder, endMillis);

// Submit the query
cur =  cr.query(builder.build(),
    INSTANCE_PROJECTION,
    selection,
    selectionArgs,
    null);

while (cur.moveToNext()) {
    String title = null;
    long eventID = 0;
    long beginVal = 0;

    // Get the field values
    eventID = cur.getLong(PROJECTION_ID_INDEX);
    beginVal = cur.getLong(PROJECTION_BEGIN_INDEX);
    title = cur.getString(PROJECTION_TITLE_INDEX);

    // Do something with the values.
    Log.i(DEBUG_TAG, "Event:  " + title);
    Calendar calendar = Calendar.getInstance();
    calendar.setTimeInMillis(beginVal);
    DateFormat formatter = new SimpleDateFormat("MM/dd/yyyy");
    Log.i(DEBUG_TAG, "Date: " + formatter.format(calendar.getTime()));
    }
 }

日历 Intent


您的应用不需要读取和写入日历数据的权限。它可以改用 Android 的日历应用支持的 Intent 将读取和写入操作转到该应用执行。下表列出了日历提供程序支持的 Intent:

操作 URI 说明 Extra

VIEW 

content://com.android.calendar/time/

您还可以通过 CalendarContract.CONTENT_URI 引用 URI。如需查看使用该 Intent 的示例,请参阅使用 Intent 查看日历数据。
打开日历后定位到 指定的时间。 无。

VIEW

content://com.android.calendar/events/

您还可以通过 Events.CONTENT_URI 引用 URI。如需查看使用该 Intent 的示例,请参阅使用 Intent 查看日历数据。
查看  指定的事件。 CalendarContract.EXTRA_EVENT_BEGIN_TIME


CalendarContract.EXTRA_EVENT_END_TIME
EDIT

content://com.android.calendar/events/

您还可以通过 Events.CONTENT_URI 引用 URI。如需查看使用该 Intent 的示例,请参阅使用 Intent 编辑事件。
编辑  指定的事件。 CalendarContract.EXTRA_EVENT_BEGIN_TIME


CalendarContract.EXTRA_EVENT_END_TIME
EDIT 

INSERT

content://com.android.calendar/events

您还可以通过 Events.CONTENT_URI 引用 URI。如需查看使用该 Intent 的示例,请参阅使用 Intent 插入事件。
创建事件。 下表列出的任一 Extra。

下表列出了日历提供程序支持的 Intent Extra:

Intent Extra 说明
Events.TITLE 事件的名称。
CalendarContract.EXTRA_EVENT_BEGIN_TIME 事件开始时间,以从公元纪年开始计算的毫秒数表示。
CalendarContract.EXTRA_EVENT_END_TIME 事件结束时间,以从公元纪年开始计算的毫秒数表示。
CalendarContract.EXTRA_EVENT_ALL_DAY 一个布尔值,表示事件属于全天事件。值可以是 true 或 false
Events.EVENT_LOCATION 事件的地点。
Events.DESCRIPTION 事件描述。
Intent.EXTRA_EMAIL 逗号分隔值形式的受邀者电子邮件地址列表。
Events.RRULE 事件的重复发生规则。
Events.ACCESS_LEVEL 事件是私人性质还是公共性质。
Events.AVAILABILITY 将此事件视为忙碌时间还是可调度的空闲时间。

下文描述如何使用这些 Intent。

使用 Intent 插入事件

您的应用可以利用 INSERT Intent 将事件插入任务转到日历应用执行。使用此方法时,您的应用甚至不需要在其清单文件中加入 WRITE_CALENDAR 权限。

当用户运行使用此方法的应用时,应用会将其转到日历来完成事件添加操作。 INSERT Intent 利用 extra 字段为表单预填充日历中事件的详细信息。 用户随后可取消事件、根据需要编辑表单或将事件保存到日历中。

以下是一个代码段,用于安排一个在 2012 年 1 月 19 日上午 7:30 开始、8:30 结束的事件。请注意该代码段中的以下内容:

  • 它将 Events.CONTENT_URI 指定为 URI。
  • 它使用 CalendarContract.EXTRA_EVENT_BEGIN_TIME 和 CalendarContract.EXTRA_EVENT_END_TIME extra 字段为表单预填充事件的时间。 这些时间的值必须以从公元纪年开始计算的协调世界时毫秒数表示。
  • 它使用 Intent.EXTRA_EMAIL extra 字段提供以逗号分隔的受邀者电子邮件地址列表。
Calendar beginTime = Calendar.getInstance();
beginTime.set(2012, 0, 19, 7, 30);
Calendar endTime = Calendar.getInstance();
endTime.set(2012, 0, 19, 8, 30);
Intent intent = new Intent(Intent.ACTION_INSERT)
        .setData(Events.CONTENT_URI)
        .putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, beginTime.getTimeInMillis())
        .putExtra(CalendarContract.EXTRA_EVENT_END_TIME, endTime.getTimeInMillis())
        .putExtra(Events.TITLE, "Yoga")
        .putExtra(Events.DESCRIPTION, "Group class")
        .putExtra(Events.EVENT_LOCATION, "The gym")
        .putExtra(Events.AVAILABILITY, Events.AVAILABILITY_BUSY)
        .putExtra(Intent.EXTRA_EMAIL, "[email protected],[email protected]");
startActivity(intent);

使用 Intent 编辑事件

您可以按更新事件中所述直接更新事件。但使用 EDIT Intent 可以让不具有事件编辑权限的应用将事件编辑操作转到日历应用执行。当用户在日历中完成事件编辑后,将会返回原来的应用。

以下是一个 Intent 的示例,它为指定事件设置新名称,并允许用户在日历中编辑事件。

long eventID = 208;
Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, eventID);
Intent intent = new Intent(Intent.ACTION_EDIT)
    .setData(uri)
    .putExtra(Events.TITLE, "My New Title");
startActivity(intent);

使用 Intent 查看日历数据

日历提供程序提供了两种不同的 VIEW Intent 使用方法:

  • 打开日历并定位到特定日期。
  • 查看事件。

下例显示如何打开日历并定位到特定日期:

// A date-time specified in milliseconds since the epoch.
long startMillis;
...
Uri.Builder builder = CalendarContract.CONTENT_URI.buildUpon();
builder.appendPath("time");
ContentUris.appendId(builder, startMillis);
Intent intent = new Intent(Intent.ACTION_VIEW)
    .setData(builder.build());
startActivity(intent);

下例显示如何打开事件进行查看:

long eventID = 208;
...
Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, eventID);
Intent intent = new Intent(Intent.ACTION_VIEW)
   .setData(uri);
startActivity(intent);

同步适配器


应用和同步适配器在访问日历提供程序的方式上只存在微小差异:

  • 同步适配器需要通过将 CALLER_IS_SYNCADAPTER 设置为 true 来表明它是同步适配器。
  • 同步适配器需要提供 ACCOUNT_NAME 和 ACCOUNT_TYPE 作为 URI 中的查询参数。
  • 与应用或小部件相比,同步适配器拥有写入权限的列更多。 例如,应用只能修改日历的少数几种特性,例如其名称、显示名称、能见度设置以及是否同步日历。 相比之下,同步适配器不仅可以访问这些列,还能访问许多其他列,例如日历颜色、时区、访问级别、地点等等。不过,同步适配器受限于它指定的 ACCOUNT_NAME 和 ACCOUNT_TYPE

您可以利用以下帮助程序方法返回供与同步适配器一起使用的 URI:

 static Uri asSyncAdapter(Uri uri, String account, String accountType) {
    return uri.buildUpon()
        .appendQueryParameter(android.provider.CalendarContract.CALLER_IS_SYNCADAPTER,"true")
        .appendQueryParameter(Calendars.ACCOUNT_NAME, account)
        .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build();
 }

如需查看同步适配器的实现示例(并非仅限与日历有关的实现),请参阅 SampleSyncAdapter。


存储访问框架

本文内容显示详细信息

  1. 概览
  2. 控制流
  3. 编写客户端应用
  4. 编写自定义文档提供程序

关键类

  1. DocumentsProvider
  2. DocumentsContract

视频

  1. DevBytes:Android 4.4 存储访问框架:提供程序
  2. DevBytes:Android 4.4 存储访问框架:客户端

代码示例

  1. 存储提供程序
  2. StorageClient

另请参阅

  1. 内容提供程序基础知识

Android 4.4(API 级别 19)引入了存储访问框架 (SAF)。SAF 让用户能够在其所有首选文档存储提供程序中方便地浏览并打开文档、图像以及其他文件。 用户可以通过易用的标准 UI,以统一方式在所有应用和提供程序中浏览文件和访问最近使用的文件。

云存储服务或本地存储服务可以通过实现封装其服务的 DocumentsProvider 参与此生态系统。只需几行代码,便可将需要访问提供程序文档的客户端应用与 SAF 集成。

SAF 包括以下内容:

  • 文档提供程序 — 一种内容提供程序,允许存储服务(如 Google Drive)显示其管理的文件。 文档提供程序作为 DocumentsProvider 类的子类实现。文档提供程序的架构基于传统文件层次结构,但其实际数据存储方式由您决定。Android 平台包括若干内置文档提供程序,如 Downloads、Images 和 Videos。
  • 客户端应用 — 一种自定义应用,它调用 ACTION_OPEN_DOCUMENT 和/或 ACTION_CREATE_DOCUMENT Intent 并接收文档提供程序返回的文件;
  • 选取器 — 一种系统 UI,允许用户访问所有满足客户端应用搜索条件的文档提供程序内的文档。

SAF 提供的部分功能如下:

  • 允许用户浏览所有文档提供程序而不仅仅是单个应用中的内容;
  • 让您的应用获得对文档提供程序所拥有文档的长期、持久性访问权限。 用户可以通过此访问权限添加、编辑、保存和删除提供程序上的文件;
  • 支持多个用户帐户和临时根目录,如只有在插入驱动器后才会出现的 USB 存储提供程序。

概览


SAF 围绕的内容提供程序是 DocumentsProvider 类的一个子类。在文档提供程序内,数据结构采用传统的文件层次结构:


图 1. 文档提供程序数据模型。根目录指向单个文档,后者随即启动整个结构树的扇出。

请注意以下事项:

  • 每个文档提供程序都会报告一个或多个作为探索文档结构树起点的“根目录”。每个根目录都有一个唯一的 COLUMN_ROOT_ID,并且指向表示该根目录下内容的文档(目录)。根目录采用动态设计,以支持多个帐户、临时 USB 存储设备或用户登录/注销等用例;
  • 每个根目录下都有一个文档。该文档指向 1 至 N 个文档,而其中每个文档又可指向 1 至 N 个文档;
  • 每个存储后端都会通过使用唯一的 COLUMN_DOCUMENT_ID 引用各个文件和目录来显示它们。文档 ID 必须具有唯一性,一旦发放便不得更改,因为它们用于所有设备重启过程中的永久性 URI 授权;
  • 文档可以是可打开的文件(具有特定 MIME 类型)或包含附加文档的目录(具有 MIME_TYPE_DIR MIME 类型);
  • 每个文档都可以具有不同的功能,如 COLUMN_FLAGS 所述。例如,FLAG_SUPPORTS_WRITEFLAG_SUPPORTS_DELETE 和 FLAG_SUPPORTS_THUMBNAIL。多个目录中可以包含相同的 COLUMN_DOCUMENT_ID

控制流


如前文所述,文档提供程序数据模型基于传统文件层次结构。 不过,只要可以通过 DocumentsProvider API 访问数据,您实际上可以按照自己喜好的任何方式存储数据。例如,您可以使用基于标记的云存储来存储数据。

图 2 中的示例展示的是照片应用如何利用 SAF 访问存储的数据:

Android Developer - ContentProvider_第2张图片

图 2. 存储访问框架流

请注意以下事项:

  • 在 SAF 中,提供程序和客户端并不直接交互。 客户端请求与文件交互(即读取、编辑、创建或删除文件)的权限;
  • 交互在应用(在本示例中为照片应用)触发 Intent ACTION_OPEN_DOCUMENT 或 ACTION_CREATE_DOCUMENT 后开始。Intent 可能包括进一步细化条件的过滤器 — 例如,“为我提供所有 MIME 类型为‘图像’的可打开文件”;
  • Intent 触发后,系统选取器将检索每个已注册的提供程序,并向用户显示匹配的内容根目录;
  • 选取器会为用户提供一个标准的文档访问界面,但底层文档提供程序可能与其差异很大。 例如,图 2 显示了一个 Google Drive 提供程序、一个 USB 提供程序和一个云提供程序。

图 3 显示了一个选取器,一位搜索图像的用户在其中选择了一个 Google Drive 帐户:

Android Developer - ContentProvider_第3张图片

图 3. 选取器

当用户选择 Google Drive 时,系统会显示图像,如图 4 所示。从这时起,用户就可以通过提供程序和客户端应用支持的任何方式与它们进行交互。

Android Developer - ContentProvider_第4张图片

图 4. 图像

编写客户端应用


对于 Android 4.3 及更低版本,如果您想让应用从其他应用中检索文件,它必须调用 ACTION_PICK 或 ACTION_GET_CONTENT 等 Intent。然后,用户必须选择一个要从中选取文件的应用,并且所选应用必须提供一个用户界面,以便用户浏览和选取可用文件。

对于 Android 4.4 及更高版本,您还可以选择使用 ACTION_OPEN_DOCUMENT Intent,后者会显示一个由系统控制的选取器 UI,用户可以通过它浏览其他应用提供的所有文件。用户只需通过这一个 UI 便可从任何受支持的应用中选取文件。

ACTION_OPEN_DOCUMENT 并非设计用于替代 ACTION_GET_CONTENT。应使用的 Intent 取决于应用的需要:

  • 如果您只想让应用读取/导入数据,请使用 ACTION_GET_CONTENT。使用此方法时,应用会导入数据(如图像文件)的副本;
  • 如果您想让应用获得对文档提供程序所拥有文档的长期、持久性访问权限,请使用 ACTION_OPEN_DOCUMENT。 例如,允许用户编辑存储在文档提供程序中的图像的照片编辑应用。

本节描述如何编写基于 ACTION_OPEN_DOCUMENT 和 ACTION_CREATE_DOCUMENT Intent 的客户端应用。

以下代码段使用 ACTION_OPEN_DOCUMENT 来搜索包含图像文件的文档提供程序:

private static final int READ_REQUEST_CODE = 42;
...
/**
 * Fires an intent to spin up the "file chooser" UI and select an image.
 */
public void performFileSearch() {

    // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file
    // browser.
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);

    // Filter to only show results that can be "opened", such as a
    // file (as opposed to a list of contacts or timezones)
    intent.addCategory(Intent.CATEGORY_OPENABLE);

    // Filter to show only images, using the image MIME data type.
    // If one wanted to search for ogg vorbis files, the type would be "audio/ogg".
    // To search for all documents available via installed storage providers,
    // it would be "*/*".
    intent.setType("image/*");

    startActivityForResult(intent, READ_REQUEST_CODE);
}

请注意以下事项:

  • 当应用触发 ACTION_OPEN_DOCUMENT Intent 时,后者会启动一个选取器来显示所有匹配的文档提供程序
  • 在 Intent 中添加类别 CATEGORY_OPENABLE 可对结果进行过滤,以仅显示可以打开的文档(如图像文件)
  • 语句 intent.setType("image/*") 可做进一步过滤,以仅显示 MIME 数据类型为图像的文档

处理结果

用户在选取器中选择文档后,系统就会调用 onActivityResult()。指向所选文档的 URI 包含在 resultData 参数中。使用 getData() 提取 URI。获得 URI 后,即可使用它来检索用户想要的文档。例如:

@Override
public void onActivityResult(int requestCode, int resultCode,
        Intent resultData) {

    // The ACTION_OPEN_DOCUMENT intent was sent with the request code
    // READ_REQUEST_CODE. If the request code seen here doesn't match, it's the
    // response to some other intent, and the code below shouldn't run at all.

    if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
        // The document selected by the user won't be returned in the intent.
        // Instead, a URI to that document will be contained in the return intent
        // provided to this method as a parameter.
        // Pull that URI using resultData.getData().
        Uri uri = null;
        if (resultData != null) {
            uri = resultData.getData();
            Log.i(TAG, "Uri: " + uri.toString());
            showImage(uri);
        }
    }
}

检查文档元数据

获得文档的 URI 后,即可获得对其元数据的访问权限。以下代码段用于获取 URI 所指定文档的元数据并将其记入日志:

public void dumpImageMetaData(Uri uri) {

    // The query, since it only applies to a single document, will only return
    // one row. There's no need to filter, sort, or select fields, since we want
    // all fields for one document.
    Cursor cursor = getActivity().getContentResolver()
            .query(uri, null, null, null, null, null);

    try {
    // moveToFirst() returns false if the cursor has 0 rows.  Very handy for
    // "if there's anything to look at, look at it" conditionals.
        if (cursor != null && cursor.moveToFirst()) {

            // Note it's called "Display Name".  This is
            // provider-specific, and might not necessarily be the file name.
            String displayName = cursor.getString(
                    cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
            Log.i(TAG, "Display Name: " + displayName);

            int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
            // If the size is unknown, the value stored is null.  But since an
            // int can't be null in Java, the behavior is implementation-specific,
            // which is just a fancy term for "unpredictable".  So as
            // a rule, check if it's null before assigning to an int.  This will
            // happen often:  The storage API allows for remote files, whose
            // size might not be locally known.
            String size = null;
            if (!cursor.isNull(sizeIndex)) {
                // Technically the column stores an int, but cursor.getString()
                // will do the conversion automatically.
                size = cursor.getString(sizeIndex);
            } else {
                size = "Unknown";
            }
            Log.i(TAG, "Size: " + size);
        }
    } finally {
        cursor.close();
    }
}

打开文档

获得文档的 URI 后,即可打开文档或对其执行任何其他您想要执行的操作。

位图

以下示例展示了如何打开 Bitmap

private Bitmap getBitmapFromUri(Uri uri) throws IOException {
    ParcelFileDescriptor parcelFileDescriptor =
            getContentResolver().openFileDescriptor(uri, "r");
    FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
    Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor);
    parcelFileDescriptor.close();
    return image;
}

请注意,您不应在 UI 线程上执行此操作。请使用 AsyncTask 在后台执行此操作。打开位图后,即可在 ImageView 中显示它。

获取 InputStream

以下示例展示了如何从 URI 中获取 InputStream。在此代码段中,系统将文件行读取到一个字符串中:

private String readTextFromUri(Uri uri) throws IOException {
    InputStream inputStream = getContentResolver().openInputStream(uri);
    BufferedReader reader = new BufferedReader(new InputStreamReader(
            inputStream));
    StringBuilder stringBuilder = new StringBuilder();
    String line;
    while ((line = reader.readLine()) != null) {
        stringBuilder.append(line);
    }
    fileInputStream.close();
    parcelFileDescriptor.close();
    return stringBuilder.toString();
}

创建新文档

您的应用可以使用 ACTION_CREATE_DOCUMENT Intent 在文档提供程序中创建新文档。要想创建文件,请为您的 Intent 提供一个 MIME 类型和文件名,然后通过唯一的请求代码启动它。系统会为您执行其余操作:

// Here are some examples of how you might call this method.
// The first parameter is the MIME type, and the second parameter is the name
// of the file you are creating:
//
// createFile("text/plain", "foobar.txt");
// createFile("image/png", "mypicture.png");

// Unique request code.
private static final int WRITE_REQUEST_CODE = 43;
...
private void createFile(String mimeType, String fileName) {
    Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);

    // Filter to only show results that can be "opened", such as
    // a file (as opposed to a list of contacts or timezones).
    intent.addCategory(Intent.CATEGORY_OPENABLE);

    // Create a file with the requested MIME type.
    intent.setType(mimeType);
    intent.putExtra(Intent.EXTRA_TITLE, fileName);
    startActivityForResult(intent, WRITE_REQUEST_CODE);
}

创建新文档后,即可在 onActivityResult() 中获取其 URI,以便继续向其写入内容。

删除文档

如果您获得了文档的 URI,并且文档的 Document.COLUMN_FLAGS 包含 SUPPORTS_DELETE,便可以删除该文档。例如:

DocumentsContract.deleteDocument(getContentResolver(), uri);

编辑文档

您可以使用 SAF 就地编辑文本文档。以下代码段会触发 ACTION_OPEN_DOCUMENT Intent 并使用类别 CATEGORY_OPENABLE 以仅显示可以打开的文档。它会进一步过滤以仅显示文本文件:

private static final int EDIT_REQUEST_CODE = 44;
/**
 * Open a file for writing and append some text to it.
 */
 private void editDocument() {
    // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's
    // file browser.
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);

    // Filter to only show results that can be "opened", such as a
    // file (as opposed to a list of contacts or timezones).
    intent.addCategory(Intent.CATEGORY_OPENABLE);

    // Filter to show only text files.
    intent.setType("text/plain");

    startActivityForResult(intent, EDIT_REQUEST_CODE);
}

接下来,您可以从 onActivityResult()(请参阅处理结果)调用代码以执行编辑。以下代码段可从 ContentResolver 获取 FileOutputStream。默认情况下,它使用“写入”模式。最佳做法是请求获得所需的最低限度访问权限,因此如果您只需要写入权限,就不要请求获得读取/写入权限。

private void alterDocument(Uri uri) {
    try {
        ParcelFileDescriptor pfd = getActivity().getContentResolver().
                openFileDescriptor(uri, "w");
        FileOutputStream fileOutputStream =
                new FileOutputStream(pfd.getFileDescriptor());
        fileOutputStream.write(("Overwritten by MyCloud at " +
                System.currentTimeMillis() + "\n").getBytes());
        // Let the document provider know you're done by closing the stream.
        fileOutputStream.close();
        pfd.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

保留权限

当您的应用打开文件进行读取或写入时,系统会为您的应用提供针对该文件的 URI 授权。 该授权将一直持续到用户设备重启时。但假定您的应用是图像编辑应用,而且您希望用户能够直接从应用中访问他们编辑的最后 5 张图像。 如果用户的设备已经重启,您就需要将用户转回系统选取器以查找这些文件,这显然不是理想的做法。

为防止出现这种情况,您可以保留系统为您的应用授予的权限。 您的应用实际上是“获取”了系统提供的持久 URI 授权。 这使用户能够通过您的应用持续访问文件,即使设备已重启也不受影响:

final int takeFlags = intent.getFlags()
            & (Intent.FLAG_GRANT_READ_URI_PERMISSION
            | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
// Check for the freshest data.
getContentResolver().takePersistableUriPermission(uri, takeFlags);

还有最后一个步骤。您可能已经保存了应用最近访问的 URI,但它们可能不再有效 — 另一个应用可能已删除或修改了文档。 因此,您应该始终调用getContentResolver().takePersistableUriPermission() 以检查有无最新数据。

编写自定义文档提供程序


如果您要开发为文件提供存储服务(如云保存服务)的应用,可以通过编写自定义文档提供程序,通过 SAF 提供您的文件。 本节描述如何执行此操作。

清单文件

要想实现自定义文档提供程序,请将以下内容添加到您的应用的清单文件:

  • 一个 API 级别 19 或更高级别的目标;
  • 一个声明自定义存储提供程序的  元素;
  • 提供程序的名称(即其类名),包括软件包名称。例如:com.example.android.storageprovider.MyCloudProvider
  • 授权的名称,即您的软件包名称(在本例中为 com.example.android.storageprovider)加内容提供程序的类型 (documents)。例如,com.example.android.storageprovider.documents
  • 属性 android:exported 设置为 "true"。您必须导出提供程序,以便其他应用可以看到;
  • 属性 android:grantUriPermissions 设置为 "true"。此设置允许系统向其他应用授予对提供程序中内容的访问权限。 如需查看有关保留对特定文档授权的阐述,请参阅保留权限;
  • MANAGE_DOCUMENTS 权限。默认情况下,提供程序对所有人可用。 添加此权限将限定您的提供程序只能供系统使用。此限制具有重要的安全意义;
  • android:enabled 属性设置为在资源文件中定义的一个布尔值。此属性的用途是,在运行 Android 4.3 或更低版本的设备上停用提供程序。例如,android:enabled="@bool/atLeastKitKat"。 除了在清单文件中加入此属性外,您还需要执行以下操作;
    • 在 res/values/ 下的 bool.xml 资源文件中,添加以下行:
       name="atLeastKitKat">false
    • 在 res/values-v19/ 下的 bool.xml 资源文件中,添加以下行:
       name="atLeastKitKat">true
  • 一个包括 android.content.action.DOCUMENTS_PROVIDER 操作的 Intent 过滤器,以便在系统搜索提供程序时让您的提供程序出现在选取器中。

以下是从一个包括提供程序的示例清单文件中摘录的内容:

... >
    ...
    
        android:minSdkVersion="19"
        android:targetSdkVersion="19" />
        ....
        
            android:name="com.example.android.storageprovider.MyCloudProvider"
            android:authorities="com.example.android.storageprovider.documents"
            android:grantUriPermissions="true"
            android:exported="true"
            android:permission="android.permission.MANAGE_DOCUMENTS"
            android:enabled="@bool/atLeastKitKat">
            
                 android:name="android.content.action.DOCUMENTS_PROVIDER" />
            
        
    

支持运行 Android 4.3 及更低版本的设备

ACTION_OPEN_DOCUMENT Intent 仅可用于运行 Android 4.4 及更高版本的设备。如果您想让应用支持 ACTION_GET_CONTENT 以适应运行 Android 4.3 及更低版本的设备,则应在您的清单文件中为运行 Android 4.4 或更高版本的设备停用 ACTION_GET_CONTENT Intent 过滤器。 应将文档提供程序和ACTION_GET_CONTENT 视为具有互斥性。如果您同时支持这两者,您的应用将在系统选取器 UI 中出现两次,提供两种不同的方式来访问您存储的数据。 这会给用户造成困惑。

建议按照以下步骤为运行 Android 4.4 版或更高版本的设备停用 ACTION_GET_CONTENT Intent 过滤器:

  1. 在 res/values/ 下的 bool.xml 资源文件中,添加以下行:
     name="atMostJellyBeanMR2">true
  2. 在 res/values-v19/ 下的 bool.xml 资源文件中,添加以下行:
     name="atMostJellyBeanMR2">false
  3. 添加一个 Activity 别名,为 4.4 版(API 级别 19)或更高版本停用 ACTION_GET_CONTENT Intent 过滤器。 例如:
    
     android:name="com.android.example.app.MyPicker"
            android:targetActivity="com.android.example.app.MyActivity"
            ...
            android:enabled="@bool/atMostJellyBeanMR2">
        
             android:name="android.intent.action.GET_CONTENT" />
             android:name="android.intent.category.OPENABLE" />
             android:name="android.intent.category.DEFAULT" />
             android:mimeType="image/*" />
             android:mimeType="video/*" />
        
    

协定类

通常,当您编写自定义内容提供程序时,其中一项任务是实现协定类,如内容提供程序开发者指南中所述。 协定类是一种 public final 类,它包含对 URI、列名称、MIME 类型以及其他与提供程序有关的元数据的常量定义。 SAF 会为您提供这些协定类,因此您无需自行编写:

  • DocumentsContract.Document
  • DocumentsContract.Root

例如,当系统在您的文档提供程序中查询文档或根目录时,您可能会在游标中返回以下列:

private static final String[] DEFAULT_ROOT_PROJECTION =
        new String[]{Root.COLUMN_ROOT_ID, Root.COLUMN_MIME_TYPES,
        Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
        Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID,
        Root.COLUMN_AVAILABLE_BYTES,};
private static final String[] DEFAULT_DOCUMENT_PROJECTION = new
        String[]{Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE,
        Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED,
        Document.COLUMN_FLAGS, Document.COLUMN_SIZE,};

为 DocumentsProvider 创建子类

编写自定义文档提供程序的下一步是为抽象类 DocumentsProvider 创建子类。您至少需要实现以下方法:

  • queryRoots()
  • queryChildDocuments()
  • queryDocument()
  • openDocument()

这些只是您需要严格实现的方法,但您可能还想实现许多其他方法。 详情请参阅 DocumentsProvider

实现 queryRoots

您实现的 queryRoots() 必须使用在 DocumentsContract.Root 中定义的列返回一个指向文档提供程序所有根目录的 Cursor

在以下代码段中,projection 参数表示调用方想要返回的特定字段。 代码段会创建一个新游标,并为其添加一行 — 一个根目录,如 Downloads 或 Images 等顶层目录。 大多数提供程序只有一个根目录。有时您可能有多个根目录,例如,当您具有多个用户帐户时。 在这种情况下,只需再为游标添加一行。

@Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {

    // Create a cursor with either the requested fields, or the default
    // projection if "projection" is null.
    final MatrixCursor result =
            new MatrixCursor(resolveRootProjection(projection));

    // If user is not logged in, return an empty root cursor.  This removes our
    // provider from the list entirely.
    if (!isUserLoggedIn()) {
        return result;
    }

    // It's possible to have multiple roots (e.g. for multiple accounts in the
    // same app) -- just add multiple cursor rows.
    // Construct one row for a root called "MyCloud".
    final MatrixCursor.RowBuilder row = result.newRow();
    row.add(Root.COLUMN_ROOT_ID, ROOT);
    row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.root_summary));

    // FLAG_SUPPORTS_CREATE means at least one directory under the root supports
    // creating documents. FLAG_SUPPORTS_RECENTS means your application's most
    // recently used documents will show up in the "Recents" category.
    // FLAG_SUPPORTS_SEARCH allows users to search all documents the application
    // shares.
    row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE |
            Root.FLAG_SUPPORTS_RECENTS |
            Root.FLAG_SUPPORTS_SEARCH);

    // COLUMN_TITLE is the root title (e.g. Gallery, Drive).
    row.add(Root.COLUMN_TITLE, getContext().getString(R.string.title));

    // This document id cannot change once it's shared.
    row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(mBaseDir));

    // The child MIME types are used to filter the roots and only present to the
    //  user roots that contain the desired type somewhere in their file hierarchy.
    row.add(Root.COLUMN_MIME_TYPES, getChildMimeTypes(mBaseDir));
    row.add(Root.COLUMN_AVAILABLE_BYTES, mBaseDir.getFreeSpace());
    row.add(Root.COLUMN_ICON, R.drawable.ic_launcher);

    return result;
}

实现 queryChildDocuments

您实现的 queryChildDocuments() 必须使用在 DocumentsContract.Document 中定义的列返回一个指向指定目录中所有文件的 Cursor

当您在选取器 UI 中选择应用时,系统会调用此方法。它会获取根目录下某个目录内的子文档。可以在文件层次结构的任何级别调用此方法,并非只能从根目录调用。 以下代码段可创建一个包含所请求列的新游标,然后向游标添加父目录中每个直接子目录的相关信息。子目录可以是图像、另一个目录乃至任何文件:

@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection,
                              String sortOrder) throws FileNotFoundException {

    final MatrixCursor result = new
            MatrixCursor(resolveDocumentProjection(projection));
    final File parent = getFileForDocId(parentDocumentId);
    for (File file : parent.listFiles()) {
        // Adds the file's display name, MIME type, size, and so on.
        includeFile(result, null, file);
    }
    return result;
}

实现 queryDocument

您实现的 queryDocument() 必须使用在 DocumentsContract.Document 中定义的列返回一个指向指定文件的 Cursor

除了特定文件的信息外,queryDocument() 方法返回的信息与 queryChildDocuments() 中传递的信息相同:

@Override
public Cursor queryDocument(String documentId, String[] projection) throws
        FileNotFoundException {

    // Create a cursor with the requested projection, or the default projection.
    final MatrixCursor result = new
            MatrixCursor(resolveDocumentProjection(projection));
    includeFile(result, documentId, null);
    return result;
}

实现 queryDocument

您必须实现 openDocument() 以返回表示指定文件的 ParcelFileDescriptor。其他应用可以使用返回的 ParcelFileDescriptor 来流式传输数据。用户选择了文件,并且客户端应用通过调用 openFileDescriptor() 来请求对文件的访问权限后,系统便会调用此方法。例如:

@Override
public ParcelFileDescriptor openDocument(final String documentId,
                                         final String mode,
                                         CancellationSignal signal) throws
        FileNotFoundException {
    Log.v(TAG, "openDocument, mode: " + mode);
    // It's OK to do network operations in this method to download the document,
    // as long as you periodically check the CancellationSignal. If you have an
    // extremely large file to transfer from the network, a better solution may
    // be pipes or sockets (see ParcelFileDescriptor for helper methods).

    final File file = getFileForDocId(documentId);

    final boolean isWrite = (mode.indexOf('w') != -1);
    if(isWrite) {
        // Attach a close listener if the document is opened in write mode.
        try {
            Handler handler = new Handler(getContext().getMainLooper());
            return ParcelFileDescriptor.open(file, accessMode, handler,
                        new ParcelFileDescriptor.OnCloseListener() {
                @Override
                public void onClose(IOException e) {

                    // Update the file with the cloud server. The client is done
                    // writing.
                    Log.i(TAG, "A file with id " +
                    documentId + " has been closed!
                    Time to " +
                    "update the server.");
                }

            });
        } catch (IOException e) {
            throw new FileNotFoundException("Failed to open document with id "
            + documentId + " and mode " + mode);
        }
    } else {
        return ParcelFileDescriptor.open(file, accessMode);
    }
}

安全性

假设您的文档提供程序是受密码保护的云存储服务,并且您想在开始共享用户的文件之前确保其已登录。如果用户未登录,您的应用应该执行哪些操作呢? 解决方案是在您实现的 queryRoots() 中返回零个根目录。也就是空的根目录游标:

public Cursor queryRoots(String[] projection) throws FileNotFoundException {
...
    // If user is not logged in, return an empty root cursor.  This removes our
    // provider from the list entirely.
    if (!isUserLoggedIn()) {
        return result;
}

另一个步骤是调用 getContentResolver().notifyChange()。还记得 DocumentsContract 吗? 我们将使用它来创建此 URI。 以下代码段会在每次用户的登录状态发生变化时指示系统查询文档提供程序的根目录。 如果用户未登录,则调用 queryRoots() 会返回一个空游标,如上文所示。 这可以确保只有在用户登录提供程序后其中的文档才可用。

private void onLoginButtonClick() {
    loginOrLogout();
    getContentResolver().notifyChange(DocumentsContract
            .buildRootsUri(AUTHORITY), null);
}

联系人提供程序

内容快览

  • Android 的联系人相关信息存储区。
  • 与网页同步。
  • 集成社交流数据。

本文内容

  1. 联系人提供程序组织
  2. 原始联系人
  3. 数据
  4. 联系人
  5. 来自同步适配器的数据
  6. 所需权限
  7. 用户个人资料
  8. 联系人提供程序元数据
  9. 联系人提供程序访问
  10. 联系人提供程序同步适配器
  11. 社交流数据
  12. 其他联系人提供程序功能

关键类

  1. ContactsContract.Contacts
  2. ContactsContract.RawContacts
  3. ContactsContract.Data
  4. android.provider.ContactsContract.StreamItems

相关示例

  1. 联系人管理器
  2. 示例同步适配器

另请参阅

  1. 内容提供程序基础知识

联系人提供程序是一个强大而又灵活的 Android 组件,用于管理设备上联系人相关数据的中央存储区。 联系人提供程序是您在设备的联系人应用中看到的数据源,您也可以在自己的应用中访问其数据,并可在设备与在线服务之间传送数据。 提供程序储存有多种数据源,由于它会试图为每个联系人管理尽可能多的数据,因此造成其组织结构非常复杂。 为此,该提供程序的 API 包含丰富的协定类和接口,为数据检索和修改提供便利。

本指南介绍下列内容:

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

本指南假定您了解 Android 内容提供程序的基础知识。如需了解有关 Android 内容提供程序的更多信息,请阅读 内容提供程序基础知识指南。 示例同步适配器示例应用是一个示例,展示如何使用同步适配器在联系人提供程序与 Google 网络服务托管的一个示例应用之间传送数据。

联系人提供程序组织


联系人提供程序是 Android 内容提供程序的一个组件。它保留了三种类型的联系人数据,每一种数据都对应提供程序提供的一个表,如图 1 所示:

Android Developer - ContentProvider_第5张图片

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

这三个表通常以其协定类的名称命名。这些类定义表所使用的内容 URI、列名称及列值相应的常量:

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

由 ContactsContract 中的协定类表示的其他表是辅助表,联系人提供程序利用它们来管理其操作,或为设备的联系人或电话应用中的特定功能提供支持。

原始联系人


一个原始联系人表示来自某一帐户类型和帐户名称、有关某个联系人的数据。 由于联系人提供程序允许将多个在线服务作为某一联系人的数据源,因此它允许同一联系人对应多个原始联系人。 借助支持多个原始联系人的特性,用户还可以将某一联系人在帐户类型相同的多个帐户中的数据进行合并。

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

重要的原始联系人列

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

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

列名称 用途 说明
ACCOUNT_NAME 作为该原始联系人来源的帐户类型的帐户名称。 例如,Google 帐户的帐户名称是设备所有者的某个 Gmail 地址。如需了解详细信息,请参阅有关ACCOUNT_TYPE 的下一条目。 此名称的格式专用于其帐户类型。它不一定是电子邮件地址。
ACCOUNT_TYPE 作为该原始联系人来源的帐户类型。例如,Google 帐户的帐户类型是 com.google。 请务必使用您拥有或控制的域的域标识符限定您的帐户类型。 这可以确保您的帐户类型具有唯一性。 提供联系人数据的帐户类型通常关联有同步适配器,用于与联系人提供程序进行同步。
DELETED 原始联系人的“已删除”标志。 此标志让联系人提供程序能够在内部保留该行,直至同步适配器能够从服务器删除该行,然后再从存储区中最终删除该行。

说明

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

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

    例如,如果您想让您的应用为您域名为 com.example.dataservice、基于网络的服务保留联系人数据,并且您的服务的用户帐户是 [email protected],则用户必须先添加帐户“类型”(com.example.dataservice) 和帐户“名称”([email protected]),然后您的应用才能添加原始联系人行。 您可以在文档中向用户解释这项要求,也可以提示用户添加类型和名称,或者同时采用这两种措施。 下文对帐户类型和帐户名称做了更详尽的描述。

原始联系人数据来源

为理解原始联系人的工作方式,假设有一位用户“Emily Dickinson”,她的设备上定义了以下三个用户帐户:

该用户已在“Accounts”设置中为全部三个帐户启用了“Sync Contacts”。

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

以上操作的结果是,联系人提供程序会创建以下这三个原始联系人:

  1. 第一个原始联系人对应“Thomas Higginson”,关联帐户 [email protected]。 用户帐户类型是 Google。
  2. 第二个原始联系人对应“Thomas Higginson”,关联帐户 [email protected]。 用户帐户类型也是 Google。由于添加的联系人对应的用户帐户不同,因此尽管名称与前一名称完全相同,也只能作为第二个原始联系人。
  3. 第三个原始联系人对应“Thomas Higginson”,关联帐户“belle_of_amherst”。用户帐户类型是 Twitter。

数据


所前所述,原始联系人的数据存储在一个 ContactsContract.Data 行中,该行链接到原始联系人的 _ID 值。这使一位原始联系人可以拥有多个具有相同数据类型的实例,例如电子邮件地址或电话号码。 例如,如果对应 [email protected] 的“Thomas Higginson”(关联 Google 帐户 [email protected] 的 Thomas Higginson 的原始联系人行)的住宅电子邮件地址为 [email protected],办公电子邮件地址为 [email protected],则联系人提供程序会存储这两个电子邮件地址行,并将它们都链接到原始联系人。

请注意,这个表中存储了不同类型的数据。显示姓名、电话号码、电子邮件、邮政地址、照片以及网站明细行都可以在 ContactsContract.Data 表中找到。 为便于管理这些数据, ContactsContract.Data 表为一些列使用了描述性名称,为其他列使用了通用名称。 使用描述性名称的列的内容具有相同的含义,与行中数据的类型无关,而使用通用名称的列的内容则会随数据类型的不同而具有不同的含义。

描述性列名称

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

RAW_CONTACT_ID
该数据对应的原始联系人  _ID 列的值。
MIMETYPE
该行中存储的数据类型,以自定义 MIME(多用途互联网邮件扩展)类型表示。联系人提供程序使用了  ContactsContract.CommonDataKinds 子类中定义的 MIME 类型。 这些 MIME 类型为开源类型,可供与联系人提供程序协作的任何应用或同步适配器使用。
IS_PRIMARY
如果一个原始联系人可能具有多个这种类型的数据行,  IS_PRIMARY 列会标记 包含该类型主要数据的数据行。例如,如果用户长按某个联系人的电话号码,并选择  Set default,则包含该号码的  ContactsContract.Data 行会将其  IS_PRIMARY 列设置为一个非零值。

通用列名称

有 15 个通用列命名为 DATA1 至 DATA15,可普遍适用;还有四个通用列命名为 SYNC1 至 SYNC4,只应由同步适配器使用。 通用列名称常量始终有效,与行包含的数据类型无关。

DATA1 列为索引列。联系人提供程序总是在此列中存储其预期会成为最频繁查询目标的数据。 例如,在一个电子邮件行中,此列包含实际电子邮件地址。

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

类型专用列名称

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

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

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

图 2 显示的是描述性列和数据列在 ContactsContract.Data 行中的显示情况,以及类型专用列名称“覆盖”通用列名称的情况

Android Developer - ContentProvider_第6张图片

图 2. 类型专用列名称和通用列名称。

类型专用列名称类

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

表 2. 类型专用列名称类

映射类 数据类型 说明
ContactsContract.CommonDataKinds.StructuredName 与该数据行关联的原始联系人的姓名数据。 一位原始联系人只有其中一行。
ContactsContract.CommonDataKinds.Photo 与该数据行关联的原始联系人的主要照片。 一位原始联系人只有其中一行。
ContactsContract.CommonDataKinds.Email 与该数据行关联的原始联系人的电子邮件地址。 一位原始联系人可有多个电子邮件地址。
ContactsContract.CommonDataKinds.StructuredPostal 与该数据行关联的原始联系人的邮政地址。 一位原始联系人可有多个邮政地址。
ContactsContract.CommonDataKinds.GroupMembership 将原始联系人链接到联系人提供程序内其中一组的标识符。 组是帐户类型和帐户名称的一项可选功能。 联系人组部分对其做了更详尽的描述。

联系人

联系人提供程序通过将所有帐户类型和帐户名称的原始联系人行合并来形成联系人。 这可以为显示和修改用户针对某一联系人收集的所有数据提供便利。 联系人提供程序管理新联系人行的创建,以及原始联系人与现有联系人行的合并。 系统不允许应用或同步适配器添加联系人,并且联系人行中的某些列是只读列。

注:如果您试图通过 insert() 向联系人提供程序添加联系人,会引发一个 UnsupportedOperationException 异常。 如果您试图更新一个列为“只读”的列,更新会被忽略。

如果添加的新原始联系人不匹配任何现有联系人,联系人提供程序会相应地创建新联系人。 如果某个现有原始联系人的数据发生了变化,不再匹配其之前关联的联系人,则提供程序也会执行此操作。 如果应用或同步适配器创建的新原始联系人的确匹配某位现有联系人,则新原始联系人将与现有联系人合并。

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

ContactsContract.Contacts 表还有一个 LOOKUP_KEY 列,它是一个指向联系人行的“永久性”链接。 由于联系人提供程序会自动维护联系人,因此可能会在合并或同步时相应地更改联系人行的 _ID 值。 即使发生这种情况,合并了联系人 LOOKUP_KEY 的内容 URI CONTENT_LOOKUP_URI 仍将指向联系人行,这样,您就能使用 LOOKUP_KEY 保持指向“最喜爱”联系人的链接,以及执行其他操作。 该列具有其自己的格式,与 _ID 列的格式无关。

图 3 显示的是这三个主要表的相互关系。

Android Developer - ContentProvider_第7张图片

图 3. 联系人表、原始联系人表与详细信息表之间的关系。

来自同步适配器的数据


虽然用户是直接将联系人数据输入到设备中,但这些数据也会通过同步适配器从网络服务流入联系人提供程序中,这些同步适配器可自动化设备与服务之间的数据传送。 同步适配器在系统控制下在后台运行,它们会调用 ContentResolver 方法来管理数据。

在 Android 中,与同步适配器协作的网络服务通过帐户类型加以标识。 每个同步适配器都与一个帐户类型协作,但它可以支持该类型的多个帐户名称。原始联系人数据来源部分对帐户类型和帐户名称做了简要描述。 下列定义提供了更多详细信息,并描述了帐户类型及帐户名称与同步适配器及服务之间的关系。

帐户类型
表示用户在其中存储数据的服务。在大多数时候,用户需要向服务验证身份。 例如,Google 通讯录是一个以代码  google.com 标识的帐户类型。 该值对应于  AccountManager 使用的帐户类型。
帐户名称
表示某个帐户类型的特定帐户或登录名。Google 通讯录帐户与 Google 帐户相同,都是以电子邮件地址作为帐户名称。 其他服务可能使用一个单词的用户名或数字 ID。

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

如果您想将服务的数据传送到联系人提供程序,则需编写您自己的同步适配器。 联系人提供程序同步适配器部分对此做了更详尽的描述。

图 4 显示的是联系人提供程序如何融入联系人数据的流动。 在名为“同步适配器”的方框中,每个适配器都以其帐户类型命名。

Android Developer - ContentProvider_第8张图片

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

所需权限


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

对一个或多个表的读取权限
READ_CONTACTS,在  AndroidManifest.xml 中指定,使用   元素作为 
对一个或多个表的写入权限
WRITE_CONTACTS,在  AndroidManifest.xml 中指定,使用   元素作为 

这些权限不适用于用户个人资料数据。下面的用户个人资料部分对用户个人资料及其所需权限做了阐述。

请切记,用户的联系人数据属于个人敏感数据。用户关心其隐私权,因此不希望应用收集有关其自身的数据或其联系人的数据。 如需权限来访问其联系人数据的理由并不充分,用户可能给您的应用作出差评或干脆拒绝安装。

用户个人资料


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。 例如,以下代码段用于检索个人资料数据:

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

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

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

联系人提供程序元数据


联系人提供程序管理用于追踪存储区中联系人数据状态的数据。 这些有关存储区的元数据存储在各处,其中包括原始联系人表行、数据表行和联系人表行、 ContactsContract.Settings 表以及 ContactsContract.SyncState 表。 下表显示的是每一部分元数据的作用:

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

含义
ContactsContract.RawContacts DIRTY “0”:上次同步以来未发生变化。 标记设备上因发生变化而需要同步回服务器的原始联系人。 当 Android 应用更新行时,联系人提供程序会自动设置该值。

修改原始联系人表或数据表的同步适配器应始终向他们使用的内容 URI 追加字符串 CALLER_IS_SYNCADAPTER。 这可以防止提供程序将行标记为已更新。 否则,即使服务器是修改的来源,同步适配器修改仍显示为本地修改,并会发送到服务器。

“1”:上次同步以来发生了变化,需要同步回服务器。
ContactsContract.RawContacts VERSION 此行的版本号。 每当行或其相关数据发生变化时,联系人提供程序都会自动增加此值。
ContactsContract.Data DATA_VERSION 此行的版本号。 每当数据行发生变化时,联系人提供程序都会自动增加此值。
ContactsContract.RawContacts SOURCE_ID 一个字符串值,用于在创建此原始联系人的帐户中对该联系人进行唯一标识。 当同步适配器创建新原始联系人时,此列应设置为该原始联系人在服务器中的唯一 ID。 当 Android 应用创建新原始联系人时,应将此列留空。 这是为了向同步适配器表明,它应该在服务器上创建新原始联系人,并获取 SOURCE_ID 的值。

具体地讲,对于每个帐户类型,该源 ID 都必须是唯一的,并且应在所有同步中保持稳定:

  • 唯一:帐户的每个原始联系人都必须有自己的源 ID。如果您不强制执行此要求,会在联系人应用中引发问题。 请注意,帐户类型相同的两个原始联系人可以具有相同的源 ID。 例如,允许帐户 [email protected] 的原始联系人“Thomas Higginson”与帐户 [email protected] 的原始联系人“Thomas Higginson”具有相同的源 ID。
  • 稳定:源 ID 是该原始联系人在在线服务中的数据的永久性组成部分。 例如,如果用户从应用设置中清除存储的联系人数据并重新同步,则恢复的原始联系人的源 ID 应与以前相同。 如果您不强制执行此要求,快捷方式将停止工作。
ContactsContract.Groups GROUP_VISIBLE “0”:此组中的联系人在 Android 应用 UI 中不应处于可见状态。 此列用于兼容那些允许用户隐藏特定组中联系人的服务器。
“1”:系统允许此组中的联系人在应用 UI 中处于可见状态。
ContactsContract.Settings UNGROUPED_VISIBLE “0”:对于此帐户和帐户类型,未归入组的联系人在 Android 应用 UI 中处于不可见状态。 默认情况下,如果联系人的所有原始联系人都未归入组,则它们将处于不可见状态(原始联系人的组成员身份通过 ContactsContract.Data表中的一个或多个ContactsContract.CommonDataKinds.GroupMembership 行指示)。 通过在 ContactsContract.Settings 表行中为帐户类型和帐户设置此标志,您可以强制未归入组的联系人处于可见状态。 此标志的一个用途是显示不使用组的服务器上的联系人。
“1”:对于此帐户和帐户类型,未归入组的联系人在应用 UI 中处于可见状态。
ContactsContract.SyncState (all) 此表用于存储同步适配器的元数据。 利用此表,您可以将同步状态及其他同步相关数据持久地存储在设备中。

联系人提供程序访问


本节描述访问联系人提供程序中数据的准则,侧重于阐述以下内容:

  • 实体查询。
  • 批量修改。
  • 通过 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。 主 Activity 显示一个联系人行列表;当用户选择一行时,该 Activity 会将其 ID 发送至详 Activity。 详 Activity 使用 ContactsContract.Contacts.Entity 显示与所选联系人关联的所有原始联系人中的所有数据行。

以下代码段摘自“详”Activity:

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

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

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

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

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

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

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

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

批量修改

您应尽可能地通过创建一个 ContentProviderOperation 对象 ArrayList 并调用 applyBatch(),以“批处理模式”在联系人提供程序中插入、更新和删除数据。 由于联系人提供程序是在 applyBatch() 中通过单一事务执行所有操作,因此您的修改绝不会使联系人存储区出现不一致问题。 此外,批量修改还有便于同时插入原始联系人及其明细数据。

注:要修改单个原始联系人,可以考虑向设备的联系人应用发送一个 Intent,而不是在您的应用中处理修改。 通过 Intent 执行检索和修改部分对此操作做了更详尽的描述。

屈服点

一个包含大量操作的批量修改可能会阻断其他进程,导致糟糕的总体用户体验。 要将您想执行的所有修改组织到尽可能少的单独列表中,同时防止它们阻断系统,则应为一项或多项操作设置屈服点。 屈服点是一个 ContentProviderOperation 对象,其 isYieldAllowed() 值设置为 true。当联系人提供程序遇到屈服点时,它会暂停其工作,让其他进程运行,并关闭当前事务。 当提供程序再次启动时,它会继续执行 ArrayList 中的下一项操作,并启动一个新的事务。

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

屈服点也是一个原子操作单元。两个屈服点之间所有访问的成功或失败都将以一个单元的形式出现。 如果您不设置任何屈服点,则最小的原子操作是整个批量操作。 如果您使用了屈服点,则可以防止操作降低系统性能,还可确保一部分操作是原子操作。

修改向后引用

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

withValueBackReference() 方法具有两个参数:

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

系统会在您首次调用 applyBatch() 时创建整个结果数组,其大小与您提供的 ContentProviderOperation 对象的 ArrayList 大小相等。 不过,结果数组中的所有元素都设置为 null,如果您试图向后引用某个尚未应用的操作的结果, withValueBackReference() 会引发一个 Exception

以下代码段说明如何批量插入新原始联系人和数据。代码段中包括用于建立屈服点和使用向后引用的代码。 这些代码段是扩展版本的createContacEntry() 方法,该方法是 Contact Manager 示例应用中 ContactAdder 类的组成部分。

第一个代码段用于检索 UI 中的联系人数据。此时,用户已经选择了应添加新原始联系人的帐户。

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

    int phoneType = mContactPhoneTypes.get(
            mContactPhoneTypeSpinner.getSelectedItemPosition());

    int emailType = mContactEmailTypes.get(
            mContactEmailTypeSpinner.getSelectedItemPosition());

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

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

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

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

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

接着,代码会创建显示姓名行、电话行和电子邮件行的数据行。

每个操作生成器对象都使用 withValueBackReference() 来获取 RAW_CONTACT_ID。引用指回来自第一次操作的 ContentProviderResult 对象,第一次操作就是添加原始联系人行并返回其新 _ID 值。 结果是,每个数据行都通过其 RAW_CONTACT_ID 自动链接到其所属的 ContactsContract.RawContacts 行。

添加电子邮件行的 ContentProviderOperation.Builder 对象带有 withYieldAllowed() 标志,用于设置屈服点:

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

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

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

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

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

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

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

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

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

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

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

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

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

最后一个代码段显示的是 applyBatch() 调用,用于插入新原始联系人行和数据行。

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

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

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

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

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

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

此外,您还可以利用批处理操作实现乐观并发控制,这是一种无需锁定底层存储区便可应用修改事务的控制方法。 要使用此方法,您需要应用事务,然后检查是否存在可能已同时做出的其他修改。 如果您发现了不一致的修改,请回滚事务并重试。

乐观并发控制对于移动设备很有用,因为在移动设备上,同一时间只有一位用户,并且同时访问数据存储区的情况很少见。 由于未使用锁定功能,因此不用浪费时间设置锁定或等待其他事务解除锁定。

要在更新某个 ContactsContract.RawContacts 行时使用乐观并发控制,请按以下步骤操作:

  1. 检索原始联系人的 VERSION 列以及要检索的其他数据。
  2. 创建一个适合使用 newAssertQuery(Uri) 方法强制执行约束 的 ContentProviderOperation.Builder 对象。对于内容 URI,请使用追加有原始联系人 _ID 的 RawContacts.CONTENT_URI 。
  3. 对于 ContentProviderOperation.Builder 对象,请调用 withValue(),对 VERSION 列与您刚检索的版本号进行比较。
  4. 对于同一 ContentProviderOperation.Builder,请调用 withExpectedCount(),确保此断言只对一行进行测试。
  5. 调用 build() 创建 ContentProviderOperation 对象,然后将此对象添加为要传递至 applyBatch() 的 ArrayList 中的第一个对象。
  6. 应用批处理事务。

如果在您读取原始联系人行到您试图对其进行修改这段时间有另一项操作更新了该行,“断言”ContentProviderOperation 将会失败,系统将终止整个批处理操作。 此情况下,您可以选择重新执行批处理操作,或执行其他某操作。

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

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

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

...

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

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

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

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

ops.add(assertOp.build());

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

...

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

    } catch (OperationApplicationException e) {

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

通过 Intent 执行检索和修改

通过向设备的联系人应用发送 Intent,您可以间接访问联系人提供程序。 Intent 会启动设备的联系人应用 UI,用户可以在其中执行与联系人有关的操作。 通过这种访问方式,用户可以:

  • 从列表中选取一位联系人并将其返回给您的应用以执行进一步操作。
  • 编辑现有联系人的数据。
  • 为其任一帐户插入新原始联系人。
  • 删除联系人或联系人数据。

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

当您使用 Intent 通过设备的联系人应用访问联系人提供程序时,您无需自行编写用于访问该提供程序的 UI 或代码。 您也无需请求对提供程序的读取或写入权限。 设备的联系人应用可以将联系人读取权限授予给您,而且您是通过另一个应用对该提供程序进行修改,不需要拥有写入权限。

内容提供程序基础知识指南的“通过 Intent 访问数据”部分详细描述了通过发送 Intent 来访问某提供程序的一般过程。 表 4 汇总了您为可用任务使用的操作、MIME 类型以及数据值,ContactsContract.Intents.Insert 参考文档列出了您可用于putExtra() 的 Extra 值:

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

任务 操作 数据 MIME 类型 说明
从列表中选取一位联系人 ACTION_PICK 下列值之一:
  • Contacts.CONTENT_URI,显示联系人列表。
  • Phone.CONTENT_URI,显示原始联系人的电话号码列表。
  • StructuredPostal.CONTENT_URI,显示原始联系人的邮政地址列表。
  • Email.CONTENT_URI,显示原始联系人的电子邮件地址列表。
未使用 显示原始联系人列表或一位原始联系人的数据列表,具体取决于您提供的内容 URI 类型。

调用startActivityForResult()方法,该方法返回所选行的内容 URI。 该 URI 的形式为:追加有该行 LOOKUP_ID的表的内容 URI。 设备的联系人应用会在 Activity 的生命周期内将读取和写入权限授予给此内容 URI。 如需了解更多详细信息,请参阅内容提供程序基础知识指南。

插入新原始联系人 Insert.ACTION 不适用 RawContacts.CONTENT_TYPE,用于一组原始联系人的 MIME 类型。 显示设备“通讯录”应用的 Add Contact 屏幕。系统会显示您添加到 Intent 中的 Extra 值。 如果是随startActivityForResult()发送,系统会将新添加的原始联系人的内容 URI 传回给 Activity 的onActivityResult() 回调方法并作为后者 Intent 参数的“data”字段。 要获取该值,请调用 getData()
编辑联系人 ACTION_EDIT 该联系人的 CONTENT_LOOKUP_URI。 该编辑器 Activity 让用户能够对任何与该联系人关联的数据进行编辑。 Contacts.CONTENT_ITEM_TYPE,一位联系人。 显示“通讯录”应用中的“Edit Contact”屏幕。系统会显示您添加到 Intent 中的 Extra 值。 当用户点击 Done 保存编辑时,您的 Activity 会返回前台。
显示一个同样可以添加数据的选取器。 ACTION_INSERT_OR_EDIT 不适用 CONTENT_ITEM_TYPE 此 Intent 始终显示“通讯录”应用的选取器屏幕。用户可以选取要编辑的联系人,或添加新联系人。 根据用户的选择,系统会显示编辑屏幕或添加屏幕,还会显示您使用 Intent 传递的 Extra 数据。 如果您的应用显示电子邮件或电话号码等联系人数据,请使用此 Intent 来允许用户向现有联系人添加数据。

注:不需要通过此 Intent 的 Extra 发送姓名值,因为用户总是会选取现有姓名或添加新姓名。 此外,如果您发送姓名,并且用户选择执行编辑操作,则联系人应用将显示您发送的姓名,该姓名将覆盖以前的值。 如果用户未注意这一情况便保存了编辑,原有值将会丢失。

设备的联系人应用不允许您使用 Intent 删除原始联系人或其任何数据。 因此,要删除原始联系人,请使用 ContentResolver.delete() 或 ContentProviderOperation.newDelete()

以下代码段说明如何构建和发送一个插入新原始联系人和数据的 Intent:

// Gets values from the UI
String name = mContactNameEditText.getText().toString();
String phone = mContactPhoneEditText.getText().toString();
String email = mContactEmailEditText.getText().toString();

String company = mCompanyName.getText().toString();
String jobtitle = mJobTitle.getText().toString();

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

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

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

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

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

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


/*
 * Defines the raw contact row
 */

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

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

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

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

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

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

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

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

/*
 * Sets up the email data row
 */

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

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

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

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

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

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

数据完整性

联系人存储区包含用户认为是正确且是最新的重要敏感数据,因此联系人提供程序具有规定清晰的数据完整性规则。 您有责任在修改联系人数据时遵守这些规则。 以下列出了其中的重要规则:

务必为您添加的每个  ContactsContract.RawContacts 行添加一个  ContactsContract.CommonDataKinds.StructuredName 行。
如果  ContactsContract.Data 表中的  ContactsContract.RawContacts 行没有  ContactsContract.CommonDataKinds.StructuredName 行,可能会在聚合时引发问题。
务必将新  ContactsContract.Data 行链接到其父  ContactsContract.RawContacts 行。
如果  ContactsContract.Data 行未链接到  ContactsContract.RawContacts,则其在设备的联系人应用中将处于不可见状态,而且这可能会导致同步适配器出现问题。
请仅更改您拥有的那些原始联系人的数据。
请切记,联系人提供程序所管理的数据通常来自多个不同帐户类型/在线服务。 您需要确保您的应用仅修改或删除归您所有的行的数据,并且仅通过您控制的帐户类型和帐户名称插入数据。
务必使用在  ContactsContract 及其子类中为权限、内容 URI、URI 路径、列名称、MIME 类型以及  TYPE 值定义的常量。
使用这些常量有助于您避免错误。如有任何常量被弃用,您还会从编译器警告收到通知。

自定义数据行

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

要显示您的自定义数据,请提供一个 contacts.xml 文件,其中须包含一个  元素,及其一个或多个  子元素。 element 部分对此做了更详尽的描述。

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

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


联系人提供程序专门设计用于处理设备与在线服务之间的联系人数据同步。 借助同步功能,用户可以将现有数据下载到新设备,以及将现有数据上传到新帐户。 此外,同步还能确保用户掌握最新数据,无需考虑数据增加和更改的来源。 同步的另一个优点是,即使设备未连接网络,联系人数据同样可用。

虽然您可以通过各种方式实现同步,不过 Android 系统提供了一个插件同步框架,可自动化完成下列任务:

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

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

同步适配器类和文件

您需要将同步适配器作为 AbstractThreadedSyncAdapter 的子类进行实现,并作为 Android 应用的一部分进行安装。系统通过您的应用清单文件中的元素以及由清单文件指向的一个特殊 XML 文件了解有关同步适配器的信息。 该 XML 文件定义在线服务的帐户类型和内容提供程序的权限,它们共同对适配器进行唯一标识。 用户为同步适配器的帐户类型添加一个帐户,并为与同步适配器同步的内容提供程序启用同步后,同步适配器才会激活。 激活后,系统将开始管理适配器,并在必要时调用它,以在内容提供程序与服务器之间同步数据。

注:将帐户类型用作同步适配器标识的一部分让系统可以发现从同一组织访问不同服务的同步适配器,并将它们组合在一起。 例如,Google 在线服务的同步适配器都具有相同的帐户类型 com.google。 当用户向其设备添加 Google 帐户时,已安装的所有 Google 服务同步适配器将一起列出;列出的每个同步适配器都与设备上不同的内容提供程序同步。

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

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

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

尽管身份验证并非必需,但大多数联系人服务都会使用它。 不过,您不一定要使用 Android 身份验证框架进行身份验证。

同步适配器实现

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

一个  Service 组件,用于响应系统发出的绑定到同步适配器的请求。
当系统想要运行同步时,它会调用服务的  onBind() 方法,为同步适配器获取一个  IBinder。这样,系统便可跨进程调用适配器的方法。

在示例同步适配器示例应用中,该服务的类名是 com.example.android.samplesync.syncadapter.SyncService

作为  AbstractThreadedSyncAdapter 具体子类实现的实际同步适配器。
此类的作用是从服务器下载数据、从设备上传数据以及解决冲突。 适配器的主要工作是在方法  onPerformSync() 中完成的。 必须将此类实例化为单一实例。

在示例同步适配器示例应用中,同步适配器是在 com.example.android.samplesync.syncadapter.SyncAdapter 类中定义的。

Application 的子类。
此类充当同步适配器单一实例的工厂。使用  onCreate() 方法实例化同步适配器,并提供一个静态“getter”方法,使单一实例返回同步适配器服务的 onBind() 方法。
可选:一个  Service 组件,用于响应系统发出的用户身份验证请求。
AccountManager 会启动此服务以开始身份验证流程。 该服务的  onCreate() 方法会将一个身份验证器对象实例化。 当系统想要对应用同步适配器的用户帐户进行身份验证时,它会调用该服务的  onBind() 方法,为该身份验证器获取一个  IBinder。 这样,系统便可跨进程调用身份验证器的方法。

在示例同步适配器示例应用中,该服务的类名是 com.example.android.samplesync.authenticator.AuthenticationService

可选:一个用于处理身份验证请求的  AbstractAccountAuthenticator 具体子类。
AccountManager 就是调用此类所提供的方法向服务器验证用户的凭据。 详细的身份验证过程会因服务器所采用技术的不同而有很大差异。 您应该参阅服务器软件的文档,了解有关身份验证的更多信息。

在示例同步适配器示例应用中,身份验证器是在 com.example.android.samplesync.authenticator.Authenticator 类中定义的。

用于定义系统同步适配器和身份验证器的 XML 文件。
之前描述的同步适配器和身份验证器服务组件都是在应用清单文件中的   元素内定义的。 这些元素包含以下用于向系统提供特定数据的  子元素:
  • 同步适配器服务的  元素指向 XML 文件 res/xml/syncadapter.xml。而该文件则指定将与联系人提供程序同步的网络服务的 URI,以及指定该 Web 服务的帐户类型。
  • 可选:身份验证器的  元素指向 XML 文件 res/xml/authenticator.xml。而该文件则指定此身份验证器所支持的帐户类型,以及指定身份验证过程中出现的 UI 资源。 在此元素中指定的帐户类型必须与为同步适配器指定的帐户类型相同。

社交流数据


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 列(一个二进制大型对象)。
照片的二进制表示,为便于存储和显示,由提供程序调整了尺寸。 此列可用于向后兼容使用它来存储照片的旧版本联系人提供程序。 不过,在当前版本中,您不应使用此列来存储照片, 而应使用 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 的一个子表,其中包含某个流项目的照片行。

社交流交互

通过将联系人提供程序管理的社交流数据与设备的联系人应用相结合,可以在您的社交网络系统与现有联系人之间建立起有效的连接。 这种结合实现了下列功能:

  • 您可以通过同步适配器让您的社交网络服务与联系人提供程序同步,检索用户联系人的近期 Activity,并将其存储在 android.provider.ContactsContract.StreamItems 表和 android.provider.ContactsContract.StreamItemPhotos 表中,以供日后使用。
  • 除了定期同步外,您还可以在用户选择某位联系人进行查看时触发您的同步适配器以检索更多数据。 这样,您的同步适配器便可检索该联系人的高分辨率照片和最近流项目。
  • 通过在设备的联系人应用以及联系人提供程序中注册通知功能,您可以在用户查看联系人时收到一个 Intent,并在那时通过您的服务更新联系人的状态。 与通过同步适配器执行完全同步相比,此方法可能更快速,占用的带宽也更少。
  • 用户可以在查看设备联系人应用中的联系人时,将其添加到您的社交网络服务。 您可以通过“邀请联系人”功能实现此目的,而该功能则是通过将 Activity 与 XML 文件结合使用来实现的,前者将现有联系人添加到您的社交网络,后者为设备的联系人应用以及联系人提供程序提供有关您的应用的详细信息。

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

通过注册处理社交网络查看

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

  1. 在您项目的 res/xml/ 目录中创建一个名为 contacts.xml 的文件。 如果您已有该文件,可跳过此步骤。
  2. 在该文件中添加元素 。 如果该元素已存在,可跳过此步骤。
  3. 要注册一项服务,以便在用户于设备的联系人应用中打开某位联系人的详细信息页面时通知该服务,请为该元素添加viewContactNotifyService="serviceclass" 属性,其中 serviceclass 是该服务的完全限定类名,应由该服务接收来自设备联系人应用的 Intent。 对于这个通知程序服务,请使用一个扩展 IntentService 的类,以让该服务能够接收 Intent。 传入 Intent 中的数据包含用户点击的原始联系人的内容 URI。 您可以通过通知程序服务绑定到您的同步适配器,然后调用同步适配器来更新原始联系人的数据。

要注册需要在用户点击流项目或照片(或同时点击这两者)时调用的 Activity,请执行以下步骤:

  1. 在您项目的 res/xml/ 目录中创建一个名为 contacts.xml 的文件。 如果您已有该文件,可跳过此步骤。
  2. 在该文件中添加元素 。 如果该元素已存在,可跳过此步骤。
  3. 要注册某个 Activity,以处理用户在设备联系人应用中点击某个流项目的操作,请为该元素添加 viewStreamItemActivity="activityclass" 属性,其中 activityclass 是该 Activity 的完全限定类名,应由该 Activity 接收来自设备联系人应用的 Intent。
  4. 要注册某个 Activity,以处理用户在设备联系人应用中点击某个流照片的操作,请为该元素添加 viewStreamItemPhotoActivity="activityclass" 属性,其中 activityclass 是该 Activity 的完全限定类名,应由该 Activity 接收来自设备联系人应用的 Intent。

元素部分对  元素做了更详尽的描述。

传入 Intent 包含用户点击的项目或照片的内容 URI。 要让文本项目和照片具有独立的 Activity,请在同一文件中使用这两个属性。

与您的社交网络服务交互

用户不必为了邀请联系人到您的社交网络网站而离开设备的联系人应用。 取而代之是,您可以让设备的联系人应用发送一个 Intent,将联系人 邀请到您的 Activity 之一。要设置此功能,请执行以下步骤:

  1. 在您项目的 res/xml/ 目录中创建一个名为 contacts.xml 的文件。 如果您已有该文件,可跳过此步骤。
  2. 在该文件中添加元素 。 如果该元素已存在,可跳过此步骤。
  3. 添加以下属性:
    • inviteContactActivity="activityclass"
    • inviteContactActionLabel="@string/invite_action_label"
    activityclass 值是应该接收该 Intent 的 Activity 的完全限定类名。 invite_action_label 值是一个文本字符串,将显示在设备联系人应用的 Add Connection 菜单中。

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

contacts.xml 引用

文件 contacts.xml 包含一些 XML 元素,这些元素控制您的同步适配器和应用与联系人应用及联系人提供程序的交互。 下文对这些元素做了描述。

元素

 元素控制您的应用与联系人应用的交互。 它采用了以下语法:


        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

可包含:

说明:

声明 Android 组件和 UI 标签,让用户能够邀请他们的一位联系人加入社交网络,在他们的某个社交网络流更新时通知用户,以及执行其他操作。

请注意,对  的属性而言,属性前缀 android: 并非必需的。

属性:

inviteContactActivity
您的应用中某个 Activity 的完全限定类名,您想要在用户于设备的联系人应用中选择  Add connection 时激活该 Activity。
inviteContactActionLabel
Add connection 菜单中为  inviteContactActivity 中指定的 Activity 显示的文本字符串。 例如,您可以使用字符串“Follow in my network”。您可以为此标签使用字符串资源标识符。
viewContactNotifyService
您的应用中某项服务的完全限定类名,当用户查看联系人时,应由该服务接收通知。 此通知由设备的联系人应用发送;您的应用可以根据通知将数据密集型操作推迟到必要时再执行。 例如,您的应用对此通知的响应可以是:读入并显示联系人的高分辨率照片和最近的社交流项目。  社交流交互部分对此功能做了更详尽的描述。 您可以在  SampleSyncAdapter 示例应用的  NotifierService.java 文件中查看通知服务的示例。
viewGroupActivity
您的应用中某个可显示组信息的 Activity 的完全限定类名。 当用户点击设备联系人应用中的组标签时,将显示此 Activity 的 UI。
viewGroupActionLabel
联系人应用为某个 UI 控件显示的标签,用户可通过该控件查看您的应用中的组。

例如,如果您在设备上安装了 Google+ 应用,并将 Google+ 与联系人应用同步,就会看到 Google+ 圈子以组的形式出现在您的联系人应用的 Groups 选项卡内。 如果您点击某个 Google+ 圈子,就会看到该圈子内的联系人以“组”的形式列出。 在该显示页面的顶部,您会看到一个 Google+ 图标;如果您点击它,控制权将切换给 Google+ 应用。 “通讯录”应用以 Google+ 图标作为 viewGroupActionLabel 的值,通过 viewGroupActivity来实现此目的。

允许使用字符串资源标识符作为该属性的值。

viewStreamItemActivity
您的应用中某个 Activity 的完全限定类名,设备的联系人应用会在用户点击原始联系人的流项目时启动该 Activity。
viewStreamItemPhotoActivity
您的应用中某个 Activity 的完全限定类名,设备的联系人应用会在用户点击原始联系人流项目中的照片时启动该 Activity。

元素

 元素控制您的应用的自定义数据行在联系人应用 UI 中的显示。 它采用了以下语法:


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

包含它的文件:

说明:

此元素用于让联系人应用将自定义数据行的内容显示为原始联系人详细信息的一部分。  的每个  子元素都代表您的同步适配器向 ContactsContract.Data 表添加的某个自定义数据行类型。 请为您使用的每个自定义 MIME 类型添加一个  元素。 如果您不想显示任何自定义数据行的数据,则无需添加该元素。

属性:

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.CommonDataKinds.GroupMembership MIME 类型存储在 ContactsContract.Data 表内。

如果您设计的同步适配器会将服务器中的原始联系人数据添加到联系人提供程序,并且您不使用组,则需要指示提供程序让您的数据可见。 在用户向设备添加帐户时执行的代码中,更新联系人提供程序为该帐户添加的 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 表中,社交流照片部分对该表做了更详尽的描述。


你可能感兴趣的:(Android,Developer)