原文译自:http://developer.android.com/intl/zh-CN/guide/topics/providers/content-provider-creating.html
内容目录
内容提供者管理着中央数据的访问。伴随着清单文件内的元素,实现提供者作为android应用里的一个或多个类。实
现的众多类中的一个作为 ContentProvider
的子类,它是提供者和其他应用间的接口。尽管内容提供者意图让数据对
其他的应用程序可用,当然,你可以让应用中的活动来允许用户查询和修改由提供者管理的数据。
该主题的其余部分是构建内容提供者的步骤及可使用的API的列表。
开始构建前
开始构建提供者之前,执行如下操作:
1. 决定是否需要内容提供者。如果需要提供如下特性中的一个或多个时,就需要构建内容提供者:
如果完全是在自己的应用内使用,提供者就不必使用SQLite数据库。
2. 如果你还没有这么做, 请阅读文章内容提供者---基础来学习更多关于提供者的内容。
接着,按照如下步骤构建提供者:
1. 设计数据的原始存储。内容提供器通过两种方式提供数据:
文件数据
指通常进入文件内的数据,譬如照片,音频或视频等。把这些文件存储在应用的私有空间里。为回应来自其他应用的文件请求,提供者能够提供对文件的处理。
"结构式"数据
指通常进入数据库的数据,如数组或类似的结构。通过与表的行和列兼容的形式来存储数据。一行就代表一个实体,比如,一个人或商品清单里的一项。列代表实体的某些数据 ,比如,人的名字或者商品的价格。存储此类数据的通常方法是通过SQLite数据库,不过你可以任何类型的持久性存储。为了解更多Android系统内可用的存储类型 ,请参阅设计数据存储章节。
2. 定义
ContentProvider
类和它的必备方法的具体实现。该类是你的数据和Android系统其余部分间的接口。更多关于该类的信息, 请参阅实现ContentProvider类章节。
3. 定义提供者权限字符串,它的内容URIs及列名。如果打算让提供者应用处理意图,则还要定义意图动作,附加数据及标记。还要定义那些想要访问你的数据的其他应用所需要的权限。应该考虑把所有作为常量的值定义在一个单独的契约类里; 然后, 把这个类公开给其他的开发者。更多关于内容URIs的信息,请参阅设计内容唯一资源标识符(URIs)章节。更多有关意图的信息,参阅意图和数据访问章节。
4. 添加其他的可选件, 比如简单的数据或是
AbstractThreadedSyncAdapter
类的实现,它能够在提供者和基于云计算的数据间同步。
1. 设计数据存储
内容提供者是按结构化的格式保存的数据接口。创建该接口之前,必须决定如何存储数据。可以用任何喜欢的形式存储数据,然后根据需要设计读和写的接口。
下面是一些Android上可用的数据存储技术:
SQLiteOpenHelper
类可以帮助我们创建数据库,而SQLiteDatabase
类则是访问数据库的基类。记住,没必要一定要使用数据库来实现数据存储。提供者在表面上呈现为表的集合,和关系数据库相似,但是,但这不必是提供者内部实现要求。
java.net
和android.net
里的类。还能把基于网络的数据同步到本地的数据存储,比如数据库,接着提供表或文件数据。Sample Sync Adapter示例应用演示了这种类型的同步。
1.1 数据设计注意事项
这有一些设计提供者数据结构的提示:
BaseColumns._ID
是最好的选择,因为把提供者查询结果关联到ListView时,需要被返回的列中的一列名为_ID。
如果想要提供位图图像或其他很大的面向文件的数据块,则把数据存储在文件中,然后间接提供它而非直接地把它存储在表中。如果这样做,需要告诉提供者的使用者,他们需要使用ContentResolver
文件方法来访问数据。
使用二进制大对象 (BLOB)数据类型来存储大小不定或结构不同的数据。例如,可以使用BLOB类型
列来存储协议缓冲区(protocol buffer) 或 JSON结构(JSON structure)
。
你还能用二进制大对象来实现独立模式表。在这类表中,定义一个主键列,一个MIME类型列及一个或多个通用的
BLOB
列。BLOB列内数据的含义由MIME类型列内的值来指明。这就允许你在相同的表内存储不同的行类型
。联系人提供者的“data”表ContactsContract.Data
就是一个独立模式表的例子。
2. 设计内容唯一资源标示符(URIs)
内容唯一资源标示符是识别提供者数据的唯一资源标示符。内容URIs包括了整个提供者的符号名(它的授权)和一个指向表或文件的名字(路径)。可选的id部分指向表内一单独行。ContentProvider
的每个数据访问方法都有一个内容URI的参数;这允许你来决定要访问的表,行或者文件。
内容URIs的基础知识在主题 内容提供者基础里有描述。
提供者通常有一个单独的授权,它作为Android内部名称。为了避免与其他的提供者存在冲突,应该使用互联网域名所有权(反序的)作为提供者授权的基础。因为这一建议对Android包名也是如此, 你可以把提供者的授权定义成包含该提供者包名的扩展。例如,如果Android包名是com.example.<appname>
,你应该把授权
com.example.<appname>.provider赋给提供者。
开发人员一般通过在授权后附加指向单独表的路径来创建内容URIs。例如,如果有两个表table1和table2,结合先前实例的授权进而产生内容URIscom.example.<appname>.provider/table1
和com.example.<appname>.provider/table2。路径不限于一个分段,并且路径的每个层级也不必存有表。
按照约定,提供者提供对表内的单行访问是依据接受尾部带有行ID值的内容URI。同样是根据约定, 提供者把ID值与表的_ID列进行匹配,并执行对与之匹配的行的请求访问。
这个约定催生了应用程序访问提供者的一个常用设计模式。应用对提供者进行查询并使用CursorAdapter
在ListView
内显示作为查询结果的Cursor
。CursorAdapter
的定义要求Cursor
内列的其中之一是_ID
。
然后,用户从UI上选取显示行中的一个以便查看或修改数据。应用从
ListView
后面的Cursor
获取获取行,得到改行的_ID值,把附加到内容URI上,然后向提供者发送访问请求。于是提供者对正确的用户选择行进行查询或修改。
2.4 内容URI模型
为了帮助你对于传入的内容URI而采取何种行为,提供者API包括方便类UriMatcher,它把内容“patterns”映射为整型值。可以在switch语句里使用整型值,在此选择期望的内容URI行为或匹配特殊模型的URIs行为。
内容URI模型使用通配符匹配内容URIs:
作为设计和编码处理内容URI的一个例子,考虑授权为com.example.app.provider的提供者识别如下内容URI指向的表:
content://com.example.app.provider/table1
: 名为table1的表。content://com.example.app.provider/table2/dataset1
:名为dataset1
的表。content://com.example.app.provider/table2/dataset2
:名为dataset2
的表。content://com.example.app.provider/table3
:名为table3
的表。如果它们后面附加行ID,提供者也能识别这些内容URIs,例如行content://com.example.app.provider/table3/1在table3中由1来标识。
下面的内容URI模型将有可能:
content://com.example.app.provider/*
:匹配提供者内的任何内容URI。
content://com.example.app.provider/table2/*
:匹配表
dataset1
和表dataset1的内容URI,但不匹配表
table1
或table3的内容URI。
content://com.example.app.provider/table3/#
:
匹配表table3里单行内容URI,譬如由6标识的行
content://com.example.app.provider/table3/6
。
接下来的代码段显示了UriMatcher
内的方法是如何工作的。这段代码处理整表的URIs不同于单行URIs,通过使用表content://<authority>/<path>,和行
content://<authority>/<path>/<id>
的内容URI模型。
方法addURI()
把权限和路径映射为一整型值。方法match()
返回URI整型值。switch
语句在整表查询和单行记录查询间进行选择:
public class ExampleProvider extends ContentProvider { ... // Creates a UriMatcher object. private static final UriMatcher sUriMatcher; ... /* * The calls to addURI() go here, for all of the content URI patterns that the provider * should recognize. For this snippet, only the calls for table 3 are shown. */ ... /* * Sets the integer value for multiple rows in table 3 to 1. Notice that no wildcard is used * in the path */ sUriMatcher.addURI("com.example.app.provider", "table3", 1); /* * Sets the code for a single row to 2. In this case, the "#" wildcard is * used. "content://com.example.app.provider/table3/3" matches, but * "content://com.example.app.provider/table3 doesn't. */ sUriMatcher.addURI("com.example.app.provider", "table3/#", 2); ... // Implements ContentProvider.query() public Cursor query( Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { ... /* * Choose the table to query and a sort order based on the code returned for the incoming * URI. Here, too, only the statements for table 3 are shown. */ switch (sUriMatcher.match(uri)) { // If the incoming URI was for all of table3 case 1: if (TextUtils.isEmpty(sortOrder)) sortOrder = "_ID ASC"; break; // If the incoming URI was for a single row case 2: /* * Because this URI was for a single row, the _ID value part is * present. Get the last path segment from the URI; this is the _ID value. * Then, append the value to the WHERE clause for the query */ selection = selection + "_ID = " uri.getLastPathSegment(); break; default: ... // If the URI is not recognized, you should do some error handling here. } // call the code to actually do the query }
另一个类,ContentUris
,提供了方便使用内容URIs的id部分的方法。类Uri
和Uri.Builder则包含解析现有
Uri
对象和构建新Uri
对象的便捷方法。
3. 实现ContentProvider类
实例是通过处理来自其他应用的请求ContentProvider
进而来管理结构化的数据集访问。所有形式的访问最终都要调用ContentResolver(内容解析器),然后它调用
ContentProvider的具体方法获取访问。
抽象类 ContentProvider
定义了六个抽象方法,必须实现它们作为自己的具体子类的一部分。除了onCreate()
外,所有的这些方法由试图访问内容提供者的客户端应用来调用:
query()
Cursor
对象。
insert()
update()
delete()
getType()
onCreate()
ContentResolver
对象试图访问提供者时,它才被创建。
ContentResolver
方法有相同的签名。
这些方法的实现应该考虑以下方面:
onCreate()
外,所有这些方法可同时被多个线程调用,所以它们必须是线程安全的。学习更多关于多线程的内容,请参阅进程和线程章节。onCreate()
方法内做长时间操作。推迟初始化工作,直到实际需要它们时。实现onCreate() 方法章节里对此有更详细地讨论。insert()
的调用然后返回0.
ContentProvider.query()
方法必须返回Cursor
对象, 或者如果它失败了,抛出Exception
。如果你正使用SQLite数据库作为数据存储,你可以简单地返回由SQLiteDatabase
类的某个query返回的Cursor
。如果查询没有匹配任何行,你应该返回一个Cursor
实例,而它的getCount()
方法返回0。只有在查询过程中发生内部错误时,才返回NULL。
如果没有使用SQLite数据库作为数据存储,而是使用Cursor
的某个具体子类。例如,MatrixCursor
类实现了Cursor
,在该cursor里,每行都是一组对象。通过该类,使用addRow()
添加新行。
记住,Android系统必须能够跨进程边界与Exception
通信。Android能够对下列异常做到这一点,它们可能有助于处理查询错误:
IllegalArgumentException
(如果提供者到无效内容URI, 可以抛出它。)NullPointerException
insert()
方法使用ContentValues
参数中的值把新行加入适当的表里。如果列名没有在ContentValues
参数里,则你可能想在提供者代码中或数据库框架中为该列提供一默认值。
该方法返回新行的内容URI。为构造该内容URI,使用withAppendedId()
方法把新行的_ID(或者其他的主键)值附加到该表的内容URI上。
delete()
方法不必在物理上从数据存储中删除行。如果正在对提供者使用同步适配器,应该考虑通过“delete”标志来标记被删除的行,而不是彻底地删除行。同步适配器能够检查被删除行,并在将其从提供者内删除之前将它们从服务器中移除。
update()
方法接受相同的由insert()
使用的ContentValues参数
, 以及相同的由
delete()
和ContentProvider.query()
使用的selection
和selectionArgs
参数。 这可以允许你在这些方法间复用代码。
当Android系统启动提供者时调用onCreate()
。应该在此方法内只执行快速运行的初始化工作,并且推迟数据库创建和数据加载,直到提供者实际收到数据请求。如果在onCreate()
中执行冗长的工作,将减缓提供者的启动。 进而,放慢了提供者对其他应用的响应。
例如,如果你正在使用SQLite数据库,可以在ContentProvider.onCreate()
方法里创建一个新的SQLiteOpenHelper
对象,然后,第一次打开数据库时创建SQL表。为使此过程变得容易,第一次调用getWritableDatabase()
时,它自动地调用了SQLiteOpenHelper.onCreate()
方法。
下面两段代码阐述了ContentProvider.onCreate()
和SQLiteOpenHelper.onCreate()
之间的相互作用。第一个片段是ContentProvider.onCreate()
的实现:
public class ExampleProvider extends ContentProvider /* * Defines a handle to the database helper object. The MainDatabaseHelper class is defined * in a following snippet. */ private MainDatabaseHelper mOpenHelper; // Defines the database name private static final String DBNAME = "mydb"; // Holds the database object private SQLiteDatabase db; public boolean onCreate() { /* * Creates a new helper object. This method always returns quickly. * Notice that the database itself isn't created or opened * until SQLiteOpenHelper.getWritableDatabase is called */ mOpenHelper = new SQLiteOpenHelper( //此处有误!应该mOpenHelper = new MainDatabaseHelper getContext(), // the application context DBNAME, // the name of the database) null, // uses the default SQLite cursor 1 // the version number ); return true; } ... // Implements the provider's insert method public Cursor insert(Uri uri, ContentValues values) { // Insert code here to determine which table to open, handle error-checking, and so forth ... /* * Gets a writeable database. This will trigger its creation if it doesn't already exist. * */ db = mOpenHelper.getWritableDatabase(); } }
接下来的代码段是SQLiteOpenHelper.onCreate()
的实现,包括一个助手类:
... // A string that defines the SQL statement for creating a table private static final String SQL_CREATE_MAIN = "CREATE TABLE " + "main " + // Table's name "(" + // The columns in the table " _ID INTEGER PRIMARY KEY, " + " WORD TEXT" " FREQUENCY INTEGER " + " LOCALE TEXT )"; ... /** * Helper class that actually creates and manages the provider's underlying data repository. */ protected static final class MainDatabaseHelper extends SQLiteOpenHelper { /* * Instantiates an open helper for the provider's SQLite data repository * Do not do database creation and upgrade here. */ MainDatabaseHelper(Context context) { super(context, DBNAME, null, 1); } /* * Creates the data repository. This is called when the provider attempts to open the * repository and SQLite reports that it doesn't exist. */ public void onCreate(SQLiteDatabase db) { // Creates the main table db.execSQL(SQL_CREATE_MAIN); } }
为返回MIME类型,
ContentProvider
类有两个方法:
getType()
getStreamTypes()
getType()
方法返回MIME格式字符串,该字符串描述了由内容URI参数返回的数据类型。Uri
参数可以是一个模式而非具体的URI;这种情况下, 应该返回与匹配该模式的内容URIs相关的数据类型。
对于常见的数据类型,比如文本,HTML或JPEG,getType()
应该返回数据的MIME类型(实为MIME格式的字符串)。这些标准类型的完整列表可在IANA MIME Media Types站点上获得。
对于指向表数据的一列或多列的内容URIs, getType()
应该按照Android厂商特定的MIME格式返回MIME类型(实为MIME格式的字符串):
vnd
android.cursor.item/
android.cursor.dir/
vnd.<name>
.<type>
由你提供<name>
和<type>
。<name>
值应该是全球唯一的, 并且<type>
值对于对应的URI模式来说也应该是唯一的。对<name>来说,一个不错的选择是公司名字或者应用的Android包名的一部分。对于<type>
, 识别与URI有关的表的字符串是个不错的选择。
例如,如果提供者的授权是com.example.app.provider
, 并且公开了名为table1的表,则table1内多行的MIME类型是:
vnd.android.cursor.dir/vnd.com.example.provider.table1
对于table1的单行来说,MIME类型是:
vnd.android.cursor.item/vnd.com.example.provider.table1
4.2 文件的MIME类型
如果提供者提供文件,请实现getStreamTypes()
。该方法返回提供者针对给定内容URI能够返回的文件的MIME类型字符串数组。你应该通过MIME类型过滤参数来过滤你给出的MIME类型,目的为了仅返回那些客户端想要处理的MIME类型。
例如,考虑一个可以提供jpg
,png
,和gif格式图片文件的提供者。
如果应用使用过滤字符串image/*(一些是“图片”的东西)来调用ContentResolver.getStreamTypes()
, 那么ContentProvider.getStreamTypes()
方法应该返回数组:
{ "image/jpeg", "image/png", "image/gif"}
*\/jpeg
调用ContentResolver.getStreamTypes()
, 那么ContentProvider.getStreamTypes()
应该返回:
{"image/jpeg"}
如果提供者没有提供任何过滤字符串中请求的MIME类型, getStreamTypes()
应该返回null
。
契约类是一个public final类,它包含了URIs,列名,MIME类型,以及其他关于提供者的元数据的常量定义。该类为确保提供者可以被正确地访问而在提供者和其他的应用间建立约定,即便是URIs,列名等发生了变化。
契约类对开发者也有帮助,因为它通常有其常量的助记名,所以开发者不大可能使用不正确的列名值或URIs值。由于它是个类,它能够包含Javadoc文档。集成开发环境(IDE),例如Eclipse,能够自动完成源自契约类的常量名并为常量显示Javadoc。
开发者不能从你的应用中访问契约类的class文件,但他们可以从你提供的.jar文件中把它静态地编译进他们的应用里。
ContactsContract
类和它的嵌套类都是契约类的例子。
Android系统的权限和访问的所有方面在安全和权限主题中有更详细地描述。数据存储主题也阐述了安全和权限对不同存储类型的影响。总之,重点是:
你创建的SQLiteDatabase
数据库对你的应用和提供者是私有的。
如果想利用内容提供者来控制你的数据访问,那么你应该把数据存储在内部文件,SQLite数据库,或是"云"(例如,在远程服务器上)中,并且应该保持文件和数据库专属于你的应用。
所有应用能够读或者写你的提供者,即便底层数据是私有的,因为默认情况下提供者没有设置权限。为改变这一点,使用属性或 <provider>
元素的子元素,在清单文件中为你的提供者设置权限。可以设置适用于整个提供者,或者某个表,甚至某个记录,又或是所有它们三个的权限。
在清单文件中,以一个或多个<permission>
元素为提供者定义权限。为使权限独有于提供者,对android:name
属性使用Java风格的作用域。例如,命名读权限com.example.app.provider.permission.READ_PROVIDER
。
接下来的列表描述提供者权限作用域,他们以适用于整个提供者的权限为开始,然后变得更细化。更细化的权限优先于更大作用域的权限:
一个控制整个提供者读和写的权限,由<provider>
元素的android:permission
的属性指定。
整个提供者的读权限和写权限。 通过<provider>
元素的android:readPermission
和android:writePermission
属于来指定他们。它们优先于android:permission
所要求的权限。
提供者的内容URI读,写,或读/写权限。通过<provider>
元素的子元素<path-permission>
来指定每个你想控制的权限。对于每个指定的内容URI, 可以指定一个读/写,读或写权限,或全部三个。读权限和写权限优先于读/写权限。同样,路径级权限优先于提供者级权限。
授予应用的临时访问权限级别,即使应用没有通常所被要求的权限。临时访问的特性减少了应用必须在清单文件里请求的权限数量。 当打开临时权限时,唯一需要“永久”提供者权限的应用是那些频繁访问你所有数据的应用。
当打算允许一个外部图片浏览器应用显示来自提供者的图片附件时,考虑需要实现邮件提供者和应用的权限。为给图片浏览器需要的访问而不请求权限,对图片内容URIs设置临时权限。设计邮件应用的目的是当用户想显示图片时,应用发送包含有图片内容URI和权限标志的意图给图片浏览器。然后,图片浏览器能够查询邮件提供者来提取图片,即便浏览器没有正常读该提供者的权限。
为打开临时权限,要么设置<provider>
元素的android:grantUriPermissions
属性,或者为<provider>
元素添加一个或更多个<grant-uri-permission>
子元素。如果使用临时权限, 每当从提供者解除对和临时权限有关系的内容URI的支持时,必须调用Context.revokeUriPermission()
。
属性值决定了提供者可被访问的程度。如果属性被设置为true,那么系统将对整个提供者授予临时权限,压倒任何其他的由提供者级和路径级权限所需要的权限。
如果标志被设置为false,那么必须为<provider>
元素添加<grant-uri-permission>
子元素。每个子元素指定被授予临时访问权限的内容URI或URIs。
为给应用委派临时访问权限,意图必须包含FLAG_GRANT_READ_URI_PERMISSION
或 theFLAG_GRANT_WRITE_URI_PERMISSION
标志,或者他们两个。他们由setFlags()
方法来设置。
如果android:grantUriPermissions
属性没有出现,则它被假设为false。
如同Activity
和Service
组件,ContentProvider
的子类必须被定义在它的应用清单文件里,通过使用<provider>
元素。Android系统从该元素获得如下信息:
android:authorities
)
系统内标识整个提供者的符号名。在设计内容URIs章节有这个属性的更详细描述。
android:name
)
实现ContentProvider
的类。该类更详细的描述在实现ContentProvider类章节。
android:grantUriPermssions
: 临时访问权限标志。android:permission
: 单个的提供者级读/写权限。android:readPermission
: 提供者级读权限。android:writePermission
:提供者级写权限。权限和它们对应属性的更详细描述在实现内容提供权限章节。
android:enabled
: 允许系统启动提供者的标志。android:exported
: 允许其他应用使用该提供者的标志。android:initOrder
: 相对于同进程里其他提供者,该提供者被启动的顺序。android:multiProcess
: 允许系统在与调用客户端相同的进程里启动提供者的标志。android:process
: 进程的名字,提供者应该在该进程中运行。android:syncable
: 暗示提供者数据将要与服务器上的数据同步的标志。这些属性被完全记录在<provider>
元素的开发指南主题中
。
android:icon
: 包含提供者图标的可绘制资源。在Settings >Apps >All中的应用列表里,图标挨着提供者标签出现。android:label
: 描述提供者或其数据,或者它们两个的信息标签。标签出现在Settings > Apps > All中的应用列表中。这些属性被完全记录在<provider>
元素的开发指南主题中。
应用可以通过Intent
间接地访问内容提供者。应用不必调用ContentResolver
或ContentProvider
的任何方法。相反,它发送意图来启动活动,该活动通常是提供者自身应用的一部分。目标活动负责取出并在它的UI里显示数据。依赖于意图内的动作,目标活动也可能提示用户对提供者数据做出修改。意图也可能包含“额外”数据,目标活动在其UI上显示该数据;然后,使用该意图修改提供者数据前,用户可以选择修改该额外数据。
你可能想使用意图访问来促使数据的完整性。提供者可能依赖于根据严格定义的商业逻辑来插入,更新,和删除数据。如果是这样的话,允许其他应用直接修改数据可能导致非法的数据。如果想让开发者使用意图访问,一定要充分地描述它。向他们解释,为什么意图访问使用提供自身应用的UI要好于试图通过他们自己的代码来修改数据。
处理传入的希望修改提供者数据的意图与处理其他的意图是没有区别的。通过阅读主题意图和意图过滤器,可以了解更多关于使用意图的知识。
2012年8月15日 毕