[译]Content Provider

Content Provider

Content Provider管理结构化数据集的入口。封装数据并提供定义数据安全的机制。Content Provider是进程间数据共享的标准接口。

当你想要使用一个Content Provider来访问数据,你使用Application的Context中的ContentResolver对象作为一个client与provider(实现ContentProvider的类实例)交互。此provider接收client端的数据请求,执行请求并返回结果。

如果你不想与其他Application共享你的数据,你不需要开发你自己的provider。然而,你要为你的Application提供search suggestion(全局搜索)时你需要实现自己的provider。当你想要从你的应用复制/粘贴复杂数据或文件到另一个应用时你需要实现自己的provider。

Android本身就包含一些管理数据(诸如audio,video,image或是contacts)的Content Provider。可以查看android.package包的文档。任何Application都可以访问这些provider。

Content Provider Basics

Content Provider管理数据仓库的入口。Provider是Android Application组件之一,通常被用来为UI提供数据。但是,Content Provider的主要目的是被其他Application使用(通过一个provider client)。Provider和Provider client一起提供一个一致的,标准的数据访问接口,且可以处理进程间通信和数据安全访问。

以下议题包括:

  • Content Provider如何运作
  • 用来通过Content Provider获取数据的API
  • 用来在Content Provider中insert,delete,update数据的API
  • 其他一些有利provider运作的API

Overview

Content Provider以一个或多个表(类似于关系型数据库中创建的表)的形式为其他Application展示数据。每一行表示一个数据实例,每一列表示一个数据实例的一个单独的部分。

比如,一个Android平台内置的provider—–用户词典,它保存在用户需要的一些不标准的拼写。如下是其中的表:

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
表中,每一行表示一个在标准词典中可能找不到的单词数据实例。每一列表示该单词的一些数据,比如属地。列标题是在provider中存储的列名。在此provider中,_ID作为主钥“primary key”列,provider会自动维护。

一个provider不要求必须有一个primary key,且如果有也不要求一定要使用_ID来作为列名。但是,如果你要将此provider的数据bind到一个ListView,就必须有一列为_ID。具体解释在Displaying query results章节有说明。

Accessing a provider

一个Application使用ContentResolver对象来访问一个Content Provider的数据。此对象的方法会调用provider对象(一个ContentProvider子类的实例)的同名方法,ContentResolver提供了基本的“CRUD”(create,retrieve,update,delete)方法操作持久型数据。

在客户端Application进程的ContentResolver和持有provider的Application中的ContentProvider之间会自动处理进程间通信(IPC)。ContentProvider同样扮演数据仓库和数据表形式之间的一个抽象层。

要访问一个Provider,你的Application通常需要在manifest文件中请求指定的权限(permission)。将会在Content Provider Permissions章节详细介绍。

例如:要获取用户词典Provider中的word和local的列表,你可以调用ContentResolver.query()。此query会调用用户词典Provider中定义的ContentProvider.query()。如下:

1
2
3
4
5
6
7
// 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

下表展示了query(Uri,projection,selection,selectionArgs,sortOrder)方法中参数的对应SQL语句中的意思:

query()参数 SELECT语句的关键字及条件 说明

Uri|FROM table_name|对应要查询的表名。
projection|col,col……|要查询的列名(字段)。
selection|WHERE col=value|查询条件
selectionArgs|Selection中的?对应value|查询条件中对应的?
sortOrder|ORDER BY col…|排序方式

Content URIs

Content URI是一个标识provider中数据的URI。Content URI包含一个provider的标识名(authority)和一个指定table的名字(path)。当你调用一个方法去访问一个provider中的table时,要包含一个指定table的Content URI作为方法参数。

在之前的代码中,常量CONTENT_URI表示用户词典Provider中的“word”表。ContentResolver对象解析出此URI的authority,通过此authority去匹配系统中已知的provider,然后将query方法分发给正确的provider执行。

ContentProvider使用Content URI的path部分去选择访问的table。通常一个provider的每一个对外的table对应的path。

在之前的代码中,“word”表的URI全写如下:

1
content://user_dictionary/words

User_dictionary是此provider的authority,而words是“word”表对应的path。content://称之为scheme,总是存在的,它表示这是一个Content URI。

一些provider允许你直接在URI后附着一个ID来访问table的某一行数据。比如,要访问“word”表中的ID为4的数据:

1
Uri singleUri = ContentUri.withAppendedId(UserDictionary.Words.CONTENT_URI,4);

你会经常使用这种方式(ID指定)来查询某一行数据或是更新,删除。

