運作時權限(Runtime Permission)是Android 6.0( 代号為 Marshmallow,API版本為 23)及以上版本新增的功能,相比于以往版本,這是一個較大變化。本文将介紹如何在代碼中加入并配置運作時權限功能。
如需閱讀英文原文,請您點選這個連結:《Everything every Android Developer must know about new Android’s Runtime Permission》。
如需閱讀官方運作時權限的相關介紹,請您點選這個連結:《Working with System Permissions》
運作時權限介紹
一直以來,為了保證最大的安全性,安裝Android應用時,系統總是讓使用者選擇是否同意該應用所需的所有權限。一旦安裝應用,就意味着該應用所需的所有權限均已獲得。若在使用某個功能時用到了某個權限,系統将不會提醒使用者該權限正在被擷取(比如微信需要使用攝像頭拍照,在Android 6.0以前的裝置上,使用者将不會被系統告知正在使用“使用系統攝像頭”的權限)。
這在安全性上是個隐患:在不經使用者同意的情況下,一些應用在背景可以自由地收集使用者隐私資訊而不被使用者察覺。
從Android 6.0版本開始,這個隐患終于被消除了:在安裝應用時,該應用無法取得任何權限!相反,在使用應用的過程中,若某個功能需要擷取某個權限,系統會彈出一個對話框,顯式地由使用者決定是否将該權限賦予應用,隻有得到了使用者的許可,該功能才可以被使用。
需要注意的是,在上述的右圖中,對話框并不會自動彈出,而需要由開發者手動調用。若程式調用的某個方法需要使用者賦予相應權限,而此時該權限并未被賦予時,那麼程式就會抛出異常并崩潰(Crash),如下圖所示。
除此之外,使用者還可以在任何時候撤銷賦予過的權限。
運作時權限無疑提升了安全性,有效地保護了使用者的隐私,這對于使用者來說确實是個好消息,但對于開發者來說簡直就是噩夢:因為這需要開發者在調用方法時,檢查該方法使用了什麼系統權限——這仿佛颠覆了傳統的程式設計的邏輯——開發者編寫每一句代碼時都得小心翼翼,否則應用可能随時崩潰。
在程式中,設定目标SDK版本(targetSDKVersion)為23及以上時(這意味着程式可以在Android 6.0及以上的版本中運作),将應用安裝在Android 6.0及以上機型中,運作時權限功能才能生效;若将其安裝在Android 6.0以前的機型中,權限檢查仍将僅僅發生在安裝應用時。
運作時權限與各版本間的相容性問題
假如将一個早期版本的應用安裝在Android 6.0版本的機型上,應用是不會崩潰的,因為這隻有兩種情況:1)該應用的targetSDKVersion < 23,在這種情況下,權限檢查仍是早期的形式(僅在安裝時賦予權限,使用時将不被提醒);2)該應用的targetSDKVersion ≥ 23時,則将使用新的運作時權限規則。
是以,這個早期版本的應用将運作如常。不過,将該應用安裝在Android 6.0上,且targetSDKVersion ≥ 23時,使用者仍然可以随時手動撤銷權限,當然這種做法不被官方推薦。
不被推薦的原因是,這種做法容易導緻應用崩潰。若targetSDKVersion < 23,當然不會出問題;若早期應用的targetSDKVersion ≥ 23,在使用應用時手動撤消了某個權限,那麼程式在調用了需要這個權限才能執行的方法時,應用什麼也不做,若該方法還有傳回值,那麼會根據實際情況傳回 0 或者 null。如下圖所示。
若上述調用的方法沒有崩潰,那麼這個方法被其他方法調用時也會因為傳回值是 0 或者 null 而崩潰。
不過好消息是,使用者幾乎不會手動撤銷已經賦予給應用的權限。
說了這麼多,在避免應用崩潰的前提下,适配新的運作時權限功能才是王道:對于那些在代碼中并未支援運作時權限的應用,請将targetSDKVersion設定為 < 23,否則應用有崩潰隐患;若代碼中支援了運作時權限,再将targetSDKVersion設定為 ≥ 23。
請注意:在Android Studio中建立Project時,會自動賦予targetSDKVersion為最新版本,若您的應用還暫時無法完全支援運作時權限功能,建議首先将targetSDKVersion手動設定為22。
自動賦予應用的權限
以下羅列了在安裝應用時,自動賦予應用的權限,這些權限無法在安裝後手動撤銷。我們稱其為基本權限(Normal Permission):
android.permission.ACCESS_LOCATION_EXTRA_COMMANDS
android.permission.ACCESS_NETWORK_STATE
android.permission.ACCESS_NOTIFICATION_POLICY
android.permission.ACCESS_WIFI_STATE
android.permission.ACCESS_WIMAX_STATE
android.permission.BLUETOOTH
android.permission.BLUETOOTH_ADMIN
android.permission.BROADCAST_STICKY
android.permission.CHANGE_NETWORK_STATE
android.permission.CHANGE_WIFI_MULTICAST_STATE
android.permission.CHANGE_WIFI_STATE
android.permission.CHANGE_WIMAX_STATE
android.permission.DISABLE_KEYGUARD
android.permission.EXPAND_STATUS_BAR
android.permission.FLASHLIGHT
android.permission.GET_ACCOUNTS
android.permission.GET_PACKAGE_SIZE
android.permission.INTERNET
android.permission.KILL_BACKGROUND_PROCESSES
android.permission.MODIFY_AUDIO_SETTINGS
android.permission.NFC
android.permission.READ_SYNC_SETTINGS
android.permission.READ_SYNC_STATS
android.permission.RECEIVE_BOOT_COMPLETED
android.permission.REORDER_TASKS
android.permission.REQUEST_INSTALL_PACKAGES
android.permission.SET_TIME_ZONE
android.permission.SET_WALLPAPER
android.permission.SET_WALLPAPER_HINTS
android.permission.SUBSCRIBED_FEEDS_READ
android.permission.TRANSMIT_IR
android.permission.USE_FINGERPRINT
android.permission.VIBRATE
android.permission.WAKE_LOCK
android.permission.WRITE_SYNC_SETTINGS
com.android.alarm.permission.SET_ALARM
com.android.launcher.permission.INSTALL_SHORTCUT
com.android.launcher.permission.UNINSTALL_SHORTCUT
開發者僅需要在
AndroidManifest.xml
中聲明這些權限,應用就能自動擷取無需使用者授權。
為應用适配新的運作時權限
為了設配新的運作時權限,首先需要将
compileSdkVersion
和
targetSdkVersion
設定為23:
android {
compileSdkVersion
...
defaultConfig {
...
targetSdkVersion
...
}
下面示範了一個增加聯系人的方法,該方法是需使用
WRITE_CONTACTS
的權限:
private static final String TAG = "Contacts";
private void insertDummyContact() {
// Two operations are needed to insert a new contact.
ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
// 、設定一個新的聯系人
ContentProviderOperation.Builder op =
ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null);
operations.add(op.build());
// 、為聯系人設定姓名
op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, )
.withValue(ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME,
"__DUMMY CONTACT from runtime permissions sample");
operations.add(op.build());
// 、使用ContentResolver添加該聯系人
ContentResolver resolver = getContentResolver();
try {
resolver.applyBatch(ContactsContract.AUTHORITY, operations);
} catch (RemoteException e) {
Log.d(TAG, "Could not add a new contact: " + e.getMessage());
} catch (OperationApplicationException e) {
Log.d(TAG, "Could not add a new contact: " + e.getMessage());
}
}
調用這個方法需要配置
WRITE_CONTACTS
權限,否則應用将崩潰:在
AndroidManifest.xml
中配置如下權限:
接着,我們需要建立一個方法用于判斷
WRITE_CONTACTS
權限是否确實被賦予;若方法為建立,那麼可以彈出一個對話框向使用者申請該權限。待權限被賦予後,方可建立聯系人。
權限被歸類成權限組(Permission Group),如下表所示:
若應用被賦予了某個權限組中的一個權限(比如
READ_CONTACTS
權限被賦予),那麼該組中的其他權限将被自動擷取(
WRITE_CONTACTS
和
GET_ACCOUNTS
權限被自動擷取)。
檢查和申請權限的方法分别是
Activity.checkSelfPermission()
和
Activity.requestPermissions
,這兩個方法是在 API 23 中新增的。
final private int REQUEST_CODE_ASK_PERMISSIONS = ;
private void insertDummyContactWrapper() {
//檢查AndroidManiFest中是否配置了WRITE_CONTACTS權限
int hasWriteContactsPermission = checkSelfPermission(Manifest.permission.WRITE_CONTACTS);
//若未配置該權限
if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
//申請配置該權限
requestPermissions(new String[] {Manifest.permission.WRITE_CONTACTS},
REQUEST_CODE_ASK_PERMISSIONS);
//直接傳回,不執行insertDummyContact()方法
return;
}
//若配置了該權限,才能調用方法
insertDummyContact();
}
若程式賦予了權限,
insertDummyContact()
方法将被調用;否則,
requestPermissions()
方法将彈出一個對話框申請權限,如下所示:
無論您選擇的是“DENY”還是“ALLOW”,程式都将回調
Activity.onRequestPermissionsResult()
方法,并将選擇的結果傳到方法的第三個參數中:
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case REQUEST_CODE_ASK_PERMISSIONS:
if (grantResults[] == PackageManager.PERMISSION_GRANTED) {
// 使用者選擇了“ALLOW”,擷取權限,調用方法
insertDummyContact();
} else {
// 使用者選擇了“DENY”,未擷取權限
Toast.makeText(MainActivity.this, "WRITE_CONTACTS Denied", Toast.LENGTH_SHORT)
.show();
}
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
這就是Android 6.0的全新運作時權限機制,為了提高安全性,增加代碼量在所難免:為了比對運作時權限機制,必須把處理方法的所有情況考慮在内。
處理 “不再詢問”(“Never Ask Again”)
每當系統申請權限時,彈出的對話框會有一個“不再詢問”(“Never Ask Again”)的勾選項。
若使用者打了勾,并選擇拒絕(“DENY”),那麼下次程式調用
Activity。requestPermissions()
方法時,将不會彈出對話框,權限也不會被賦予。
這種沒有回報的互動并不是一個好的使用者體驗(User Experience)。是以,下次啟動時,程式應彈出一個對話框,提示使用者“您已經拒絕了使用該功能所需要的權限,若需要使用該功能,請手動開啟權限”,應調用
Activity.shouldShowRequestPermissionRationale()
方法:
final private int REQUEST_CODE_ASK_PERMISSIONS = ;
private void insertDummyContactWrapper() {
int hasWriteContactsPermission = checkSelfPermission(Manifest.permission.WRITE_CONTACTS);
if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
if (!shouldShowRequestPermissionRationale(Manifest.permission.WRITE_CONTACTS)) {
showMessageOKCancel("You need to allow access to Contacts",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
requestPermissions(new String[] {Manifest.permission.WRITE_CONTACTS},
REQUEST_CODE_ASK_PERMISSIONS);
}
});
return;
}
requestPermissions(new String[] {Manifest.permission.WRITE_CONTACTS},
REQUEST_CODE_ASK_PERMISSIONS);
return;
}
insertDummyContact();
}
private void showMessageOKCancel(String message, DialogInterface.OnClickListener okListener) {
new AlertDialog.Builder(MainActivity.this)
.setMessage(message)
.setPositiveButton("OK", okListener)
.setNegativeButton("Cancel", null)
.create()
.show();
}
效果如下:
上述對話框應在兩種情形下彈出:
1)應用第一次申請權限時;
2)使用者勾選了“不再詢問”複選框。
對于第二種情況,
Activity.onRequestPermissionsResult()
方法将被回調,并回傳參數
PERMISSION_DENIED
,該對話框将不再彈出。
一次性申請多個權限
有些功能需要申請多個權限,仍然可以像上述方式一樣編寫代碼:
final private int REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS = ;
private void insertDummyContactWrapper() {
//提示使用者需要手動開啟的權限集合
List<String> permissionsNeeded = new ArrayList<String>();
//功能所需權限的集合
final List<String> permissionsList = new ArrayList<String>();
//若使用者拒絕了該權限申請,則将該申請的提示添加到“使用者需要手動開啟的權限集合”中
if (!addPermission(permissionsList, Manifest.permission.ACCESS_FINE_LOCATION))
permissionsNeeded.add("GPS");
if (!addPermission(permissionsList, Manifest.permission.READ_CONTACTS))
permissionsNeeded.add("Read Contacts");
if (!addPermission(permissionsList, Manifest.permission.WRITE_CONTACTS))
permissionsNeeded.add("Write Contacts");
//若在AndroidManiFest中配置了所有所需權限,則讓使用者逐一賦予應用權限,若權限都被賦予,則執行方法并傳回
if (permissionsList.size() > ) {
//若使用者賦予了一部分權限,則需要提示使用者開啟其餘權限并傳回,該功能将無法執行
if (permissionsNeeded.size() > ) {
// Need Rationale
String message = "You need to grant access to " + permissionsNeeded.get();
for (int i = ; i < permissionsNeeded.size(); i++)
message = message + ", " + permissionsNeeded.get(i);
//彈出對話框,提示使用者需要手動開啟的權限
showMessageOKCancel(message,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
}
});
return;
}
requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
return;
}
insertDummyContact();
}
//判斷使用者是否授予了所需權限
private boolean addPermission(List<String> permissionsList, String permission) {
//若配置了該權限,傳回true
if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
//若未配置該權限,将其添加到所需權限的集合,傳回true
permissionsList.add(permission);
// 若使用者勾選了“永不詢問”複選框,并拒絕了權限,則傳回false
if (!shouldShowRequestPermissionRationale(permission))
return false;
}
return true;
}
當使用者設定了每個權限是否可被賦予後,Activity.onRequestPermissionsResult()方法被回調,并傳入第三個參數:
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS:
{
//初始化Map集合,其中Key存放所需權限,Value存放該權限是否被賦予
Map<String, Integer> perms = new HashMap<String, Integer>();
// 向Map集合中加入元素,初始時所有權限均設定為被賦予(PackageManager.PERMISSION_GRANTED)
perms.put(Manifest.permission.ACCESS_FINE_LOCATION, PackageManager.PERMISSION_GRANTED);
perms.put(Manifest.permission.READ_CONTACTS, PackageManager.PERMISSION_GRANTED);
perms.put(Manifest.permission.WRITE_CONTACTS, PackageManager.PERMISSION_GRANTED);
// 将第二個參數回傳的所需權限及第三個參數回傳的權限結果放入Map集合中,由于Map集合要求Key值不能重複,是以實際的權限結果将覆寫初始值
for (int i = ; i < permissions.length; i++)
perms.put(permissions[i], grantResults[i]);
// 若所有權限均被賦予,則執行方法
if (perms.get(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
&& perms.get(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED
&& perms.get(Manifest.permission.WRITE_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
// All Permissions Granted
insertDummyContact();
}
//否則彈出toast,告知使用者需手動賦予權限
else {
// Permission Denied
Toast.makeText(MainActivity.this, "Some Permission is Denied", Toast.LENGTH_SHORT)
.show();
}
}
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
使用支援庫(Support Library)提高程式的相容性
盡管上述代碼在Android 6.0版本的裝置上能夠正常運作,但運作在早前版本的裝置上,程式将崩潰。
簡單直接的方式是事先進行版本判斷:
if (Build.VERSION.SDK_INT >= ) {
// Marshmallow+
} else {
// Pre-Marshmallow
}
但這樣會使程式變得臃腫。
比較好的解決方式是使用
Support Library v4
支援庫中的方法替換原來的方法,這将省去為不同版本的裝置分别提供代碼的麻煩:
// 将Activity.checkSelfPermission()方法替換為如下方法
ContextCompat.checkSelfPermission()
// 将Activity.requestPermissions()方法替換為如下方法
ActivityCompat.requestPermissions()
//将Activity.shouldShowRequestPermissionRationale()方法替換為如下方法,在早期版本中,該方法直接傳回false
ActivityCompat.shouldShowRequestPermissionRationale()
無論哪個版本,調用上面的三個方法都需要Content或Activity參數。
以下是使用
Support Library v4
支援庫中的方法替換原代碼中相應方法後的程式:
private void insertDummyContactWrapper() {
int hasWriteContactsPermission = ContextCompat.checkSelfPermission(MainActivity.this,
Manifest.permission.WRITE_CONTACTS);
if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
if (!ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this,
Manifest.permission.WRITE_CONTACTS)) {
showMessageOKCancel("You need to allow access to Contacts",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ActivityCompat.requestPermissions(MainActivity.this,
new String[] {Manifest.permission.WRITE_CONTACTS},
REQUEST_CODE_ASK_PERMISSIONS);
}
});
return;
}
ActivityCompat.requestPermissions(MainActivity.this,
new String[] {Manifest.permission.WRITE_CONTACTS},
REQUEST_CODE_ASK_PERMISSIONS);
return;
}
insertDummyContact();
}
需要注意的是,若程式中用到了Fragment,也最好使用android.support.v4.app.Fragment,這樣可以相容更低的版本,使應用适配更多裝置。
使用第三方開源庫(3rd Party Library)簡化代碼
為了是代碼更加簡潔,推薦一個第三方架構。該架構可以友善地內建運作時權限機制并有效相容新舊版本。
在應用打開時撤銷權限所帶來的問題
如上所述,使用者可以随時撤銷賦予應用的權限,若某個應用正在運作時,使用者撤消了其某些權限,應用所在程序會立刻終止(application’s process is suddenly terminated),是以盡量不要在應用運作時,改變其權限規則。
總結與建議
總結:
運作時權限機制大大提高了應用的安全性,不過開發者需要為此修改代碼以比對新的版本,不過好消息是,大部分常用的權限都被自動賦予了,是以,隻有很小一部分代碼需要修改。
建議:
- 使用運作時機制時應該以版本的相容作為前提。
- 不要将未适配運作時機制的程式的targetSdkVersion設定為 23 及以上。
感謝
特别感謝原創作者的付出,下面是作者的介紹資訊: