一塊兒學Android之ContentProvider

本文主要講解在Android開發中ContentProvider的常規用法,僅供學習分享使用,若有不足之處,還請指正。android

訪問一個ContentProvider

Android開發中,應用程序經過ContentResolver(內容解析器)ContentProvider(內容提供者)中獲取數據,ContentResolver提供訪問ContentProvider中同名方法,ContentProvider包括ContentProvider和它的子類,ContentResolverContentProvider的持久層存儲提供了基本的CRUDCreate,Retrieve,Update,Delete)方法進行訪問。客戶端AppContentResolver對象自動處理和ContentProviderApp之間的進程間通訊。ContentProvider還充當數據庫和外部數據視圖表現之間的抽象層。sql

備註:若是要訪問一個ContentProviderApp須要在清單文件中請求對應的權限。數據庫

例如:從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 URIProvider中標識數據的URI,包括整個Provider(其權限)的符號名和指向表(或路徑)的名稱,Content URI是訪問ContentProvider的參數之一。app

在前面的代碼行中,常量_uri包含了用戶詞典「Word」表的Content URIContentResolver對象經過將權限與已知提供者的系統表進行比較,將查詢參數發送到正確的Provider 異步

ContentProvider使用URI的路徑部分來選擇要訪問的表,一般爲公開的每一個表設置路徑。ide

在前面的代碼行中,Word」表的全稱爲:函數

1 content://user_dictionary/words

其中user_dictionary 字符串是Provider的權限,words是表的路徑。content:// (the scheme)始終存在,並將其標識爲Content URI佈局

許多Provider容許將id值附加到URI的末尾來訪問表中的單個行。例如,要從User Dictionary中檢索_id4的行,可使用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獲取數據,請遵循如下基本步驟:

  1. 請求讀取Provider的訪問權限。
  2. 定義查詢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()客戶端方法老是返回一個CursorCursor對象提供對其包含的行和列的讀取訪問權。使用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支持ListViewCursor必須包含一個名爲_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 的數據類型在其contractUserDictionary.Words的參考文檔中列出。也能夠經過Cursor.getType()來肯定數據類型。

Provider訪問的替代形式

在應用程序開發中,三種可供選擇的Provider訪問形式很是重要:

  1. 批量訪問:能夠在ContentProviderOperation中使用方法建立批量處理訪問調用,而後用ContentResolver.applyBatch()應用它們。
  2. 異步查詢:應該在單獨的線程中進行查詢,其中一種方法是使用CursorLoader對象。
  3. 經過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     }

讀取短信記錄

 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     }

備註

千里之行,始於足下。

相關文章
相關標籤/搜索