讀古今文學網 > 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);
1
    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);
2
            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;
3
}
  

以下是一些重點代碼的解釋:

1 使用預構建的URI匹配器匹配URI。

2 設置FinchVideo.SimpleVideos.CONTENT_URI的通知URI,它使得游標能夠接收到該URI所指向的數據的所有內容解析程序通知事件。在這個例子中,cursor會接收到和所有視頻相關的所有事件,因為FinchVideo.SimpleVideos.CONTENT_URI就是指向這些事件。

3 直接返回光標。正如前面提到的,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);
1
        getContext.getContentResolver.
            notifyChange(videoURi, null);
2
        return videoURi;
    }
    throw new SQLException(\"Failed to insert row into \" + uri);
}
  

insert方法還會匹配輸入的URI,執行相應的數據庫插入操作,然後返回指向新的數據庫記錄的URI。因為SQLiteDatabase.insert方法返回的是新插入記錄的數據庫記錄ID,即_id字段的值,所以內容提供者可以很容易地通過把rowID變量附加到在第3章提到的內容提供者public API中定義的內容提供者authority中,生成正確的URI。

以下是代碼的一些要點:

1 使用Android工具管理內容提供者URI——特別是,ContentUris.withAppendedId把rowId作為返回結果插入URI的ID。客戶端也可以使用該URI查詢內容提供者,選擇包含插入記錄的數據值的游標。

2 內容提供者通知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的客戶端。