手把手教你撸一个一行代码请求权限的框架

前言

Android6.0之后,谷歌对Android权限策略做出了重大调整,将权限分为了普通权限和危险权限,对于危险权限不仅需要在AndroidManifest.xml中注册,还需要动态申请。这一改变,极大地保护了用户的隐私与安全,但是却给开发者增加了一些麻烦。对于Android6.0权限策略的具体变化,这里不做过多描述,直接上代码体验下对我们平时开发的影响,下面以拨打电话为例

1. Android6.0以前拨打电话

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_request);
    findViewById(R.id.callBtn).setOnClickListener(v -> {
        call();
    });
}

private void call() {
    Intent intent = new Intent();
    intent.setAction(Intent.ACTION_CALL);
    intent.setData(Uri.parse("tel:1008611"));
    startActivity(intent);
}

可以看出Android6.0之前的操作完全符合我们的逻辑,点击按钮,直接调用方法就可以拨打电话了

2. Android6.0之后拨打电话

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_request);
    findViewById(R.id.callBtn).setOnClickListener(v -> {
        //检测是否具有权限
        if (ActivityCompat.checkSelfPermission(OriginalMethodActivity.this, Manifest.permission.CALL_PHONE)
                != PackageManager.PERMISSION_GRANTED) {
            //没有就去申请
            ActivityCompat.requestPermissions(OriginalMethodActivity.this, new String[]{Manifest.permission.CALL_PHONE}, 1000);
        } else {
            call();
        }
    });
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode == 1000) {
        //检测用户授权结果 
        if(grantResults[0]==PackageManager.PERMISSION_GRANTED){
            call();
        }else {
            Toast.makeText(this,"权限被拒绝",Toast.LENGTH_SHORT).show();
        }
    }
}

同样的功能,Android6.0之后需要的代码量比之前多的不是一星半点,我们需要用ActivityCompat.checkSelfPermission()检查是否具有权限,如果没有还需要调用ActivityCompat.requestPermissions()进行权限申请,最后在onRequestPermissionsResult()回调中进行处理,虽然这些处理都是流程化的,没有任何技术含量,但是在实际应用中我们避无可避,于是我们实际项目中或许就对它有了一些封装。

3. Android6.0之后拨打电话(基于BaseActivity)

基于BaseActivity封装运行时权限这里不做过多阐述,网上对于这方面封装的也比较多,不外乎是将上面流程化的步骤封装在基类,然后在onRequestPermissionsResult()中将回调分发到子类。个人不是很喜欢这种,基类封装的东西本来就多了,这些能剔除去就尽量剔除去,毕竟它还是个孩子,放过它吧。

4. Android6.0之后拨打电话(基于开源库,比如RxPermissions)

RxPermissions使用比较简单,大大优化了我们的代码

new RxPermissions(this).request(Manifest.permission.CALL_PHONE)
    .subscribe(aBoolean -> {
        if (aBoolean != null && aBoolean.booleanValue()) {
            call();
        }
    });

短短的几行代码就完成了需求,仿佛又回到了10年前。到这里,关于Android6.0权限处理基本方式就介绍完了,接下来进入正题,我们自己来实现一个权限申请框架,为什么要自己重复造轮子呢,GitHub上面开源的那么多?对于这个问题,每个人都有自己的见解,在我看来,别人的轮子再圆也始终是别人的,不重复造轮子的前提是会造轮子。毕竟遨游在代码的海洋里,我们怎么能只甘心做一个“掉包侠”呢。


实战

  1. 为了实现一行代码完成Android6.0权限处理以及尽可能让写法与Android6.0之前相同,我们这里需要使用AspectJ,AspectJ是对AOP的实现,篇幅原因,关于它们的详细使用及原理这里不做讲解。
  2. 既然是造轮子,那我们先建一个Android Library,这里命名为SmartPermission,为啥叫Smart,可能是我不够Smart吧。建好之后回到我们的目的————一行代码完成请求,先看看我们最初的写法
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_request);
    findViewById(R.id.callBtn).setOnClickListener(v -> {
        call();
    });
}
private void call() {
    Intent intent = new Intent();
    intent.setAction(Intent.ACTION_CALL);
    intent.setData(Uri.parse("tel:1008611"));
    startActivity(intent);
}

