前言
Android 是一个权限分隔的操作系统,在默认情况下任何应用都没有权限执行对其他应用、操作系统或用户有不利影响的任何操作。这包括读取或写入用户的私有数据(例如联系人或电子邮件)、读取或写入其他应用程序的文件、执行网络访问、使设备保持唤醒状态等。
Android 引入权限机制来管控基本沙盒未提供的额外功能。应用以静态方式声明它们需要的权限,然后 Android 系统提示用户同意。
在Android 6.0(Api 23)之前,我们想使用某个功能,只需要在Manifest中声明,在安装程序包时,系统会自动提示用户授权。但是 6.0 时系统行为发生了变更,安装程序时不再进行权限提示,一些普通权限只要进行声明会默认授予,但一些危险权限(例如读取联系人)除了声明外,还需要动态请求用户授权,否则可能导致功能不可用或产生crash。
变更概览
- 手机系统版本 < 23 : 只需要在 Manifest 中声明即可
- 手机系统版本 >=23 但应用 targetSdkVersion < 23 : 系统会认为你未适配好运行时权限,还是只需声明了即可
- 手机系统版本 >=23 且应用 targetSdkVersion >=23 : 既需要声明,又需要动态申请
权限声明
在 AndroidManifest 中 使用 <uses-permission>标签,比如写存储权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
权限分类
系统权限分为几个保护级别
正常权限
涵盖应用需要访问其沙盒外部数据或资源,但对用户隐私或其他应用操作风险很小的区域。例如,设置时区的权限就是正常权限。如果应用声明其需要正常权限,系统会自动向应用授予该权限。如需当前正常权限的完整列表,请参阅正常权限。危险权限
涵盖应用需要涉及用户隐私信息的数据或资源,或者可能对用户存储的数据或其他应用的操作产生影响的区域。例如,能够读取用户的联系人属于危险权限。如果应用声明其需要危险权限,则用户必须明确向应用授予该权限。特殊权限
有许多权限其行为方式与正常权限及危险权限都不同。SYSTEM_ALERT_WINDOW 和 WRITE_SETTINGS 特别敏感,因此大多数应用不应该使用它们。如果某应用需要其中一种权限,必须在清单中声明该权限,并且发送请求用户授权的 intent。系统将向用户显示详细管理屏幕,以响应该 intent。特殊权限.jpg
权限组
所有危险的 Android 系统权限都属于权限组。如果设备运行的是 Android 6.0(API 级别 23),并且应用的 targetSdkVersion 是 23 或更高版本,则当用户请求危险权限时系统会发生以下行为:
- 如果应用请求其清单中列出的危险权限,而应用目前在权限组中没有任何权限,则系统会向用户显示一个对话框,描述应用要访问的权限组。对话框不描述该组内的具体权限。例如,如果应用请求 READ_CONTACTS 权限,系统对话框只说明该应用需要访问设备的联系信息。如果用户批准,系统将向应用授予其请求的权限。
- 如果应用请求其清单中列出的危险权限,而应用在同一权限组中已有另一项危险权限,则系统会立即授予该权限,而无需与用户进行任何交互。例如,如果某应用已经请求并且被授予了 READ_CONTACTS 权限,然后它又请求 WRITE_CONTACTS,系统将立即授予该权限(马上进行成功回调)。
需要注意的是:
- 我们不应该依赖权限组,而要对具体权限进行判断及申请,因为具体权限可能在后续版本中产生分组变动
- 系统在展示权限说明时,只会展示权限组的解释,并不会解释某个具体权限,所以在请求前弹出一个详细的说明界面是很有必要的,以防用户产生误解
最后附上危险权限的列表:
危险权限.png
动态请求权限
首先我们需要了解几个方法
- 判断是否有某个权限
/**
* 检查用户是否授予了某个权限
*
* @param permission 待检查的权限
* @return true:{@link PackageManager#PERMISSION_GRANTED} false:{@link PackageManager#PERMISSION_DENIED}
*/
private boolean hadPermission(String permission) {
return ContextCompat.checkSelfPermission(this, permission)
== PackageManager.PERMISSION_GRANTED;
}
- 请求权限
/**
* 动态请求权限 系统会弹个框
*
* @param permissions 待用户授权的权限集
*/
private void requestPermission(String[] permissions) {
ActivityCompat.requestPermissions(this, permissions, REQ_CODE);
}
- 是否被拒绝过一次
/**
* 是否应该展示一下申请权限的理由
*
* @param permission 待授权的权限
* @return 如果用户拒绝过一次权限请求就会返回 true
*/
private boolean showRationale(String permission) {
return ActivityCompat.shouldShowRequestPermissionRationale(this, permission);
}
比如我们现在要使用写存储的权限,结合起来就是:
private void writeText2Storage() {
File file = new File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
"permission.txt");
if (!file.exists()) {
if (hadPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
//授予了权限
onPermissionGranted(file);
} else {
if (showRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
//被用户拒绝过一次请求
toast("需要我们展示申请权限的理由");
} else {
//请求权限弹窗
requestPermission(new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE });
}
}
}
}
然后我们在 Activity 和 Fragment 中的 onRequestPermissionsResult 中可以收到请求权限的回调
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQ_CODE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//权限被允许
toast("允许了权限");
} else {
//权限被拒绝 我们需要展示请求权限的理由 并且提供一键跳转应用权限管理界面
onPermissionDenied();
}
}
}
使用框架
可以看到,虽然动态权限的流程很简单,但每次请求权限时都写这些代码还是很麻烦的,好在Github上有很多封装的库,我一直在使用 AndPermission 也推荐给大家,这个库除了危险权限外,还对URI权限,特殊权限也进行了封装,使用起来很方便
AndPermission Feature.png
工作中遇到的坑
华为手机在运行中,跳到权限管理界面,勾选拒绝之前授予的正在使用的权限,然后回到应用,华为默认会销毁重建这个Activity,并且只保存了一些和Activity相关的东西,导致Activity抽风..
比如我的游戏界面里有语音连麦功能,用户在玩游戏时手动跳到应用权限管理界面,然后拒绝录音权限,再回到应用时,游戏Activity会销毁重建…