content provider管理着对一个中央仓库式存储的数据的访问。provider是app的一部分,通常有自己的UI来提供数据。但是content providers主要是来给其它app使用的,其它app使用一个client对象来访问provider. 这样providers和provider clients一起就提供了一套标准一致的数据接口来处理进程间通讯和安全的数据访问。
本文主要讨论以下部分:
1.content providers是如何工作的。
2.从一个content provider中获取数据的API.
3.对一个content provider进行数据的插入,更新,删除操作的API.
4.其它对provider有帮助的API.
Overview
content provider以一张或多张表的形式向其它app来提供数据,这种表和关系型数据库的表类似。一行表示一个provider收集的数据类型的一个对象实例,一列表示每个对象实例的一部分数据。
比如,系统内置的一个provider是用户词典,它保存了用户想要保留的那些非标准单词的拼写。表1描述了provider的数据表中数据:
表1: 用户词典表的示例:
在表1中,每一行代表了一个单词的实例,列代表了单词的某一个数据,比如首次出现的地域。列标题是provider存储的列名称。对于这个provider来说,_ID是主键,provider自动维护。
备注:provider不是必须有主键的,即使有也不是必须使用_ID来作为主键。但是,如果我们想把provider的数据绑定到ListView,其中一列的名字必须是_ID.在 Displaying query results中我们会更加详细的解释。
访问一个provider
app使用一个ContentResolver的client对象来通过content provider来请问数据。这个对象里边的方法和provider对象(ContentProvider的子类对象)的方法同名。ContentResolver提供了持久化数据的基本的CRUD方法(创建,查询,更新,删除).
ContentResolver在client app的进程中,ContentProvider在拥有provider的app中自动进行进程间通讯。ContentProvider也是数据库和表格显示的数据之间的抽象层。
备注:为了访问provider,app通常需要在清单文件中声明一定的权限。参阅Content Provider Permissions来了解更多。
比如,为了从User Dictionary Provider得到一些单词和它们的地域,可以调用ContentResolver.query(). query方法会调用User Dictionary Provider定义的ContentProvider的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显示了参数是如何匹配到SQL语句中的查询语句。
表2,Query和SQL query的对比。
内容URI
一个content URI是标记provider中的数据的URI. Content URI包括整个provider的符号名称(它的授权)和一个指定一张表(路径)的名字。当我们调用client的方法来访问provider的表时,content URI是一个参数。
在前边的代码中,常量CONTENT_URI包含了用户词典的单词表的content URI。ContentResolver解析URI的授权,然后使用它来对比系统已知的provider的系统表来找到对应的provider。ContentResolver然后把对应的参数传递给正确的provider.
ContentProvider使用content URI的路径部分去查询正确的表。provider通常为每个公开的表都有一个路径。
前边的代码中,table的全部URI是:
content://user_dictionary/words
user_dictionary是provider的授权,words是表的路径。content://始终需要显示,表示这是一个content URI.
许多providers允许我们通常在URI的尾部追加ID来只查询一条记录。比如,为了查询_ID为4的那行,我们可以使用如下语句:
Uri singleUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);
当我们已经取得了一组数据,只想更新或者删除某一条记录时,我们也可以使用id.
备注:
Uri和Uri.Builder包含了许多便利的方法来创建一些格式规范的URI.ContentUris包含了一些便利的方法来给一个URI追加id.前边的使用withAppendedId来给URI追加id.
从provider中获取数据
这一部分会使用 User Dictionary Provider作为示例来讲解如何从provider中获取数据。
为了代码的简洁,ContentResolver.query()都是在主线程调用。但是在真实的开发中,我们应该在一个独立的线程中做异步查询。其中一个办法是使用CursorLoader,在Loaders中已经详细描述了。同时,代码只是片断,不是一个完整的app.
从provider中获取数据,遵循以下几步:
1.请求读取provider的权限。
2.定义代码发发送一个查询请求。
1.请求读取provider的权限
为了能够从provider中读取数据,app需要provider的y读取权限。我们不能在运行时申请权限,必须在清单文件中声明。声明时需要使用
为了能找到provider准确的读取权限名称,和其它的权限名称,请查询provider的文档。
参阅Content Provider Permissions来了解更多。
User Dictionary Provider定义了android.permission.READ_USER_DICTIONARY的权限,所以app如果想要读取必须声明这个权限。
2.构建查询
获取数据的下一步是构建一个请求。首先定义一些变量:
// 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 = {""};
下面以 User Dictionary Provider为例来说明如何使用ContentResolver.query()。一个provider clien的请求和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语句中,会使用真实的列名而不是类的常量。
防止恶意注入
如果provider管理的数据是存储在SQL数据库中,原始的SQL语句如果包含那些外部不受信任的数据会导致SQL注入。
考虑这样一个查询语句:
// Constructs a selection clause by concatenating the user's input to the column name
String mSelectionClause = "var = " + mUserInput;
如果我们这么做,就允许用户将恶意的代码注入我们的SQL语句中。比如,用户可以键入"nothing; DROP TABLE *;",这就会导致查询语句变成了var=noting;DROP TABLE *;因为这是一个SQL语句,所以可以让provider擦除所有的数据表(除非这个provider设置了捕获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;
即使provider不是基于SQL数据库的,使用?作为占位符加查询参数的查询语句也是更推荐的做法。
展示查询结果
ContentResolver.query()总是返回一个cursor,它包含了指定列的投影的符合查询规则的行。Cursor可以随机读取它包含的行和列。使用cursor的方法,我们可以在结果中遍历每一行,确定每一列的数据类型,获取列的数据和查询结果的其它属性。一些cursor还可以在provider数据变化时自动更新,或者在cursor变化时,触发一个观察者对身上的方法,也有一些两者都有。
备注:provider可能根据请求对象的性质来限制一些列的访问权限。比如,Contacts Provider对异步的adapter限制了一些列的访问,这样就不会把它们返回到activity或者service.
如果没有行符合选择的条件,provider会返回一个cursor对象,它的getCount为0(一个空的cursor).
如果发生内部错误,查询的结果由具体的provider来决定。有的会返回null,有的会抛出异常。
因为cursor是行的一个列表,所以展示cursor结果的一个很好的办法是通过一个SimpleCursorAdapter来绑定到一个ListView.
下边的代码是前边代码的后续。创建一个包含查询结果的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,cursor必须有一列是_ID.因此,前边的查询必须获取_ID这一列,即使ListView不去展示它。这个限制也解释了为什么provider的每张表都必须有一列_ID.
从查询结果中获取数据
不仅可以简单的展示查询结果,我们也可以用它们来做其它工作。比如,我们可以从用户的dictionary中获取拼写然后用其它的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.cursor也提供了一个getType方法来获取返回值当列的类型。
content provider权限
provider的app可以指定其它app访问自己需要的权限。这些权限保证金了用户知道app想要访问哪些数据。根据provider的需要,其它app为了能够访问就需要请求这些权限。最终用户会在安装app时看到这些权限。
如果一个provider不指定任何权限,其它的app就无法访问。但是,无论指定的权限量什么,provider app内部的其它组件都有完全的读写权限。
前边已经提到,User Dictionary Provider需要android.permission.READ_USER_DICTIONARY 读的权限来获取数据,需要android.permission.WRITE_USER_DICTIONARY的权限来做插入,更新,和删除的操作。
为了获取访问provider的权限,app需要在清单文件中使用
下边是使用
参阅Security and Permissions来了解更多。
增删改数据
从provider请求数据类似,我们也可以使用provider client和provider的ContentProvider之间的这种交互来修改数据。调用ContentResolver的有参方法然后传递给对应的ContentProvider的方法。provider和provider client就可以自动处理安全和进程间通讯。
插入数据
调用ContentResolver.insert()可以插入数据。这个方法可以插入新的一行并返回那行的content URI.下边的代码展示了如何插入一个新的单词到 User Dictionary Provider:
// 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对象,它和单行的cursor格式类似。这个对象的列的值不需要数据类型相同,如果我们不想对某一列赋值可以使用ContentValues.putNull()来把这它置空。
代码中并没有添加_ID,因为这一列是由系统自动维护的。provider会给每一行自动添加一个惟一的_ID. provider通常使用这个值作为表的主键。
返回的content URI表明了新添加的这一行,使用如下的格式:
content://user_dictionary/words/
调用ContentUris.parseId()可以解析出URI的_ID。
更新数据
按照插入的方式来使用ContentValues更新行,查询的方式来使用选择条件这样我们就可以更新一行数据。我们使用的客户端方法是 ContentResolver.update().只需要为我们更新的列来添加一些值。如果我们想要清除列的值,设置值为null即可。
下边的代码改为了所有locale为en的行为locale为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时还需要检查用户的输入。参阅 Protecting against malicious input来了解更多。
删除数据
删除数据和获取数据类似:指定删除条件,客户端方法会返回删除的行数。下边的代码删除了appid包含"user"的行。返回了删除的行数。
// 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.update时还需要检查用户的输入。参阅 Protecting against malicious input来了解更多。
provider的数据类型
Content providers提供了多种不同的数据类型。User Dictionary Provider只提供了文本,provider也可以提供下边的格式:
integer
long integer (long)
floating point
long floating point (double)
另一个常用的数据类型是二进制大型对象(BLOB),它是一个64KB的字节数组。通常查看Cursor类的get方法我们可以看到可用的数据类型。
provider每行可用的数据类型通常在文档中列出。User Dictionary Provider的数据类型在它的协定类UserDictionary.Words中定义。也可以通过调用Cursor.getType()来确定数据的类型。
provider也为每个定义的content URI保留了一个MIME数据类型。我们可以使用MIME来确定我们的app是否可以处理provider提供的数据或者根据MIME来选择一个处理方式。通常当我们处理包含复杂数据结构或者文件的provider时,需要使用MIME类型。比如,Contacts Provider中的ContactsContract.Data表使用MIME来标记每一列的数据类型。调用ContentResolver.getType()可以得到content URI对应的MIME类型。
MIME Type Reference 讨论了标准和自定义MIME的语法。
访问Provider的其它方式
在app开发中,我们还有其它三种替代方式可以访问provider:
1.批量访问:我们可以通过ContentProviderOperation中的方法来创建一组的访问请求,然后通过ContentResolver.applyBatch()应用它们。
2.异步查询:可以在独立的线程中进行查询。一种方法是使用CursorLoader,Loaders中的示例。
3.通过Intent来访问数据:虽然我们不能直接向provider发送一个intent,但是我们可以向provider的app发送一个intent,通常这样可以修改provider的数据。
批量访问和通过intent来修改会在后续进行讨论。
批量访问
批量访问对于大批量插入或者一个方法要在多张表同时进行插入,或者进行一组跨进程的操作的事务(原子操作)都是非常有用的。
想要进行批量模式的访问,我们创建一个ContentProviderOperation的数组,然后通过 ContentResolver.applyBatch()来把它分发给一个provider.传递provider的权限给这个方法,而不是一个特定的URI。这样就可以让每个ContentProviderOperation可以对任意表进行操作。调用ContentResolver.applyBatch()返回一组结果。
ContactsContract.RawContacts的协定类包含了展示批量插入的代码。 Contact Manager 有源码示例。
使用帮助app来展示数据
如果我们的app有权限,我们也可以用Intent来在其它app中展示数据。比如,Calendar的app接收ACTION_VIEW的intent,来展示一个日期或事件。这可以让我们不使用自己的UI来展示日历。参阅 Calendar Provider来了解更多。
我们发送Intent的目标app不需要和provider关联。比如,可以从Contact Provider获取一个联系人,然后发送一个包含了这个联系人图片的content URI的ACTION_VIEW intent给一个图片查看器。
通过Intent来访问数据
intent可以提供间接的provider数据访问。即使我们的app没有权限,也可以让用户访问数据,要么通过一个拥有权限的app来返回,或者启动一个有权限的app让用户进行操作。
使用临时权限来访问数据
即使我们没有权限,可以通过发送一个intent到一个有权限的app去获取一个包含了URI权限的结果intent. 这些指定content URI的权限会持续到接收他们的activity 销毁。有永久权限的app通过在结果intent中设置flag来授予临时的权限:
Read permission: FLAG_GRANT_READ_URI_PERMISSION
Write permission: FLAG_GRANT_WRITE_URI_PERMISSION
备注:这些标记不会给予provider的通用的读写权限,只是那些URI的权限。
provider在清单文件中为content URI来定义URI权限,具体是在
比如,即使我们没有 READ_CONTACTS permission权限,我们也可以从Contacts Provider获取数据。我们可能需要在app中向一个联系人发送一个生日祝福。与其请求READ_CONTACTS可以让我们访问用户的所有联系人,我们也可以让用户控制选择一个联系人。过程如下所示:
1.我们的app发送一个包含了ACTION_PICK和CONTENT_ITEM_TYPE的intent,调用startActivityForResult方法。
2.因为这个intent会匹配到联系人的app的选择activity的intent filter,这个activity会切换到前台。
3.在选择activity中,用户选择一个联系人。之后,activity会调用 setResult(resultcode, intent) 来创建一个intent返回给我们的app. 这个intent包含了用户选择的联系人的content URI和FLAG_GRANT_READ_URI_PERMISSION。这些flags授予我们app通过这个content URI去读取联系人的权限。选择activity会调用finish来把控制权交还给我们的app.
4.我们的app返回前台,系统调用onActivityResult. 这个方法会接收到联系人APP的选择activity创建的结果intent.
5.通过结果intent中的content URI,我们可以从Contacts Provider中读取到联系人的数据,即使我们没有在清单文件中请求永久性的读取权限。我们可以获取到联系人的生日信息和邮箱然后发送问好 的邮件。
使用其它应用
没有权限最简单可以让用户去修改数据的方式是启动一个有权限的app,然后让用户去操作它。
比如,Calendar app接收ACTION_INSERT的Intent,这样我们可以启动app的插入UI.传递一些extras在Intent中,app就可以使用这些extra来预创建一个UI.因为创建一个事件的语法非常复杂,所以更推荐的方法是使用ACTION_INSERT来启动Calendar app 让用户自己去插入事件。
Contract Classes
协定类定义了一些常量来方便app和content provider的 content URI,列名称,intent actions和其它的特点打交道。协定类不会自动化包含到一个provider, 开发者必须定义然后给其它的开发者使用。系统的很多provider都在android.provider包下有对应的协定类。
比如, User Dictionary Provider有一个UserDictionary的协定类,包含了content URI和列名的常量。words表的content URI是UserDictionary.Words.CONTENT_URI. UserDictionary.Words也包含了列名的常量(示例代码中)。比如,一个查询可以定义如下:
String[] mProjection =
{
UserDictionary.Words._ID,
UserDictionary.Words.WORD,
UserDictionary.Words.LOCALE
};
另外一个协定类是Contacts Provider的ContactsContract. ContactsContract.Intents.Insert是一个intents和intent data的协定类。
MIME Type Reference
Content providers 会返回标准的MIME媒体类型,或者自定义的string类型,或者都返回。
MIME types有如下格式:
type/subtype
比如,最常见的text/html是文本类型和html的子类型。如果provider返回这种类型的URI,表示一个使用这种URI的查询会返回包含HTML标签的text。
自定义的string类型,也叫做vendor-specific,有着更复杂的类型和子类型。这些类型有:
vnd.android.cursor.dir
多行
vnd.android.cursor.item
单行
子类型是由provider确定的。系统的provider通常是一个简单的子类型。比如,当Contacts创建了一行来记录一个电话,会设置如下的MIME:
vnd.android.cursor.item/phone_v2
注意,子类型只是phone_v2.
其它的provider开发者也可以根据provider的权限和表的名称来创建自己的子类型。比如,一个包含了列车时刻表的provider.这个provider的授权是 com.example.trains,它的表是Line1, Line2, 和 Line3。响应的content URI
content://com.example.trains/Line1
时,provider返回MIME type
vnd.android.cursor.dir/vnd.example.line1
响应 content URI
content://com.example.trains/Line2/5
```时,返回的
vnd.android.cursor.item/vnd.example.line2
大多数的content provider都会为他们使用的MIME定义协定类。联系人的协定类是ContactsContract.RawContacts,它为每一行的数据都定义了CONTENT_ITEM_TYPE。
Content URIs 介绍了单个行的内容 URI。