将这一行代码放在那里才能实现需求呢?还记得butterknife的用法(仅用法,非原理)吗,我们将@OnClick(R.id.xxx)放在某个方法上面便可实现id为xxx控件的点击,这里我们也模仿他的处理,在call()方法上面放一个注解,先新建一个注解SmartPermission

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SmartPermission {
    String[] value();
}

由于需要同时申请多个权限,所以这里使用String[]

  1. 接下来就要使用到AspectJ了,对它不熟悉的可以先去了解下,由于使用AspectJ需要配置很多,这里使用沪江的开源库gradle_plugin_android_aspectjx,我们先按照他的引入说明进行相关配置在项目根目录的build.gradle里依赖AspectJX
dependencies {
    classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0'
}

在app项目的build.gradle里应用插件

apply plugin: 'com.hujiang.android-aspectjx'
  1. 新建类SmartPermissionAspect
@Aspect
public class SmartPermissionAspect {
    @Pointcut("execution(@com.lantian.smartpermission.annotation.SmartPermission * *(..))")
    public void checkPermission() {
    }
    @Around("checkPermission()")
    public void check(ProceedingJoinPoint point) {
    }
}

接下来我们的工作就是补全check()方法,从参数point中我们可以获取SmartPermission注解修饰方法的详细信息,首先,我们要获取需要申请的权限列表

MethodSignature signature = (MethodSignature) point.getSignature();
SmartPermission annotation = signature.getMethod().getAnnotation(SmartPermission.class);
String[] permissions = annotation.value();

有了权限列表后,我们需要先进行处理一下,如果权限列表为空,或者这些权限都是被允许通过的,直接执行原方法就行了

if (permissions == null || permissions.length == 0) {
    proceed(args, point, new String[]{}, new String[]{}, new String[]{});
    return;
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
    proceed(args, point, new String[]{}, new String[]{}, permissions);
    return;
}
//权限过滤,只申请被拒绝了的
final List<String> deniedPermissions = new ArrayList<>();
for (String permission : permissions) {
    if (ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) {
        deniedPermissions.add(permission);
    }
}
if (deniedPermissions.size() == 0) {
    proceed(args, point, new String[]{}, new String[]{}, permissions);
    return;
}
//TODO 权限申请
  1. 接下来就需要申请我们的权限了,这一步模仿了RxPermissions的方式,我们向Activity中添加一个隐藏的Fragment,然后在这个Fragment中完成权限申请,在onRequestPermissionsResult中将结果回调回去,先写一个接口,方便处理结果
public interface PermissionRequestCallback {
    /**
     * 所有申请的权限都被允许
     */
    void onGranted();
    /**
     * 没有全部通过 (grantedPermissions+deniedPermissions+dontAskAgainPermissions)=一共申请的权限
     * @param grantedPermissions  通过了的权限
     * @param deniedPermissions   被拒绝的权限(不包括不再被询问的)
     * @param dontAskAgainPermissions 不再询问的权限
     */
    void onDenied(String[] grantedPermissions, String[] deniedPermissions, String[] dontAskAgainPermissions);
}

然后编写我们的核心Fragment,我们在权限处理完成后将这些结果再处理一下,分为通过的,拒绝了的和不再询问的3类

public class SmartSupportFragment extends Fragment {
    private PermissionRequestCallback callback;
    public void setPermissionRequestCallback(PermissionRequestCallback callback) {
        this.callback = callback;
    }
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (permissions.length == 0 && grantResults.length == 0) {
            return;
        }
        List<String> grantedPermissions = new ArrayList<>();
        List<String> deniedPermissions = new ArrayList<>();
        List<String> dontAskAgainPermissions = new ArrayList<>();
        for (int i = 0; i < permissions.length; i++) {
            String permission = permissions[i];
            if (grantResults.length <= i || grantResults[i] != PackageManager.PERMISSION_GRANTED) {
                if (ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), permission)) {
                    //被拒绝的权限(仅仅是被拒绝,可以再次申请)
                    deniedPermissions.add(permission);
                } else {
                    //Don’t ask again,即使再次申请,也不会弹出提示,需要进入设置页面,手动开启权限
                    dontAskAgainPermissions.add(permission);
                }
            } else {
                grantedPermissions.add(permission);
            }
        }
        if (callback != null) {
            //没有权限被拒绝
            if (permissions.length == grantedPermissions.size()) {
                callback.onGranted();
            } else {
                callback.onDenied(grantedPermissions.toArray(new String[grantedPermissions.size()]),
                        deniedPermissions.toArray(new String[deniedPermissions.size()]),
                        dontAskAgainPermissions.toArray(new String[dontAskAgainPermissions.size()]));
            }
        }
    }
}
  1. 至此,我们只需要发起申请,就可以收到回调了,回到SmartPermissionAspect类中,在第3步的后面开始申请权限
private void supportPermissions(final Activity activity, final Object[] args, final ProceedingJoinPoint point,
                                    List<String> deniedPermissions, final String[] allPermissions) {
    FragmentManager fm = ((FragmentActivity) activity).getSupportFragmentManager();
    SmartSupportFragment smartSupportFragment = new SmartSupportFragment();
    smartSupportFragment.setPermissionRequestCallback(new PermissionRequestCallback() {
        @Override
        public void onGranted() {
            proceed(args, point, new String[]{}, new String[]{}, allPermissions);
        }
        public void onDenied(String[] grantedPermissions, String[] deniedPermissions, String[] dontAskAgainPermissions) {
            String[] gs = getGrantedPermissions(deniedPermissions, dontAskAgainPermissions, allPermissions);
            proceed(args, point, deniedPermissions, dontAskAgainPermissions, gs);
        }
    });
    fm.beginTransaction().add(smartSupportFragment, TAG_FRAGMENT_SUPPORT).commitAllowingStateLoss();
    fm.executePendingTransactions();
    smartSupportFragment.requestPermissions(deniedPermissions.toArray(new String[deniedPermissions.size()]), 65535);
}

这里获取FragmentManager需要用到activity,那么这个activity如何获取呢,我们可以获取当前最顶部正在显示的Activity,所以需要先编写一个工具类,记录打开的Activity

public class SmartPermissionUtil {
    private static SmartPermissionUtil instance;
    private List<Activity> activities = new ArrayList<>();
    public static SmartPermissionUtil getInstance() {
        if (instance == null) {
            synchronized (SmartPermissionUtil.class) {
                if (instance == null) {
                    instance = new SmartPermissionUtil();
                }
            }
        }
        return instance;
    }
    public Activity getTopActivity() {
        if (activities == null) {
            throw new RuntimeException("Activity是空的");
        } else {
            for (int i = activities.size() - 1; i >= 0; i++) {
                Activity activity = activities.get(i);
                if (!activity.isDestroyed()) {
                    return activity;
                }
            }
            throw new RuntimeException("Activity是空的");
        }
    }
    public void init(Application context) {
        context.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
            public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
                if (!activities.contains(activity)) {
                    activities.add(activity);
                }
            }
            public void onActivityDestroyed(Activity activity) {
                activities.remove(activity);
            }
            //省略其他方法
    }
}

这里调用registerActivityLifecycleCallbacks()方法需要context,这个context我们可以让开发者在Application#onCreate()中进行初始化,但是这样无疑又多了一行代码,这里我们可以使用ContentProvider来进行初始化

public class SmartProvider extends ContentProvider {
    public boolean onCreate() {
        SmartPermissionUtil.getInstance().init((Application) getContext());
        return true;
    }
    //省略其他方法
}
<provider
    android:authorities="${applicationId}.smartpermission.provider"
    android:multiprocess="true"
    android:exported="false"
    android:name=".provider.SmartProvider"/>
  1. 最后一步,我们只需要根据回调的结果调用下原方法就可以了
public void proceed(Object[] args, ProceedingJoinPoint point,
                    String[] deniedPermissions, String[] dontAskAgainPermissions, String[] grantedPermissions) {
    boolean allGranted = deniedPermissions.length == 0 && dontAskAgainPermissions.length == 0;//是否全部允许
    if (allGranted) {
        point.proceed(args);
    } else {
        //TODO
    }
}
  1. 至此,我们一行代码请求权限就完成了,但是目前我们只适配了用户允许权限的情况,只有在这种情况下才会调用我们真正需要使用权限的方法,而在实际情况中,我们可能还需要在权限被拒绝的情况下提示并引导用户开启权限,所以我们可以再加点料,将权限的结果封装在一个对象中,然后传回原方法
public class SmartPermissionResult {
    private boolean allGranted;
    //被拒绝的权限(不包括不再被询问的)
    private String[] deniedPermissions;
    //不再询问的权限
    private String[] dontAskAgainPermissions;
    //通过了的权限
    private String[] grantedPermissions;
}

