一个动态权限库的设计

简介: 在经过上一次尝试剖析源码后,我意识到自己并没有一种比较好的方式去讲解代码,从而无法把自己所知道的知识更好地输出。所以接下来,至少在源码讲解有新想法前,我都不会再去尝试,也尽量减少博客中的非核心代码,而以思路及想法为主。

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

从 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执行申请未被授予的权限yesnoyesno

对应的核心代码则是:

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?回调权限已被拒绝回调权限已被永久拒绝yesnoyesnoyesno

对应的核心代码则是:

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
目录
相关文章
|
SQL 存储 数据库
数据权限这样设计,你觉得如何?
在项目实际开发中我们不光要控制一个用户能访问哪些资源,还需要控制用户只能访问资源中的某部分数据。 控制一个用户能访问哪些资源我们有很成熟的权限管理模型即RBAC,但是控制用户只能访问某部分资源(即我们常说的数据权限)使用RBAC模型是不够的,本文我们尝试在RBAC模型的基础上融入数据权限的管理控制。
2788 1
|
Web App开发
【自然框架】通用权限的视频演示(一):添加角色,权限到功能节点和按钮
写了几个关于权限的东东,好像大家都不大理解,也不太清楚我的权限到底能做什么,所以想来想去还是弄点视频吧,就是屏幕录像,这样大家看起来就方便了吧。      为了大家便于观看视频,我先说一下视频的步骤。
1152 0
|
数据安全/隐私保护
【自然框架】之通用权限的Demo(一):角色的添加和修改
      非常抱歉,我是一个靠激情来工作的人,有心情做什么多快,没心情的时候什么都不爱做。最近很烦,所以速度也很慢。原本打算周一拿出来Demo的,结果延迟了现在。希望大家多多包含。这个Demo并不完整,目前权限方面只实现了角色的添加和修改,其他的还没有实现。
1022 0
|
SQL 数据库
【自然框架】之通用权限(六):权限到节点
      “直率没有错,但是也要考虑对方的承受能力呀!对方都承受不了了,你还直率,那就是你的错了!”  ——我的名言,呵呵。     ====================我就是传说中的,可爱的、无奈的、笑笑而过的分割线====================         继续,这是第六章了。
961 0
|
数据库
【自然框架】之通用权限(九):权限的验证
继续,这是第九章了。本来这张应该好好写的,不过还是先简单介绍一下吧,以后有空再补上详细说明吧。 通用权限想要写的文章目录:(这是第九章)   1、 简介、数据库的总体结构2、 介绍人员表组3、 介绍组织结构表组4、 介绍角色表组5、 介绍“项目自我描述表组”6、 权限到节点7、 权限到...
767 0
【自然框架】之通用权限(七):权限到按钮
      继续,这是第七章了。我已经到了无话可说的地步了。哎,在坚持几章就结束了。第七章到第十章,我打算采用简单说明的方式来做,因为我感觉我这么写好像大家都不打感兴趣,或者说都比较忙,没有时间细看,或者说我写的太乱了,看不明白。
860 0
|
关系型数据库 数据库
通用数据级别权限的框架设计与实现(2)-数据权限的准备工作
查看上篇文章通用数据级别权限的框架设计(1)-相关业务场景的分析",我们要继续做一些准备工作。 我们先要设置当前用户信息的类 /** * @description: 用户对象 * @author: starmark * @create: 2...
847 0