Content Provider Basics
英文原文:http://developer.android.com/guide/topics/providers/content-provider-basics.html
采集日期:2015-01-07
Content Provider 管理着数据库的访问。
Provider 是某个 Android 应用程序的一部分,此类应用程序通常拥有自己的数据操作界面,
不过,Content Provider 主要是供其他应用程序使用的,通过 Provider 客户端对象可以访问到 Content Provider 。
Provider 及其客户端相互合作,提供了标准化、持久化的数据访问接口,并完成进程间通讯和权限控制工作。
本文简要介绍了以下内容:
- Content Provider 的工作机制
- 用于读取 Content Provider 数据的 API
- 用于插入、修改、删除 Content Provider 数据的 API
- 其他操作 Provider 的 API。
概述
Content Provider 以数据表的形式向外部应用程序提供数据,这与关系型数据库中的表很类似。
其中,行(row)表示由多个不同类型数据构成的单个实体,每行数据中的列(column)代表实体中的一个数据项。
例如,用户词典就是 Android 系统内置的 Provider 之一,里面记录着用户需要留存的自定义拼写规则的单词。
表1例举了此 Provider 数据表中可以查询的字段信息:
表1: 用户词典表举例
word | app id | frequency | locale | _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
列被用作“主键”(primary key),并且是由 Provider 自动维护的。
注意: Provider 本身不需要用到主键,主键的名称也不一定要是 _ID
。
但是,如果要把 Provider 作为数据源与ListView
绑定,则必须有一个列的名称是 _ID
。
详细要求将在 显示查询结果 中描述。
访问 Provider
应用程序是通过客户端对象ContentResolver
访问 Content Provider 的。 此对象中包含一些方法,这些方法将会调用 Provider 对象中的同名方法。而 Provider 对象是ContentProvider
某个具体子类的实例。ContentResolver
中的方法内置了基本的“CRUD”(创建、查询、更新、删除(create、retrieve、update 和 delete))功能。
ContentResolver
对象运行于客户端应用的进程中,而ContentProvider
运行于提供 Provider 应用的进程中,两者会自动完成进程间的通讯。ContentProvider
还发挥着数据抽象层的作用,负责将内部数据以数据库表的形式提供出来。
注意:
为了访问 Provider,应用程序通常必须在 Manifest 文件中请求相应的权限。
详情请参阅Content Provider 权限一节。
例如,要从 User Dictionary Provider 中读取单词及地区列表,就要用到ContentResolver.query()
。query()
方法会去调用 User Dictionary Provider 中对应的ContentResolver.query()
方法。以下代码演示了ContentResolver.query()
的调用过程:
// 查询用户词典并返回结果
mCursor = getContentResolver().query(
UserDictionary.Words.CONTENT_URI, // 单词表的 Content URI
mProjection, // 需要返回的列
mSelectionClause, // 查询条件
mSelectionArgs, // 查询条件的参数
mSortOrder); // 返回结果的排序要求
表2给出了 query(Uri,projection,selection,selectionArgs,sortOrder)
的参数与 SQL SELECT 语句的对应关系:
表2: Query() 与 SQL 查询的对比
query() 参数 | SELECT 关键字/参数 | 说明 |
---|---|---|
Uri |
FROM table_name |
Uri 对应于 table_name 指定的 Provider 数据表名。 |
projection |
col,col,col,... |
projection 是包含返回列名称的数组。 |
selection |
WHERE col = value |
selection 指定查询条件。 |
selectionArgs |
(没有固定值,该查询参数将会替换查询语句中的占位符“?”。) | |
sortOrder |
ORDER BY col,col,... |
sortOrder 指定了返回 Cursor 中各行的显示顺序。 |
Content URI
Content URI 是一种用于标识 Provider 数据的 URI。 Content URI 包括了整个 Provider 的符号名称(authority)和表名(path)。 调用客户端的方法访问 Provider 数据表时,表的 Content URI 是参数之一。
在前面的代码中,常量 CONTENT_URI
包含了指向用户词典中 “word” 表的 Content URI。 ContentResolver
对象将分离出 URI 中的 authority ,并用它“解析” 出 Provider,这是通过将 authority 与系统记录的已有 Provider 清单进行比较来实现的。 然后 ContentResolver
就可以将查询参数发送给相应的 Provider 了。
ContentProvider
用 Content URI 的 path 部分选择要访问的数据表。 通常, Provider 公开的所有数据表都会带有自己的 path 。
在上述代码中,“word”表的完整 URI 为:
content://user_dictionary/words
这里的字符串 user_dictionary
是 Provider 的 authority 部分, 字符串 words
是数据表的 path 部分。 字符串 content://
(scheme)是必须指定的,以表明这是一个 Content URI。
很多 Provider 提供了对单条记录的访问能力,只要在 URI 后面跟一个 ID 值即可。 例如,要读取用户词典中 _ID
为 4
的数据行,可以使用以下 Content URI:
Uri singleUri =ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);
如果已经读取了一些数据,然后需要修改或删除其中的某一条,这时就经常会用到 ID 值了。
注意: Uri
和Uri.Builder
类中已内置了一些工具性的方法,可以由字符串搭建合乎规则的 Uri 对象。 ContentUris
中有一些在 URI 后面追加 ID 值的常用方法。 上述代码就用了 withAppendedId()
把 ID 追加到 UserDictionary 的 Content URI 之后。
从 Provider 读取数据
本节将介绍从 Provider 读取数据的过程,还是以 User Dictionary Provider 为例。
为了清晰起见,本节中的代码将会调用“UI 线程”中的 ContentResolver.query()
。但是在实际的代码中,应该在单独的线程中实现异步查询。 一种方案是利用 CursorLoader
类,有关细节将在 Loaders 指南中介绍。而且,以下只给出了部分代码,而非一个完整的应用程序。
从 Provider 中读取数据的基本步骤如下所示:
- 申请读取 Provider 的权限。
- 编写向 Provider 发送查询请求的代码。
申请读取权限
要从 Provider 读取数据,应用程序需要拥有对 Provider 的“读权限”。 在运行时是无法申请该权限的,只能在 Manifest 文件中通过 <uses-permission>
指定。在 Manifest 文件中的定义,实际上是表明此应用程序需要“申请”该权限。 这样用户在安装此应用程序时,就可以明确授权。
在 Provider 的参考文档中,给出了其用到的全部权限的准确名称。
访问 Provider 时权限所起的作用,将在Content Provider 权限一节中详细介绍。
User Dictionary Provider 在其 Manifest 文件中定义了 android.permission.READ_USER_DICTIONARY
权限, 因此要读它的应用程序就必须请求该权限。
构建查询
接下来是构建查询请求。 以下代码定义了一些变量,在访问 User Dictionary Provider 时将会用到:
// "projection" 定义了要返回的数据列
String[] mProjection =
{
UserDictionary.Words._ID, &n // 对应列名为 _ID 的 Contract Class 常量
UserDictionary.an class="typ">Words.WORD, // 对应列名为 word 的 Contract Class 常量
UserDictionary.an class="typ">Words.LOCALE &nbLOCALE // 对应列名为 local 的 Contract Class 常量
}; // 定义存放查询条件的字符串
String mSelectionClause =an class="pln"> null;<s; // 初始化存放查询参数的数组
String[]an class="pln"> mSelectionArgs ={""};
接下来的代码演示了 ContentResolver.query()
的使用方法,这里以 User Dictionary Provider 为例。 Provider 客户端查询与 SQL 查询很类似,也包含了需返回的列名、查询条件和排序要求。
查询返回的列名集合对象被称为”投影“( Projection )(即变量 mProjection
)。
查询数据的表达式被拆分为查询条件和查询参数。 查询条件是由逻辑/布尔表达式、列名、数值组成(即变量 mSelectionClause
)。 如果用参数 ?
代替了具体数值,则查询方法将会从查询参数数组(变量 mSelectionArgs
)中读取实际的值。
在以下代码中,如果用户没有输入单词,则查询语句将被置为 null
,这样查询将会返回 Provider 中的所有单词。 如果用户输入了单词,那么查询语句将会是 UserDictionary.Words.WORD + " = ?"
,且查询参数数组中的第一个成员被设为用户输入的单词。
/*
* 定义只有一个成员的字符串数组,用于存放查询参数。
*/
String[] mSelectionArgs ={""}; // 从用户界面读取一个单词
mSearchString = mSearchWord.getText().toString(); // 别忘了在这里添加检查输入内容是否非法或恶意的代码 // 如果单词为空字符串,则读取所有数据
if(TextUtils.isEmpty(mSearchString)){
// 将查询语句设为 null 将返回所有数据
mSelectionClause =null;
mSelectionArgs[0]=""; }else{
// 由用户录入单词构建查询语句
mSelectionClause =UserDictionary.Words.WORD +" = ?"; // 将用户录入的字符串置入查询参数数组中
mSelectionArgs[0]= mSearchString; } // 查询数据并返回游标(Cursor)对象
mCursor = getContentResolver().query(
UserDictionary.Words.CONTENT_URI, // 单词表的 Content URI
mProjection, // 需返回的列
mSelectionClause // 为 null 或是用户录入的单词
mSelectionArgs, // 为空或是用户录入的字符串
mSortOrder); // 定义返回数据的排序规则 // 在出错时,某些 Provider 返回 null,另一些会抛出异常
if(null== mCursor){
/*
* 在这里插入处理错误的代码。
* 请勿在这里使用游标!
* 可能需要调用
*/
// 如果游标中没有内容,表示 Provider 没找到匹配的记录。
}elseif(mCursor.getCount()<1){ /*
* 在这里插入通知用户查询失败的代码。
* 这不一定是出错了,可以让用户录入新记录,也可以重新输入查询条件。
*/ }else{
// 在这里插入处理查询结果的代码。 }
查询的语句与以下 SQL 语句类似:
SELECT _ID, word, locale FROM words WHERE word =<userinput> ORDER BY word ASC;
这条 SQL 语句中使用的是真实的列名,而不是 Contract 类常量。
防止非法输入
如果 Content Provider 管理的数据存放于 SQL 数据库中,那么在 SQL 语句中插入某些非法信息可能会引发 SQL 注入问题。
请看下面这条查询语句:
// 将用户输入内容拼接在列名之后,构造一条查询语句。
String mSelectionClause = "var = "+ mUserInput;
这时,用户就可以将恶意 SQL 拼接到查询语句中。 比如,用户可以将 mUserInput
输入为“nothing; DROP TABLE *;”,这样查询语句就会成为“var = nothing; DROP TABLE *;
”. 因为查询语句将用作 SQL 语句,所以会导致 Provider 删除底层 SQLite 数据库中的所有数据表(除非 Provider 设置为捕获 SQL 注入 异常)。
为了避免这类问题,可以在查询语句中使用 ?
作为可替代参数,并用另一个数组作为实际的参数值。 这样,用户的输入就与查询直接关联,而不会被解释为 SQL 语句的一部分。 因为不再用作 SQL 语句,用户输入就无法注入恶意 SQL 了。 用户的输入内容不直接用于拼接 SQL 语句,查询语句如下:
// 用可替代参数构造查询语句
String mSelectionClause = "var = ?";
查询参数数组定义如下:
// 定义存放查询参数值的数组
String[] selectionArgs ={""};
在数组中放入一个查询参数值:
// 将查询参数赋为用户的输入值
selectionArgs[0]= mUserInput;
在构造查询时,推荐使用这种将 ?
作为形参、数组提供实参的查询语句,即使不是基于 SQL 数据库的 Provider 也可以使用。
显示查询结果
客户端方法 ContentResolver.query()
将返回一个 Cursor
,其中的数据列由对应查询条件的 Projection 指定。 Cursor
对象支持对数据行和数据列的随机读取。通过 Cursor
的内部方法,可以遍历结果数据行、获取每一列的数据类型、读取某一字段的数据并检查其他属性。 某些 Cursor
对象可以在 Provider 的数据发生变化时进行自动更新,或是在 Cursor
数据变动时触发其他监听对象的方法。
注意: 根据建立查询的对象性质, Provider 可以限制对数据列的访问。 比如,联系人 Provider 就不允许 Sync Adapter 访问某些数据列,也就不会在 Activity 和服务中返回这些列。
如果没有找到符合条件的数据, Provider 就会返回一个 Cursor.getCount()
为 0 的 Cursor
对象(即空游标)。
如果发生了内部错误,查询返回的结果将视 Provider 的不同而定。 可能是返回 null
,也可能抛出一个 Exception
。
因为 Cursor
是一个数据行的“列表”,所以一种较好的显示方式就是通过 SimpleCursorAdapter
把它与 ListView
关联起来。
以下代码将延续上面的代码。 创建了一个含有 Cursor
的 SimpleCursorAdapter
对象,并将其设置为一个 ListView
的数据源适配器(Adapter):
// 定义需要从 Cursor 读取并显示出来的数据列
String[] mWordListColumns =
{
UserDictionary.Words.WORD, // 对应 word 列的 Contract 类常量
UserDictionary.Words.LOCALE // 对应 locale 列的 Contract 类常量
}; // 定义 View ID 列表,用于保存 Cursor 返回的一行数据。
int[] mWordListItems ={ R.id.dictWord, R.id.locale}; // 新建一个 SimpleCursorAdapter 对象
mCursorAdapter =newSimpleCursorAdapter(
getApplicationContext(), // 应用程序的 Context 对象
R.layout.wordlistrow, // XML 格式的 Layout,用于 ListView 中每一行的布局
mCursor, // 查询结果
mWordListColumns, // 字符串数组,存放游标中的列名
mWordListItems, // 整形数组,存放行布局中的 View ID
0); // 标志位(一般用不上) // 设置 ListView 的 Adapter
mWordList.setAdapter(mCursorAdapter);
注意: 要将 Cursor
用作 ListView
的后台数据源,游标必须包含一个名为 _ID
的数据列。 因此,上述查询从“word”表中读取了 _ID
列,当然 ListView
并不会显示这个字段。 这也是大部分 Provider 中的数据表都带有 _ID
列的原因所在。
从查询结果中读取数据
查询结果不只是简单地用于显示,还可以用来完成其他操作。 比如,可以从用户词典中读取单词并在其他 Provider 中进行检索。 这时就需要遍历 Cursor
中的每行数据:
// 找到列名为“word”的字段编号
int index = mCursor.getColumnIndex(UserDictionary.Words.WORD); /*
* 仅当游标可用时才会执行。
* 如果发生内部错误,User Dictionary Provider 将会返回 null。而其他 Provider 可能会抛出异常。
*/ if(mCursor !=null){
/*
* 前进至下一行。
* 在第一次移动之前,“记录指针”为 -1,如果这时读取数据,将会触发异常。
*/
while(mCursor.moveToNext()){ // 读取值
newWord = mCursor.getString(index); // 在这里插入处理返回单词的代码 ... // while 循环结束
}
}else{ // 如果游标为空或 Provider 抛出异常,在这里插入显示错误的代码。
}
Cursor
中有很多用于读取不同类型数据的“get”方法。 例如,上述代码中用到了 getString()
。还有一个 getType()
方法用于返回字段的类型。
Content Provider 权限
Provider 应用可以设定一些访问权限要求,其他应用程序访问该 Provider 中的数据时必须拥有这些权限。 通过这些权限,用户可以了解并确认某个应用程序将会访问的数据。 根据 Provider 的要求,其他应用需要对访问权限提出申请。 在安装这些应用的时候,最终用户就能看到这些权限请求。
如果 Provider 应用没有设定任何权限要求,其他应用就无权访问该 Provider 中的数据。 不过,该 Provider 内部的组件都是拥有完整的读写权限的,这与权限设定没有关系。
如上所注,读取 User Dictionary Provider 中的数据需要 android.permission.READ_USER_DICTIONARY
权限。 此 Provider 还有另一个 android.permission.WRITE_USER_DICTIONARY
权限用于插入、修改和删除数据。
为了申请 Provider 所需的权限,应用程序可以通过 Manifest 文件中的 <uses-permission>
元素。 当 Android Package Manager 安装应用程序时,用户必须同意所有的权限请求。 只有全部同意,Package Manager 才会继续安装,否则就会退出。
读取 User Dictionary Provider 需要设置以下 <uses-permission>
元素:
<uses-permissionandroid:name="android.permission.READ_USER_DICTIONARY">
在 安全与权限 指南中,将对 Provider 访问权限的效果进行更为详细的介绍。
插入、修改、删除数据
采用与读操作类似的方式,通过 Provider 客户端和 Provider 的 ContentProvider
之间的连接,还能进行数据修改操作。 然后再调用 ContentResolver
方法,参数中就包含了传给 ContentProvider
对应方法的参数。Provider 及其客户端会自动保证通讯安全并完成进程间通讯。
插入数据
调用 ContentResolver.insert()
方法可以将数据插入 Provider 到中去。 该方法将在 Provider 中插入新数据行,并返回一个指向改行数据的 Content URI。 以下代码将在 User Dictionary Provider 中插入一条新的单词:
新行的数据存放在一个 ContentValues
对象中, 对象中,这个对象类似于只包含一条数据的游标。 该对象中的各个字段的类型可以各不相同。如果不需要指定值,可以用 ContentValues.putNull()
方法置为 null
。
上述代码并没有给 _ID
字段赋值,因为这个字段是由系统自动维护的。 Provider 会自动给插入行的 _ID
字段赋一个唯一值,并且通常把它作为表的主键使用。
在 newUri
中返回的 Content URI 唯一标识了新插入的数据行,格式如下:
content://user_dictionary/words/<id_value>
<id_value>
是新插入行的 _ID
字段值。 绝大部分 Provider 都能自动识别这种格式,以便在指定数据行上进行所需的操作。
调用 ContentUris.parseId()
可以读取 Uri
的 _ID
值。
更新数据
利用 ContentValues
对象可以 对象可以更新一行数据,新数据的给出方式与插入操作类似,查询条件的给出方式与查询请求类似。 更新操作的客户端方法是 ContentResolver.update()
,只需把要更新的数据字段添加到 ContentValues
对象中即可。如果要清除字段值,请把值赋为 null
。
以下代码将把地区代码为“en”的行全部置为 null
, 返回值是更新成功的行数:
// 定义对象,存放要更新的数据
ContentValuesan class="pln"> mUpdateValues =newContentValues(); // 定义要更新数据的查询条件
String mSelectionClause =UserDictionary.Words.LOCALE + "LIKE ?";
String[] mSelectionArgs ={"en_%"}; // 定义变量,存放更新成功的行数
int mRowsUpdated =0; ... /*
* 设置更新数据并进行更新
*/
mUpdateValues.putNull(UserDictionary.Words.LOCALE); mRowsUpdated = getContentResolver().update(
UserDictionary.Words.CONTENT_URI, // 用户词典 Content URI
mUpdateValues // 要更新的字段
&nb mSelectionClause // 查询条件
mSelectionArgs // 查询数据值an class="pln">
);
在调用 ContentResolver.update()
时,同样应该对用户输入内容进行必要的过滤,请阅读 防止非法输入一节。
删除数据
删除与读取相类似:指定要删除数据的查询条件,客户端方法会返回删除成功的行数。 以下代码将删除 appid 包含 “user”的数据行,并返回删除成功的行数。
// 定义要删除数据的查询条件
String mSelectionClause =UserDictionary.Words.APP_ID +" LIKE ?";
String[] mSelectionArgs ={"user"}; // 定义变量,用于保存返回的删除成功行数
int mRowsDeleted =0; ... // 删除符合查询条件的单词数据
mRowsDeleted = getContentResolver().delete(
UserDictionary.Words.CONTENT_URI, // 用户词典 Content URI
mSelectionClause // 查询条件
mSelectionArgs // 查询条件值
);
在调用 ContentResolver.delete()
时,同样应该对用户输入内容进行必要的过滤,请阅读 防止非法输入一节。
Provider 支持的数据类型
Content Provider 支持很多种数据类型。 User Dictionary Provider 只支持文本类型,但 Provider 还可以支持以下类型:
- 整数
- 长整数(long)
- 浮点数
- 长浮点数(double)
Provider 经常用到的另一类数据是二进制大数据对象(Binary Large OBject,BLOB),这实现为 64KB 字节的数组。 具体有哪些数据类型可用,请查阅 Cursor
类的“get”方法。
通常,在 Provider 的文档中都会列出所有字段的数据类型。 User Dictionary Provider 的数据类型列在其 Contract 类 UserDictionary.Words
的参考手册中。(Contract 类在 Contract 类一节中介绍)。 通过调用 Cursor.getType()
方法也可以确定字段的数据类型。
Provider 还Provider 还维护着内部数据对应 Content URI 的 MIME 数据类型信息。 通过 MIME 类型信息,可以知晓应用程序能否处理这些数据,或者根据这些 MIME 信息选择相应的处理方式。 当 Provider 中包含了复杂的数据结构或文件时,通常就要用到 MIME 类型了。 比如,Contact Provider 中的 ContactsContract.Data
表就用 MIME 类型来标记了联系人的数据。调用 ContentResolver.getType()
可以读取某个 Content URI 对应的 MIME 类型。
MIME 类型参考一节中介绍了标准的 MIME 及自定义 MIME 类型。
Provider 的其他访问形式
在开发应用时,访问 Provider 还有其他三种重要的形式:
-
批量访问:通过
ContentProviderOperation
类的一些方法,可以创建批量访问任务,并通过ContentResolver.applyBatch()
来提交。 - 异步查询:在单独的线程中执行查询。有一种方案是用
CursorLoader
对象来实现。在指南 Loaders 中给出了示例。 - 利用 Intent 访问数据: 虽然不能向 Provider 直接发送 Intent,但可以向 Provider 所在应用发送 Intent, 通常这些应用都具备修改 Provider 数据的能力。
后续章节将介绍如何利用 Intent 批量读取和修改数据。 id="Batch">批量访问
如果要插入大量数据,或是在一次调用时要同时插入多张表的数据,或者要把多个跨进程的操作放入一个事务中完成(原子操作), 这时 Provider 的批量访问将十分有用。
为了能以“批量”方式访问 Provider,需要创建一个 ContentProviderOperation
对象的数组,并通过 ContentResolver.applyBatch()
方法将它传给 Content Provider。 在调用时不是指定某个 Content URI,而是要给出 Content Provider 的 authority。 数组中的每个 ContentProviderOperation
对象可以对不同的数据表进行操作。 ContentResolver.applyBatch()
返回的结果也是数组。
在 Contract 类 ContactsContract.RawContacts
的介绍中 的介绍中,包含了批量插入数据的代码片段。在应用示例 Contact Manager 的 ContactAdder.java
文件中,也包含了批量访问 Provider 的例子。
通过助手应用显示数据
如果拥有足够的访问权限,也许会通过 Intent 显示其他应用的数据。 比如,日历应用可以接收 ACTION_VIEW
Intent,用于显示某个日期或事件。 这样就不用自己创建界面来显示日程信息了。 更多信息请参阅 Calendar Provider 指南。
发送 Intent 的应用不一定是与 Provider 关联的应用。 比如,可以从 Contact Provider 中读取一条联系人信息,然后向图片浏览应用发送一个包含了 Content URI 的 ACTION_VIEW
Intent,以便显示联系人的头像。
利用 Intent 访问数据
通过 Intent 可以直接访问 Content Provider。 即使应用程序不具备访问权限,用户也可访问 Provider 中的数据。 可以是从具备权限的应用中获取一个结果 Intent,也可以启动具备权限的应用并在其中进行操作。
通过临时许可权限访问
即便没有相应的访问权限,通过向具备权限的应用发送 Intent 并接收包含“URI”许可的结果 Intent,也可以访问 Content Provider 中的数据。 这个许可权只针对某个 Content URI,且只在接收方 Activity 存活时才有效。 具备永久访问权限的应用程序通过设置结果 Intent 的以下标志位来授予这种临时许可权:
注意: 这两个标志位并不是针对 Provider (Content URI 中包含其 authority)赋读写权限的。 而只是对 URI 本身赋权。
Provider 在其 Manifest 文件中定义了 Content URI 的权限,这通过 <provider>
元素的 android:grantUriPermission
属性和 <provider>
的子元素 <grant-uri-permission>
来实现。关于 URI 权限的机制,将在 安全和权限 指南的“URI 权限”一节中详细介绍。
例如,即使没有 READ_CONTACTS
权限,也可以读取 Contacts Provider 中的联系人信息。 也许在某个发送电子生日贺卡的应用中,会需要用到这一功能。 拥有 READ_CONTACTS
权限可以访问所有的联系人相关信息,但更合适的做法是让用户来控制应用程序可以访问到的联系人。 按照以下步骤可以实现这一点:
- 应用程序通过
startActivityForResult()
方法发送一个 Intent,其中包含的 Action 为ACTION_PICK
,“contacts” MIME 类型为CONTENT_ITEM_TYPE
。 - 因为这个 Inte因为这个 Intent 与 People 应用中“selection”Activity 的 Intent 过滤器相匹配,所以该 Activity 将会被调入前台。
- 在 selection Activity 中,用户选中一个要修改的联系人。 selection Activity 将调用
setResult(resultcode, intent)
创建一个作为结果返回的 Intent 。这个 Intent 包含了用户选中联系人的 Content URI,“extras” 中给出了FLAG_GRANT_READ_URI_PERMISSION
权限,允许应用程序读取该 Content URI 指向的联系人信息。 然后, selection Activity 会调用finish()
将控制权返回调用方应用。 - 然后调用方应用返回前台,系统将调用其
onActivityResult()
方法。该方法将获取由 People 应用的 selection Activity 返回的 Intent。 - 利用返回 Intent 中的 Content URI,可以读取 Contacts Provider 中相应的联系人信息,即使在 Manifest 文件中没有申请永久性的读取权限也没关系。 然后,就可以获取联系人的生日信息、email 地址,并发送电子贺卡了。
利用其他应用访问数据
还有一种方法比较简还有一种方法比较简单,也可以让用户修改未获授权的数据,这就是启动另一个具备权限的应用并让用户在其中完成修改工作。
比如,日历应用可以接收 ACTION_INSERT
Intent,这可以启动它的插入界面。 可以在这个 Intent 中附带“extras”数据,用于指定要打开的内置界面。 因为定义一个日程事件的语法比较复杂,所以为了在 Calendar Provider 中插入一条事件,建议通过 ACTION_INSERT
启动日历应用并让用户在其中完成操作。
Contract 类
合约类(Contract Class)定义了一些常量,以方便应用程序对 Content Provider中 的 Content URI、列名、Intent Action 等对象进行操作。 Contract 类不会跟随 Provider 自动生成,开发 Provider 的人员必须自行定义并提供给其他开发人员使用。 很多 Android 系统自带的 Provider 已经在 android.provider
包中给出了相应的 Contract 类。
例如,User Dictionary Provider 就带有 Contract 类 UserDictionary
,里面定义了 Content URI 和列名常量。 “words”表的 Content URI 在常量 UserDictionary.Words.CONTENT_URI
中定义。 UserDictionary.Words
类还定 类还定义了列名常量,本文的代码中用到了这些常量。 比如,查询 Projection 可以定义如下:
Stringpan class="pun">[] mProjection =
{
UserDictionary.Words._ID,
UserDictionary.Words.WORD,
UserDictionary.Words.LOCALE
};
Contacts Provider 的另一个 Contract 类为 ContactsContract
,在它的参考文档中给出了示例代码。 ContactsContract.Intents.Insert
是它的 是它的一个子类,其中定义了有关 Intent 及其数据的常量。
MIME 类型参考
Content Provider 可以返回标准的 MIME 媒体类型、自定义 MIME 类型字符串,或是两者都有。
MIME 类型的格式如下:
type/subtype
比如,常见的 MIME 类型 text/html
包含 text
类型和 html
子类型。 如果 Provider 返回该类型的 URI,就表示使用此 URI 的查询将返回带有 HTML 标记的文本。
自定义 MIME 类型字符串也被成为“供应商指定(vendor-specific)”的 MIME 类型,type 值和 subtype 值也更为复杂。 多行数据的 type 值通常为:
vnd.android.cursor.dir
单行数据则为:
vnd.android.cursor.item
subtype 值是由 Prov 值是由 Provider 定义的(provider-specific)。 Android 内置 Provider 的子类型通常都比较简单。 比如,联系人应用在新建一条电话号码数据时,就把其 MIME 类型设置为:
vndpan class="pun">.android.cursor.item/phone_v2
请注意,这里的子类请注意,这里的子类型值即为 phone_v2
其他开发人员也许会基于 Provider 的 authority 和表名创建自己的子类定义规则。 比如,假设某个 Provider 用于保存列车时刻表,其 authority 为 com.example.trains
,其中包括 Line1、Line2 和 Line3 三张表。 对应 Line1 表的 Content URI 为:
content://com.example.trains/Line1
Provider 返回的 MIME 类型为:
vnd.android.cursor.dir/vnd.example.line1
对应 Line2 表第 5 行数据的 URI 为:
content://com.example.trains/Line2/5
Provider 返回的 MIME 类型为:
vnd.android.cursor.item/vnd.example.line2
大部分 Cont大部分 Content Provider 都定义了自己的 Contract 类常量。 比如, Contacts Provider 的 Contract 类 ContactsContract.RawContacts
就为单条联系人数据的 MIME 类型定义了常量 CONTENT_ITEM_TYPE
。
关于单行数据的 Content URI 已经在 Content URI 一节中详细介绍过了。