然后在第6步方法中将权限申请结果塞到SmartPermissionResult中去

public void proceed(Object[] args, ProceedingJoinPoint point,String[] deniedPermissions, String[] dontAskAgainPermissions, String[] grantedPermissions) {
    SmartPermissionResult smartPermissionResult = null;
    if (args != null && args.length != 0) {
        for (Object arg : args) {
            if (arg instanceof SmartPermissionResult) {
                smartPermissionResult = (SmartPermissionResult) arg;
                break;
            }
        }
    }
    boolean allGranted = deniedPermissions.length == 0 && dontAskAgainPermissions.length == 0;//是否全部允许
    if (smartPermissionResult != null) {
        smartPermissionResult.setAllGranted(allGranted);
        smartPermissionResult.setGrantedPermissions(grantedPermissions);
        smartPermissionResult.setDeniedPermissions(deniedPermissions);
        smartPermissionResult.setDontAskAgainPermissions(dontAskAgainPermissions);
        point.proceed(args);
    } else {
        if (allGranted) {
            point.proceed(args);
        } else {
        }
    }
}

最后我们就可以像View.getLocationInWindow(location)一样,修改下我们的call()方法,新增一个SmartPermissionResult参数,用来接收权限申请的结果

@SmartPermission({Manifest.permission.CALL_PHONE,Manifest.permission.READ_EXTERNAL_STORAGE,Manifest.permission.CAMERA})
private void call(String arg1, SmartPermissionResult smartPermissionResult, String arg2) {
    if (smartPermissionResult.isAllGranted()) {
        Log.i(TAG, "参数=" + arg1 + "---" + arg2);
    } else {
        Log.i(TAG, "不再询问的权限" + Arrays.toString(smartPermissionResult.getDontAskAgainPermissions()) + "");
        Log.i(TAG, "允许的权限" + Arrays.toString(smartPermissionResult.getGrantedPermissions()) + "");
        Log.i(TAG, "被拒绝权限" + Arrays.toString(smartPermissionResult.getDeniedPermissions()) + "");
    }
}
  1. 独乐乐不如众乐乐,我们还可以将库发布到jitpack以供大家使用
    1. 在根build.gradle配置classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
    2. smartpermissionbuild.gradle配置apply plugin: 'com.github.dcendents.android-maven'group='com.github.username'
    3. 将项目提交到GitHub,打开项目的GitHub主页,创建一个Release,然后点Publish release
    4. 登录https://jitpack.io/然后填写Library的Github地址,点击Look up 就可以在项目中使用我们的smartpermission
  2. 缺陷(TODO)
    1. 由于系统限制,如果我们连续调用两次call()方法,真正的权限回调其实只有一次,虽然这种需求不太合理,但这也是我们框架的不足之处,后续可以考虑将请求添加到队列,一个一个执行
    //Activity#requestPermissions()方法中mHasCurrentPermissionsRequest为true时直接返回,此时permissions大小为0
    if (mHasCurrentPermissionsRequest) {
        Log.w(TAG, "Can request only one set of permissions at a time");
        // Dispatch the callback with empty arrays which means a cancellation.
        onRequestPermissionsResult(requestCode, new String[0], new int[0]);
        return;
    }
    
    1. 一般情况我们在请求权限之前会先弹框提示用户为什么需要这些权限,在权限被拒绝之后提示用户开启(仅拒绝)或者引导用户跳转设置界面开启权限(拒绝且不再询问),虽然这两步不是我们权限请求的核心,但是加上的话应该会更加方便开发者
    2. 等你来提

写在最后

  1. 虽然是手把手“教”你撸一个权限框架,但更多的是自省以及记笔记,防止忘了
  2. 初步尝试,文笔和技术都有待提高,如果有什么错误还请各位大佬指出
  3. 欢迎star https://github.com/oslantian/SmartPermission

参考

  1. https://blog.csdn.net/guolin_blog/article/details/106181780
  2. https://www.jianshu.com/p/efc42b63202e
  3. https://www.jianshu.com/p/f577aec99e17
  4. https://www.jianshu.com/p/d287db9fae38?from=timeline&isappinstalled=0
  5. https://github.com/tbruyelle/RxPermissions
  6. https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx
  7. http://www.apkbus.com/thread-591403-1-3.html