注意:Uri和Uri.Builder提供了一些方便的方法来从String构建形式良好的Uri对象。ContentUri中包含了方便的方法去在URI后附着一个ID。上述代码就是使用wirhAppendedId()方法来给URI附着ID的。

Retrieving Data from the Provider

此段讨论如何从一个provider中检索(retrieve,query)数据。将用户词典Provider作为例子。

说明:为了清晰起见,此章节中代码调用ContentProvider.query()是在“UI Thread”。实际上,你应该使用异常查询方式(使用单独线程)。其中一种方式就是使用CursorLoader类,(在Loader文档中有详细介绍,译者注Loader是3.0提供的,之前的API不提供)。还有,讲解的代码都是片段而非一个完整的Application。

从一个Provider中query数据,基本有以下两步:

  1. 请求到此provider的读权限
  2. 定义代码去发送一个query给Provider

Requesting read access permission

要访问一个provider,你的Application需要此provider的读权限。你不能在运行时请求此权限,而是要在manifest文件中指明你需要的权限,使用元素和provider中定义的明确permission name。当你在你的manifest中指明了此元素,你的Application就可以有效请求此权限。当用户安装你的Application时,会被隐式授予此请求权限。

更多permission相关知识,参看Content Provider Permissions章节

用户词典Provider在manifest中定义了android.permission.READ_USER_DICTIONARY的权限,所以想要从此Provider中读取数据的Application都必须请求此权限。

Constructing the query

下一步就是构造一个query来从provider检索数据。如下代码定义了一些列的查询参数变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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作为例子。Provider的query类似于SQL的query,包含一系列的返回列,一系列的选择准则,一系列的排序规则。

返回列称之为projection。

检索准则的表达式分为一个查询条件(selection clause)和查询参数(selection argument)。查询条件是一个逻辑和Boolean表达式(列名和值匹配)的组合。可以使用查询参数的值去对应查询条件中“?”。

在下面的代码片段中,如果用户不输入单词,查询语句就会设置为null,这样会查询出provider的所有单词。如果用户输入了一个word,查询语句会设置成“UserDictionary.Words.Word + “ = ?””且“?”被相应替换成用户输入的word值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/*
* 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 = " = ?";
// 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
}

与此query相类似的SQL语句如下:

1
SELECT _ID, word, frequency, locale FROM words WHERE word = <userinput> ORDER BY word ASC;

Protecting against malicious input

如果Provider管理的数据是在一个SQL数据库中,在原始SQL语句中包含不被信任的数据会导致SQL注入。

看如下的selection clause:

1
2
// Constructs a selection clause by concatenating the user's input to the column name
String mSelectionClause = "var = " + mUserInput;

如果你这样做,用户就可以在你的SQL语句中串联恶意SQL。例如,用户可能输入“nothing; DROP TABLE ;”来代替mUserInput,这样会导致selection clause变成“var = nothing; DROP TABLE ;”。因为此selection clause会被当做一个SQL语句,这可能导致provider清除掉SQLite数据库下的所有table(除非provider设置了捕获SQL注入尝试)。

为了避免这种问题,可以在selection clause中使用“?”作为可代替的参数,而在一个单独的数组中赋值(selection arguments)。当你这样做时,用户的输入会直接绑定到query而不是被解释成SQL语句的一部分。因为它不是被当做一个SQL来对待,所以用户不能注入恶意SQL。如下:

1
2
// Constructs a selection clause with a replaceable parameter
String mSelectionClause = "var = ?";

如此初始化一个selection argument的数组:

1
2
// Defines an array to contain the selection arguments
String[] selectionArgs = {""};

如此将用户输入放入数组中:

1
2
// Sets the selection argument to the user's input
selectionArgs[0] = mUserInput;

Selection clause使用“?”而对应selection argument数组是一个Select的首选方式,即使provider不是基于SQLite数据库。

Displaying query results

ContentResolver.query()方法通常会返回一个Cursor:包含query的projection指定的列和与query的selection匹配的行。Cursor提供随机读访问其包含的行和列。使用Cursor的方法,你可以遍历结果集中的行,确定每一列的数据类型,获取每一列的数据以及获取结果集的其他属性。一些Cursor实现会在provider的数据变化时自动更新对象,或是在Cursor变化时触发一个Observer的方法,或是二者兼是。

注意:Provider可能限制访问基于对象性质查询出的列(不理解,需看源码分析)。例如Contacts Provider限制访问一些列给同步适配器,所以它不会返回给一个Activity或Service。

如果没有行匹配此查询准则,provider会返回一个空的Cursor,cursor的count为0。

如果出现内部错误,查询的结果取决于特定的provider,可能是null也可能抛出一个Exception。

因为Cursor是一个行的列表,所以显示Cursor中内容的最好方式是通过一个SimpleCursorAdapter与一个ListView关联。

下列代码片段是上面代码的延续。创建一个包含Cursor的SimpleCursorAdapter的对象,且设置此对象为ListView的适配器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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的列。为此,前面查询“word”表时也检索了_ID列,虽然ListView并不显示此列。这个限制也解释了为什么大多数的provider的每个表都有_ID列字段。

Getting data from query results

除了简单的显示查询的结果,你还可以以其他方式使用它。比如,你可以在用户词典中检索拼写,然后在其他provider中查找。你可以遍历Cursor中的行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 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 Permissions

一个provider的Application可以指定一个permission,其他Application要访问此provider的数据就必须包含此permission。这些permission确保用户知道一个Application将会尝试访问哪些数据。基于Provider的要求,其他Application需要请求相应的permission去访问此Provider。终端用户会在他们安装Application看到应用请求的permission。

一个provider的Application没有指定任何permission,那么其他Application不能访问此provider的数据。然而,此provider的Application的其他组件总是拥有全部的读写权限,无论是否指定permission。

如前所述,用户词典Provider需要android.permission.READ_USER_DICTIONARY权限从它检索数据,需要另外一个android.permission.WRITE_USER_DICTIONARY权限来插入,更新和删除数据。

为了获得访问一个Provider的权限,一个Application需要在其manifest文件中指定元素。当Android Package Manager安装此Application时,用户必须同意此Application请求的所有权限。如果用户同意所有请求的权限,Package Manager继续安装,否则会取消安装。

如下是访问用户词典Provider定义的permission:

1
<uses-permission android:name="android.permission.READ_USER_DICTIONARY">

关于Provider权限的更多细节参看Security and Permissions文档。

Inserting, Updating, and Deleting Data

和检索数据一样,你也可以通过client端与provider的交互去修改数据。你可以调用ContentResolver中的带参方法执行ContentProvider中的相应方法。Provider和client端会自动处理IPC以及安全性。

Inserting data

插入数据到provider,可以调用ContentResolver.insert()方法。此方法会向provider插入一行数据并返回此行数据的URI。下面的代码段就是展示如何向用户词典provider插入一个word:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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包含,此ContentValues类似于一个单行的Cursor。此对象中的列不需要是相同的数据类型,如果你不想指定一个值,你可以使用ContentValues.putNull()方法设置一个null值。

上述代码中没有添加_ID值,这是因为此列是自动维护的。Provider会给每一个新加的行赋予一个唯一的_ID值。Provider通常将此值作为table的主钥(primary key)。

Insert方法会返回一个表示新加行的URI,格式如下:

1
content://user_dictionary/words/<id_value>

就是此新加行的_ID。大多数的provider都能自动检测此格式的URI并且在指定行上执行请求。

可以使用ContentUri.parseId()来从返回的URI中获取_ID。

Updating data

要更新一行数据,你要使用一个ContentValues作为更新值,和一个查询条件。然后调用ContentResolver.update()方法。你仅仅需要将你想要更新的列的值加入ContentValues中。如果你想清除某列的内容,可以给它赋予一个null值。

如下代码展示了修改所有local为en的行的local为null。Update的返回值为更新行的个数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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章节。

Deleting data

删除行类似于查询,你要指定一个你想要删除行的查询条件,delete方法会返回你删除了的行的个数。如下代码展示了删除appid为user的行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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.delete()方法你也应该过滤用户输入,参考Protecting against malicious input章节。

Provider Data Types

ContentProvider提供诸多不同的数据类型。例如text,integer,long,float,double。还有一个经常会使用到数据类型是BLOB(Binary Large Object,一个64KB的字节数组)。你可以通过Cursor的“get”方法查看有效的数据类型。

Provider中的每列的数据类型一般会在其文档中说明。你也可以使用Cursor.getType()来确定数据类型。

Provider还维护着他们定义的Content URI的MIME数据类型信息。你可以使用MIME类型信息来确定你的Application是否可以处理Provider提供的数据,或是基于MIME类型来选择一个类型去处理。通常在平provider包含复杂数据或是文件。可以调用ContentResolver.getType()去获取相应URI的MIME类型。

如下MIME Type Reference章节会描述标准MIME和自定义MIME类型的语法。

Alternative Forms of Provider Access

在Application开发时有三种重要的可选provider访问格式:

  • Batch access:你可以在ContentProviderOperation类中绑定一组访问调用,然后通过调用ContentResolver.applyBatch()来应用此Batch。
  • Asynchronous queries:你应该在一个单独线程中做query操作。其中一种方式就是使用CursorLoader对象,在Loader文档中有此实例。(注:此是3.0引入,之前的API可以使用AsyncQueryHandler处理)。
  • Data access via intents:虽然你不能直接发送一个Intent给provider,但是你可以发送一个Intent给此provider的Application,通常是最好的修改provider数据的方式。

Batch access

使用Batch方式访问provider对于insert多行数据,或者是在同样一个方法调用中往多个table中insert行,又或者类似事务(transaction)那样跨进程执行一系列操作是非常有用的。

要以“Batch mode”访问一个Provider,你要创建一组ContentProviderOperation对象,然后调用ContentResolver.applyBatch()发送给ContentProvider。你给此方法传一个provider的authority作为参数而不是一个具体的Content URI,这样你那一系列ContentProviderOperation就可以针对不同的table工作。ContentResolver.applyBatch会放回一个结果集的数组。

Data access via intents

Intent提供对一个Content Provider的直接访问。允许用户访问一个provider中数据即使你的Application没有访问权限,无论是从一个有访问权限的Application获得一个结果Intent,或是激活一个有访问权限的Application且让用户在那里执行操作。

Getting access with temporary permissions

你可以访问你一个provider的数据,即使你没有合适的访问权限,通过发送一个Intent给拥有权限的Application然后接受一个带有“URI”权限的结果Intent。这些指定特定URI的权限持续到接收的Activity finished。拥有永恒权限的Application通过在其结果Intent中设置如下Flag来授予临时权限。

  • 读权限: FLAG_GRANT_READ_URI_PERMISSION
  • 写权限:FLAG_GRANT_WRITE_URI_PERMISSION

注意:这些Flag不会授予一般的读写权限给包含此URI的authority。此访问仅限于此URI本身。

Provider在manifest文件中为content URI定义URI权限,在元素中使用android:grantUriPermission属性,与在元素中添加子元素。此URI权限机制会在Security and Permissions文档的“URI Permissions”章节详细介绍。

例如,你可以在Contacts Provider检索一个Contact的数据,即使你没有READ_CONTACTS的权限。你可能想要在一个Application中发送一封电子贺卡(当一个联系人生日时)。不是请求READ_CONTACTS权限(这会给你访问所有联系人及其信息的权限),你更应该让用户控制你的Application要用到的联系人。你可以使用如下步骤:

  1. 你的Application调用startActivityForResult()发送一个包含Action为“ACTION_PICK”和MIME type为“CONTENT_ITEM_TYPE”的Intent。
  2. 因为此Intent与联系人应用的选择Activity的Intent filter匹配,此Activity会切换到前台。
  3. 在此选择Activity中,用户可以选择一个联系人。一旦选定,此Activity会调用setResult(resultCode, Intent)来初始化一个Intent会传回给你的Application。此Intent包含用户选择的联系人的Content URI,还有额外的flag(FLAG_GRANT_READ_URI_PERMISSION)。此flag会授予你的Application读取此返回的Content URI指定的联系人的信息的权限。之后此选择Activity会调用finish()切换回你的Application。
  4. 你的Activity回到前台,系统会调用你的Activity的onActivityResult方法,此方法接收联系人应用的选择Activity创建的结果Intent。
  5. 使用此结果Intent中的Content URI,你可以从Contacts Provider中读取此联系人的数据,虽然你没有在你的manifest文件中请求访问此Provider的永恒权限。你可以获取此联系人的生日信息以及email地址来发送一个电子贺卡。

Using another application

在你的Application没有访问权限时,一个简单的允许用户修改数据的方式是激活一个拥有权限的Application,然后让用户在那儿工作。

例如:日历应用接收ACTION_INSERT intent,它允许你激活此应用的insert界面。你可以在此Intent中添加额外数据,这用应用就会使用预填充的界面。因为周期性事件有很复杂的逻辑语法,更好的往Calendar Provider中插入事件的方式是使用ACTION_INSERT激活Calendar应用然后让用户在此应用中insert。

Contract Classes

Contract类定义了一些协助Application工作的常量,包括content URI、列名、intent Action和provider的其他的特性等。Contract类不是provider自动包含的,provider的开发者需要自己定义他们并让他们对其他开发者可用。Android平台中很多provider拥有相应的Contract类。

具体实例,参看源码。Contacts Provider的ContactsContract类。

MIME Type Reference

Content Provider可以返回标准的MIME媒体类型,或自定义的MIME类型字符串,或是二者兼并。

MIME类型有如下格式:

1
type/subtype

例如:知名的MIME类型text/html,包含text类型和html子类型。如果一个provider返回一个此类型给URI,意味着使用此URI的query会返回一个包含HTML标签的text。

自定义的MIME类型字符串也称之为“vendor-specific”MIME类型,有更复杂的type和subtype值。此type值通常为

1
vnd.android.cursor.dir

来表示多行数据,或者

1
vnd.android.cursor.item

来表示单行。

subtype是provider-specific。Android内置的provider通常有一个简单的subtype。例如,当联系人应用创建一行电话号码的数据时,它会设置MIME类型如下:

1
vnd.android.cursor.item/phone_v2

注意,subtype只是简单的phone_v2。

Provider开发者可以基于provider的authority和table名创建自己的subtype格式。例如:假设一个provider包含列车时刻表,provider的authority是com.example.train,包含Line1、Line2、Line3的table。对于以下content URI:

1
content://com.example.trains/Line1

请求table Line1,provider返回如下MIME类型:

1
vnd.android.cursor.dir/vnd.example.line1

对于这个Content URI:

1
content://com.example.trains/Line2/5

请求table Line2的第5行,provider返回如下MIME类型:

1
vnd.android.cursor.item/vnd.example.line2

大多数provider在其Contract类中定义了他们用到的MIME类型。

Creating a Content Provider

Content Provider管理数据仓储的入口。你通过一个或多个类来在你Application实现一个provider,并在manifest文件中添加元素。你的其中一个类实现ContentProvider,它会作为你的provider与其他Application的交互接口。虽然Content Provider意味着你在共享你的数据给其他Application,你自己Application内的Activity也可以通过你的provider允许用户查询和修改数据。

如下章节会描述创建一个provider的步骤以及可用的一些API。

Before You Start Building

在你开始创建一个provider之前,确认以下几点:

  1. 确定你是否需要一个Content Provider。在以下的情况下你需要创建一个provider:
    a) 你想要提供复杂数据或文件给其他Application
    b) 你想要允许用户从你的app复制复杂数据给其他app
    c) 你想要通过Search框架提供一个自定义本地搜索
    你不需要为一个SQLite DB提供一个provider,如果你仅仅是在你的Application内部使用。
  2. 如果你还没有做以上工作,请阅读Content Provider Basics章节来了解Provider的更多知识。

下面,如下步骤帮助你创建一个provider:

  1. 设计你的数据的原存储。Content Provider以下面两种方式提供数据:
    a) File Data
    数据通常是file的形式,例如photo,audio,video。在你的App的私有空间存储这些数据。当其他App请求一个文件时,你的provider会提供一个此文件的操作句柄(handle)。

b) “Structured Data”
数据通常是database,array或类似结构的形式。数据以类似table的行列形式表格存储。一行代表一个数据实体,比如一个人或一个库存条目等。每列代表给数据实体的一些数据,例如人的名字或库存条目的价格等。通常以此方式存储数据的是使用SQLite Database,但你也可以使用其他的持久存储类型。更过Android系统的存储类型信息,参见Designing Data Storage章节。

  1. 定义一个ContentProvider的具体实现类并实现其需要的方法。此类是你的数据与Android系统的其他部分的交互接口。更多信息,参见Implementing the ContentProvider Class章节。

  2. 定义provider的authority字符串,content URI,列名等。如果你想provider的App处理Intent,还要定义Intent Action,extra data,Flag等。还要定义permission作为你的App请求访问你的数据的权限。你应该考虑在一个单独的Contract类中定义以上所有的值,然后,你可以暴露此类给其他开发者。更多content URI的信息,参考Designing Content URIs章节。更多Intent信息,参考Intent and Data Access章节。

  3. 添加其他可选项,例如实例数据,或是实现AbstractThreadedSyncAdapter使你可以在provider和基于云的数据(云端)进行同步。

Designing Data Storage

Content Provider是一个结构化数据的接口。在你创建这个接口之前,你必须决定如何存储你的数据。你可以以你喜欢的任何方式保存数据,然后设计一个读写此数据的接口。

Android中如下几种数据存储技术是有效的:

  • Android系统包含了SQLite数据库的API,Android内置的provider使用SQLite来保存表型(table-oriented)数据。SQLiteOpenHelper类协助你创建database,SQLiteDatabase类是访问数据的基类。

你不需要使用一个database去实现你的数据仓储。Provider的外观是一系列表格,类似于关系型数据库,但是这不是Provider内部实现的要求。

  • 对于文件存储,Android包含各种面向文件(file-oriented)的API。更多file存储信息,参见Data Storage章节。如果你为媒体相关的数据(例如music,video)设计一个Provider,你可能使用table和file相结合的方式。

  • 对于基于网络存储的数据,使用java.net和android.net中的类。你也可以同步网络数据到你的本地数据(比如数据库)中,然后以table或者file的形式提供出来。同步数据可以参考Sample Sync Adapter的Demo。

Data design considerations

如下有些建议,当你设计你的provider的数据结构时:

  • Table数据应该总是有一个主钥(primary key)列,provider将其作为每行数据的唯一数值来维护。你可以使用此行的此值与其他table关联(作为一个外键(foreign key))。虽然你可以使用为此列选择任何名字,但是使用BaseColumns._ID是最好的选择。

  • 如果你想要提供一个Bitmap的图片或是其他基于文件的大数据,直接保存在文件中提供出来而不是使用table。如果你这样做了,你需要告诉你的Provider的用户,他们需要使用ContentResolver的file方法去访问这些数据。

  • 使用BLOB类型去保存不同大小不同结构的数据。例如,你可以使用BLOB列去保存一个协议缓存或是JSON结构。

你也可以使用BLOB去实现一个schema-independent的表,在这种类型的table中,你定义一个主键列,一个MIME类型的列,还一个或多个BLOB的普通列。这意味着BLOB列的数据通过MIME列来指示。它允许你在一张table的不同行存储不同的数据类型。参见Contacts Provider中的“data”表。

Designing Content URIs

Content URI是一个标识provider中数据的URI。Content URI包含整个provider的标识符(authority)和一个指定table或file的名称(path)。一个可选的id部分指定table的某一行。所有ContentProvider的数据访问方法都有一个Content URI的参数;它允许你确定要访问的table,row或是file。

Designing an authority

一个Provider通常拥有一个单一的authority,作为Android内部名称。为了避免去其他provider冲突,你应该使用你的所有权域名反序(com.android.xxx)作为你的provider的authority的基础。因为此建议同样针对Android的包名,你可以定义你的provider的authority作为包名的扩展。例如,如果你的Android应用包名为com.example.xxx,你应该定义你的provider的authority为com.example.xxx.provider。

Designing a path structure

开发者通常在authority后附着一个path来创建一个Content URI去指定一个确定的table。例如,如果你有两个table:table1和table2,你可以使用com.example.xxx.provider/table1和com.example.xxx.provider/table2分别表示。Path不仅限于一个分部(segment),且每级path都不是必须指定一个table。

Handling content URI IDs

为了方便,provider提供了直接访问table中单独行的方式,通过一个附着ID的content URI。Provider会将此ID与table中的_ID列进行匹配,并在相应的匹配行中执行操作。

此设计有利于app访问provider。App对应provider执行一个query,然后使用CursorAdapter在一个ListView中显示结果。CursorAdapter中的Cursor要求必须定义有_ID。

用户可能选择UI上显示的一行数据去查看或是修改。App会从ListView的Cursor中获取相应的行,取得此行的_ID,加入到content URI后,然后发送访问请求给provider。Provider可以对用户指定的行进行query或modify操作。

Content URI patterns

为了协助更好处理传入的Content URI执行的操作,provider API提供了一个方便的类:UriMatcher,它映射content URI为integer值。你可以使用switch语句来选择执行请求的操作。

Content URI可以使用以下通配符:

    • :任意长度的任意有效字符
  • :任意长度的数字字符

假设有一个authority为com.example.app.provider的Provider,以下分别表示:

  • content://com.example.app.provider/table1:叫table1的表
  • content://com.example.app.provider/table2/dataset1:叫dataset1的表
  • content://com.example.app.provider/table2/dataset2:叫dataset2的表
  • content://com.example.app.provider/table3:叫table3的表。

Provider同样可以识别带行ID的URI,例如content://com.example.app.provider/table3/1表示table3中_ID为1的行。

以下content URI也是可以的:

1
2
3
4
5
6
7
content://com.example.app.provider/*
```
匹配provider中的任意URI
```java
content://com.example.app.provider/table2/*

匹配dataset1和dataset2表,但是不匹配table1和table3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
content://com.example.app.provider/table3/#
```
匹配table3中的一行
底下的代码片段展示了UriMatcher的方法是如何工作的。此段代码分别不同处理代表整个table的URI和代表单独行的URI。
addURI()方法映射一个authority和path为一个integer值。match()方法为一个URI放回一个integer值。Switch语句判断此integer值来分别处理:
```java
public class ExampleProvider extends ContentProvider {
...
// Creates a UriMatcher object.
private static final UriMatcher sUriMatcher;
...
/*
* The calls to addURI() go here, for all of the content URI patterns that the provider
* should recognize. For this snippet, only the calls for table 3 are shown.
*/
...
/*
* Sets the integer value for multiple rows in table 3 to 1. Notice that no wildcard is used
* in the path
*/
sUriMatcher.addURI("com.example.app.provider", "table3", 1);
/*
* Sets the code for a single row to 2. In this case, the "#" wildcard is
* used. "content://com.example.app.provider/table3/3" matches, but
* "content://com.example.app.provider/table3 doesn't.
*/
sUriMatcher.addURI("com.example.app.provider", "table3/#", 2);
...
// Implements ContentProvider.query()
public Cursor query(
Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String sortOrder) {
...
/*
* Choose the table to query and a sort order based on the code returned for the incoming
* URI. Here, too, only the statements for table 3 are shown.
*/
switch (sUriMatcher.match(uri)) {
// If the incoming URI was for all of table3
case 1:
if (TextUtils.isEmpty(sortOrder)) sortOrder = "_ID ASC";
break;
// If the incoming URI was for a single row
case 2:
/*
* Because this URI was for a single row, the _ID value part is
* present. Get the last path segment from the URI; this is the _ID value.
* Then, append the value to the WHERE clause for the query
*/
selection = selection + "_ID = " uri.getLastPathSegment();
break;
default:
...
// If the URI is not recognized, you should do some error handling here.
}
// call the code to actually do the query
}

