内容提供者基础 Content Provider Basics——翻译自developer.android.com

时间:2022-09-17 09:21:27

#内容提供者基础 Content Provider Basics

content provicer 管理着中心数据仓库的访问。一个provider是Android的应用的一部分,它可以提供数据工作的UI。


然而,content provider基本都是被其他的应用访问,使用一个provider客户端对象来访问provider。provider和provider client一同创立了一个持续的标准的数据接口,它可以提供进程内通信以及安全数据访问。


这个主题讲解下面内容的基础部分;
- content provider 是怎样工作的
- 从content provider中获取数据的api
- 从content provider中插入、删除、更新数据的api
- 其他装备了content provider 的功能的api


##一览 Overview

content provider给外部应用的数据以表的形式,类似于关系型数据库。行表示provider汇集的数据类型,行中的列表示一个对象的一块数据。


例如一个Android平台内置的provider——用户字典,它保存着用户的一些非标准的词汇的拼写。表1展示了content 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中,每一个行头代表了一个词的实例,他们在标准的词典中找不到。其每一栏都表示这个词的数据,比如说locale就是第一次遇见的场所。每一栏的头是存储在provider中的栏目的名字。想要引用一行的 locale,你要引用“locale”栏。对于这个提供者,_ID作为你的关键字栏目,是由provider自动维护的。

***********提示*************

一个provider的关键字不是必须的,即使存在,其名字也可以不是_ID。但如果你想要从provider中获取数据装入到listView中,那么你必须要有一栏为_ID。这个要求的更多细节请参见 Displaying query results一节。

*****************************


访问一个provider

应用可以通过访问ContentResolver对象来访问content provider。这个对象中的方法和provider对象的中的同名,是ContentProvider的子类的具体实现。ContentResolver方法中包含了维护数据的增删改查操作。

在客户应用进程中的ContentProvider对象和在拥有provider的应用的进程中的ContentProvider对象会自动处理进程内的交流。ContentProvider还会作为在数据仓库和比如说表这样的数据形式的之间的抽象层。

**********提示******************

访问contentprovider你的你需要在manifest中申请特殊的权限。在Content Provider Permissions会有更详细的讨论。

*********************************

举个例子,获取从用户字典中获取词汇和对应的现场,你可以调用ContentResolver.query()方法。这里面的query()方法会调用ContentProvider的query()方法,它要由用户词典提供者定义。下面的代码展示了一个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展示了query(Uri,projection,selection,selectionArgs,sortOrder)中的参数是如何对应 SQL SELECT statement(SQL查询语句的)。

Table 2: Query() compared to SQL query.

query() argument SELECT keyword/parameter Notes
Uri FROM table_name Uri 对应的provider中的名为table_name的表。
projection col,col,col,... projection 每一行中需要查询的栏目名字构成的数组。
selection WHERE col =value selection 选择行的选择条件的说明。
selectionArgs (没有准确的 SQL语句中的对应,表示的是在选择从句中的?的占位。)
sortOrder ORDER BYcol,col,... sortOrder
返回的Cursor中的行的排序方式。


内容的URI


content URI是指定了provider中的数据的uri。Content URI中包含了整个provider的符号名字(它的职权),和一个指向一个表的名字。(一个路径)当你调用一个方法来访问provider中的一个表的时候,表的content URI就是其中 的一个参数。
在运行的代码中,常量CONSTANT_URI包含了用户字典word表的内容URI。ContentProvider对象解析出URI的职权,并使用它们通过比较用户职权和系统已知的provider的表,来处理provider。ContentResolver接着就可以传递查询参数给正确的provider了。
ContentProvider通过使用contentURI中的路径部分来选择要访问的表。provider都会给他呈现的表设定一个路径。 在上一段代码中,words表的完全的URI是:
content://user_dictionary/words

其中的user_dictionary 是provider职权,words字符串是表的路径。content://(模式)字符串则一直都会有,它定义为content URI。
很多provider允许你在content URI中的最后一个字段来追加一个ID值,从而可以访问表中的某一行。例如,想要从用户字典里面检索一个行的 _ID 字段为4的行,就尅使用这个content URI。
Uri singleUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);
当你想要删除或者更新很多行中的一个的时候,你需要使用id字段。************提示*****************
Uri和Uri.Builder类都有使用构建URI格式对象的便捷方法。ContentUris类中有在URI后边追加ID字段的方法。之前的代码段就使用了withAppendedId()方法来向用户字典中的content URI中追加 di。 ***********************************

