源文:http://cw1057.blogspot.tw/2011/08/android-content-provider.html
Android content provider 可以看做是一種以 REST 方式存取的 data wrapper,其主要目的在於Android applications 間的資料分享,也就是說如果資料不需要與其他 app 共用的話,就不必提供 content provider。
Android 提供四種資料存取機制:
Preferences - 以 key/value 的形式儲存 app 的設定值。 Files - app 專屬的檔案。 SQLite - 專屬於建立該資料庫的 package。 Network - 透過網路存取外部資料。 以上四種資料存取機制都是專屬於本身的 app,其他 app 是無法共用的,如果要與其他 app 共用,則可以為該資料建立 content provider。
REST(REpresentational State Transfer)簡述
REST 舉例說明,若要取得所有的蘋果:
content://idv.neil.AppleProvider/apples
若要取得某一顆蘋果:
content://idv.neil.AppleProvider/apples/16
另外還有增修刪的 uri。
Android 內建的 content providers
Android 將通訊錄(Contacts)或系統設定(Settings)等資訊以 SQLite 資料庫的方式儲存,因為只有建立該資料庫的 app 可以透過 SQLite 存取該資料庫,也就是只有同一個 package 的 code 可以使用,所以其他 app 只能透過其開放出來的 content provider 存取。
透視 SQLite
開啟 Android plugin 的 File Explorer,瀏覽至 /data/data/com.android.providers.contacts/databases,可以看到 contacts.db,選取該檔案,按下 File Explorer 右上方的「Pull a file from the device」,就可以將通訊錄檔案抓到電腦裡,再去下載免費的 SQLite GUI,如 Sqliteman,就可以看光光了。
Authority
content provider uri 的格式為 content://authority/path1/path2/...,authority 定義在 AndroidManifest.xml 裡,只有 Android 可以使用縮寫的 authority,如 contacts,其他 app 得用全名。
<provider android:name="apple" android:authorities="idv.neil.AppleProvider"/>
MIME type
透過 content provider 取得的資料形式為類似資料庫由 row 與 column 組成的 cursor 物件。
Android 提供類似 HTTP 的方式讓 content provider 藉由 MIME type 知道回傳物件的型別。
MIME type 由兩部份字串組成,如 text/html,text為主型別,html 為子型別,目前常見的主型別有 application, audio, image, multipart, text, video,子型別若為 vnd. 開頭表示專用的子型別,如 vnd.me-excel,子型別若為 x- 開頭表示為接收雙方私底下的協定。
Anddroid 沿用 MIME type 的概念,進一步套用到因回傳物件數量不同的狀況,若回傳單筆物件,MIME type 格式為 vnd.android.cursor.item/vnd.neil.apple,若回傳多筆物件,則為 vnd.android.cursor.dir/vnd.neil.apple。
Android 運用 MIME type 的概念到 intent 上,也就是 intent 包含的 MIME type 會決定啟動該 intent 的 activity,所以 MIME type 必須加上 domain name 來做 unique。
WordCloud 範例
開發 ContentProvider 的步驟:
定義 ContentProvider 所有資訊 實做 ContentProvider 註冊 ContentProvider WordCloudProvider 只有一個 words table,words 除了 _id 以外只有兩個欄位,word 和 size,目的就是做的像部落格常見的標籤雲,使用頻率愈高的標籤字型愈大。
輸入 Word 與 Size 按下 Add 就可以新增一筆 words 並重整頁面,單點頁面任一筆 words 就會帶出原資料供修改或刪除。
點下 Update 或 Delete 後就可以回到原畫面,點下 Empty 則會將 table 裡所有資料清空。
定義 ContentProvider 所有資訊
public class WordCloudMetaData { /** content provider 的 unique key,會定義在 manifest 裡,也是對外開放的 uri */ public static final String AUTHORITY = "idv.neil.provider.WordCloudProvider"; /** 用 db 實做 content provider */ public static final String DB_NAME = "wordcloud.db"; /** 小心。只要版本一改,舊資料會被清空 */ public static final int DB_VERSION = 1; private WordCloudMetaData() { }; /** 定義 words 表格的所有資訊,一個 content provider 可以有多個 table */ public static final class WordsTableMetaData implements BaseColumns { private WordsTableMetaData() { }; public static final String TABLE_NAME = "words"; /** words 專用的 uri */ public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/words"); /** 複數與單數的 MIME 型別*/ public static final String CONTENT_DIR_TYPE = "vnd.android.cursor.dir/vnd.neil.words"; public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.neil.words"; /** 預設的排序 */ public static final String DEFAULT_SORT_ORDER = BaseColumns._ID + " asc"; /** 定義所有欄位 */ // 不用定義 _id,因為 BaseColumns 已經定義了 public static final String WORDS_WORD = "word"; public static final String WORDS_SIZE = "size"; }} 實做 ContentProvider
public class WordCloudProvider extends ContentProvider {private static final String TAG = "WordCloudProvider";// 因為有時候 db 查詢因為使用 join 導致欄位名稱衝突或因為使用 table alias 讓欄位名稱變複雜// 所以定義一份對應關係,將 user 使用的欄位名稱對應到db使用的欄位名稱// 所有 user 會用到的欄位名稱都要定義,或者說 user 只能使用這個 map 裡定義的欄位名稱private static Map<String, String> projectionMap;static {projectionMap = new HashMap<String, String>();projectionMap.put(WordsTableMetaData._ID, WordsTableMetaData._ID);projectionMap.put(WordsTableMetaData.WORDS_WORD,WordsTableMetaData.WORDS_WORD);projectionMap.put(WordsTableMetaData.WORDS_SIZE,WordsTableMetaData.WORDS_SIZE);}// 因為一個 ContentProvider 可以處理多種 uri,例如取得一堆資料或者只取得一筆資料// Android 提供 UriMatcher 來簡化 uri 的識別工作private static final UriMatcher uriMatcher;private static final int WORDS_DIR_URI_INDICATOR = 1;private static final int WORDS_ITEM_URI_INDICATOR = 2;static {uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);// 當 uri 符合前兩個參數組成的語法時,回傳第三個參數uriMatcher.addURI(WordCloudMetaData.AUTHORITY, "words",WORDS_DIR_URI_INDICATOR);uriMatcher.addURI(WordCloudMetaData.AUTHORITY, "words/#",WORDS_ITEM_URI_INDICATOR);}// db 連線工具private static class DBHelper extends SQLiteOpenHelper {public DBHelper(Context context) {super(context, WordCloudMetaData.DB_NAME, null,WordCloudMetaData.DB_VERSION);}@Overridepublic void onCreate(SQLiteDatabase db) {Log.d(TAG, "create db...");// 建立 table 的語法db.execSQL("create table " + WordsTableMetaData.TABLE_NAME + " ("+ WordsTableMetaData._ID + " integer primary key,"+ WordsTableMetaData.WORDS_WORD + " text not null, "+ WordsTableMetaData.WORDS_SIZE + " integer not null"+ "); ");}@Overridepublic void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {Log.d(TAG, "modify db from version " + oldVersion + " to version "+ newVersion + " ...");// 當 version 改變時,一般會 drop 掉舊 table,然後重建立立 table,所以要小心舊資料會被清掉db.execSQL("drop table if exists " + WordsTableMetaData.TABLE_NAME);this.onCreate(db);}}private DBHelper dbHelper;@Overridepublic boolean onCreate() {Log.d(TAG, "onCreate");// 建立或重建 dbthis.dbHelper = new DBHelper(this.getContext());return true;}@Overridepublic Cursor query(Uri uri, String[] projection, String selection,String[] selectionArgs, String sortOrder) {SQLiteQueryBuilder b = new SQLiteQueryBuilder();// 這兩個一定要b.setTables(WordsTableMetaData.TABLE_NAME);b.setProjectionMap(WordCloudProvider.projectionMap);// 因 uri 不同而導致查詢語法的不同switch (uriMatcher.match(uri)) {case WORDS_DIR_URI_INDICATOR:break;case WORDS_ITEM_URI_INDICATOR:b.appendWhere(WordsTableMetaData._ID + " = "+ uri.getPathSegments().get(1));break;default:throw new IllegalArgumentException("Unknown Uri - " + uri);}// 資料排序方式String orderBy = WordsTableMetaData.DEFAULT_SORT_ORDER;if (!TextUtils.isEmpty(sortOrder)) {orderBy = sortOrder;}// 取得 cursorSQLiteDatabase db = this.dbHelper.getReadableDatabase();Cursor c = b.query(db, projection, selection, selectionArgs, null,null, orderBy);// 因為是取得資料,所以當資料有改變時,需要被通知c.setNotificationUri(this.getContext().getContentResolver(), uri);return c;}@Overridepublic String getType(Uri uri) {switch (uriMatcher.match(uri)) {case WORDS_DIR_URI_INDICATOR:return WordsTableMetaData.CONTENT_DIR_TYPE;case WORDS_ITEM_URI_INDICATOR:return WordsTableMetaData.CONTENT_ITEM_TYPE;default:throw new IllegalArgumentException("Unknown Uri - " + uri);}}@Overridepublic Uri insert(Uri uri, ContentValues values) {// 只能是複數的 uriif (uriMatcher.match(uri) != WORDS_DIR_URI_INDICATOR) {throw new IllegalArgumentException("Unknown Uri - " + uri);}ContentValues cv;if (values == null) {cv = new ContentValues();}else {// 包一層的原因是待會可能會修改cv = new ContentValues(values);}// 檢查必要的欄位if (!cv.containsKey(WordsTableMetaData.WORDS_WORD)) {throw new IllegalArgumentException("The word is required!");}if (!cv.containsKey(WordsTableMetaData.WORDS_SIZE)) {throw new IllegalArgumentException("The size is required!");}// 在這邊也可以修正或補充一些可以有預設值的資料SQLiteDatabase db = this.dbHelper.getWritableDatabase();long rowId = db.insert(WordsTableMetaData.TABLE_NAME, null, cv);if (rowId > 0) {Log.d(TAG, "Insert " + rowId + " data");Uri addedUri = ContentUris.withAppendedId(WordsTableMetaData.CONTENT_URI, rowId);// 資料改變了,發出通知this.getContext().getContentResolver().notifyChange(addedUri, null);return addedUri;}throw new IllegalArgumentException("Failed to insert data to uri - "+ uri);}@Overridepublic int update(Uri uri, ContentValues values, String selection,String[] selectionArgs) {SQLiteDatabase db = this.dbHelper.getWritableDatabase();int cnt;switch (uriMatcher.match(uri)) {case WORDS_DIR_URI_INDICATOR:// 更新多筆資料cnt = db.update(WordsTableMetaData.TABLE_NAME, values, selection,selectionArgs);break;case WORDS_ITEM_URI_INDICATOR:// 更新單筆資料String rowId = uri.getPathSegments().get(1);cnt = db.update(WordsTableMetaData.TABLE_NAME,values,WordsTableMetaData._ID+ " = "+ rowId+ (TextUtils.isEmpty(selection) ? "" : " and ("+ selection + ") "), selectionArgs);break;default:throw new IllegalArgumentException("Unknown Uri - " + uri);}// 資料改變了,發出通知this.getContext().getContentResolver().notifyChange(uri, null);return cnt;}@Overridepublic int delete(Uri uri, String selection, String[] selectionArgs) {SQLiteDatabase db = this.dbHelper.getWritableDatabase();int cnt;switch (uriMatcher.match(uri)) {case WORDS_DIR_URI_INDICATOR:Log.d(TAG, "Delete all data...");// 如果 selection 是空的,可是會將整個 table 清空,這邊就是用來作為清空 table 的功能cnt = db.delete(WordsTableMetaData.TABLE_NAME, selection,selectionArgs);break;case WORDS_ITEM_URI_INDICATOR:// 刪除單筆資料String rowId = uri.getPathSegments().get(1);Log.d(TAG, "Delete data [" + rowId + "]...");cnt = db.delete(WordsTableMetaData.TABLE_NAME,WordsTableMetaData._ID+ " = "+ rowId+ (TextUtils.isEmpty(selection) ? "" : " and ("+ selection + ") "), selectionArgs);break;default:throw new IllegalArgumentException("Unknown Uri - " + uri);}// 資料改變了,發出通知this.getContext().getContentResolver().notifyChange(uri, null);return cnt;}} 註冊 ContentProvider
<application android:icon="@drawable/icon" android:label="@string/app_name"> <activity android:name=".WordCloudActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <provider android:name=".WordCloudProvider" android:authorities="idv.neil.provider.WordCloudProvider" /></application>
怎麼用 ContentProvider。
public class WordCloudActivity extends Activity { private static final String TAG = "WordCloudActivity"; @Override public void onCreate(Bundle savedInstanceState) { Log.d(TAG, "onCreate"); super.onCreate(savedInstanceState); setContentView(R.layout.wordcloud); this.loadWords(); } public void onclick(View v) { Log.d(TAG, "onclick..."); TextView idTV = (TextView) this.findViewById(R.id.idValue); EditText wordET = (EditText) this.findViewById(R.id.wordValue); EditText sizeET = (EditText) this.findViewById(R.id.sizeValue); Words w; switch (v.getId()) { case R.id.addBtn: Log.d(TAG, "add word..."); w = new Words(); w.setWord(wordET.getText().toString()); w.setSize(Integer.parseInt(sizeET.getText().toString())); WordsHelper.addWords(this.getContentResolver(), w); this.resetForm(); this.showBtns(true); break; case R.id.updateBtn: Log.d(TAG, "update word..."); w = new Words(); w.setId(Integer.parseInt(idTV.getText().toString())); w.setWord(wordET.getText().toString()); w.setSize(Integer.parseInt(sizeET.getText().toString())); WordsHelper.updateWords(this.getContentResolver(), w); this.resetForm(); this.showBtns(true); break; case R.id.deleteBtn: Log.d(TAG, "delete word..."); WordsHelper.deleteWords(this.getContentResolver(), Integer.parseInt(idTV.getText().toString())); this.resetForm(); this.showBtns(true); break; case R.id.emptyBtn: Log.d(TAG, "delete all words..."); WordsHelper.deleteAllWords(this.getContentResolver()); break; } // 重整頁面 this.loadWords(); } private void loadWords() { Log.d(TAG, "loadWords"); List<Words> wlist = WordsHelper.listWords(this); LinearLayout tvs = (LinearLayout) this.findViewById(R.id.tvs); // 清空 tvs tvs.removeAllViews(); for (Words w : wlist) { // 將 TextView 加入頁面裡 tvs.addView(this.createWordTextView(w), new LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); } } private TextView createWordTextView(Words w) { Log.d(TAG, "createWordTextView"); TextView tv = new TextView(this); tv.setText(w.getWord()); tv.setTextSize(w.getSize()); tv.setId(w.getId()); tv.setClickable(true); tv.setOnClickListener(new OnC