Android8.0运行时权限策略变化和适配方案
Android8.0运行时权限策略变化和适配方案
在 Android O 之前,如果应用在运行时请求权限并且被授予该权限,系统会错误地将属于同一权限组并且在清单中注册的其他权限也一起授予应用。对于针对Android O的应用,此行为已被纠正。系统只会授予应用明确请求的权限。然而一旦用户为应用授予某个权限,则所有后续对该权限组中权限的请求都将被自动批准。例如,假设某个应用在其清单中列出READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE。应用请求READ_EXTERNAL_STORAGE,并且用户授予了该权限,如果该应用针对的是API级别24或更低级别,系统还会同时授予WRITE_EXTERNAL_STORAGE,因为该权限也属于STORAGE权限组并且也在清单中注册过。如果该应用针对的是Android O,则系统此时仅会授予READ_EXTERNAL_STORAGE,不过在该应用以后申请WRITE_EXTERNAL_STORAGE权限时,系统会立即授予该权限,而不会提示用户。
下面我们还是以READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE为例来具体分析一下,这对我们现有的代码有什么影响。
正式开始之前,我们先约定两个方法:
/** * 拿到没有被授权的权限。 */ getDeinedPermission(String... permissions); /** * 请求几个权限。 */ requestPermission(String... deinedPermissions);
- 权限的常量在Manifest.permission类中,而READ_EXTERNAL_STORAGE权限是在API 16之后才添加的,所以在在Android M出来后为了适配更低版本的系统,我们一般是这样申请权限的(伪代码):
// 需要申请的权限。 String[] permissions = { Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_SMS, ... }; String[] deniedPermissions = getDeinedPermission(permissions); if(deniedPermissions.length <= 0) { // TODO do something... } else { requestPermission(deniedPermissions, callback); }
霸特,此时应用如果安装在Android O的系统中我们会发现,判断了有WRITE_EXTERNAL_STORAGE权限后去读取存储卡内容时应用崩溃了,原因就是我们没有申请READ_EXTERNAL_STORAGE权限。
对Android O运行时权限策略变化的应对方案
针对Android O的运行时权限策略的特点,为了适配各个版本的系统,我们的代码会变成如下方式(伪代码):
// 需要申请的权限。 String[] permissions = { Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.READ_SMS, ... }; String[] deniedPermissions = getDeinedPermission(permissions); if(deniedPermissions.length <= 0) { // TODO do something... } else { requestPermission(deniedPermissions, callback); }但是这样会存在两个问题,一是有的权限组权限比较多,开发者难易全部记住;二是READ_EXTERNAL_STORAGE这个权限常量是在API 16时才被添加到SDK中,类似这样的权限常量还有好几个,有的甚至在Android M时才被添加到SDK中。如果我们强制写了,当APP运行在低版本的系统中时,还是会崩溃。有人就说了,我们在申请之前判断系统版本不就好啦?当然,如果你不嫌麻烦,这是完全可以的。
升级方案
因此我们总结出一个更优的方案,归根结底就是申请权限时要申请权限组,而不是单一的某个权限。所以我们按照系统权限组分类,把一个组的常量放到一个数组中,并根据系统版本为这个数组赋值,于是乎产生了这样一个类:
public final class Permission { public static final String[] CALENDAR; // 读写日历。 public static final String[] CAMERA; // 相机。 public static final String[] CONTACTS; // 读写联系人。 public static final String[] LOCATION; // 读位置信息。 public static final String[] MICROPHONE; // 使用麦克风。 public static final String[] PHONE; // 读电话状态、打电话、读写电话记录。 public static final String[] SENSORS; // 传感器。 public static final String[] SMS; // 读写短信、收发短信。 public static final String[] STORAGE; // 读写存储卡。 static { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { CALENDAR = new String[]{}; CAMERA = new String[]{}; CONTACTS = new String[]{}; LOCATION = new String[]{}; MICROPHONE = new String[]{}; PHONE = new String[]{}; SENSORS = new String[]{}; SMS = new String[]{}; STORAGE = new String[]{}; } else { CALENDAR = new String[]{ Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR}; CAMERA = new String[]{ Manifest.permission.CAMERA}; CONTACTS = new String[]{ Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS, Manifest.permission.GET_ACCOUNTS}; LOCATION = new String[]{ Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION}; MICROPHONE = new String[]{ Manifest.permission.RECORD_AUDIO}; PHONE = new String[]{ Manifest.permission.READ_PHONE_STATE, Manifest.permission.CALL_PHONE, Manifest.permission.READ_CALL_LOG, Manifest.permission.WRITE_CALL_LOG, Manifest.permission.USE_SIP, Manifest.permission.PROCESS_OUTGOING_CALLS}; SENSORS = new String[]{ Manifest.permission.BODY_SENSORS}; SMS = new String[]{ Manifest.permission.SEND_SMS, Manifest.permission.RECEIVE_SMS, Manifest.permission.READ_SMS, Manifest.permission.RECEIVE_WAP_PUSH, Manifest.permission.RECEIVE_MMS}; STORAGE = new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}; } } }在Android M以前使用某权限是不需要用户授权的,只要在Manifest中注册即可,在Android M之后需要注册并申请用户授权,所以我们根据系统版本在Android M以前用一个空数组作为权限组,在Android M以后用真实数组权限。
因为要传入多个权限组,所以我们约定的两个方法就不够用了,所以我们加两个方法:
/** * 拿到没有被授权的权限。 */ String[] getDeinedPermission(String... permissions); /** * 请求几个权限。 */ void requestPermission(String... deinedPermissions); /** * 拿到没有被授权的权限。 */ String[] getDeinedPermission(String[]... permissions); /** * 请求几个权限。 */ void requestPermission(String[]... deinedPermissions);于是我们申请权限的代码就简化成这样了:
// 这方法里面判断版本,返回空数组或者没有权限的数组。 String[] deniedPermissions = getDeinedPermission(Permission.STORAGE, Permission.SMS); if(deniedPermissions.length <= 0) { // TODO do something... } else { requestPermission(deniedPermissions, callback); }当然这不是最简化的,但是已经足以兼容到Android O的权限策略的变化了。
如果是AndPermission如何做到最简
这里只是介绍下AndPermisison也兼容了Android O的权限变化,如果你觉得这个项目不适合你,你可以自行封装一个,我比较鼓励开发者自己动手,下面是开源地址:
https://github.com/yanzhenjie/AndPermission
它的一些简单的特点:
1. 链式调用,一句话申请权限,省去复杂的逻辑判断。
2. 支持注解回调结果、支持Listener回调结果。
3. 拒绝一次某权限后,再次申请该权限时可使用Rationale向用户说明申请该权限的目的,在用户同意后再继续申请,避免用户勾选不再提示而导致不能再次申请该权限。
4. 就算用户拒绝权限并勾选不再提示,可使用SettingDialog提示用户去设置中授权。
5. RationaleDialog和SettingDialog允许开发者自定义。
6. AndPermission自带默认对话框除可自定义外,也支持国际化。
7. 支持在任何地方申请权限,不仅限于Activity和Fragment等。
8. 支持申请权限组、兼容Android8.0。
申请多个权限组示例:
AndPermission.with(this) .permission(Permission.CAMERA, Permission.SMS) // 多个权限组。 .callback(new PermissionListener() { @Override public void onSucceed(int i, @NonNull List list) { // TODO do something... } @Override public void onFailed(int i, @NonNull List list) { // TODO 用户没有同意授权,一般弹出Dialog让用户去Setting中授权。 } }) .start();申请单个或者某几个权限示例,因为Android O的出现,现在不鼓励这样使用了,但是在Android O正式发布前没有问题:
AndPermission.with(this) .permission( // 多个不同权限组权限,现在不鼓励这样使用了,但是在Android O正式发布前没有问题。 Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_SMS ) .callback(new PermissionListener() { @Override public void onSucceed(int i, @NonNull List list) { // TODO do something... } @Override public void onFailed(int i, @NonNull List list) { // TODO 用户没有同意授权,一般弹出Dialog让用户去Setting中授权。 } }) .start();