原文:https://developer.android.google.cn/guide/topics/providers/content-provider-basics.html
Content Provider管理对中央数据仓库的访问,它是Android应用的一部分,通常会提供自己的UI来使用数据。然而,Content Provider主要是为了给其他应用使用而设计的,这些应用可以通过Provider客户端程序访问内容提供者。Provider和Provider客户端共同提供了一个稳定的、标准的访问数据的接口,并且也很好地解决了进程间通信和安全访问数据的问题。
一般来说,你会在这两种场合下使用Content Provider:想要访问其他程序中一个已有的Content Provider,或是想要在应用中创建一个新的Content Provider来和其他应用分享数据。
本文主要讲述了以下几点:
Content Provider利用若干张表来向外部应用提供数据,这些表的形式类似于关系型数据库中的表。表中的每行代表Provider收集的某类数据的一个实例,每列代表数据的一个部分。
Content Provider通过众多的API和构建管理到应用的数据存储层的访问,包括:
当你想要访问Content Provider中的数据时,你需要使用你的应用的Context中的ContentResolver对象,它可以作为一个客户端来与Provider进行交流。和ContentResolver对象进行交流的是ContentProvider的实现类的一个实例。这个Provider实例从客户端接收数据请求,执行被请求的行为,并最终返回结果。ContentResolver类提供了对永久存储的基础的增删改查的方法。
使用CursorLoader执行异步查询是从UI访问 ContentProvider对象的一个常用模式。UI中的Activity或者Fragment通过CursorLoader执行查询操作,并最终利用ContentResolver获得ContentProvider对象。这让你的UI在查询过程中也能和用户交互。这个模式涉及到一系列的对象,如下图所示:
注意:想要访问Provider,你的应用通常需要在manifest文件中声明相应的权限。
用户词典是Android平台的一个内嵌的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中,每行代表一个单词,这个单词可能不能在标准字典中找到。每列代表这个单词的某些方面的信息,比如首次遇到的地点。每列的头是它们在Content Provider中的列名。这个Provider中,_ID列充当了主键的角色,它是Provider自动保持的。
为了从用户词典中获取单词和它们的地点,你需要调用ContentResolver.query()。这个方法会调用用户词典Provider定义的ContentProvider.query()方法。下面是使用ContentResolver.query()的一个示例:
// 查询用户词典并返回结果
mCursor = getContentResolver().query(
UserDictionary.Words.CONTENT_URI, // 单词表的内容URI
mProjection, // 需要返回的列
mSelectionClause // 选取准则
mSelectionArgs, // 选取准则
mSortOrder); // 行间的排序方法
下面介绍 query(Uri,projection,selection,selectionArgs,sortOrder) 方法的参数是如何和SQL SELECT语句匹配的:
内容URI是一个标识Provider中的数据的URI,它包括整个Provider的符号名(授权机构)、指向的表的名称(路径)。当你调用客户端方法访问Provider中的一张表时,内容URI是一个必要的参数。
在前面的代码中,CONTENT_URI常量包含了用户词典的内容URI。ContentResolver对象会分析出URI的授权机构,并用它来解析出对应的Provider。之后,ContentResolver就能将查询分发到正确的Provider处。
ContentProvider通过内容URI的路径部分选择需要访问的表。Provider一般会为每张表准备一个路径。
在前面的代码中,用户词典完整的URI为:
content://user_dictionary/words
user_dictionary代表授权机构,words代表路径。content://代表协议,用于标明这是一个内容URI。
许多的Provider允许你通过在URI的最后追加一个ID值来访问某个单独的行。比如,如果你想要访问_ID==4的行,可以这么做:
Uri singleUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);
注意:Uri与Uri.Builder类包含了一些用于构建形式良好的Uri对象的方法。 ContentUris类包含了为URI追加ID的方法,比如上面的withAppendedId()。
这部分描述了如何从Provider中获取数据,同样使用用户词典的例子。
注意:为了保持简洁,这部分的代码片段都在UI线程中调用了 ContentResolver.query()方法。在实际的程序中,你应当在一个单独的线程中执行异步查询。一种实现的方法是使用CursorLoader类。并且,这里演示用的代码都只是一些片段,不是一个完整的应用。
想要从Provider中获取数据,需要以下两个步骤:
为了从Provider中获取数据,你的应用需要该Provider的读取访问权限。这个权限不能在运行时获取,只能在Manifest文件中通过
为了找到你需要使用的Provider所需的权限的名称,你需要去查看这个Provider的文档。例如,用户词典Provider需要的权限名为android.permission.READ_USER_DICTIONARY
,如果你想要读取用户词典的内容,就必须在Manifest文件中声明该权限。
获取数据的下一步是构建查询代码。下面的代码片段定义了一些用于访问用户词典的变量:
// 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()。Provider的客户端查询代码和SQL语句非常接近,它包含了需要被返回的列的集合、选取准则和排序方法。
需要被返回的列的集合也称为投影(projection),对应mProjection变量。
SELECT从句和SELECT参数定义了需要被返回的行的特征。SELECT从句是一系列逻辑和布尔表达式、列名称和值(mSelectionClause)的组合。如果你指定了占位符‘?’而不是具体值,查询方法会从SELECT参数数组(mSelectionArgs)中获得值。
下面的片段中,如果用户没有输入一个单词,SELECT从句就会被设为null,并且此次查询会返回用户词典中的所有单词。如果用户输入了一个单词,SELECT从句就会被设为UserDictionary.Words.WORD + ” = ?” ,并且SELECT参数数组的第一个元素会被设为用户输入的单词。
/*
* 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;
如果该Provider使用一个SQL数据库来管理数据,那么将不受信任的数据包含在原始SQL语句中会导致SQL注入。
考虑这样的SELECT从句:
// Constructs a selection clause by concatenating the user's input to the column name
String mSelectionClause = "var = " + mUserInput;
如果这么做,用户将拥有将恶意的SQL语句和你的SQL语句串联起来的能力。比如,用户可以输入“nothing; DROP TABLE ;”作为mUserInput的值,这会导致最终的SQL语句变为“var = nothing; DROP TABLE ;”。由于SELECT从句会被作为SQL语句对待,这可能会导致Provider删除数据库中的所有表(除非Provider设置了防止SQL注入的方法)。
为了避免这个问题,需要在SELECT子句中使用“?”占位符,并提供一个单独的SELECT参数列表。这样做的话,用户的输入会和查询绑定起来,而不会被视为SQL语句的一部分。由于不会被当成SQL语句,自然也就避免了SQL注入问题。上面的例子可以用这样的SELECT子句代替:
// Constructs a selection clause with a replaceable parameter
String mSelectionClause = "var = ?";
SELECT参数数组:
// Defines an array to contain the selection arguments
String[] selectionArgs = {""};
可以这样来向SELECT参数数组中添加值:
// Sets the selection argument to the user's input
selectionArgs[0] = mUserInput;
使用SELECT子句搭配SELECT参数的方法要比直接指定SELECT语句
好,即使Provider使用的不是SQL数据库。
客户端方法ContentResolver.query()会返回一个Cursor,它包含投影指定的列以及满足选取准则的行。Cursor提供了对各行各列的随机访问。使用Cursor提供的方法,你可以遍历结果的所有行,确定每列的数据类型,从各列中获得数据,并且检查结果的其他属性。一些Cursor实现能够在Provider的数据改变时自动更新,或者在Cursor发生变化时触发某些观察者中的方法。
注意:一些Provider可能会限制对它的某些列的访问,基于进行查询的对象。
如果没有任何行能满足选取准则的要求,那么Provider会返回一个空Cursor对象,调用它的getCount()方法会返回0。
如果出现内部错误,那么查询的结果会根据具体的Provider而定。可能会返回null或者抛出异常。
由于Cursor通常包含许多行,可以利用ListView来展示它的内部数据,通过SimpleCursorAdapter。
接着上面的代码片段,下面的片段中创建了一个包含查询结果的 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列,ListView没有把它展示出来。这也是许多Provider提供了_ID列的原因。
除了简单地将查询结果展示出来之外,你也可以在其他任务中使用它们。比如,你可以从用户词典中获取单词拼写,并在其他Provider中进行查询。为了这么做,你需要遍历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对象实现了一系列的get方法,用于从该对象中获取不同类型的数据。上面的代码中使用的是getString(),你也可以用getType()来获取某列的数据类型。
Provider所属的应用可以指定一系列的权限,其他应用在获取数据之前必须要获取这些权限。这些权限保证用户能够知道某个应用正在获取什么类型的数据。用户会在安装应用时了解到应用需要哪些权限。
如果Provider没有声明任何权限,那么除了它的内部构件之外,任何应用都无法获得它的内部数据。
比如,想要从用户词典中获取数据的话,需要 android.permission.READ_USER_DICTIONARY权限,而想要进行增删改操作的话还需要 android.permission.WRITE_USER_DICTIONARY权限。
权限需要在Manifest文件中声明:
<uses-permission android:name="android.permission.READ_USER_DICTIONARY">
这些权限会在安装应用时向用户申请。除非用户全部同意,否则安装不会继续进行。