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";
}
}實做 ContentProviderpublic 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);
}
}


沒有留言:
張貼留言