运行时权限(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 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
中配置如下权限:
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
接着,我们需要创建一个方法用于判断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 及以上。
特别感谢原创作者的付出,下面是作者的介绍信息: