前言

Android M(6.0)开始,一个非常重要的改动,就是权限系统

Android M占比越来越高,最近仔细调研了新权限系统的特点、申请权限的流程,记录如下。

最后介绍了如何使用AOP的方式,优雅的适配新权限系统。

新权限系统特点

运行时授权

在旧系统中安装app,会弹出一个安装确认的界面,让用户进行授权。
不仅不授权就无法安装这个应用,而且只能对所有权限一起授权。 如下图中左边

在Android M及以后的系统中安装new app,安装过程中是不需要用户确认权限的。对应的授权方式类似iPhone,是在使用过程中,让用户针对单个权限去授权。如下图中右边是demo app在申请权限

new app: 打包时,targetSDKVersion>=23

permission_permission_request

用户随时可修改权限

Android M以后的系统,用户随时可以去系统设置中,打开或关闭某个权限
permission_permission_request

old app

Android 6.0为了保证兼容性,对于old app,沿用了旧系统的处理方式,且安装成功后,old app会自动获得所声明的所有权限。

那么是否old app就可以不适配新权限系统?
不是的,因为用户仍然可以去系统设置中,关闭old app的某个权限。

当old app使用被关闭的权限,并不会崩溃,不过相关行为会被系统忽略。
如果行为是获取数据,获取到的结果,会是null。

old app: 打包时,targetSDKVersion\<=22

permission_permission_request

new app

对于new app,使用未授权的系统接口,会崩溃:
permission_permission_request

权限声明

声明app已经支持新权限系统

只需要在打包的时候,将targetSdkVersion 设置为23

声明要使用的权限

无论是new app还是old app,都需要在manifest里面,声明要使用的权限

1
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>

如果是普通权限,只要声明在manifest中,就可以自动获授权。
如果是涉及用户隐私的敏感权限,需要app主动调用系统接口,然后系统会弹出确认页面,让用户授权。

敏感权限

如果app获取了某个敏感权限,同组的权限会自动获取到。
比如app获取了 读取联系人 的权限,那么app在申请 修改联系人 的权限时,会自动授权通过。

敏感权限 如下表格:

权限组 权限列表
通讯录 修改您的通讯录, 查找设备上的帐户, 读取您的通讯录
电话 读取通话记录, 读取手机状态和身份, 使用即时通讯通话服务, 直接拨打电话号码, 写入通话记录, 拨打/接听SIP电话, 重新设置外拨电话的路径, 添加语音邮件
日历 读取日历活动和机密信息, 添加或修改日历活动,并在所有者不知情的情况下向邀请对象发送电子邮件
相机 拍摄照片和视频
安全 读取手机黑名单, 更改手机黑名单
身体传感器: 人体传感器(如心跳速率检测器), 使用指纹硬件
位置信息 精确位置(基于GPS和网络), 大致位置(基于网络)
存储空间 读取您的USB存储设备中的内容, 修改或删除您的USB存储设备中的内容
麦克风 录音
短信 读取您的讯息(短信或彩信), 接收讯息 (WAP), 接收讯息(彩信), 接收讯息(短信), 发送和查看短信, 读取小区广播消息

权限申请流程

需要权限的业务代码

以获取用户设备id举例例,tm.getDeviceId需要 READ_PHONE_STATE 这个权限。

1
2
3
4
private void readDeviceId() {
TelephonyManager tm = (TelephonyManager) this.getSystemService(Context.TELEPHONY_SERVICE);
System.out.println("Permission DeviceId:" + tm.getDeviceId());
}

检查是否已经拥有权限

我们需要使用系统接口checkSelfPermission检查是否拥有该权限。

在确认已经获得之后,才能调用业务代码,否则app会崩溃。

无论从前是否成功获得过授权,这个检查都是必须的。因为即使app在运行状态,用户也可以去设置中,修改允许的权限。

1
2
checkSelfPermission(Manifest.permission.READ_PHONE_STATE)
== PackageManager.PERMISSION_GRANTE

申请权限

如果没有该权限,我们需要调用requestPermissions进行权限申请,第一个参数,是要申请的权限列表。第二个参数是请求id,在回调的时候用到。

1
2
3
4
5
6
7
if (checkSelfPermission(Manifest.permission.READ_PHONE_STATE)
== PackageManager.PERMISSION_GRANTED) {
readDeviceId();
} else {
requestPermissions(new String[]{Manifest.permission.READ_PHONE_STATE},
MY_PERMISSIONS_REQUEST_READ_PHONE_STATE);
}

处理回调

