通过前面的介绍,已经了解了和编写内容提供者及Android MVC关联的重要任务——Android内容提供者的通信系统。下面我们一起来看看如何构建自己的内容提供者。如下所示,SimpleFinchVideoContentProvider类继承自ContentProvider类:
public class SimpleFinchVideoContentProvider extends ContentProvider {
SimpleFinchVideoContentProvider类和实例变量
和前面一样,在查看方法是如何工作的之前,最好先理解该方法所使用的主要类和实例变量。对于SimpleFinchVideoContentProvider,需要理解的成员变量是:
private static final String DATABASE_NAME = /"simple_video.db/";private static final int DATABASE_VERSION = 2;private static final String VIDEO_TABLE_NAME = /"video/";private DatabaseHelper mOpenHelper;
DATABASE_NAME
设备上的数据库文件名称。对于简单的Finch视频,该文件的完整路径是/data/data/com.oreilly.demo.pa.finchvideo/databases/simple_video.db。
DATABASE_VERSION
和代码兼容的数据库版本。如果其版本号比数据库本身的版本号高,应用会调用DatabaseHelper.onUpdate方法。
VIDEO_TABLE_NAME
simple_video数据库内的视频表的名称。
mOpenHelper
onCreate方法中初始化的数据库helper实例变量。它为insert、query、update和delete方法提供了访问数据库的方式。
sUriMatcher
静态初始化代码块,它执行静态变量的初始化,这些变量不能作为简单的单行语句执行。例如,简单视频内容提供者就是以在UriMatcher的静态初始化部分构建内容提供者URI映射开始的,构建的具体方式如下:
private static UriMatcher sUriMatcher; private static final int VIDEOS = 1; private static final int VIDEO_ID = 2; static { sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); sUriMatcher.addURI(AUTHORITY, FinchVideo.SimpleVideos.VIDEO_NAME, VIDEOS); // use of the hash character indicates matching of an id sUriMatcher.addURI(AUTHORITY, FinchVideo.SimpleVideos.VIDEO_NAME + /"/#/", VIDEO_ID); ... // more initialization to follow
UriMatcher类提供了基础的便捷工具,Android使用这些工具实现对内容提供者URI的映射。要使用UriMatcher实例,需要把URI字符串,如videos映射到常量成员变量。在这里,映射工作如下:应用首先给提供者UriMatcher的构造函数提供一个参数Uri Matcher.NO_MATCH,定义所有的URI都和给定的URI不匹配。然后,应用把多个视频的映射添加到VIDEOS,然后把特定视频映射到VIDEO_ID。对于映射到整数值的所有提供者URI,该提供者可以执行切换操作,跳到多个和单个视频的相应的处理代码。
该映射使得如content://com.oreilly.demo.pa.finch video.SimpleFinchVideo/video这样的URI映射到常量VIDEOS,表示所有的视频。单个视频的URI,如content://oreilly.demo.pa.finchvideo.SimpleFinchVideo/video/7,对于单个视频,会映射到常量VIDEO_ID。URI匹配绑定的散列标识是以整数结束的通配符。
sVideosProjectionMap
它是query方法使用的项目映射。该HashMap把内容提供者的字段名映射到了数据库的字段。项目映射不是必须的,但是如果使用这个项目映射,就必须列出query方法可能返回的所有字段。在SimpleFinchVideoContentProvider类中,内容提供者的字段和数据库的字段名称是完全一样的,因此sVideosProjectionMap不是必须的。但是在这里,我们提供该项目映射就是为了说明它,有时候应用可能会用到它。在下面这段代码中,创建了一个示例映射:
// example projection map, not actually used in this application sVideosProjectionMap = new HashMap<String, String>; sVideosProjectionMap.put(FinchVideo.Videos._ID, FinchVideo.Videos._ID); sVideosProjectionMap.put(FinchVideo.Videos.TITLE, FinchVideo.Videos.TITLE); sVideosProjectionMap.put(FinchVideo.Videos.VIDEO, FinchVideo.Videos.VIDEO); sVideosProjectionMap.put(FinchVideo.Videos.DESCRIPTION, FinchVideo.Videos.DESCRIPTION);
实现onCreate方法
在SimpleFinchVideoContentProvider的初始化中,创建了该视频的SQLite数据存储,具体代码如下:
private static class DatabaseHelper extends SQLiteOpenHelper { public void onCreate(SQLiteDatabase sqLiteDatabase) { createTable(sqLiteDatabase); } // create table method may also be called from onUpgrade private void createTable(SQLiteDatabase sqLiteDatabase) { String qs = /"CREATE TABLE /" + VIDEO_TABLE_NAME + /" (/" + FinchVideo.SimpleVideos._ID + /" INTEGER PRIMARY KEY, /" + FinchVideo.SimpleVideos.TITLE_NAME + /" TEXT, /" + FinchVideo.SimpleVideos.DESCRIPTION_NAME + /" TEXT, /" + FinchVideo.SimpleVideos.URI_NAME + /" TEXT);/"; sqLiteDatabase.execSQL(qs); }}
当创建SQLite表以支持内容提供者操作时,开发人员需要提供_id字段。虽然你可能不太清楚为什么要提供这个字段,除非你详细阅读了Android开发者文档,但Android内容管理系统确实强制要求在query方法返回的游标中必须有_id字段。_id用于和内容提供者URL中的特殊的#字符匹配。例如,如content://contacts/people/25这样的URL会映射到contacts表中_id为25的数据记录。强制提供_id实际上是为了用一个专用的名称来表示数据库表的主键。
实现getType方法
下一步,实现getType方法以确定从客户端传递过来的任意URI的MIME类型。正如你将在下面的代码中所见到的,在public API的定义中,采用URI表示VIDEOS,VIDEO_ID表示MIME类型。
public String getType(Uri uri) { switch (sUriMatcher.match(uri)) { case VIDEOS: return FinchVideo.SimpleVideos.CONTENT_TYPE; case VIDEO_ID: return FinchVideo.SimpleVideos.CONTENT_VIDEO_TYPE; default: throw new IllegalArgumentException(/"Unknown video type: /" + uri); }}
实现提供者API
内容提供者的实现中必须覆盖基类ContentProvider的各个数据处理方法:insert、query、update和delete。对于本章的简单视频应用这个例子,这些方法是在SimpleFinchVideoContentProvider类中定义的。
query方法
当匹配了输入的URI后,内容提供者的query方法会把处理委托给SQLiteDatabase.query,在一个可读的数据库上执行相应的选择操作,然后以数据库Cursor对象的形式返回结果。该游标会包含URI参数所描述的所有数据库记录。在执行完查询后,Android的内容提供者机制会自动支持多进程使用cursor实例,它支持提供者的query方法简单地把cursor值作为正常值返回,使得其他进程的客户端可以使用该返回值。
query方法还支持参数uri、projection、selection、selectionArgs和sortOrder,其使用方式和我们在第9章中介绍的SQLiteDatabase.query方法相同。正如任何SQL SELECT语句那样,query方法的参数使得提供者客户端只需要选择和query参数匹配的特定视频。除了传递URI,调用SimpleFinchVideoContentProvider的客户端还可以传递包含where参数的where子句。例如,开发人员可以使用这个参数实现对某个作者的视频的查询。
注意:正如我们看到的,Android的MVC模式依赖于游标和它们包含的数据,以及框架的内容观察者更新消息的传递。因为进程会共享Cursor对象,所以内容提供者实现必须注意不要在query方法中关闭游标。如果游标在query方法中被关闭了,那么客户端将无法看到抛出的异常;相反,游标总是会表现得似乎其指向的数据是空的,而且不再接收更新事件——由activity负责合理地管理返回的游标。
当数据库查询完成后,provider会调用Cursor.setNotificationUri方法来设置URI,提供者架构要根据这个URI决定哪个provider更新事件要被传递给新创建的游标。该URI成为观察URI指向的数据的客户端和通知该URI的内容提供者之间的交互参数。简单的方法调用驱动内容提供者更新消息,我们在P333“Android MVC和内容查看器”一节中探讨过。
下面给出本书描述的内容提供者的query方法,它执行URI匹配,查询数据库并返回光标:
@Overridepublic Cursor query(Uri uri, String projection, String where, String whereArgs, String sortOrder){ // If no sort order is specified use the default String orderBy; if (TextUtils.isEmpty(sortOrder)) { orderBy = FinchVideo.SimpleVideos.DEFAULT_SORT_ORDER; } else { orderBy = sortOrder; } int match = sUriMatcher.match(uri);① Cursor c; switch (match) { case VIDEOS: // query the database for all videos c = mDb.query(VIDEO_TABLE_NAME, projection, where, whereArgs, null, null, sortOrder); c.setNotificationUri( getContext.getContentResolver, FinchVideo.SimpleVideos.CONTENT_URI);② break; case VIDEO_ID: // query the database for a specific video long videoID = ContentUris.parseId(uri); c = mDb.query(VIDEO_TABLE_NAME, projection, FinchVideo.Videos._ID + /" = /" + videoID + (!TextUtils.isEmpty(where) ? /" AND (/" + where + /')/' : /"/"), whereArgs, null, null, sortOrder); c.setNotificationUri( getContext.getContentResolver, FinchVideo.SimpleVideos.CONTENT_URI); break; default: throw new IllegalArgumentException(/"unsupported uri: /" + uri); } return c;③}
以下是一些重点代码的解释:
① 使用预构建的URI匹配器匹配URI。
② 设置FinchVideo.SimpleVideos.CONTENT_URI的通知URI,它使得游标能够接收到该URI所指向的数据的所有内容解析程序通知事件。在这个例子中,cursor会接收到和所有视频相关的所有事件,因为FinchVideo.SimpleVideos.CONTENT_URI就是指向这些事件。
③ 直接返回光标。正如前面提到的,Android的内容提供者系统支持进程之间光标中的数据的共享。进程间数据作为内容提供者系统的一部分“自由”共享。可以返回光标,选择不同的进程就可以访问该光标。
insert方法
insert方法接收客户端输入的数据值,校验这些值,然后向数据库中增加包含这些值的一条新的记录。这些数据值会传递给ContentValues对象的ContentProvider类:
@Overridepublic Uri insert(Uri uri, ContentValues initialValues) { // Validate the requested uri if (sUriMatcher.match(uri) != VIDEOS) { throw new IllegalArgumentException(/"Unknown URI /" + uri); } ContentValues values; if (initialValues != null) { values = new ContentValues(initialValues); } else { values = new ContentValues; } verifyValues(values); // insert the initialValues into a new database row SQLiteDatabase db = mOpenDbHelper.getWritableDatabase; long rowId = db.insert(VIDEO_TABLE_NAME, FinchVideo.SimpleVideos.VIDEO_NAME, values); if (rowId > 0) { Uri videoURi = ContentUris.withAppendedId( FinchVideo.SimpleVideos.CONTENT_URI, rowId);① getContext.getContentResolver. notifyChange(videoURi, null);② return videoURi; } throw new SQLException(/"Failed to insert row into /" + uri);}
insert方法还会匹配输入的URI,执行相应的数据库插入操作,然后返回指向新的数据库记录的URI。因为SQLiteDatabase.insert方法返回的是新插入记录的数据库记录ID,即_id字段的值,所以内容提供者可以很容易地通过把rowID变量附加到在第3章提到的内容提供者public API中定义的内容提供者authority中,生成正确的URI。
以下是代码的一些要点:
① 使用Android工具管理内容提供者URI——特别是,ContentUris.withAppendedId把rowId作为返回结果插入URI的ID。客户端也可以使用该URI查询内容提供者,选择包含插入记录的数据值的游标。
② 内容提供者通知URI,向相关的游标发送和传递内容更新事件。注意,提供者的通知调用是唯一会被发送给内容观察者的事件。
update方法
update方法和insert方法的执行方式相同。update方法在相应的数据库上执行,以改变URI所指向的数据库记录。但update方法返回的是该操作所影响的记录数:
@Overridepublic int update(Uri uri, ContentValues values, String where, String whereArgs){ // the call to notify the uri after deletion is explicit getContext.getContentResolver.notifyChange(uri, null); SQLiteDatabase db = mOpenDbHelper.getWritableDatabase; int affected; switch (sUriMatcher.match(uri)) { case VIDEOS: affected = db.update(VIDEO_TABLE_NAME, values, where, whereArgs); break; case VIDEO_ID: String videoId = uri.getPathSegments.get(1); affected = db.update(VIDEO_TABLE_NAME, values, FinchVideo.SimpleVideos._ID + /"=/" + videoId + (!TextUtils.isEmpty(where) ? /" AND (/" + where + /')/' : /"/"), whereArgs); break; default: throw new IllegalArgumentException(/"Unknown URI /" + uri); } getContext.getContentResolver.notifyChange(uri, null); return affected;}
delete方法
delete方法和update方法类似,它会删除给定URI所指向的记录。和update方法类似,delete方法也是返回该操作所影响的记录数:
@Overridepublic int delete(Uri uri, String where, String whereArgs) { int match = sUriMatcher.match(uri); int affected; switch (match) { case VIDEOS: affected = mDb.delete(VIDEO_TABLE_NAME, (!TextUtils.isEmpty(where) ? /" AND (/" + where + /')/' : /"/"), whereArgs); break; case VIDEO_ID: long videoId = ContentUris.parseId(uri); affected = mDb.delete(VIDEO_TABLE_NAME, FinchVideo.SimpleVideos._ID + /"=/" + videoId + (!TextUtils.isEmpty(where) ? /" AND (/" + where + /')/' : /"/"), whereArgs); // the call to notify the uri after deletion is explicit getContext.getContentResolver. notifyChange(uri, null); break; default: throw new IllegalArgumentException(/"unknown video element: /" + uri); } return affected;}
注意,前面介绍的只是简单内容提供者中所需的内容,更复杂的场景中可能涉及某个查询需要连接多张表,或者某个数据项会被级联删除。内容提供者可以通过Android的SQLite API自由选择自己的数据管理机制,只要它不破坏内容提供者的客户端API。
确定通知Observer的频繁度
正如我们在内容提供者数据管理操作列表中看到的,在Android内容管理系统中,通知不是免费的:SQLite表的插入操作不会自动替内容提供者设置发送消息通知,需要提供者的开发人员实现一种机制,确定发送通知的合适时间,决定当内容提供者的数据发生改变时,应该发送哪个URI。通常情况下,Android中的内容提供者会马上为在某个数据操作后发生变化的所有的URI发送通知。
当开发人员设计通知机制时,应该考虑如下的权衡:细粒度的通知机制会带来更精确的变化更新,这会降低用户接口系统的负载。如果在一个列表中,有一个元素发生了变化,如果该元素可见,则该列表可以只重绘该元素。但是细粒度的通知机制的缺点在于,在系统中需要发送更多的事件。由于UI会接收更多的通知事件,它有可能需要绘制更多次。粗粒度的通知在系统中发送的事件较少,但是这通常意味着UI每次接收到通知时,需要重新绘制的工作量更大。例如,一个列表在收到要求更新所有元素的事件时,可能实际上只有3个元素发生了变化。这里建议在选择通知机制时,要考虑到不同粒度的利弊。例如你可能想要等待读完大量的事件后,再发送“改变所有元素”的事件,而不是每接到一个事件就发送一次更新。
通常情况下,内容提供者只是通知和数据变化相关的URI的客户端。