編輯:關於Android編程
運行時權限(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),如下圖所示。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4KCjxocj4KCjxwPjxpbWcgc3JjPQ=="/uploadfile/Collfiles/20160413/20160413100413482.jpg" alt="這裡寫圖片描述" title="\">
除此之外,用戶還可以在任何時候撤銷賦予過的權限。
運行時權限無疑提升了安全性,有效地保護了用戶的隱私,這對於用戶來說確實是個好消息,但對於開發者來說簡直就是噩夢:因為這需要開發者在調用方法時,檢查該方法使用了什麼系統權限——這仿佛顛覆了傳統的編程的邏輯——開發者編寫每一句代碼時都得小心翼翼,否則應用可能隨時崩潰。
在程序中,設置目標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 23
...
defaultConfig {
...
targetSdkVersion 23
...
}
下面演示了一個增加聯系人的方法,該方法是需使用WRITE_CONTACTS
的權限:
private static final String TAG = "Contacts";
private void insertDummyContact() {
// Two operations are needed to insert a new contact.
ArrayList operations = new ArrayList(2);
// 1、設置一個新的聯系人
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());
// 1、為聯系人設置姓名
op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
.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());
// 3、使用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 = 123;
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[0] == 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”)的勾選項。
若用戶打了勾,並選擇拒絕(“DENY”),那麼下次程序調用Activity。requestPermissions()
方法時,將不會彈出對話框,權限也不會被賦予。
這種沒有反饋的交互並不是一個好的用戶體驗(User Experience)。所以,下次啟動時,程序應彈出一個對話框,提示用戶“您已經拒絕了使用該功能所需要的權限,若需要使用該功能,請手動開啟權限”,應調用Activity.shouldShowRequestPermissionRationale()
方法:
final private int REQUEST_CODE_ASK_PERMISSIONS = 123;
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 = 124;
private void insertDummyContactWrapper() {
//提示用戶需要手動開啟的權限集合
List permissionsNeeded = new ArrayList();
//功能所需權限的集合
final List permissionsList = new ArrayList();
//若用戶拒絕了該權限申請,則將該申請的提示添加到“用戶需要手動開啟的權限集合”中
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() > 0) {
//若用戶賦予了一部分權限,則需要提示用戶開啟其余權限並返回,該功能將無法執行
if (permissionsNeeded.size() > 0) {
// Need Rationale
String message = "You need to grant access to " + permissionsNeeded.get(0);
for (int i = 1; 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 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 perms = new HashMap();
// 向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 = 0; 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);
}
}
盡管上述代碼在Android 6.0版本的設備上能夠正常運行,但運行在早前版本的設備上,程序將崩潰。
簡單直接的方式是事先進行版本判斷:
if (Build.VERSION.SDK_INT >= 23) {
// 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,這樣可以兼容更低的版本,使應用適配更多設備。
為了是代碼更加簡潔,推薦一個第三方框架。該框架可以方便地集成運行時權限機制並有效兼容新舊版本。
如上所述,用戶可以隨時撤銷賦予應用的權限,若某個應用正在運行時,用戶撤消了其某些權限,應用所在進程會立刻終止(application’s process is suddenly terminated),所以盡量不要在應用運行時,改變其權限規則。
總結:
運行時權限機制大大提高了應用的安全性,不過開發者需要為此修改代碼以匹配新的版本,不過好消息是,大部分常用的權限都被自動賦予了,所以,只有很小一部分代碼需要修改。
建議:
使用運行時機制時應該以版本的兼容作為前提。
不要將未適配運行時機制的程序的targetSdkVersion設置為 23 及以上。
特別感謝原創作者的付出,下面是作者的介紹信息:
通過SpannableStringBuilder來實現,它就像html裡邊的元素改變指定文字的文字顏色或背景色public class MainActivity exte
博主在剛剛在學習過程中發現了一個關於android往sdcard讀寫的問題, 配置了該配置的提示無讀寫權限。 在AndroidManifest.xml文件中配置清單如下
有時候有這樣的需求:同一份源碼,需要打出多個apk,而且包名和logo不一樣,就這麼簡單,怎麼很便捷的搞定呢 ?ok,毫無疑問,這裡涉及到多渠道。1. 多渠道配置第一步打
第1節 概述主題theme與風格style是兩個很相近的概念,經常把它們混亂著稱呼。它們都定義在xml文件中,都使用標簽。主題與風格是包含與被包含的關系。例如同一個主題中