一个动态权限库的设计

在经过上一次尝试剖析源码后,我意识到自己并没有一种比较好的方式去讲解代码,从而无法把自己所知道的知识更好地输出。所以接下来,至少在源码讲解有新想法前,我都不会再去尝试,也尽量减少博客中的非核心代码,而以思路及想法为主。另外,我也将尝试改进技术博客的笔法,段落之间尽量连贯,整体内容尽量有节奏感,目标是做到深入浅出地表达出主题相关内容。

从 Android 6.0(API 23)开始,用户可以在应用运行时向其授予权限(6.0 以下,国内厂商也多数做了类似的权限管理),而不是在应用安装时授予。这一方法既可以简化应用的安装过程,也可以让用户对应用的功能进行更多的限制。

在 Android 系统中,系统权限分为两类:正常权限以及危险权限。其中正常权限是不会直接给用户隐私权带来风险的,如果应用在其清单中声明了该权限,则系统会自动授予该权限。而危险权限,从 6.0 开始,而需要应用动态申请,并且由用户授予。

官方的动态权限 API 是从 Android 6.0 才引入的,那么,自然需要做好6.0 起及 6.0 之前的不同版本的权限适配,从而催生了一些动态权限兼容封装库。比如:

然而,我在项目中准备去使用的时候,却发现这几个库都无法完全满足我的想法,于是就想到自己也去造一个轮子,也就是今天要讲的——hey-permission。

我所设想的权限封装库是这样子的:

  • 6.0 前后接口调用的兼容处理。
  • 可以按权限组申请,及申请多个权限组(也就是可变长参数)。
  • 申请前判断是否获得所申请的权限,如果有未获得的权限,则只申请这些未获得的权限。
  • 能分别回调以下结果(开发者有声明则回调):
    • 权限申请通过;
    • 权限申请被拒绝(还可再申请);
    • 权限申请被永久拒绝(再申请会直接回调拒绝)。
  • 一个界面可能有不同的功能需要分别申请相同或不同的权限。所以:
    • 权限申请通过的回调方法里能区分不同的 requestCode;
    • 权限申请被拒绝(包括永久拒绝)的方法可以统一处理,也可以分开处理;
    • 如果父类统一处理,子类可以单独处理某个权限申请被拒绝的结果。
  • 支持不管申请通过还是拒绝都执行同样方法的情况。
  • 申请权限时,如果没有被用户拒绝过,则直接申请。如果被用户拒绝过(但不是永久拒绝),则能弹出提示用户权限重要性的对话框。
  • 如果申请权限时,已经被永久拒绝,也就是无法再弹出申请权限的请求对话框时,可以引导用户到应用设置里面授予权限。
  • 权限申请的回调中,如果用户拒绝了部分权限,但不是永久拒绝,则开发者可以处理是否弹出提示用户权限重要性的对话框。如果开发者在这里已处理,则不回调被拒绝的方法。
  • 轻量,不使用编译期注解(会受构建工具影响)。

那么,再看看 Android 6.0(API 23)里都为此提供了哪些 API ?

  • 请求申请多个权限。
  • 查询是否具有某个权限。
  • 查询是否应该显示提示用户权限重要性的UI。它有如下结果:
    • 如果在之前没有被拒绝过,则返回否。
    • 如果被拒绝过,但还可以再申请,则返回是。
    • 如果被永久拒绝,则返回否。
  • 权限申请结果回调。它会返回请求码,所申请的权限以及处理结果的数组,它们一一对应。

其中根据第三条,我们可以在权限被拒绝时来判断是否被永久拒绝。

接下来,我们可以开始构思整个权限申请的流程,它主要有两部分:一是权限申请;二是申请结果的回调。
下面来绘制一下权限申请的流程:

Created with Raphaël 2.1.2 开始 请求申请 多个权限 是否都 有权限? 回调权限 已被授予 结束 是否显示 权限重要性 的UI? 显示提示 权限重要性 的UI 执行申请 未被授予 的权限 yes no yes no

对应的核心代码则是:

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);
}

然后是权限申请结果的回调处理:

Created with Raphaël 2.1.2 开始 权限申请 结果回调 按被授予和 被拒绝对 权限分组 是否没有 被拒绝 的权限? 回调权限 已被授予 结束 是否显示 提示权限 重要性的 UI? 开发者返回 已处理显示 权限重要性 的UI? 回调权限 已被拒绝 回调权限已 被永久拒绝 yes no yes no yes no

对应的核心代码则是:

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);
}

以上就是最核心的逻辑。

接下来思考三个问题:

  1. Activity, Fragment, SupportFragment 都会需要申请权限及结果回调。
  2. 采用哪种形式进行回调?是回调接口还是注解?
  3. 在显示权限重要性的回调里,需要提供一个对象用于实际上的直接发起权限申请。

对于第一个问题,我是参考了 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

猜你喜欢

转载自blog.csdn.net/maosidiaoxian/article/details/79045213