AOP学习总结-AspectJ编写权限判断框架

当我们做权限判断和申请时,虽然有很多优秀的框架去帮我们完成,但是是否有想过这样的一个问题,以使用 RxPermissions 为例,每个权限判断的地方都要这样调用:

RxPermissions rxPermissions = new RxPermissions(this);
rxPermissions
    .request(Manifest.permission.CAMERA)
    .subscribe(granted -> {
        if (granted) { // Always true pre-M
           // I can control the camera now
        } else {
           // Oups permission denied
        }
    });

是不是觉得非常麻烦呢,能否像 PermissionsDispatcher 那样,用注解去优雅的实现权限申请的问题,下面通过 AspectJ 简单的实现一下该功能。
首先,我希望权限申请效果是这样的:

//  点击一个按钮进行权限申请
findViewById(R.id.button)
        .setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                requestPermissions();
            }
        });

//通过注解可以同时申请一个或多个权限,当全部权限申请成功后会自动执行 requestPermissions 方法。
@GetPermissions({
        "android.permission.READ_EXTERNAL_STORAGE",
        "android.permission.WRITE_EXTERNAL_STORAGE",
        "android.permission.CAMERA"
})
public void requestPermissions() {
    Log.i("MainActivity", "==权限申请成功==");
}

//通过注解标记申请失败时的操作,并且在参数中获取到申请失败的权限
@OnPermissionDenied
public void requestPermissionsFail(List<String> failPermissions) {
    Log.i("MainActivity", "==权限申请失败,失败的权限有:" + failPermissions.toString());
}

第一步

第一步先创建两个注解:

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

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface OnPermissionDenied {
}

第二步

创建 PermissionsAspect 类,并且加上 @Aspect 注解:

@Aspect
public class PermissionsAspect {
}

通过 @Pointcut 注解添加匹配规则,这里只是简单的匹配,在实际项目中,应该匹配更多的情况:

@Pointcut("execution(@com.lzx.aop.annotation.GetPermissions * *(..)) && @annotation(permission)")
public void methodGetPermissions(GetPermissions permission) {
}

使用 @Around 注解处理权限申请:

@Around("methodGetPermissions(permission)")
public void aroundPermissions(final ProceedingJoinPoint joinPoint, GetPermissions permission) {
}

我们发现,@Around 注解的参数是跟上面定义的方法一样的。下面看看在 aroundPermissions 方法里面需要做什么。

首先,因为权限申请回调需要在 Activity 或 Fragment 里面,所以这里我们先模仿 RxPermission 那样创建一个 Fragment 去做权限申请(代码最后贴出),这个Fragment 我们叫它 PermissionsFragment。创建 Fragment 需要先拿到 FragmentManager,所以我们先要得到 Activity 或者 Fragment 对象:

final Object object = joinPoint.getThis();
if (object instanceof AppCompatActivity) {
    AppCompatActivity activity = (AppCompatActivity) object;
    mPermissionsFragment = getLazySingleton(activity.getSupportFragmentManager());
} else if (object instanceof Fragment) {
    Fragment fragment = (Fragment) object;
    mPermissionsFragment = getLazySingleton(fragment.getChildFragmentManager());
}
if (mPermissionsFragment == null) {
    return;
}
final Context context = mPermissionsFragment.get().getActivity();
if (context == null) {
    return;
}

然后获取到需要申请的权限数组,还有创建一个 list,用来存放申请失败的权限:

 String[] permissions = permission.value();
final List<String> failPermissions = new ArrayList<>();

接下来开始权限申请:

mPermissionsFragment.get()
        .requestPermissions(permissions)
        .setPermissionsListener(new PermissionsFragment.OnRequestPermissionsListener() {
            @Override
            public void onRequestResult(String[] permissions, int[] grantResults, boolean[] shouldShowRequestPermissionRationale) {
                int failNumber = 0;
                boolean showGoToSettingActivity = false;
                for (int i = 0, size = permissions.length; i < size; i++) {
                    boolean granted = grantResults[i] == PackageManager.PERMISSION_GRANTED;
                    boolean rationale = shouldShowRequestPermissionRationale[i];
                    if (!granted) {
                        failNumber++;
                        failPermissions.add(permissions[i]);
                    }
                    if (!granted && !rationale) {
                        showGoToSettingActivity = true;
                    }
                }
                goToSettingActivity(showGoToSettingActivity, context);
                if (failNumber == 0) {
                    onPermissionGranted(joinPoint);
                } else {
                    onPermissionDenied(object, failPermissions);
                }
            }
        });

在申请权限的回调中,有三个参数,第一个是权限数组,它跟你传进去的权限数组是一样的,第二个参数,长度对应这第一个数组,它代表着是否申请成功,第三个数组,如果返回 true,则代表你点击了不再提醒的选项。

这里的逻辑是这样的:

  1. 拿到每个权限的申请状态(成功或失败)
  2. 拿到每个权限是否点击了不再提醒选项
  3. 如果权限申请不成功,则通过 failNumber 记录下来,并且存放在 failPermissions 列表中
  4. 如果申请失败并且点击了不再提醒选项则转跳到设置页面引导用户手动打开权限
  5. 如果所有权限都申请成功,则调用 onPermissionGranted 方法处理申请成功逻辑
  6. 如果有权限申请失败,则调用 onPermissionDenied 方法处理申请失败逻辑

goToSettingActivity:

//点击不再提醒后,引导用户至设置页手动授权
private void goToSettingActivity(boolean showGoToSettingActivity, Context context) {
    if (showGoToSettingActivity) {
        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
        Uri uri = Uri.fromParts("package", context.getApplicationContext().getPackageName(), null);
        intent.setData(uri);
        context.startActivity(intent);
    }
}

onPermissionGranted:

//处理权限申请成功,执行申请后的方法
private void onPermissionGranted(ProceedingJoinPoint joinPoint) {
    try {
        joinPoint.proceed();
    } catch (Throwable throwable) {
        throwable.printStackTrace();
    }
}

这里直接调用 joinPoint.proceed(); ,会执行我们一开始所示的权限申请回调。

onPermissionDenied:

//处理权限申请失败
private void onPermissionDenied(Object object, List<String> failPermissions) {
    Class<?> currentClass = object.getClass();
    Method[] methods = currentClass.getDeclaredMethods();
    if (methods.length == 0) {
        return;
    }
    boolean isDeniedAnnotation;
    for (Method method : methods) {
        isDeniedAnnotation = method.isAnnotationPresent(OnPermissionDenied.class);
        if (isDeniedAnnotation) {
            method.setAccessible(true);//允许通过反射访问字段
            OnPermissionDenied permissionDenied = method.getAnnotation(OnPermissionDenied.class);
            if (permissionDenied == null) {
                return;
            }
            handlerDeniedImpl(object, failPermissions, method);
            break;
        }
    }
}

//通过反射调用注解方法
private void handlerDeniedImpl(Object object, List<String> failPermissions, Method method) {
    Class<?>[] types = method.getParameterTypes();
    try {
        //方法没有参数时
        if (types.length == 0) {
            method.invoke(object);
        } else {
            method.invoke(object, failPermissions);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

申请失败处理主要逻辑是:

  1. 找到被注解 OnPermissionDenied 标记的方法
  2. 通过反射去回调方法

下面给出 PermissionsAspect 类的全部代码:

参考文章:一文应用 AOP | 最全选型考量 + 边剖析经典开源库边实践,美滋滋

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容