上文簡要介紹了Android應用程序組件Content Provider在應用程序間共享數據的原理,但是沒有進一步研究它的實現。本文將實現兩個應用程序,其中一個以Content Provider的形式來提供數據訪問入口,另一個通過這個Content Provider來訪問這些數據。本文的例子不僅可以為下文分析Content Provider的實現原理準備好使用情景,還可以學習到它的一個未公開接口。
本文中的應用程序是按照上一篇文章Android應用程序組件Content Provider簡要介紹和學習計劃中提到的一般應用程序架構方法來設計的。本文包含兩個應用程序,其中,第一個應用程序命名為ArticlesProvider,它使用了SQLite數據庫來維護一個文章信息列表,同時,它定義了訪問這個文章信息列表的URI,這樣,我們就可以通過一個Content Provider組件來向第三方應用程序提供訪問這個文章信息列表的接口;第二個應用程序命名為Article,它提供了管理保存在ArticlesProvider應用程序中的文章信息的界面入口,在這個應用程序中,用戶可以添加、刪除和修改這些文章信息。接下來我們就分別介紹這兩個應用程序的實現。
1. ArticlesProvider應用程序的實現
首先是參照在Ubuntu上為Android系統內置Java應用程序測試Application Frameworks層的硬件服務一文,在packages/experimental目錄下建立工程文件目錄ArticlesProvider。在繼續介紹這個應用程序的實現之前,我們先介紹一下這個應用程序用來保存文章信息的數據庫的設計。
我們知道,在Android系統中,內置了一款輕型的數據庫SQLite。SQLite是專門為嵌入式產品而設計的,它具有占用資源低的特點,而且是開源的,非常適合在Android平臺中使用,關于SQLite的更多信息可以訪問官方網站http://www.sqlite.org。
ArticlesProvider應用程序就是使用SQLite來作為數據庫保存文章信息的,數據庫文件命名為Articles.db,它里面只有一張表ArticlesTable,表的結構如下所示:
-------------------------------------------------------------
| -- _id -- | -- _title -- | -- _abstrat -- | -- _url -- |
-------------------------------------------------------------
| | | | |
它由四個字段表示,第一個字段_id表示文章的ID,類型為自動遞增的integer,它作為表的key值;第二個字段_title表示文章的題目,類型為text;第三個字段_abstract表示文章的摘要,類型為text;第四個字段_url表示文章的URL,類型為text。注意,當我們打算將數據庫表的某一列的數據作為一個數據行的ID時,就約定它的列名為_id。這是因為我們經常需要從數據庫中獲取一批數據,這些數據以Cursor的形式返回,對這些返回來的數據我們一般用一個ListView來顯示,而這個ListView需要一個數據適配器Adapter來作為數據源,這時候就我們就可以以這個Cursor來構造一個Adapter。有些Adapter,例如android.widget.CursorAdapter,它們在實現自己的getItemId成員函數來獲取指定數據行的ID時,就必須要從這個Cursor中相應的行里面取出列名為_id的字段的內容出來作為這個數據行的ID返回給調用者。當然,我們不在數據庫表中定義這個_id列名也是可以的,不過這樣從數據庫中查詢數據后得到的Cursor適合性就變差了,因此,建議我們在設計數據庫表時,盡量設置其中一個列名字_id,并且保證這一列的內容是在數據庫表中是唯一的。
下面我們就開始介紹這個應用程序的實現了。這個應用程序只有兩個源文件,分別是Articles.java和ArticlesProvider,都是放在shy.luo.providers.articles這個package下面。在Articles.java文件里面,主要是定義了一些常量,例如用來訪問文章信息數據的URI、MIME(Multipurpose Internet Mail Extensions)類型以及格式等,這些常量是第三方應用程序訪問這些文章信息數據時要使用到的,因此,我們把它定義在一個單獨的文件中,稍后我們會介紹如果把這個Articles.java文件打包成一個jar文件,然后第三方應用程序就可以引用這個常量了,這樣也避免了直接把這個源代碼文件暴露給第三方應用程序。
源文件Articles.java位于src/shy/luo/providers/articles目錄下,它的內容如下所示:
package shy.luo.providers.articles; import android.net.Uri; public class Articles { /*Data Field*/ public static final String ID = "_id"; public static final String TITLE = "_title"; public static final String ABSTRACT = "_abstract"; public static final String URL = "_url"; /*Default sort order*/ public static final String DEFAULT_SORT_ORDER = "_id asc"; /*Call Method*/ public static final String METHOD_GET_ITEM_COUNT = "METHOD_GET_ITEM_COUNT"; public static final String KEY_ITEM_COUNT = "KEY_ITEM_COUNT"; /*Authority*/ public static final String AUTHORITY = "shy.luo.providers.articles"; /*Match Code*/ public static final int ITEM = 1; public static final int ITEM_ID = 2; public static final int ITEM_POS = 3; /*MIME*/ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.shy.luo.article"; public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.shy.luo.article"; /*Content URI*/ public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/item"); public static final Uri CONTENT_POS_URI = Uri.parse("content://" + AUTHORITY + "/pos"); }ID、TITLE、ABSTRACT和URL四個常量前面已經解釋過了,它是我們用來保存文章信息的數據表的四個列名;DEFAULT_SORT_ORDER常量是調用ContentProvider接口的query函數來查詢數據時用的,它表示對查詢結果按照_id列的值從小到大排列;METHOD_GET_ITEM_COUNT和KEY_ITEM_COUNT兩個常量是調用ContentProvider接口的一個未公開函數call來查詢數據時用的,它類似于微軟COM中的IDispatch接口的Invoke函數,使用這個call函數時,傳入參數METHOD_GET_ITEM_COUNT表示我們要調用我們自定義的ContentProvider子類中的getItemCount函數來獲取數據庫中的文章信息條目的數量,結果放在一個Bundle中以KEY_ITEM_COUNT為關鍵字的域中。
剩下的常量都是跟數據URI相關的,這個需要詳細解釋一下。URI的全稱是Universal Resource Identifier,即通用資源標志符,通過它用來唯一標志某個資源在網絡中的位置,它的結構和我們常見的HTTP形式URL是一樣的,其實我們可以把常見的HTTP形式的URL看成是URI結構的一個實例,URI是在更高一個層次上的抽象。在Android系統中,它也定義了自己的用來定痊某個特定的Content Provider的URI結構,它通常由四個組件來組成,如下所示:
[content://][shy.luo.providers.articles][/item][/123]
|------A------|-----------------B-------------------|---C---|---D--|
A組件稱為Scheme,它固定為content://,表示它后面的路徑所表示的資源是由Content Provider來提供的。
B組件稱為Authority,它唯一地標識了一個特定的Content Provider,因此,這部分內容一般使用Content Provider所在的package來命名,使得它是唯一的。
C組件稱為資源路徑,它表示所請求的資源的類型,這部分內容是可選的。如果我們自己所實現的Content Provider只提供一種類型的資源訪問,那么這部分內部就可以忽略;如果我們自己實現的Content Provider同時提供了多種類型的資源訪問,那么這部分內容就不可以忽略了。例如,我們有兩種電腦資源可以提供給用戶訪問,一種是筆記本電腦,一種是平板電腦,我們就把分別它們定義為notebook和pad;如果我們想進一步按照系統類型來進一步細分這兩種電腦資源,對筆記本電腦來說,一種是安裝了windows系統的,一種是安裝了linux系統的,我們就分別把它們定義為notebook/windows和notebook/linux;對平板電腦來說,一種是安裝了ios系統的,一種是安裝了android系統的,我們就分別把它們定義為pad/ios和pad/android。
D組件稱為資源ID,它表示所請求的是一個特定的資源,它通常是一個數字,對應前面我們所介紹的數據庫表中的_id字段的內容,它唯一地標志了某一種資源下的一個特定的實例。繼續以前面的電腦資源為例,如果我們請求的是編號為123的裝了android系統的平板電腦,我們就把它定義為pad/android/123。當忽略這部分內容時,它有可能是表示請求某一種資源下的所有實例,取決于我們的URI匹配規則,后面我們將會進一步解釋如何設置URI匹配規則。
回到上面的Articles.java源文件中,我們定義了兩個URI,分別用COTENT_URI和CONTENT_POS_URI兩個常量來表示,它們的Authority組件均指定為shy.luo.providers.articles。其中,COTENT_URI常量表示的URI表示是通過ID來訪問文章信息的,而CONTENT_POS_URI常量表示的URI表示是通過位置來訪問文章信息的。例如,content://shy.luo.providers.articles/item表示訪問所有的文章信息條目;content://shy.luo.providers.articles/item/123表示只訪問ID值為123的文章信息條目;content://shy.luo.providers.articles/pos/1表示訪問數據庫表中的第1條文章信息條目,這條文章信息條目的ID值不一定為1。通過常量CONTENT_POS_URI來訪問文章信息條目時,必須要指定位置,這也是我們設置的URI匹配規則來指定的,后面我們將會看到。
此外,我們還需要定義與URI對應的資源的MIME類型。每個MIME類型由兩部分組成,前面是數據的大類別,后面定義具體的種類。在Content Provider中,URI所對應的資源的MIME類型的大類別根據同時訪問的資源的數量分為兩種,對于訪問單個資源的URI,它的大類別就為vnd.android.cursor.item,而對于同時訪問多個資源的URI,它的大類別就為vnd.android.cursor.dir。Content Provider的URI所對應的資源的MIME類型的具體類別就需要由Content Provider的提供者來設置了,它的格式一般為vnd.[company name].[resource type]的形式。例如,在我們的例子中,CONTENT_TYPE和COTENT_ITEM_TYPE兩個常量分別定義了兩種MIME類型,它們的大類別分別為vnd.android.cursor.dir和vnd.android.cursor.item,而具體類別均為vdn.shy.luo.article,其中shy.luo就是表示公司名了,而article表示資源的類型為文章。這兩個MIME類型常量主要是在實現ContentProvider的getType函數時用到的,后面我們將會看到。
最后,ITEM、ITEM_ID和POS_ID三個常量分別定了三個URI匹配規則的匹配碼。如果URI的形式為content://shy.luo.providers.articles/item,則匹配規則返回的匹配碼為ITEM;如果URI的形式為content://shy.luo.providers.articles/item/#,其中#表示任意一個數字,則匹配規則返回的匹配碼為ITEM_ID;如果URI的形式為#也是表示任意一個數字,則匹配規則返回的匹配碼為ITEM_POS。這三個常量的用法我們在后面也將會看到。
這樣,Articles.java文件的內容就介紹完了。下面我們再接著介紹位于src/shy/luo/providers/articles目錄下的ArticlesProvider.java文件,它的內容如下所示:
import java.util.HashMap; import android.content.ContentValues; import android.content.Context; import android.content.UriMatcher; import android.content.ContentProvider; import android.content.ContentUris; import android.content.ContentResolver; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.os.Bundle; import android.text.TextUtils; import android.util.Log; public class ArticlesProvider extends ContentProvider { private static final String LOG_TAG = "shy.luo.providers.articles.ArticlesProvider"; private static final String DB_NAME = "Articles.db"; private static final String DB_TABLE = "ArticlesTable"; private static final int DB_VERSION = 1; private static final String DB_CREATE = "create table " + DB_TABLE + " (" + Articles.ID + " integer primary key autoincrement, " + Articles.TITLE + " text not null, " + Articles.ABSTRACT + " text not null, " + Articles.URL + " text not null);"; private static final UriMatcher uriMatcher; static { uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); uriMatcher.addURI(Articles.AUTHORITY, "item", Articles.ITEM); uriMatcher.addURI(Articles.AUTHORITY, "item/#", Articles.ITEM_ID); uriMatcher.addURI(Articles.AUTHORITY, "pos/#", Articles.ITEM_POS); } private static final HashMap<String, String> articleProjectionMap; static { articleProjectionMap = new HashMap<String, String>(); articleProjectionMap.put(Articles.ID, Articles.ID); articleProjectionMap.put(Articles.TITLE, Articles.TITLE); articleProjectionMap.put(Articles.ABSTRACT, Articles.ABSTRACT); articleProjectionMap.put(Articles.URL, Articles.URL); } private DBHelper dbHelper = null; private ContentResolver resolver = null; @Override public boolean onCreate() { Context context = getContext(); resolver = context.getContentResolver(); dbHelper = new DBHelper(context, DB_NAME, null, DB_VERSION); Log.i(LOG_TAG, "Articles Provider Create"); return true; } @Override public String getType(Uri uri) { switch (uriMatcher.match(uri)) { case Articles.ITEM: return Articles.CONTENT_TYPE; case Articles.ITEM_ID: case Articles.ITEM_POS: return Articles.CONTENT_ITEM_TYPE; default: throw new IllegalArgumentException("Error Uri: " + uri); } } @Override public Uri insert(Uri uri, ContentValues values) { if(uriMatcher.match(uri) != Articles.ITEM) { throw new IllegalArgumentException("Error Uri: " + uri); } SQLiteDatabase db = dbHelper.getWritableDatabase(); long id = db.insert(DB_TABLE, Articles.ID, values); if(id < 0) { throw new SQLiteException("Unable to insert " + values + " for " + uri); } Uri newUri = ContentUris.withAppendedId(uri, id); resolver.notifyChange(newUri, null); return newUri; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { SQLiteDatabase db = dbHelper.getWritableDatabase(); int count = 0; switch(uriMatcher.match(uri)) { case Articles.ITEM: { count = db.update(DB_TABLE, values, selection, selectionArgs); break; } case Articles.ITEM_ID: { String id = uri.getPathSegments().get(1); count = db.update(DB_TABLE, values, Articles.ID + "=" + id + (!TextUtils.isEmpty(selection) ? " and (" + selection + ')' : ""), selectionArgs); break; } default: throw new IllegalArgumentException("Error Uri: " + uri); } resolver.notifyChange(uri, null); return count; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { SQLiteDatabase db = dbHelper.getWritableDatabase(); int count = 0; switch(uriMatcher.match(uri)) { case Articles.ITEM: { count = db.delete(DB_TABLE, selection, selectionArgs); break; } case Articles.ITEM_ID: { String id = uri.getPathSegments().get(1); count = db.delete(DB_TABLE, Articles.ID + "=" + id + (!TextUtils.isEmpty(selection) ? " and (" + selection + ')' : ""), selectionArgs); break; } default: throw new IllegalArgumentException("Error Uri: " + uri); } resolver.notifyChange(uri, null); return count; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { Log.i(LOG_TAG, "ArticlesProvider.query: " + uri); SQLiteDatabase db = dbHelper.getReadableDatabase(); SQLiteQueryBuilder sqlBuilder = new SQLiteQueryBuilder(); String limit = null; switch (uriMatcher.match(uri)) { case Articles.ITEM: { sqlBuilder.setTables(DB_TABLE); sqlBuilder.setProjectionMap(articleProjectionMap); break; } case Articles.ITEM_ID: { String id = uri.getPathSegments().get(1); sqlBuilder.setTables(DB_TABLE); sqlBuilder.setProjectionMap(articleProjectionMap); sqlBuilder.appendWhere(Articles.ID + "=" + id); break; } case Articles.ITEM_POS: { String pos = uri.getPathSegments().get(1); sqlBuilder.setTables(DB_TABLE); sqlBuilder.setProjectionMap(articleProjectionMap); limit = pos + ", 1"; break; } default: throw new IllegalArgumentException("Error Uri: " + uri); } Cursor cursor = sqlBuilder.query(db, projection, selection, selectionArgs, null, null, TextUtils.isEmpty(sortOrder) ? Articles.DEFAULT_SORT_ORDER : sortOrder, limit); cursor.setNotificationUri(resolver, uri); return cursor; } @Override public Bundle call(String method, String request, Bundle args) { Log.i(LOG_TAG, "ArticlesProvider.call: " + method); if(method.equals(Articles.METHOD_GET_ITEM_COUNT)) { return getItemCount(); } throw new IllegalArgumentException("Error method call: " + method); } private Bundle getItemCount() { Log.i(LOG_TAG, "ArticlesProvider.getItemCount"); SQLiteDatabase db = dbHelper.getReadableDatabase(); Cursor cursor = db.rawQuery("select count(*) from " + DB_TABLE, null); int count = 0; if (cursor.moveToFirst()) { count = cursor.getInt(0); } Bundle bundle = new Bundle(); bundle.putInt(Articles.KEY_ITEM_COUNT, count); cursor.close(); db.close(); return bundle; } private static class DBHelper extends SQLiteOpenHelper { public DBHelper(Context context, String name, CursorFactory factory, int version) { super(context, name, factory, version); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(DB_CREATE); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE); onCreate(db); } } }
我們在實現自己的Content Provider時,必須繼承于ContentProvider類,并且實現以下六個函數:
-- onCreate(),用來執行一些初始化的工作。
-- query(Uri, String[], String, String[], String),用來返回數據給調用者。
-- insert(Uri, ContentValues),用來插入新的數據。
-- update(Uri, ContentValues, String, String[]),用來更新已有的數據。
-- delete(Uri, String, String[]),用來刪除數據。
-- getType(Uri),用來返回數據的MIME類型。
這些函數的實現都比較簡單,這里我們就不詳細介紹了,主要解釋五個要點。
第一點是我們在ArticlesProvider類的內部中定義了一個DBHelper類,它繼承于SQLiteOpenHelper類,它用是用輔助我們操作數據庫的。使用這個DBHelper類來輔助操作數據庫的好處是只有當我們第一次對數據庫時行操作時,系統才會執行打開數據庫文件的操作。拿我們這個例子來說,只有第三方應用程序第一次調用query、insert、update或者delete函數來操作數據庫時,我們才會真正去打開相應的數據庫文件。這樣在onCreate函數里,就不用執行打開數據庫的操作,因為這是一個耗時的操作,而在onCreate函數中,要避免執行這些耗時的操作。
第二點是設置URI匹配規則。因為我們是根據URI來操作數據庫的,不同的URI對應不同的操作,所以我們首先要定義好URI匹配規則,這樣,當我們獲得一個URI時,就能快速地判斷出要如何去操作數據庫。設置URI匹配規則的代碼如下所示:
private static final UriMatcher uriMatcher; static { uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); uriMatcher.addURI(Articles.AUTHORITY, "item", Articles.ITEM); uriMatcher.addURI(Articles.AUTHORITY, "item/#", Articles.ITEM_ID); uriMatcher.addURI(Articles.AUTHORITY, "pos/#", Articles.ITEM_POS); }
在創建UriMatcher對象uriMatcher時,我們傳給構造函數的參數為UriMatcher.NO_MATCH,它表示當uriMatcher不能匹配指定的URI時,就返回代碼UriMatcher.NO_MATCH。接下來增加了三個匹配規則,分別是content://shy.luo.providers.articles/item、content://shy.luo.providers.articles/item/#和content://shy.luo.providers.articles/pos/#,它們的匹配碼分別是Articles.ITEM、Articles.ITEM_ID和Articles.ITEM_POS,其中,符號#表示匹配任何數字。
第三點是SQLiteQueryBuilder的使用。在query函數中,我們使用SQLiteQueryBuilder來輔助數據庫查詢操作,使用這個類的好處是我們可以不把數據庫表的字段暴露出來,而是提供別名給第三方應用程序使用,這樣就可以把數據庫表內部設計隱藏起來,方便后續擴展和維護。列別名到真實列名的映射是由下面這個HashMap成員變量來實現的:
private static final HashMap<String, String> articleProjectionMap; static { articleProjectionMap = new HashMap<String, String>(); articleProjectionMap.put(Articles.ID, Articles.ID); articleProjectionMap.put(Articles.TITLE, Articles.TITLE); articleProjectionMap.put(Articles.ABSTRACT, Articles.ABSTRACT); articleProjectionMap.put(Articles.URL, Articles.URL); }在上面的put函數中,第一個參數表示列的別名,第二個參數表示列的真實名稱。在這個例子中,我們把列的別名和和真實名稱都設置成一樣的。
第四點是數據更新機制的使用。執行insert、update和delete三個函數時,都會導致數據庫中的數據發生變化,所以這時候要通過調用ContentResolver接口的notifyChange函數來通知那些注冊了監控特定URI的ContentObserver對象,使得它們可以相應地執行一些處理,例如更新數據在界面上的顯示。在query函數中,最終返回給調用者的是一個Cursor,調用者獲得這個Cursor以后,可以通過它的deleteRow或者commitUpdates來執行一些更新數據庫的操作,這時候也要通知那些注冊了相應的URI的ContentObserver來作相應的處理,因此,這里在返回Cursor之前,要通過Cursor類的setNotificationUri函數來把當前上下文的ContentResolver對象保存到Curosr里面去,以便當通過這個Cursor來改變數據庫中的數據時,可以通知相應的ContentObserver來處理。不過這種用法已經過時了,即不建議通過這個Cursor來改變數據庫的數據,要把Cursor中的數據看作是只讀數據。這里調用Cursor類的setNotificationUri函數還有另外一個作用,我們注意到它的第二個參數uri,對應的是Cursor中的內容,當把這個uri傳給Cursor時,Cursor就會注冊自己的ContentObserver來監控這個uri對應的數據的變化。一旦這個uri對應的數據發生變化,這個Cursor對應的數據就不是再新的了,這時候就需要采取一些操作來更新內容了。
第五點我們實現了ContentProvider的call函數。這個函數是一個未公開的函數,第三方應用程序只有Android源代碼環境下開發,才能使用這個函數。設計這個函數的目的是什么呢?我們知道,當我們需要從Content Provider中獲得數據時,一般都是要通過調用它的query函數來獲得的,而這個函數將數據放在Cursor中來返回給調用者。以前面一篇文章Android應用程序組件Content Provider簡要介紹和學習計劃中,我們提到,Content Provider傳給第三方應用程序的數據,是通過匿名共享內存來傳輸的。當要傳輸的數據量大的時候,使用匿名共享內存來傳輸數據是有好處的,它可以減入數據的拷貝,提高傳輸效率。但是,當要傳輸的數據量小時,使用匿名共享內存來作為媒介就有點用牛刀來殺雞的味道了,因為匿名共享內存并不是免費的午餐,系統創建和匿名共享內存也是有開銷的。因此,Content Provider提供了call函數來讓第三方應用程序來獲取一些自定義數據,這些數據一般都比較小,例如,只是傳輸一個整數,這樣就可以用較小的代價來達到相同的數據傳輸的目的。
至此,ArticlesProvider的源代碼就分析完了,下面我們還要在AndroidManifest.xml文件中配置這個ArticlesProvider類才能正常使用。AndroidManifest.xml文件的內容如下所示:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="shy.luo.providers.articles"> <application android:process="shy.luo.process.article" android:label="@string/app_label" android:icon="@drawable/app_icon"> <provider android:name="ArticlesProvider" android:authorities="shy.luo.providers.articles" android:label="@string/provider_label" android:multiprocess="false"> </provider> </application> </manifest>在配置Content Provider的時候,最重要的就是要指定它的authorities屬性了,只有配置了這個屬性,第三方應用程序才能通過它來找到這個Content Provider。這要需要注意的,這里配置的authorities屬性的值是和我們前面在Articles.java文件中定義的AUTHORITY常量的值是一致的。另外一個屬性multiprocess是一個布爾值,它表示這個Content Provider是否可以在每個客戶進程中創建一個實例,這樣做的目的是為了減少進程間通信的開銷。這里我們為了減少不必要的內存開銷,把屬性multiprocess的值設置為false,使得系統只能有一個Content Provider實例存在,它運行在自己的進程中。在這個配置文件里面,我們還可以設置這個Content Provider的訪問權限,這里我們為了簡單起見,就不設置權限了。有關Content Provider的訪問權限的設置,可以參考官方文檔http://developer.android.com/guide/topics/manifest/provider-element.html。
這個應用程序使用到的字符串資源定義在res/values/strings.xml文件中,它的內容如下所示:
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_label">Articles Storage</string> <string name="provider_label">Articles</string> </resources>由于Content Provider類型的應用程序是沒有用戶界面的,因此,我們不需要在res/layout目錄下為程序準備界面配置文件。
程序的編譯腳本Android.mk的內容如下所示:
LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_SRC_FILES := $(call all-subdir-java-files) LOCAL_PACKAGE_NAME := ArticlesProvider include $(BUILD_PACKAGE)下面我們就可以參照如何單獨編譯Android源代碼中的模塊一文來編譯和打包這個應用程序了:
USER-NAME@MACHINE-NAME:~/Android$ mmm packages/experimental/ArticlesProvider USER-NAME@MACHINE-NAME:~/Android$ make snod
這樣,打包好的Android系統鏡像文件system.img就包含我們這里所創建的ArticlesProvider應用程序了。
前面說過,在Articles.java文件中定義的常量是要給第三方應用程序使用的,那么我們是不是直接把這個源文件交給第三方呢?這樣就顯得太不專業了,第三方拿到這個文件后,還必須要放在shy/luo/providers/articles目錄下或者要把這個Articles類所在的package改掉才能正常使用。正確的做法是把編譯好的Articles.java文件打包成一個jar文件交給第三方使用。編譯ArticlesProvider這個應用程序成功后,生成的中間文件放在out/target/common/obj/APPS/ArticlesProvider_intermediates目錄下,我們進入到這個目錄中,然后執后下面的命令把Articles.class文件提取出來:
USER-NAME@MACHINE-NAME:~/Android/out/target/common/obj/APPS/ArticlesProvider_intermediates$ jar -xvf classes.jar shy/luo/providers/articles/Articles.class
然后再單獨打包這個Articles.class文件:
USER-NAME@MACHINE-NAME:~/Android/out/target/common/obj/APPS/ArticlesProvider_intermediates$ jar -cvf ArticlesProvider.jar ./shy這樣,我們得到的ArticlesProvider.jar文件就包含了Articles.java這個文件中定義的常量了,第三方拿到這個文件后,就可以開發自己的應用程序來訪問我們在ArticlesProvider這個Content Provider中保存的數據了。接下來我們就介紹調用這個ArticlesProvider來獲取數據的第三方應用程序Article。
2. Article應用程序的實現
首先是參照前面的ArticlesProvider工程,在packages/experimental目錄下建立工程文件目錄Article。這個應用程序的作用是用來管理ArticlesProvider應用程序中保存的文章信息的,因此,它需要獲得相應的Content Provider接口來訪問ArticlesProvider中的數據。我們首先在工程目錄Article下面創建一個libs目錄,把上面得到的ArticlesProvider.jar放在libs目錄下,后面我們在編譯腳本的時候,再把它引用到工程上來。下面我們就開始分析這個應用程序的實現。
這個應用程序的主界面MainActivity包含了一個ListView控件,用來顯示從ArticlesProvider中得到的文章信息條目,在這個主界面上,可以瀏覽、增加、刪除和更新文章信息。當需要增加、刪除或者更新文章信息時,就會跳到另外一個界面ArticleActivity中去執行具體的操作。為了方便開發,我們把每一個文章信息條目封裝成了一個Article類,并且把與ArticlesProvider進交互的操作都通過ArticlesAdapter類來實現。下面介紹每一個類的具本實現。
下面是Article類的實現,它實現在src/shy/luo/Article.java文件中:
package shy.luo.article; public class Article { private int id; private String title; private String abs; private String url; public Article(int id, String title, String abs, String url) { this.id = id; this.title = title; this.abs = abs; this.url = url; } public void setId(int id) { this.id = id; } public int getId() { return this.id; } public void setTitle(String title) { this.title = title; } public String getTitle() { return this.title; } public void setAbstract(String abs) { this.abs = abs; } public String getAbstract() { return this.abs; } public void setUrl(String url) { this.url = url; } public String getUrl() { return this.url; } }下面是ArticlesAdapter類的實現,它實現在src/shy/luo/ArticlesAdapter.java文件中:
package shy.luo.article; import java.util.LinkedList; import shy.luo.providers.articles.Articles; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.IContentProvider; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.RemoteException; import android.util.Log; public class ArticlesAdapter { private static final String LOG_TAG = "shy.luo.article.ArticlesAdapter"; private ContentResolver resolver = null; public ArticlesAdapter(Context context) { resolver = context.getContentResolver(); } public long insertArticle(Article article) { ContentValues values = new ContentValues(); values.put(Articles.TITLE, article.getTitle()); values.put(Articles.ABSTRACT, article.getAbstract()); values.put(Articles.URL, article.getUrl()); Uri uri = resolver.insert(Articles.CONTENT_URI, values); String itemId = uri.getPathSegments().get(1); return Integer.valueOf(itemId).longValue(); } public boolean updateArticle(Article article) { Uri uri = ContentUris.withAppendedId(Articles.CONTENT_URI, article.getId()); ContentValues values = new ContentValues(); values.put(Articles.TITLE, article.getTitle()); values.put(Articles.ABSTRACT, article.getAbstract()); values.put(Articles.URL, article.getUrl()); int count = resolver.update(uri, values, null, null); return count > 0; } public boolean removeArticle(int id) { Uri uri = ContentUris.withAppendedId(Articles.CONTENT_URI, id); int count = resolver.delete(uri, null, null); return count > 0; } public LinkedList<Article> getAllArticles() { LinkedList<Article> articles = new LinkedList<Article>(); String[] projection = new String[] { Articles.ID, Articles.TITLE, Articles.ABSTRACT, Articles.URL }; Cursor cursor = resolver.query(Articles.CONTENT_URI, projection, null, null, Articles.DEFAULT_SORT_ORDER); if (cursor.moveToFirst()) { do { int id = cursor.getInt(0); String title = cursor.getString(1); String abs = cursor.getString(2); String url = cursor.getString(3); Article article = new Article(id, title, abs, url); articles.add(article); } while(cursor.moveToNext()); } return articles; } public int getArticleCount() { int count = 0; try { IContentProvider provider = resolver.acquireProvider(Articles.CONTENT_URI); Bundle bundle = provider.call(Articles.METHOD_GET_ITEM_COUNT, null, null); count = bundle.getInt(Articles.KEY_ITEM_COUNT, 0); } catch(RemoteException e) { e.printStackTrace(); } return count; } public Article getArticleById(int id) { Uri uri = ContentUris.withAppendedId(Articles.CONTENT_URI, id); String[] projection = new String[] { Articles.ID, Articles.TITLE, Articles.ABSTRACT, Articles.URL }; Cursor cursor = resolver.query(uri, projection, null, null, Articles.DEFAULT_SORT_ORDER); Log.i(LOG_TAG, "cursor.moveToFirst"); if (!cursor.moveToFirst()) { return null; } String title = cursor.getString(1); String abs = cursor.getString(2); String url = cursor.getString(3); return new Article(id, title, abs, url); } public Article getArticleByPos(int pos) { Uri uri = ContentUris.withAppendedId(Articles.CONTENT_POS_URI, pos); String[] projection = new String[] { Articles.ID, Articles.TITLE, Articles.ABSTRACT, Articles.URL }; Cursor cursor = resolver.query(uri, projection, null, null, Articles.DEFAULT_SORT_ORDER); if (!cursor.moveToFirst()) { return null; } int id = cursor.getInt(0); String title = cursor.getString(1); String abs = cursor.getString(2); String url = cursor.getString(3); return new Article(id, title, abs, url); } }這個類首先在構造函數里面獲得應用程序上下文的ContentResolver接口,然后通過就可以通過這個接口來訪問ArticlesProvider中的文章信息了。成員函數insertArticle、updateArticle和removeArticle分別用來新增、更新和刪除一個文章信息條目;成員函數getAllArticlese用來獲取所有的文章信息;成員函數getArticleById和getArticleByPos分別根據文章的ID和位置來獲得具體文章信息條目;成員函數getArticleCount直接使用ContentProvider的未公開接口call來獲得文章信息條目的數量,注意,這個函數要源代碼環境下編譯才能通過。
下面是程序主界面MainActivity類的實現,它實現在src/shy/luo/article/MainActivity.java文件中:
package shy.luo.article; import shy.luo.providers.articles.Articles; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.database.ContentObserver; import android.os.Bundle; import android.os.Handler; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.Button; import android.widget.ListView; import android.widget.TextView; public class MainActivity extends Activity implements View.OnClickListener, AdapterView.OnItemClickListener { private final static String LOG_TAG = "shy.luo.article.MainActivity"; private final static int ADD_ARTICAL_ACTIVITY = 1; private final static int EDIT_ARTICAL_ACTIVITY = 2; private ArticlesAdapter aa = null; private ArticleAdapter adapter = null; private ArticleObserver observer = null; private ListView articleList = null; private Button addButton = null; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); aa = new ArticlesAdapter(this); articleList = (ListView)findViewById(R.id.listview_article); adapter = new ArticleAdapter(this); articleList.setAdapter(adapter); articleList.setOnItemClickListener(this); observer = new ArticleObserver(new Handler()); getContentResolver().registerContentObserver(Articles.CONTENT_URI, true, observer); addButton = (Button)findViewById(R.id.button_add); addButton.setOnClickListener(this); Log.i(LOG_TAG, "MainActivity Created"); } @Override public void onDestroy() { super.onDestroy(); getContentResolver().unregisterContentObserver(observer); } @Override public void onClick(View v) { if(v.equals(addButton)) { Intent intent = new Intent(this, ArticleActivity.class); startActivityForResult(intent, ADD_ARTICAL_ACTIVITY); } } @Override public void onItemClick(AdapterView<?> parent, View view, int pos, long id) { Intent intent = new Intent(this, ArticleActivity.class); Article article = aa.getArticleByPos(pos); intent.putExtra(Articles.ID, article.getId()); intent.putExtra(Articles.TITLE, article.getTitle()); intent.putExtra(Articles.ABSTRACT, article.getAbstract()); intent.putExtra(Articles.URL, article.getUrl()); startActivityForResult(intent, EDIT_ARTICAL_ACTIVITY); } @Override public void onActivityResult(int requestCode,int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); switch(requestCode) { case ADD_ARTICAL_ACTIVITY: { if(resultCode == Activity.RESULT_OK) { String title = data.getStringExtra(Articles.TITLE); String abs = data.getStringExtra(Articles.ABSTRACT); String url = data.getStringExtra(Articles.URL); Article article = new Article(-1, title, abs, url); aa.insertArticle(article); } break; } case EDIT_ARTICAL_ACTIVITY: { if(resultCode == Activity.RESULT_OK) { int action = data.getIntExtra(ArticleActivity.EDIT_ARTICLE_ACTION, -1); if(action == ArticleActivity.MODIFY_ARTICLE) { int id = data.getIntExtra(Articles.ID, -1); String title = data.getStringExtra(Articles.TITLE); String abs = data.getStringExtra(Articles.ABSTRACT); String url = data.getStringExtra(Articles.URL); Article article = new Article(id, title, abs, url); aa.updateArticle(article); } else if(action == ArticleActivity.DELETE_ARTICLE) { int id = data.getIntExtra(Articles.ID, -1); aa.removeArticle(id); } } break; } } } private class ArticleObserver extends ContentObserver { public ArticleObserver(Handler handler) { super(handler); } @Override public void onChange (boolean selfChange) { adapter.notifyDataSetChanged(); } } private class ArticleAdapter extends BaseAdapter { private LayoutInflater inflater; public ArticleAdapter(Context context){ inflater = LayoutInflater.from(context); } @Override public int getCount() { return aa.getArticleCount(); } @Override public Object getItem(int pos) { return aa.getArticleByPos(pos); } @Override public long getItemId(int pos) { return aa.getArticleByPos(pos).getId(); } @Override public View getView(int position, View convertView, ViewGroup parent) { Article article = (Article)getItem(position); if (convertView == null) { convertView = inflater.inflate(R.layout.item, null); } TextView titleView = (TextView)convertView.findViewById(R.id.textview_article_title); titleView.setText("Title: " + article.getTitle()); TextView abstractView = (TextView)convertView.findViewById(R.id.textview_article_abstract); abstractView.setText("Abstract: " + article.getAbstract()); TextView urlView = (TextView)convertView.findViewById(R.id.textview_article_url); urlView.setText("URL: " + article.getUrl()); return convertView; } } }
在應用程序的主界面中,我們使用一個ListView來顯示文章信息條目,這個ListView的數據源由ArticleAdapter類來提供,而ArticleAdapter類又是通過ArticlesAdapter類來獲得ArticlesProvider中的文章信息的。在MainActivity的onCreate函數,我們還通過應用程序上下文的ContentResolver接口來注冊了一個ArticleObserver對象來監控ArticlesProvider中的文章信息。一旦ArticlesProvider中的文章信息發生變化,就會通過ArticleAdapter類來實時更新ListView中的文章信息。
下面是ArticleActivity類的實現,它實現在src/shy/luo/article/ArticleActivity.java文件中:
package shy.luo.article; import shy.luo.providers.articles.Articles; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.EditText; public class ArticleActivity extends Activity implements View.OnClickListener { private final static String LOG_TAG = "shy.luo.article.ArticleActivity"; public final static String EDIT_ARTICLE_ACTION = "EDIT_ARTICLE_ACTION"; public final static int MODIFY_ARTICLE = 1; public final static int DELETE_ARTICLE = 2; private int articleId = -1; private EditText titleEdit = null; private EditText abstractEdit = null; private EditText urlEdit = null; private Button addButton = null; private Button modifyButton = null; private Button deleteButton = null; private Button cancelButton = null; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.article); titleEdit = (EditText)findViewById(R.id.edit_article_title); abstractEdit = (EditText)findViewById(R.id.edit_article_abstract); urlEdit = (EditText)findViewById(R.id.edit_article_url); addButton = (Button)findViewById(R.id.button_add_article); addButton.setOnClickListener(this); modifyButton = (Button)findViewById(R.id.button_modify); modifyButton.setOnClickListener(this); deleteButton = (Button)findViewById(R.id.button_delete); deleteButton.setOnClickListener(this); cancelButton = (Button)findViewById(R.id.button_cancel); cancelButton.setOnClickListener(this); Intent intent = getIntent(); articleId = intent.getIntExtra(Articles.ID, -1); if(articleId != -1) { String title = intent.getStringExtra(Articles.TITLE); titleEdit.setText(title); String abs = intent.getStringExtra(Articles.ABSTRACT); abstractEdit.setText(abs); String url = intent.getStringExtra(Articles.URL); urlEdit.setText(url); addButton.setVisibility(View.GONE); } else { modifyButton.setVisibility(View.GONE); deleteButton.setVisibility(View.GONE); } Log.i(LOG_TAG, "ArticleActivity Created"); } @Override public void onClick(View v) { if(v.equals(addButton)) { String title = titleEdit.getText().toString(); String abs = abstractEdit.getText().toString(); String url = urlEdit.getText().toString(); Intent result = new Intent(); result.putExtra(Articles.TITLE, title); result.putExtra(Articles.ABSTRACT, abs); result.putExtra(Articles.URL, url); setResult(Activity.RESULT_OK, result); finish(); } else if(v.equals(modifyButton)){ String title = titleEdit.getText().toString(); String abs = abstractEdit.getText().toString(); String url = urlEdit.getText().toString(); Intent result = new Intent(); result.putExtra(Articles.ID, articleId); result.putExtra(Articles.TITLE, title); result.putExtra(Articles.ABSTRACT, abs); result.putExtra(Articles.URL, url); result.putExtra(EDIT_ARTICLE_ACTION, MODIFY_ARTICLE); setResult(Activity.RESULT_OK, result); finish(); } else if(v.equals(deleteButton)) { Intent result = new Intent(); result.putExtra(Articles.ID, articleId); result.putExtra(EDIT_ARTICLE_ACTION, DELETE_ARTICLE); setResult(Activity.RESULT_OK, result); finish(); } else if(v.equals(cancelButton)) { setResult(Activity.RESULT_CANCELED, null); finish(); } } }在ArticleActivity窗口中,我們可以執行新增、更新和刪除文章信息的操作。如果啟動ArticleActivity時,沒有把文章ID傳進來,就說明要執行操作是新增文章信息;如果啟動ArticleActivity時,把文章ID和其它信自都傳進來了,就說明要執行的操作是更新或者刪除文章,根據用戶在界面點擊的是更新按鈕還是刪除按鈕來確定。
程序使用到的界面文件定義在res/layout目錄下,其中,main.xml文件定義MainActivity的界面,它的內容如下所示:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:gravity="bottom"> <ListView android:id="@+id/listview_article" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_weight="1" android:background="@drawable/border" android:choiceMode="singleChoice"> </ListView> <LinearLayout android:orientation="horizontal" android:layout_height="wrap_content" android:layout_width="match_parent" android:gravity="center" android:layout_marginTop="10dp"> <Button android:id="@+id/button_add" android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingLeft="15dp" android:paddingRight="15dp" android:text="@string/add"> </Button> </LinearLayout> </LinearLayout>item.xml文件定義了ListView中每一個文章信息條目的顯示界面,它的內容如下所示:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="wrap_content"> <TextView android:id="@+id/textview_article_title" android:layout_width="fill_parent" android:layout_height="wrap_content"> </TextView> <TextView android:id="@+id/textview_article_abstract" android:layout_width="fill_parent" android:layout_height="wrap_content"> </TextView> <TextView android:id="@+id/textview_article_url" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginBottom="10dp"> </TextView> </LinearLayout>article.xml文件定義了ArticleActivity的界面,它的內容如下所示:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:gravity="center"> <LinearLayout android:orientation="horizontal" android:layout_height="wrap_content" android:layout_width="fill_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginRight="24dp" android:text="@string/title"> </TextView> <EditText android:id="@+id/edit_article_title" android:layout_width="fill_parent" android:layout_height="wrap_content"> </EditText> </LinearLayout> <LinearLayout android:orientation="horizontal" android:layout_height="wrap_content" android:layout_width="fill_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/abs"> </TextView> <EditText android:id="@+id/edit_article_abstract" android:layout_width="fill_parent" android:layout_height="wrap_content" > </EditText> </LinearLayout> <LinearLayout android:orientation="horizontal" android:layout_height="wrap_content" android:layout_width="fill_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginRight="27dp" android:text="@string/url"> </TextView> <EditText android:id="@+id/edit_article_url" android:layout_width="fill_parent" android:layout_height="wrap_content" > </EditText> </LinearLayout> <LinearLayout android:orientation="horizontal" android:layout_height="wrap_content" android:layout_width="match_parent" android:gravity="center" android:layout_marginTop="10dp"> <Button android:id="@+id/button_modify" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/modify"> </Button> <Button android:id="@+id/button_delete" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/delete"> </Button> <Button android:id="@+id/button_add_article" android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingLeft="16dp" android:paddingRight="16dp" android:text="@string/add"> </Button> <Button android:id="@+id/button_cancel" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/cancel"> </Button> </LinearLayout> </LinearLayout>在res/drawable目錄下,有一個border.xml文件定義了MainActivity界面上的ListView的背景,它的內容如下所示:
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <solid android:color="#ff0000ff"/> <stroke android:width="1dp" android:color="#000000"> </stroke> <padding android:left="7dp" android:top="7dp" android:right="7dp" android:bottom="7dp"> </padding> <corners android:radius="10dp" /> </shape>程序使用到的字符串資源文件定義在res/values/strings.xml文件中,它的內容如下所示:
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">Article</string> <string name="article">Article</string> <string name="add">Add</string> <string name="modify">Modify</string> <string name="delete">Delete</string> <string name="title">Title:</string> <string name="abs">Abstract:</string> <string name="url">URL:</string> <string name="ok">OK</string> <string name="cancel">Cancel</string> </resources>接下來再來看程序的配置文件AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="shy.luo.article" android:versionCode="1" android:versionName="1.0"> <application android:icon="@drawable/icon" android:label="@string/app_name"> <activity android:name=".MainActivity" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".ArticleActivity" android:label="@string/article"> </activity> </application> </manifest>編譯腳本Android.mk的內容如下所示:
LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_STATIC_JAVA_LIBRARIES := libArticlesProvider LOCAL_SRC_FILES := $(call all-subdir-java-files) LOCAL_PACKAGE_NAME := Article include $(BUILD_PACKAGE) ################################################### include $(CLEAR_VARS) LOCAL_PREBUILT_STATIC_JAVA_LIBRARIES := libArticlesProvider:./libs/ArticlesProvider.jar include $(BUILD_MULTI_PREBUILT)這個編譯腳本包含了兩個部分的指令,一個是把libs目錄下的預編譯靜態庫ArticlesProvider.jar編譯成一本地靜態庫libArticlesProvider,它的相關庫文件保存在out/target/common/obj/JAVA_LIBRARIES/libArticlesProvider_intermediates目錄下;另一個就是編譯我們的程序Article了,它通過LOCAL_STATIC_JAVA_LIBRARIES變量來引用前面的libArticlesProvider庫,這個庫包含了所有我們用來訪問ArticlesProvider這個Content Provider中的數據的常量。
下面我們就可以編譯和打包這個應用程序了:
USER-NAME@MACHINE-NAME:~/Android$ mmm packages/experimental/Article USER-NAME@MACHINE-NAME:~/Android$ make snod
這樣,打包好的Android系統鏡像文件system.img就包含我們這里所創建的Article應用程序了。
最后,就是運行模擬器來運行我們的例子了。關于如何在Android源代碼工程中運行模擬器,請參考在Ubuntu上下載、編譯和安裝Android最新源代碼一文。執行以下命令啟動模擬器:
USER-NAME@MACHINE-NAME:~/Android$ emulator這個應用程序的主界面如下圖所示:
點擊下面的Add按鈕,可以添加新的文章信息條目:
在前一個界面的文件列表中,點擊某一個文章條目,便可以更新或者刪除文章信息條目:
這樣,Content Provider的使用實例就介紹完了。這篇文章的目的是使讀者對Content Provider有一個大概的了解和感性的認識,在下一篇文章中,我們將詳細介紹Article應用程序是如何獲得ArticlesProvider這個ContentProvider接口的,只有獲得了這個接口之后,Article應用程序才能訪問ArticlesProvider的數據,敬請關注。