Content Provider Basics

来源:互联网 发布:伟大的艺术家 知乎 编辑:程序博客网 时间:2024/05/21 06:55

Content Provider用来管理数据,Provider是android 那些提供操作数据的UI的app的一部分。当然,content provider的主要目的还是为其它app所使用,这些app使用provider  client对象来访问provider。provider和provider client提供一个连续,标志的数据接口,并且能够处理内部进程通信和数据安全。

这节主要讲述下面及部分:

content provider是如何工作的

用来从content provider获取数据的API

用来在content provider插入,更新,删除数据的API

其它用于和provider交互的API功能。

Overview (概况)

content provider将数据以一个或多个表格展示给外部的app,正如关系型数据库的表格一样。每一个行代买某个数据类型的一个实例。

例如,在android平台内嵌的一个provider就是用户词典,用来储存用户想要保存的非标准字的拼写,如下表所示:


上表中每行代表一个在标准字典中找不到的词,每一列表示这个词中的相关数据。列的header是储存在provider中的列名。为了寻找一行中的某个locale,可以通过列中的locale进行查找。在这个provider中,列_ID作为主键。

注意:provider并不要求都必须有个主键,而且也不要求列名为_ID的都为主键。但是如果你想将provider中的数据与ListView绑定,那么这个provider必有一列名为_ID。在Displaying query results将会详细讨论这个。

Accessing a provider (访问provider)

app通过ContentResolver对象访问content provider中的数据。这个对象中有在provider 对象中同名的函数,是ContentProvider的子类。ContentResolver提供了用于创建,获取,更新,删除provider中的数据的函数。

ContentResolver对象位于client app的进程中,在app的ContentProvider自动拥有provider,可以处理内部进程通信。ContentProvider也可以作为一个抽象层,介于储存的数据和在外部看来以表格组织的数据。

注意:为了访问provider,你的app通常需要在manifest中申请相应的权限。在Content Provider Permissions将会详细讲解。

例如,为了从上表中获取words和locales,你可以使用ContentResolver.query(),这个函数将会调用ContentProvider.query()函数,下面代码展示如何使用ContentResolver.query()函数。

// Queries the user dictionary and returns resultsmCursor = 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()函数中各个参数:


Content URIs

一个content URI是定义在Provider的数据对应的URI,包含了整个Provider的名字和指向table的名字(路径)。当你你调用client的函数访问Provider中的数据时,table的content URI是其中的一个参数。

在上面的URI(UserDictionary.Words.CONTENT_URI),ContentResolver将URI中的authority(即UserDictionary)提前出来与系统的authorithy比较,通过比较,将这个查询派发给正确的Provider。

ContentProvider通过content URI里面的路径来选择访问的表格,Provider的表格都有对应的路径。

在前面的代码中,表格“words"的完整URI为:

content://user_dictionary/words
其中user_dictionary是Provider的authority,words是表格的路径。content://用来标识这个是content URI。

许多Provider允许你通过在URI后面加上行所在的ID来访问表格中的某一行。例如,下面例子就是获取行的ID为4的相应URI:

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

在获取一系列数据行时,接下来通常使用ID来更新或者删除相应的数据。

注意:在Uri和Uri.Builder类中包含从字符串构造规范的Uri函数。ContentUris中提供将id加到URI的函数。

Retrieving Data from the Provider (从Provider获取数据)

这部分讲解如何从Provider中获取数据,使用上面的User Dictionary Provider作为例子.

为了方便,这部分的代码直接在UI线程中调用ContentResolver.query()函数。但是实际你必须在另一个进程中调用query函数,即进行异步查询。其中的一种就是使用CursorLoader类。

按照下面步骤从Provider获取数据:

1. 请求从Provider读取数据的权限

2. 定义发送一个查询到Provider的代码。

Requesting read access permission (请求读取数据权限)

为了从Provider中获取数据,你的app需要读取Provider的权限。你不能在程序运行的时候请求这个权限,而只能在你的app的manifest文件中请求权限。

可以通过查阅Provider文档获取相关的访问provider的权限。

关于更多访问provider的权限详见Content Provider Permissions。

User Dictionary Provider定义了android.permission.READ_USER_DICTIONARY权限,所以app必须在manifest中请求这个权限。

Constructing the query (构造查询)

下一步就是如何查询。下面代码是如果从User Dictionary Provider中进行查询。

// A "projection" defines the columns that will be returned for each rowString[] 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 clauseString mSelectionClause = null;// Initializes an array to contain selection argumentsString[] mSelectionArgs = {""};

一个provider client 查询类似与SQL查询,包含需要返回的列表,选择规则和排序方式。

