第3-2章 创建一个content provider
Content provider对中心存储数据的访问进行管理。你将provider看作是Android应用程序中的一个或多个类来实现,这些类之后跟着manifest文件中的节点。你的类会实现一个ContentProvider子类,这个类是provider与其他应用程序之间的接口。虽然content provider一定会让数据对其他应用程序可用,但你可能也会需要一些在应用程序中的activities,这些activities允许用户查询和修改由provider管理的数据。
下面是创建一个content provider的基本步骤以及要使用的API列表。
3-2.1 在开始创建之前
在你开始创建一个provider之前,你要做下面的工作:
决定好你是否需要一个content provider。如果你想提供下面的一个或多个特性的话,你就需要创建一个content provider:
◆你想给其他的应用程序提供复杂的数据或文件。
◆你想允许用户从你的应用中把复杂的数据复制到其他应用中。
◆你想通过搜索框架来提供自定义搜索建议。
如果完全是在你自己的应用程序中使用SQLite数据库,那么你就不需要一个provider。
接下来,可以遵循下面这些步骤来创建你的provider:
- 为你的数据设计原始储存。Provider提供数据的方式有两种:
◆文件数据:这通常是进入到文件的数据,比如照片、音频或视频。在应用程序的私有空间储存文件。为了响应另一个应用程序的文件请求,你的provider可以提供一个对文件的处理。
◆“结构化的”数据:这通常是进入到一个数据库、数组或类似结构的数据。以带有行和列的表格形式来存储数据。一行代表一个实体,如一个人或在库存的一个item。一列代表实体的一些数据,如人名或item的价格。储存这类数据的一个常用方法是用一个SQLite数据库,但是你可以使用任何持久储存的类型来存储。
- 定义一个ContentProvider类的具体实现和它所需的方法。这个类是数据和Android系统其他部分之间的接口。
- 定义provider的authority字符串、content URI和列名。如果你想要应用程序处理intents,你也需要定义intent动作、额外数据和标记。还要定义应用程序为了访问数据所必需的权限。你应该考虑要定义作为一个单独合约类中的常量的所有这些值。然后,你就可以暴露这个类给其他的开发者。
- 添加其他的可选块,比如例子数据或一个AbstractThreadsSyncAdapter的实现,这个类可以同步provider和基于云存储数据之间的数据。
3-2.2 设计数据存储
content provider是以一种结构化格式保存的数据接口。在你创建这个接口之前,你必须决定好怎样存储数据。你可以按你喜欢的形式来存储数据,当必要时你可以设计接口来读写数据。
下面是一些在Android中可用的数据存储技术:
1. Android系统包含一个SQLite数据库API,Android自己的provider可以用它来存储面向表的数据。SQLiteOpenHelper类会帮你创建数据库,并且SQLiteDatabase类是用来访问数据库的基类。
记住,你不需要用一个数据库来实现你的仓储。Provider外表看起来是一个表集,这与一个关系型数据库相类似,但是这个不需要provider的内部实现。
2. 为了存储文件数据,Android系统有各种各样面向文件的APIs。如果你正在设计一个provider,让它能提供与媒体相关的数据,如音乐或视频,那么你可以拥有一个结合数据表和文件表的provider。
3. 要想用基于网络的数据工作,你可以使用在java.net和android.net里的类。你还可以把基于网络的数据同步到本地数据存储,如一个数据库,然后提供以表或文件的形式显示的数据。API Demo中的Sample Sync Adapter这个示例应用程序会演示这类同步操作。
数据设计的考虑:
这里有几个方法,你可以用来设计provider的数据结构:
1. 数据表应该总是要有一个“主键”列,这个列作为每一行的唯一数值被provider保留。你可以用这个值把行链接到其他表中的相关行(把它作为一个“外键”来使用)。虽然你可以为这个列取任意名称,但是用BaseColumns._ID这个名称是最好的选择,因为把provider查询的结果链接到一个ListView,需要一个拥有_ID名称的取出列。
2. 如果你想提供位图图片或其他非常大块的面向文件的数据,那么你就存储文件中的数据,然后间接地显示而不是直接把它储存在表中。如果你要这样做,那么你就要告诉provider的用户,他们需要使用一个ContentResolver文件方法才能访问数据。
3. 使用二进制大型对象(BLOB)数据类型来存储大小不同或结构不同的数据。例如,你可以使用一个BLOB列来存储一个protocol buffer或JSON structure。
你也可以用一个BLOB来实现一个模式(schema)-独立的表。在这类表中,你要定义一个主键列,一个MIME类型列和一个或多个作为BLOB的普通列。在BLOB列中的数据含义由MIME类型列中的值来指定。这就允许你在相同的表中储存不同的行类型。联系人provider的ContactsContract.Data“数据”表就是模式(schema)-独立表的一个示例。
3-2.3 设计Content URIs
Content URI是标识provider中数据的一个URI。Content URI包含了整个provider的符号名(它的权利)和指向表或文件的名称(一条路径)。可选的id部分会指向表中一个单独的行。ContentProvider的每个数据访问方法都有一个作为参数的content URI,它允许你来决定要访问的表、行或文件。
1. 设计一个Authority
provider通常有一个担当Android内部名称的单一权利。为了避免与其他providers起冲突,你应该把互联网域所有权(反过来)当作provider权利的基础来使用。因为这个建议对Android包名来说也是true,所以你可以把你的provider权利定义成一个含provider的包名继承。例如,如果你的Android包名是com.example.
2. 设计路径的结构
开发者经常会通过在权利后面,追加指向单独表的路径来创建content URI。例如,如果你有两个表:表1和表2,那么你应该组合前面示例的权利,来产生名为com.example.
3. 处理content URI的IDs
根据约定,通过接受一个在URI结尾行带有ID值的content URI,provider会提供对表中一个单一行的访问权。同时也是根据约定,provider会把ID值与表的_ID列相匹配,并且对匹配行执行请求的访问。这个约定会产生一个应用程序访问provider的通用设计模式。应用对provider执行查询,并且它会使用一个CursorAdapter来让结果Cursor在一个ListView上显示。要定义CursorAdapter,就必须要求Cursor中的其中一个列是_ID。然后用户可以从UI中挑选出显示行的其中一行,来查看或修改数据。应用程序会从返回ListView的Cursor中获取相应的行,获得这一行的_ID,把它追加到content URI,并且给provider发送访问请求。然后provider会对用户挑选出来的具体行执行查询或修改。
4. content URI的模式
为了帮助你选择采取哪个动作来应对一个输入的content URI,provider API会包含UriMatcher便利类,它会把content URI的“模式”映射到整型值。你可以在一个switch语句中使用整型值,switch语句是为匹配特殊模式的content URI或URIs来选择想要的动作。
一个Content URI会使用下面的通配字符来匹配content URIs:
(1)*:匹配一个有着任意长度、任意可用字符的字符串。
(2)#:匹配一个有着任意长度的数字字符串。
作为处理content URI的设计和编码的一个示例,你应该考虑带有com.example.app.provider权利的一个provider,它会组织下面这些指向表的content URI:
content://com.example.app.provider/table1:一个名为table1的表。
content://com.example.app.provider/table2/dabaset1:一个名为dataset1的表。
content://com.example.app.provider/table2/dataset2:一个名为dataset2的表。
content://com.example.app.provider/table3:一个名为table3的表
如果这些content URI有一个行ID追加到他们后面,那么provider也可以组织他们,例如:content://com.example.app.provider/table3/1,这表示在名为table3的表中是用1来标识行。
下面的content URI模式是可行的:
content://com.example.app.provider/*:匹配provider中的任意content URI。
content://com.example.app.provider/table2/*:匹配表dataset1和表dataset2中的一个content URI,但是它不会匹配表table1或表table3中的content URIs。
content://com.example.app.provider/table3/#: 为表table3中的单一行匹配一个content URI,如content://com.example.app.provider/table3/6,就用6来标识行。
下面的代码片段展示UriMatcher中的方法是如何工作的。通过对表用content://
addURI()方法会把权力和路径映射到一个整型值上。Match()方法会返回对应一个URI的整型值。一个switch语句会在查询整个表和查询单一记录之间选择,如代码清单3-2-1所示:
public class ExampleProvider extends ContentProvider { ... // 创建一个UriMatcher 对象. private static final UriMatcher sUriMatcher; ... sUriMatcher.addURI("com.example.app.provider", "table3", 1); sUriMatcher.addURI("com.example.app.provider", "table3/#", 2); ... // 实现 ContentProvider.query() public Cursor query( Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { ... switch (sUriMatcher.match(uri)) { // 如果是table3 case 1: if (TextUtils.isEmpty(sortOrder)) sortOrder = "_ID ASC"; break; // 如果是一个单行 case 2: selection = selection + "_ID = " uri.getLastPathSegment(); break; default: ... //如果URI不能确认,我们应该在这里处理错误 } }
代码清单3-2-1
另一个ContentUris类,它会提供一些便利方法,以便用来与content URI的id部分工作。Uri类和Uri.Builder会包含用来解析现有Uri对象和创建新对象的便利方法。
3-2.4 实现ContentProvider类
ContentProvider实例通过处理其他应用程序的请求,来对一套结构化的数据进行管理。所有访问的形式最终都会调用ContentResolver,然后它会调用ContentProvider的一个具体方法来获取访问权。
1. 必要的方法
抽象类ContentProvider会定义六个抽象方法,你必须把他们作为你自己具体子类的一部分来实现。除onCreate()方法之外的其他方法都是由尝试访问content provider的客户端应用程序调用。
(1)query():取出provider中的数据。使用参数来选择要查询的表、要返回的行和列,以及结果的排列次序。返回作为一个Cursor对象的数据。
(2)insert():插入一个新行到你的provider。使用参数来选择目标表并获得要使用的列值。返回新插入行的一个content URI。
(3)update():更新provider中的现有行。使用参数来选择要更新的表和行,并获得更新的列值。返回被更新行的数量。
(4) delete():清除provider中的行。使用参数来选择要清除的表和行。返回被清除行的数量。
(5) getType():返回对应一个content URI的MIME类型。
(6)onCreate():将你的provider初始化。Android系统会在它创建provider后,立刻调用这个方法。注意,你的provider会直到一个ContentResolver对象试图访问它时,才会被创建。
注意:这些方法与ContentResolver中的方法名字一样并有着相同的特征。
你要实现这些方法还应该考虑下面这些事情:
◆除onCreate()之外的其他方法都可以同时被多个线程调用,所以他们必须被设计成是线程安全的。
◆要避免在onCreate()上执行过长的操作。直到实际的需要他们时,才不会延迟初始化任务。
◆尽管你必须实现这些方法,你的代码却只要返回想要的数据类型。例如,你可能想阻止其他的应用程序把数据插入到一些表中。要做到这一点,你可以跳过对insert()方法的调用并返回0。
2. 实现query()方法:
ContentProvider.query()方法必须返回一个Cursor对象,或者如果没有返回,就要抛出一个Exception。如果你正在用一个SQLite数据库作为你的数据储存,那么你就只要返回Cursor,这个cursor是SQLiteDatabase类的query()方法之一返回的。如果查询没有匹配任何一个行,那么你应该返回一个Cursor实例,它的getCount()方法会返回0。只有在查询进程中有一个内部错误发生时,你才应该返回null。
如果你不用SQLite数据库来作为你的数据储存,那么你就用Cursor的具体子类之一来作数据存储。例如,MatrixCursor类会实现一个cursor,在它里面每一行都是一个Object数组。通过这个类,你可以用addRow()方法来添加一个新行。
记住,Android系统必须要能够在各个进程之间与Exception通信。对于下面可能对处理查询错误有用的异常,Android可以与他们通信:
(1)IllegalArgumentException(如果你的provider接收了一个无效的content URI,那么你可能会选择抛出这个异常)
(2)NullPointerException
3. 实现insert()方法:
Insert()方法会使用ContentValues参数中的值,来给合适的表添加一个新行。如果列名不在ContentValues参数中,那么你可能想要给你的provider代码或你的数据库模式(schema)提供一个默认值。
这个方法还应该返回新行的content URI。要构造它,你就要使用withAppendedId()方法,把新行的_ID(或其他主键)值追加到表的content URI后面。
4. 实现delete()方法:
Delete()方法没有必要物理删除数据存储中的行。如果你的provider正在使用一个同步adapter(适配器),那么你应该考虑用一个“删除”标记来为一个被删除行做标志,而不是整个地移除所有行。同步adapter(适配器)可以检查被删除行,并会在provider删除他们之前,从服务器中移除它们。
5. 实现update()方法:
Update()和insert()都采用相同的ContentValues参数,与delete()方法、ContentProvider.query()方法一样都是使用selection和selectionArgs参数,这就允许你在这些方法之间重复使用代码。
6. 实现onCreate()方法:
当Android系统启动provider时,它就会调用onCreate()方法。在这个方法中你应该只能执行快速运行的初始化任务,延长数据库的创建,并加载数据,直到provider实际接收一个数据请求时才停止。如果你在onCreate()上执行过长的任务,那将会减慢provider的启动速度。反过来,它将增加provider到其他应用程序的响应时间。
例如,如果你正在使用一个SQLite数据库,这个数据库可以在ContentProvider.onCreate()中创建一个新的SQLiteOpenHelper对象,然后在你第一次打开数据库时会创建SQL表。为了做到这一点,首先你就调用getWritableDatabase()方法,它会自动地调用SQLiteOpenHelper.onCreate()方法。
下面的两个代码片段就演示了ContentProvider.onCreate()和SQLiteOpenHelper.onCreate()之间的交互。下面我们就演示ContentProvider.onCreate()方法的实现,如代码清单3-2-2所示:
public class ExampleProvider extends ContentProvider private MainDatabaseHelper mOpenHelper; private static final String DBNAME = "mydb"; private SQLiteDatabase db; public boolean onCreate() { mOpenHelper = new SQLiteOpenHelper( getContext(), DBNAME, null, 1 ); return true; } ... public Cursor insert(Uri uri, ContentValues values) { ... db = mOpenHelper.getWritableDatabase(); } }
代码清单3-2-2
下面是SQLiteOpenHelper.onCreate()方法的实现,它包括一个辅助类,如代码清单3-2-3所示:
... private static final String SQL_CREATE_MAIN = "CREATE TABLE " + "main " + // Table 名 "(" + // table中的列 " _ID INTEGER PRIMARY KEY, " + " WORD TEXT" " FREQUENCY INTEGER " + " LOCALE TEXT )"; ... protected static final class MainDatabaseHelper extends SQLiteOpenHelper { MainDatabaseHelper(Context context) { super(context, DBNAME, null, 1); } public void onCreate(SQLiteDatabase db) { db.execSQL(SQL_CREATE_MAIN); } }
代码清单3-2-3
3-2.5 实现ContentProvider的MIME类型
ContentProvider类有两个返回MIME类型的方法:
getType()方法:它是你必须为provider实现的必需方法之一。
getStreamTypes()方法:如果你的provider会提供文件,那么这个是想要你去实现的方法。
1. 表的MIME类型
getType()方法会在MIME格式中返回一个String,这个MIME格式描述了由content provider参数返回的数据类型。Uri参数是一个模式,它并不是一个特定的URI;在这种情况下,你应该返回与匹配该模式的content URI相关联的数据类型。
对于通用数据类型如文本、HTML或JPEG来说,getType()方法应该返回这个数据的标准MIME类型。这些标准类型的一个完整列表在IANA MIME Media Types网站上可以用。
对指向数据表中一个或多个行的content URI来说,getType()方法会在Android的特定供应商MIME格式中返回一个MIME类型:
(1) 类型部分:vnd
(2)子类型部分:
◆如果对应的是一个单一行的URI模式:android.cursor.item/
◆如果对应的是多行的URI模式:android.cursor.dir/
(3)特定provide部分:vnd.
你提供
例如,如果provider的权利是com.example.app.provider,并且它暴露一个名为table1的表,那么在table1中对应多行的MIME类型就如代码清单3-2-4所示:
vnd.android.cursor.dir/vnd.com.example.provider.table1
代码清单3-2-4
在table1中对应单一行的MIME类型就如代码清单3-2-5所示:
vnd.android.cursor.item/vnd.com.example.provider.table1
代码清单3-2-5
2. 文件的MIME类型
如果你的provider会提供文件,那么就实现getStreamType()方法。这个方法会为文件返回一个MIME类型的String数组,这个文件是provider为一个给定的content URI返回来的。你应该通过MIME类型filter参数来filter这个MIME类型,这样你就只能返回那些客户端想要处理的MIME类型。
例如,考虑一个provider,它能提供相片是.jpg、.png和.gif格式的文件。如果一个应用程序用filter的image/ *字符串(一些是“图片”的东西)来调用ContentResolver.getStreamType()方法,然后ContentProvider.getStreamType()方法应该要返回下面的数组,如代码清单3-2-6所示:
{ "image/jpeg", "image/png", "image/gif"}
代码清单3-2-6
如果你的应用只对.jpg文件感兴趣,那么它用filter的*\/jpeg字符串就可以调用ContentResolver.getStreamTypes()方法,并且ContentProvider.getStreamTypes()方法应该要返回下面的数组,如代码清单3-2-7所示:
{"image/jpeg"}
代码清单3-2-7
如果你的provider不提供在filter字符串中请求的任意MIME类型,那么getStreamTypes()方法应该返回null。
3-2.6 实现一个Contract(合约)类
合约类是一个包含常量定义的public final类,常量定义是用于URIs、列名、MIME类型以及其他涉及到provider的元数据。这个类会通过确保provider可以被正确访问,即使URIS、列名的实际值改变也会被访问,这样它就可以在provider和其他应用程序之间建立一个合约。
合约类也会帮助开发者,因为它通常会为它的常量配备助记符名称,这样开发者就能较少的使用到列名或URIs的不正确值。因为它是一个类,所以它可以包含Javadoc文档。集成开发环境(IDE)如Eclipse可以自动补全合约类中的常量名并显示常量的Javadoc。
开发者不能访问应用程序中合约类的类文件,但它们可以静态地把你提供的一个.jar文件编译进应用程序。
ContactsContract类和它的内嵌类是合约类的示例。
3-2.7 Content Provider权限
权限和对Android系统所有方面的访问可以参见Security and Permissions。Data Storage主题也描述了对不同类型存储有效的安全与权限。简单来说,重要点就是下面这些:
- 默认情况下,存储在设备内部存储上的数据文件对应用程序和provider来说是私有。
- 你创建的SQLiteDatabase数据库对你的应用程序和provider来说是私有。
- 默认情况下,你保存在内部存储上的数据文件是公有的和全局可读的。你不能用content provider来限制对内部存储中文件的访问,因为其他应用程序可以使用其他API调用来读写他们。
- 为打开或创建文件或你设备内部存储上的SQLite数据库调用的方法可以隐式地给予其他应用程序读写访问权。如果你把一个内部文件或数据库作为provider的仓库,并且给与它“全局可读”或“全局可写”的访问权,那么在它manifest文件中你为provider设置的权限将不能保护你的数据。对内部存储中文件或数据库的默认访问权是“私有的”,而对你provider的仓库,你却不应该去改变。
如果你想用content provider权限来控制对数据的访问,那么你应该在内部文件、SQLite数据库或“云存储”(例如,在一个远程服务器)上来存储你的数据,并且你应该让文件和数据库对你的应用程序私有。
实现权限
所有的应用程序都可以读取或写入provider,即使底层数据是私有的,因为默认情况下,你的provider没有拥有权限设置。为了改变这点,使用属性或
你可以用manifest文件中的一个或多个
下面将介绍provider权限的作用域,从应用到整个provider的权限开始,然后权限会变得更细粒度。更细粒度的权限优先于那些范围大的权限。
- 单一读写provider-级的权限:控制对整个provider的读写访问权的一个权限,用
节点的android:permission属性指定。 - 分离读写provider-级的权限:对整个provider的一个读取权限和一个写入权限。你可以用
节点的android:readPermission和android:writePermission属性来指定他们。他们的权限优于android:permission所需的权限。 - 路径级权限:provider中对content URI的读取、写入或读/写权限。你可以用
节点的一个 子节点来指定每个你想要控制的URI。对于你指定的每个content URI来说,你可以指定一个读/写权限,一个读取权限或一个写入权限,或者三个权限都指定。这读取和写入权限优于读/写的权限。同样的,路径级权限优于provider-级的权限。 - 临时权限:这个是给应用程序授予临时访问权的一个权限级,即使应用程序没有正常情况下所必需的权限。临时访问权特性会减少应用程序不得不在manifest文件中请求的权限数量。当你打开临时权限时,provider所需“永久”权限的应用程序只是那些不断访问provider所有数据的应用程序。
当你想允许一个外部图片查看器应用程序来显示provider中的照片附件时,你可以考虑需要用来实现一个电子邮件provider和应用的权限。为了在没有所需的权限情况下,给予图片查看器所必需的访问权,你就要为照片的content URI配置临时权限。设计你的电子邮件应用,这样当用户想显示照片时,应用就可以给图片查看器发送一个包含照片content URI和权限标记的intent。然后图片查看器可以查询电子邮件provider来取出照片,即使查看器没有对provider的正常读取权限。
为了打开临时权限,你要么设置
这个属性值会决定你的provider拥有多少程度的可访问。如果属性被设置成true,那么系统将给你整个provider授予临时权限,覆盖你的provider-级或路径级权限所需的其他任意权限。
如果这个标记被设置成false,那么你必须把
为了给一个应用程序委托临时访问权,intent就必须包含FLAG_GRANT_READ_URI_PERMISSION或FLAG_GRANT_WRITE_URI_PERMISSION标记或者两个都包含。你可以用setFlags()方法来设置它们。
如果android:grantUriPermission属性不存在,那么它就会被假设成是false。
3-2.8 节点
与Activity、Service组件一样,一个ContentProvider子类必须是通过
- 权利(android:authorities):它是标识系统内整个provider的符号名。
- provider类名(android:name):这是实现ContentProvider的类。
- 权限:这是指定其他应用程序为了访问provider数据必须拥有的权限的属性:
◆android:grantUriPermission:临时权限标记
◆android:permission:单一provider-范围的读/写权限
◆android:readPermission:provider-范围的读取权限
◆android:writaPermission:provider-范围的写入权限
- 启动和控制属性:这些属性决定Android系统怎样启动和何时启动provider,provider的进程特征以及其他运行设置:
◆android:enabled:允许系统启动provider的标记
◆android:exported:允许其他应用程序使用这个provider的标记
◆android:initOrder:相对于同一进程中的其他provider来说,这个provider应该被启动的顺序
◆android:multiProcess:允许系统在同一进程上启动provider作为调用客户端的标记。
◆android:process:provider应该运行的进程名
◆android:syncable:指示provider数据将被服务器上的数据同步的标记
这些属性会在Android Manifest章节的
- 带信息的属性:一个用于provider的可选图标和标签
◆android:icon:是一个可绘制资源,它包含一个用于provider的图标。在设置>应用>全部的应用列表中,图标显示在provider的标签旁边。
◆android:label:一个介绍provider或它的数据或者两个都介绍的信息标签。标签显示在设置>应用>全部的应用列表中。
这个属性会在Android Manifest章节的
3-2.9 Intents和数据访问
应用程序可以用一个Intent来间接访问content provider。它不会调用ContentResolver或ContentProvider的任何方法。相反,它会发送一个启动activity的intent,这个activity经常是provider自己应用程序的一部分。目标activity负责取出或显示UI中的数据。它会依赖intent中的动作,所以目标activity也可能会提示用户来对provider中的数据做一些修改。一个intent也会包含目标activity在UI显示的“额外”数据;然后用户会在用它来修改provider数据之前,拥有改变这个数据的选择权。
你可能想用intent访问来帮助确保数据的完整性。你的provider中数据应该严格根据定义的业务逻辑来被插入、更新和删除。如果是这种情况的话,允许其他应用程序直接地修改你的数据将可能导致无效数据。如果你想开发者使用intent 访问,那么就要确保你已经详细的阅读了关于intent访问的介绍。向他们解释为什么用你自己应用程序的UI的intent访问,会比用他们的代码来尝试修改数据要好得多。
处理一个想修改provider数据的输入intent和处理其他的intent之间没有什么不同。
本文来自jy02432443,QQ78117253。是本人辛辛苦苦一个个字码出来的,转载请保留出处,并保留追究法律责任的权利