另外一个类ContentUris提供了一些方便的方法来操作Content URI的id部分。Uri和Uri.Builder类提供了一些方法来解析Uri对象和新建一个Uri对象。

Implementing the ContentProvider Class

ContentProvider通过处理其他App的请求来管理数据入口。所有形式的访问最终会调到ContentProvider,然后通过ContentProvider的具体方法来访问。

Required methods

ContentProvider抽象类定义6个抽象方法,你必须在你的具体实现类中实现它们。所有这些方法除了onCreate都被client app用来调用去访问你的provider。

  • query
    从你的Provider中检索数据。使用参数来确定要查询的table,返回的行和列,以及结果集的排序方式。返回的数据是一个Cursor。

  • insert
    往你的Provider中插入一行数据。使用参数来确定具体的table和对应列值。返回一个URI代表新插入的那行数据。

  • update
    更新你的Provider中的已有行。使用参数来确定更新的table,要更新的行,以及更新的对应列值。返回一个int表示更新了多少行。

  • delete
    从你的Provider中删除行。使用参数确定对应的table,以及需要删除的行。返回一个int表示删除了多少行。

  • getType
    返回一个Content URI的对应MIME类型。此方法在Implementing Content Provider MIME Types有详细说明。

  • onCreate
    初始化你的Provider。Android系统会调用此方法后立即创建你的Provider。注意:你的Provider不会被创建直到一个ContentResolver对应视图去访问你的Provider。此处很重要,通俗的说,数据库是在第一次请求时创建的,而不是开启应用时就创建了。