从provider中检索数据

下面介绍怎样从provider中检索数据,使用用户字典(User Dictionary Provider)作为例子。

******************************

为了便于说明,这一节中的代码片中调用的ContentResolver.query()都是在UI线程中调用的。在实际的应用中,你应该异步地在另一个线程中进行查询。一种方法是使用CursorLoader类,在Loader的教程中有更多的说明。并且,这些代码只是一个片段,并没有展现一个整个的应用。

想要从provider中检索数据,要遵循下面的基本步骤:

1.为provider请求读权限。

2.定义向provider发送查询的代码。


获取读访问权限

要从provider中获取数据,你的应用怒要provider的 读访问权限。必不能在运行时获得这样的权限,只可以在manifest文件中声明,使用<uses-permission>元素,以及在provider中定义的名字。当你在manifest中声明这个元素以后,就是在想你的应用请求权限了。当用户安装了你的应用的时候,他就隐式地授予了这些权限。

在User Dictionary Provider 在manifest文件在中定义了android.permission.READ_USER_DICTIONNARY权限,所以想要从这个provider中读取数据的应用就要请求这个权限。


构建一个查询

从provider中检索数据的下一个步骤就是构建一个查询。第一个代码片中定义了一些用来访问User Dictionary Provider的的变量。

// 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()方法,还是使用User Dictionary Provider作为例子。一个ContentProvider查询和一个SQL查询类似,都包含要获取的栏目,筛选标准以及结果排序的顺序。

查询中要返回的栏目叫做projection(英语中有投影的意思)(mProjection变量)。

说明那些行要查询的表达式,则被分成了selection clause 和 selection arguments两部分。selection clause 是一个逻辑和布尔表达式,栏目的名字,以及值(mSelectionClause)的结合。如果你使用了占位符?而不是数值,那么查询方法会从来选择参数数组中获取值(mSelectArgs 变量)。


下一个代码片中,如果用户没有输入一个单词,selection clause就会被设置成null,从而查询会返回所有的provider中的单词。如果用户输入了一个单词,那么selection clause就会设定为“UserDictionary.Words.WORD+"=?”,同时selection arguments 数组中的第一个元素会被设定为用户输入的这个单词。

/*
 * 这里定义一个只有一个元素的String数组来表示selection argument
 */

String[] mSelectionArgs = {""};

// 从UI中获取一个输入
mSearchString
= mSearchWord.getText().toString();

// 记得检查用户的输入是否合法或者是否是恶意代码

// 如果获得是一个空的String,就查询所有的内容
if (TextUtils.isEmpty(mSearchString)) {
   
// 把筛选标砖的从句设为null,就会返回所有行
    mSelectionClause
= null;
    mSelectionArgs
[0] = "";

} else {
   
// 构建一个和用户输入相对应的查询选择从句
    mSelectionClause
= UserDictionary.Words.WORD + " = ?";

   
//把用户输入放入到选择的参数的数组中
    mSelectionArgs
[0] = mSearchString;

}

// 对指定的的表进行查询并在Cursor中返回
mCursor
= getContentResolver().query(
   
UserDictionary.Words.CONTENT_URI,  // The content URI 表的
    mProjection
,                       // The 每一行指定的栏目
    mSelectionClause                  
// 是空或者是用户用户输入的单词
    mSelectionArgs
,                    // 是空或者是用户输入的字符串
    mSortOrder
);                       // 返回的一组行的排序顺序

// 如果发生了一些错误,就会返回一个null的Cursor,或者是抛出异常
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.
在这里处理错误,一定要确保不要使用这个cursor,你可以调用Log.e来记录这些错误
     *     */// 如果返回的Cursor中没有元素,说明没有对应的内容} 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 = <userinput> ORDER BY word ASC;
在SQL语句中,使用了真实的栏目的名字,而不是协议类中的常量。

防止恶意输入

