第七章 跨程序共享數據——探究內容提供器
數據持久化技術,包括文件存儲、SharedPreferences存儲以及數據庫存儲,都只能在當前應用程序中訪問。跨程序數據共享需要用到另一種技術——內容提供器。
7.1 內容提供器簡介
內容提供器(Content Provider)主要用於不同的應用程序之間實現數據共享的功能,同時保證被訪數據的安全性,是實現跨程序共享數據的標準方式。
7.2 運行時權限
Android權限機制作用比較有限,容易出現“店大欺客”現象。因此在Android 6.0引入運行時權限。
7.2.1 Android權限機制詳解
爲了訪問系統的網絡狀態以及監聽開機廣播,於是在AndroidManifest.xml
文件中添加了兩句權限聲明:
<use-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<use-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
因爲涉及用戶設備安全性,因此必須在該文件加入權限聲明,否則程序會崩潰。
使用該機制,用戶主要在兩方面得到了保護:
- 在低於6.0系統的設備安裝程序,在安裝界面會給出提醒,讓用戶知曉程序申請了哪些權限,從而決定是否安裝該程序。
- 用戶可以隨時在應用程序管理界面查看任意一個程序的權限申請情況,保證應用程序不會濫用權限。
該權限設計思路:用戶如果認可你所申請的權限,那就安裝,否則就拒絕安裝。
運行時權限:用戶不需要在安裝應用程序時一次性賦予所有申請權限,而是在運行的時候再對某一權限的申請進行授權。這樣使得用戶可以拒絕某一權限,但仍然可以繼續使用該應用。
Android權限分爲三類:
- 普通權限(不會直接威脅到用戶的安全和隱私的權限,如設備網絡狀態和開機自啓動等,由系統自動授權)
- 危險權限(可能會觸及用戶隱私和對設備的安全性造成影響,如設備聯繫人信息和地理位置等,由用戶手動點擊授權)
- 特殊權限(用的很少)
除了危險權限之外,剩餘的都是普通權限。下表列出了Android中的所有危險權限,一共9組24個權限。
權限組名 | 權限名 |
---|---|
CALENDAR | READ_CALENDAR WRITE_CALENDAR |
CAMERA | CAMERA |
CONTACTS | READ_CONTACTS WRITE_CONTACTS GET_ACCOUNTS |
LOCATION | ACCESS_FINE_LOCATION - ACCESS_COARSE_LOCATION - |
MICROPHONE | RECORD_AUDIO |
PHONE | READ_PHONE_STATE CALL_PHONE READ_CALL_LOG WRITE_CALL_LOG ADD_VOICEMAIL USE_SIP - PROCESS_OUTGOING_CALLS |
SENSOR | BODY_SENSORS |
SMS - | SEND_SMS RECEIVE_SMS READ_SMS RECEIVE_WAP_PUSH RECEIVE_MMS |
STORAGE | READ_EXTERNAL_STORAGE WRITE_EXTERNAL_STORAGE |
7.2.2 在程序運行時申請權限
新建項目RuntimePermissionTest
,修改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.example.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 = findViewById(R.id.make_call);
makeCall.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try{
Intent intent = new Intent(Intent.ACTION_CALL);
//Intent.ACTION_CALL是一個系統內置的打電話動作
intent.setData(Uri.parse("tel:10086"));
//指定了協議是tel,號碼是10086
startActivity(intent);
}catch (SecurityException e){
e.printStackTrace();
}
}
});
}
}
接下來修改AndroidManifest.xml
文件
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.runtimepermissiontest">
<!--聲明權限-->
<uses-permission android:name="android.permission.CALL_PHONE" />
<application
...
</application>
</manifest>
運行程序報錯是因爲權限被禁止所導致,因爲6.0系統及以上系統在使用危險權限時都必須進行運行時權限處理。
修改MainActivity
的代碼,來修復這個問題,如下所示:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button makeCall = findViewById(R.id.make_call);
makeCall.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CALL_PHONE)
!= PackageManager.PERMISSION_GRANTED) {
/*ContextCompat.checkSelfPermission()方法判斷用戶是否已經給該應用授權,第二個參數是權限名
返回值與PackageManager.PERMISSION_GRANTED比較,相等表示授權,否則表示沒有授權*/
ActivityCompat.requestPermissions(MainActivity.this, new
String[]{Manifest.permission.CALL_PHONE}, 1);
//如果沒有授權就調用 ActivityCompat.requestPermissions()方法,第二個參數是權限名,第三個是請求碼,唯一值就行
} 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();
}
}
/*
*調用完ActivityCompat.requestPermissions()方法之後,系統會彈出一個權限申請的對話框讓用戶選擇,無論結果如何
*都會回調至onRequestPermissionsResult()方法中。授權結果將會放在grantResults參數中。
* */
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case 1:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//同意則撥打電話
call();
} else {
//否則放棄操作,彈出失敗提示
Toast.makeText(this, "You denied th permission", Toast.LENGTH_SHORT).show();
}
break;
}
}
}
7.3 訪問其他程序中的數據
內容提供器的使用方法有兩種,一種是使用現在的內容提供器來讀取和操作相應程序中的數據,另一種是創建自己的內容提供器給我們的程序提供外部訪問接口。
7.3.1 ContentResolver
的基本用法
首先通過Context
中的getContextResolver()
方法獲取該類的實例,然後調用該類提供的一系列方法用於對數據進行CRUD操作。其中:
- insert() 方法:用於添加數據
- update() 方法:用於更新數據
- delete() 方法:用於刪除數據
- query() 方法:用於查詢數據
內容URI
這些方法接收一個Uri參數,這個參數被稱爲內容URI,用於給內容提供器中的數據建立起唯一的標識符。它由兩部分組成:
- authority:用於區分不同的應用程序,一般用程序包名命名,比如某個程序的包名是
com.example.app
,那麼該程序的authority可以命名爲com.example.app.provider
。 - path:用於區分同一個應用程序中的不同表,通常添加到authority的後面
此外,在前面還需要加上協議聲明。因此,內容URI的最標準格式寫法如下:content://com.example.app.provider/table
另外,可以在後面加上一個id,表示訪問id爲該值的數據,如下所示標準訪問id爲1的數據:
content://com.example.app.provider/table/1
我們還可以使用通配符的方式來分別匹配這兩種格式的內容URI,規則如下:
*:表示匹配任意長度的任意字符。
#:表示匹配任意長度的數字。
所以一個能匹配任意表的內容URI格式可以寫成:
content://com.example.app.provider/*
而一個能夠匹配table表中任意一行數據的內容URI格式可以寫成:
content://com.example.app.provider/table/#
接着,我們再借助UriMatcher
類就可以實現匹配內容URI的功能了。
得到內容URI後還需要將它解析成Uri對象纔可以作爲參數傳入。解析方法如下:
Uri uri = Uri.parse("content://com.example.app.provider/table")
查詢數據示例
然後,就可以使用這個Uri對象來查詢table表中的數據了,代碼如下所示:
Cursor cursor = getContextResolver().query(uri,projection,selection,selectionArgs,sortOrder);
其中參數解釋如下:
query()方法參數 | 對應SQL部分 | 描述 |
---|---|---|
uri | from table_name | 指定查詢某個應用程序下的某一張表 |
projection | select column1,column2 | 指定查詢的列名 |
selection | where column = value | 指定where的約束條件 |
selectionArgs | - | 爲where中的佔位符提供具體的值 |
sortOrder | order by column1,column2 | 指定查詢結果的排序方式 |
查詢完成後將返回一個Cursor對象,可以通過移動遊標位置來遍歷Cursor的每一行,然後再取出每一行的相應列的數據,代碼如下所示:
if(cusor != null){
while(cursor.moveToNext()){
String column1 = cursor.getString(cursor.getColumnIndex("column1");
int column1 = cursor.getInt(cursor.getColumnIndex("column1");
}
cursor.close();
}
除了查詢數據比較複雜,增刪改操作就比較容易了。
添加數據示例
ContentValues values = new ContentValues();
values.put("column1", "text");
values.put("column2", 1);
getContentResolver().insert(uri,values);
更新數據示例
ContentValues values = new ContentValues();
values.put("column1", "text2");
values.put("column2", 2);
getContentResolver().update(uri,values,"column1 = ? and column2 = ?", new String[]{"text","1"});
刪除數據示例
getContentResolver().delete(uri,"column2 = ?", new String[]{"1"});
7.3.2 讀取系統聯繫人
首先,在手機電話簿中創建一些聯繫人。然後創建一個ContactsTest
項目。接着修改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"
android:orientation="vertical">
<ListView
android:id="@+id/contacts_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>
</LinearLayout>
接着修改MainActivity
中的代碼,如下所示:
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 = findViewById(R.id.contacts_view);
adapter = new ArrayAdapter<String>(this, android.R.layout.simple_expandable_list_item_1, contactsList);
contactsView.setAdapter(adapter);
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) {
/*ContextCompat.checkSelfPermission()方法判斷用戶是否給該應用授權,第二個參數是權限名
返回值與PackageManager.PERMISSION_GRANTED比較,相等表示授權,否則表示沒有授權*/
ActivityCompat.requestPermissions(MainActivity.this, new
String[]{Manifest.permission.READ_CONTACTS}, 1);
//如果沒有授權就調用 ActivityCompat.requestPermissions()方法,第二個參數是權限名,第三個是請求碼,唯一值就行
} else {
readContacts();
}
}
private void readContacts() {
Cursor cursor = null;
try {
//查詢聯繫人數據,ContactsContract.CommonDataKinds.Phone.CONTENT_URI封裝了聯繫人數據表的URI
getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null);
if (cursor.moveToNext()) {
//獲取聯繫人姓名,常量ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME對應聯繫人姓名
String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
//獲取聯繫人手機號碼,常量ContactsContract.CommonDataKinds.Phone.NUMBER對應聯繫人手機號碼
String number = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
contactsList.add(displayName + "\n" + number);
}
//通知刷新一下ListView
adapter.notifyDataSetChanged();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (cursor != null) {
cursor.close();
}
}
}
/*
*調用完ActivityCompat.requestPermissions()方法之後,系統會彈出一個權限申請的對話框讓用戶選擇,無論結果如何
*都會回調至onRequestPermissionsResult()方法中。授權結果將會放在grantResults參數中。
* */
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case 1:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//同意則讀取聯繫人
readContacts();
} else {
//否則放棄操作,彈出失敗提示
Toast.makeText(this, "You denied th permission", Toast.LENGTH_SHORT).show();
}
break;
}
}
}
別忘了,還需要在AndroidManifest.xml
聲明讀取系統聯繫人的權限,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.contactstest">
<!--聲明讀取系統聯繫人的權限-->
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<application
...
</application>
</manifest>
7.4 創建自己的內容提供器
7.4.1 創建內容提供器的步驟
新建一個類去繼承ContentProvider
的方式來創建一個自己的內容提供器,然後重寫該類的6個抽象方法,代碼如下所示:
class MyProvider extends ContentProvider{
/**
* 通常會在這完成數據庫的創建和升級等操作
* @return 返回true表示初始化成功,否則表示失敗
*/
@Override
public boolean onCreate() {
return false;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
return null;
}
/**
* 根據傳入的內容URI來返回相應的MIME類型
* @param uri 內容URI
* @return 相應的MIME類型
*/
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
return null;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
}
因爲所有的CRUD操作都一定要匹配到相應的內容URI格式才能進行的,而我們當然不可能向UriMatch
中添加隱私數據的URI,所以這部分數據根本無法被外部程序訪問到,因此保證了隱私數據不會泄露出去。
7.4.2 實現跨程序數據共享
打開上一章DatabaseTest
項目,創建一個內容提供器,代碼如下所示:
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.example.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) {
//刪除數據
SQLiteDatabase db = dbHelper.getWritableDatabase();
int deletedRows = 0;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
deletedRows = db.delete("Book", selection, selectionArgs);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
deletedRows = db.delete("Book", "id = ?", new String[]{bookId});
break;
case CATEGORY_DIR:
deletedRows = db.delete("Category", selection, selectionArgs);
break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
deletedRows = db.delete("Category", "id = ?", new String[]{categoryId});
break;
default:
break;
}
return deletedRows;
}
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
return "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.book";
case BOOK_ITEM:
return "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.book";
case CATEGORY_DIR:
return "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.category";
case CATEGORY_ITEM:
return "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.category";
}
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
//添加數據
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() {
dbHelper = new MyDatabaseHelper(getContext(), "BookStore.db", null, 2);
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
//查詢數據
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);//將內容URI權限後的部分以“/”進行分割並放入一個字符串列表,第0個位置是路徑,第1個位置是id
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) {
//更新數據
SQLiteDatabase db = dbHelper.getWritableDatabase();
int updatedRows = 0;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
updatedRows = db.update("Book", values, selection, selectionArgs);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
updatedRows = db.update("Book", values, "id =?", new String[]{bookId});
break;
case CATEGORY_DIR:
updatedRows = db.update("Category", values, selection, selectionArgs);
break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
db.update("Category", values, "id =?", new String[]{categoryId});
break;
}
return updatedRows;
}
}
刪除並重新安裝DatabaseTest
程序,接着新建一個新項目ProviderTest
。先修改activity_main
佈局文件,如下所示:
<?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"
android:orientation="vertical">
<Button
android:id="@+id/add_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Add To Book" />
<Button
android:id="@+id/query_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Query From Book" />
<Button
android:id="@+id/update_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Update Book" />
<Button
android:id="@+id/delete_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Delete From Book" />
</LinearLayout>
然後修改MainActivity
中的代碼,如下所示:
public class MainActivity extends AppCompatActivity {
private String newId;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button addData = findViewById(R.id.add_data);
addData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//添加數據
Uri uri = Uri.parse("content://com.example.databasetest.provider/book");
ContentValues values = new ContentValues();
values.put("name", "A Clash of Kings");
values.put("author", "George Martin");
values.put("pages", 1040);
values.put("price", 22.85);
Uri newUri = getContentResolver().insert(uri, values);
newId = newUri.getPathSegments().get(1);
}
});
Button queryData = findViewById(R.id.query_data);
queryData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//查詢數據
Uri uri = Uri.parse("content://com.example.databasetest.provider/book");
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
if (cursor != null) {
while (cursor.moveToNext()) {
String name = cursor.getString(cursor.getColumnIndex("name"));
String author = cursor.getString(cursor.getColumnIndex("author"));
int pages = cursor.getInt(cursor.getColumnIndex("pages"));
float prices = cursor.getFloat(cursor.getColumnIndex("prices"));
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 prices is " + prices);
}
cursor.close();
}
}
});
Button updateData = findViewById(R.id.update_data);
updateData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//更新數據
Uri uri = Uri.parse("content://com.example.databasetest.provider/book/" + newId);
ContentValues values = new ContentValues();
values.put("name", "A Storm of Swords");
values.put("pages", 1216);
values.put("price", 24.05);
getContentResolver().update(uri, values, null, null);
}
});
Button deleteData = findViewById(R.id.delete_data);
updateData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//刪除數據
Uri uri = Uri.parse("content://com.example.databasetest.provider/book/" + newId);
getContentResolver().delete(uri, null, null);
}
});
}
}
7.5 Git時間——版本控制進階
略
7.6 小結和點評
在本章中,我們一開始瞭解了Android的權限機制,並且學會了如何在6.0以上的系統使用運行時權限,然後又學習了內容提供器的相關內容,以實現跨程序數據共享的功能。