Content providers管理一套有组织的数据的使用。它们封装了数据,提供定义数据安全机制。Content providers是一个正在运行的进程连接另一个进程数据的标准接口。
当我们想要获取content provider中的数据时,我们使用应用程序Context中的ContentResolver对象与作为客户端的provider通信。ContentResolver对象与特定的provider对象通信,provider对象是实现ContentProvider的某个类的实例。provider对象接收客户端要求的数据,进行客户端要求的操作,并返回结果。
如果我们不打算与其它应用分享数据,则不需要使用provider。然而,在自己的应用中,我们需要自己的provider提供特定的搜索建议。如果我们想要复制粘贴复杂数据或文件到其他应用中,我们也需要自己的provider。
安卓系统包含了一些providers管理例如audio,video,images和个人联系人信息的content providers。我们可以在android.provider包中查询到相关引用列表。通过一些限制措施,这些content providers可以被任一应用获取。
Content Provider Basics
一个content provider管理一个数据中心的使用。一个provider是一个安卓应用的一部分,并且提供了操作数据的UI视图。然而,content provider最初设计来被其它应用引用,通过使用一个provider客户端对象获取provider。同时,providers和provider客户端提供一个不变的标准数据接口处理进程间通信和数据安全。
基础部分讨论以下内容:
- content providers是如何工作的
- 从一个content provider获取数据的API
- 操作一个content provider,比如插入,更新和删除数据需要使用的API
- 与其他provider交互的API特性
Overview
当一个或多个tables和一个相关的database中的tables相似时,一个content provider代表外部应用数据。列代表provider提供的数据类型实例,列中的每一栏代表一个实例的单个数据。
比如,安卓平台的一个内建providers是用户字典,它保存了用户想要保存的非标准单词的拼音。下表展示了这个provider table的可能样式:
在上表中,每一列代表一个在标准辞典中没有的单词的实例。每一栏代表该单词的相关数据,比如首次使用它的地区。栏首是储存在provider中的栏目名称。指向地区时,我们指向地区栏。在这个provider中,_ID栏作为"primary key"栏由provider自动保存。
Note:一个provider并不要求一定要有primary key,也不要求使用_ID代表栏目名。然而,如果我们想要绑定provider数据到一个ListView,必须有一栏命名为_ID。在Displaying query results部分将讨论更多相关细节。
Accessing a provider
应用通过ContentResolver客户端对象获取content provider数据。这个对象提供了一些函数调用provider对象中的相同名字方法,provider对象是一个ContentProvider子类的一个具体实例。ContentResolver方法提供了持久性存储的"CRUD"(创建,取出,更新和删除)方法。
客户端应用进程的ContentResolver对象和和应用的ContentProvider对象自动处理进程间通信。ContentProvider还作为数据目录和数据table外部样式的抽象。
Note:获取一个provider,我们的应用通常需要manifest中一些特定的permissions。
例如,从用户辞典Provider中获取一组单词和它们的初始地区,我们调用ContentResolver.query()。query()方法调用定义在该辞典provider中的ContentProvider.query()方法。下面的代码展示了ContentResolver.query()调用:
下表展示了query(Uri, projection, selection, selectionArgs, sortOrder)方法的参数是如何匹配SQL SELECT格式的:
query() argument | SELECT keyword/parameter | Notes |
Uri | FROM table_name | Uri匹配provider中的表格名 |
projection | col, col, col,... | projection是需要取出的列的栏名 |
selection | WHERE col = value | selection指明选中列的方式 |
selectionArgs | 不完全相等。selection参数代替selection cluse的placeholders | |
sortOrder | ORDER BY col, col, ... | sortOrder指定在返回的Cursor中的列的顺序 |
Content URIs
一个content URI是一个URI对应provider中的数据。Content URIs包括整个provider的特征名(授权)和指向表格的名字(路径)当我们调用一个客户端方法获取一个provider中的一个表格时,该表格的content URI是一个参数。
在之前的代码中,CONTENT_URI常数包含用户辞典"words"表格的内容URI。ContentResolver对象解析出URI的授权,使用它和系统已知provider的授权进行比较以分离出provider。ContentResolver之后迅速处理正确的provider的查询参数。
ContentProvider使用内容URI的部分路径选择表格入口。一个provider通常对它拥有的每个表格都有一个路径。先前代码中,完整的"words"表格的URI如下:
content://user_dictionary/words
其中,user_dictionary是provider的授权,words是表格的路径。content://表示这是一个内容URI。
许多providers允许我们通过在URI结尾添加ID值直接表格的一个单独列。例如,取出用户辞典中_ID为4的列,它的内容URI可以如下:
当取出一组列进行更新或删除时,我们通常使用id值。
Note:Uri和Uri.Builder类包含从string组件很好的Uri对象的简便方法。ContentUris包含添加id值到URI结尾的简便方法。如上面代码段使用的withAppendedId()方法。
Retrieving Data from the Provider
这部分讨论如何从一个provider中取出数据,依旧使用用户辞典Provider作为实例。
为了清晰,这部分的代码段在UI线程中使用ContentResolver.query()方法。实际上,我们需要在分开的线程中执行异步查询。。一个方法是如同Loaders中讲的使用CursorLoader类。同时,这些代码都只是片段,没有完全的显示整个应用。
从一个provider中取出数据,需要遵循以下步骤:
1.要求provider的可读权限。
2.定义送到provider的查询代码。
Requesting read access permission
从一个provider中取出数据,应用需要有该provider的可读权限。我们不能在运行时要求这一权限,而是在manifest中使用<uses-permission>标签对应定义provider的准确名字。当用户安装了应用时,它们即获取了这些权限。
找出需要使用的provider的可读权限的准确名字和该provider的其他权限名字,查询provider的相关文献。
用户辞典Provider在它的manifest文件中定义了android.permission.READ_USER_DICTIONARY权限,所以一个应用想要从该provider中读取数据时必须要求有这一权限。
Constructing the query
查询步骤先要定义一些变量,如下示例:
provider查询类似于SQL查询,它包括一组栏名,和一系列选择标准和排列顺序。
返回的栏名称作projection(如上的mProjection变量)。
指定取出列的描述分开放在selection clause和selection参数里。selection clause是一个逻辑和布尔描述,栏名称和值(mSelectionClause)的结合。如果指定可替代的参数?代替一个值,则查询方法从selection参数序列(mSelectionArgs)中取出值。
在下面的代码段中,如果用户没有输入任何单词,selection clause设置为空,则从provider中查询取出所有单词。如果用户输入一个单词,selection clause设置为UserDictionary.Words.WORD + " = ?"并且selection参数的第一个元素被设置为用户输入的那个单词。
如上,查询方法很类似于SQL语句:
Protecting against malicious input
如果content provider管理的数据是在SQL database里,包括不规范的有外部不受信任的数据的SQL语句都会引起SQL注入。
考虑下面的selection clause:
这么做,相当于我们允许用户蓄意的把一些SQL输入和SQL语句联系起来。比如,如果用户输入"nothing; DROP TABLE*"给mUserInput,那么将会导致selection clause的结果为var = nothing; DROP TABLE *;既然selection clause也被认为是SQL语句,这可能会引起provider删除SQLite database里的所有表格(除非provider设置了SQL注入捕捉)
为了避免这一问题,让selection clause使用?作为可替代参数并把selection clause作为selection arguments分开的一部分。这么做,用户输入直接作为查询语句一部分而不是SQL语句的一部分。因为不作为SQL语句,用户输入不会引起恶意SQL注入问题。所以通常如下这样写包含用户输入:
如下设置selection参数序列:
如下把一个值放置到selection参数中:
使用?作为可替代参数的selection clause和一组selection参数是一个很好的指定selection的方式,尽管provider不是以SQL database为基础的。
Displaying query results
ContentResolver.query()客户端方法总是返回一个由query列设计匹配query selection匹配规则的一些栏目的Cursor对象。一个Cursor对象提供随机的读取列和栏的方法。使用Cursor方法,我们可以反复申明结果列,决定每一栏的数据类型,获取栏外的数据,测试结果的其他特性。当provider数据改变时会Cursor实现会自动更新,当Cursor改变时,观察对象会触发方法。
Note:一个provider可能会根据对象查询方式的自然特性限制对一些栏目的获取。例如,Contacts Provider限制一些栏目以同步adapters。
如果没有列匹配selection规则,那么provider返回一个Cursor.getCount()是0的Cursor对象(空的Cursor)。
如果出现了内部错误,查询结果取决于每个provider。可能会返回空,也可能跑出异常。
由于Cursor是一个列的"list",一个很好的显示Cursor内容的方式是通过SimpleCursorAdapter把它和ListView联系起来。
如下:
Note:返回一个Cursor的ListView,cursor必须包含有_ID的栏。因为,即使listview不显示它,查询也会首先取出“words”表的_ID栏。这一限定也解释了为什么大部分providers的表格都有一个_ID项。
Getting data from query results
除了简单的显示查询结果之外,还有其他用处。比如,可以取出用户辞典里的拼音然后在其它providers里进行查询。这样,我们可以重复Cursor里的列。如下:
Cursor实现包含许多get方法让我们取出对象不同类型的数据。例如,getString)_。还有一个getType()方法返回栏可能的数据类型。
Content Provider Permissions
一个provider应用可以指定其它应用想要获取它的数据的限定。这些限定保证了应用完全知悉它所要获取数据的内容。这也让用户在安装某个应用时能知道这个应用获取了哪些权限。
如果一个provider应用不指定任何权限,那么其他应用就没办法获取它的数据。
如前,用户辞典Provider要求有android.permission.READ_USER_DICTIONARY权限以获取数据。还有android.permission.WRITE_USER_DICTIONARY权限以插入,更新和删除数据。
获取provider权限,应用要求它们在manifest文件里有<uses-permission>标签。当安卓package Manager安装应用后,用户必须允许应用获得所有权限,这样,才会继续安装;如果用户不允许,Package Manager取消安装。如下:
Inserting, Updating, and Deleting Data
除了取出数据我们还可以修改数据。调用ContentResolver的有参函数传递给ContentProvider的相应函数。provider和provider客户端自动处理安全和进程间通信问题。
Inserting data
调用ContentResolver.insert()方法向provider中插入数据。这个方法向provider中插入一个列并返回这列的content URI。如下:用户辞典插入一个新单词。
新的列的数据存入一个单独的ContentValues对象,它和一个只有一列的cursor很类似。这个对象里的栏不需要有相同的数据类型,如果你不想为它指定任何值,可以调用ContentValues.putNull()设置某一栏为空。
代码段中并没有添加_ID栏,因为这栏自动保存。provider为它添加的每一列分配一个特殊的_ID值。Providers通常使用这个值作为表格的primary key。
newUri返回的新近添加的列内容URI格式如下:
<id_value>是新列的_ID内容。大多数providers会自动检测内容URI表格然或作这个列的其它操作。从返回的Uri中获取_ID值,调用ContentUris.parseId()。
Updating data
更新一列,使用带有更新数据的ContentValues对象,如同插入数据中所作的那样,选择规范如同查询中所作的那样。使用的客户端方法是ContentResolver.update()。我们只需要添加要更新的数据到ContentValues中。如果想清楚栏内容,设置值为空。
下面的代码段更新所有的语言为"en"的地区为空。返回值为更新后的列。
如同Protecting against malicious input中所述,调用ContentResolver.update()时我们需要审查用户输入。
Deleting data
删除列同取出列数据很相近:指定需要删除列的选择规范,然后客户端方法返回删除列的数目。下面的代码段删除appid为"user"的列:
同样我们需要审查输入。
Provider Data Types
Content providers可以提供许多不同的数据类型。用户辞典提供了text,但是provider还可以提供如下格式:
- integer
- long integer(long)
- floating point
- long floating point(double)
Providers经常使用的另一个数据类型是继承作为64KB比特序列的Binary Large OBject(BLOB)。查询Cursor类的"get"方法可以查看可使用的数据类型。
provider中的每栏的数据类型经常列在它的文献中。用户辞典的数据类型列在联系人类UserDictionary.Words类的引用文献中()。通过调用Cursor.getType()可以决定数据类型。
Providers也可以为他们定义的每个内容URI保持MIME数据类型信息。我们可以使用MIME类型信息找出应用是否可以处理provider提供的数据,或者选择一个基于MIME类型的类型来处理。我们通常在处理包含复杂数据结构或文件的provider时需要MIME类型。例如Contacts Provider中的ContactsContact.Data表使用MIME类型标识每列存储的联系人数据类型。获取内容URI相应MIME类型,调用ContentResolver.getType()。
MIME Type Reference部分详细描述标准和自定义MIME类型的语法
Alternative Forms of Provider Access
编写应用时,有三中比较重要的获取provider的方式。
- Batch access:使用ContentResolver.applyBatch()方法可以在ContentProviderOperation类中创建一些获取调用。
- 异步查询:必须在另一个线程里作查询操作。一个做法是使用CursorLoader对象。
- Data access via intents:尽管不能直接发送intent到一个provider,我们可以发送一个intent到provider应用,这是最好的修改provider数据的方式。
Batch access
这种方式对于插入大量列数据,或在同一个函数中插入数据到不同的表格,或跨进程边界操作(原子操作)很有用。
在"batch mode"情况下获取一个provider,可以创建一个ContentProviderOperation对象数组,然后使用ContentResolver.applyBatch()分配到一个content provider中去。我们把content provider的授权传递给这个方法,而不是一个内容URI。这允许数组中的每个ContentProviderOperation对象与一个不同的表截然相反。ContentResolver.applyBatch()调用返回结果的数组。
ContactsContract.RawContacts的contract类的描述包括一个代码片段验证了batch插入。Contact Manager样例应用的ContactAdder.java源码中包括一个batch获取的示例。
Data access via intents
Intents提供了间接获取content provider的方法。通过从一个有权限的应用中获取一个结果intent或激活一个有权限的应用让用户操作的方式都允许用户从一个provider中获取数据。
Getting access with temporary permissions
即使没有合适的读取权限,发送一个intent到一个有权限的应用返回一个包含“URI”权限的intent结果,我们也可以从一个content provider中获取数据。特殊内容URI的这种权限到activity接收到他们时结束。有永久权限的应用应用在结果intent中设置标志位设置临时权限。
- Read permission:FLAG_GRANT_READ_URI_PERMISSION
- Write permission:FLAG_GRANT_WRITE_URI_PERMISSION
Note:标志位并不能对那些授权包含在内容URI中的provider进行读取,读取仅仅争对于URI自身。
在manifest文件中,provider为内容URI定义权限在<provider>标签中使用android.grantUriPermission属性,在子标签中使用<grant-uri-permission>。
例如:即使没有READ_CONTACTS权限,我们也可以从Contacts Provider中取出联系人数据。READ_CONTACTS权限使我们可以获取用户的所有联系人信息,而我们的应用需要让用户选择使用某个联系人,因此不用这个权限。做法如下:
1. 我们的应用发送一个包含ACTION_PICK和联系人的MIME类型CONTENT_ITEM_TYPE的intent。使用方法startActivityForResult()。
2. 因为这个intent匹配People这个应用的"selection"activity,这个activity来到前台。
3. 在selection activity中,用户选择一个联系人更新。之后selection activity调用setResult(resultcode, intent)设置返回给我们应用的intent。这个intent包含用户选择的联系人的内容URI,并且"extra"标志为FLAG_GRANT_URI_PERMISSION。这些标志授予我们的应用从内容URI指定的联系人读取数据的权限。selection activity则调用finish()把控制权交还给我们的应用。
4. 我们的应用回到前台,系统调用onActivityResult(),这个方法接收People app的selection activity创建的结果intent。
5. 有了结果intent中的内容URI,我们就可以从Contacts Provider中读取联系人数据,尽管我们并未在manifest中授予这一权限。
Using another application
无权限读取却可以修改数据的一个简单方式是激活一个有权限的应用,让用户去作相关操作。
例如,Calendar应用接收ACTION_INSERT intent,它使我们可以激活应用的插入UI。我们可以在这个intent中传递应用使用进行预操作的"extras"数据。因为重复事件需要复杂的语法,一个简单的插入事件到Calendar Provider的方法是使用ACTION_INSERT激活Calendar app然后让用户插入事件。
Contract Classes
contract类定义了一些常数,它们辅助应用同内容URI,栏名,intent actions和content provider其他特征的交互。Contract类不自动包括在provider中;需要provider开发这定义它们并让其他开发者可以获取它们。安卓平台的许多provider都有相关的contract类在android.provider包中。
例如,用户辞典Provider有一个contract 类UserDictionary包含内容URI和栏名常数。"words"表的内容URI定义在常数UserDictionary.Words.CONTENT_URI中。UserDictionary.Words类也包含栏名常数,如下:
MIME Type Reference
Content providers可以返回标准MIME media类型或自定义MIME类型。
MIME类型格式如下:
例如一个广为人知的MIME类型是text/html它有text类型和html子类型。如果这个provider返回一个URI的类型,这意味着使用这个URI的查询将会返回一个包含HTML标签的text。
自定义MIME类型,也称作"vendor-specific"MIME类型,它有着复杂的类型和子类型值。类型值通常是
上述形式对应多列,如下对应单个列:
子类型是provider指定的。安卓创建的providers通常拥有简单的子类型。例如,当联系人应用为电话号码创建了一个列,它如下在列中设置MIME类型:
vnd.android.cursor.item/phone_v2
注意到phone_v2的子类型值仅仅为phone_v2。
另外的provider开发者可能会基于已有的provider授权和表名的子类型创建模型。例如,考虑一个包含train timetables的provider。权限为com.example.trains,它包含三个表格Line1, Line2, Line3
对表Line1,provider返回MIME类型如下:
对于内容URI为:
代表表Line2的第5列,provider返回MIME类型:
大部分content providers都为它们使用的MIME类型定义了contract class 常数。联系人Provider contract类为ContactsContract.RawContacts。