实现上述方法时要注意以下几点:

  • 所有以上方法除了onCreate可以被多个线程同时调用,所以它们必须是线程安全的。更多多线程的细节,参见Processes and Threads文档。
  • 避免在onCreate中执行长时间的操作。延迟初始化任务知道真正需要。参看Implementing the onCreate method章节。
  • 虽然你必须实现这些方法,你的代码可以不做任何处理除了返回预期的数据类型。例如,你可能想要阻止其他App往table中insert数据,你可以实现insert方法只是返回0而不做其他实现。

Implementing the query() method

ContentResolver.query()方法必须返回一个Cursor对象,如果失败则抛出一个Exception。如果你使用SQLite数据库作为数据存储。你可以通过SQLiteDatabase类的query方法简单的返回一个Cursor。如果这个query没有匹配任何行,你应该返回一个Cursor实例(其getCount方法返回0)。仅仅在query过程中发生内部错误时你应该放回null。

如果你不是使用SQLite数据库作为数据存储,使用Cursor的一个具体子类。例如,MatrixCursor实现Cursor,每行是一个对象数组。使用此类,addRow()方法添加一个新行。

Android系统必须可以跨进程与一个Exception关联(也就是说跨进程捕获Exception)。Android在处理query错误时,通常抛出以下类型的Exception:

  • IllegalArgumentException(通常在无效URI时抛出此异常)
  • NullPointerException

