Content Providers
Content providers是用来管理对结构化数据集进行访问的一组接口。这组接口对数据进行封装,并提供了用于定义数据安全的机制。Content providers是一个进程使用另一个进程数据的标准接口。
当要使用content provider访问数据时,我们需要在应用程序的Context中使用ContentResolver对象作为客户端,同provider进行通信。与provier对象通信的ContentResolver对象是ContentProvider类的一个实例。provider对象接收从客户端发来的数据,执行请求的动作并返回结果。
如果你不打算同其他应用程序共享数据,就没必要实现provider。但是,如果希望在自己的应用程序中搜索建议的功能,就需要实现自己的provider。同样的,如果希望在自己的应用程序和其他的应用程序间拷贝粘贴复杂的数据或文件,也需要实现自己的provider。
Android系统本身也通过content providers来管理数据,如音频,视频,图像,个人联系信息等。我们可以在android.provider包的参考文档中看到这些providers列表。在一定条件下,这些providers能够访问任何Android应用程序。
接下来,将就content providers的以下题目做详细说明:
-
Content Provider 基本介绍 - Content Provider Basics
-
当数据以表的方式组织时,如何通过content provider访问数据。
创建Content Provider - Creating a Content Provider
-
如何创建自己的content provider。
日历Provider - Calendar Provider
-
如何访问Android平台的Calendar Provider。
Content Provider 基本介绍 - Content Provider Basics
Content provider管理对于中央数据库的访问。而provider是Android应用程序的一部分,通常每个provider自己提供界面同数据交互。然而,content providers最主要的目的是为了让其他应用程序使用provider的客户端对象访问provider。所以,providers和provider客户端共同提供了一个一致的标准数据接口用来处理进程间通信和安全的数据访问。
本节主要介绍以下几个方面的内容:
- Content providers是如何工作的。
- 如何使用API从content provider中接收数据。
- 在content provider中如何使用API进行数据的插入,更新或删除。
- Providers提供的其他API功能,以便更好的使用providers。
概述-Overview
Content provider以类似于关系数据库中一个或多个表的方式给外部的应用程序提供数据。一行代表provider收集的一些类型数据的一个实例,一列中的每一行代表数据中某个类型的一个实例。
例如,用户字典就是Android平台内置的providers中的一个,用来保存用户希望保存的非标准词的拼写。表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中,每行代表了在标准字典中可能找不到的一个词的实例。每一列表明这个词的一些数据,例如它第一次出现时的语言环境。列标题是存储在provider中的列名。为了找到一行的语言环境,你就需要索引到locale这个列所指向的内容。对于此provider来说,_ID列作为主键提供自动索引。
注:主键对于provider来说不是必须的,即使有主键,provider也不必一定要使用_ID作为主键的列名。但是,如果希望将provider的数据绑定到ListView上,就要使用_ID作为一列的名字。这些会在显示查询结果的小结中详细介绍。
访问 provider - Accessing a provider
应用程序使用ContentResolver客户端对象来访问content provider的数据。ContentResolver对象与ContentProvider的一个具体子类的实例拥有相同名字的接口。ContentResolver的方法提供了基本的‘CRUD’(创建,检索,更新和删除)数据存储的功能。
客户端应用程序进程中的ContentResolver对象和我们自己的应用程序中的ContentProvider对象会自动处理进程间通信。ContentProvider也会作为其存储的数据和数据以表的形式展现之间的抽象层。
注:为访问provider,应用程序通常需要在manifest文件中添加特定的权限。这些将在Content Provider 权限小结中详细介绍。
举例来说,为了从用户字典的Provider中获得单词和它们出现的语言环境列表,就需要调用ContentResolver.query()方法。query()方法会调用在用户字典的Provider中定义的ContentProvider.query()方法。下面这行代码展示了ContentResolver.query()是如何调用的:
// Queries the user dictionary and returns results
mCursor = getContentResolver().query(
UserDictionary.Words.CONTENT_URI, // 词表的内容URI
mProjection, // 每行中返回数据的列的名称,
// null表示返回所有列的数据。
mSelectionClause // 过滤条件
mSelectionArgs, // 过滤条件的参数
mSortOrder); // 返回行的排序方式
表2介绍了函数query(Uri,projection,selection,selectionArgs,sortOrder)的参数同SQL SELECT语句之间的关系:
表2:Query()函数与SQL的查询语句间的对应关系
query() argument | SELECT keyword/parameter | Notes |
---|---|---|
Uri | FROM table_name | provider中的Uri相当于表名。 |
projection | col,col,col,... | projection 是一个列名的数组,规定了查询结果中每行应该包含哪些列。 |
selection | WHERE col = value | selection 指定了符合条件的行。 |
selectionArgs | (No exact equivalent. Selection arguments replace ? placeholders in the selection clause.) | |
sortOrder | ORDER BY col,col,... | sortOrder 指定了在返回结果Cursor中行排序的规则。 |
内容 URI - Content URIs
内容URI是provider中用来唯一标识数据的URI。内容URIs包括整个provider的符号名称(它的权威)和指向表的名字(路径)。当调用客户端的方法来访问provider中的一个表时,这个表的内容URI会作为这个方法一个参数。
在前面的代码中,常量CONTENT_URI包含用户词典中的词表的内容URI。ContentResolver对象解析出URI的权威,并将其与系统已知provider的权威表比较,从而获取对应的provider。然后,ContentResolver就可以将查询参数发送给正确的provider。
ContentProvider根据内容URI中的路径部分选择需要访问的表。Provider通常为每个表都公开一个路径。
在前面的程序中,“词”表的完整URI如下所示:
content://user_dictionary/words
这里的user_dictionary是provider的权威,words是表的路径。content://(大纲)是一定要有的,作为内容URI的唯一标识。
许多provider允许通过在URI结尾追加一个ID值来访问表中的单独一行。例如,从用户词典中接收_ID为4的那行数据,就要使用如下的内容URI:
Uri singleUri = ContentUri.withAppendedId(UserDictionary.Words.CONTENT_URI,4);
当我们希望查询多行数据,然后更新或删除其中一行的数据时,就会经常用到id值。
注:Uri和Uri.Builder类包含了从字符串构建Uri对象的一些方法。ContentUris包含了将id值追加到URI结尾的方法。前面的程序段使用withAppendedId()方法将id追加到用户词典的内容URI后面。
从Provider中检索数据 - Retrieving Data from the Provider
本节介绍了如何从provider中检索数据,使用用户词典的Provider来举例说明。
为了方便起见,本节中的代码会在“UI线程”中调用ContentResolver.query()函数。但是,在实际代码中,应该将查询操作放到一个单独的线程中异步执行。一种方法就是使用CursorLoader类,这个类的详细说明可以在Loaders手册中查到。此外,例子代码仅仅是一部分,它们并没有显示完整的应用程序。
为了从provider中检索数据,请遵循以下的基本步骤:
-
- 获得provider的读访问权限。
- 定义发送给provider的查询代码。
获得读访问权限-Requesting read access permission
为了从provider中查询数据,应用程序必须有此provider的读访问权限。该权限只能在应用程序的manifest文件中指定,而不能在运行时获得,使用<uses-permission>标签,并指明该provider定义的此权限的确切名称。当在应用程序的manifest文件中指定了该标签,实际上是要求为您的应用程序赋予此权限。当用户安装你的应用时,会隐式的赋予此权限。
要知道你使用的provider的读访问权限和其他权限的确切名字,请参考provider的文档。
在 Content Provider Permissions中详细说明了访问provider的各个权限的作用。
用户词表的Provider在它的manifest文件中定义了android.permission.READ_USER_DICTIONARY权限,所以如果一个应用程序希望从provider中读取信息,就必须获得此权限。
构建查询程序 - Constructing the query
从provider中检索数据的下一步就是构建查询程序。程序的第一部分定义了访问用户词典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
};
// 定义一个字符串用来包含selection的条件。
String mSelectionClause = null;
// 初始化数组包含selection的参数。
String[] mSelectionArgs = {""};
接下来的程序以用户词典的Provider为例,显示如何使用ContentResolver.query()。Provider客户端查询类似于SQL查询,它包含一组返回的列值,一组查询条件和排序规则。
查询返回的列的集合被称作一个projection(即变量mProjection)。
检索指定行的条件表达式被分成selection语句和selection参数。selection语句是由逻辑和布尔表达式,列名和值(变量mSelection)组成的。如果指定的是替换参数?而不是值,查询方法会从selection参数数组(变量mSelectionArgs)中检索对应的值。
下一段程序中,如果用户没有输入任何单词,selection语句被设置成null,查询会返回provider中所有的单词。如果用户输入一个单词,查询语句被设置成UserDictionary.Words.Word + " = ?",同时selection参数数组中的第一个元素被设置成用户输入的单词。
/*
* 这里定义了只有一个元素的字符串数组用来包含 selection 的参数。
*/
String[] mSelectionArgs = {""};
// 从UI获得单词
mSearchString = mSearchWord.getText().toString();
// 这里记着要添加错误检查。
// 如果输入的单词为空,返回所有词。
if (TextUtils.isEmpty(mSearchString)) {
// 设置selection语句,如果为空返回所有单词
mSelectionClause = null;
mSelectionArgs[0] = "";
} else {
// 构建selection语句来匹配用户输入的单词
mSelectionClause = " = ?";
// 将用户输入的单词放到selection的参数列表中
mSelectionArgs[0] = mSearchString;
}
// 查询表,并返回一个Cursor对象
mCursor = getContentResolver().query(
UserDictionary.Words.CONTENT_URI, // 单词表的URI
mProjection, // 返回结果中每行包含的列
mSelectionClause // 用户输入的单词或null
mSelectionArgs, // 用户输入的字符串或null
mSortOrder); // 返回行的排序规则
// 一些provider在查询发生错误后会返回null,其他的会抛出异常
if (null == mCursor) {
/*
* 这里需要添加你的错误处理程序,可以调用
* android.util.Log.e()函数输出日志
*
*/
// 如果Cursor为空,表明provider中没有匹配条件的结果
} else if (mCursor.getCount() < 1) {
/*
* 添加代码通知用户查询未成功。这些不一定是错误。可以供用户选择是插入
* 新的一行还是重新输入检索词。
*/
} else {
// 插入对查询结果的操作
}
此查询类似于下面的SQL语句:
SELECT _ID, word, frequency, locale FROM words WHERE word = <userinput> ORDER BY word ASC;
在这条SQL语句中用实际的列名替换了contract类的常量。
防止恶意输入 - Protecting against malicious input
如果content provider管理的数据保存在SQL数据库中,在数据库中包含了外部不信任的数据,可能会引起SQL注入的风险。
考虑下面的selection语句:
// 构建一个selection语句直接将用户输入连接到列名后面
String mSelectionClause = "var = " + mUserInput;
如果我们这样写程序,将允许用户把恶意的SQL语句连接到你的SQL语句中。例如,用户可以在变量mUserInput中输入“nothing; DROP TABLE *;”,会导致selection语句变为var = nothing; DROP TABLE *;
。因为selection语句会作为SQL语句,所以这条语句可能会导致provider删除底层SQLite数据库中的所有表(除非provider建立了捕获SQL注入的机制)。
为了避免SQL注入,在selection语句中使用?作为替代参数,并使用一个单独的selection参数数组。当使用参数的时候,用户的输入被直接添加到查询中,而不是被解释为SQL语句的一部分。因为它不是作为SQL处理,用户输入不能注入恶意的SQL。使用selection语句参数而不是直接将用户的输入连接起来的方式如下:
// 构建一个用参数替代的selection语句
String mSelectionClause = "var = ?";
用如下方式建立selection的参数数组:
// 定义一个包含selection的数组
String[] selectionArgs = {""};
以如下方式将一个值放入selection的参数数组中:
// 将用户的输入添加到selection的参数数组中
selectionArgs[0] = mUserInput;
一个使用?作为替代参数并使用了selection参数数组的selection语句是使用selection的首要方式,即使provider并不是基于SQL数据库。
显示查询结果 - Displaying query results
客户端的ContentResolver.query()方法会返回一个Cursor对象,包含了满足查询条件的行及由查询的projection指定的列。一个Crusor对象为它所包含的行和列提供了随机读取的访问。使用Cursor的方法,可以遍历查询结果中的所有行,确定每列的数据类型,获取一列的数据,及检验结果的其他属性。当provider的数据改变时,有些Crusor实现了自动更新的功能,或者当Crusor改变时,会触发一个监听对象的方法,或者两者兼而有之。
注:provider可能会基于查询对象本身而限制对一些列的访问。例如,联系人的Provider会限制对同步适配器的一些列的访问,所以这些列将不会返回给activity或service。
如果没有行能匹配selection条件,provider会返回一个Cursor.getCount()数量为0的Cursor对象(一个空的cursor)。
如果发生内部错误,查询的结果取决于具体的provider。有的会返回null,有的会抛出异常
因为Cursor是所有行的列表,所以显示Cursor内容的一个好的方式是使用SimpleCursorAdapter将其连接到ListView上。
下面的代码延续了前面的程序。创建了一个SimpleCursorAdapter对象,其中包含了查询结果的Cursor,并将此对象设置为一个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,就需要此cursor包含一个名为_ID的列。正因为这样,前面对单词表的查询会收到_ID这一列,即使ListView并没有显示该列。此限制条件也说明了为什么大多数providers会在它们的每个表中包含一个_ID列。
从查询结果中获得数据 - Getting data from query results
我们还可以将查询结果用于别的任务,而不是仅仅将其显示出来。例如,你可以从用户词典中检索出拼写,然后在其他的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()方法获得一个值,显示该列的数据类型。
Content Provider 权限-Content Provider Permissions
提供provider的应用程序可以指定访问权限,以便其他应用能够访问provider提供的数据。此权限可以保证用户能够知道应用程序中的那些数据可以访问。基于provider提供的说明,其他的应用程序可以根据自身的需求来请求权限去访问provider。最后,用户在安装此应用时会看到该应用请求获得的权限。
如果包含provider的应用没有指定任何权限,其他的应用程序是无法访问该provider的数据的。但是包含provider应用的其他组件拥有对该provider的完全读写权限,无论是否指定了权限。
正如上面所说,用户字典的Provider要android.permission.READ_USER_DICTIONARY权限来控制对其数据的访问。此provider还提供了android.permission.WRITE_USER_DICTIONARY权限控制对数据的插入,更新和删除。
为获得访问provider的权限,应用程序在manifest文件中需要使用<uses-permission>标签。当Android Package Manager 安装应用时,用户必须批准应用程序的所有权限请求。如果用户允许了,Package Manager会继续安装流程;如果用户不允许,Package Manager会终止安装。
下面的<uses-permission>标签请求获取用户词典Provider的读权限:
<uses-permission android:name="android.permission.READ_USER_DICTIONARY">
在安全性和权限中会详细介绍provider访问权限的影响。
插入,更新和删除数据 - Inserting, Updating, and Deleting Data
同从provider获取数据的方式一样,我们也要使用在provider的客户端和ContentProvider之间的交互来修改数据。调用ContentResolver的方法并传入参数,最终会调用ContentProvider的对应方法。provider和provider客户端会自动处理安全性和进程间通信。