本文和接下來的幾篇文章為閱讀郭霖先生所著《第一行代碼:Android(篇第2版)》的學習筆記,按照書中的內容順序進行記錄,書中的Demo本人全部都做過了。 每一章節本人都做了詳細的記錄,以下是我學習記錄(包含大量書中內容的整理和自己在學習中遇到的各種bug及解決方案),方便以後閱讀和查閱。最後,感激 ...
本文和接下來的幾篇文章為閱讀郭霖先生所著《第一行代碼:Android(篇第2版)》的學習筆記,按照書中的內容順序進行記錄,書中的Demo本人全部都做過了。
每一章節本人都做了詳細的記錄,以下是我學習記錄(包含大量書中內容的整理和自己在學習中遇到的各種bug及解決方案),方便以後閱讀和查閱。最後,感激感激郭霖先生提供這麼好的書籍。
第7章 跨程式共用數據——探究內容提供器
在上一章中我們學了Android數據持久化的技術,包括文件存儲、SharedPreferences存儲以及資料庫存儲。使用這些持久化技術所保存的數據都只能在當前應用程式中訪問。
雖然文件和SharedPreferences存儲中提供了MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE這兩種操作模式,用於供給其他的應用程式訪問當前應用的數據,但這兩種模式在Android 4.2版本中都已被廢棄了。為什麼呢?因為Android官方已經不再推薦使用這種方式來實現跨程式數據共用的功能,而是應該使用更加安全可靠的內容提供器技術。
為什麼要將我們程式中的數據共用給其他程式呢?
當然,這個是要視情況而定的,比如說賬號和密碼這樣的隱私數據顯然是不能共用給其他程式的,不過一些可以讓其他程式進行二次開發的基礎性數據,我們還是可以選擇將其共用的。例如系統的電話簿程式,它的資料庫中保存了很多的聯繫人信息,如果這些數據都不允許第三方的程式進行訪問的話,恐怕很多應用的功能都要大打折扣了。除了電話簿之外,還有簡訊、媒體庫等程式都實現了跨程式數據共用的功能,而使用的技術當然就是內容提供器了,下麵我們就來對這一技術進行深入的探討。
7.1 內容提供器簡介
內容提供器(Content Provider)主要用於在不同的應用程式之間實現數據共用的功能,它提供了一套完整的機制,允許一個程式訪問另一個程式中的數據,同時還能保證被訪數據的安全性。
目前,使用內容提供器是Android實現跨程式共用數據的標準方式。不同於文件存儲和SharedPreferences存儲中的兩種全局可讀寫操作模式,內容提供器可以選擇只對哪一部分數據進行共用,從而保證我們程式中的隱私數據不會有泄漏的風險。
不過在正式開始學習內容提供器之前,我們需要先掌握另外一個非常重要的知識——Android運行時許可權,因為待會的內容提供器示例中會使用到運行時許可權的功能。當然不光是內容提供器,以後我們的開發過程中也會經常使用到運行時許可權,因此你必須能夠牢牢掌握它才行。
7.2 運行時許可權
Android的許可權機制,從系統的第一個版本開始就已經存在了。但其實之前Android的許可權機制在保護用戶安全和隱私等方面起到的作用比較有限,尤其是一些大家都離不開的常用軟體,非常容易“店大欺客”。為此,Android開發團隊在Android 6.0系統中引用了運行時許可權這個功能,從而更好地保護了用戶的安全和隱私,那麼本節我們就來詳細學習一下這個6.0系統中引入的新特性。
7.2.1 Android許可權機制詳解
首先來回顧一下過去Android的許可權機制是什麼樣的。在第5章寫BroadcastTest項目的時候第一次接觸了Android許可權相關的內容,當時為了要訪問系統的網路狀態以及監聽開機廣播,於是在AndroidManifest.xml文件中添加了這樣兩句許可權聲明:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.zhouzhou.broadcasttest">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
...
</manifest>
因為訪問系統的網路狀態以及監聽開機廣播涉及了用戶設備的安全性,因此必須在AndroidManifest.xml中加入許可權聲明,否則我們的程式就會崩潰。
加入了這兩句許可權聲明後,對於用戶來說到底有什麼影響呢?為什麼這樣就可以保護用戶設備的安全性了呢?
其實用戶主要在以下兩個方面得到了保護,一方面,如果用戶在低於6.0系統的設備上安裝該程式,會在安裝界面給出下圖所示的提醒。這樣用戶就可以清楚地知曉該程式一共申請了哪些許可權,從而決定是否要安裝這個程式。
另一方面,用戶可以隨時在應用程式管理界面查看任意一個程式的許可權申請情況,以此保證應用程式不會出現各種濫用許可權的情況。
這種許可權機制的設計思路其實非常簡單,就是用戶如果認可你所申請的許可權,那麼就會安裝你的程式,如果不認可你所申請的許可權,那麼拒絕安裝就可以了。
但是理想是美好的,現實卻很殘酷,因為很多我們所離不開的常用軟體普遍存在著濫用許可權的情況,不管到底用不用得到,反正先把許可權申請了再說。比如說微信所申請的許可權列表如圖所示:
這隻是微信所申請的一半左右的許可權,因為許可權太多一屏截不下來。其中有一些許可權我並不認可,比如微信為什麼要讀取我手機的簡訊和彩信?但是我不認可又能怎樣,難道我拒絕安裝微信?
Android開發團隊當然也意識到了這個問題,於是在6.0系統中加入了運行時許可權功能。也就是說,用戶不需要在安裝軟體的時候一次性授權所有申請的許可權,而是可以在軟體的使用過程中再對某一項許可權申請進行授權。比如說一款相機應用在運行時申請了地理位置定位許可權,就算我拒絕了這個許可權,但是我應該仍然可以使用這個應用的其他功能,而不是像之前那樣直接無法安裝它。
當然,並不是所有許可權都需要在運行時申請,對於用戶來說,不停地授權也很煩瑣。
Android現在將所有的許可權歸成了兩類,一類是普通許可權,一類是危險許可權。準確地講,其實還有第三類特殊許可權,不過這種許可權使用得很少,因此不在本書的討論範圍之內。
- 普通許可權:指的是那些不會直接威脅到用戶的安全和隱私的許可權,對於這部分許可權申請,系統會自動幫我們進行授權,而不需要用戶再去手動操作了,比如在BroadcastTest項目中申請的兩個許可權就是普通許可權。
- 危險許可權:則表示那些可能會觸及用戶隱私或者對設備安全性造成影響的許可權,如獲取設備聯繫人信息、定位設備的地理位置等,對於這部分許可權申請,必須要由用戶手動點擊授權才可以,否則程式就無法使用相應的功能。
但是,Android中有一共有上百種許可權,我們怎麼從中區分哪些是普通許可權,哪些是危險許可權呢?其實並沒有那麼難,因為危險許可權總共就那麼幾個,除了危險許可權之外,剩餘的就都是普通許可權了。下表列出了Android中所有的危險許可權,一共是9組24個許可權。
你並不需要瞭解表格中每個許可權的作用,只要把它當成一個參照表來查看就行了。每當要使用一個許可權時,可以先到這張表中來查一下,如果是屬於這張表中的許可權,那麼就需要進行運行時許可權處理,如果不在這張表中,那麼只需要在AndroidManifest.xml文件中添加一下許可權聲明就可以了。
另外註意一下,表格中每個危險許可權都屬於一個許可權組,我們在進行運行時許可權處理時使用的是許可權名,但是用戶一旦同意授權了,那麼該許可權所對應的許可權組中所有的其他許可權也會同時被授權。訪問:http://developer.android.google.cn/reference/android/Manifest.permission.html可以查看Android系統中完整的許可權列表。
7.2.2 在程式運行時申請許可權
新建一個RuntimePermissionTest項目,在這個項目的基礎上來學習運行時許可權的使用方法。
簡單起見就使用CALL_PHONE這個許可權來作為本小節中的示例。CALL_PHONE這個許可權是編寫撥打電話功能的時候需要聲明的,因為撥打電話會涉及用戶手機的資費問題,因而被列為了危險許可權。
在Android 6.0系統出現之前,撥打電話功能的實現其實非常簡單,修改activity_main.xml佈局文件,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/make_call"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Make Call"/>
</LinearLayout>
在佈局文件中只是定義了一個按鈕,當點擊按鈕時就去觸發撥打電話的邏輯。接著修改MainActivity中的代碼,如下所示:
package com.zhouzhou.runtimepermissiontest;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button makeCall = (Button) findViewById(R.id.make_call);
makeCall.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
try {
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
} catch (SecurityException e) {
e.printStackTrace();
}
}
});
}
}
在按鈕的點擊事件中,構建了一個隱式Intent , Intent的action指定為Intent.ACTION_ CALL,這是一個系統內置的打電話的動作,然後在data部分指定了協議是tel,號碼是10086。
(在2.3.3小節中就已經見過了,當時指定的action是Intent.ACTION_DIAL,表示打開撥號界面,這個是不需要聲明許可權的,而Intent.ACTION_ CALL則可以直接撥打電話,因此必須聲明許可權。)
另外為了防止程式崩潰,我們將所有操作都放在了異常捕獲代碼塊當中。那麼接下來修改AndroidManifest.xml文件,在其中聲明如下許可權:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.zhouzhou.runtimepermissiontest">
<uses-permission android:name="android.permission.CALL_PHONE"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.RuntimePermissionTest">
...
</manifest>
這樣我們就將撥打電話的功能成功實現了,並且在低於Android 6.0系統的手機上都是可以正常運行的,但是如果我們在6.0或者更高版本系統的手機上運行,點擊Make Call按鈕就沒有任何效果,這時觀察logcat中的列印日誌,你會看到如圖:
錯誤信息中提醒我們“Permission Denial”,可以看出,是由於許可權被禁止所導致的,因為6.0及以上系統在使用危險許可權時都必須進行運行時許可權處理。那麼下麵我們就來嘗試修複這個問題,修改MainActivity中的代碼,如下所示:
package com.zhouzhou.runtimepermissiontest;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button makeCall = (Button) findViewById(R.id.make_call);
makeCall.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(MainActivity.this,new String[]{ Manifest.permission.CALL_PHONE },1);
} else {
call();
}
}
});
}
private void call() {
try {
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
} catch (SecurityException e) {
e.printStackTrace();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case 1:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
call();
} else {
Toast.makeText(this,"You denied the permission",Toast.LENGTH_SHORT).show();
}
break;
default:
}
}
}
上面的代碼將運行時許可權的完整流程都覆蓋了,下麵我們來具體解析一下。
運行時許可權的核心就是在程式運行過程中由用戶授權我們去執行某些危險操作,程式是不可以擅自做主去執行這些危險操作的。
因此,第一步就是要先判斷用戶是不是已經給過我們授權了。
藉助的是ContextCompat.checkSelfPermission()方法。checkSelfPermission()方法接收兩個參數,第一個參數是Context,第二個參數是具體的許可權名,比如打電話的許可權名就是Manifest.permission.CALL_PHONE,然後我們使用方法的返回值和PackageManager. PERMISSION_GRANTED做比較,相等就說明用戶已經授權,不等就表示用戶沒有授權。
- 如果已經授權的話就簡單了,直接去執行撥打電話的邏輯操作就可以了,這裡我們把撥打電話的邏輯封裝到了call()方法當中。
- 如果沒有授權的話,則需要調用ActivityCompat. requestPermissions()方法來向用戶申請授權,requestPermissions()方法接收3個參數,第一個參數要求是Activity的實例,第二個參數是一個String數組,我們把要申請的許可權名放在數組中即可,第三個參數是請求碼,只要是唯一值就可以了,這裡傳入了1。
調用完了requestPermissions()方法之後,系統會彈出一個許可權申請的對話框,然後用戶可以選擇同意或拒絕我們的許可權申請,不論是哪種結果,最終都會回調到onRequestPermissionsResult()方法中,而授權的結果則會封裝在grantResults參數當中。這裡我們只需要判斷一下最後的授權結果,如果用戶同意的話就調用call()方法來撥打電話,如果用戶拒絕的話我們只能放棄操作,並且彈出一條失敗提示。
現在重新運行一下程式,並點擊Make Call按鈕,效果如圖:
由於用戶還沒有授權過我們撥打電話許可權,因此第一次運行會彈出這樣一個許可權申請的對話框,用戶可以選擇同意或者拒絕,比如說這裡點擊了DENY,結果如圖:
由於用戶沒有同意授權,我們只能彈出一個操作失敗的提示。下麵我們再次點擊Make Call按鈕,仍然會彈出許可權申請的對話框,這次點擊ALLOW,結果如圖:
可以看到,這次我們就成功進入到撥打電話界面了,並且由於用戶已經完成了授權操作,之後再點擊Make Call按鈕就不會再彈出許可權申請對話框了,而是可以直接撥打電話。
用戶隨時都可以將授予程式的危險許可權進行關閉,進入Settings → Apps→ RuntimePermissionTest → Permissions,在這裡我們就可以對任何授予過的危險許可權進行關閉了,界面如圖:
7.3 訪問其他程式中的數據
內容提供器的用法一般有兩種:
- 一種是使用現有的內容提供器來讀取和操作相應程式中的數據。
- 一種是創建自己的內容提供器給我們程式的數據提供外部訪問介面。
使用現有的內容提供器:如果一個應用程式通過內容提供器對其數據提供了外部訪問介面,那麼任何其他的應用程式就都可以對這部分數據進行訪問。
Android系統中自帶的電話簿、簡訊、媒體庫等程式都提供了類似的訪問介面,這就使得第三方應用程式可以充分地利用這部分數據來實現更好的功能。下麵我們就來看一看,內容提供器到底是如何使用的。
7.3.1 ContentResolver的基本用法
對於每一個應用程式來說,如果想要訪問內容提供器中共用的數據,就一定要藉助ContentResolver類,可以通過Context中的getContentResolver()方法獲取到該類的實例。
ContentResolver中提供了一系列的方法用於對數據進行CRUD操作,其中insert()方法用於添加數據,update()方法用於更新數據,delete()方法用於刪除數據,query()方法用於查詢數據。
不同於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可以非常清楚地表達出我們想要訪問哪個程式中哪張表裡的數據。也正是因此,ContentResolver中的增刪改查方法才都接收Uri對象作為參數,因為如果使用表名的話,系統將無法得知我們期望訪問的是哪個應用程式里的表。在得到了內容URI字元串之後,我們還需要將它解析成Uri對象才可以作為參數傳入。解析的方法也相當簡單,代碼如下所示:
Uri uri = Uri.parse("content://com.example.app.provider/table1")
只需要調用Uri.parse()方法,就可以將內容URI字元串解析成Uri對象了。現在我們就可以使用這個Uri對象來查詢table1表中的數據了,代碼如下所示:
Cursor cursor = getContentResolver().query(
uri,
projection,
selection,
selectionArgs,
sortOrder);
這些參數和SQLiteDatabase中query()方法里的參數很像,但總體來說要簡單一些,畢竟這是在訪問其他程式中的數據,沒必要構建過於複雜的查詢語句。下表對使用到的這部分參數進行了詳細的解釋。
查詢完成後返回的仍然是一個Cursor對象,這時就可以將數據從Cursor對象中逐個讀取出來了。讀取的思路仍然是通過移動游標的位置來遍歷Cursor的所有行,然後再取出每一行中相應列的數據,代碼如下所示:
if (cursor != null) {
while (cursor.moveToNext()) {
String column1 = cursor.getString(cursor.getColumnIndex("column1"));
int column2 = cursor.getInt(cursor.getColumnIndex("column2"));
}
cursor.close();
}
掌握了最難的查詢操作,剩下的增加、修改、刪除操作就更不在話下了。我們先來看看如何向table1表中添加一條數據,代碼如下所示:
ContentValues values = new ContentValues();
values.put("column1","text");
values.put("column2",1);
getContentResolver().insert(uri,values);
可以看到,仍然是將待添加的數據組裝到ContentValues中,然後調用ContentResolver的insert()方法,將Uri和ContentValues作為參數傳入即可。現在如果我們想要更新這條新添加的數據,把column1的值清空,可以藉助ContentResolver的update()方法實現,代碼如下所示:
ContentValues values = new ContentValues();
values.put("column1","");
getContentResolver().update(uri,values,"column1 = ? and column2 = ? ",new String[] {"text","1"});
註意上述代碼使用了selection和selectionArgs參數來對想要更新的數據進行約束,以防止所有的行都會受影響。最後,可以調用ContentResolver的delete()方法將這條數據刪除掉,代碼如下所示:
getContentResolver().delete(uri,"column2 = ? ",new String[] {"1"});
到這裡為止,我們就把ContentResolver中的增刪改查方法全部學完了。那麼接下來,就利用目前所學,看一看如何讀取系統電話簿中的聯繫人信息。
7.3.2 讀取系統聯繫人
由於我們之前一直使用的都是模擬器,電話簿裡面並沒有聯繫人存在,所以現在需要自己手動添加幾個,以便稍後進行讀取。打開電話簿程式,界面如圖:
目前電話簿里是沒有任何聯繫人的,我們可以通過點擊ADD ACONTACT按鈕來對聯繫人進行創建。這裡就先創建兩個聯繫人吧,分別填入他們的姓名和手機號:
這樣準備工作就做好了,現在新建一個ContactsTest項目。首先還是來編寫一下佈局文件,這裡我們希望讀取出來的聯繫人信息能夠在ListView中顯示,因此,修改activity_main.xml中的代碼,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/contacts_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
LinearLayout里就只放置了一個ListView。這裡使用ListView而不是RecyclerView,是因為我們要將關註的重點放在讀取系統聯繫人上面,如果使用RecyclerView的話,代碼偏多,會容易讓我們找不著重點。接著修改MainActivity中的代碼,如下所示:
package com.zhouzhou.contactstest;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
ArrayAdapter<String> adapter;
List<String> contactsList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListView contactsView = (ListView) findViewById(R.id.contacts_view);
adapter = new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,contactsList);
contactsView.setAdapter(adapter);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,new String[] { Manifest.permission.READ_CONTACTS },1);
} else {
readContacts();
}
}
private void readContacts() {
Cursor cursor = null;
try {
//查詢聯繫人數據
cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,null,null,null,null);
if (cursor != null) {
while (cursor.moveToNext()) {
//獲取聯繫人姓名
@SuppressLint("Range") String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
//獲取聯繫人手機號
@SuppressLint("Range") String number = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
contactsList.add(displayName + "\n" + number);
}
adapter.notifyDataSetChanged();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (cursor != null) {
cursor.close();
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case 1:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
readContacts();
} else {
Toast.makeText(this,"You denied the permission",Toast.LENGTH_SHORT).show();
}
break;
default:
}
}
}
在onCreate()方法中,我們首先獲取了ListView控制項的實例,並給它設置好了適配器,然後開始調用運行時許可權的處理邏輯,因為READ_CONTACTS許可權是屬於危險許可權的。這裡在用戶授權之後調用readContacts()方法來讀取系統聯繫人信息。
下麵重點看一下readContacts()方法,這裡使用了ContentResolver的query()方法來查詢系統的聯繫人數據。
不過,傳入的Uri參數為什麼沒有調用Uri.parse()方法去解析一個內容URI字元串呢?
這是因為ContactsContract.CommonDataKinds.Phone類已經幫我們做好了封裝,提供了一個CONTENT_URI常量,而這個常量就是使用Uri.parse()方法解析出來的結果。接著我們對Cursor對象進行遍歷,將聯繫人姓名和手機號這些數據逐個取出,聯繫人姓名這一列對應的常量是ContactsContract.CommonDataKinds. Phone.DISPLAY_NAME,聯繫人手機號這一列對應的常量是ContactsContract.CommonData-Kinds.Phone.NUMBER。兩個數據都取出之後,將它們進行拼接,並且在中間加上換行符,然後將拼接後的數據添加到ListView的數據源里,並通知刷新一下ListView。最後千萬不要忘記將Cursor對象關閉掉。
讀取系統聯繫人的許可權千萬不能忘記聲明。修改AndroidManifest.xml中的代碼,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.zhouzhou.contactstest">
<uses-permission android:name="android.permission.READ_CONTACTS"/>
...
</manifest>
加入了android.permission.READ_CONTACTS許可權,這樣我們的程式就可以訪問到系統的聯繫人數據了。來運行一下程式吧,效果如圖:
首先彈出了申請訪問聯繫人許可權的對話框,我們點擊DENY,然後結果如圖:
點擊ALLOW,剛剛添加的兩個聯繫人的數據都成功讀取出來了!這說明跨程式訪問數據的功能確實是實現了。結果如圖:
7.4 創建自己的內容提供器
上一節當中,思路還是非常簡單的,只需要獲取到該應用程式的內容URI,然後藉助ContentResolver進行CRUD操作就可以了。
那些提供外部訪問介面的應用程式都是如何實現這種功能的呢?它們又是怎樣保證數據的安全性,使得隱私數據不會泄漏出去?學習完本節的知識後,疑惑將會被一一解開。
7.4.1 創建內容提供器的步驟
前面已經提到過,如果想要實現跨程式共用數據的功能,官方推薦的方式就是使用內容提供器,可以通過新建一個類去繼承ContentProvider的方式來創建一個自己的內容提供器。ContentProvider類中有6個抽象方法,我們在使用子類繼承它的時候,需要將這6個方法全部重寫。新建MyProvider繼承自ContentProvider,代碼如下所示:
package com.zhouzhou.contactstest;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class MyProvider extends ContentProvider {
@Override
public boolean onCreate() {
return false;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] strings, @Nullable String s, @Nullable String[] strings1, @Nullable String s1) {
return null;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
return null;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String s, @Nullable String[] strings) {
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues contentValues, @Nullable String s, @Nullable String[] strings) {
return 0;
}
}
- onCreate()
初始化內容提供器的時候調用。通常會在這裡完成對資料庫的創建和升級等操作,返回true表示內容提供器初始化成功,返回false則表示失敗。
- query()
從內容提供器中查詢數據。使用uri參數來確定查詢哪張表,projection參數用於確定查詢哪些列,selection和selectionArgs參數用於約束查詢哪些行,sortOrder參數用於對結果進行排序,查詢的結果存放在Cursor對象中返回。
- insert()
向內容提供器中添加一條數據。使用uri參數來確定要添加到的表,待添加的數據保存在values參數中。添加完成後,返回一個用於表示這條新記錄的URI。
- update()
更新內容提供器中已有的數據。使用uri參數來確定更新哪一張表中的數據,新數據保存在values參數中,selection和selectionArgs參數用於約束更新哪些行,受影響的行數將作為返回值返回。
- delete()
從內容提供器中刪除數據。使用uri參數來確定刪除哪一張表中的數據,selection和selectionArgs參數用於約束刪除哪些行,被刪除的行數將作為返回值返回。
- getType()
根據傳入的內容URI來返回相應的MIME類型。
可以看到,幾乎每一個方法都會帶有Uri這個參數,這個參數也正是調用ContentResolver的增刪改查方法時傳遞過來的。
而現在,我們需要對傳入的Uri參數進行解析,從中分析出調用方期望訪問的表和數據。回顧一下,一個標準的內容URI寫法是這樣的:
content://com.example.app.provider/table1
這就表示調用方期望訪問的是com.example.app這個應用的table1表中的數據。除此之外,我們還可以在這個內容URI的後面加上一個id,如下所示:
content://com.example.app.provider/table1/1
內容URI的格式主要就只有以上兩種,以路徑結尾就表示期望訪問該表中所有的數據,以id結尾就表示期望訪問該表中擁有相應id的數據。我們可以使用通配符的方式來分別匹配這兩種格式的內容URI,規則如下。
- 星號(*) 表示匹配任意長度的任意字元。
- 井號(#) 表示匹配任意長度的數字。
所以,一個能夠匹配任意表的內容URI格式就可以寫成:
content://com.example.app.provider/*
而一個能夠匹配table1表中任意一行數據的內容URI格式就可以寫成:
content://com.example.app.provider/table1/#
接著,再藉助UriMatcher這個類就可以輕鬆地實現匹配內容URI的功能。UriMatcher中提供了一個addURI()方法,這個方法接收3個參數,可以分別把authority、path和一個自定義代碼傳進去。這樣,當調用UriMatcher的match()方法時,就可以將一個Uri對象傳入,返回值是某個能夠匹配這個Uri對象所對應的自定義代碼,利用這個代碼,我們就可以判斷出調用方期望訪問的是哪張表中的數據了。修改MyProvider中的代碼,如下所示:
public class MyProvider extends ContentProvider {
public static final int TABLE1_DIR = 0;
public static final int TABLE1_ITEM = 1;
public static final int TABLE2_DIR = 2;
public static final int TABLE2_ITEM = 3;
private static UriMatcher uriMatcher;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI("com.zhouzhou.app.provider","table1",TABLE1_DIR);
uriMatcher.addURI("com.zhouzhou.app.provider","table1/#",TABLE1_ITEM);
uriMatcher.addURI("com.zhouzhou.app.provider","table2",TABLE2_DIR);
uriMatcher.addURI("com.zhouzhou.app.provider","table2/#",TABLE2_ITEM);
}
...
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] strings, @Nullable String s, @Nullable String[] strings1, @Nullable String s1) {
switch (uriMatcher.match(uri)) {
case TABLE1_DIR:
//查詢table1表中的所有數據
break;
case TABLE1_ITEM:
//查詢table1表中的單條數據
break;
case TABLE2_DIR:
//查詢table2表中的所有數據
break;
case TABLE2_ITEM:
//查詢table2表中的單條數據
break;
default:
break;
}
...
}
...
}
可以看到,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個部分做瞭如下格式規定。
❑ 必須以vnd開頭。
❑ 如果內容URI以路徑結尾,則後接android.cursor.dir/,如果內容URI以id結尾,則後接android.cursor.item/。
❑ 最後接上vnd.
所以,對於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()方法中的邏輯,代碼如下所示:
...
public class MyProvider extends ContentProvider {
...
@Nullable
@Override
public String getType(@NonNull Uri uri) {
switch (uriMatcher.match(uri)) {
case TABLE1_DIR:
return "vnd.android.cursor.dir/vnd.com.zhouzhou.app.provider.table1";
case TABLE1_ITEM:
return "vnd.android.cursor.item/vnd.com.zhouzhou.app.provider.table1";
case TABLE2_DIR:
return "vnd.android.cursor.dir/vnd.com.zhouzhou.app.provider.table2";
case TABLE2_ITEM:
return "vnd.android.cursor.item/vnd.com.zhouzhou.app.provider.table2";
default:
break;
}
return null;
}
}
到這裡,一個完整的內容提供器就創建完成了,現在任何一個應用程式都可以使用ContentResolver來訪問我們程式中的數據。
那麼,如何才能保證隱私數據不會泄漏出去呢?
內容提供器的良好機制使得這個問題在不知不覺中已經被解決了。因為所有的CRUD操作都一定要匹配到相應的內容URI格式才能進行的,而我們不可能向UriMatcher中添加隱私數據的URI,所以這部分數據根本無法被外部程式訪問到,安全問題也就不存在了。實戰一下,真正體驗一回跨程式數據共用的功能。
7.4.2 實現跨程式數據共用
- 在上一章中DatabaseTest項目的基礎上繼續開發,通過內容提供器來給它加入外部訪問介面。
- 打開DatabaseTest項目,首先將MyDatabaseHelper中使用Toast彈出創建資料庫成功的提示去除掉,因為跨程式訪問時我們不能直接使用Toast。
- 然後,創建一個內容提供器,右擊com.zhouzhou.databasetest包→New→Other→Content Provider。
會彈出如圖所示的視窗:
- 將內容提供器命名為DatabaseProvider。
- authority指定為com.zhouzhou. databasetest.provider。
- Exported屬性表示是否允許外部程式訪問我們的內容提供器。
- Enabled屬性表示是否啟用這個內容提供器。將兩個屬性都勾中,點擊Finish完成創建。
接著我們修改DatabaseProvider中的代碼,如下所示:
package com.zhouzhou.databasetest;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
public class DatabaseProvider extends ContentProvider {
public static final int BOOK_DIR = 0;
public static final int BOOK_ITEM = 1;
public static final int CATEGORY_DIR = 2;
public static final int CATEGORY_ITEM = 3;
public static final String AUTHORITY = "com.zhouzhou.databasetest.provider";
private static UriMatcher uriMatcher;
private MyDatabaseHelper dbHelper;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(AUTHORITY,"book",BOOK_DIR);
uriMatcher.addURI(AUTHORITY,"book/#",BOOK_ITEM);
uriMatcher.addURI(AUTHORITY,"category",CATEGORY_DIR);
uriMatcher.addURI(AUTHORITY,"category/#",CATEGORY_ITEM);
}
public DatabaseProvider() {
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// Implement this to handle requests to delete one or more rows.
//throw new UnsupportedOperationException("Not yet implemented");
//刪除數據
SQLiteDatabase db = dbHelper.getWritableDatabase();
int deleteRows = 0;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
deleteRows = db.delete("Book",selection,selectionArgs);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
deleteRows = db.delete("Book","id = ?",new String[] { bookId });
break;
case CATEGORY_DIR:
deleteRows = db.delete("Category",selection,selectionArgs);
break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
deleteRows = db.delete("Category","id = ?",new String[] { categoryId });
break;
default:
break;
}
return deleteRows;
}
@Override
public String getType(Uri uri) {
// TODO: Implement this to handle requests for the MIME type of the data
// at the given URI.
//throw new UnsupportedOperationException("Not yet implemented");
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
return "vnd.android.cursor.dir/vnd.com.zhouzhou.databasetest.provider.book";
case BOOK_ITEM:
return "vnd.android.cursor.item/vnd.com.zhouzhou.database.provider.book";
case CATEGORY_DIR:
return "vnd.android.cursor.dir/vnd.com.zhouzhou.databasetest.provider.category";
case CATEGORY_ITEM:
return "vnd.android.cursor.item/vnd.com.zhouzhou.databasetest.provider.category";
}
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
// TODO: Implement this to handle requests to insert a new row.
//throw new UnsupportedOperationException("Not yet implemented");
//添加數據
SQLiteDatabase db = dbHelper.getWritableDatabase();
Uri uriReturn = null;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
case BOOK_ITEM:
long newBookId = db.insert("Book",null,values);
uriReturn = Uri.parse("content://" + AUTHORITY + "/book/" + newBookId);
break;
case CATEGORY_DIR:
case CATEGORY_ITEM:
long newCategoryId = db.insert("Category",null,values);
uriReturn = Uri.parse("content://" + AUTHORITY + "/category/" + newCategoryId);
break;
default:
break;
}
return uriReturn;
}
@Override
public boolean onCreate() {
// TODO: Implement this to initialize your content provider on startup.
dbHelper = new MyDatabaseHelper(getContext(),"BookStore.db",null,2);
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// TODO: Implement this to handle query requests from clients.
//throw new UnsupportedOperationException("Not yet implemented");
//查詢數據
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor = null;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
cursor = db.query("Book",projection,selection,selectionArgs,null,null,sortOrder);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
cursor = db.query("Book",projection,"id = ?",new String[]{ bookId },null,null,sortOrder);
break;
case CATEGORY_DIR:
cursor = db.query("Category",projection,selection,selectionArgs,null,null,sortOrder);
break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
cursor = db.query("Category",projection,"id = ?",new String[]{ categoryId },null,null,sortOrder);
break;
default:
break;
}
return cursor;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// TODO: Implement this to handle requests to update one or more rows.
//throw new UnsupportedOperationException("Not yet implemented");
//更新數據
SQLiteDatabase db = dbHelper.getWritableDatabase();
int updateRows = 0;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
updateRows = db.update("Book",values,selection,selectionArgs);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
updateRows = db.update("Book",values,"id = ?",new String[] { bookId });
break;
case CATEGORY_DIR:
updateRows = db.update("Category",values,selection,selectionArgs);
break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
updateRows = db.update("Category",values,"id = ?",new String[] { categoryId });
break;
default:
break;
}
return updateRows;
}
}
首先在類的一開始,同樣是定義了4個常量,分別用於表示訪問Book表中的所有數據、訪問Book表中的單條數據、訪問Category表中的所有數據和訪問Category表中的單條數據。
然後,在靜態代碼塊里對UriMatcher進行了初始化操作,將期望匹配的幾種URI格式添加了進去。
- onCreate()方法,這個方法的代碼很短,就是創建了一個MyDatabaseHelper的實例,然後返回true表示內容提供器初始化成功,這時資料庫就已經完成了創建或升級操作。
- query()方法,在這個方法中先獲取到了SQLiteDatabase的實例,然後根據傳入的Uri參數判斷出用戶想要訪問哪張表,再調用SQLiteDatabase的query()進行查詢,並將Cursor對象返回就好了。
註意,當訪問單條數據的時候有一個細節,這裡調用了Uri對象的getPathSegments()方法,它會將內容URI許可權之後的部分以“/”符號進行分割,並把分割後的結果放入到一個字元串列表中,那這個列表的第0個位置存放的就是路徑,第1個位置存放的就是id了。得到了id之後,再通過selection和selectionArgs參數進行約束,就實現了查詢單條數據的功能。
- insert()方法,同樣它也是先獲取到了SQLiteDatabase的實例,然後根據傳入的Uri參數判斷出用戶想要往哪張表裡添加數據,再調用SQLiteDatabase的insert()方法進行添加就可以了。
註意insert()方法,要求返回一個能夠表示這條新增數據的URI,所以我們還需要調用Uri.parse()方法來將一個內容URI解析成Uri對象,當然這個內容URI是以新增數據的id結尾的。
- update()方法,也是先獲取SQLiteDatabase的實例,然後根據傳入的Uri參數判斷出用戶想要更新哪張表裡的數據,再調用SQLiteDatabase的update()方法進行更新就好了,受影響的行數將作為返回值返回。
- delete()方法,仍然是先獲取到SQLiteDatabase的實例,然後根據傳入的Uri參數判斷出用戶想要刪除哪張表裡的數據,再調用SQLiteDatabase的delete()方法進行刪除就好了,被刪除的行數將作為返回值返回。
- getType()方法,這個方法中的代碼完全是按照上一節中介紹的格式規則編寫的,這樣我們就將內容提供器中的代碼全部編寫完了。
註意,內容提供器一定要在AndroidManifest.xml文件中註冊才可以使用。不過幸運的是,由於我們是使用Android Studio的快捷方式創建的內容提供器,因此註冊這一步已經被自動完成了。打開AndroidManifest.xml文件瞧一瞧,代碼如下所示:
<provider
android:name=".DatabaseProvider"
android:authorities="com.zhouzhou.databasetest.provider"
android:enabled="true"
android:exported="true"></provider>
可以看到,<application>
標簽內出現了一個新的標簽<provider>
,使用它來對DatabaseProvider這個內容提供器進行註冊。android:name
屬性指定了DatabaseProvider的類名,android:authorities
屬性指定了DatabaseProvider的authority,而enabled和exported屬性則是根據剛纔勾選的狀態自動生成的,這裡表示允許DatabaseProvider被其他應用程式進行訪問。
現在DatabaseTest這個項目就已經擁有了跨程式共用數據的功能了。
- 首先,需要將DatabaseTest程式從模擬器中刪除掉,以防止上一章中產生的遺留數據對我們造成干擾。
- 然後,運行一下項目,將DatabaseTest程式重新安裝在模擬器上了。
- 接著,關閉掉DatabaseTest這個項目,並創建一個新項目ProviderTest,我們就將通過這個程式去訪問DatabaseTest中的數據。
還是先來編寫一下佈局文件吧,修改activity_main.xml中的代碼,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/add_data"
android:text="Add To Book"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/query_data"
android:text="Query From Book"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/update_data"
android:text="Update Book"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/delete_data"
android:text="Delete From Book"/>
</LinearLayout>
佈局文件很簡單,裡面放置了4個按鈕,分別用於添加、查詢、修改和刪除數據。然後修改MainActivity中的代碼,如下所示:
package com.zhouzhou.providertest;
import androidx.appcompat.app.AppCompatActivity;
import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
private String newId;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button addData = (Button) findViewById(R.id.add_data);
addData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//添加數據
Uri uri = Uri.parse("content://com.zhouzhou.databasetest.provider/book");
ContentValues values = new ContentValues();
values.put("name","A Clash of Kings");
values.put("author","George Martin");
values.put("pages","1024");
values.put("price","22.85");
Uri newUri = getContentResolver().insert(uri,values);
newId = newUri.getPathSegments().get(1);
}
});
Button queryData = (Button) findViewById(R.id.query_data);
queryData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//查詢數據
Uri uri = Uri.parse("content://com.zhouzhou.databasetest.provider/book");
Cursor cursor = getContentResolver().query(uri,null,null,null,null);
if (cursor != null) {
while (cursor.moveToNext()) {
@SuppressLint("Range") String name = cursor.getString(cursor.getColumnIndex("name"));
@SuppressLint("Range") String author = cursor.getString(cursor.getColumnIndex("author"));
@SuppressLint("Range") int pages = cursor.getInt(cursor.getColumnIndex("pages"));
@SuppressLint("Range") double price = cursor.getDouble(cursor.getColumnIndex("price"));
Log.d("MainActivity","book name is " + name);
Log.d("MainActivity","book author is " + author);
Log.d("MainActivity","book pages is " + pages);
Log.d("MainActivity","book price is " + price);
}
cursor.close();
}
}
});
Button updateData = (Button) findViewById(R.id.update_data);
updateData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//更新數據
Uri uri = Uri.parse("content://com.zhouzhou.databasetest.provider./book" + newId);
ContentValues values = new ContentValues();
values.put("name","A Storm of Swords");
values.put("pages",1216);
values.put("price","77.77");
getContentResolver().update(uri,values,null,null);
}
});
Button deleteData = (Button) findViewById(R.id.delete_data);
deleteData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//刪除數據
Uri uri = Uri.parse("content://com.zhouzhou.databasetest.book" + newId);
getContentResolver().delete(uri,null,null);
}
});
}
}
分別在這4個按鈕的點擊事件裡面處理了增刪改查的邏輯。
添加數據的時候,首先調用了Uri.parse()方法將一個內容URI解析成Uri對象,然後把要添加的數據都存放到ContentValues對象中,接著調用ContentResolver的insert()方法執行添加操作就可以了。註意insert()方法會返回一個Uri對象,這個對象中包含了新增數據的id,我們通過getPathSegments()方法將這個id取出,稍後會用到它。
查詢數據的時候,同樣是調用了Uri.parse()方法將一個內容URI解析成Uri對象,然後調用ContentResolver的query()方法去查詢數據,查詢的結果當然還是存放在Cursor對象中的。之後對Cursor進行遍歷,從中取出查詢結果,並一一列印出來。
更新數據的時候,也是先將內容URI解析成Uri對象,然後把想要更新的數據存放到ContentValues對象中,再調用ContentResolver的update()方法執行更新操作就可以了。註意這裡我們為了不想讓Book表中的其他行受到影響,在調用Uri.parse()方法時,給內容URI的尾部增加了一個id,而這個id正是添加數據時所返回的。這就表示我們只希望更新剛剛添加的那條數據,Book表中的其他行都不會受影響。
刪除數據的時候,也是使用同樣的方法解析了一個以id結尾的內容URI,然後調用ContentResolver的delete()方法執行刪除操作就可以了。由於我們在內容URI里指定了一個id,因此只會刪掉擁有相應id的那行數據,Book表中的其他數據都不會受影響。
現在,運行一下ProviderTest項目,出現幾大問題:
-
日誌報錯:Failed to find provider info for com.zhouzhou.databasetest.provider,Unknown URL content:...
原因是:因為測試用的模擬器的SDK是API 30的,該版本(Android 11)的更新中,改變了當前應用於本機其他應用進行交互的方式,由此若按照一些教程的常式去學習,會出現以上的一些訪問許可權問題。
-
啟動兩個App後,出現屏幕閃爍/黑屏
解決:參考博客:https://blog.csdn.net/qq_34727886/article/details/110951082。
刪除所有 device 模擬器,關閉AS再重新來過(解決閃爍/黑屏/屏幕有點呆/無法卸載應用的問題)),並且分別在DatabaseTest項目的AndroidManifest.xml文件的manifest中加入如下代碼:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.zhouzhou.databasetest">
<uses-permission android:name="android.permission.INTERNET" />
<permission
android:name="DatabaseProvider._READ_PERMISSION"
android:protectionLevel="normal" />
<permission
android:name="DatabaseProvider._WRITE_PERMISSION"
android:protectionLevel="normal" />
<application
...
</application>
</manifest>
在ProviderTest項目的AndroidManifest.xml文件中的manifest中加入如下代碼:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.zhouzhou.providertest">
<uses-permission android:name="DatabaseProvider._READ_PERMISSION" />
<uses-permission android:name="DatabaseProvider._WRITE_PERMISSION" />
<queries>
<package android:name="com.zhouzhou.databasetest" />
<!-- 也可以單獨指定provider -->
<!--<provider android:authorities="com.zhouzhou.databasetest.provider" />-->
</queries>
<application
...
</application>
</manifest>
修改之後再次運行,完全OK,點擊一下Add To Book按鈕,此時數據就應該已經添加到DatabaseTest程式的資料庫中了,我們可以通過點擊Query From Book按鈕來檢查一下,列印日誌如圖:
然後,點擊一下Update Book按鈕來更新數據,再點擊一下Query From Book按鈕進行檢查,結果如圖: