1. 程式人生 > >Android學習之基礎知識十—內容提供器(Content Provider)

Android學習之基礎知識十—內容提供器(Content Provider)

一、跨程式共享資料——內容提供器簡介

  內容提供器(Content Provider)主要用於在不同的應用程式之間實現資料共享的功能,它提供了一套完整的機制,允許一個程式訪問另一個程式中的資料,同時還能保證被訪資料的安全性,目前,使用內容提供器是Android實現跨程式共享資料的標準方式。

  不同於檔案儲存和SharedPreferences儲存中的兩種全域性可讀寫操作模式,內容提供器可以選擇只對哪一部分資料進行共享,從而保證我們程式中的隱私資料不會有洩漏的風險。

  在正式學習內容提供器之前,我們還需要先掌握另外一個非常重要的知識——Android執行時許可權,因為待會兒的內容提供器示例中會使用到執行時許可權的功能。不光是內容提供器,開發過程中經常會用到執行時許可權,因此必須牢牢掌握。

二、執行時許可權

  Android開發團隊在Android6.0系統中引用了執行時許可權這個功能,比之前的Android許可權機制能更好的保護使用者的安全和隱私。

2.1、Android許可權機制詳解

  Android現在將所有的許可權歸成了兩類:一類是普通許可權,一類是危險許可權。普通許可權是指那些不會直接威脅到使用者的安全和隱私的許可權,對於這部分許可權申請,系統會自動幫我們進行授權,而不需要使用者再去手動操作了,比如檢視網路連線,開機自啟等許可權;危險許可權則表示那些可能會觸及使用者隱私,或者對裝置安全性造成影響的許可權,如獲取裝置聯絡人資訊、定位裝置的地理位置等,對於這部分許可權申請,必須要有使用者手動點選授權才可以,否則程式就無法使用相應的功能。

  雖然Android中一共有上百種許可權,但是危險許可權一共就只有9組24個,其他剩下的就都是普通許可權。下面是危險許可權的列表:

注意:表格中每個危險許可權都是屬於一個許可權組,我們在進行執行時許可權處理時使用的是許可權名,但是使用者一旦同意授權了,那麼該許可權所對應的許可權組中所有的其他許可權也會同時被授權。

  檢視Android系統完整的許可權列表:http://developer.android.com/reference/android/Manifest.permission.html

 2.2、在程式執行時申請許可權

   為簡單起見,我們使用CALL_PHONE這個危險許可權來作為本小節中的示例,CALL_PHONE這個許可權是編寫撥打電話功能的時候需要宣告的,因為撥打電話會涉及使用者手機的資費問題,因而被列為了危險許可權,在Android6.0系統出現之前,撥打電話功能的實現其實非常簡單:

第一步:設定一個按鈕,通過點選按鈕來完成撥打電話的操作

第二步:在MainActivity中實現點選按鈕的邏輯功能

第三步:在AndroidManifest中新增許可權宣告

第四步:分別在Android系統低於6.0和高於6.0的模擬器上執行,點選按鈕,效果如下:Android系統低於6.0(左)、Android系統高於6.0(右)

   

第五步:效果分析

  從執行效果中我們可以看出,在Android系統小於6.0的手機上,點選按鈕後是直接就可以打電話了,但是在Android系統高於6.0的手機上,點選按鈕後,並沒有打電話。這時候我們檢視Logcat日誌,發現丟擲了以下異常:(Permission Denial,許可權被禁止,說明6.0以上系統在使用危險許可權時都必須進行執行時許可權處理)

接下來我們嘗試修復這個問題:

第六步:修改MainActivity中的程式碼:

程式碼分析:

  上面的程式碼將執行時許可權的完整流程都覆蓋了。執行時許可權的核心其實就是在程式執行的過程中由使用者授權我們去執行某些危險操作,程式是不可以擅自做主去執行這些危險操作的。因此第一步,要先判斷使用者是不是已經給我們授權了,藉助的是:ContextCompat.checkSelfPermission()方法,checkSelfPermission()方法接收兩個引數,第一個引數是:Context,第二個引數是具體的許可權名,比如打電話的許可權名就是:Manifest.permission.CALL_PHONE,然後我們使用方法的返回值和:PackageManager.PERMISSION_GRANTED做比較,相等就說明使用者已經授權,不等就表示使用者沒有授權。

  如果已經授權的話,就直接去執行打電話的邏輯操作就行了,這裡把撥打電話的邏輯封裝到了:call()方法中。如果沒有授權,則需要呼叫ActivityCompat.requestPermissions()方法來向用戶申請授權,requestPermissions()方法接收3個引數,第一個引數要求是Activity的例項,第二個引數是一個String陣列,把要申請的許可權名放在陣列中即可,第三個引數是請求碼,只要是唯一值就可以了,這裡傳入的是1.

  呼叫完了:requestPermissions()方法之後,系統會彈出一個許可權申請的對話方塊,然後使用者可以選擇同意或拒絕我們的許可權申請,不論是哪種結果,最終都會回撥到:onRequestPermissionsResult()方法中,而授權的結果則會封裝在:grantResults引數當中,這裡我們只需要判斷一下最後的授權結果:

  1、如果使用者同意的話就會呼叫:call()方法來撥打電話,並且後面如果再點選:Make Call 按鈕,就不會再彈出許可權申請的對話方塊了,而是直接就撥打電話了,如果想要關閉許可權,進入:setting ---> Apps ---> RunTimePermissionTest ---> Permissions,關閉許可權就行了。

  2、如果使用者拒絕的話我們只能放棄操作,並且彈出一條失敗提示。下次再點選按鈕,仍然會再彈出許可權申請的對話方塊。

第七步:再Android系統大於6.0的模擬器上執行程式,點選按鈕,檢視執行狀態:

 

      (授權申請對話方塊)                                                      (不授權)

 

       (授權)                  (關閉授權)

 三、訪問其他程式中的資料

   內容提供器的用法一般有兩種:一種是使用現有的內容提供器來讀取和操作相應程式中的資料,另一種是建立自己的內容提供器給我們程式的資料提供外部訪問介面。先看第一種用法:

  如果一個應用程式通過內容提供器對其資料提供了外部訪問介面,那麼任何其他的應用程式就都可以對這部分資料進行訪問。Android系統中自帶的電話簿、簡訊、媒體庫等程式都提供了類似的介面,這使得第三方應用程式可以充分地利用這部分資料來實現更好的功能。

 3.1、ContentResolver的基本用法

   對於一個應用程式來說,如果想要訪問內容提供器中共享的資料,就一定要藉助ContentResolver類,可以通過Context中的:getContentResolver()方法獲取到該類的例項。ContentResolver中提供了一系列的方法用於對資料進行CRUD操作,其中:insert()方法用於新增資料,update()方法用於更新資料,delete()方法用於刪除資料,query()方法用於查詢資料。這和SQLiteDatabase相似,但不同於SQLiteDatabase,ContentResolver中的增刪改查方法都不是接收表名引數,而是使用一個Uri引數代替,這個引數被稱為內容URI,內容URI給內容提供器中的資料建立了唯一的識別符號,它主要由兩部分組成:authority和path。authority是用於對不同的應用程式做區分的,一般為了避免衝突,都會採用程式包名的方式來進行命名。比如某個程式的包名是:com.example.app,那麼該程式對應的authority就可以命名為:com.example.app.provider。path則是用於對同一應用程式中不同的表做區分的,通常都會新增到authority的後面,比如某個程式的資料庫存在兩張表:table1和table2,這時就可以將path分別命名為:/table1 和 /table2,然後把authority和path進行組合,內容URI就變成了:com.example.app.provider/table1 和 com.example.app.provider/table2。不過,目前還很難辨認出這兩個字串就是兩個內容URI,我們還需要在字串的頭部加上協議宣告,因此,內容URI最標準的格式寫法如下:

  content://com.example.app.provider/table1

  content://com.example.app.provider/table2

 內容URI可以很清楚的表達我們想要訪問哪個程式中的哪張表裡的資料。在得到了內容URI字串之後,我們還需要將它解析成Uri物件才可以作為引數傳入,解析的方法也很簡單,程式碼如下:只需要呼叫:Uri.parse()方法,就可以將內容URI字串解析成Uri物件了。

現在我們就能使用這個Uri物件來查詢table1表中的資料了,程式碼如下:

這些引數和SQLiteDatabase中query()方法裡的引數很像,但是總體來說要簡單一些,下表是對這些引數的詳細解釋:

查詢完成後返回的仍然是一個Cursor物件,這時候我們就可以將資料從Cursor物件中逐個讀取出來了。讀取的思路仍然是通過移動遊標的位置來遍歷Cursor的所有行,然後再取出每一個行中相應列的資料,程式碼如下所示:

掌握了最難的查詢操作,剩下的增加、修改、刪除操作就輕鬆多了,先看向table1表中新增一條資料,程式碼如下:

可以看到,仍然是將待新增的資料組裝到:ContentValues中,然後呼叫ContentResolver的:insert()方法,將Uri和ContentValues作為引數傳入即可。如果現在我們想要更新這條新新增的資料,把column1的值清空,可以藉助ContentResolver的:update()方法實現,程式碼如下:

注意上述程式碼使用了selection和selectionArgs引數來對想要更新的資料進行約束,以防止所有的行都會受到影響。

最後可以呼叫ContentResolver的:delete()方法將這條資料刪除掉,程式碼如下:

到此為止,我們就把ContentResolver中的增刪改查方法全部學完了,接下來我們就看看如何讀取系統電話簿中的聯絡人。

3.2、讀取系統聯絡人

   由於一直使用都是模擬器,電話簿裡面並沒有聯絡人存在,所以現在需要自己手動新增幾個,以便稍後進行讀取。新增如下兩個聯絡人:

第一步:新建一個ContactsTest專案,在佈局檔案中只放置一個ListView,我們希望讀取出來的聯絡人資訊能夠在ListView中顯示

第二步:在MainActivity中實現邏輯功能

程式碼分析:

  在onCreate()方法中,我們首先獲取到了ListView控制元件的例項,並給它設定好了介面卡,然後開始呼叫執行時許可權的處理邏輯,因為READ_CONTACTS許可權是屬於危險許可權,關於執行時許可權的處理流程這裡就不多說了,這裡使用者授權之後呼叫:readContacts()方法來讀取系統聯絡人資訊。

  下面重點來說一下:readContacts()方法,可以看到,這裡使用了ContentResolver的query()方法來查詢系統聯絡人的聯絡人資料。不過這裡傳入的Uri並沒有呼叫:Uri.parse()方法去解析一個URI字串,這是因為:ContactsContract.CommonDataKinds.Phone類已經幫我們做好了封裝,提供了一個:CONTENT_URI常量,而這個常量就是使用:Uri.parse()方法解析出來的結果,接著我們對Cursor物件進行遍歷,將聯絡人姓名和手機號這些資料逐個取出,聯絡人姓名這一列對應的常量是:ContactsContract.CommonDataKinds.Phone.DISPLAR_NAME,聯絡人手機號這一列對應的常量是:ContactsContract.CommonDataKinds.Phone.NUMBER,這兩個資料都取出後,將它們拼接,並且在中間加上換行符,然後將拼接後的資料新增到ListView的資料來源裡,並通知重新整理一下ListView,最後不要忘記將Cursor物件關閉。

第三步:在AndroidManifest.xml中進行讀取系統聯絡人的許可權宣告:

第四步:執行程式(左),點選ALLOW授權後顯示出系統聯絡人和手機號(右)

 

四、建立自己的內容提供器

4.1、建立內容提供器的步驟

   在上一節中,我們學習瞭如何在自己的程式中訪問其他應用程式的資料,總體來說,思路比較簡單,只需要獲取到該應用程式的內容URI,然後藉助ContentResolver進行CRUD操作。那麼那些提供外部訪問介面的應用程式都是如何實現這種功能的呢?

  如果想要實現跨程式共享資料的功能,官方推薦的方式就是使用內容提供器,可以通過新建一個類去繼承ContentProvider的方式來建立一個自己的內容提供器。ContentProvider類中有6個抽象方法,我們在使用子類繼承它的時候,需要將這6個方法全部重寫。程式碼如下:

  從上面我們可以看到,幾乎每一個方法都會帶有uri這個引數,這個引數也正是呼叫ContentResolver的增刪改查方法是傳遞過來的,而現在我們需要對傳入的Uri引數進行解析,從中分析出呼叫方期望訪問的表和資料。

  回顧一下,一個標準的內容URI寫法是這樣的

  這就表示呼叫方期望訪問的是:com.example.app這個應用的table1表中的資料,除此之外,我們還可以在這個內容URI的後面加上一個id,如下所示:

  這就表示呼叫方期望訪問的是:com.example.app這個應用的table1表中id為1的資料。

  內容URI的格式主要就只有以上兩種,以路徑結尾就表示期望訪問該表中所有的資料,以id結尾就表示期望訪問該表中擁有相應id的資料。我們可以使用萬用字元的方式來分別匹配這兩種格式的內容URI,規則如下:

所以,一個能夠匹配任意表的內容URI格式就可以寫成:

而一個能夠匹配table1表中任意一行資料的內容URI格式就可以寫成:

 

  接著,我們再借助:UriMatcher這個類就可以輕鬆的實現匹配內容URI的功能,UriMatcher中提供了一個:addURI()方法,這個方法接收3個引數,可以分別把authority、path和一個自定義程式碼傳進去。這樣,當呼叫UriMatcher的:match()方法時,就可以將一個Uri物件傳入,返回值是某個能夠匹配這個Uri物件所對應的自定義程式碼,利用這個程式碼,我們就可以判斷出呼叫方期望訪問的是哪張表中的資料了,修改MyProvider中的程式碼如下:

  可以看到,MyProvider中新增了4個整型常量,其中TABLE1_DIR表示訪問table1表中的所有資料,TABLE1_ITEM表示訪問table1表中的單條資料,TABLE2_DIR表示訪問table2表中的所有資料,TABLE2_ITEM表示訪問table2表中的單條資料。接著在靜態程式碼塊裡我們建立了UriMatcher的例項,並呼叫addURI()方法,將期望匹配的內容URI格式傳遞進去,注意這裡傳入的路徑引數是可以使用萬用字元的。然後當:query()方法被呼叫的時候,就會通過UriMatcher的:match()方法對傳入的Uri物件進行匹配,如果發現UriMatcher中某個內容URI格式成功匹配了該Uri物件,則會返回相應的自定義程式碼,然後我們就可以判斷出呼叫方期望訪問的到底是什麼資料了。

  上述的程式碼只是以:query()方法為例做了示範,其實:insert()、update()、delete()這幾個方法的實現也是差不多的,它們都會攜帶Uri這個引數,然後同樣利用UriMatcher的:match()方法判斷出呼叫期望訪問的是哪張表,再對該表中的資料進行相應的操作就可以了。

  除此之外,還有一個方法你會比較陌生,即:getType()方法,它是所有的內容提供器都必須提供的一個方法,用於獲取Uri物件所對應的MIME型別,一個內容URI所對應的MIME字串主要由3個部分組成,Android對這3個部分做了如下格式規定:

  1、必須以vnd開頭

  2、如果內容URI以路徑結尾,則後接:android.cursor.dir/,如果內容URI以id結尾,則後接:android.cursor.item/

  3、最後接上:vnd.<authority>.<path>

所以,對於:content://com.example.app.provider/table1這個內容URI,它所對應的MIME型別就可以寫成:vnd.android.cursor.dir/vnd.com.example.app.provider.table1

     對於:content://com.example.app.provider/table1/1這個內容URI,它所對應的MIME型別就可以寫成:vnd.android.cursor.item/vnd.com.example.app.provider.table1

現在我們繼續完善MyProvider中的內容,這次來實現getType()方法中的邏輯,程式碼如下:

  到這裡,一個完整的內容提供器就建立完成了,現在任何一個應用程式都可以使用ContentResolver來訪問我們程式中的資料。那麼如何保證隱私資料不會洩露出去呢?其實多虧了內容提供器的良好機制,這個問題在不知不覺中已經解決了,因為所有的CRUD操作都一定要匹配到相應的內容URI格式才能進行的,而我們當然不可能向UriMatcher中新增隱私資料的URI,所以這部分資料根本無法被外部程式訪問到,安全問題也就不存在了。

  接下來通過實際案例來體驗跨程式的資料共享功能:

 4.2實現跨程式的資料共享

第一步:建立內容提供器:

  簡單起見,我們還是在上一章中的DatabaseTest專案的基礎上繼續開發,通過內容提供器來給它加入外部訪問介面。開啟DatabaseTest專案,首先將MyDatabaseHelper中使用Toast彈出建立資料庫成功的提示去掉,因為跨程式訪問時我們不直接使用Toast,然後建立一個內容提供器,右擊:com.workspace.hh.databasetest包 ---> New ---> Other ---> Content Provider,如圖所示:

  這裡我們將內容提供器命名為:DatabaseProvider,authority指定為:com.workspace.hh.databasetest.provider,Exported屬性表示是否允許外部程式訪問我們的內容提供器,Enabled屬性表示是否啟用這個內容提供器,將兩個屬性都勾中,點選Finish完成建立

第二步:修改DatabaseProvider中的程式碼:

  

程式碼分析:

  1、首先在類的開始,同樣定義了4個常量,分別用於表示訪問Book表中的所有資料,訪問Book表中的單條資料、訪問Category表中的所有資料和訪問Category表中的單條資料,然後在靜態程式碼塊裡對UriMatcher進行了初始化操作,將期望匹配的幾種URI格式添加了進去。

  2、接下來是每個抽象方法的具體實現,先看:onCreate()方法,這個方法的程式碼很短,就是建立一個MyDatabaseHelper的例項,然後返回true表示內容提供器初始化成功,這時候資料庫就已經完成了建立或升級操作。

  3、query()方法:這個方法中先獲取到SQLiteDatabase的例項,然後根據傳入的Uri引數判斷出使用者想要訪問哪張表,再呼叫SQLiteDatabase的query()進行查詢,並將Cursor物件返回就好了。注意當訪問單條資料的時候有一個細節,這裡呼叫看Uri物件的:getPathSegments()方法,它會將內容URI許可權之後的部分以“/”符號進行分割,並把分割後的結果放入到一個字串列表中,那這個列表的第0個位置存放的就是路徑,第一個位置存放的就是id了。得到了id之後,再通過selection和selectionArgs引數進行約束,就實現了查詢單條資料的功能。

  4、insert()方法,同樣它也是先獲取到SQLiteDatabase的例項,然後根據傳入的Uri引數判斷出使用者想要往哪張表裡新增資料,再呼叫SQLiteDatabase的insert()方法進行新增就可以了,注意insert()方法要求返回一個能夠表示這條新增資料的URI,所以我們還需要呼叫:Uri.parse()方法來將一個內容URI解析成Uri物件,當然這個內容URI是以新增資料的id結尾的。

  5、update()方法:先獲取SQLiteDatabase例項,然後根據傳入的Uri引數判斷出使用者想要更新哪張表裡的資料,再呼叫SQLiteDatabase的update()方法進行更新就好了,受影響的行數將作為返回值返回

  6、delete()方法,先獲取SQLiteDatebase的例項,然後根據傳入的Uri引數判斷出使用者想要刪除哪張表裡的資料,再呼叫SQLiteDatebase的delete()方法進行刪除就好了,被刪除的行數將作為返回值返回。

  7、getType()方法:這個方法中的程式碼完全是按照上一節中介紹的格式規則編寫的,這裡就不多說了。

第三步:在AndroidManifest中註冊內容提供器:

  注意:內容提供器一定要在AndroidManifest檔案中註冊後才能使用,這裡我們使用的是Android Studio的快捷方式建立的內容提供器,因此註冊這一步已經被自動完成了。

  我們看到,<application>標籤中出現了一個新的標籤:<provider>,使用這個標籤來對DatebaseProvider這個內容提供器進行註冊,android:name屬性指定了DatebaseProvider的類名,android:authorities屬性指定了DatebaseProvider的authority,而enabled和exported屬性則是根據我們剛才勾選的狀態自動生成的,這裡表示允許DatebaseProvider被其他應用程式進行訪問。

第四步:先將DatebaseTest程式從模擬器中刪除,以防止上一章中產生的遺留資料對我們造成干擾。然後重新在模擬器上執行程式,重新安裝程式,點選Create Book按鈕,先建立表,然後關閉DatebaseTest這個專案,重新建立一個新的專案:ProviderTest,我們將通過這個程式去訪問DatebaseTest中的資料。

第五步:在activity_main.xml檔案中新增四個按鈕:新增、查詢、更新、刪除。

第六步:在MainActivity中實現按鈕的邏輯功能

 

 程式碼分析:

  1、新增資料:首先呼叫:Uri.parse()方法將一個內容URI解析成Uri物件,然後把要新增的資料都存放到ContentValues物件中,接著呼叫ContentResolver的:insert()方法執行新增操作就可以了。注意:insert()方法會返回一個Uri物件,這個物件中包含了新增資料的id,我們通過:getPathSegments()方法將這個id取出,稍後會用到它。

  2、查詢資料:同樣先呼叫Uri.parse()方法將一個內容URI解析成Uri物件,然後呼叫ContentResolver的:query()方法去查詢資料,查詢的結果當然還是存放在Cursor物件中,之後對Cursor進行遍歷,從中取出查詢結果,並一一打印出來。

  3、更新資料:先將內容URI解析成Uri物件,然後把想要更新的資料存放到ContentValues物件中,在呼叫ContentResolver的:update()方法執行更新操作就可以了,注意這裡我們為了不想讓Book表中的其他行受到影響,在呼叫:Uri.parse()方法時,給內容URI的尾部增加了一個id,而這個id正是新增資料時所返回的,這就是我們只希望更新剛剛新增的那條資料,Book表中的其他行都不會受到影響。

  4、刪除資料的時候,也是 使用同樣的方法解析了一個以id結尾的內容URI,然後呼叫ContentResolver的:delete()方法執行刪除操作就可以了,由於我們在內容URI裡指定了一個id,因此只會刪掉擁有相應id的那行資料,Book表中的其他資料不會受到影響。

第七步:執行程式

  1、點選:Add to Book按鈕新增資料,然後再點選:Query from Book來檢查是否新增成功

 

  2、更新資料