部分同学可能无法访问https://developer.android.com,将里面的文档贴出来供更多人学习,原链接https://developer.android.com/guide/topics/providers/content-providers.html
内容提供程序管理对结构化数据集的访问。它们封装数据,并提供用于定义数据安全性的机制。 内容提供程序是连接一个进程中的数据与另一个进程中运行的代码的标准界面。
如果您想要访问内容提供程序中的数据,可以将应用的 Context
中的 ContentResolver
对象用作客户端来与提供程序通信。 ContentResolver
对象会与提供程序对象(即实现 ContentProvider
的类实例)通信。 提供程序对象从客户端接收数据请求,执行请求的操作并返回结果。
如果您不打算与其他应用共享数据,则无需开发自己的提供程序。 不过,您需要通过自己的提供程序在您自己的应用中提供自定义搜索建议。 如果您想将复杂的数据或文件从您的应用复制并粘贴到其他应用中,也需要创建您自己的提供程序。
Android 本身包括的内容提供程序可管理音频、视频、图像和个人联系信息等数据。 android.provider
软件包参考文档中列出了部分提供程序。 任何 Android 应用都可以访问这些提供程序,但会受到某些限制。
以下主题对内容提供程序做了更详尽的描述:
ContentProvider
ContentResolver
Cursor
Uri
内容提供程序管理对中央数据存储区的访问。提供程序是 Android 应用的一部分,通常提供自己的 UI 来使用数据。 但是,内容提供程序主要旨在供其他应用使用,这些应用使用提供程序客户端对象来访问提供程序。 提供程序与提供程序客户端共同提供一致的标准数据界面,该界面还可处理跨进程通信并保护数据访问的安全性。
本主题介绍了以下基础知识:
内容提供程序以一个或多个表(与在关系型数据库中找到的表类似)的形式将数据呈现给外部应用。 行表示提供程序收集的某种数据类型的实例,行中的每个列表示为实例收集的每条数据。
例如,Android 平台的内置提供程序之一是用户字典,它会存储用户想要保存的非标准字词的拼写。 表 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 语句:
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 将是其参数之一。
在前面的代码行中,常量 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
类,加载器指南中对此有更为详细的介绍。 此外,前述代码行只是片段;它们不会显示整个应用。
要从提供程序中检索数据,请按照以下基本步骤执行操作:
要从提供程序检索数据,您的应需要具备对提供程序的“读取访问”权限。 您无法在运行时请求此权限;相反,您需要使用
元素和提供程序定义的准确权限名称,在清单文件中指明您需要此权限。 在您的清单文件中指定此元素后,您将有效地为应用“请求”此权限。 用户安装您的应用时,会隐式授予允许此请求。
要找出您正在使用的提供程序的读取访问权限的准确名称,以及提供程序使用的其他访问权限的名称,请查看提供程序的文档。
内容提供程序权限部分详细介绍了权限在访问提供程序过程中的作用。
用户字典提供程序在其清单文件中定义了权限 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 进行的批量访问和修改。
批量访问提供程序适用于插入大量行,或通过同一方法调用在多个表中插入行,或者通常用于跨进程界限将一组操作作为事务处理(原子操作)执行。
要在“批量模式”下访问提供程序, 您可以创建 ContentProviderOperation
对象数组,然后使用 ContentResolver.applyBatch()
将其分派给内容提供程序。您需将内容提供程序的授权传递给此方法,而不是特定内容 URI。 这样可使数组中的每个 ContentProviderOperation
对象都能适用于其他表。 调用 ContentResolver.applyBatch()
会返回结果数组。
ContactsContract.RawContacts
协定类 的说明包括展示批量注入的代码段。 联系人管理器示例应用包含在其 ContactAdder.java
源文件中进行批量访问的示例。
如果您的应用具有访问权限,您可能仍想使用 Intent 对象在其他应用中显示数据。 例如,日历应用接受ACTION_VIEW
Intent 对象,用于显示特定的日期或事件。 这样,您可以在不创建自己的 UI 的情况下显示日历信息。 如需了解有关此功能的更多信息,请参见日历提供程序指南。
您向其发送 Intent 对象的应用不必是与提供程序关联的应用。 例如,您可以从联系人提供程序中检索联系人,然后将包含联系人图像的内容 URI 的 ACTION_VIEW
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
,让您能够访问用户的所有联系人及其信息。 要执行此操作,您需要使用以下进程:
startActivityForResult()
发送包含操作 ACTION_PICK
和“联系人”MIME 类型 CONTENT_ITEM_TYPE
的 Intent 对象。setResult(resultcode, intent)
以设置用于返回至应用的 Intent。Intent 包含用户选择的联系人的内容 URI,以及“extra”标志 FLAG_GRANT_READ_URI_PERMISSION
。这些标志会为您的应用授予读取内容 URI 所指向联系人数据的 URI 权限。 然后,选择 Activity 会调用 finish()
, 将控制权交还给您的应用。onActivityResult()
方法。此方法会收到“联系人”应用中选择 Activity 所创建的结果 Intent。允许用户修改您无权访问的数据的简单方法是激活具有权限的应用,让用户在其中执行工作。
例如,日历应用接受 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 类型具有格式
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。
ContentProvider
Cursor
Uri
内容提供程序管理对中央数据存储区的访问。您将提供程序作为 Android 应用中的一个或多个类(连同清单文件中的元素)实现。 其中一个类会实现子类 ContentProvider
,即您的提供程序与其他应用之间的接口。 尽管内容提供程序旨在向其他应用提供数据,但您的应用中必定有这样一些 Activity,它们允许用户查询和修改由提供程序管理的数据。
本主题的其余部分列出了开发内容提供程序的基本步骤和需要使用的 API。
请在着手开发提供程序之前执行以下操作:
如果完全是在您自己的应用中使用,则不需要提供程序即可使用 SQLite 数据库。
接下来,请按照以下步骤开发您的提供程序:
ContentProvider
类及其所需方法的具体实现。 此类是您的数据与 Android 系统其余部分之间的接口。 如需了解有关此类的详细信息,请参阅实现 ContentProvider 类部分。AbstractThreadedSyncAdapter
实现。内容提供程序是用于访问以结构化格式保存的数据的接口。在您创建该接口之前,必须决定如何存储数据。 您可以按自己的喜好以任何形式存储数据,然后根据需要设计读写数据的接口。
以下是 Android 中提供的一些数据存储技术:
SQLiteOpenHelper
类可帮助您创建数据库,SQLiteDatabase
类是用于访问数据库的基类。 请记住,您不必使用数据库来实现存储区。提供程序在外部表现为一组表,与关系型数据库类似,但这并不是对提供程序内部实现的要求;
java.net
和 android.net
中的类。 您也可以将基于网络的数据与本地数据存储(如数据库)同步,然后以表或文件的形式提供数据。 示例同步适配器示例应用展示了这类同步。以下是一些设计提供程序数据结构的技巧:
BaseColumns._ID
是最佳选择,因为将提供程序查询的结果链接到 ListView
的条件是,检索到的其中一个列的名称必须是 _ID
;ContentResolver
文件方法来访问数据;您也可以使用 BLOB 来实现独立于架构的表。在这类表中,您需要以 BLOB 形式定义一个主键列、一个 MIME 类型列以及一个或多个通用列。 这些 BLOB 列中数据的含义通过 MIME 类型列中的值指示。 这样一来,您就可以在同一个表中存储不同类型的行。 举例来说,联系人提供程序的“数据”表 ContactsContract.Data
便是一个独立于架构的表。
内容 URI 是用于在提供程序中标识数据的 URI。内容 URI 包括整个提供程序的符号名称(其授权)和一个指向表或文件的名称(路径)。 可选 ID 部分指向表中的单个行。 ContentProvider
的每一个数据访问方法都将内容 URI 作为参数;您可以利用这一点确定要访问的表、行或文件。
内容提供程序基础知识主题中描述了内容 URI 的基础知识。
提供程序通常具有单一授权,该授权充当其 Android 内部名称。为避免与其他提供程序发生冲突,您应该使用互联网网域所有权(反向)作为提供程序授权的基础。 由于此建议也适用于 Android 软件包名称,因此您可以将提供程序授权定义为包含该提供程序的软件包名称的扩展名。 例如,如果您的 Android 软件包名称为 com.example.
,则应为提供程序提供 com.example.
授权。
开发者通常通过追加指向单个表的路径来根据权限创建内容 URI。 例如,如果您有两个表:table1 和 table2,则可以通过合并上一示例中的权限来生成 内容 URI com.example.
和 com.example.
。路径并不限定于单个段,也无需为每一级路径都创建一个表。
按照惯例,提供程序通过接受末尾具有行所对应 ID 值的内容 URI 来提供对表中单个行的访问。 同样按照惯例,提供程序会将该 ID 值与表的 _ID
列进行匹配,并对匹配的行执行请求的访问。
这一惯例为访问提供程序的应用的常见设计模式提供了便利。应用会对提供程序执行查询,并使用 CursorAdapter
以 ListView
显示生成的 Cursor
。 定义 CursorAdapter
的条件是, Cursor
中的其中一个列必须是 _ID
用户随后从 UI 上显示的行中选取其中一行,以查看或修改数据。 应用会从支持 ListView
的 Cursor
中获取对应行,获取该行的 _ID
值,将其追加到内容 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/*
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
实例通过处理来自其他应用的请求来管理对结构化数据集的访问。 所有形式的访问最终都会调用 ContentResolver
,后者接着调用ContentProvider
的具体方法来获取访问权限。
抽象类 ContentProvider
定义了六个抽象方法,您必须将这些方法作为自己具体子类的一部分加以实现。 所有这些方法(onCreate()
除外)都由一个尝试访问您的内容提供程序的客户端应用调用:
query()
Cursor
对象返回。
insert()
update()
delete()
getType()
onCreate()
ContentResolver
对象尝试访问您的提供程序时,系统才会创建它。
请注意,这些方法的签名与同名的 ContentResolver
方法相同。
您在实现这些方法时应考虑以下事项:
onCreate()
除外)都可由多个线程同时调用,因此它们必须是线程安全方法。如需了解有关多个线程的更多信息,请参阅进程和线程主题;onCreate()
中执行长时间操作。将初始化任务推迟到实际需要时进行。 实现 onCreate() 方法部分对此做了更详尽的描述;insert()
调用并返回 0。ContentProvider.query()
方法必须返回 Cursor
对象。如果失败,则会引发 Exception
。 如果您使用 SQLite 数据库作为数据存储,则只需返回由 SQLiteDatabase
类的其中一个 query()
方法返回的 Cursor
。 如果查询不匹配任何行,您应该返回一个 Cursor
实例(其 getCount()
方法返回 0)。只有当查询过程中出现内部错误时,您才应该返回 null
。
如果您不使用 SQLite 数据库作为数据存储,请使用 Cursor
的其中一个具体子类。 例如,在 MatrixCursor
类实现的游标中,每一行都是一个 Object
数组。 对于此类,请使用 addRow()
来添加新行。
请记住,Android 系统必须能够跨进程边界传播 Exception
。 Android 可以为以下异常执行此操作,这些异常可能有助于处理查询错误:
IllegalArgumentException
(您可以选择在提供程序收到无效的内容 URI 时引发此异常)NullPointerException
insert()
方法会使用 ContentValues
参数中的值向相应表中添加新行。 如果 ContentValues
参数中未包含列名称,您可能想在您的提供程序代码或数据库架构中提供其默认值。
此方法应该返回新行的内容 URI。要想构建此方法,请使用 withAppendedId()
向表的内容 URI 追加新行的 _ID
(或其他主键)值。
delete()
方法不需要从您的数据存储中实际删除行。 如果您将同步适配器与提供程序一起使用,应该考虑为已删除的行添加“删除”标志,而不是将行整个移除。 同步适配器可以检查是否存在已删除的行,并将它们从服务器中移除,然后再将它们从提供程序中删除。
update()
方法采用 insert()
所使用的相同 ContentValues
参数,以及 delete()
和 ContentProvider.query()
所使用的相同 selection
和 selectionArgs
参数。 这样一来,您就可以在这些方法之间重复使用代码。
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); } }
ContentProvider
类具有两个返回 MIME 类型的方法:
getType()
getStreamTypes()
getType()
方法会返回一个 MIME 格式的 String
,后者描述内容 URI 参数返回的数据类型。Uri
参数可以是模式,而不是特定 URI;在这种情况下,您应该返回与匹配该模式的内容 URI 关联的数据类型。
对于文本、HTML 或 JPEG 等常见数据类型,getType()
应该为该数据返回标准 MIME 类型。 IANA MIME Media Types 网站上提供了这些标准类型的完整列表。
对于指向一个或多个表数据行的内容 URI,getType()
应该以 Android 供应商特有 MIME 格式返回 MIME 类型:
vnd
android.cursor.item/
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
如果您的提供程序提供文件,请实现 getStreamTypes()
。 该方法会为您的提供程序可以为给定内容 URI 返回的文件返回一个 MIME 类型 String
数组。 您应该通过 MIME 类型过滤器参数过滤您提供的 MIME 类型,以便只返回客户端想处理的那些 MIME 类型。
例如,假设提供程序以 .jpg
、.png
和 .gif
格式文件形式提供照片图像。 如果应用调用 ContentResolver.getStreamTypes()
时使用了过滤器字符串 image/*
(任何是“图像”的内容),则 ContentProvider.getStreamTypes()
方法应返回数组:
{ "image/jpeg", "image/png", "image/gif"}
如果应用只对 .jpg
文件感兴趣,则可以在调用 ContentResolver.getStreamTypes()
时使用过滤器字符串 *\/jpeg
,ContentProvider.getStreamTypes()
应返回:
{"image/jpeg"}
如果您的提供程序未提供过滤器字符串中请求的任何 MIME 类型,则 getStreamTypes()
应返回 null
。
协定类是一种 public final
类,其中包含对 URI、列名称、MIME 类型以及其他与提供程序有关的元数据的常量定义。 该类可确保即使 URI、列名称等数据的实际值发生变化,也可以正确访问提供程序,从而在提供程序与其他应用之间建立协定。
协定类对开发者也有帮助,因为其常量通常采用助记名称,因此可以降低开发者为列名称或 URI 使用错误值的可能性。 由于它是一种类,因此可以包含 Javadoc 文档。 集成开发环境(如 Android Studio)可以根据协定类自动完成常量名称,并为常量显示 Javadoc。
开发者无法从您的应用访问协定类的类文件,但他们可以通过您提供的 .jar
文件将其静态编译到其应用内。
举例来说,ContactsContract
类及其嵌套类便属于协定类。
安全与权限主题中详细描述了 Android 系统各个方面的权限和访问。 数据存储主题也描述了各类存储实行中的安全与权限。 其中的要点简述如下:
SQLiteDatabase
数据库是您的应用和提供程序的私有数据库;如果您想使用内容提供程序权限来控制对数据的访问,则应将数据存储在内部文件、SQLite 数据库或“云”中(例如,远程服务器上),而且您应该保持文件和数据库为您的应用所私有。
即使底层数据为私有数据,所有应用仍可从您的提供程序读取数据或向其写入数据,因为在默认情况下,您的提供程序未设置权限。 要想改变这种情况,请使用属性或
元素的子元素在您的清单文件中为您的提供程序设置权限。 您可以设置适用于整个提供程序、特定表甚至特定记录的权限,或者设置同时适用于这三者的权限。
您可以通过清单文件中的一个或多个
元素为您的提供程序定义权限。要使权限对您的提供程序具有唯一性,请为 android:name
属性使用 Java 风格作用域。 例如,将读取权限命名为 com.example.app.provider.permission.READ_PROVIDER
。
以下列表描述了提供程序权限的作用域,从适用于整个提供程序的权限开始,然后逐渐细化。 更细化的权限优先于作用域较大的权限:
元素的 android:permission
属性指定。
元素的 android:readPermission
属性和 android:writePermission
属性 指定它们。它们优先于 android:permission
所需的权限。
元素的
子元素指定您想控制的每个 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
)
android:name
)
ContentProvider
的类。 实现 ContentProvider 类中对此类做了更详尽的描述。
android:grantUriPermssions
:临时权限标志android:permission
:统一提供程序范围读取/写入权限android:readPermission
:提供程序范围读取权限android:writePermission
:提供程序范围写入权限实现内容提供程序权限部分对权限及其对应属性做了更详尽的描述。
android:enabled
:允许系统启动提供程序的标志。android:exported
:允许其他应用使用此提供程序的标志。android:initOrder
:此提供程序相对于同一进程中其他提供程序的启动顺序。android:multiProcess
:允许系统在与调用客户端相同的进程中启动提供程序的标志。android:process
:应在其中运行提供程序的进程的名称。android:syncable
:指示提供程序的数据将与服务器上的数据同步的标志。开发指南中针对
元素的主题提供了这些属性的完整资料。
android:icon
:包含提供程序图标的可绘制对象资源。 该图标出现在Settings > Apps > All 中应用列表内的提供程序标签旁;android:label
:描述提供程序和/或其数据的信息标签。 该标签出现在Settings > Apps > All中的应用列表内。开发指南中针对
元素的主题提供了这些属性的完整资料。
应用可以通过 Intent
间接访问内容提供程序。 应用不会调用 ContentResolver
或 ContentProvider
的任何方法,它并不会直接提供,而是会发送一个启动某个 Activity 的 Intent,该 Activity 通常是提供程序自身应用的一部分。 目标 Activity 负责检索和显示其 UI 中的数据。 视 Intent 中的操作而定,目标 Activity 可能还会提示用户对提供程序的数据进行修改。 Intent 可能还包含目标 Activity 在 UI 中显示的“extra”数据;用户随后可以选择更改此数据,然后使用它来修改提供程序中的数据。
您可能想使用 Intent 访问权限来帮助确保数据完整性。您的提供程序可能依赖于根据严格定义的业务逻辑插入、更新和删除数据。 如果是这种情况,则允许其他应用直接修改您的数据可能会导致无效的数据。 如果您想让开发者使用 Intent 访问权限,请务必为其提供详尽的参考资料。 向他们解释为什么使用自身应用 UI 的 Intent 访问比尝试通过代码修改数据更好。
处理想要修改您的提供程序数据的传入 Intent 与处理其他 Intent 没有区别。 您可以通过阅读 Intent 和 Intent 过滤器主题了解有关 Intent 用法的更多信息。
CalendarContract.Calendars
CalendarContract.Events
CalendarContract.Attendees
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 的格式为
。例如,Events.CONTENT_URI
。
图 1 是对日历提供程序数据模型的图形化表示。它显示了将彼此链接在一起的主要表和字段。
用户可以有多个日历,可将不同类型的日历与不同类型的帐户(Google 日历、Exchange 等)关联。
CalendarContract
定义了日历和事件相关信息的数据模型。这些数据存储在以下所列的若干表中。
表(类) | 说明 |
---|---|
|
此表储存日历特定信息。 此表中的每一行都包含一个日历的详细信息,例如名称、颜色、同步信息等。 |
CalendarContract.Events |
此表储存事件特定信息。 此表中的每一行都包含一个事件的信息 — 例如事件标题、地点、开始时间、结束时间等。 事件可一次性发生,也可多次重复发生。参加者、提醒和扩展属性存储在单独的表内。它们各自具有一个 EVENT_ID ,用于引用 Events 表中的 _ID 。 |
CalendarContract.Instances |
此表储存每个事件实例的开始时间和结束时间。 此表中的每一行都表示一个事件实例。 对于一次性事件,实例与事件为 1:1 映射。对于重复事件,会自动生成多个行,分别对应多个事件实例。 |
CalendarContract.Attendees |
此表储存事件参加者(来宾)信息。 每一行都表示事件的一位来宾。 它指定来宾的类型以及事件的来宾出席响应。 |
CalendarContract.Reminders |
此表储存提醒/通知数据。 每一行都表示事件的一个提醒。一个事件可以有多个提醒。 每个事件的最大提醒数量在 MAX_REMINDERS 中指定,后者由拥有给定日历的同步适配器设置。 提醒以事件发生前的分钟数形式指定,其具有一个可决定用户提醒方式的方法。 |
Calendar Provider API 以灵活、强大为设计宗旨。提供良好的最终用户体验以及保护日历及其数据的完整性也同样重要。 因此,请在使用该 API 时牢记以下要点:
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;
如果您查询 Calendars.ACCOUNT_NAME
,还必须将Calendars.ACCOUNT_TYPE
加入选定范围。这是因为,对于给定帐户,只有在同时指定其 ACCOUNT_NAME
及其 ACCOUNT_TYPE
的情况下,才能将其视为唯一帐户。ACCOUNT_TYPE
字符串对应于在 AccountManager
处注册帐户时使用的帐户验证器。还有一种名为 ACCOUNT_TYPE_LOCAL
的特殊帐户类型,用于未关联设备帐户的日历。ACCOUNT_TYPE_LOCAL
帐户不会进行同步。
在示例的下一部分,您需要构建查询。选定范围指定查询的条件。 在此示例中,查询寻找的是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 |
参加者与事件的关系。下列值之一:
|
ATTENDEE_TYPE |
参加者的类型。下列值之一:
|
ATTENDEE_STATUS |
参加者的出席状态。下列值之一:
|
以下是一个为事件添加一位参加者的示例。请注意,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 |
服务器上设置的提醒方法。下列值之一:
|
下例显示如何为事件添加提醒。提醒在事件发生前 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())); } }
您的应用不需要读取和写入日历数据的权限。它可以改用 Android 的日历应用支持的 Intent 将读取和写入操作转到该应用执行。下表列出了日历提供程序支持的 Intent:
操作 | URI | 说明 | Extra |
---|---|---|---|
VIEW |
CalendarContract.CONTENT_URI 引用 URI。如需查看使用该 Intent 的示例,请参阅使用 Intent 查看日历数据。 |
打开日历后定位到 指定的时间。 |
无。 |
|
Events.CONTENT_URI 引用 URI。如需查看使用该 Intent 的示例,请参阅使用 Intent 查看日历数据。 |
查看 指定的事件。 |
CalendarContract.EXTRA_EVENT_BEGIN_TIME CalendarContract.EXTRA_EVENT_END_TIME |
EDIT |
Events.CONTENT_URI 引用 URI。如需查看使用该 Intent 的示例,请参阅使用 Intent 编辑事件。 |
编辑 指定的事件。 |
CalendarContract.EXTRA_EVENT_BEGIN_TIME CalendarContract.EXTRA_EVENT_END_TIME |
EDIT INSERT |
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。
您的应用可以利用 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);
您可以按更新事件中所述直接更新事件。但使用 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);
日历提供程序提供了两种不同的 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。
DocumentsProvider
DocumentsContract
Android 4.4(API 级别 19)引入了存储访问框架 (SAF)。SAF 让用户能够在其所有首选文档存储提供程序中方便地浏览并打开文档、图像以及其他文件。 用户可以通过易用的标准 UI,以统一方式在所有应用和提供程序中浏览文件和访问最近使用的文件。
云存储服务或本地存储服务可以通过实现封装其服务的 DocumentsProvider
参与此生态系统。只需几行代码,便可将需要访问提供程序文档的客户端应用与 SAF 集成。
SAF 包括以下内容:
DocumentsProvider
类的子类实现。文档提供程序的架构基于传统文件层次结构,但其实际数据存储方式由您决定。Android 平台包括若干内置文档提供程序,如 Downloads、Images 和 Videos。ACTION_OPEN_DOCUMENT
和/或 ACTION_CREATE_DOCUMENT
Intent 并接收文档提供程序返回的文件;SAF 提供的部分功能如下:
SAF 围绕的内容提供程序是 DocumentsProvider
类的一个子类。在文档提供程序内,数据结构采用传统的文件层次结构:
请注意以下事项:
COLUMN_ROOT_ID
,并且指向表示该根目录下内容的文档(目录)。根目录采用动态设计,以支持多个帐户、临时 USB 存储设备或用户登录/注销等用例;COLUMN_DOCUMENT_ID
引用各个文件和目录来显示它们。文档 ID 必须具有唯一性,一旦发放便不得更改,因为它们用于所有设备重启过程中的永久性 URI 授权;MIME_TYPE_DIR
MIME 类型);COLUMN_FLAGS
所述。例如,FLAG_SUPPORTS_WRITE
、FLAG_SUPPORTS_DELETE
和 FLAG_SUPPORTS_THUMBNAIL
。多个目录中可以包含相同的 COLUMN_DOCUMENT_ID
。如前文所述,文档提供程序数据模型基于传统文件层次结构。 不过,只要可以通过 DocumentsProvider
API 访问数据,您实际上可以按照自己喜好的任何方式存储数据。例如,您可以使用基于标记的云存储来存储数据。
图 2 中的示例展示的是照片应用如何利用 SAF 访问存储的数据:
请注意以下事项:
ACTION_OPEN_DOCUMENT
或 ACTION_CREATE_DOCUMENT
后开始。Intent 可能包括进一步细化条件的过滤器 — 例如,“为我提供所有 MIME 类型为‘图像’的可打开文件”;图 3 显示了一个选取器,一位搜索图像的用户在其中选择了一个 Google Drive 帐户:
当用户选择 Google Drive 时,系统会显示图像,如图 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 时,后者会启动一个选取器来显示所有匹配的文档提供程序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
中显示它。
以下示例展示了如何从 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 提供您的文件。 本节描述如何执行此操作。
要想实现自定义文档提供程序,请将以下内容添加到您的应用的清单文件:
元素;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" />
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 过滤器:
res/values/
下的 bool.xml
资源文件中,添加以下行: name="atMostJellyBeanMR2">true
res/values-v19/
下的 bool.xml
资源文件中,添加以下行: name="atMostJellyBeanMR2">false
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
创建子类。您至少需要实现以下方法:
queryRoots()
queryChildDocuments()
queryDocument()
openDocument()
这些只是您需要严格实现的方法,但您可能还想实现许多其他方法。 详情请参阅 DocumentsProvider
。
您实现的 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()
必须使用在 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()
必须使用在 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; }
您必须实现 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); }
ContactsContract.Contacts
ContactsContract.RawContacts
ContactsContract.Data
联系人提供程序是一个强大而又灵活的 Android 组件,用于管理设备上联系人相关数据的中央存储区。 联系人提供程序是您在设备的联系人应用中看到的数据源,您也可以在自己的应用中访问其数据,并可在设备与在线服务之间传送数据。 提供程序储存有多种数据源,由于它会试图为每个联系人管理尽可能多的数据,因此造成其组织结构非常复杂。 为此,该提供程序的 API 包含丰富的协定类和接口,为数据检索和修改提供便利。
本指南介绍下列内容:
本指南假定您了解 Android 内容提供程序的基础知识。如需了解有关 Android 内容提供程序的更多信息,请阅读 内容提供程序基础知识指南。 示例同步适配器示例应用是一个示例,展示如何使用同步适配器在联系人提供程序与 Google 网络服务托管的一个示例应用之间传送数据。
联系人提供程序是 Android 内容提供程序的一个组件。它保留了三种类型的联系人数据,每一种数据都对应提供程序提供的一个表,如图 1 所示:
这三个表通常以其协定类的名称命名。这些类定义表所使用的内容 URI、列名称及列值相应的常量:
ContactsContract.Contacts
表
ContactsContract.RawContacts
表
ContactsContract.Data
表
由 ContactsContract
中的协定类表示的其他表是辅助表,联系人提供程序利用它们来管理其操作,或为设备的联系人或电话应用中的特定功能提供支持。
一个原始联系人表示来自某一帐户类型和帐户名称、有关某个联系人的数据。 由于联系人提供程序允许将多个在线服务作为某一联系人的数据源,因此它允许同一联系人对应多个原始联系人。 借助支持多个原始联系人的特性,用户还可以将某一联系人在帐户类型相同的多个帐户中的数据进行合并。
原始联系人的大部分数据并不存储在 ContactsContract.RawContacts
表内,而是存储在 ContactsContract.Data
表中的一行或多行内。每个数据行都有一个 Data.RAW_CONTACT_ID
列,其中包含其父级 ContactsContract.RawContacts
行的 RawContacts._ID
值。
表 1 列出了 ContactsContract.RawContacts
表中的重要列。请阅读表后的说明:
列名称 | 用途 | 说明 |
---|---|---|
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”,她的设备上定义了以下三个用户帐户:
[email protected]
[email protected]
该用户已在“Accounts”设置中为全部三个帐户启用了“Sync Contacts”。
假定 Emily Dickinson 打开一个浏览器窗口,以 [email protected]
身份登录 Gmail,然后打开 “联系人”,并添加“Thomas Higginson”。后来,她以 [email protected]
身份登录 Gmail,并向“Thomas Higginson”发送一封电子邮件,此操作会自动将他添加为联系人。 她还在 Twitter 上关注了“colonel_tom”(Thomas Higginson 的 Twitter ID)。
以上操作的结果是,联系人提供程序会创建以下这三个原始联系人:
[email protected]
。 用户帐户类型是 Google。[email protected]
。 用户帐户类型也是 Google。由于添加的联系人对应的用户帐户不同,因此尽管名称与前一名称完全相同,也只能作为第二个原始联系人。所前所述,原始联系人的数据存储在一个 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
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
行中的显示情况,以及类型专用列名称“覆盖”通用列名称的情况
表 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 显示的是这三个主要表的相互关系。
虽然用户是直接将联系人数据输入到设备中,但这些数据也会通过同步适配器从网络服务流入联系人提供程序中,这些同步适配器可自动化设备与服务之间的数据传送。 同步适配器在系统控制下在后台运行,它们会调用 ContentResolver
方法来管理数据。
在 Android 中,与同步适配器协作的网络服务通过帐户类型加以标识。 每个同步适配器都与一个帐户类型协作,但它可以支持该类型的多个帐户名称。原始联系人数据来源部分对帐户类型和帐户名称做了简要描述。 下列定义提供了更多详细信息,并描述了帐户类型及帐户名称与同步适配器及服务之间的关系。
google.com
标识的帐户类型。 该值对应于 AccountManager
使用的帐户类型。
帐户类型不必具有唯一性。用户可以配置多个 Google 通讯录帐户并将它们的数据下载到联系人提供程序;如果用户为个人帐户名称和工作帐户名称分别设置了一组联系人,就可能发生这种情况。 帐户名称通常具有唯一性。 它们共同标识联系人提供程序与外部服务之间的特定数据流。
如果您想将服务的数据传送到联系人提供程序,则需编写您自己的同步适配器。 联系人提供程序同步适配器部分对此做了更详尽的描述。
图 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
表。 下表显示的是每一部分元数据的作用:
表 | 列 | 值 | 含义 |
---|---|---|---|
ContactsContract.RawContacts |
DIRTY |
“0”:上次同步以来未发生变化。 | 标记设备上因发生变化而需要同步回服务器的原始联系人。 当 Android 应用更新行时,联系人提供程序会自动设置该值。 修改原始联系人表或数据表的同步适配器应始终向他们使用的内容 URI 追加字符串 |
“1”:上次同步以来发生了变化,需要同步回服务器。 | |||
ContactsContract.RawContacts |
VERSION |
此行的版本号。 | 每当行或其相关数据发生变化时,联系人提供程序都会自动增加此值。 |
ContactsContract.Data |
DATA_VERSION |
此行的版本号。 | 每当数据行发生变化时,联系人提供程序都会自动增加此值。 |
ContactsContract.RawContacts |
SOURCE_ID |
一个字符串值,用于在创建此原始联系人的帐户中对该联系人进行唯一标识。 | 当同步适配器创建新原始联系人时,此列应设置为该原始联系人在服务器中的唯一 ID。 当 Android 应用创建新原始联系人时,应将此列留空。 这是为了向同步适配器表明,它应该在服务器上创建新原始联系人,并获取 SOURCE_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) | 此表用于存储同步适配器的元数据。 | 利用此表,您可以将同步状态及其他同步相关数据持久地存储在设备中。 |
本节描述访问联系人提供程序中数据的准则,侧重于阐述以下内容:
联系人提供程序同步适配器部分也对通过同步适配器进行修改做了更详尽的阐述。
由于联系人提供程序表是以层级形式组织,因此对于检索某一行以及与其链接的所有“子”行,往往很有帮助。 例如,要想显示某位联系人的所有信息,您可能需要检索某个 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
行时使用乐观并发控制,请按以下步骤操作:
VERSION
列以及要检索的其他数据。newAssertQuery(Uri)
方法强制执行约束 的 ContentProviderOperation.Builder
对象。对于内容 URI,请使用追加有原始联系人 _ID
的 RawContacts.CONTENT_URI
。ContentProviderOperation.Builder
对象,请调用 withValue()
,对 VERSION
列与您刚检索的版本号进行比较。ContentProviderOperation.Builder
,请调用 withExpectedCount()
,确保此断言只对一行进行测试。build()
创建 ContentProviderOperation
对象,然后将此对象添加为要传递至 applyBatch()
的 ArrayList
中的第一个对象。如果在您读取原始联系人行到您试图对其进行修改这段时间有另一项操作更新了该行,“断言”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 会启动设备的联系人应用 UI,用户可以在其中执行与联系人有关的操作。 通过这种访问方式,用户可以:
如果用户要插入或更新数据,您可以先收集数据,然后将其作为 Intent 的一部分发送。
当您使用 Intent 通过设备的联系人应用访问联系人提供程序时,您无需自行编写用于访问该提供程序的 UI 或代码。 您也无需请求对提供程序的读取或写入权限。 设备的联系人应用可以将联系人读取权限授予给您,而且您是通过另一个应用对该提供程序进行修改,不需要拥有写入权限。
内容提供程序基础知识指南的“通过 Intent 访问数据”部分详细描述了通过发送 Intent 来访问某提供程序的一般过程。 表 4 汇总了您为可用任务使用的操作、MIME 类型以及数据值,ContactsContract.Intents.Insert
参考文档列出了您可用于putExtra()
的 Extra 值:
任务 | 操作 | 数据 | MIME 类型 | 说明 |
---|---|---|---|---|
从列表中选取一位联系人 | ACTION_PICK |
下列值之一:
|
未使用 | 显示原始联系人列表或一位原始联系人的数据列表,具体取决于您提供的内容 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
文件,其中须包含一个
元素,及其一个或多个
子元素。
部分对此做了更详尽的描述。
如需了解有关自定义 MIME 类型的更多信息,请阅读创建内容提供程序指南。
联系人提供程序专门设计用于处理设备与在线服务之间的联系人数据同步。 借助同步功能,用户可以将现有数据下载到新设备,以及将现有数据上传到新帐户。 此外,同步还能确保用户掌握最新数据,无需考虑数据增加和更改的来源。 同步的另一个优点是,即使设备未连接网络,联系人数据同样可用。
虽然您可以通过各种方式实现同步,不过 Android 系统提供了一个插件同步框架,可自动化完成下列任务:
要使用此框架,您需要提供一个同步适配器插件。每个同步适配器都专用于某个服务和内容提供程序,但可以处理同一服务的多个帐户名称。 该框架还允许同一服务和提供程序具有多个同步适配器。
您需要将同步适配器作为 AbstractThreadedSyncAdapter
的子类进行实现,并作为 Android 应用的一部分进行安装。系统通过您的应用清单文件中的元素以及由清单文件指向的一个特殊 XML 文件了解有关同步适配器的信息。 该 XML 文件定义在线服务的帐户类型和内容提供程序的权限,它们共同对适配器进行唯一标识。 用户为同步适配器的帐户类型添加一个帐户,并为与同步适配器同步的内容提供程序启用同步后,同步适配器才会激活。 激活后,系统将开始管理适配器,并在必要时调用它,以在内容提供程序与服务器之间同步数据。
注:将帐户类型用作同步适配器标识的一部分让系统可以发现从同一组织访问不同服务的同步适配器,并将它们组合在一起。 例如,Google 在线服务的同步适配器都具有相同的帐户类型 com.google
。 当用户向其设备添加 Google 帐户时,已安装的所有 Google 服务同步适配器将一起列出;列出的每个同步适配器都与设备上不同的内容提供程序同步。
大多数服务都要求用户验证身份后才能访问数据,为此,Android 系统提供了一个身份验证框架,该框架与同步适配器框架类似,并且经常与其联用。 该身份验证框架使用的插件身份验证器是 AbstractAccountAuthenticator
的子类。 身份验证器通过下列步骤验证用户的身份:
如果服务接受了凭据,身份验证器便可存储凭据以供日后使用。 由于插件身份验证器框架的存在,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 文件 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
值。 原始联系人的帐户类型和帐户名称也存储在流项目行中。
将您的流数据存储在以下列中:
fromHtml()
渲染的任何格式设置和嵌入式资源图像。 提供程序可能会截断或省略较长内容,但它会尽力避免破坏标记。
要显示您的流项目的标识信息,请使用 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
列中的值。 照片引用存储在表中的以下列:
DisplayPhoto.CONTENT_URI
,获取指向单一照片文件的内容 URI,然后调用 openAssetFileDescriptor()
来获取照片文件的句柄。
openAssetFileDescriptor()
以获得照片文件的句柄。
这些表的工作方式与联系人提供程序中的其他主表基本相同,不同的是:
null
。 查询会返回一个 Cursor,其中包含一行,并且只有 android.provider.ContactsContract.StreamItems#MAX_ITEMS 一列。android.provider.ContactsContract.StreamItems.StreamItemPhotos 类定义了 android.provider.ContactsContract.StreamItemPhotos 的一个子表,其中包含某个流项目的照片行。
通过将联系人提供程序管理的社交流数据与设备的联系人应用相结合,可以在您的社交网络系统与现有联系人之间建立起有效的连接。 这种结合实现了下列功能:
流项目与联系人提供程序的定期同步与其他同步相同。 如需了解有关同步的更多信息,请参阅 联系人提供程序同步适配器部分。接下来的两节介绍如何注册通知和邀请联系人。
要注册您的同步适配器,以便在用户查看由您的同步适配器管理的联系人时收到通知,请执行以下步骤:
res/xml/
目录中创建一个名为 contacts.xml
的文件。 如果您已有该文件,可跳过此步骤。
。 如果该元素已存在,可跳过此步骤。viewContactNotifyService="serviceclass"
属性,其中 serviceclass
是该服务的完全限定类名,应由该服务接收来自设备联系人应用的 Intent。 对于这个通知程序服务,请使用一个扩展 IntentService
的类,以让该服务能够接收 Intent。 传入 Intent 中的数据包含用户点击的原始联系人的内容 URI。 您可以通过通知程序服务绑定到您的同步适配器,然后调用同步适配器来更新原始联系人的数据。要注册需要在用户点击流项目或照片(或同时点击这两者)时调用的 Activity,请执行以下步骤:
res/xml/
目录中创建一个名为 contacts.xml
的文件。 如果您已有该文件,可跳过此步骤。
。 如果该元素已存在,可跳过此步骤。viewStreamItemActivity="activityclass"
属性,其中 activityclass
是该 Activity 的完全限定类名,应由该 Activity 接收来自设备联系人应用的 Intent。viewStreamItemPhotoActivity="activityclass"
属性,其中 activityclass
是该 Activity 的完全限定类名,应由该 Activity 接收来自设备联系人应用的 Intent。
元素做了更详尽的描述。
传入 Intent 包含用户点击的项目或照片的内容 URI。 要让文本项目和照片具有独立的 Activity,请在同一文件中使用这两个属性。
用户不必为了邀请联系人到您的社交网络网站而离开设备的联系人应用。 取而代之是,您可以让设备的联系人应用发送一个 Intent,将联系人 邀请到您的 Activity 之一。要设置此功能,请执行以下步骤:
res/xml/
目录中创建一个名为 contacts.xml
的文件。 如果您已有该文件,可跳过此步骤。
。 如果该元素已存在,可跳过此步骤。inviteContactActivity="activityclass"
inviteContactActionLabel="@string/invite_action_label"
activityclass
值是应该接收该 Intent 的 Activity 的完全限定类名。 invite_action_label
值是一个文本字符串,将显示在设备联系人应用的 Add Connection 菜单中。 注:ContactsSource
是 ContactsAccountType
的一个已弃用的标记名称。
文件 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
inviteContactActionLabel
inviteContactActivity
中指定的 Activity 显示的文本字符串。 例如,您可以使用字符串“Follow in my network”。您可以为此标签使用字符串资源标识符。
viewContactNotifyService
NotifierService.java
文件中查看通知服务的示例。
viewGroupActivity
viewGroupActionLabel
例如,如果您在设备上安装了 Google+ 应用,并将 Google+ 与联系人应用同步,就会看到 Google+ 圈子以组的形式出现在您的联系人应用的 Groups 选项卡内。 如果您点击某个 Google+ 圈子,就会看到该圈子内的联系人以“组”的形式列出。 在该显示页面的顶部,您会看到一个 Google+ 图标;如果您点击它,控制权将切换给 Google+ 应用。 “通讯录”应用以 Google+ 图标作为 viewGroupActionLabel
的值,通过 viewGroupActivity
来实现此目的。
允许使用字符串资源标识符作为该属性的值。
viewStreamItemActivity
viewStreamItemPhotoActivity
元素控制您的应用的自定义数据行在联系人应用 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:summaryColumn
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 表中,社交流照片部分对该表做了更详尽的描述。