2011-08-21

Android content provider 筆記

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 的步驟:
  1. 定義 ContentProvider 所有資訊
  2. 實做 ContentProvider
  3. 註冊 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);
		}

		@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);
    }
}

沒有留言:

張貼留言