如果content Provider管理的 是一个SQL数据库,在SQL语句中包含不信任的外部数据可以导致SQL注入。

思考下面的选择从句:

// Constructs a selection clause by concatenating the user's input to the column name
String mSelectionClause =  "var = " + mUserInput;
如果你这样做,就留存了用户进行连接恶意SQL代码到你的SQL语句中的漏洞。例如,用户可以输入“nothing;DROP TALBE *;”给变量mUserInput, 会导致这个选择的从句变成了var=nothing;DROP TABLE *;。以为这个选择的语句会被当做一个SQL语句,所以这可能会导致provider擦出除了对应的SQLitedatabase中的所有的表。(除非这个providerprovider设置了捕获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;

使用?占位符,以及一个选择的参数的数组是一个推荐方法,即使不是在SQL数据库中,也推荐这么做。


表示查询结果

ContentResolver.query()的客户端方法总是返回一个包含查询中mProject中指定的栏目,和筛选条件中指定的行的一个cursor。一个Cursor可以为它包含的栏目和行提供随机访问的功能。通过使用Cursor中的方法,你可以迭代Cursor中行,对每一行中的栏目进行判断,获取这些栏目,以及检查结果中的其他的属性。一些Cursor对象中定义了自动更新的方法,在数据库中对应的数据发生变化的时候会自动的更新的奥cursor中,还有一些包含着触发器,当Cursor中的数据发生变化会自动同步到数据库当中。,或者两者都有。


*********提示*************

provider可能会限制对于一些栏目的访问,基于进行查询的对象的一些特性。例如,Contacts Provider会只允许同步适配器访问以下栏目,这样的话就不会想activity或者service返回这些栏目。

***************************


如果没有行和选择中的标准相互对应,那么就会返回一个Cursor.getCount()方法为0的Cursor对象。(一个空的cursor)


如果内部发生了异常,那么返回的结果取决于具体的provider,通常会返回一个指向null 的cursor或者抛出异常 。

因为Cursor对象的组织就如同一个行的列表,所以用来表达内容的简单的办法就是使用SimpleCursorAdaper来把它和一个ListView进行关联。


下面是一个承接上面代码片的新代码片。其中创建了SimpleCurcorAdaper的对象,其中包含了通过查询获得的Cursor对象,并且设置一个ListView来适配这个对象。


// 定义了Cursor中的,我们想要表示 栏目的名字的数组。
String[] mWordListColumns ={    UserDictionary.Words.WORD,   // 协议来中的包含word栏目名字的常量。    UserDictionary.Words.LOCALE  // 协议类中locale栏目的常量};// 定义一个接受从Cursor中获取的栏目的内容的View的数组int[] mWordListItems = { R.id.dictWord, R.id.locale};// 创建一个SimpleCursorAdaptermCursorAdapter = new SimpleCursorAdapter(    getApplicationContext(),               // 应用的上下文对象    R.layout.wordlistrow,                  // ListView中一行的布局的xml布局    mCursor,                               // 查询的结果    mWordListColumns,                      // cursor需要展示的栏目的名字    mWordListItems,                        // 行布局中接收数据的view的id的int数组    0);                                    // 标志位,通常不需要// 设置ListView的适配器mWordList.setAdapter(mCursorAdapter);


*************提示*******************

想要从Cursor构建一个ListView,cursor中一定要包含一个名为_ID的字段。正式因为这样,所以之前的查询都要包含_ID字段,尽管在ListView中并不需要显示。这个限制也解释了为什么contentProider表大都包含_ID字段。

**************************************

从查询结果中获取数据

预期仅仅是显示查询结果,你可以把它们用作其他任务中。例如你可以在用户字典里面检索拼写,然后在其他的provider里面进行查找。你可以在Cursor的行上面迭代来完成。

// 确定一个叫做“word”的栏目的索引号
int index = mCursor.getColumnIndex(UserDictionary.Words.WORD);

/*
 * Only executes if the cursor is valid. The User Dictionary Provider returns null if
 * a只有在Cursor合法的时候才可以执行。如果获得Cursor过程中发生内部错误,其他的provider可能会抛出异常而不是返回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
     *移动到Cursor的的下一行。当你移动cursor之前,它默认的指针的位置是-1。如果你在这个位置读数据的话就会发生异常
     */

   
while (mCursor.moveToNext()) {

       
// 从这一栏中中获取数据。
        newWord
= mCursor.getString(index);

       
// 这里插入处理检索出的单词的代码。

       
...

       
// 循环体结束
   
}
} else {

   
// 如果cursor为null或者发生异常,那么在这里插入处理的代码。
}
Cursor的构建里面包含了很多get类型的方法,以从对象中获取不同类型的信息。例如上面的代码片里面就是使用了getString()方法。同样,还有getType()方法来返回所指定的栏目的数据类型。


Content Provider的权限许可

provider可以指定其他应用想要访问这个provider必须要声明的权限。这个许可保证了使用者知道自己将访问哪一个provider。基于这个要求,其他的应用就要通过请求响应的权限来访问provider。终端用户在安装这个应用的时候就可以看见权限请求。


如果一个provider没有指定一个权限许可,那么其他的应用是不可以访问该provider的。但是,无论是否声明权限,provider的同一个应用的其他组件拥有provider的完全读写权限。

如同之前提到的那样,User Dictionary Provider需要请求android.permission.READ_USER_DICTIONARY的权限,来从中获取数据。provider还有另外一个分离的权限android.permission.WRITE_USER_DICTIONARY来进行插入更新和删除数据。

要获取想访问的provider的权限,应用通过在manifest文件当中使用<uses-permission>标签。当Android Package Manager安装应用时,用户必须要允许所有应用请求的权限。如果用户用户允许了所有,安装才可以继续进行,如果不允许,Android Packet Manager就中断安装。

下面的<user-permission>标签请求了访问User Dictionary Provider的权限。

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

权限许可对于provider访问的影响将在Security and Permission一节进行更多的讲解。

插入,更新和删除数据

和你从provider中获取数据的方式类似,你在客户端组件和ContentProvider之间使用同样的方式来修改数据。你通过调用ContentProvider的一个方法,同时还有一些参数,这些参数会被传入到ContentProvider的对应方法当中。provider和provider客户端之间就会自动处理安全的跨进程的通信。

插入数据

可以使用ContentResolver.insert()方法来插入数据。这个方法在ContentProvider中插入一行数据,并返回这行数据的uri。下面的代码片展示了向User Dictionary Provider插入一个新单词的例子。

// 定义一个接受插入返回值的uri
Uri mNewUri;

...

// 定义一个包含要插入值的对象
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"为插入的数据设定每一栏的值,put方法的两个参数分别为栏目的名字和插入的值
 */

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,   // user dictionary content 的uri
    mNewValues                          
// 要插入的值
);

