在经过上一次尝试剖析源码后,我意识到自己并没有一种比较好的方式去讲解代码,从而无法把自己所知道的知识更好地输出。所以接下来,至少在源码讲解有新想法前,我都不会再去尝试,也尽量减少博客中的非核心代码,而以思路及想法为主。另外,我也将尝试改进技术博客的笔法,段落之间尽量连贯,整体内容尽量有节奏感,目标是做到深入浅出地表达出主题相关内容。
从 Android 6.0(API 23)开始,用户可以在应用运行时向其授予权限(6.0 以下,国内厂商也多数做了类似的权限管理),而不是在应用安装时授予。这一方法既可以简化应用的安装过程,也可以让用户对应用的功能进行更多的限制。
在 Android 系统中,系统权限分为两类:正常权限以及危险权限。其中正常权限是不会直接给用户隐私权带来风险的,如果应用在其清单中声明了该权限,则系统会自动授予该权限。而危险权限,从 6.0 开始,而需要应用动态申请,并且由用户授予。
官方的动态权限 API 是从 Android 6.0 才引入的,那么,自然需要做好6.0 起及 6.0 之前的不同版本的权限适配,从而催生了一些动态权限兼容封装库。比如:
- 谷歌官方的 googlesamples/easypermissions
- 基于编译期注解的 permissions-dispatcher/PermissionsDispatcher
- yanzhenjie/AndPermission
- 其他的封装库等等,就不一一列举了。
然而,我在项目中准备去使用的时候,却发现这几个库都无法完全满足我的想法,于是就想到自己也去造一个轮子,也就是今天要讲的——hey-permission。
我所设想的权限封装库是这样子的:
- 6.0 前后接口调用的兼容处理。
- 可以按权限组申请,及申请多个权限组(也就是可变长参数)。
- 申请前判断是否获得所申请的权限,如果有未获得的权限,则只申请这些未获得的权限。
- 能分别回调以下结果(开发者有声明则回调):
- 权限申请通过;
- 权限申请被拒绝(还可再申请);
- 权限申请被永久拒绝(再申请会直接回调拒绝)。
- 一个界面可能有不同的功能需要分别申请相同或不同的权限。所以:
- 权限申请通过的回调方法里能区分不同的 requestCode;
- 权限申请被拒绝(包括永久拒绝)的方法可以统一处理,也可以分开处理;
- 如果父类统一处理,子类可以单独处理某个权限申请被拒绝的结果。
- 支持不管申请通过还是拒绝都执行同样方法的情况。
- 申请权限时,如果没有被用户拒绝过,则直接申请。如果被用户拒绝过(但不是永久拒绝),则能弹出提示用户权限重要性的对话框。
- 如果申请权限时,已经被永久拒绝,也就是无法再弹出申请权限的请求对话框时,可以引导用户到应用设置里面授予权限。
- 权限申请的回调中,如果用户拒绝了部分权限,但不是永久拒绝,则开发者可以处理是否弹出提示用户权限重要性的对话框。如果开发者在这里已处理,则不回调被拒绝的方法。
- 轻量,不使用编译期注解(会受构建工具影响)。
那么,再看看 Android 6.0(API 23)里都为此提供了哪些 API ?
- 请求申请多个权限。
- 查询是否具有某个权限。
- 查询是否应该显示提示用户权限重要性的UI。它有如下结果:
- 如果在之前没有被拒绝过,则返回否。
- 如果被拒绝过,但还可以再申请,则返回是。
- 如果被永久拒绝,则返回否。
- 权限申请结果回调。它会返回请求码,所申请的权限以及处理结果的数组,它们一一对应。
其中根据第三条,我们可以在权限被拒绝时来判断是否被永久拒绝。
接下来,我们可以开始构思整个权限申请的流程,它主要有两部分:一是权限申请;二是申请结果的回调。
下面来绘制一下权限申请的流程:
对应的核心代码则是:
private static void requestPermissions(@NonNull BasePermissionInvoker invoker,
@IntRange(from = 0) int requestCode,
@Size(min = 1) @NonNull String[]... permissionSets) {
final List<String> permissionList = new ArrayList<>();
for (String[] permissionSet : permissionSets) {
permissionList.addAll(Arrays.asList(permissionSet));
}
final String[] permissions = permissionList.toArray(new String[permissionList.size()]);
if (hasPermissions(invoker.getContext(), permissions)) {
notifyAlreadyHasPermissions(invoker, requestCode, permissions);
return;
}
if (invoker.shouldShowRequestPermissionRationale(permissions)) {
if (invokeShowRationaleMethod(false, invoker, requestCode, permissions)) {
return;
}
}
invoker.executeRequestPermissions(requestCode, permissions);
}
然后是权限申请结果的回调处理:
对应的核心代码则是:
private static void onRequestPermissionsResult(
@NonNull BasePermissionInvoker invoker, @IntRange(from = 0) int requestCode,
@Size(min = 1) @NonNull String[] permissions, @NonNull int[] grantResults) {
if (!invoker.needHandleThisRequestCode(requestCode)) {
return;
}
final List<String> granted = new ArrayList<>();
final List<String> denied = new ArrayList<>();
for (int i = 0; i < permissions.length; i++) {
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
granted.add(permissions[i]);
} else {
denied.add(permissions[i]);
}
}
if (denied.isEmpty()) {
// all permissions were granted
invokePermissionsResultMethod(PermissionsGranted.class, invoker, requestCode, granted);
invokePermissionsResultMethod(PermissionsResult.class, invoker, requestCode, denied);
return;
}
final String[] deniedPermissions = denied.toArray(new String[denied.size()]);
boolean neverAskAgain = true;
if (invoker.shouldShowRequestPermissionRationale(deniedPermissions)) {
neverAskAgain = false;
if (invokeShowRationaleMethod(true, invoker, requestCode, deniedPermissions)) {
return;
}
}
if (neverAskAgain) {
invokePermissionsResultMethod(PermissionsNeverAskAgain.class,
invoker, requestCode, denied);
} else {
invokePermissionsResultMethod(PermissionsDenied.class, invoker, requestCode, denied);
}
invokePermissionsResultMethod(PermissionsResult.class, invoker, requestCode, denied);
}
以上就是最核心的逻辑。
接下来思考三个问题:
- Activity, Fragment, SupportFragment 都会需要申请权限及结果回调。
- 采用哪种形式进行回调?是回调接口还是注解?
- 在显示权限重要性的回调里,需要提供一个对象用于实际上的直接发起权限申请。
对于第一个问题,我是参考了 google 官方的 easypermissions,对实际发起权限申请的 Activity, Fragment 及 SupportFragment 做了一层包装,抽象为 BasePermissionInvoker
抽象类,并定义了一些依赖它们去实现的行为,如 shouldShowRequestPermissionRationale(@NonNull String... permissions)
,startActivityForResult(Intent intent, int requestCode)
等。
而发起权限的方法并没有定义在这里,这是考虑到了第三个问题的对象只需要直接申请权限的接口,而不需要暴露其他接口。所以针对第三个问题,我另外定义了 PermissionRequestExecutor
接口,里面只声明了一个方法 void executeRequestPermissions(int code, String... permissions);
,由 BasePermissionInvoker
去实现。
然后继承自 BasePermissionInvoker
,在 Activity, Fragment 及 SupportFragment 的封装中对这些方法做具体的实现。
最后是对第二个问题的思考,回调方式如何选择?
一是开发者向用户显示权限重要性的提示,这个开发者可以显示也可以不显示。并且如上面的流程图所示,如果权限申请被拒绝了,而开发者显示了权限重要性的话,那么即表明权限申请已被拒绝,则不用再回调被拒绝的方法。否则表示开发者未处理,则要回调到权限被拒绝的方法。所以这种情况可以使用回调接口的方法,通过返回值来判断。
第二种情况是权限申请结果的回调。申请结果的回调有通过、拒绝、永久拒绝多种,并且我们希望可以在 Activity 或 Fragment 等的基类能够统一处理被拒绝的情况,另一方面,我们需要不同的权限请求,能够在各自的方法里去处理回调。基于这些想法,因此采用了注解的方式。通过不同的 requestCode
来回调对应的方法;并且被拒绝的注解,它接收的参数是应该是数组类型,也就是我们可以在基本统一处理,也可以在某一个类通过再声明对应的注解来单独处理某一种情况。
以上就是 hey-permission 的整个结构设计。具体实现时会遇到一些细节问题,比如判断权限时考虑 6.0 之前的 Ops,比如 SupportFragment 的权限申请结果是通过 AppCompatActivity 来分发,比如回调的注解方法的参数,等等,这些就不在这里赘述,具体可参考项目源码,地址为:https://github.com/parkingwang/hey-permission 。
目前已实现如下特性:
- 单个权限/权限组申请
- 注解回调结果
-
@PermissionsGranted
申请权限均被允许 -
@PermissionsDenied
申请权限被拒绝(下次还可询问用户) -
@PermissionsNeverAskAgain
申请权限被永久拒绝 -
@PermissionsResult
申请权限结果(允许或拒绝都会回调)
-
- 注解回调方法可以是任意参数
- 如果参数中有 int,则第一个 int 参数将接收此次的 requestCode
- 如果参数中有 List,则第一个 List 参数将接收此次申请通过(
@PermissionsGranted
)或被拒绝(其他注解)的 permissions
- 回调注解支持处理多个 requestCode
- 仅申请允许的注解只能处理单个 requestCode
- 其他权限结果注解支持多个 requestCode,如果为空则表示处理所有 requestCode
- 提示用户权限的重要性(回调方法)
- 默认的对话框供调用
- 被永久拒绝后提示用户去系统设置添加权限的对话框
- 支持以下组件
- Activity
- Fragment
- SupportFragment