用来筛选返回那些行的表达式是分开放在selection clause和selection arguments中。其中selection clause是逻辑和布尔表达式,列名,values组成。如果你在SelectionClause中制定一个?元素,那么查询函数将会从selection argument数组中获取相应的值。

如果用户没有输入一个word,那么selection clause将设置为null,查询结果将返回所有的words。如果用户输入一个word,那么selection clause将被设置为UserDictionary.Words.WORD + " = ?",在selection argument数组的第一个元素就是用户输入的word。

/* * This defines a one-element String array to contain the selection argument. */String[] mSelectionArgs = {""};// Gets a word from the UImSearchString = mSearchWord.getText().toString();// Remember to insert code here to check for invalid or malicious input.// If the word is the empty string, gets everythingif (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 objectmCursor = 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 exceptionif (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 = <userinput> ORDER BY word ASC;

在这个SQL中使用实际的列名,而不是contract类的常量。

Protecting against malicious input(防止恶意输入)

如果管理数据的Content provider是SQL数据库,在SQL语句中包含不可信的数据将导致SQL故障。

下面的选择语句:

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

如果你这样做,那么你将允许用户将恶意的SQL添加到你的SQL语句中。比如如果用户输入”nothing; DROP TABLE *”,这样选择语句将变为var=nothing; DROP TABLE *;由于selection clause是被当作SQL语句的,那么上面的选择语句将导致provide清除所有SQLite数据库中的表(除非在provider中设置了捕获injection的错误)。

为了防止这个问题,才使用将选择语句分为selection clause并使用?代替参数,和一个selection argument数组。这样做以后,就不会出现上面这种问题了。所以上面的选择语句应该是这样子的:

// Constructs a selection clause with a replaceable parameterString mSelectionClause =  "var = ?";

然后在selection argument数组中添加相应的value:
// Sets the selection argument to the user's inputselectionArgs[0] = mUserInput;

// Defines an array to contain the selection argumentsString[] selectionArgs = {""};

在selection clause中用?作为代替,然后在selection argument数组中的每一个元素对照selection clause中的每个?。

Displaying query results (显示查询结果)

ContentResolver.query()将会返回一个包含查询结果的Cursor。Cursor对象提供随机访问它里面包含的行和列。使用Cursor的函数,你可以遍历结果中所有的行,选择每个列的数据类型,从一列中获取数据,检查结果的其它属性。一些Cursor的实现中在provider数据变化时能够自动更新里面的对象,或者在当一个Cursor变化时触发一个观察对象中的相关函数。

注意:provider必须根据对象的性质来限制对列的访问。比如Contacts Provider限制了对一些用来同步adapters列的访问,所以不会返回这些列。

如果没有行能够匹配选择规则,那么provider也会返回一个Cursor对象,只不过Cursor.getCount()返回0(即一个空的cursor)。

由于Cursor里面的结果是一列值,所以最好的显示方式是将Cursor通过SimpleCursorAdapter链接到ListView中显示。

下面的代码创建了一个SimpleCursorAdapter对象,包含返回的Cursor,并将这个SimpleCursorAdapter设置成ListView的adapter。

// Defines a list of columns to retrieve from the Cursor and load into an output rowString[] 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 rowint[] mWordListItems = { R.id.dictWord, R.id.locale};// Creates a new SimpleCursorAdaptermCursorAdapter = 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 ListViewmWordList.setAdapter(mCursorAdapter);

注意:为了将Cursor连接到ListView中,cursor必须包含一个名为_ID的列。虽然在ListView中不会显示这一列。

Getting data from query results (从查询结果中获取数据)

不仅仅只是显示查询的结果,还可以使用这些数据用于其它任务。比如,你可以从user Dictionary中获取spellings,并在其它的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 Permissions (访问Content Provider权限)

在provider的app中可以指定相关权限,其他app如果想访问这个provider,必须具备这个权限。这些权限保证用户知道app要获取那些数据。在安装app时,用户将会看到app需要哪些权限。

如果provider的app中没有指定任何权限,那么其他app就不能访问这个provider的数据。但是,在provider的app中的组件通过具有完全可读取的权限。

上面的例子张,访问User Dictionary Provider需要android.permission.READ_USER_DICTIONARY权限,还有另外一个权限是android.permission.WRITE_URER_DICTIONARY,用来允许其他app在User Dictionary Provider中插入,更新,删除数据。

可以在app的manifest文件中通过<uses-permission>申请相关的权限。android package manager在安装app时,用户必须同意这个app所需要的所有权限。如果不同意,那么将中断安装。

下面是申请访问User Dictionary Provider的代码:

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

关于更多权限参照Security and Permissions。

Inserting, Updating, and Deleting Data (在provider中插入,更新,删除数据)

