本文主要講解在Android開發中ContentProvider的常規用法,僅供學習分享使用,如有不足之處,還請指正。 ...
本文主要講解在Android開發中ContentProvider的常規用法,僅供學習分享使用,如有不足之處,還請指正。
訪問一個ContentProvider
在Android開發中,應用程式通過ContentResolver(內容解析器)從ContentProvider(內容提供者)中獲取數據,ContentResolver提供訪問ContentProvider中同名方法,ContentProvider包括ContentProvider和它的子類,ContentResolver對ContentProvider的持久層存儲提供了基本的CRUD(Create,Retrieve,Update,Delete)方法進行訪問。客戶端App的ContentResolver對象自動處理和ContentProvider的App之間的進程間通信。ContentProvider還充當資料庫和外部數據視圖表現之間的抽象層。
備註:如果要訪問一個ContentProvider,App需要在清單文件中請求對應的許可權。
例如:從User Dictionary Provider中獲取單詞和區域的列表,可以調用ContentResolver.query()方法,如下圖所示:
1 // 查詢用戶定義字典並返回結果 2 mCursor = getContentResolver().query( 3 UserDictionary.Words.CONTENT_URI, // 單詞表的內容URI 4 mProjection, // 查詢的數據列名數組 5 mSelectionClause //查詢條件,可以為null 6 mSelectionArgs, // 查詢參數,可以為null 7 mSortOrder); // 返回數據對象的排序條件
下表顯示了query(Uri,projection,selection,selectionArgs,sortOrder) 如何與SQL語句進行匹配:
Content URIs
Content URI是Provider中標識數據的URI,包括整個Provider(其許可權)的符號名和指向表(或路徑)的名稱,Content URI是訪問ContentProvider的參數之一。
在前面的代碼行中,常量_uri包含了用戶詞典“Word”表的Content URI。ContentResolver對象通過將許可權與已知提供者的系統表進行比較,將查詢參數發送到正確的Provider。
ContentProvider使用URI的路徑部分來選擇要訪問的表,通常為公開的每個表設置路徑。
在前面的代碼行中,“Word”表的全稱為:
1 content://user_dictionary/words
其中user_dictionary 字元串是Provider的許可權,而 words是表的路徑。content:// (the scheme)始終存在,並將其標識為Content URI。
許多Provider允許將id值附加到URI的末尾來訪問表中的單個行。例如,要從User Dictionary中檢索_id為4的行,可以使用Content URI:
1 Uri singleUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);
當要修改或刪除其中一行時,經常使用ID值。
備註:Uri 和 Uri.Builder類包含了用字元串構造形式良好的uri對象的方法。ContentUris包含了將ID附加到uri的方法。前面片段使用withAppendedId() 將ID附加到UserDictionary.Words.CONTENT_URI。
從Provider獲取數據
本節介紹如何使用User Dictionary Provider作為示例,從中檢索數據。
為了清晰起見,本節中的代碼段調用“UI線程”上的ContentResolver.query()。在實際代碼中,應該在非UI線程上非同步地進行查詢。
要從Provider獲取數據,請遵循以下基本步驟:
- 請求讀取Provider的訪問許可權。
- 定義查詢Provider的代碼。
訪問許可權
要從Provider中檢索數據,應用程式需要Provider的“讀取訪問許可權”。不能在運行時請求此許可權;必須在您的清單中指定需要此許可權,使用<uses-permission>元素和由Provider定義的許可權名稱。當在清單中指定此元素時,實際上是在為App“請求”此許可權。當用戶安裝App時,會隱式地批准這個請求。
User Dictionary Provider在清單文件中定義的許可權名稱為android.permission.READ_USER_DICTIONARY,所以App中想要從Provider中獲取數據,需要請求這個許可權。
構造查詢
查詢數據的下一步是構造查詢。以下片段定義了訪問User Dictionary Provider的一些變數:
1 // "projection" 定義每行返回的列名數組 2 String[] mProjection = 3 { 4 UserDictionary.Words._ID, // _ID column name 5 UserDictionary.Words.WORD, // word column name 6 UserDictionary.Words.LOCALE // locale column name 7 }; 8 9 // 定義查詢條件 10 String mSelectionClause = null; 11 12 // 定義查詢條件參數 13 String[] mSelectionArgs = {""};
下一個片段顯示如何使用ContentResolver.query(),以User Dictionary Provider 為例,查詢類似於sql查詢,它包含要返回的列名、查詢條件和排序。
查詢返回的列集合稱為投影(變數投影)。
查詢條件表達式被拆分為選擇子句和選擇參數。選擇子句是邏輯表達式和布爾表達式、列名稱和值的組合。如果指定可替換參數“?”,查詢條件不再是一個值,而是從條件參數數組(mSelectionArgs)中查詢該值。
如果用戶沒有輸入一個單詞,則選擇子句設置為空,查詢返回Provider中的所有單詞。
如果用戶輸入了一個單詞,查詢條件將設置UserDictionary.Words.WORD + " = ?"。參數數組的第一個元素設置為用戶輸入的單詞。
1 /* 2 * 定義查詢參數 3 */ 4 String[] mSelectionArgs = {""}; 5 6 // 獲取界面輸入的查詢條件 7 mSearchString = mSearchWord.getText().toString(); 8 9 //此處插入代碼校驗數據是否有效 10 //如果條件為空,則查詢所有 11 if (TextUtils.isEmpty(mSearchString)) { 12 // 如果查詢條件為空,則返回所有內容 13 mSelectionClause = null; 14 mSelectionArgs[0] = ""; 15 } else { 16 // 構造查詢條件,匹配用戶輸入的數據. 17 mSelectionClause = UserDictionary.Words.WORD + " = ?"; 18 // 查詢參數. 19 mSelectionArgs[0] = mSearchString; 20 } 21 22 // 對錶進行查詢並返回Cursor對象 23 mCursor = getContentResolver().query( 24 UserDictionary.Words.CONTENT_URI, // URI 25 mProjection, // 查詢數據列 26 mSelectionClause // 查詢條件 27 mSelectionArgs, // 查詢參數 28 mSortOrder); // 返回結果排序行 29 30 // 如果出現查詢異常,則返回空 31 if (null == mCursor) { 32 /* 33 * 插入代碼捕獲異常 34 */ 35 // 如果返回為空,則沒有匹配的內容 36 } else if (mCursor.getCount() < 1) { 37 /* 38 * 通知用戶查詢不成功. 但這不是必須的*/ 39 } else { 40 // 插入代碼處理結果 41 }
類似於sql語句:
1 SELECT _ID, word, locale FROM words WHERE word = <userinput> ORDER BY word ASC;
在這個sql語句中,使用的是實際的列名稱,而不是Contract類常量。
防止惡意輸入
如果content provider管理的數據在sql資料庫中,外部不受信任的數據輸入到原始sql語句中,就會導致sql註入。
考慮這個查詢條件:
1 //通過拼接用戶輸入和列名的方式構造查詢條件 2 String mSelectionClause = "var = " + mUserInput;
如果這樣做,用戶可能將惡意sql連接到您的sql語句中。例如,用戶可以輸入"nothing; DROP TABLE *;"用於mUserInput,這將導致選擇子句var = nothing; DROP TABLE *;。由於選擇條件被視為sql語句,這可能會導致Provider刪除sqlite資料庫中的所有表。
為了避免此問題,請使用可替換的參數和單獨的選擇參數數組的查詢條件。採用這種方式,用戶輸入將直接綁定到查詢,而不是被解釋為sql語句的一部分,用戶無法註入惡意sql。如下所示:
1 // 用一個可替換參數來包含用戶輸入 2 String mSelectionClause = "var = ?";
如下設置查詢參數數組:
1 // 定義一個查詢條件的數組 2 String[] selectionArgs = {""};
在查詢參數數組中進行賦值:
1 // 將用戶數據作為參數數據 2 selectionArgs[0] = mUserInput;
顯示查詢結果
ContentResolver.query()客戶端方法總是返回一個Cursor。Cursor對象提供對其包含的行和列的讀取訪問權。使用Cursor中的方法可以迭代行數據,確定每列的數據類型,將數據從列中取出,並檢查結果的其他屬性。有些Cursor實現會在提供者的數據變更時自動更新,或在Cursor變更時觸發對應的事件,或兩者兼而有之。
如果沒有行符合查詢條件,provider將返回一個Cursor, 其Cursor.getCount()為0(空游標)。
如果發生內部錯誤,查詢的結果取決於特定的Provider。它可以返回null,也可以拋出異常。
由於Cursor是行的“列表”,顯示Cursor內容的一個好方法是通過SimpleCursorAdapter綁定到ListView。
如下代碼所示:它創建一個SimpleCursorAdapter對象,包含查詢到的Cursor,並將此對象設置到ListView的適配器
1 // 定義從Cursor中檢索並載入到輸出行的列名 2 String[] mWordListColumns = 3 { 4 UserDictionary.Words.WORD, // word column name 5 UserDictionary.Words.LOCALE // locale column name 6 }; 7 8 //定義一個視圖ID列表,該列表將接收每行的Cursor列 9 int[] mWordListItems = { R.id.dictWord, R.id.locale}; 10 11 // 創建一個SimpleCursorAdapter對象 12 mCursorAdapter = new SimpleCursorAdapter( 13 getApplicationContext(), // 應用程式上下文對象 14 R.layout.wordlistrow, // ListView單行配置文件 15 mCursor, // query函數返回的結果 16 mWordListColumns, // Cursor中的列名數組 17 mWordListItems, // ListView中Item項的佈局文件 18 0); // Flags (usually none are needed) 19 20 // 設置 adapter到ListView 21 mWordList.setAdapter(mCursorAdapter);
備註:要使用Cursor支持ListView,Cursor必須包含一個名為_id的列。這個限制也解釋了為什麼大多數Provider的每個表都有一個_id列。
從查詢結果中獲取數據
您可以將查詢結果用於其他任務,而不是簡單地顯示查詢結果。要做到這一點,需要迭代Cursor中的行:
1 // 定義"word"列的索引 2 int index = mCursor.getColumnIndex(UserDictionary.Words.WORD); 3 4 /* 5 * 當cursor有效的時候才執行. User Dictionary Provider如果發生內部錯誤,將返回null,其他provider可能會拋出異常 6 */ 7 8 if (mCursor != null) { 9 /* 10 * 移動到cursor的下一行.在第一行移動之前, 行指向是-1,如果試圖去查詢此位置上的內容,將會拋出一個異常 11 */ 12 while (mCursor.moveToNext()) { 13 //獲取對應的列的值. 14 newWord = mCursor.getString(index); 15 // 插入代碼處理獲取的值. 16 ... 17 // while 迴圈結束 18 } 19 } else { 20 // 展示錯誤和異常信息 21 }
Cursor實現包含檢索不同類型數據的幾種“get”方法。例如,上一個片段使用getString()。同時也有一個gettype()方法,該方法返回列的數據類型。
Content Provider許可權
訪問Provider中的數據,調用方必須具有相應的許可權,這些許可權確保用戶知道應用程式試圖訪問哪些數據,用戶在安裝App時會看到請求的許可權。
如前所述,User Dictionary Provider要求使用android.permission.READ_USER_DICTIONARY許可權獲取數據。Provider需要android.permission.WRITE_USER_DICTIONARY許可權來插入、更新或刪除數據。
為了獲得訪問provider所需的許可權,App在其清單文件中以<uses-permission>元素請求它們。當安裝App時,用戶必須允許應用程式請求的所有許可權。如果用戶全部允許,將繼續安裝;如果用戶不允許,Package Manager將中止安裝。
以下<uses-permission>元素請求讀取 User Dictionary Provider的訪問許可權:
1 <uses-permission android:name="android.permission.READ_USER_DICTIONARY">
Inserting, Updating, and Deleting Data
與從provider獲取數據的方式相同,還可以使用provider客戶端與provider's 提供方之間的交互來修改數據。provider 和provider客戶端自動處理安全以及進程間通信。
插入數據(Inserting data)
將數據插入到provider中,請調用ContentResolver.insert()。此方法將新行插入到provider中,並返回新增行的 content URI。此片段顯示如何將新詞插入到User Dictionary Provider中:
1 // 定義一個新的 Uri對象,接收插入新行放回的內容 2 Uri mNewUri; 3 4 // 要插入的新值 5 ContentValues mNewValues = new ContentValues(); 6 7 /* 8 * 設置每列對應的值 9 */ 10 mNewValues.put(UserDictionary.Words.APP_ID, "example.user"); 11 mNewValues.put(UserDictionary.Words.LOCALE, "en_US"); 12 mNewValues.put(UserDictionary.Words.WORD, "insert"); 13 mNewValues.put(UserDictionary.Words.FREQUENCY, "100"); 14 15 mNewUri = getContentResolver().insert( 16 UserDictionary.Word.CONTENT_URI, // 內容 URI 17 mNewValues // 插入的值 18 );
新行的數據對應單個ContentValues對象,該對象在形式上類似於單行cursor。此對象中的列不需要具有相同的數據類型,如果不想指定值,則可以使用ContentValues.putNull()設置列為空。
代碼段不會添加_id列,因為此列是自動維護的。provider為添加的每一行指定一個唯一_id,通常使用_id作為表的主鍵。
返回的新行的newUri,格式如下:
1 content://user_dictionary/words/<id_value>
<id_value>是新行的_id。大多數provider可以自動檢測到這種形式的內容,然後在該特定行上執行請求的操作。
若要從返回的Uri中得到_id值,請調用ContentUris.parseId()。
更新數據(Updating data)
要更新行,將使用帶有更新值的ContentValues對象,就像使用插入時一樣,選擇條件也與使用查詢時一樣。調用方法是ContentResolver.update()。您只需要為需要更新的列向ContentValues對象添加值。如果要清除列的內容,請將值設置為null。
下麵的片段將locale設置有語言"en"的所有行更改為locale為空。返回值是更新的行數:
1 // 包含更新的內容的對象 2 ContentValues mUpdateValues = new ContentValues(); 3 4 // 定義需要更新的查詢條件 5 String mSelectionClause = UserDictionary.Words.LOCALE + "LIKE ?"; 6 String[] mSelectionArgs = {"en_%"}; 7 8 // 定義更新行得到的行數 9 int mRowsUpdated = 0; 10 11 /* 12 * 設置更新的內容. 13 */ 14 mUpdateValues.putNull(UserDictionary.Words.LOCALE); 15 16 mRowsUpdated = getContentResolver().update( 17 UserDictionary.Words.CONTENT_URI, // URI 18 mUpdateValues // 更新的內容 19 mSelectionClause //查詢條件 20 mSelectionArgs // 查詢內容參數 21 );
在調用ContentResolver.update()時,對用戶輸入進行處理。
刪除數據(Deleting data)
刪除行類似於查詢行數據:為要刪除的行指定選擇條件,而客戶端方法返回已刪除行的數目如下所示:
1 // 定義需要刪除的條件 2 String mSelectionClause = UserDictionary.Words.APP_ID + " LIKE ?"; 3 String[] mSelectionArgs = {"user"}; 4 5 //定義刪除掉行數 6 int mRowsDeleted = 0; 7 8 // 刪除匹配條件的內容 9 mRowsDeleted = getContentResolver().delete( 10 UserDictionary.Words.CONTENT_URI, // URI 11 mSelectionClause // 刪除條件 12 mSelectionArgs // 刪除參數 13 );
在調用 ContentResolver.delete()方法時,對用戶輸入進行處理。
Provider數據類型
Content providers可以提供許多不同的數據類型。User Dictionary Provider只提供文本,但也可以提供以下格式:
- integer
- long integer (long)
- floating point
- long floating point (double)
providers經常使用的另一種數據類型是Binary Large OBject (BLOB),它是64kb位元組數組。通過查看Cursor類“get”方法,可以看到可用的數據類型。
provider中每一列的數據類型通常在其文檔中列出。User Dictionary Provider 的數據類型在其contract類UserDictionary.Words的參考文檔中列出。也可以通過Cursor.getType()來確定數據類型。
Provider訪問的替代形式
在應用程式開發中,三種可供選擇的Provider訪問形式非常重要:
- 批量訪問:可以在ContentProviderOperation中使用方法創建批量處理訪問調用,然後用ContentResolver.applyBatch()應用它們。
- 非同步查詢:應該在單獨的線程中進行查詢,其中一種方法是使用CursorLoader對象。
- 通過intents訪問數據:雖然不能直接向提供者發送intent,但可以向provider's application發送intent,而provider's application序通常是最適合修改provider數據的應用程式。
批量訪問(Batch access)
對provider的批量訪問用於插入多行,或在同一方法中在多個表中插入行,或通常用於作為事務(原子操作)執行一組跨進程的操作。
要以“batch mode”訪問provider,您可以創建一組 ContentProviderOperation 對象,然後通過ContentResolver.applyBatch()方法將對象分發到provider。將provider的許可權傳遞給此方法,而不是特定的內容。這允許數組中的每個ContentProviderOperation對象對不同的表操作。ContentResolver.applyBatch() 返回結果數組。
通過Intent進行數據訪問(Data access via intents)
Intents可以提供對 content provider的間接訪問。允許用戶訪問provider中的數據,即使您的App沒有訪問許可權,也可以從有許可權的App獲得結果Intent,或者通過激活有許可權的App併在其中工作。
合同類別(Contract Classes)
contract類定義了幫助App處理content URIs、列名稱、意圖操作和 content provider的其他特性的常量。Contract類不自動包含在provider中;provider的開發人員必須定義它們,然後將其提供給其他開發人員。android平臺中的許多提供商在android.provider中都有相應的contract類。
例如,User Dictionary Provider有一個包含內容URI和列名常量的contract類用戶詞典。“單詞”表的內容以“常量”為定義。UserDictionary.Words.CONTENT_URI,在以下示例片段中使用。例如,查詢投影可以定義為:
1 String[] mProjection = 2 { 3 UserDictionary.Words._ID, 4 UserDictionary.Words.WORD, 5 UserDictionary.Words.LOCALE 6 };
ContentProvider示例
讀取通話記錄
1 //通訊記錄URI 2 private String call_uri = "content://call_log/calls"; 3 4 //內容解析器 5 private ContentResolver mResolver; 6 7 //列表 8 private ListView lvCall; 9 10 //獲取的通訊記錄的列名 11 private String[] columns = new String[]{ 12 CallLog.Calls._ID, CallLog.Calls.CACHED_NAME, CallLog.Calls.NUMBER, CallLog.Calls.TYPE, CallLog.Calls.DATE,CallLog.Calls.DURATION 13 }; 14 15 @Override 16 protected void onCreate(Bundle savedInstanceState) { 17 super.onCreate(savedInstanceState); 18 setContentView(R.layout.activity_main); 19 //初始化內容解析器 20 mResolver = getContentResolver(); 21 lvCall = (ListView) this.findViewById(R.id.lv_call); 22 } 23 24 /** 25 * 獲取通訊記錄事件 26 * @param v 27 */ 28 public void bn_call(View v) { 29 List<Map<String, String>> list = new ArrayList<>(); 30 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); 31 Cursor cursor = mResolver.query(Uri.parse(call_uri), columns, null, null, CallLog.Calls.DEFAULT_SORT_ORDER); 32 //以下是為了轉換數據格式 33 if(cursor!=null){ 34 while (cursor.moveToNext()){ 35 long dt=cursor.getLong(cursor.getColumnIndex("date")); 36 Date callDate = new Date(dt); 37 String callDateStr = sdf.format(callDate); 38 String name=cursor.getString(cursor.getColumnIndex("name")); 39 String number=cursor.getString(cursor.getColumnIndex("number")); 40 String duration =cursor.getString(cursor.getColumnIndex("duration"))+"s"; 41 Map<String, String> map=new HashMap<String, String>() ; 42 map.put("name",name); 43 map.put("number",number); 44 map.put("date",callDateStr); 45 map.put("duration",duration); 46 list.add(map); 47 } 48 } 49 //將數據填充到Adapter 50 SimpleAdapter adapter=new SimpleAdapter(this,list,R.layout.list_item, 51 new String[]{"name", "number", "date","duration"}, 52 new int[]{R.id.tv_name, R.id.tv_number, R.id.tv_time,R.id.tv_duration}); 53 54 /*SimpleCursorAdapter adapter = new SimpleCursorAdapter(this, R.layout.list_item, cursor, 55 new String[]{"name", "number", "date"}, 56 new int[]{R.id.tv_name, R.id.tv_number, R.id.tv_time}, 57 CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);*/ 58 //綁定Adapter到ListView 59 lvCall.setAdapter(adapter); 60 }
讀取簡訊記錄