从Provider取得data
本节讲述了如何从provider取得数据,使用用户词典作为例子.
为了清析易懂,本节中调用ContentResolver.query()的代码片断置于"UI 线程"中.但是,在实际代码中,你应该在另一个线程执行查询动作,这样做的一种方法是使用CursorLoader 类.而,那几行示例代码仅是片断,它们不能展示一个完整的应用.
要从provider取得data,须依如下步骤:
1 请求provider的读权限.
2 定义发送请求到provider的代码.
请求读权限
要从一个provider中获取数据,你的应用需要对目标provider具有"读权限".你不能在运行时请求此权限,而只能在manifest文件中使用 <uses-permission> 元素指定你的权限需求.当你在manifest中指定此元素时,你实际上就是在为你的应用请求这个权限.当用户安装你的应用时,就表示同意了这个权限请求.
要找到你使用的provider读权限的所对应的准确名字,以及其它用于provider的权限的名字,请浏览provider的文档.
关于操作provider的权限的角色的更多信息,请见Content Provider权限一节.
用户词典Provider在它的manifest 中定义了android.permission.READ_USER_DICTIONARY 权限,所以一个想读取它内容的应用必须请求此权限.
构建请求
获取数据的下一步是构建一个请求(query).这里的第一个代码片段定义了一些用于操作用户词典Provider的变量:
// "projection" 定义了要返回的各列们
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
};
// 定义一个包含"select"条款的字符串
String mSelectionClause = null;
// 初始化一个包含"select"参数的字符串
String[] mSelectionArgs = {""};
下一个代码片段演示了如何使用ContentResolver.query(),将用户词典Provider作为一个例子.一个provider客户端查询极像一个SQL查询,它包含了要返回的一坨column们,一堆筛选条件,和一个排序方式.
查询返回的column集合被称作projection (变量 mProjection).
指定返回的列的语句被分解为选择条款和选择参数两部分.选择条款是逻辑和布尔表达式,列名以及值的组合体(变量mSelection).如果你在其中指定了使用 ? 来代表一个值,查询方法就会从选择参数部分取得这个值(变量mSelectionArgs).
在下一个代码片段中,如果用户没有输入单词,选择条款就被设为null,并且查询会反回所provider中所有的单词.如果用户输入了单词,选择条款就被设置为UserDictionary.Words.Word + " = ?" 并且选择参数(数组)的第一项被设置为用户输入的单词.
/*
* 定义一个一维的字符串数组来容纳选择参数们
*/
String[] mSelectionArgs = {""};
// 从界面中获取一个单词
mSearchString = mSearchWord.getText().toString();
// 记住要在此插插入代码检查不合法的或恶意的输入.
// 如果单词是空的,则获取所有数据
if (TextUtils.isEmpty(mSearchString)) {
// 设置选择条款为null就会返回所有单词
mSelectionClause = null;
mSelectionArgs[0] = "";
} else {
// 构造一个匹配用户输入的单词的选择条款
mSelectionClause = " = ?";
// 将用户输入的单词置于选择参数中
mSelectionArgs[0] = mSearchString;
}
// 执行查询并返回游标对象
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
// 有些provider在出错时返回null,有抛出异常
if (null == mCursor) {
/*
* 在此插入代码处理错误.记住不要使用游标! 你可能要调用
* android.util.Log.e()把错误记录的日志
*
*/
// 如果游标是空的,找不到匹配的provider
} else if (mCursor.getCount() < 1) {
/*
* 在此插入代码来通知用户,查找不成功.这也不能完全算是个错误.你可能想为用户提供插入一个新行或重新输入查询单词的选项
*/
} else {
// 在此插入代码,利用返回的结果做想做的事
}
查询与下面的SQL语句等价:
SELECT _ID, word, frequency, locale FROM words WHERE word = <userinput> ORDER BY word ASC;
在此SQL 语句中,以实际的列名代替了内置的类别常量.
防止恶意输入
如果被content provider管理的数据是一个SQL 数据库,在原始的SQL语句中包含不可信的数据会导致SQL注入.
思考以下选择条款:
// 通过连接用户输入到列名来构造一个选择条款
String mSelectionClause = "var = " + mUserInput;
如果你这样做,你就允许用户连接恶意的SQL语句到你的SQL语句中.例如,用户可以输入"nothing; DROP TABLE *;" ,这将在选择条款中变为 var = nothing; DROP TABLE *;..既然选择条款被作为SQL语句,这就可能导致provider删除SQLite数据库中的所有的表(除非provider被设置成捕获SQL injection 阴谋).
要避免此问题,应使用一个运用?作为可替换参数的选择条款和一个作为选择参数的数组.当你这样做时,用户输入被直接绑定到查询而不是被解释为SQL语句的一部分.因为它不被认为是SQL,于是用户输入就不能注入恶意SQL.使用以下选择条款来代替连接用户输入的那个:
// 构造一个带有占位符的选择条款
String mSelectionClause = "var = ?";
像这样建立起选择参数数组:
// 定义一个数组来容纳选择参数
String[] selectionArgs = {""};
像这样把一个值置入选择参数数组中:
// Sets the selection argument to the user's input
selectionArgs[0] = mUserInput;
一个使用?作为占位符的选择条款+一个选择参数数组是指定一个选择器的最佳方式,即使provider不是基于SQL数据库的.
显示查询结果
客户端方法ContentResolver.query() 总是返回一个包含所查询的列们的Cursor .一个Cursor 对象提供了随机的读取它所包含的行和列的能力.使用Cursor 的方法们,你可以迭代结果中的行,决定每列的数据类型,从列获得数据,以及检测结果的其它属性.一些Cursor 的实现会在provider的数据改变时自动更新,或在Cursor 改变时触发监听者的方法,或者两者都支持.
注: 一个provider可能跟据构建查询的对象的性质限制对某些列的操作.例如,联系人Provider会禁止同步适配器操作某些列,所以它不会把它们返回给一个activity或service.
如果没有符合选择条件的行,provider返回一个Cursor 对象,其Cursor.getCount() 为0 (一个空cursor).
如果一个内部错误发生,查询结果会因provider的不同而不同.它可能返回null,也可能抛出一个Exception.
既然一个Cursor 是行组成的"列表",那么一个和显示Cursor 内容的好方法就是把它链接到一个ListView 上,通过SimpleCursorAdapter.
下面的代码片段是衔接前面的代码来的.它创建一个SimpleCursorAdapter 对象,包含有查询返回的Cursor ,然后设置这个对象为ListView的适配器.
// 定义要从Cursor取出的并要加载到view中的列们
String[] mWordListColumns =
{
UserDictionary.Words.WORD, // Contract class constant containing the word column name
UserDictionary.Words.LOCALE // Contract class constant containing the locale column name
};
// 定义一个View ID组成的列表,它们将接收每行的Cursor列的值
int[] mWordListItems = { R.id.dictWord, R.id.locale};
// Creates a new SimpleCursorAdapter
mCursorAdapter = new SimpleCursorAdapter(
getApplicationContext(), // The application's Context object
R.layout.wordlistrow, // ListView的一行的layout
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)
// 将适配器设置给ListView
mWordList.setAdapter(mCursorAdapter);
注:要使Cursor支持ListView,cursor必须包含一个叫做_ID的列,因此,上面所示的查询从"单词"表中取出了_ID ,当然ListView 可以不显示它.这条限制同时也解释了为毛大多数provider在它们的表中都具有一个_ID 列.
从查询结果中获取数据
你可以使用查询结果做更多是事情,而不是仅简单地显示它们.比如,你可以从用户词典中获取拼法然后在其它provider中查找它们.要这样做,你需在Cursor中迭代所有的行.
// 获取叫做"word"的列的序号
int index = mCursor.getColumnIndex(UserDictionary.Words.WORD);
/*
* 仅在cursor有效时执行下面语句.如果发生内部错误,用户词典Provider返回null.
* 其它provider可能抛出一个异常而不是返回null.
*/
if (mCursor != null) {
/*
* 移到cursor中的下一行.在第一次移动之前,
* "行指针" 为-1,并且,如果你想获取那个位置的数据,你将得到一个异常
*/
while (mCursor.moveToNext()) {
// 从列中获取值.
newWord = mCursor.getString(index);
// 在此插入代码处理获取到的单词
...
// 循环结束
}
} else {
// 如果cursor为null或前面抛出了异常,在处插入代码报告错误.
}
Cursor 的实现包含了多个"get" 方法,用于从对象中获取不同类型的数据.例如,前面的代码片段使用getString().它们也具有一个getType() 方法,用它可以返回的值代表了数据的类型.