1、概述
Content Providers是用来管理对结构化数据集的访问。它们把数据封装好,提供了一套定义数据安全的机制。Content Providers为不同应用程序之间数据的存储与访问提供了一个标准接口。
当我们想访问ContentProvider的数据时,可以在应用程序的上下文中,用一个ContentResolver的对象作为与ContentProvider交流的客户端。ContentResolver对象与ContentProvider对象相关联时,ContentProvider对象接收来自客户端的请求,执行请求的动作,然后返回结果。
如果我们不想与其他应用程序共享数据,则无需实现自己的ContentResolver。当需要在自己的应用程序里提供自定义的搜索建议则需要实现自己的ContentProvider,同样地,如果想将我们应用程序中复杂的数据或文件复制粘贴到其他的应用程序当中,也需要实现。
Android自带了一些ContentProvider,比如音频、视频、图片和联系人信息等(所有的都可以在android.provider包中查阅到)。使用一定的条件,所有的Android应用程序都可以访问它们。
2、ContentProvider基础
ContentProvider管理对中央数据存储库数据的访问,它是应用程序的一部分,主要是提供给其他应用程序一个使用自己应用程序数据的访问接口。providers和provider客户端共同提供了一个处理应用程序间通信和数据安全访问的统一标准接口。
2.1 原理
对于外部应用程序来说,ContentProvider呈现的数据是一张或多张类似于关系型数据库中的表。每一行代表了一个provider集合中众多数据类型的一个实例,一列代表了这个实例的一个独立的数据片段。
2.1.1 访问Provider
使用ContentResolver对象来访问数据,这个对象里有provider对象的同名方法。ContentResolver对象提供了对持久数据的CRUD(create,retrieve,update,delete)。
客户端应用程序中的ContentResolver对象和带有provider的应用程序中的ContentProvider对象会自动建立进程之间的通信。ContentProvider作为表的形式,扮演了数据存储库和外部数据表示之间的抽象层。
(注:要访问ContentProvider,应用程序需要请求特定的权限)
举例来说:
// 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)方法的参数说明:
query() 参数 查询关键字/参数 备注 Uri
FROM table_name
Uri
映射到provider中的表table_nameprojection
col,col,col,...
projection
查询列的集合.selection
WHERE col = value
selection
指定了查询条件selectionArgs
sortOrder
ORDER BYcol,col,...
sortOrder
给查询结果排序
2.1.2 Content URIs
Content URI是在provider中标识数据的一个URI,它包含了整个provider的符号名称(它的权限,比如user_dictionary)和指向的表名(路径,比如words),当调用访问provider表的客户端方法时,需要提供这个Content URI作为参数。
ContentResolver解析出URI的权限,并通过比较系统已知的providers表来决定哪个provider来处理,然后ContentResolver将查询参数分配到相应的provider。ContentResolver使用Content URI的路径部分来选择要访问的表。
content://user_dictionary/words
其中user_dictionary是provider的权限,words是表的路径,“content://”代表这是一个content URI。
许多Providers可以通过在URI的尾部加上一个ID值来获得表中的一行数据。比如,要从user_dictionary中取到_ID为4的行,我们应该用这样的URI:Uri singleUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);
(注:Uri和Uri.Builder类包含了从一个字符串中轻松构造出格式良好的Uri方法,ContentUris类包含了给Uri追加一个id的简便方法。)
2.2 从Provider中获取数据
(为了清晰起见,这里统一在UI线程里调用ContentResolver.query()方法,但在实践中,我们应该单独开辟一个线程,异步地执行查询操作。可以使用CursorLoader类来实现)
为了从Provider中获取数据,需要按以下步骤来执行:1、请求读provider的权限;2、定义发送查询给provider的代码。
2.2.1 请求读取访问权限
在manifest文件中使用<uses-permission>定义权限。
2.2.2 构造查询
下面举一个访问用户词典的例子来说明:// 定义投影的列 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 }; // 定义选择子句,where col=... String mSelectionClause = null; // 初始化一个包含选择条件参数的数组 String[] mSelectionArgs = {""};
下面举例说明查询子句和参数:/* * 定义包含选择子句参数值的数组. */ String[] mSelectionArgs = {""}; // 从输入框中获取一个单词 mSearchString = mSearchWord.getText().toString(); // 记得在此验证输入的有效性和安全性 // 如果单词为空,返回所有结果 if (TextUtils.isEmpty(mSearchString)) { // Setting the selection clause to null will return all words mSelectionClause = null; mSelectionArgs[0] = ""; } else { // 构造一个和用户输入相匹配的选择子句. mSelectionClause = UserDictionary.Words.WORD + " = ?"; // 将用户输入的参数复制给查询参数. mSelectionArgs[0] = mSearchString; } // 对表进行查询,并返回一个Cursor对象 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 // 有些provider发生错误时返回null,有些则抛出异常 if (null == mCursor) { /* * 处理错误的代码,这里不能使用Cursor,可以输出日志 * */ // 如果游标为空,没有查询结果 } else if (mCursor.getCount() < 1) { /* * 通知用户查询失败,可以选择提示用户插入新单词,或者重新查询 */ } else { // Insert code here to do something with the results }
2.2.3 阻止恶意输入
若Content Provider管理的数据是来自于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;
2.2.4 显示查询结果
查询结果返回一个Cursor对象,Cursor对象提供对行和列的随机访问,可以使用Cursor方法来迭代获取结果。有些特定的Cursor会在Provider数据改变时自动更新,或者触发观察着对象里的方法。
(注:Provider可能限制访问基于对象的性质进行查询结果的列,比如,Contacts Provider限制访问同步适配器中的某些列,因此它们不会返回给Activity或Service)
Cursor是查询结果行的列表,一个比较好的显示Cursor内容的方法是,通过SimpleCursorAdapter关联到ListView。如:// 定义要从Cursor对象获取结果的列名数组 String[] mWordListColumns = { UserDictionary.Words.WORD, // Contract class constant containing the word column name UserDictionary.Words.LOCALE // Contract class constant containing the locale column name }; // 定义为每一行接收Cursor列的View ID列表 int[] mWordListItems = { R.id.dictWord, R.id.locale}; // 创建一个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) // 为ListView设置适配器 mWordList.setAdapter(mCursorAdapter);
2.2.5 从查询结果中获取数据
举例说明:// 定义列"word"的索引值 int index = mCursor.getColumnIndex(UserDictionary.Words.WORD); /* * 只有Cursor有效时才能执行 */ if (mCursor != null) { /* * 移到Cursor的下一行,在最初时,Cursor的行指针是-1,必须先移到下一行才能获取数据,否则报错 */ 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 { // 错误处理 }
2.3 Content Provider权限
权限是让用户知道我们的应用程序想要访问的数据。如果一个provider应用程序不指定任何权限,其他的应用程序都不能访问这个provider的数据,但是即使不指定任何权限,provider应用程序里面的组件始终有全部的读写权限。
2.4 插入,更新,删除数据
和查询provider里的数据一样,我们用provider客户端和ContentProvider相互联系来修改数据,调用带参数的ContentResolver方法,这些参数传递给相应的ContentProvider。provider和provider客户端自动处理安全性和进程间的通信。
2.4.1 插入数据
可以使用ContentResolver.insert()方法来往provider里插入数据。这个方法往provider里插入新行,并返回那行的content URI。下面举例说明:// 定义一个Uri对象,来接收插入结果 Uri mNewUri; ... // 定义包含插入新值的对象 ContentValues mNewValues = new ContentValues(); /* * 设置每一列的值并插入 */ 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.putNull()给列设置空值即可。我们不需要指定_ID列,因为它是自动生成的,并在返回的content URI中给出了,我们可以使用ContentUris.parseId()来获得这个ID值。
2.4.2 更新数据
和插入数据一样,使用ContentValues对象来更新数据,和查询数据一样来设定选择条件。我们是通过调用ContentResolver.update(),只需要往ContentValues对象里插入我们需要更新的列的值,若需要清空一列的值,只需给这列设置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 URI mUpdateValues // the columns to update mSelectionClause // the column to select on mSelectionArgs // the value to compare to );
2.4.3 删除数据
删除行与获取行数据类似:首先设定好要删除行的查询条件,并且客户端方法返回要删除的行数。举例说明:// 定义要删除行的查询条件 String mSelectionClause = UserDictionary.Words.APP_ID + " LIKE ?"; String[] mSelectionArgs = {"user"}; // 定义一个存储删除行数的变量 int mRowsDeleted = 0; ... // 删除符合条件的行 mRowsDeleted = getContentResolver().delete( UserDictionary.Words.CONTENT_URI, // the user dictionary content URI mSelectionClause // the column to select on mSelectionArgs // the value to compare to );
2.5 Provider数据类型
Provider支持的数据类型主要有:
- integer——整型
- long integer (long)——长整型
- floating point——浮点型
- long floating point (double)——双精度浮点型
provider还提供了用一个64KB数组实现的Blob类型。
provider也为它们定义的每一个content URI维持了一个MIME(多用途因特网邮件扩展)数据类型。我们可以使用MIME类型信息来发现应用程序是否能够处理provider提供的数据,或者选择一个基于MIME类型的处理类型。当我们要处理复杂的数据结构或者文件时,就要用到MIME类型。我们可以使用ContentResolver.getType()来获得content URI的MIME类型。
2.6 可替代的Provider访问形式
- 批量存取:可以通过调用ContentProviderOperation类的方法来创建一个批量存取,然后使用ContentResolver.applyBatch()来完成。
- 异步查询:需要在一个单独开辟的线程里执行查询。可以通过CusorLoader对象来实现。
- 通过Intent来访问:虽然我们不能直接给provider发送一个Intent,但我们可以给provider应用程序发送一个Intent。这种方法通常被认为是修改provider数据最好的方法。
2.6.1 批量存取
当我们需要插入大量行的时候,或者在同一个方法里往多个表里插入数据时,或者执行一系列跨进程的操作的事务时,我们可以使用批量存取。
为了使用provider的“批量模式”,我们可以创建ContentProviderOperation对象数组,并通过ContentResolver.applyBatch()派发给一个content provider。我们需要给这个方法传递一个content provider的权限(而不是一个特定的content URI),这允许数组里的每一个ContentProviderOperation对象操作不同的表,并通过ContentResolver.applyBatch()。
2.6.2 通过Intent进行数据访问
Intent可以提供对content provider的间接访问,这样,即使应用程序没有访问权限,也可以允许用户访问provider的数据。可以从一个有访问权限的应用程序返回一个结果Intent,也可以激活一个有权限的应用程序并让用户在这个程序里面做相关的工作。
通过临时权限访问
我们可以给有权限的应用程序发送一个Intent并接收一个包含URI权限的结果Intent。当Activity接收到这些包含权限的content URI时,权限就结束了。拥有永久权限的应用程序可以通过给结果Intent设置一个标记来授予临时权限:
- Read permission:
FLAG_GRANT_READ_URI_PERMISSION
- Write permission:
FLAG_GRANT_WRITE_URI_PERMISSION
可以在manifest文件中,provider可以通过android:grantUriPermission给content URIs定义URI权限,或者给provider元素提供一个<grant-uri-permission>子元素。
比如,你想在联系人的生日时给他发送一个祝福,我们在这个应用程序里,即使没有访问用户联系人的权限,也可以指定这样一个联系人:
1、应用程序通过startActivityForResult()方法,发送一个包含ACTION_PICK 动作和contacts MIME类型的
CONTENT_ITEM_TYPE的Intent。
2、跳转到选择联系人的界面进行联系人的选择。
3、在这个选择界面,用户选择一个联系人来更新。当完成选择后,选择界面通过调用setResult(resultcode, intent)方法来建立一个返回到请求应用程序的Intent。这个Intent包含了一个用户选择联系人的content URI,和额外的标志FLAG_GRANT_READ_URI_PERMISSION,这些标记给应用程序授予了读取该content URI指向的联系人数据的权限。这个选择界面调用finish()方法,返回到该应用程序界面。
4、应用程序界面恢复到前台,系统调用该Activity的onActivityResult()回调方法,这个方法接收到了这个结果Intent。
5、通过结果Intent里的content URI,我们可以从Contact Provider中读取这个联系人的数据。这样,即使这个应用程序没有永久的联系人访问权限,也可以获取到某个联系人的生日或者E-mail信息,从而给他发送祝福。
使用另外一个应用程序
应用程序即使没有更改数据的存取权限,我们也可以通过激活另一个有权限的程序,用户在这个应用程序里做相应的工作。
例如,日历应用程序接收一个能够激活系统插入界面的ACTION_INSERT
Intent,我们可以在这个Intent放入系统用来填充UI的额外数据。因为重复事件的语法比较复杂,往Calendar Provider插入事件的首选方法是,通过ACTION_INSERT 激活日历应用程序,并让用户在这里插入事件。
2.7 合约类
合约类定义了一些用content URI、列名、Intent动作以及content provider的其他特征来帮助应用程序工作的常量。合约类不会自动包含在provider中,provider的开发者必须定义它们,然后让它们对其他的开发者是可利用的。Android平台包含许多提供器在android.provider包中都有对应的合约类。
例如,用户字典Provider有一个合约类UserDictionary,它包含了content URI和列名常量。针对“words”表的content URI是常量UserDictionary.WordsCONTENT_URI中定义了。UserDictionary.Words类也包含了在这个指南的例子中使用的列名常量。如,查询的投影可以这样定义:
String[] mProjection =
{
UserDictionary.Words._ID,
UserDictionary.Words.WORD,
UserDictionary.Words.LOCALE
};
另一个合约类是Contact Provider中的ContactsContract类。它的一个子类——ContactsContract.Intents.Insert是一个包含了Intent对象和Intent数据的常量的合约类。
2.8 MIME类型参考
Content Provider可以返回标准的MIME媒体类型,或自定义MIME类型字符串,或两者都返回。
MIME类型的格式为
type/subtype
例如,一个知名的MIME类型——text/html,拥有text类型和html子类型,如果Provider返回这个类型的URI,这就意味着使用这个URI进行查询将返回包含html标记的文本。
自定义的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,那么与表Line1的content URI:
content://com.example.trains/Line1
相对应,provider返回的MIME类型为:
vnd.android.cursor.dir/vnd.example.line1
表Line2第5行的content URI:content://com.example.trains/Line2/5 相对应,provider返回的MIME类型为:
vnd.android.cursor.item/vnd.example.line2
大多数的content provider都为它们使用的MIME类型定义了合约类常量。通讯录Provider的合约类常量ContactsContract.RawContacts,
为单个原始联系人行的MIME类型定义了一个常量
CONTENT_ITEM_TYPE。