权限申请的回调,是通过覆写父类函数来处理的。(权限申请页面,其实是一个activity)

我们需要在activity或fragment中,覆写onRequestPermissionsResult,然后根据requestCode判断是哪次权限申请的结果,来进行相应的处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);

switch (requestCode) {
//判断是我们刚才申请的权限
case MY_PERMISSIONS_REQUEST_READ_PHONE_STATE:
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//授权成功,调用业务代码
readDeviceId();
} else {
//授权失败
Toast.makeText(this, "fuck you", Toast.LENGTH_LONG).show();
}
}
}

处理“不再询问”

如果用户之前拒绝过,当我们再次发起权限申请的时候,系统对话框会多出一个勾选框 不再询问 ,如下图是前后对比:
permission_permission_request

一旦用户 勾选 了不再询问,我们以后的权限申请,会被系统 忽略

所以,在进行第二次权限申请前,需要先向用户解释——为啥我们需要这个权限,当用户接受我们的解释之后,再去进行第二次权限申请

系统提供了一个接口 shouldShowRequestPermissionRationale,用来获取这个解释的时机。如果它返回true,我们要显示一个界面(一般为对话框),向用户解释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//检查是否已经拥有权限
if (checkSelfPermission(Manifest.permission.READ_PHONE_STATE)
!= PackageManager.PERMISSION_GRANTED) {
if (shouldShowRequestPermissionRationale(Manifest.permission.READ_PHONE_STATE)) {
//如果没有对应的权限,且需要向解释,就弹出解释对话框
showExplanationDialog(new Runnable() {
@Override
public void run() {
//继续进行第二次申请权限
requestPermissions(new String[]{Manifest.permission.READ_PHONE_STATE},
MY_PERMISSIONS_REQUEST_READ_PHONE_STATE);
}
});
} else {
//如果不需要解释,直接申请权限
requestPermissions(new String[]{Manifest.permission.READ_PHONE_STATE},
MY_PERMISSIONS_REQUEST_READ_PHONE_STATE);
}
} else {
//已经有权限,直接执行业务代码
readDeviceId();
}

showExplanationDialog是我们自定义的弹出解释对话框,比如,显示一些描述性的文字, 当用户做出正面选择后(比如下面左图中的“去打开权限”),我们就弹出系统权限申请对话框(如下右图)
permission_permission_request

相关代码为:

1
2
3
4
5
6
7
8
9
10
new AlertDialog.Builder(TasksActivity.this)
.setMessage("打开权限,获取更好的用户体验")
.setPositiveButton("去打开权限", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
requestPermission.run();
}
})
.setNegativeButton("残忍拒绝", null)
.create().show();

不要在onResume中申请权限

测试代码的时候,发现一个现象:

当用户勾选不再询问,且拒绝授权后,onResume会死循环。

原因是申请权限的对话框,其实是一个activity。用户默认拒绝的情况下,每次申请权限,就会启动它,由于默认拒绝,它会自动关闭,它关闭后会触发我们onResume中的申请权限,然后又启动这个activity,然后它关闭后又触发我们申请权限。。。
最后形成了死循环。

旧机型兼容

上述系统接口,都是在sdk 23及以上才提供,为适配旧机型,只需把相关接口替换为support库中的ActivityCompat.xxx 即可,比如ActivityCompat. requestPermissions

特殊权限申请

显示悬浮窗 在老版本中,只需要声明以下权限即可:

android.permission.SYSTEM_ALERT_WINDOW

在Android 6.0以后, 需要补充使用特殊的方式,进行申请的,会打开系统的设置页面:

1
2
3
4
5
6
7
8
9
10
//检查是否有显示悬浮窗的权限
if (Settings.canDrawOverlays(target)) {
return true;
}

//去系统设置页面,需要用户手动打开
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + packageName));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
target.startActivity(intent);

效果如下图

permission_permission_request

否则,没有此权限的new app,会直接崩溃:

Unable to add window. Permission denied for this window type

AOP权限申请方案

可以发现,为了适配Android 6.0的权限系统,需要在Activity或Fragment里面加入一大堆冗余代码,对原本的业务造成了很大污染。

permission_permission_request

AOP是为了解决这种痛点而生的。在Android中,比较优秀的AOP技术,是aspectj。

介绍一套基本无侵入的权限方案。

用法

在需要权限的方法上面,打上Annotation即可:

1
2
3
4
@RequestPermission(Manifest.permission.READ_PHONE_STATE)
public void readDeviceId() {
...
}