通過前面的介紹,已經瞭解了和編寫內容提供者及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的客戶端。