一个动态权限库的设计-阿里云开发者社区

开发者社区> 浩码农> 正文

一个动态权限库的设计

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

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

从 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

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
阿里云服务器怎么设置密码?怎么停机?怎么重启服务器?
如果在创建实例时没有设置密码,或者密码丢失,您可以在控制台上重新设置实例的登录密码。本文仅描述如何在 ECS 管理控制台上修改实例登录密码。
9964 0
使用SSH远程登录阿里云ECS服务器
远程连接服务器以及配置环境
2487 0
windows server 2008阿里云ECS服务器安全设置
最近我们Sinesafe安全公司在为客户使用阿里云ecs服务器做安全的过程中,发现服务器基础安全性都没有做。为了为站长们提供更加有效的安全基础解决方案,我们Sinesafe将对阿里云服务器win2008 系统进行基础安全部署实战过程! 比较重要的几部分 1.
9135 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,阿里云优惠总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系.
13724 0
阿里云ECS云服务器初始化设置教程方法
阿里云ECS云服务器初始化是指将云服务器系统恢复到最初状态的过程,阿里云的服务器初始化是通过更换系统盘来实现的,是免费的,阿里云百科网分享服务器初始化教程: 服务器初始化教程方法 本文的服务器初始化是指将ECS云服务器系统恢复到最初状态,服务器中的数据也会被清空,所以初始化之前一定要先备份好。
7315 0
阿里云服务器ECS登录用户名是什么?系统不同默认账号也不同
阿里云服务器Windows系统默认用户名administrator,Linux镜像服务器用户名root
4443 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,云吞铺子总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系统盘、创建快照、配置安全组等操作如何登录ECS云服务器控制台? 1、先登录到阿里云ECS服务器控制台 2、点击顶部的“控制台” 3、通过左侧栏,切换到“云服务器ECS”即可,如下图所示 通过ECS控制台的远程连接来登录到云服务器 阿里云ECS云服务器自带远程连接功能,使用该功能可以登录到云服务器,简单且方便,如下图:点击“远程连接”,第一次连接会自动生成6位数字密码,输入密码即可登录到云服务器上。
22343 0
+关注
浩码农
开源社区技术专家
222
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
《2021云上架构与运维峰会演讲合集》
立即下载
《零基础CSS入门教程》
立即下载
《零基础HTML入门教程》
立即下载