Implementing the insert() method

Insert方法往对应的table中插入一行数据,使用ContentValues值对参数。如果某一列在ContentValues参数中没有定义,你可能需要提供一个默认值,不管是在provider代码还是数据库架构中。

此方法应该返回新增行的URI。可以使用withAppendedId()方法在此table对应的URI后附着主键_ID。

Implementing the delete() method

实现此delete方法,并不是必须要将数据从数据存储中实际删除。如果你在你的Provider中使用了同步适配器,你应该考虑标记要删除的行一个“delete flag”而不是彻底的删除此行。同步适配器可以监测这些将要被删除的行数据,在删除provider中数据之前将server中的对应数据删除。

Implementing the update() method

Update方法拥有一个和insert方法相同的ContentValues参数,和delete,query相同的selection,selectionArgs参数。这可以允许你在这些方法之间重用代码。

Implementing the onCreate() method

Android在启动provider时会调用其onCreate方法。你应该在此方法中仅仅执行快速初始化任务,推迟数据库创建和数据加载到provider实际接收到数据请求时。如果你在onCreate中执行长时间操作,将会使provider的启动变慢。依次,会使provider与其他App之间的响应变慢。

例如:如果你使用SQLite数据库,你可以在ContentProvider.onCreate方法中创建一个SQLiteOpenHelper对象,然后在你第一次打开数据库时创建table。为了达成这样,你第一次调用getWritableDatabase方法时,会自动调用SQLiteOpenHelper.onCreate方法。

