知識點目錄
知識點回顧
7.1 內容提供器簡介
內容提供器(Content Provider)主要用於在不同的應用程序之間實現數據共享的功能。它提供了一套完整的機制,允許一個程序訪問另一個程序中的數據,同時還能保證被訪問數據的安全性。
內容提供器可以選擇只對哪一部分數據進行共享,從而保證我們程序中的隱私數據不會有泄漏的風險。
7.2 運行權限
Android 6.0引用了運行時權限功能,很好地保護了用戶的安全和隱私。
7.2.1 Android權限機制詳解
Android要求我們在訪問用戶涉及到安全性的時候,需要在AndroidManifest.xml中加入相應的權限。這樣用戶就在如下兩個方面得到了保護:
-
在低於6.0的系統設備上安裝程序時,就會在安裝界面提醒用戶該apk需要的權限
-
用戶可以在應用程序管理界面查看任意一個程序的權限申請情況
但很多常用的軟件存在“店大欺客”的情況,濫用很多權限。因此Android研發團隊在6.0時就加入了運行時權限功能。即用戶不需要在安裝軟件的時候一次性授權所有申請的權限,而是可以在軟件的使用過程中再對某一項權限申請進行授權。
當然並不是所有的權限都是在運行時申請。Android將所有的權限分爲了兩類:
-
普通權限
-
危險權限
普通權限:是指那些不會威脅到用戶的安全和隱私的權限,系統會自動幫我們進行授權。
危險權限:是指那些可能會觸及用戶隱私或者對設備安全性造成影響的權限,必須要由用戶手動點擊授權才行,否則程序就無法使用相應的功能。
Android中的危險權限主要有如下幾種:
注意:表中每一個權限都屬於一個權限組,在處理運行時權限時使用權限名,如果用戶一旦同意授權了,那麼該權限所對應的權限組中所有的其他權限也會同時被授權。
Android系統中完整的權限列表如下:
7.2.2 在程序運行時申請權限
下面我們來使用CALL_PHONE來演示。
示例代碼:
-
定義一個button觸發點
<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" android:textAllCaps="false"/> </LinearLayout>
-
在AndroidManifest.xml中聲明權限
<uses-permission android:name="android.permission.CALL_PHONE"/>
-
運行時權限
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 v) { //檢查權限 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")); if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) { return; } startActivity(intent); } catch (Exception e) { e.printStackTrace(); } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { switch (requestCode) { case 1: //如果授權成功則調用call()方法 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()方法去判斷用戶是否授過權。共接收兩個參數:
-
參數一:上下文
-
參數二:具體的權限名
第二步:將返回值與PackageManager.PERMISSION_GRANTED做比較,如果相等就說明用戶已經授權,如果不相等就表示用戶沒有授權。
第三步:如果授權了,則直接調用call()方法,如果沒有授權,則調用ActivityCompat.requestPermissions()方法向用戶申請授權。requestPermissions()共接收三個參數:
-
參數一:Activity實例
-
參數二:String數組,把要申請的權限名放在數組中
-
參數三:請求碼,只要確保是唯一值就行
調用完requestPermissions()方法後,系統會彈出一個權限申請的對話框,然後用戶可以選擇同意或拒絕,無論哪種最終都會調用onRequestPermissionsResult()方法,授權結果封裝在grantResults參數中,我們只需要判斷一下最後的授權結果。
效果圖:
備註:可以在Settings—>Apps—>RunntimePermissionTest—>Permissions中去關閉已經授予程序的危險權限。
7.3 訪問其他程序中的數據
內容提供器的用法主要有兩種:
-
使用現有的內容提供器來讀取和操作相應程序中的數據
-
創建自己的內容提供器給我們程序的數據提供外部訪問接口
7.3.1 ContentResolver的基本用法
Android中想要訪問內容提供器中共享的數據,需要藉助ContentResolver類,可以通過Context中的getContentResolver()方法獲取到類的實例。它提供了一系列的方法對數據進行CRUD操作。
ContentResolver不接受表名,是接收內容URI,內容URI給內容提供器中的數據建立了唯一標識符,主要由authority和path組成:
-
authority:是用於對不同的應用程序做區分,一般都會採用程序包名
-
path:是用於對同一個程序中的不同表做區分
不過我們需要在頭部加上協議聲明,因此,內容URI最標準的格式寫法如下:
content://com.example.app.provider/table1
在得到內容URI字符串後,還需要將它解析成Uri對象。
Uri uri = Uri.parse("content://com.example.app.provider/table1");
查詢數據
Uri uri = Uri.parse("content://com.example.app.provider/table1");
Cursor cursor = getContentResolver().query(
uri, //指定查詢哪個應用程序下哪張表
projection, //指定查詢的列名
selection, //指定where的約束條件
selectionArgs, //爲where中的佔位符提供具體的值
sortOrder); //指定查詢結果的排序方式
if (cursor != null) {
while (cursor.moveToNext()) {
String column1 = cursor.getString(cursor.getColumnIndex("column1"));
int column2 = cursor.getInt(cursor.getColumnIndex("column2"));
}
}
添加數據
ContentValues values = new ContentValues();
values.put("column1","text");
values.put("column2",1);
getContentResolver().insert(uri, values);
更新數據
ContentValues values = new ContentValues();
values.put("column1","");
getContentResolver().update(uri, values, "column1 = ? and column2 = ?", new String[]{"text", "1"});
刪除數據
getContentResolver().delete(uri, "column2 = ?", new String[]{"1"});
7.3.2 讀取系統聯繫人
下面我們來讀取電話簿中的聯繫人。
示例代碼
-
寫一個ListView佈局
<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"> </ListView> </LinearLayout>
-
讀取聯繫人邏輯
public class MainActivity extends AppCompatActivity { List<String> contactsList = new ArrayList<>(); private ArrayAdapter<String> mAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ListView contactsView = (ListView) findViewById(R.id.contacts_view); mAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, contactsList); contactsView.setAdapter(mAdapter); 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()) { //獲取聯繫人姓名 String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)); //獲取聯繫人號碼 String number = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)); contactsList.add(displayName + "\n" + number); } mAdapter.notifyDataSetChanged(); } } catch (Exception e) { e.printStackTrace(); } finally { if (cursor != null) { cursor.close(); } } } @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 the permission", Toast.LENGTH_SHORT).show(); } break; default: } } }
-
添加權限
<uses-permission android:name="android.permission.READ_CONTACTS"/>
效果圖:
7.4 創建自己的內容提供器
7.4.1 創建內容提供器的步驟
新建一個類去繼承ContentProvider的方式來創建一個自己的內容提供器。
ContentProvider類中有6個抽象方法,子類繼承它的時候,需要將這6個方法全部重寫。
public class MyProvider extends ContentProvider {
@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;
}
@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;
}
}
onCreate()
初始化內容提供器時調用。主要完成對數據庫的創建和升級等操作。
返回true表示內容提供器初始化成功;返回false表示失敗。
備註:只有當存在ContentResolver嘗試訪問我們程序中的數據時,內容提供器纔會被初始化。
query()
從內容提供器中查詢數據,查詢的結果存放在Cursor對象中。共接收5個參數:
Uri:確定查詢哪張表
projection:確定查詢哪些列
selection和selectionArgs:約束查詢哪些行
sortOrder:對查詢結果進行排序
insert()
向內容提供器中添加一條數據。共接收2個參數:
Uri:確定要添加到哪張表
values:待添加的數據保存到ContentValues中
返回一個用於表示這條新紀錄的URI。
update()
更新內容提供器中已有的數據。共接收4個參數:
Uri:確定要更新哪張表
values:保存要更新的數據
selection和selectionArgs:約束更新哪些行
受到影響的行數將作爲返回值返回。
delete()
從內容提供器中刪除數據。共接收3個參數:
Uri:確定要刪除哪張表中的數據
selection和selectionArgs:約束刪除哪些行
被刪除的行數將作爲返回值返回。
getType()
根據傳入內容的URI來返回相應的MIME類型
內容URI主要的兩種格式:
格式一:
content://com.example.app.provider/table1
表示訪問com.example.app.provider這個應用的table1表中的數據。
格式二:
content://com.example.app.provider/table2/1
表示訪問com.example.app.provider這個應用的table2表中id爲1的數據
我們可以使用通配符的方式來分別匹配這兩種格式的內容URI,規則如下:
-
*:表示匹配任意長度的任意字符
-
#:表示匹配任意長度的數字
所以,一個能匹配任意表的內容URI格式就可以寫成:
content://com.example.app.provider/*
一個能匹配table1表中任意一行數據的內容URI格式就可以寫成:
content://com.example.app.provider/table1/#
緊接着,我們可以藉助UriMatcher這個類實現匹配內容URI的功能,UriMatcher提供了一個addURI()方法,該方法共接收3個參數:
-
參數一:authority (一般是應用包名+provider)
-
參數二:path (一般是表名)
-
參數三:自定義代碼
然後調用UriMatcher的match()方法時,就可以將一個Uri對象傳入,返回值是某個能夠匹配這個Uri對象所對應的自定義代碼,根據這個自定義就可以判斷出調用方期望訪問的是哪張表中的數據。
示例代碼:
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.example.app.provider", "table1", TABLE1_DIR);
uriMatcher.addURI("com.example.app.provider", "table1", TABLE1_ITEM);
uriMatcher.addURI("com.example.app.provider", "table2", TABLE2_DIR);
uriMatcher.addURI("com.example.app.provider", "table2", TABLE2_ITEM);
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
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;
}
return null;
}
@Override
public boolean onCreate() {
return false;
}
@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;
}
}
上面只演示了query()方法,其他的insert()、update()、delete()方法實現起來也一樣,都通過UriMatcher的match()方法去判斷訪問方期望訪問哪張表。
MIME類型
getType()方法會根據Uri返回一個MIME類型,一個URI所對應的MIME字符串主要由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
示例代碼:
public String getType(@NonNull Uri uri) {
switch (uriMatcher.match(uri)) {
case TABLE1_DIR:
return "vnd.android.cursor.dir/vnd.com.example.app.provider.table1";
case TABLE1_ITEM:
return "vnd.android.cursor.item/vnd.com.example.app.provider.table1";
case TABLE2_DIR:
return "vnd.android.cursor.dir/vnd.com.example.app.provider.table2";
case TABLE2_ITEM:
return "vnd.android.cursor.item/vnd.com.example.app.provider.table2";
default:
break;
}
return null;
}
7.4.2 實現跨程序數據共享
在上一章DatabaseTest項目上繼續開發,新建一個內容提供器,右擊com.example.broadcasttest包—>New—>Other—>Content Provider。
自定義內容提供器代碼如下:
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 UriMatcher mUriMatcher;
public static final String AUTHORITY = "com.example.databasetest.provider";
static {
mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
mUriMatcher.addURI(AUTHORITY,"book",BOOK_DIR);
mUriMatcher.addURI(AUTHORITY,"book/#",BOOK_ITEM);
mUriMatcher.addURI(AUTHORITY,"category",CATEGORY_DIR);
mUriMatcher.addURI(AUTHORITY,"category/#",CATEGORY_ITEM);
}
private MyDatabaseHelper mDbHelper;
public DatabaseProvider() {
}
@Override
public boolean onCreate() {
mDbHelper = 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 = mDbHelper.getReadableDatabase();
Cursor cursor = null;
switch (mUriMatcher.match(uri)) {
case BOOK_DIR:
cursor = db.query("Book", projection, selection, selectionArgs, null, null, sortOrder);
break;
case BOOK_ITEM:
//getPathSegments()會將內容URI權限之後的部分以"/"符號進行分割,分割後的結果放入到一個
//字符串列表中,列表的第0個位置存放的是路徑,第1個位置存放的是id
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 Uri insert(Uri uri, ContentValues values) {
//添加數據
SQLiteDatabase db = mDbHelper.getWritableDatabase();
Uri uriReturn = null;
switch (mUriMatcher.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 int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
//更新數據
SQLiteDatabase db = mDbHelper.getWritableDatabase();
int updateRows = 0;
switch (mUriMatcher.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;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// 刪除數據
SQLiteDatabase db = mDbHelper.getWritableDatabase();
int deleteRows = 0;
switch (mUriMatcher.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) {
switch (mUriMatcher.match(uri)) {
case BOOK_DIR:
return "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.book";
case BOOK_ITEM:
return "vnd.android.cursor.item/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.item/vnd.com.example.databasetest.provider.category";
default:
break;
}
return null;
}
}
DatabaseTest項目的完整代碼我已上傳我到gitHub,有需要的朋友可以進入查看:
訪問內容提供器中的數據:
public class MainActivity extends AppCompatActivity {
private String mNewId;
@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 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);
mNewId = newUri.getPathSegments().get(1);
}
});
//查詢數據
Button queryData = (Button) 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"));
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);
}
}
}
});
//更新數據
Button updateData = (Button) 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/" + mNewId);
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 = (Button) findViewById(R.id.delete_data);
deleteData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Uri uri = Uri.parse("content://com.example.databasetest.provider/book/" + mNewId);
getContentResolver().delete(uri, null, null);
}
});
//清空表中的數據
Button clearData = (Button) findViewById(R.id.clear_data);
clearData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Uri uri = Uri.parse("content://com.example.databasetest.provider/book/");
int delete = getContentResolver().delete(uri, null, null);
Log.d("MainActivity", "delete = " + delete);
}
});
}
}
ProvideTest程序是去訪問DatabaseTest中的數據,完整代碼我已上傳我到gitHub,有需要的朋友可以進入查看:
7.5 Git時間
這裏推薦一個Git詳細教程。
7.6 小結點評
本章瞭解了Android的權限機制,並且學會了如何在6.0以上的系統中使用運行時的權限,然後又重點學習了內容提供器的相關內容,以實現跨程序共享的功能。