hook Android 权限请求, 插入权限目的dialog显示

Posted -SOLO-

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了hook Android 权限请求, 插入权限目的dialog显示相关的知识,希望对你有一定的参考价值。

Permission_aim_tip

作用

因为越来越严格的隐私政策要求,需要在申请权限的时候,告知用户需要该权限的目的。为了能快速适配已有项目,需要一个能自动感知权限申请,并显示申请原因的框架。于是编写了该框架。

效果

特点

  1. 100%拦截fragment的权限请求
  2. 100%拦截 RxPermission的权限请求(因为RxPermission就是基于Fragment)
  3. 方便配置,使用json配置
  4. 集成简便,一行代码即可。
  5. 可定制UI

使用

注册代理

只需要在Application中注册即可使用

public class MyApp  extends Application 

    @Override
    public void onCreate() 
        super.onCreate();
        //注册代理,一句话即可使用
         PermissionAimTipHelper.init(new TRSTipShowController
                (new RawAimTipAdapter(this, R.raw.permission_aim_description)));
    

类说明

    1. 类名作用备注
      PermissionAimTipHelper核心类,用于拦截通过ActivityCompat调用权限的过程,唯一构造参数为AimTipShowController必须调用init方法,之后才能通过getInstance获取实例。否则会报错。
      AimTipShowController接口,在拦截到权限请求的时候,通过该类来显示提示信息。
      TRSTipShowControllerAimTipShowController的实现类,需要两个构造参数,其中之一是AimTipAdapter,通过AimTipAdapter将用户申请的权限转换为可以显示的语义化文字。还有一个是DialogStyleData 可以指定dialog的样式,可以缺省。
      AimTipAdapter抽象类,定义了从android权限到需要显示信息的抽象过程
      RawAimTipAdapterAimTipAdapter的实现类,实现了加载raw目录下的配置文件
      DialogStyleData用来保存dialog的布局文件id,和item的布局文件id ,以此来实现样式的自定义

填写配置文件

其中的R.raw.permission_aim_description 是配置文件的id (保存在raw文件夹下)。配置文件如下

[
  
    "androidPermissionNames": [
      "android.permission.ACCESS_FINE_LOCATION",
      "android.permission.ACCESS_COARSE_LOCATION"
    ],
    "showPermissionName": "定位 GPS定位,WIFI定位",
    "permissionAimDescription": "用于新闻下微站展示,自动定位区县栏目展示场景"
  ,
  
    "androidPermissionNames": [
      "android.permission.WRITE_EXTERNAL_STORAGE",
      "android.permission.READ_EXTERNAL_STORAGE"
    ],
    "showPermissionName": "内存读,写",
    "permissionAimDescription": "用于APP写入/下载/保存/读取图片、文件等信息"
  ,
  
    "androidPermissionNames": [
      "android.permission.CAMERA"
    ],
    "showPermissionName": "访问摄像头",
    "permissionAimDescription": "用于拍照、录制视频、扫一扫AR识别等场景"
  ,
  
    "androidPermissionNames": [
      "android.permission.RECORD_AUDIO"
    ],
    "showPermissionName": "录音功能",
    "permissionAimDescription": "通过手机和耳机的麦克 用于录音、语音检索等场景"
  

]

配置说明

字段名称用途
androidPermissionNames用来配置对应的权限,如果用户申请的权限包括在其中。那么就会提示用户。必须是Manifest.permission中定义的常量
showPermissionName用于显示给用户看的权限名称
permissionAimDescription权限目的的描述

Activity中使用

直接使用Activity的requestPermissions方法,将无法拦截。需要使用以下方式请求权限才能拦截

  ActivityCompat.requestPermissions(this, locationPermission, 100);

其中的ActivityCompat是Android本身的适配库

样式自定义

原理是通过指定布局ID来替换样式,只需要在布局ID中出现以下控件即可。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--用来获取recycleView的id,控件必须是RecycleView-->
    <item name="aim_tip_id_recycle_view" type="id"/>
    <!--在item布局中用于显示权限名称,控件必须是TextView-->
    <item name="aim_tip_id_item_title" type="id"/>
    <!--在item布局中用于显示权限的目的,控件必须是TextView-->
    <item name="aim_tip_id_item_content" type="id"/>
</resources>

设置样式

    DialogStyleData dialogStyleData = new DialogStyleData(R.layout.custom_dialog, DialogStyleData.USE_DEFAULT_STYLE);
                //修改样式
                PermissionAimTipHelper.getInstance().setShowController(new TRSTipShowController(new RawAimTipAdapter(v.getContext(), R.raw.permission_aim_description), dialogStyleData));yleData));

运行Demo

更多使用细节请参考这个项目,这是一个Demo项目。

源码

zhuguohui/permission-aim-tip

原理

简单的说一句,从fragment中发起的权限请求最后都会转发到FragmentActivity中的requestPermissionsFromFragment方法。而这个方法的具体的实现是由ActivityCompat实现的

ActivityCompat中的实现

可以看到ActivityCompat中可以设置一个代理,来自己处置权限申请。于是我们就通过这个代理来实现。需要注意的是,我们弹出提示框,用户点击同意以后,需要将FragmentActivity中的变量mRequestedPermissionsFromFragment重新置为true就可以回调到原来的fragment。整个流程就不会有影响。

核心类源码

这一切都是通过PermissionAimTipHelper实现的,其他的弹出提示框都是简单的内容,无需赘言。

package com.trs.app.aim_tip;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;

import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.FragmentActivity;


