Design of a dynamic permission library

After the last attempt to analyze the source code, I realized that I don't have a better way to explain the code, so I can't output the knowledge I know better. So next, at least until there are new ideas in the source code explanation, I will not try again, and try to reduce the non-core code in the blog, and focus on ideas and ideas. In addition, I will also try to improve the writing style of the technical blog. The paragraphs should be as coherent as possible, and the overall content should be as rhythmic as possible. The goal is to express the subject-related content in a simple way.

Starting from Android 6.0 (API 23), users can grant permissions to applications when they are running (below 6.0, most domestic manufacturers have done similar permission management), rather than granting them when the application is installed. This approach both simplifies the installation process of the app and allows the user to place more restrictions on the functionality of the app.

In the Android system, system permissions are divided into two categories: normal permissions and dangerous permissions. Among them, the normal permission will not directly bring risks to the user's privacy. If the application declares the permission in its manifest, the system will automatically grant the permission. Dangerous permissions, starting from 6.0, need to apply dynamic applications and be granted by users.

The official dynamic permission API was introduced from Android 6.0, so naturally, it is necessary to do a good job in the adaptation of permissions of different versions starting from 6.0 and before 6.0, thus giving birth to some dynamic permission compatible package libraries. for example:

However, when I was going to use it in the project, I found that none of these libraries could fully satisfy my idea, so I thought of building a wheel myself, which is what I'm going to talk about today - hey-permission.

The permission encapsulation library I envision is like this:

  • Compatible handling of interface calls before and after 6.0.
  • You can apply by permission group, and apply for multiple permission groups (that is, variable-length parameters).
  • Before applying, judge whether you have obtained the permissions you have applied for. If there are permissions that have not been obtained, only apply for those permissions that have not been obtained.
  • The following results can be called back separately (the developer will call back if they have a statement):
    • The permission application is approved;
    • The permission application is rejected (you can apply again);
    • The permission application is permanently rejected (re-application will be directly rejected).
  • An interface may have different functions that require the same or different permissions. so:
    • Different requestCodes can be distinguished in the callback method passed by the permission application;
    • The method of permission application rejection (including permanent rejection) can be handled collectively or separately;
    • If the parent class handles it uniformly, the child class can handle the result of a permission application being rejected separately.
  • Supports situations where the same approach is performed regardless of whether the application is approved or rejected.
  • When applying for permission, if it has not been rejected by the user, apply directly. If it is rejected by the user (but not permanently), a dialog box can pop up to remind the user of the importance of permissions.
  • If the application for permission has been permanently rejected, that is, when the request dialog box for applying for permission can no longer pop up, you can guide the user to the application settings to grant the permission.
  • In the callback of the permission application, if the user refuses some permissions, but not permanently, the developer can handle whether to pop up a dialog box to remind the user of the importance of permissions. The rejected method is not called back if the developer has handled it here.
  • Lightweight, does not use compile-time annotations (will be affected by build tools).

So, look at what APIs are provided for this in Android 6.0 (API 23)?

  • Request to apply for multiple permissions.
  • Query whether a certain permission is available.
  • Query whether a UI that prompts the user about the importance of permissions should be displayed. It has the following result:
    • Returns no if it has not been rejected before.
    • If it has been rejected, but you can apply again, return yes.
    • Returns no if permanently rejected.
  • Permission request result callback. It will return the request code, the requested permission and the array of processing results, which correspond one by one.

According to Article 3, we can judge whether the permission is permanently denied when the permission is denied.

Next, we can start to conceive the entire permission application process, which mainly consists of two parts: one is the permission application; the other is the callback of the application result.
Let's draw the process of permission application:

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

The corresponding core code is:

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

Then there is the callback processing of the permission application result:

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

The corresponding core code is:

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

The above is the core logic.

Then consider three questions:

  1. Activity, Fragment, SupportFragment will need to apply for permission and result callback.
  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

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325371725&siteId=291194637