Android 提供四種資料存取機制:
- Preferences - 以 key/value 的形式儲存 app 的設定值。
- Files - app 專屬的檔案。
- SQLite - 專屬於建立該資料庫的 package。
- Network - 透過網路存取外部資料。
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
輸入 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); } @Override public 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" + "); "); } @Override public 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; @Override public boolean onCreate() { Log.d(TAG, "onCreate"); // 新建或重建 db this.dbHelper = new DBHelper(this.getContext()); return true; } @Override public 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; } // 取得 cursor SQLiteDatabase db = this.dbHelper.getReadableDatabase(); Cursor c = b.query(db, projection, selection, selectionArgs, null, null, orderBy); // 因為是取得資料,所以當資料有改變時,需要被通知 c.setNotificationUri(this.getContext().getContentResolver(), uri); return c; } @Override public 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); } } @Override public Uri insert(Uri uri, ContentValues values) { // 只能是複數的 uri if (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); } @Override public 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; } @Override public 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 OnClickListener() { @Override public void onClick(View v) { int id = v.getId(); // 被點選時,取得 id 去 db 撈出對應物件 Words w = WordsHelper.getWords(getContentResolver(), id); // 將值寫入 form 裡 TextView idTV = (TextView) findViewById(R.id.idValue); EditText wordET = (EditText) findViewById(R.id.wordValue); EditText sizeET = (EditText) findViewById(R.id.sizeValue); idTV.setText(String.valueOf(w.getId())); wordET.setText(w.getWord()); sizeET.setText(String.valueOf(w.getSize())); // 切換按鈕 showBtns(false); } }); return tv; } private void showBtns(boolean showAdd) { Log.d(TAG, "showBtns"); if (showAdd) { this.findViewById(R.id.addBtn).setVisibility(View.VISIBLE); this.findViewById(R.id.updateBtn).setVisibility(View.GONE); this.findViewById(R.id.deleteBtn).setVisibility(View.GONE); this.findViewById(R.id.emptyBtn).setVisibility(View.VISIBLE); } else { this.findViewById(R.id.addBtn).setVisibility(View.GONE); this.findViewById(R.id.updateBtn).setVisibility(View.VISIBLE); this.findViewById(R.id.deleteBtn).setVisibility(View.VISIBLE); this.findViewById(R.id.emptyBtn).setVisibility(View.GONE); } } private void resetForm() { Log.d(TAG, "resetForm"); TextView idTV = (TextView) this.findViewById(R.id.idValue); EditText wordET = (EditText) this.findViewById(R.id.wordValue); EditText sizeET = (EditText) this.findViewById(R.id.sizeValue); idTV.setText(""); wordET.setText(""); sizeET.setText(""); wordET.requestFocus(); } }因為 ContentProvider 都是用 ContentValues,一方面為了把這個包起來,另一方面也為了把 ContentProvider 包起來,這邊用了 Words.java 表示 words table 裡的資料,再用 WordsHelper 將 ContentProvider 包起來。
@SuppressWarnings("serial") public class Words implements Serializable { private int id; private String word; private int size; public int getId() { return this.id; } public void setId(int id) { this.id = id; } public String getWord() { return this.word; } public void setWord(String word) { this.word = word; } public int getSize() { return this.size; } public void setSize(int size) { this.size = size; } @Override public int hashCode() { ... } @Override public boolean equals(Object obj) { ... } @Override public String toString() { return "Words [id=" + this.id + ", word=" + this.word + ", size=" + this.size + "]"; } } public class WordsHelper { private static final String TAG = "WordHelper"; public static List<Words> listWords(Activity act) { Log.d(TAG, "listWords..."); Cursor c = act.managedQuery(WordsTableMetaData.CONTENT_URI, null, null, null, null); int idIdx = c.getColumnIndex(WordsTableMetaData._ID); int wordIdx = c.getColumnIndex(WordsTableMetaData.WORDS_WORD); int sizeIdx = c.getColumnIndex(WordsTableMetaData.WORDS_SIZE); List<Words> wlist = new ArrayList<Words>(); Words w; for (c.moveToFirst(); !c.isAfterLast(); c.moveToNext()) { w = new Words(); w.setId(c.getInt(idIdx)); w.setWord(c.getString(wordIdx)); w.setSize(c.getInt(sizeIdx)); wlist.add(w); Log.d(TAG, "listWords - " + w); } Log.d(TAG, "listWords - " + c.getCount()); c.close(); return wlist; } public static Words getWords(ContentResolver cr, int id) { Log.d(TAG, "getWords..." + id); Uri getUri = Uri.withAppendedPath(WordsTableMetaData.CONTENT_URI, String.valueOf(id)); Cursor c = cr.query(getUri, null, null, null, null); int wordIdx = c.getColumnIndex(WordsTableMetaData.WORDS_WORD); int sizeIdx = c.getColumnIndex(WordsTableMetaData.WORDS_SIZE); Words w; if (c.moveToNext()) { String word = c.getString(wordIdx); int size = c.getInt(sizeIdx); w = new Words(); w.setId(id); w.setWord(word); w.setSize(size); } else { throw new IllegalArgumentException("Unknown id - " + id); } c.close(); Log.d(TAG, "getWords - " + w); return w; } public static void addWords(ContentResolver cr, Words w) { Log.d(TAG, "addWords..." + w); ContentValues cv = new ContentValues(); cv.put(WordsTableMetaData.WORDS_WORD, w.getWord()); cv.put(WordsTableMetaData.WORDS_SIZE, String.valueOf(w.getSize())); Uri addedUri = cr.insert(WordsTableMetaData.CONTENT_URI, cv); Log.d(TAG, "addWords - " + addedUri); } public static void updateWords(ContentResolver cr, Words w) { Log.d(TAG, "updateWords..." + w); ContentValues cv = new ContentValues(); cv.put(WordsTableMetaData.WORDS_WORD, w.getWord()); cv.put(WordsTableMetaData.WORDS_SIZE, String.valueOf(w.getSize())); Uri updatedUri = Uri.withAppendedPath(WordsTableMetaData.CONTENT_URI, String.valueOf(w.getId())); int cnt = cr.update(updatedUri, cv, null, null); Log.d(TAG, "updateWords - " + cnt); } public static void deleteAllWords(ContentResolver cr) { Log.d(TAG, "deleteAllWords..."); int cnt = cr.delete(WordsTableMetaData.CONTENT_URI, null, null); Log.d(TAG, "deleteAllWords - " + cnt); } public static void deleteWords(ContentResolver cr, int id) { Log.d(TAG, "deleteWords..."); Uri deletedUri = Uri.withAppendedPath(WordsTableMetaData.CONTENT_URI, String.valueOf(id)); int cnt = cr.delete(deletedUri, null, null); Log.d(TAG, "deleteWords - " + cnt); } }
沒有留言:
張貼留言