转载请标注原文地址:http://blog.csdn.net/uranus_wm/article/details/11598471
翻译基于andriod4.0
android Content Provider详解
内容提供者-Content Provider
Content providers管理对结构化数据集的使用.它们封装数据,并提供了数据安全的机制.Content providers是从一个进程连接另一个进程中的数据的标准接口.
当你想使用一个content provider中的数据,你需在你的应用的Context 中使用ContentResolver对象作为客户端与provider 进行通讯.ContentResolver对象与provider对象通讯,provider是实现ContentProvider的类.Provider对象接收客户端发来的请求,执行请求的动作,返回结果.
如果你不想把你的数据共享给其它应用,你不需开发你自己的provider.然而,你需要自己的provider来在你的应用中提供自定义搜索建议.如果你需要从你的应用中考贝复杂的数据或文件粘贴到其它应用中,你也需要提供自己的provider.
Android自己包含了管理音频,视频,图像,个人通讯录等数据的content providers.你可以从android.provider 包的参考文档中爪到它们.这些providers 可以被所有的android应用使用,但可能带有一些限制.
Content Provider 基础
一个content provider 管理对中央数据仓库的使用.一个provider是一个Android应用的一部分,应用一般提供它自己的UI来操作数据.然而,content providers主要是为了给其它应用使用,其它的应用使用provider客户端对象来操作provider.providers 和provider客户端一起提供了一致的,标准的接口来操作用于进程间通讯的数据并保处数据的安全性.
本节讲解以下基础知识:
• content providers如何工作.
• 从content provider取得数据的API.
• 向content provider插入,更新以及删除数据的API.
• 其它有助于使用providers的API.
概述
一个content provider代表了面向外部应用的数据,这些数据看起来就像关系型数据库中的一个或多个table.一行代表某种数据类型的一个实例,一列代表这个实例的一个属性或字段.
举个例子,Android平台中的一个内建的provider是用户词典,它存储了用户想保存的非标准词的拼写.表1 演示了数据在provider的表中可能看起来的样子:
Table 1: 简单用户词典表
在上表中,每行代表了一个不能在标准字典中找到的词.每一列代表了这个词了一个属性.列头是存储在provider中的列的名字.要引用一行的locale属性,需引用locale 列.对于这个provider,_ID列作为"主键"列,provider会自动管理它.
注:一个provider不是必须具备主键的,并且也不是必须使用_ID 作为主键的列名来引用一行.然而,如果你把一个provider绑定到一个ListView,就必须有一个列名叫做_ID.此需求将在显示查询结果一节中有详细的解释.
操作一个provider
应用使用ContentResolver客户端对象来操作content provider中的数据.此对象具有一些与provider 对象中同名的方法,provider对象指的是某个ContentProvider具体派生类的实例.ContentResolver 的方法们提供了对存储数据的基本的"CRUD" (增查改删)功能.
ContentResolver 对象处于客户端应用的线程中,ContentProvider 对象位于另外的进程并且自动处理进程间通讯. ContentProvider 也代表了数据层与可视层之间的一个抽象层.
注:要使用一个provider,你的应用通常需要在manifest请求一些权限, 这将在Content Provider 权限一节中进行更详细的讲解.
举个例子,要从用户词典Provider中获取取单词和它们的locale列表,你需调用ContentResolver.query().query() 方法会调用用户词典中的ContentProvider.query() 方法.下面的代码演示了ContentResolver.query() 调用:
// 查询用户词典并返回结果 mCursor = getContentResolver().query( UserDictionary.Words.CONTENT_URI, // 单词表的content URI mProjection, // 每行要返回的列们 mSelectionClause // Selection的条件 mSelectionArgs, // Selection的条件 mSortOrder); // 返回各行要如何排序
表2展示了query(Uri,projection,selection,selectionArgs,sortOrder) 的参数们如何与一个SQL SELECT语句匹配:
Table 2: Query() 与SQL 查询的对比
Content URIs
content URI 是一个标志provider中的数据的URI.Content URI中包含了整个provider的以符号表示的名字(它的authority) 和指向一个表的名字(一个路径).当你调用一个客户端的方法来操作一个provider中的一个表,指向表的content URI是参数之一.
常量CONTENT_URI 中包含了用户词典table的content URI.ContentResolver 对象分析出URI的authority,并使用它与一个已知provider组成的系统表中的authority进行对比来"解决"provider.ContentResolver之后就会派送查询参数给正确的 provider.
ContentProvider使要长content URI的路径辨别分来选择要操作的表.通常一个provider中要暴露的每个表都具有一个路径.
在上面的例子的代码中,"词典"表的全URI是:
content://user_dictionary/words
user_dictionary部分是provider的 authority,words部分是表的路径.字符串 content:// (the scheme) 总是要存在,它表示引用一个content URI.
很多provider允许你通过在URI的末尾增加一个ID来操作表中一个单独的行.例如,要从用户词典中获取_ID是4的一行,你可要行使用这样的content URI:
当你要获取多行然后更新或删除其中一时,你经常要使用的是id值.
Uri singleUri = ContentUri.withAppendedId(UserDictionary.Words.CONTENT_URI,4);
注:Uri和Uri.Builder类包含由字符串构建格式正确的Uri对象的简便的方法们.ContentUris 包含向一个URI添加id值的简便方法们.上面的小代码片段就是使用了withAppendedId() 来向UserDictionary content URI添加id.
从Provider取得data
本节讲述了如何从provider取得数据,使用用户词典作为例子.
为了清析易懂,本节中调用ContentResolver.query()的代码片断置于"UI 线程"中.但是,在实际代码中,你应该在另一个线程执行查询动作,这样做的一种方法是使用CursorLoader 类.而,那几行示例代码仅是片断,它们不能展示一个完整的应用.
要从provider取得data,须依如下步骤:
1 请求provider的读权限.
2 定义发送请求到provider的代码.
请求读权限
要从一个provider中获取数据,你的应用需要对目标provider具有"读权限".你不能在运行时请求此权限,而只能在manifest文件中使用 <uses-permission> 元素指定你的权限需求.当你在manifest中指定此元素时,你实际上就是在为你的应用请求这个权限.当用户安装你的应用时,就表示同意了这个权限请求.
要找到你使用的provider读权限的所对应的准确名字,以及其它用于provider的权限的名字,请浏览provider的文档.
关于操作provider的权限的角色的更多信息,请见Content Provider权限一节.
用户词典Provider在它的manifest 中定义了android.permission.READ_USER_DICTIONARY 权限,所以一个想读取它内容的应用必须请求此权限.
构建请求
获取数据的下一步是构建一个请求(query).这里的第一个代码片段定义了一些用于操作用户词典Provider的变量:
// "projection" 定义了要返回的各列们 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 }; // 定义一个包含"select"条款的字符串 String mSelectionClause = null; // 初始化一个包含"select"参数的字符串 String[] mSelectionArgs = {""};
下一个代码片段演示了如何使用ContentResolver.query(),将用户词典Provider作为一个例子.一个provider客户端查询极像一个SQL查询,它包含了要返回的一坨column们,一堆筛选条件,和一个排序方式.
查询返回的column集合被称作projection (变量 mProjection).
指定返回的列的语句被分解为选择条款(策略)和选择参数两部分.选择条款是逻辑和布尔表达式,列名以及值的组合体(变量mSelection).如果你在使用可替换参数 ? 来代表一个mSelection值,查询方法就会从选择参数部分作为限定条件来获取结果(变量mSelectionArgs).
在下一个代码片段中,如果用户没有输入单词,选择条款就被设为null,并且查询会返回provider中所有的单词.如果用户输入了单词,选择条款就被设置为UserDictionary.Words.Word + " = ?" 并且选择参数(数组)的第一项被设置为用户输入的单词.
/* * 定义一个一维的字符串数组来容纳选择参数们 */ String[] mSelectionArgs = {""}; // 从界面中获取一个单词 mSearchString = mSearchWord.getText().toString(); // 记住要在此插插入代码检查不合法的或恶意的输入. // 如果单词是空的,则获取所有数据 if (TextUtils.isEmpty(mSearchString)) { // 设置选择条款为null就会返回所有单词 mSelectionClause = null; mSelectionArgs[0] = ""; } else { // 构造一个匹配用户输入的单词的选择条款 mSelectionClause = " = ?"; // 将用户输入的单词置于选择参数中 mSelectionArgs[0] = mSearchString; } // 执行查询并返回游标对象 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) { /* * 在此插入代码处理错误.记住不要使用游标! 你可能要调用 * android.util.Log.e()把错误记录的日志 * */ // 如果游标是空的,找不到匹配的provider } else if (mCursor.getCount() < 1) { /* * 在此插入代码来通知用户,查找不成功.这也不能完全算是个错误.你可能想为用户提供插入一个新行或重新输入查询单词的选项 */ } else { // 在此插入代码,利用返回的结果做想做的事 }
查询与下面的SQL语句等价:
SELECT _ID, word, frequency, locale FROM words WHERE word = <userinput> ORDER BY word ASC;
在此SQL 语句中,以实际的列名代替了内置的类别常量.
防止恶意输入
如果被content provider管理的数据是一个SQL 数据库,在原始的SQL语句中包含不可信的数据会导致恶意SQL注入.
思考以下选择条款:
// 通过连接用户输入到列名来构造一个选择条款 String mSelectionClause = "var = " + mUserInput;
如果你这样做,你就允许用户连接恶意的SQL语句到你的SQL语句中.例如,用户可以输入"nothing; DROP TABLE *;" ,这将在选择条款中变为 var = nothing; DROP TABLE *;..既然选择条款被作为SQL语句,这就可能导致provider删除SQLite数据库中的所有的表(除非provider被设置成捕获SQL injection 阴谋).
要避免此问题,应使用一个运用?作为可替换参数的选择条款和一个作为选择参数的数组.当你这样做时,用户输入被直接绑定到查询而不是被解释为SQL语句的一部分.因为它不被认为是SQL,于是用户输入就不能注入恶意SQL.使用以下选择条款来代替连接用户输入的那个:
// 构造一个带有占位符的选择条款 String mSelectionClause = "var = ?";
像这样建立起选择参数数组:
// 定义一个数组来容纳选择参数 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可能跟据构建查询的对象的性质限制对某些列的操作.例如,联系人Provider会禁止同步适配器操作某些列,所以它不会把它们返回给一个activity或service.
如果没有符合选择条件的行,provider返回一个Cursor 对象,其Cursor.getCount() 为0 (一个空cursor).
如果一个内部错误发生,查询结果会因provider的不同而不同.它可能返回null,也可能抛出一个Exception.
既然一个Cursor 是行组成的"列表",那么一个显示Cursor 内容的好方法就是把它链接到一个ListView 上,通过SimpleCursorAdapter.
下面的代码片段是衔接前面的代码来的.它创建一个SimpleCursorAdapter 对象,包含有查询返回的Cursor ,然后设置这个对象为ListView的适配器.
// 定义要从Cursor取出的并要加载到view中的列们 String[] mWordListColumns = { UserDictionary.Words.WORD, // Contract class constant containing the word column name UserDictionary.Words.LOCALE // Contract class constant containing the locale column name }; // 定义一个View ID组成的列表,它们将接收每行的Cursor列的值 int[] mWordListItems = { R.id.dictWord, R.id.locale}; // Creates a new SimpleCursorAdapter mCursorAdapter = new SimpleCursorAdapter( getApplicationContext(), // The application's Context object R.layout.wordlistrow, // ListView的一行的layout 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);
注:要使Cursor支持ListView,cursor必须包含一个叫做_ID的列,因此,上面所示的查询从"单词"表中取出了_ID ,当然ListView 可以不显示它.这条限制同时也解释了为什么大多数provider在它们的表中都具有一个_ID 列.
从查询结果中获取数据
你可以使用查询结果做更多事情,而不是仅简单地显示它们.比如,你可以从用户词典中获取拼法然后在其它provider中查找它们.要这样做,你需在Cursor中迭代所有的行.
// 获取叫做"word"的列的序号 int index = mCursor.getColumnIndex(UserDictionary.Words.WORD); /* * 仅在cursor有效时执行下面语句.如果发生内部错误,用户词典Provider返回null. * 其它provider可能抛出一个异常而不是返回null. */ if (mCursor != null) { /* * 移到cursor中的下一行.在第一次移动之前, * "行指针" 为-1,并且,如果你想获取那个位置的数据,你将得到一个异常 */ while (mCursor.moveToNext()) { // 从列中获取值. newWord = mCursor.getString(index); // 在此插入代码处理获取到的单词 ... // 循环结束 } } else { // 如果cursor为null或前面抛出了异常,在此处插入代码报告错误. }
Cursor 的实现包含了多个"get" 方法,用于从对象中获取不同类型的数据.例如,前面的代码片段使用getString().它们也具有一个getType() 方法,用它可以返回的值代表了数据的类型.
Content Provider 的权限
一个具有provider的应用可以指定其它要操作自己的数据所应具有的权限.这些权限保证了用户能了解一个应用将要操作那个数据.其它应用需基于provider的需求请求相应的权限.用户在安装应用时会看到它们所请求的权限.
如果一个provider的应用没有指定任务权限,那么其它应用就不能操作provider的数据.然而,provider所在的应用的组件们却具有完整的读写权限,而不管是否指定了权限.
如上面所提到的,用户词典Provider需要android.permission.READ_USER_DICTIONARY 权限来从它取得数据.Provider具有另一个android.permission.WRITE_USER_DICTIONARY权限,代表了插入,更新或删除的权限.
要获取操作一个provider的权限,应用需在自己的manifest文件中使用<uses-permission> 元素.当Android包管理器安装这个应用时,用户必须批准所有的权限请求.如果用户批准了所有的权限请求,包管理器会继续安装这个应用;如果没有,包管理器就会取消安装过程.
下面的<uses-permission> 元素请求对用户词典的读权限:
<uses-permission android:name="android.permission.READ_USER_DICTIONARY">
Provider操作权限的作用在指南安全和权限一节中有详细的描述.
插入更新删除数据
用从provider取得数据相同的方法,你也可以让provider 客户端与provider的ContentProvider以交互方式修改数据.你调用一个ContentResolver 的方法,其参数是要传给 ContentProvider对应方法的.Provider与provider客户端自动处理安全问题和进程间通信问题.
插入数据
要向一个provider中插入数据,需调用ContentResolver.insert() 方法.此方法插入一个新行到provider中并且返回一个代表这一行的content URI.下面的代码片段演示了如何将一个新行插入到用户词典中:
// 定义一个新的Uri对象,用于接收插入后的返回值 Uri mNewUri; ... // 定义一个对象来包含要插入的值们 ContentValues mNewValues = new ContentValues(); /* * 设置要插入行的每列的值."put"方法的参数是"column name"和"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, // 用户词典的content URI mNewValues // 要插入的值们 );
新行的数据被置入一个ContentValues 对象,就像构建一个单行cursor.对象中的列们不必都是相同的数据类型,并且如果你不想指定某列的值,你可以设置一个列为null ,使用ContentValues.putNull().
此代码片段中没有添加_ID 列,因为此列是被自动维护的.Provider会为每个添加的新行分配一个唯一的_ID 值.Provider总是把它用作表的主键.
返回的content URI newUri 代表了新添加的行,以下面的形式:
content://user_dictionary/words/<id_value>
<id_value> 是新行的 _ID 的值.大多数可以自动检测content URI 的格式然后执行对此行的请求的操作.
要从返回的Uri,获得_ID 的值,调用ContentUris.parseId().
更新数据
要更新一行,你可以使用一个ContentValues 对象,向它填充要更新的值,就像你插入时做的,并且选择条件跟查询时是一样的.你应使用的客户端方法是ContentResolver.update().你只需把要更新的列的值添加到ContentValues 对象.如果你想去清空一列的内容,设置其值为null.
下面的片段改变所有语言列中带有"en"的行,把其locale置为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 );
删除数据
删除行与获取行的方式相似:你为想要更新的行指定选择条款,然后客户端方法就会返回被删除的行数.下面的代码片段删除那些appid等于"user"的行们.返回被删除的行数.
// 定义要删除的行们的选择条款 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 );
Provider数据类型
Contentproviders能识别多种不同的数据类型.用户词典只识别文本类型,但其它provider可以识别下列格式:
• 整型 • 长整型(long) • 浮点 • 长浮点(double)
另一个provider经常使用的数据类型是"二进制大数据对象"(BLOB) ,它就像一个64KB的字节数组.你可以通过Cursor类的"get"方法查看可用的数据类型.
provider中的每列的数据类型都是在它们的文档中列出.用户词典Provider的各数据类型在它的契约类UserDictionary.Words(契约类在ContractClasses一节中讲解)的参考文档中列出.你也可以调用Cursor.getType()来确定数据类型.
Provider还为每个contentURI维护着MIME数据类型信息.你可以用MIME信息来确定你的应用是否能处理provider提供的信息,或基于MIME信息去选择一个要处理的数据类型.当你使用包含复杂数据结构或文件的provider时,你经常需要MIME信息.例如,联系人Provider中的表ContactsContract.Data用MIME类型来标志所存储的联系人数据的每行类据.要获得一个contentURI的MIME类型,需调用ContentResolver.getType().
Thesection MIME类型参考一节描述了标准的和自定义的MIME类型语法.
操作Provider的三种型式
在应用开发中,有三种很重要的操作provider的型式:
• Batchaccess:你可以使用ContentProviderOperation类的方法创建一批操作调用,然后使用ContentResolver.applyBatch()应用它们. • Asynchronousqueries: 你需要在另一个线程中执行请求.这样做的一个办法使用一个CursorLoader对象.在Loaders指南中的例子演示了如何做. • Dataaccess via intents:尽管你不能直接向一个provider发送intent,但你可以向provider的应用发送intent,这通常是修改provider'的数据的最佳方法.
通过intent批量操作和修改的方法在下面的章节中讲解.
批操作
对一个provider的批操作在插入很大数量的行时很有用,也可以用于在一个方法调用中向多个表中插入多行,或用于将一系列操作作为一个事物执行时(一个原子操作).
要在"批量模式"下操作一个provider,你需创建一个ContentProviderOperation对象的数组然后使用ContentResolver.applyBatch()把它派送到一个contentprovider.你应把contentprovider的authority而不是一般的contentURI传给这个方法,authority允许数组中的每一个ContentProviderOperation对象作用于不同的表.对ContentResolver.applyBatch()的调用会返回一个数组存放结果.
ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>(); operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI) .withValue(RawContacts.ACCOUNT_NAME, accountName) .withValue(RawContacts.ACCOUNT_TYPE, accountType) .withValue(RawContacts.DATA_SET, dataSet) .build()); int size = valueList.size(); for (int i = 0; i < size; i++) { ContentValues values = valueList.get(i); values.keySet().retainAll(ALLOWED_DATA_COLUMNS); operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI) .withValueBackReference(Data.RAW_CONTACT_ID, 0) .withValues(values) .build()); } ContentResolver resolver = getContentResolver(); ContentProviderResult[] results; try { results = resolver.applyBatch(ContactsContract.AUTHORITY,
契约类ContactsContract.RawContacts的描述中包含一个代码片段演示了批插入.ContactManager应用例子包含了一个批操作的例子,在它的ContactAdder.java源码文件中.
使用其它应用显示数据
如果你的应用拥于操作权限,但你可能仍想使用一个intent在其它应用中显示数据.例如,日历应用接受一个ACTION_VIEW intent,然后显示一个日期或事件.这使你可以显示一个日期信息但不用自己创建界面.更多知识,请看CalendarProvider指南.
你发送intent的目标应用不一定必须是与provider相关连的应用.例如,你可以从联系人Provider获取一个联系人,然后发送一个包含了联系人图像的contentURI的ACTION_VIEW intent到一个图像显示应用.
通过intent操作数据
Intents可以提供间接操作contentprovider的功能.于是你的应用可以操作不具有权限的provider中的数据.这既可以通过从有权限的应用中获取一个返回的intent实现,也可以通过激活一个具有权限的应用来让用户操作方式来实现.
使用临时权限操作
即使你不具有合适的权限,你还是可以操作contentprovider中的数据的,只需把一个intent发送给一个具有权限的应用然后获得一个返回的包含"URI"权限的intent即可.获得的特定的URI的权限会一直持续到获取它们的activity结束.那些具有权限的应用通过在要返回的intent中设置一个标志来赋与临时权限:
• 读权限:FLAG_GRANT_READ_URI_PERMISSION • 写权限:FLAG_GRANT_WRITE_URI_PERMISSION
注:这些标志不会给于对contentURI中包含的authority对应的provider的读写权限.操作仅仅针对当前URI.
一个provider在它的manifest中定义contentURI们的URI权限,使用<provider>元素的android:grantUriPermission属性和<provider>子元素的<grant-uri-permission>属性.URI权限机制在Securityand Permissions指南中的"URI权限"一节中做了详细的描述.
例如,你可以从ContactsProvider中取得一个联系人数据,即使你不具有READ_CONTACTS权限.你可能想在一个在某人生日时向他发送电子贺卡的应用中这样做.你首选的是让用户仅控制你的应用使用的联系人们,而不是直接请READ_CONTACTS权限以操作所有的联系人和他们所有的信息.要这样做,需执行以下过程:
1. 你的应用发送一个intent,它包含了action ACTION_PICK和"contacts"MIME 类型CONTENT_ITEM_TYPE,使用方法startActivityForResult(). 2. 因为这个intent匹配了People app的"selection" activity,activity将被置于前台. 3. 在selection activity中,用户选择一个contact来更新.当这样做时,selection activity调用setResult(resultcode,intent)来创建一个intent返回给你的应用.Intent中包含了用户选择的联系人的contentURI和"extras"标志FLAG_GRANT_READ_URI_PERMISSION.这个标志赋与你的应用URI权限以从URI指定的联系人中读取数据.这个selection activity之后调用finish()将控制返回给你的应用. 4. 你的activity返回到前台,并且系统调用你的activity的onActivityResult()方法.这个方法接收被People app中的selection activity 创建并返回的intent. 5. 使用返回的intent中的contentURI,你可以从ContactsProvider读contact data,即使你没有在你的manifest中请求永久性的Provider读权限.然后你就可以获取联系人的生日信息和他的email地址然后给他发送电子贺卡.
使用其它的应用
一个使用户修改你的应用不具有权限的数据的简单方法是激活一个有权限的应用,然后让用户在它之上进程操作.
举个例子,日历应用接收一个ACTION_INSERT intent,使你激活日历应用的插入界面.你可以在这个intent中传递"额外的"数据,应用用它来预置界面.因为返回的事件具有复杂的语法,所以向日历Provider中插入事件的最好的方式就是使用一个ACTION_INSERT激活Calendar应用然后让用户在它上面插入事件.
契约类
契约类定义了帮助应用使用content URIs的常量,列名,intent action们,和其它contentprovider特性。契约类不会被provider自动包含;provider开发者必须定义它们然后使它们对其它开发者生效。Android平台中包含的很多provider都在包android.provider中有对应的契约类。
例如,用户词典Provider具有一个契约类UserDictionary,它包含了contentURI和columnname常量。"words"表的contentURI定义在常量
UserDictionary.Words.CONTENT_URI上。UserDictionary.Words类也包含了列名常量,在下面的代码片段中就用到了。举例,一个查询projection可以如下定义: String[]mProjection = { UserDictionary.Words._ID, UserDictionary.Words.WORD, UserDictionary.Words.LOCALE };
另一个契约类是用于联系人Provider的ContactsContract类。此类的参考文档中包含示例代码片段。它的一个子类,ContactsContract.Intents.Insert,也是一个契约类,它包含了用于intent和intent数据的契约。
引用MIME类型
Contentprovider可以返回标准的MIME类型或自定义的MIME类型字符串,或同时返回两者。
MIME类型具有以下形式
type/subtype
例如,世人皆知的MIME类型text/html具有text类型和html子类型。如果provider从一个URI返回此类型,这表示面向此URI的查询将返回带有HTML标记的文本。
自定义的MIME字符串,也被叫做"vendor-specific"MIME类型,具有更复杂的类型和子类型值,此类型的值总是这样
vnd.android.cursor.dir
用于多行,或这样
vnd.android.cursor.item
用于单行。
而子类型是每个provider都不相同的。Android内置的provider通常具有一个简单的子类型。例如,当联系人应用为一个电话号码创建一个新行时,它为新行设置下面的MIME类型:
vnd.android.cursor.item/phone_v2
可以看到子类型的值只是简单的phone_v2.
其它provider开发者可能基于provider的authority和表名字创建他们自己的子类型式样。例如,假设一个provider含有列车时刻表,provider的authority是com.example.trains,并且它包含三个表:线路1,线路2,线路3。相对应URI为:
content://com.example.trains/Line1
指向线路1表,provider返回对应的MIME类型为:
vnd.android.cursor.dir/vnd.example.line1
而content URI:
content://com.example.trains/Line2/5
指向线路2表的第5行,provider返回的对应的MIME类型为:
vnd.android.cursor.item/vnd.example.line2
大多数contentproviders定义了契约类来包含它们所用到的MIME类型。例如,联系人Provider的契约类ContactsContract.RawContacts,定义了常量CONTENT_ITEM_TYPE,它对应于一行原始的联系人数据。
创建一个Content Provider
content provider管理对中央数据仓库的存取。你实现一个provider,就是在一个Android应用中实现一个或多个类,再加上manifest文件中的一些元素。你实现一个 ContentProvider的子类,它作为你的provider和其它应用之间的接口。尽管content providers的目的是向其它应用提供数据,但当然也可以在你自己的应用中创建activity允许用户来查询和修改你的provider所管理的数据。
创建之前的准备工作
在创建一个provider之前,需做以下工作:
1. 确定你是否需要一个content provider。你如果需要提供一个或多个下列特性,你就需创建一个content provider:
你想向其它应用提供复杂数据或文件。
你想让用户从你的应用复制复杂的数据到其它应用。
你想根据框架层的search类提供自定义的搜索功能。
如果完全是在你的应用内部使用,你的provider不需使用 SQLite数据库。
2. 如果你还未作出决定,请阅读主题 Content Provider Basics 来进一步了解provider。
下一步,按以下步骤来创建你的provider:
1. 为你的数据设计原始存储方式。一个content provider以两种方式提供数据:
文件数据
指那些存储在文件中的数据,比如图片,视频,音频等。把文件们存储在你的应用的私有空间。为响应从其它应用发来的对某个文件的请求,你的provider可以为文件提供一个句柄。
"结构化" 数据
指那些存储于数据库中的数据、数组或小型结构。数据以兼容于表的形式存储。一行代表一条数据,就像一个人员或条目清单中的一条。一列代表一条数据中的一部分数据,比如人的名字或条目的价格。存储这些类型的数据的一个常用方法是使用SQLite数据库,但是你也可以使用其它形式。要了解android 系统中更多的存储方式,见 设计数据存储一节。
2. 具体定义ContentProvider 类和它的方法。此类是你的数据与Android系统中其它东西的接口。要进一步了解此类,见实现ContentProvider类 一节。
3. 定义provider的authority字符串,content URIs,和列的名字。如果你想让provider的应用处理intent,还要定义intent 的action、附加数据和标志。还要定义对那些要访问你的数据的应用所需具有的权限。你应该考虑把这些值作为契约定义到另外一个单独的契约类中。以后就可以向其它的开发者展示这个类。更多关于content URI的信息,见 设计Content URI.一节。更多关于intent的信息,见Intent和数据操作一节。
4. 添加其它可选内容,比如样本数据或实现 AbstractThreadedSyncAdapter 以实现provider和基于云的数据之间的数据同步。
定义数据存储
一个content provider是一个结构化存储的数据接口。在你创建这个接口之前,你必须确定如如何存储数据。你可以以任何你喜欢的方式存储数据,然后设计读写数据的接口。
下面是一些在android中可用的数据存储技术:
1 Android系统中包含一个 SQLite数据库API,Android自己的provider使用它来存储表格类的数据。SQLiteOpenHelper类帮助你创建数据库,而类SQLiteDatabase是操作数据库的基础类。
记住你不是必须使用一个数据库来实现你的数据仓库。一个 provider在外部的表现就像一个表的集合,类似于一个关系型数据库,但是provider的内部实现是可以与此不同的。
2 对于数据存储,Android具有各种各样的面向文件的API。要更多的了解文件存储,请阅读主题数据存储。如果你 设计的provider要提供媒体相关的数据,比如音频和视频,你可以在provider中将表数据和文件混合来使用。
3 要处理网络数据,应使用java.net 和 android.net中的类。你也可以同步网络数据到本地数据存储中(比如数据库中),然后以表或文件的形式提供此数据。例子Sample Sync Adapter 演示了这种同步技术。
思考数据设计
下面是一些设计你的provider的数据结构的技巧:
1 表数据应总是具有一个"主键"列,它被用于管理一个代表每各行的唯一数值。你可以使用这个值链接某行到其它表中的相关行 (也就是"外键")。尽管你可以为此列取任何名字,但使用BaseColumns._ID 才是最佳选择,因为当把一个provider的查询结果关联到一个 ListView 时,需要有一个列叫做 _ID。
2 如果你想提供位图图像或其它的非常大的面向文件的数据,那么应把它存于文件中,然后间接的提供它,而不是直接把数据存储于一个表中。如果你这样做了,你还需要告诉你的provider的用户,他们需要使用一个ContentResolver 的文件方法来操作数据。
3用大二进制对象 (BLOB)数据类型来存储大小不定或没有固定结构的数据。例如,你可以使用一个 BLOB列来存储 协议缓冲 或 JSON 结构。
你也可以使用一个BLOB来实现一个独立模式的表。在此种表中,你定义一个主键列,一个MIME类型列,和一个或多个普通的BLOB列。在 BLOB 列中的数据的意义由MIME列中的值所表明。这使你可以在一个表的各行中存储不同类型的数据。联系人Provider的"data"表ContactsContract.Data ,就是一个独立模式表的例子。
设计内容URIs
一个内容URI 标志一个provider中的数据。内容URI包含了整个provider (他的 authority)的符号名和一个指向某个表或文件的名字。可选的id部分指向表中的一个独立的行。ContentProvider 的每个数据操作方法具有一个内容URI作为一个参数;这允许你决定要操作的表,行或文件。
内容URI的基础在标题Content Provider基础 中被描述。
设计一个authority
一个provider通常具有单个authority,这作为它的安卓内部名字。为了避免与其它provider冲突,你应该使用互联网域名方式取名 (不过是倒着的)来作为你的provider authority的基础名字。因为此种建议也用于Android包的名字,所以你可以定义你的provider authority作为包含此provider的包名的扩展。 例如,如果你的Android包名是 com.example.<appname>,你应为你的provider authority命名为 com.example.<appname>.provider。
定义一个路径结构
开发者通常通过添加指向独立表的路径来从authority 创建content URI。例如,如果你具有两个表 table1和table2,你从前面例子中的authority 合并出来的内容 URI为 com.example.<appname>.provider/table1 和com.example.<appname>.provider/table2。路径中并不限制只有一个参数,并且路径的每一层也不是必须是一个表。
处理内容URI的 ID
为了方便,provider接受把一行的ID放在URI的尾部来提供对表中某一行的访问。同样为了方便,provider比较此 ID和表的_ID列,然后执行对匹配行的操作请求。.
这种方便性有助于在应用操作一个provider时使用一种通用的设计模式。应用向provider发出查询然后在一个ListView 中使用一个CursorAdapter 显示结果 Cursor 。CursorAdapter 的定义需要Cursor 中有一个列叫做 _ID 。
用户之后就可以在UI中显示的行中选择一行进行查看或修改数据。app之后从ListView 背后的Cursor 中获得相应的行,再获得行的 _ID值,把此值添加到内容URI上,再把操作请求发送给provider。provider之后就执行查询或修改用户所指定的行。
内容URI的模式
为了帮助你根据得到的内容URI 选择要执行的动作,provider API包含了简便的类UriMatcher,它映射内容URI 的"模式" 到一个整数值。你可以在一个switch 语句中使用这个整数值来选择为匹配一种模式的某个内容URI或多个URI们执行某种动作。
一个内容 URI使用通配符来匹配其它内容URI们:
*: 匹配任意长度的任意有效字符串。
#: 匹配由数字组成的任意长度的字符串。
看一个设计和编码处理content URI的例子,假设一个provider其authority是 com.example.app.provider ,从它可以识别以下指向各表的内容URI:
1 content://com.example.app.provider/table1: 一个叫table1 的表。 2 content://com.example.app.provider/table2/dataset1: 一个叫dataset1 的表。 3 content://com.example.app.provider/table2/dataset2: 一个叫dataset2 的表。 4 content://com.example.app.provider/table3: 一个叫做table3 的表。
provider也识别那些具有一行的ID的内容URI,例如 content://com.example.app.provider/table3/1 指向表table3中的行1.
下面的内容 URI 模式也可能出现:
content://com.example.app.provider/*
匹配provider 中的任务内容URI。
content://com.example.app.provider/table2/*:
匹配表dataset1和表dataset2中的一个内容URI,但是不匹配table1或table3中的内容URI。
content://com.example.app.provider/table3/#: 匹配表 table3 中的任意一行,比如 content://com.example.app.provider/table3/6 指向行6.
下面的代码片段演示了UriMatcher 中的方法们如何工作。此代码通过使用内容URI模式content://<authority>/<path> 指向表,用content://<authority>/<path>/<id> 指向一个单独行,以不同的方式来处理指向整个表的 URI和指向单行的URI 。
方法addURI() 映射一个authority和路径到一个整数值。方法android.content.UriMatcher.match(Uri)返回一个URI对应的整数值。一个switch 语句从查询整个表和查询单条记录之间作出选择:
[java] view plaincopyprint? 1. public class ExampleProvider extends ContentProvider { 2. ... 3. // 创建一个UriMatcher对象 4. private static final UriMatcher sUriMatcher; 5. ... 6. /* 7. * 到这里,是对addURI()的调用。provider应能识别所有的内容URI的模式。 8. * 但在此片段中,只演示对表3的调用。 9. */ 10. ... 11. /* 12. * 映射表3中的多行模式为1 。注意路径中没有用通配符。 13. */ 14. sUriMatcher.addURI("com.example.app.provider", "table3", 1); 15. /* 16. * 映射单行模式为2 。 此例中,通配符"#"被使用。 17. * "content://com.example.app.provider/table3/3" 能匹配此模式,但是 18. * "content://com.example.app.provider/table3就不能了。 19. */ 20. sUriMatcher.addURI("com.example.app.provider", "table3/#", 2); 21. ... 22. // 实现ContentProvider.query() 23. public Cursor query( 24. Uri uri, 25. String[] projection, 26. String selection, 27. String[] selectionArgs, 28. String sortOrder) { 29. ... 30. /* 31. * 选择一个要查询的表并跟据为输入URI返回的代码来选择排序顺序。 32. * 这里也是只演示表3 。 33. */ 34. switch (sUriMatcher.match(uri)) { 35. // 如果输入URI指向整个表3 36. case 1: 37. if (TextUtils.isEmpty(sortOrder)) sortOrder = "_ID ASC"; 38. break; 39. // 如果输入URI指向单行 40. case 2: 41. /* 42. * 因为这个URI指向单行,所以_ID部分就出现了。 43. * 从URI获取最后最后面的路径段;它就是_ID值。 44. * 然后,添加这个值到查询语句的WHERE子语句中。 45. */ 46. selection = selection + "_ID = " uri.getLastPathSegment(); 47. break; 48. default: 49. ... 50. // 如果URI不能被识别,你应该在此处做一些错误处理。 51. } 52. // 调用进行实际查询的代码 53. }
另一个类,ContentUris, 提供处理内容URI中的id部分的简便方法。类Uri 和 Uri.Builder 包含了分析已存在的Uri 对象和创建新对象的简便方法。
实现ContentProvider类
ContentProvider 实例管理对一个结构型数据集的操作以处理从另外一个应用发来的请求。所有的操作最终都调用ContentResolver,然后它又调用ContentProvider 的一个具体的方法。
查询方法们
抽象类 ContentProvider 定义了六个抽象方法,你必须在你的派生类中实现它们。这些方法们,除了 onCreate() ,都会被想要操作你的content provider的客户端应用调用:
query()
从你的provider获取数据。使用参数来指定要查询的表,要返回行和列,和结果的排序方式。返回一个 Cursor 对象。
insert()
向你的provider插入一个新行。参数们指定了要选择的表和要插入的列的值。返回一个指向新行的content URI。
update()
更新你的provider中已存在的行。参数中指定了要选择的表和要更新的行以及要更新的列数据。返回已更新行的数量。
delete()
从你的provider中删除行。参数们指定了要选择是表和要删除的行们。返回已删除行的数量。
getType()
返回对应一个content URI的MIME类型。
onCreate()
初始化你的provider。Android系统在创建你的provider之后立即调用此方法。注意你的Provider直到一个ContentResolver对象要操作它时才会被创建。
你要实现这些方法们,需要负责以上事情:
• 除了 onCreate() 所有的这些方法们都可以被多线程同时调用。所以它们必须是多线程安全的。
• 避免在onCreate() 中进行耗时的操作。推迟初始化工作直到真正需要做的时候。
• 尽管你必须实现这些方法们,你的方法除了返回期望的数据类型外,并不是需要做所有的事情。例如,你可能想阻止其它应用向某些表中插入数据,你就可以不理会对insert() 的调用并返回0.
实现query()方法
ContentProvider.query() 方法必须返回一个 Cursor 对象,或者,如果它失败了,抛出一个 Exception。如果你使用一个SQLite数据库作你的数据存储,你可以简单的返回从 SQLiteDatabase 类的query() 方法返回的Cursor 对象。如果查询不到任何行,你应该返回一个Cursor 的实例,但它的getCount() 方法返回0。你应该只在查询过程中发生内部错误时才返回null。
如果你不用SQLite数据库作为你的数据存储,那么需使用 Cursor的派生类。例如MatrixCursor 类,它实现了一个 cursor,cursor中的每一行都是一个Object的数组。这个类使用 addRow() 添加一个新行。
记住Android系统必须能跨进程传送Exception 。Android可以为以下异常做这些。这些异常可能在处理查询错误时有帮助:
• IllegalArgumentException (如果你的provider收到一个不合法的content URI,你可以要抛出它)
• NullPointerException
实现insert()方法
insert() 方法向适当的表添加一个新行,使用的值都存放在ContentValues参数中。如果一个列的名字不在在 ContentValues 参数中,你可能要为此列提供一个默认的值,这可以在provider的代码中做也可以在数据库表中做。
此方法应返回新行的content URI。要构建此URI,使用withAppendedId()向表的content URI的后面添加新行的_ID (或其它的主键)的值即可。
实现delete()方法
delete() 方法不必在物理上从你的数据存储中删除行。如果你正在为你的provider用一种同步适配器,你应该考虑把一个要删除的行打上"delete"标志而不是把行整个删除。同步适配器可以检查要被删除的行们并且在provider中删除它们之前先从server删除它们。
实现update() 方法
update() 方法带有与insert()相同的参数类型ContentValues ,以及与delete() 和ContentProvider.query()相同的selection 和selectionArgs参数。这可能使你能在这些方法之间重用代码。
实现onCreate() 方法
Android 系统在启动provider后调用onCreate() 。你应该在此方法中只执行不耗时的初始化任务,并且推迟数据库创建和数据加载工作,直到provider直正收到对数据的请求时再做。如果你在onCreate()中做了耗时的工作,你将减慢provider的启动。相应的,这也会减缓从provider到其它应用的反应速度。
例如,如果你正在使用一个SQLite 数据库,你可以在ContentProvider.onCreate()中创建一个新的SQLiteOpenHelper 对象,然后在第一次打开数据库时创建SQL的表们。为了帮助你这样做,第一次调用getWritableDatabase()时,它会自动调用SQLiteOpenHelper.onCreate()方法。
下面的两个代码片段演示了在ContentProvider.onCreate() 和SQLiteOpenHelper.onCreate()之间的互动。第一段对ContentProvider.onCreate()的实现:
[java] view plaincopyprint? 1. public class ExampleProvider extends ContentProvider 2. 3. /* 4. * Defines a handle to the database helper object. The MainDatabaseHelper class is defined 5. * in a following snippet. 6. */ 7. private MainDatabaseHelper mOpenHelper; 8. 9. // Defines the database name 10. private static final String DBNAME = "mydb"; 11. 12. // Holds the database object 13. private SQLiteDatabase db; 14. 15. public boolean onCreate() { 16. 17. /* 18. * Creates a new helper object. This method always returns quickly. 19. * Notice that the database itself isn't created or opened 20. * until SQLiteOpenHelper.getWritableDatabase is called 21. */ 22. mOpenHelper = new SQLiteOpenHelper( 23. getContext(), // the application context 24. DBNAME, // the name of the database) 25. null, // uses the default SQLite cursor 26. 1 // the version number 27. ); 28. 29. return true; 30. } 31. 32. ... 33. 34. // Implements the provider's insert method 35. public Cursor insert(Uri uri, ContentValues values) { 36. // Insert code here to determine which table to open, handle error-checking, and so forth 37. 38. ... 39. 40. /* 41. * Gets a writeable database. This will trigger its creation if it doesn't already exist. 42. * 43. */ 44. db = mOpenHelper.getWritableDatabase(); 45. } 46. }
第二段代码是对SQLiteOpenHelper.onCreate()的实现,包含了一个helper类:
[java] view plaincopyprint? 1. ... 2. // A string that defines the SQL statement for creating a table 3. private static final String SQL_CREATE_MAIN = "CREATE TABLE " + 4. "main " + // Table's name 5. "(" + // The columns in the table 6. " _ID INTEGER PRIMARY KEY, " + 7. " WORD TEXT" 8. " FREQUENCY INTEGER " + 9. " LOCALE TEXT )"; 10. ... 11. /** 12. * Helper class that actually creates and manages the provider's underlying data repository. 13. */ 14. protected static final class MainDatabaseHelper extends SQLiteOpenHelper { 15. 16. /* 17. * Instantiates an open helper for the provider's SQLite data repository 18. * Do not do database creation and upgrade here. 19. */ 20. MainDatabaseHelper(Context context) { 21. super(context, DBNAME, null, 1); 22. } 23. 24. /* 25. * Creates the data repository. This is called when the provider attempts to open the 26. * repository and SQLite reports that it doesn't exist. 27. */ 28. public void onCreate(SQLiteDatabase db) { 29. 30. // Creates the main table 31. db.execSQL(SQL_CREATE_MAIN); 32. } 33. }
实现ContentProvider MIME 类型
ContentProvider 有两个方法返回MIME类型。
getType()
一个对任何provider都要实现的方法。
getStreamTypes()
如果你的provider提供的是文件,此方法是期望被实现的。
表的MIME类型们
getType()方法返回一个MIME格式的String ,此String描述了由content URI参数计算出的数据类型。Uri 可以是一个模式而不是一个具体的URI;此时,你应该返回对应于那些符合content URI模式的的数据的类型。
对于普通的数据类型,比如文本, HTML或 JPEG,getType()应该返回标准的MIME类型。关于标准MIME类型的一个完整的列表能在IANA MIME Media Types 网站上找到。
对于指向表数据的一行或多行的content URI们,getType()应返回一个Android vendor-specific MIME 格式:
• 类型部分:vnd
• 子类型部分:
1. 如果URI指向单行:android.cursor.item/
2. 如果URI 指向多行:android.cursor.dir/
• Provider-specific 部分:vnd.<name>.<type>
你要提供 <name> 和 <type>。<name>的值应是一个全局唯一的,并且<type> 的值对于对应的URI模式也需是唯一的。对<name> 的一个好的选择是用你公司的名字或你的应用的Android包名的一部分。对于<type>的一个好的选择是使用标志与URI相关连的表的字符串。
例如,如果一个provider的authority是com.example.app.provider,并且它曝露了一个叫做table1的表,那么表示表中多行的MIME类型就是:
vnd.android.cursor.dir/vnd.com.example.provider.table1
表示单行的MIME类型是:
vnd.android.cursor.item/vnd.com.example.provider.table1
表示文件的MIME 们
如果你的provider提供文件,就要实现getStreamTypes()。这个方法对传入的content URI返回一个由指向文件们的MIME类型组成的String 数组,你应该跟据MIME类型过滤参数过滤MIME 类型们,所以你只返回那些客户端真正想要的MIME类型们。
例如,假设一个provider提供图像文件,有 .jpg, .png,和 .gif类型。如果一个应用使用过滤字符串image/* (表示某些是"image"的东西)调用ContentResolver.getStreamTypes() ,那么ContentProvider.getStreamTypes() 方法应返回数组:
{ "image/jpeg", "image/png", "image/gif"}
如果应用只对.jpg 文件感兴趣,它应使用过滤字符串 *\/jpeg 调用 ContentResolver.getStreamTypes(),并且 ContentProvider.getStreamTypes()应该返回:
{"image/jpeg"}
如果你的provider没有提供过滤字符串中所请求的MIME, getStreamTypes() 应返回 null.
实现一个Contract(契约)类
Contract class是一个public final 类,它包含有定义URI们的常量,列的名字,MIME类型们,以及其它用于provider的元数据。这个类建立了一个provider和其它应用之间的契约以保证在URI、列名等项的值发生变化时依然能被正确的操作。
一个契约类对开发者有帮助,因为它提供了一个好识别的名字,而不用直接使用数字,于是开发者就不会为列名或URI们使用错误的值。因为它是一个类,它就可以包含Javadoc文档。集成开发环境,比如Eclipse可以跟据契约类自动完成常量名并为常量显示Javadoc。
开发者不能从你的应用操作契约类的类文件,但是他们可以从你提供的.jar文件中静态的把它编译到他们的应用中 。
ContactsContract 类和它的嵌套类们就是契约类的例子。
实现Content Provider 权限
对android系统的所有方面的权限在主题Security and Permissions中有详细的描述。主题 Data Storage 中也描述了安全和权限对各种存储类型的影响。简略的说,重要的几点是:
• 默认下,存储在设备的内部存储器上的数据文件是你的应用和provider私有的。
• 你创建的SQLiteDatabase 数据库是你的应用和provider私有的。
• 默认上,保存到外部存储器上的数据文件是公有的和全局可读的。你不能使用一个content provider来限制外部存储上的文件的操作,因为其它应用可以使用其它API 来读写它们。
• 那些用于在你的设备内部存储器上打开或创建文件或SQLite数据的方法们可能会暗中给予其它应用读写的权限。如果你使用一个内部文件或数据库作为你的provider的数据仓库,并且你给予它们"world-readable" 或"world-writeable" 权限,你在manifest中对你的provider的权限声明将不能保护你的数据。内部存储上的文件和数据库的的默认权限是"private",并且你也不应该改变你的provider的数据仓库的权限。
如果你想使用content provider 的权限来控制对你的数据的操作,那么你应该存储你的数据到内部文件,SQLite 数据库,或"cloud" (例如,在一个远程服务上),并且你应该保持文件和数据库私属于你的应用。
实现权限
所有的应用都可以从你的provider读或写你的provider,即使后台的数据是私有的。因为默认下你的provider 没有权限设置。要改变这种情况,可以在manifest文件中设置你的provider的权限,使用<provider> 元素的属性或其儿子元素。你可以设置应用于整个provider的权限,或只应用于特定表的,或特定记录的,或所有三者的。
你在manifest文件中用一个或多个<permission> 元素定义你的provider的权限。要使用于你的provider的权限是唯一的,为使用android:name属性使用Java风格的范围限定。例如, 为读权限命名为com.example.app.provider.permission.READ_PROVIDER.
下面所列的描述说明了provider权限的范围,开始是应用于整个provider的然后变成更细颗粒度。更细颗粒度的权限优先级高于大范围的权限:
单一的read-write provider-level权限
一个权限,它控制对整个provider的读和写权限,用<provider> 元素的android:permission 属性指定。
分开的读和写provider-level权限
控制整个provider的一个读权限和一个写权限。你用<provider> 元素的android:readPermission和android:writePermission属性指定它们。它们的优先级比android:permission更高。
Path-level 权限
对你的provider中的content URI的读、写,或读/写权限。你使用<provider> 元素<path-permission> 子元素指定的指定每个URI的权限,你可以指定一个读/写权限,一个读权限,或一个写权限,或所有三者。读和写权限优先级高于读/写权限。并且,path-level权限优先级高于 provider-level权限。
临时权限
也是一个权限级别,它代表了临时获取并赋于一个应用的权限,即使这个应用不具有权限。临时权限特性减少了一个应用需要在其mainifest中声明的权限的数量。当你打开了临时权限,只有那些持续操作你的所有数据的应用们才需要对你的provider有“持久的”权限.
考虑一下你实现一个email provider和应用,当你想允许一个外部图像查看应用通过你的provider来显示图像附件时所需的权限。要想不用声明权限就给予图像查看应用所需的权限,就需设置图像的content URI的临时权限。这样设计你的email 应用:当用户想要显示一个图像,你的应用发送一个intent给图像查看应用,这个intent包含了图像的content URI 和权限。图像查看应用之后可以请求你的email provider来获取图像,即使图像查看应用对你的provider不具有普通的读权限。
要开启临时权限,既可以设置<provider>元素的android:grantUriPermissions 属性,也可以添加一个或多个 <grant-uri-permission> 子元素到你的<provider> 元素。如果你使用临时权限,你必须在从你的provider删除对一个content URI的支持时调用Context.revokeUriPermission() ,如果这个content URI关联了一个临时权限的话。
属性的值决定了你的provider具有什么操作的可操作性。如果属性被设置为true,那么系统将为你的整个provider获取临时权限,覆盖任何其它通过provider-level或path-level 获取的权限。
如果这个flag 设置为false, 那么你必须添加<grant-uri-permission> 子元素到你的<provider> 元素上。每个子元素指定了哪个content URI 或哪些URI们授予了临时权限。
要把临时权限委托给一个应用,intent必须包含FLAG_GRANT_READ_URI_PERMISSION 或FLAG_GRANT_WRITE_URI_PERMISSION 标志,或两者都有。它们可用setFlags() 方法设置。
如果android:grantUriPermissions 属性不存在,就被认为是false。
<provider>元素
________________________________________
就像Activity和Service组件,ContentProvider的子类必须在应用的manifest文件中进行定义。使用<provider>元素。Android系统从元素中获取以下信息:
Authority(android:authorities)
在系统中标志整个provider的代号。此属性在设计ContentURI 一节中有更详细的描述。
Provider类名(android:name)
实现了ContentProvider的类。此类在实现ContentProvider类一节中有更多描述。
权限
此属性指定了其它应用想要操作provider的数据所需的权限:
• android:grantUriPermssions:临时权限标记。
• android:permission:指定整个provider的read/write权限。
• android:readPermission:指定整个Provider的读权限。
• android:writePermission:指定整个Provider的写权限。
启动和控制权限
这些属性决定了android系统如何以及何时启动provider,provider的处理属性,以及其它运行时设置:
• android:enabled:允许系统启动provider的标志。.
• android:exported:允许其它应用使用此provider。
• android:initOrder:此provider相对于同进程中的其它provider的启动顺序。
• android:multiProcess:允许系统在调用客户端所在的进程中启动provider的标记。
• android:process:provider应该在其中运行的进程的名字。
• android:syncable:Flag indicating that the provider's data is to be sync'ed with dataon a server.
全部的属性在<provider> 元素的开发指南中有详细讲解。
信息类的属性
一个可选的图标和一个label:
• android:icon:一个包含图标的drawable资源。在Settings > Apps > All所显示的应用列表中显示,位于provider的旁边。
• android:label:一个描述provider或它的数据信息的label。出现在Settings > Apps > All所显示的应用列表中。
Intents和数据操作
________________________________________
应用可以通过Intent直接操作contentprovider。应用不调用ContentResolver或ContentProvider的任何方法,而是发送一个intent启动一个activity,这个activity一般是provider所在应用的一部分,它负责取得数据并显示在自己的UI上。跟据intent中的action的不同,目标activity可能会提示用户对provider的数据作出修改。intent可能还包含显示在界面上的额外的数据;于是用户可选择在使用这些数据修改provider的数据之前改变这些数据。
你可能想使用intent方式来帮助保证数据的完整性。你的provider可能跟据所严格定义的商业逻辑来决定是否具有数据插入、更新、删除等操作。如果是这样,允许其它应用直接修改你的数据可能导致非法的数据出现。如果你想让开发者使用intent方式,需保证你的文档中对它有完整的说明。向他们解释为什么使用intent方式通过你的自己的应用UI来操作好于使用他们的代码。
处理要修改你的provider的数据的intent与处理其它intent没有什么区别。你可以从Intents and Intent Filters学到更多信息