import com.trs.app.aim_tip.dialog.AimTipShowController;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by zhuguohui
 * Date: 2022/4/14
 * Time: 10:14
 * Desc:用于提示申请权限的目的的。
 * 在申请权限的时候自动提示。
 */
public class PermissionAimTipHelper implements ActivityCompat.PermissionCompatDelegate 
    static PermissionAimTipHelper instance;
    /**
     * 这个字段是FragmentActivity中所有的。如果fragment发起权限申请会被置为true。
     * 但是在Delegate的requestPermissions 方法执行后会必然被置为false。
     * 因此无法正确的回调到fragment的请求中。需要手动设置为true。
     */
    private Field fromFragmentField;
    private AimTipShowController showController;
    private static boolean callInitMethod=false;

    private PermissionAimTipHelper(AimTipShowController showController) 
        this.showController = showController;
        try 
            fromFragmentField = FragmentActivity.class.getDeclaredField("mRequestedPermissionsFromFragment");
            fromFragmentField.setAccessible(true);

         catch (NoSuchFieldException e) 
            e.printStackTrace();
        
    

    public static synchronized PermissionAimTipHelper getInstance() 
        if(!callInitMethod)
            throw new IllegalStateException("请先调用init方法进行初始化");
        
        return instance;
    

    public void setShowController(AimTipShowController showController) 
        this.showController = showController;
    

    /**
     * 入口函数
     * @param showController
     */
    public static synchronized void init(AimTipShowController showController) 
        if (instance != null) 
            return;
        
        callInitMethod=true;
        instance = new PermissionAimTipHelper(showController);
        ActivityCompat.setPermissionCompatDelegate(instance);
    

    @Override
    public boolean requestPermissions(@NonNull Activity activity, @NonNull String[] permissions, int requestCode) 
        boolean fromFragment = false;
        FragmentActivity fragmentActivity = null;
        if (activity instanceof FragmentActivity && fromFragmentField != null) 
            fragmentActivity = (FragmentActivity) activity;
            //检查是否是来自fragment的请求
            try 
                fromFragment = (boolean) fromFragmentField.get(fragmentActivity);
             catch (IllegalAccessException e) 
                e.printStackTrace();
            
        
        boolean finalFromFragment = fromFragment;
        FragmentActivity finalFragmentActivity = fragmentActivity;
        //过滤已经获取的权限,避免重复提示。
        String[] needPermissions=getNeedPermissions(activity,permissions);
        if(needPermissions.length==0) 
            //已经授予全部权限,不需要拦截弹出提示框。
            return false;
        
        showController.showTipDialog(activity, needPermissions, requestCode, () -> 
            if (finalFromFragment) 
                //将字段重置
                try 
                    fromFragmentField.set(finalFragmentActivity, true);
                 catch (IllegalAccessException e) 
                    e.printStackTrace();
                
            
            requestPermissionsDefaultImpl(activity, permissions, requestCode);
        );

        return true;
    

    private String[] getNeedPermissions(Activity activity, String[] permissions) 
        PackageManager packageManager = activity.getPackageManager();
        String packageName = activity.getPackageName();
        List<String> permissionList=new ArrayList<>();
        for (String permission : permissions) 
            if (checkNeedPermission(permission, packageManager, packageName)) 
                permissionList.add(permission);
            
        
        return permissionList.toArray(new String[]);
    

    private boolean checkNeedPermission(String string,PackageManager packageManager,String PackageName) 
        int state = packageManager.checkPermission(string, PackageName);
        if (PackageManager.PERMISSION_GRANTED ==state)
            //已经授予获取已经拒绝就不需要重复获取
            return false;
        
        return true;
    
    @Override
    public boolean onActivityResult(@NonNull Activity activity, int requestCode, int resultCode, @Nullable Intent data) 
        return false;
    


    /**
     * 从ActivityCompat.requestPermissions()方法,copy过来的。
     * 也就是默认的实现
     *
     * @param activity
     * @param permissions
     * @param requestCode
     */
    @SuppressLint("RestrictedApi")
    private void requestPermissionsDefaultImpl(final @NonNull Activity activity,
                                               final @NonNull String[] permissions, final @IntRange(from = 0) int requestCode) 

        if (Build.VERSION.SDK_INT >= 23) 
            if (activity instanceof ActivityCompat.RequestPermissionsRequestCodeValidator) 
                ((ActivityCompat.RequestPermissionsRequestCodeValidator) activity)
                        .validateRequestPermissionsRequestCode(requestCode);
            
            activity.requestPermissions(permissions, requestCode);
         else if (activity instanceof ActivityCompat.OnRequestPermissionsResultCallback) 
            Handler handler = new Handler(Looper.getMainLooper());
            handler.post(new Runnable() 
                @Override
                public void run() 
                    final int[] grantResults = new int[permissions.length];

                    PackageManager packageManager = activity.getPackageManager();
                    String packageName = activity.getPackageName();

                    final int permissionCount = permissions.length;
                    for (int i = 0; i < permissionCount; i++) 
                        grantResults[i] = packageManager.checkPermission(
                                permissions[i], packageName);
                    

                    ((ActivityCompat.OnRequestPermissionsResultCallback) activity).onRequestPermissionsResult(
                            requestCode, permissions, grantResults);
                
            );
        
    



以上是关于hook Android 权限请求, 插入权限目的dialog显示的主要内容,如果未能解决你的问题,请参考以下文章

如何为android中的APP设置默认权限值?

Frida 实现 Hook 功能的强大能力

Android运行时权限对话框未显示

Android检测获取MAC权限--基于Xposed的方法检测

谈谈Android 6.0运行时权限理解

Android ActivityResultContracts 请求权限(封装;含android 11权限变更)