本文主要講解在Android開發中ContentProvider的常規用法,僅供學習分享使用,若有不足之處,還請指正。android
在Android開發中,應用程序經過ContentResolver(內容解析器)從ContentProvider(內容提供者)中獲取數據,ContentResolver提供訪問ContentProvider中同名方法,ContentProvider包括ContentProvider和它的子類,ContentResolver對ContentProvider的持久層存儲提供了基本的CRUD(Create,Retrieve,Update,Delete)方法進行訪問。客戶端App的ContentResolver對象自動處理和ContentProvider的App之間的進程間通訊。ContentProvider還充當數據庫和外部數據視圖表現之間的抽象層。sql
備註:若是要訪問一個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 URI是Provider中標識數據的URI,包括整個Provider(其權限)的符號名和指向表(或路徑)的名稱,Content URI是訪問ContentProvider的參數之一。app
在前面的代碼行中,常量_uri包含了用戶詞典「Word」表的Content URI。ContentResolver對象經過將權限與已知提供者的系統表進行比較,將查詢參數發送到正確的Provider。 異步
ContentProvider使用URI的路徑部分來選擇要訪問的表,一般爲公開的每一個表設置路徑。ide
在前面的代碼行中,「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。
本節介紹如何使用User Dictionary Provider做爲示例,從中檢索數據。
爲了清晰起見,本節中的代碼段調用「UI線程」上的ContentResolver.query()。在實際代碼中,應該在非UI線程上異步地進行查詢。
要從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()方法,該方法返回列的數據類型。
訪問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">
與從provider獲取數據的方式相同,還可使用provider客戶端與provider's 提供方之間的交互來修改數據。provider 和provider客戶端自動處理安全以及進程間通訊。
將數據插入到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()。
要更新行,將使用帶有更新值的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()時,對用戶輸入進行處理。
刪除行相似於查詢行數據:爲要刪除的行指定選擇條件,而客戶端方法返回已刪除行的數目以下所示:
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()方法時,對用戶輸入進行處理。
Content providers能夠提供許多不一樣的數據類型。User Dictionary Provider只提供文本,但也能夠提供如下格式:
providers常用的另外一種數據類型是Binary Large OBject (BLOB),它是64kb字節數組。經過查看Cursor類「get」方法,能夠看到可用的數據類型。
provider中每一列的數據類型一般在其文檔中列出。User Dictionary Provider 的數據類型在其contract類UserDictionary.Words的參考文檔中列出。也能夠經過Cursor.getType()來肯定數據類型。
在應用程序開發中,三種可供選擇的Provider訪問形式很是重要:
對provider的批量訪問用於插入多行,或在同一方法中在多個表中插入行,或一般用於做爲事務(原子操做)執行一組跨進程的操做。
要以「batch mode」訪問provider,您能夠建立一組 ContentProviderOperation 對象,而後經過ContentResolver.applyBatch()方法將對象分發到provider。將provider的權限傳遞給此方法,而不是特定的內容。這容許數組中的每一個ContentProviderOperation對象對不一樣的表操做。ContentResolver.applyBatch() 返回結果數組。
Intents能夠提供對 content provider的間接訪問。容許用戶訪問provider中的數據,即便您的App沒有訪問權限,也能夠從有權限的App得到結果Intent,或者經過激活有權限的App並在其中工做。
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 };
讀取通話記錄
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 }
讀取短信記錄
1 private String sms_uri="content://sms"; 2 3 private String[] columns=new String[]{ 4 Telephony.Sms._ID, Telephony.Sms.ADDRESS,Telephony.Sms.CREATOR, Telephony.Sms.BODY, Telephony.Sms.DATE, Telephony.Sms.PERSON, Telephony.Sms.STATUS, Telephony.Sms.DATE_SENT 5 }; 6 7 private ContentResolver mResolver; 8 9 private ListView lvMsg; 10 11 @Override 12 protected void onCreate(Bundle savedInstanceState) { 13 super.onCreate(savedInstanceState); 14 setContentView(R.layout.activity_main2); 15 mResolver=getContentResolver(); 16 lvMsg= (ListView) this.findViewById(R.id.lv_sms); 17 } 18 19 public void bn_sms(View view) { 20 List<Map<String, String>> list = new ArrayList<>(); 21 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); 22 Cursor cursor = mResolver.query(Uri.parse(sms_uri), columns, null, null, Telephony.Sms.DEFAULT_SORT_ORDER); 23 if (cursor != null) { 24 while (cursor.moveToNext()) { 25 Log.e("TAG", "bn_sms: "+cursor.getColumnIndex("person")+"---"+ cursor.getColumnIndex("date")+"---"+cursor.getColumnIndex("creator")+"---"+cursor.getColumnIndex("address")); 26 long dt = cursor.getLong(cursor.getColumnIndex("date")); 27 Date callDate = new Date(dt); 28 String callDateStr = sdf.format(callDate); 29 String person = cursor.getString(cursor.getColumnIndex("address")); 30 String creator = cursor.getString(cursor.getColumnIndex("creator")); 31 //String duration =cursor.getString(cursor.getColumnIndex("duration"))+"s"; 32 String body = cursor.getString(cursor.getColumnIndex("body")); 33 Map<String, String> map = new HashMap<String, String>(); 34 map.put("person", person); 35 map.put("creator", creator); 36 map.put("date", callDateStr); 37 //map.put("duration",duration); 38 map.put("body", body); 39 list.add(map); 40 } 41 } 42 //將數據填充到Adapter 43 SimpleAdapter adapter = new SimpleAdapter(this, list, R.layout.msg_item, 44 new String[]{"person", "creator", "date", "body"}, 45 new int[]{R.id.tv_name, R.id.tv_number, R.id.tv_time, R.id.tv_msg}); 46 47 //綁定Adapter到ListView 48 lvMsg.setAdapter(adapter); 49 }
千里之行,始於足下。