新的要插入的值被放入到ContentValues的对象当中,在形式上和只有一行的Cursor类似。对象中的栏目不必是相同的类型,并且如果你不想给某一栏设定值,你可以使用ContentValues.putNull()来设定为null。

代码片中没有插入_ID一栏,因为这一栏会被自动插入。provider会给每一个插入的数据设定惟一的_ID。provider通常使用这个值作为表格的主键。

返回的指示新插入行的content URI是下面的这种形式

content://user_dictionary/words/<id_value>

<id_value>是新插入行的_ID的内容。大多数的provider都可以自动探测这种类型的URI,并且在对应的行上面完成相应的操作。

想要从返回的Uri中获取_ID的值,可以调用ContentUris.parseId()方法。

更新数据

更新数据的一行,使用的方法和参数类似于你插入数据,并且如同你查找数据那样设定选取行的标准。客户端你使用的方法是ContentResolver.update()。你只需要把你要更新的数据添加到ContentValues对象的当中即可。如果你想清除某一栏的数据,就把对应的栏目设置为null。

下面的代码片就是把local语言为“en”的行都更新为null。返回值是更新了的行的数量。

// 定义包含要更新的值的对象
ContentValues mUpdateValues = new ContentValues();

// 定义选择你要更新行的标准
String mSelectionClause = UserDictionary.Words.LOCALE +  "LIKE ?";
String[] mSelectionArgs = {"en_%"};

// 定义返回的更新的行目数量
int mRowsUpdated = 0;

...

