名著阅读 > Android程序设计:第2版 > 一个完整的内容提供者代码:SimpleFinchVideoContentProvider >

一个完整的内容提供者代码:SimpleFinchVideoContentProvider

通过前面的介绍,已经了解了和编写内容提供者及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匹配,查询数据库并返回光标:


@Override
public 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类:


@Override
public 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方法返回的是该操作所影响的记录数:


@Override
public 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方法也是返回该操作所影响的记录数:


@Override
public 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的客户端。