如下的代码片段展示了ContentProvider.onCreate和SQLiteOpenHelper.onCreate之间的交互。第一段是ContentProvider.onCreate的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
public class ExampleProvider extends ContentProvider
/*
* Defines a handle to the database helper object. The MainDatabaseHelper class is defined
* in a following snippet.
*/
private MainDatabaseHelper mOpenHelper;
// Defines the database name
private static final String DBNAME = "mydb";
// Holds the database object
private SQLiteDatabase db;
public boolean onCreate() {
/*
* Creates a new helper object. This method always returns quickly.
* Notice that the database itself isn't created or opened
* until SQLiteOpenHelper.getWritableDatabase is called
*/
mOpenHelper = new SQLiteOpenHelper(
getContext(), // the application context
DBNAME, // the name of the database)
null, // uses the default SQLite cursor
1 // the version number
);
return true;
}
...
// Implements the provider's insert method
public Cursor insert(Uri uri, ContentValues values) {
// Insert code here to determine which table to open, handle error-checking, and so forth
...
/*
* Gets a writeable database. This will trigger its creation if it doesn't already exist.
*
*/
db = mOpenHelper.getWritableDatabase();
}
}
下面是SQLiteOpenHelper.onCreate的实现:
...
// A string that defines the SQL statement for creating a table
private static final String SQL_CREATE_MAIN = "CREATE TABLE " +
"main " + // Table's name
"(" + // The columns in the table
" _ID INTEGER PRIMARY KEY, " +
" WORD TEXT"
" FREQUENCY INTEGER " +
" LOCALE TEXT )";
...
/**
* Helper class that actually creates and manages the provider's underlying data repository.
*/
protected static final class MainDatabaseHelper extends SQLiteOpenHelper {
/*
* Instantiates an open helper for the provider's SQLite data repository
* Do not do database creation and upgrade here.
*/
MainDatabaseHelper(Context context) {
super(context, DBNAME, null, 1);
}
/*
* Creates the data repository. This is called when the provider attempts to open the
* repository and SQLite reports that it doesn't exist.
*/
public void onCreate(SQLiteDatabase db) {
// Creates the main table
db.execSQL(SQL_CREATE_MAIN);
}
}