/*
 * 设定更新的值,并更新选定的单词
 */

mUpdateValues
.putNull(UserDictionary.Words.LOCALE);

mRowsUpdated
= getContentResolver().update(
   
UserDictionary.Words.CONTENT_URI,   // the user dictionary content URIcontent的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.delete()中,同样需要注意防范用户输入。


Provider数据类型

ContentProvider可以提供不同种类的数据类型。User Dictionary Provider里面仅仅提供了text类型,但是provider还可以提供下面的类型。

- integer

- long integer(long)

- floating point

- long floating point (double)

还有一种provider经常使用的数据是Binary Large OBject(BLOB),由一个64kb的byte数据构成。你可以通过查看Cursor类的get方法来看支持的数据类型。

每一栏的数据类型都会在provider的文档中列举。User Dictionary Provider的数据类型,在他的协议类UserDictionary.Words的参考文档中列举。(Contract Classes中有关于它的介绍)。你也可以通过调用Cursor.getType()来确定数据类型。


Provider同样可以维护每一个定义的URI对应的MIME类型信息。你可以使用MIME类型信息来确定你的应用是否支持provider提供的类型,或者是基于MIME类型选择一种处理的类型。当你使用包含复杂的数据结构或者文件的provider的时候,你总是需要使用MIME类型。例如,ContactsProvider中的ContactsContarct.Data表,就使用MIME类型来标明每一行中存储的数据的类型。想要获取MIME类型对应的content URI,可以调用ContentResolver.getType()。

MIME Type Reference描述了标准和用户MIME类型的语意。

Provider的访问形式

在应用开发中有三种访问provider的形式: - Batch access: 你可以使用ContentProviderOperation类来创建batch access,然后使用ContentResolver.applyBatch()方法来应用这些访问。 - 异步查询 Asynshronous: 在分离的线程中进行查询。使用它的一种方法是使用一个CursorLoader对象。更多说明在Loader的教程中。 - 使用intent进行数据访问:尽管你不能直接用intent访问provider,但是你可以访问provider所在的应用,这个应用应该总是准备好了来修改provider的数据。
下面将介绍Batch access和使用intent进行修改。

Batch access组团访问

Batch访问适合于处理插入大量的行,或者在同一个方法调用中向不同的表中插入行,或者通常作为一个事物(transaction,一种原子操作)来跨进程一系列操作。 使用batch模式访问provider,你要创建一个ContentProviderOperation对象数组,并调用ContentResolver.applyBatch()方法来把它们派发到content provider上。你要把contentProvider的职权传递到这个方法上,而不仅是一个特定的content URI。这使得ContentProviderOperation中的每一个对象都可以在不同的表上工作。对于ContentResolver.applyBatch()的调用返回一个结果数组。
关于ContactsContract.RawContacts的协议类的表述中包含了一个展示batch 插入的例子的代码片。Contact Manager的示例应用中在ContactAdder.java源文件中,有一个batch模式访问的例子。

通过intent访问数据

intent可以间接对于content provider进行访问。尽管你的应用没有访问某provider的权限,你也可以让用户访问content provider中的数据。可以通过接受一个拥有权限的应用返回的intent。或者激活一个拥有权限的应用,来让用户从provider中获取数据。 *****************侧栏提示****************************
使用一个帮助者应用来现实数据 如果你的应用有content provider的访问权限,你可能仍然想使用intent来在另一个应用中来现实数据。例如,日历app接收一个ACTION_VIEW的intent,来现实一个特定的日期或者事件。这可以让你不用自己创建UI就可以显式日历。更多请参考Calender Provider一章。
你发送的intent的目的app不一定与provider关联。例如你可以从Contact Provider中获取一个联系人,然后把传送一个包含联系人图像对应的content URI的ACTION_VIEW intent 给一个图像浏览器。

********************************************************

使用暂时权限来访问

尽管你没有访问某个contentProvider的权限,但是你可以通过向某个有权限的应用发出intent并接受返回的intent,其中包含了URI的权限许可。这是一个访问某个特定的URI的权限许可,一直可以持续到接受它的应用finish就截止。那些有永久访问权的应用可以通过在返回的intent中设定一个标志位来授权。 - Read permission:FLAG_GRANT_READ_URI_PERMISSION -Write permission:FLAG_GRANT_WRITE_URI_PERMISSON
*****************提示**************************** 这些标志位不能赋予包含在content URI中的provider的通常的访问权限。权限仅仅作用于uri本身。 ******************************************************* 一个provider在manifest中定义contentURI的访问权限。使用<provider>元素的android:grantUriPermisson属性,以及<grant-uri-permission>的子元素。关于URI权限机制的更多说明在Security and Permission教程中的“URI Permssions”一章。
例如你可以从Contacts Provider中检索联系人,尽管你不用有该provider的READ_CONTACTS访问权限。你可能在一个应用中想通过这种方式,向你的联系人在生日那天发送电子祝福。与其获取联系人provider的READ_CONTACTS权限,来可以访问所有的联系人,不如由用户指定你的应用可以访问那些联系人。这么做要遵循以下几个步骤: 1.使用startActivityForResult方法,来传递一个包含了ACTION_PICK 的action以及一个contact MIME类型CONTENT_ITEM_TYPE. 2.因为这个intent匹配了另一个应用的“选择”activity,这个activity出现到了前台。 3.在选择activity中,用户选择了一个要更新的联系人。当这时,选择activity就会通过调用setResult(resultcode,intent)来建立一个返回给你原来应用的intent。这个intent包含了用户选择的联系人的content URI,和一个Extra flags的数据,FLAG_GRANT_READ_URI_PERMISSON。这些标志位赋予了你的应用读取content URI指定的的联系人数据。选择activity会接着调用finish()方法来将控制权交回到原来的activity。 4.你的应用重新回到了前台,并且系统调用了你的应用中的onActivityResult()方法。这个方法接收从那个app中的选择activity中返回的结果intent。 5.通过使用结果intent中包含的content URI,你可以从Contact Provider中读取联系人的数据,尽管你没有那个provider的永久访问权限。你就可以获取这个联系人的生日信息或者email地址,来发出电子祝福了。

使用另一个应用

另个访问你没有永久权限的provider中的 数据的办法就是激活另一个有访问权限的应用,然后让用户在这个应用上进行操作。
例如,日历应用就可以接受一个 ACTION_INSERT intent,它可以让你激活这这个应用的插入UI。你可以在intnet的Extra中插入数据,这样UI可以使用这些数据进行提前填充。因为重复时间具有复杂的语法,所以更好的向日历provider插入事件的方法就是使用ACTION_INSERT来激活日历应用,然后让用户在那里插入事件。


协议类

协议类中定义了一些可以有助于content provider使用 content URI,栏目名称,intent action,以及其他功能的一些常量。协议类不会自动地包含进content provider,开发content provider的人必须自己定义他们并且可以让其他的开发者能够获取。很多的android平台的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,是一个包含intent和intent data的常量。

MIME类型的引用

content provider可以返回标准的MIME类型数据,或者自定义MIME类型,或者两者都支持。 MIME类型数据格式如下。
type/subtype
例如有一个熟悉是MIME类型叫做 text/html,它的类型是text,子类型是html。如果一个provider 针对一个 URI返回这种类型,那意味着使用这个URI的查询将会得到包含HTML标签的text。
自定义类型的MIME类型字符串,也叫作“供应商指定”MIME类型,拥有更加复杂的类型和子类型。类型的值经常是:
vnd.android.cursor.dir
来表示多行。
vnd.android.cursor.item
来表示一行。子类型是provider指定的。Android内置的provider通常都用一个简单的子类型。例如,当联系人应用创建了电话号码的行的时候,就在这一行上设定下面的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
对应table Line1,provider返会如下是数据类型。
vnd.android.cursor.dir/vnd.example.line1


而对于下面的URI
content://com.example.trains/Line2/5
对应的是Line2表达第五行,provider返回MIME类型如下。
vnd.android.cursor.item/vnd.example.line2

大多数content Provider都会定义他们使用 的MIME类型对应的协议类。例如 联系人provider对应的协议类ContactsContract.RawContacts,就为单个的原始联系人行数据定义了CONTENT_ITEM_TYPE。

单独一行的Content URIs在Content URIs里面有更多的描述。