除了从provider获取数据,你还可以通过provider client与provider的ContentProvider进行交互来更改数据,即通过调用ContentResolver的相关函数。

Inserting Data (插入数据)

调用ContentResolver.insert()函数可以插入数据到provider中。这个函数将在provider插入一个新的行并返回这个行的content URI。下面代码演示如何在User Dictionary Provider中插入一个新的word:

// Defines a new Uri object that receives the result of the insertionUri mNewUri;...// Defines an object to contain the new values to insertContentValues 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。

在ContentValues中并没有添加_ID列,这是因为_ID是provider自动管理的,provider将分配给每个新添加的行一个唯一的_ID。Provider通常使用_ID作为表的主键。

返回的行的content URI如下:

content://user_dictionary/words/<id_value>

其中<id_value>为这行的_ID。可以通过函数ContentUris.parseId()从Uri中获取_ID。

Updating data (更新数据)

类似添加数据,可以使用ContentValues对象来更新数据,通过跟查找一样的选择规则来更高特定的数据。调用ContentResolver.update()来进行更改。在ContentValues中仅需要添加想要更改的列,如果你想清空某个列的内容,可通过将value设置为null。

下面代码演示如果将locale中的语言包含“en”的数据设置为null,并返回更新的行数。

// Defines an object to contain the updated valuesContentValues mUpdateValues = new ContentValues();// Defines selection criteria for the rows you want to updateString mSelectionClause = UserDictionary.Words.LOCALE +  "LIKE ?";String[] mSelectionArgs = {"en_%"};// Defines a variable to contain the number of updated rowsint 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);

Deleting Data (删除数据)

删除数据跟获取数据类似,通过指定选择规则来删除你想要的行,通过函数getContentResolver().delete()进行删除,这个函数将返回删除的行数。

// Defines selection criteria for the rows you want to deleteString mSelectionClause = UserDictionary.Words.APP_ID + " LIKE ?";String[] mSelectionArgs = {"user"};// Defines a variable to contain the number of rows deletedint mRowsDeleted = 0;...// Deletes the words that match the selection criteriamRowsDeleted = getContentResolver().delete(    UserDictionary.Words.CONTENT_URI,   // the user dictionary content URI    mSelectionClause                    // the column to select on    mSelectionArgs                      // the value to compare to);

Provider Data Types (Provider的数据类型)

Content Provider可以提供很多数据类型。在User Dictionary Provider中只提供text,但provider还可以提供一下几种:

integer

long integer  (long)

floating point

long floating point (double)

另外一种数据类似是BLOB (Binary Large OBject), 是一个64KB的数组。你可以通过Cursor的“get”函数查看可用的数据类型。

在provider每列的数据类型都会在它的文档中列出来。User Dictionary Provider中的数据类型都列在它的contract class: UserDictionary.Words中。你也可以通过调用Cursor.getType()来查询数据类型。

在Provider会保留每个content URI的MIME数据类型。你可以使用MIME类型信息来判断你的app是否能够处理provider的数据,或者根据MIME的类型来选择相关的类型。当你的provider包含复制的数据结构和文件时,你通常需要知道MIME 类型。比如,ContactsContract.Data表格中Contacts Provider使用MIME类型来标注每行的contact数据类型。通过调用函数ContentResolver.getType()来获取content URI对应的MIME类型。

Alternative Forms of Provider Access (其它访问provider的方式)

在开发应用中,有三种provider访问方式:

Batch access(批量访问):你可以通过调用ContentProviderOperation类来创建批量访问,然后通过ContentResolver.applyBatch()运用它们。

Asynchronous queries (异步查询):你必须在另一个线程中进行查询。其中一种方式是使用CursorLoader对象。

Data access via intents:虽然你不能直接将一个intent发送给provider,你可以通过将intent发送给一个provider的app,这是用来修改provider数据最好的方式。

Batch access (批量访问)

对provider批量访问用于当要在provider插入大量的行或者在多个表格中插入行,即需要调用同一个函数多次。

为了能够在“批量模式”下访问一个provider,可以创建一个包含ContentProviderOperation对象的数组,然后通过函数ContentResolver.applyBatch()将这个数组派发给content provider。在这个函数中传入的是content provider的authority而不是一个特定的content URI。这使得数组中的ContentProviderOperation对象都能工作再不同的表中。ContentProviderOperation将返回包含结果的数组。

Data access via intents(通过intents访问)

intent可以提供对content provider的间接访问。即便你的app不能访问provider,你可以允许用户访问在provider中的数据,或者得到一个具有访问provider权限的app的intent,或者激活一个能够访问provider的app,让用户通过这个app进行操作。

Getting access with temporary permissions (获取临时访问权限)