Implementing ContentProvider MIME Types

ContentProvider类提供了两个方法来返回一个MIME类型。

  • getType
    实现一个Provider必须实现的方法。

  • getStreamType
    如果你的provider提供file,你可能要实现此方法。

MIME types for tables

getType放回一个MIME格式的String,描述content URI参数对应返回的数据的类型。此URI参数可能是一个模式而非一个具体的URI。在这种情况下,你要返回与此模式匹配的Content URI的数据类型。

常见的数据类型例如text,html或jpeg。getType方法应该返回数据的标准MIME类型.

由于Content URI表示table数据中一行或多行。getType应该返回Android供应商指定的MIME格式:

  • 类型部分:vnd
  • 子类型部分:
  • 单行数据:android.cursor.item/
  • 多行数据:android.cursor.dir/
  • Provider指定部分:vnd..
  • 你提供name和type。Name的值应该是全局唯一的,type应该对应URI模式唯一。一个好的定义name方式是使用你的公司名或你的App的包名,type的话,选择代表URI关联的table的String是一个好选择。

例如,一个Provider的authority为com.example.app.provider,且有一个暴露的table名为table1。那么table1中多行数据的MIME为:

1
vnd.android.cursor.dir/vnd.com.example.provider.table1

table1中的单行数据:

1
vnd.android.cursor.item/vnd.com.example.provider.table1