即便你没有合适的访问权限,你也可以起访问content provider的数据,通过发生一个intent到一个又有权限的app中,这样返回的intent将具备访问权限。有些content URI的权限可以一尺持续到这个activity结束。在返回的intent中含有由具备永久权限的app赋予的临时权限。

Read permission(阅读权限):FLAG_GRANT_READ_URI_PERMISSION

Write permission(写权限):FLAG_GRANT_WIRTE_URI_PERMISSION

注意:这些权限只能访问对应的content URI,而不是整个provider的数据。

provider在它的manifest文件定义了content URI的权限,即在<provider>中添加android:grantUriPermission属性,还有<grant-uri-permission>权限。

比如,即便你没有READ_CONTACTS权限,你也可以从contacts provider中获取一个联系人信息。你可能想在一个app中发送一个生日祝福到一个联系人。除了可以请求READ_CONTACTS访问所有的联系人信息,你更倾向在你的app让用户自己控制那个联系人,可以通过下面步骤实现:

1. 设置intent的action为ACTION_PICK,包含contact的MIME 类型为CONTENT_ITEM_TYPE,将这个intent传给startActivityForResult()函数。

2. 因为这个intent符合People这个app中的“selection”activity的intent-filter,所以“selection” activity将会调到屏幕前端。

3. 在“selection” activity中,用户选择一个联系人后,“selection” activity将会调用setResult(resultcode, intent)返回给你app一个intent。这个intent包含用户选择联系人的content URI,和在“extras”中的flag FLAG_GRANT_READ_URI_PERMISSION。这些flag保证你的app具备访问指定content URI中的数据。设置完这个intent后,“selection”将调用finish()结束并返回到你的app。

4. 你的activity回到前端,系统将调用activity中的onActivityResult()函数,在这个函数的参数中含有返回的上述intent。

5. 有了上面的intent,拟具可以从Contacts Provider中读取数据,即便你没有在你的manifest文件中申请访问contact provider权限。有了联系人信息,你可以发送生日祝福了。

Using another application (使用其它的app)

如果用户想去修改自己没有权限访问的provider,那么唯一可做的就是通过其它具有权限的app修改provider的数据。

例如,在Calendar app中能接收一个action为ACTION_INSERT的intent,你就可以通过创建一个intent,在intent的“extras”中放入数据,并将这个intent传到Calendar app中,用来添加UI。因为recurring 时间的语法较为复杂,所以在Calentdar Provider中插入事件时激活Calentdar  app,然后让用户在这个app中添加时间。

Contract Classes 

在contract 类中定义了相关常量,用于帮助app与content URI, 列名,intent的action和其他content provider功能交互。在provider不是都有Contract class类,provider的开发者必须定义这个contract类并使得这个类对其他开发者可用。

例如,User Dictionary Provider有一个UserDictionary的contract类,这个类包含了content URI和列名常量。其中表里面的”words"对应的content URI定义在UserDictionary.Words.CONTENT_URI中,UserDictionary.Words中还包含列名。可以用于定义查询反射:

String[] mProjection ={    UserDictionary.Words._ID,    UserDictionary.Words.WORD,    UserDictionary.Words.LOCALE};

在Contacts Provider中也有对应的contract 类:ContactsContract。

MIME Type Reference(MIME类型引用)

Content Provider可以返回标准MIME类型或者自定义MIME类型。

MIME类型有下面的格式:

type/subtype
比例,比较出名的MIME类型是text/html,包含text类型和html子类型。如果provider返回这个类型,说明使用这个URI查询将会返回包含HTML标签的text。

自定义类型,也叫“vendor-specific”MIME类型,有着更复杂的类型和子类型。比如:

vnd.android.cursor.dir
用于多个行,又如:

vnd.android.cursor.item

用于当个行。

子类型是provider特定的。android的内部provider通常有单一的子类型。比如,当Contacts app创建一个行用来显示电话号码是,用于这个行的MIME类型如下:

vnd.android.cursor.item/phone_v2
注意子类型是phone_v2.

其他provider开发者可能会根据provider的authority和表格名字来创建自己的子类型,例如,当一个provider含有列车时刻表时,provider的authority是com.example.trains,包含三个表格的Line1, Line2, Line3,对于表格Line1的content URI如下:

content://com.example.trains/Line1

provider将会返回如下的MIME类型:

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

对于表格Line2的第五行,对应的content URI如下:

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

返回如下的MIME类型:

vnd.android.cursor.item/vnd.example.line2
大多数的content provider都会定义contract类常量用于MIME类型。比如Contacts Provider 的contract class:ContractsContract.RowContacts定义了常量CONTENT_ITEM_TYPE作为一个contact行的MIME类型。

更多关于content URI详见Content URIs。







 

0 0