如何写一个无侵入式的动态权限申请Android框架?
1、核心逻辑
在Activity或者fragment中,写在几个方法写一些注释,用来表示权限申请成功
,申请失败
,多次拒绝
。以上就是使用者需要做的。
简单吧,简单就对了,不用传任何上下文。只需要写注解。给大家看下。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void permissionRequestTest(View view) {
testRequest();
}
// 申请权限 函数名可以随意些
@Permission(value = Manifest.permission.READ_EXTERNAL_STORAGE, requestCode = 200)
public void testRequest() {
Toast.makeText(this, "权限申请成功...", Toast.LENGTH_SHORT).show();
}
// 权限被取消 函数名可以随意些
@PermissionCancel
public void testCancel() {
Toast.makeText(this, "权限被拒绝", Toast.LENGTH_SHORT).show();
}
// 多次拒绝,还勾选了“不再提示”
@PermissionDenied
public void testDenied() {
Toast.makeText(this, "权限被拒绝(用户勾选了 不再提示),注意:你必须要去设置中打开此权限,否则功能无法使用", Toast.LENGTH_SHORT).show();
}
2、实现
需要用到的技术有Aspect、注解、反射
2.1、Aspect
它的作用就是劫持被注释的方法的执行。比如上方testRequest()是用来请求权限的,但是我在ASPECT中配置拦截@permission注释的方法。先做判断。
如果没有听过Aspect的话,AOP面向切面编程,大家应该听说过,它可以用来配置事务、做日志、权限验证、在用户请求时做一些处理等等。而用@Aspect做一个切面,就可以直接实现。
2.2、PermissionAspect
我们会创建一个PermissionAspect类,整一个切点,让@Permission被劫持。
// AOP 思维 切面的思维
// 切点 --- 是注解 @
// * * 任何函数 都可以使用 此注解
//(..) 我要带参数 带的参数就是后面那个 @annotation(permission)意思就是 参数里是permission。这样我就拿到了Permission注解它里面的参数
//这样通过切点就拿到了下面这个注解
//@Permission(value = Manifest.permission.READ_EXTERNAL_STORAGE, requestCode = 200) 这就是 Permission permission
@Pointcut
("execution(@com.derry.premissionstudy.permission.annotation.Permission * *(..)) && @annotation(permission)")
//那么这个
// @Permission == permission
public void pointActionMethod(Permission permission) {
} // 切点函数
//切面
@Around("pointActionMethod(permission)")
public void aProceedingJoinPoint(final ProceedingJoinPoint point,Permission permission) throws Throwable{
//我需要拿到 MainActivity this
这样@Permission就被切点劫持了,然后方法就会跑到切面aProceedingJoinPoint。然后获取上下文Context,把权限请求交给一个透明的Activity来做。做完之后判断结果,用户是同意了还是拒绝了还是曲线了。同意了直接执行point.proceed(),其他方式则通过Activity或者fragment获取带注解的方法,反射执行即可。
//切面
@Around("pointActionMethod(permission)")
public void aProceedingJoinPoint(final ProceedingJoinPoint point,Permission permission) throws Throwable{
//我需要拿到 MainActivity this
Context context = null;
// MainActivity this == thisObject
final Object thisobject = point.getThis();
// context初始化
if(thisobject instanceof Context){
context = (Context) thisobject;
} else if(thisobject instanceof Fragment){
context = ((Fragment) thisobject).getActivity();
}
// 判断是否为null
if (null == context || permission == null) {
throw new IllegalAccessException("null == context || permission == null is null");
}
//trestRequest 次函数被控制了 不会执
//
//动态申请 危险权限 透明的空白的Ativity
//这里一定要得知接口三个状态 已经授权 取消授权 拒绝授权
//调用 空白的Actiivty 开始授权
MyPermissionActivity.requestPermissionAction(context, permission.value(), permission.requestCode(), new IPermission() {
@Override
public void ganted() {
try {
point.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
@Override
public void cancel() {
PermissionUtils.invokeAnnotion(thisobject, PermissionCancel.class);
}
@Override
public void denied() {
PermissionUtils.invokeAnnotion(thisobject, PermissionDenied.class);
}
});
}
2.3、空白执行权限的Activity
执行请求权限的Activity的的相应方法会流到PermissionAspect,然后到空白Activity请求。请求完之后的结果,再通过回调传回去就好了。
public class MyPermissionActivity extends AppCompatActivity {
// 定义权限处理的标记, ---- 接收用户传递进来的
private final static String PARAM_PERMSSION = "param_permission";
private final static String PARAM_PERMSSION_CODE = "param_permission_code";
public final static int PARAM_PERMSSION_CODE_DEFAULT = -1;
private String[] permissions;
private int requestCode;
// 方便回调的监听 告诉外交 已授权,被拒绝,被取消
private static IPermission iPermissionListener;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my_permission);
permissions = getIntent().getStringArrayExtra(PARAM_PERMSSION);
requestCode = getIntent().getIntExtra(PARAM_PERMSSION_CODE, PARAM_PERMSSION_CODE_DEFAULT);
if(permissions == null){
this.finish();
return;
}
// 能够走到这里,就开始去检查,是否已经授权了
boolean permissionRequest = PermissionUtils.hasPermissionRequest(this,permissions);
if (permissionRequest) {
// 通过监听接口,告诉外交,已经授权了
iPermissionListener.ganted();
this.finish();
return;
}
ActivityCompat.requestPermissions(this,permissions,requestCode);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if(PermissionUtils.requestPermissionSuccess(grantResults)){
iPermissionListener.ganted();//已经授权成功了 告知AspectJ
this.finish();
}
// 没有成功,可能是用户 不听话
// 如果用户点击了,拒绝(勾选了”不再提醒“) 等操作
if(!PermissionUtils.shouldShowRequestPermissionRationable(this,permissions)){
iPermissionListener.denied();
this.finish();
return;
}
// 取消
iPermissionListener.cancel(); // 接口告知 AspectJ
this.finish();
return;
}
// 让此Activity不要有任何动画
@Override
public void finish() {
super.finish();
overridePendingTransition(0, 0);
}
public static void requestPermissionAction(Context context, String[] permissions, int requestCode, IPermission iPermissionLIstener){
MyPermissionActivity.iPermissionListener = iPermissionLIstener;
Intent intent = new Intent(context,MyPermissionActivity.class);
//效果
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
Bundle bundle = new Bundle();
Log.d("TAG", "requestPermissionAction: "+requestCode);
bundle.putInt(PARAM_PERMSSION_CODE,requestCode);
bundle.putStringArray(PARAM_PERMSSION,permissions);
intent.putExtras(bundle);
context.startActivity(intent);
}
}
2.4、其它
app gradle中
apply plugin: 'com.android.application'
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'org.aspectj:aspectjtools:1.8.9'
classpath 'org.aspectj:aspectjweaver:1.8.9'
}
}
android {
compileSdkVersion 29
buildToolsVersion "29.0.2"
defaultConfig {
applicationId "com.netease.premissionstudy"
minSdkVersion 19
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation 'org.aspectj:aspectjrt:1.8.13'
}
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
final def log = project.logger
final def variants = project.android.applicationVariants
variants.all { variant ->
if (!variant.buildType.isDebuggable()) {
log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")
return;
}
JavaCompile javaCompile = variant.javaCompile
javaCompile.doLast {
String[] args = ["-showWeaveInfo",
"-1.8",
"-inpath", javaCompile.destinationDir.toString(),
"-aspectpath", javaCompile.classpath.asPath,
"-d", javaCompile.destinationDir.toString(),
"-classpath", javaCompile.classpath.asPath,
"-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
log.debug "ajc args: " + Arrays.toString(args)
MessageHandler handler = new MessageHandler(true);
new Main().run(args, handler);
for (IMessage message : handler.getMessages(null, true)) {
switch (message.getKind()) {
case IMessage.ABORT:
case IMessage.ERROR:
case IMessage.FAIL:
log.error message.message, message.thrown
break;
case IMessage.WARNING:
log.warn message.message, message.thrown
break;
case IMessage.INFO:
log.info message.message, message.thrown
break;
case IMessage.DEBUG:
log.debug message.message, message.thrown
break;
}
}
}
}
项目 gradle中
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.1'
classpath 'org.aspectj:aspectjtools:1.8.9'
classpath 'org.aspectj:aspectjweaver:1.8.9'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
3、总结
其实核心就是,用aspect去劫持注解,然后让一个公共的Activity来处理这个事情,然后回调,再反射其它方法执行。
等有时间了给他打成一个包。
作者:KentWang
来源:juejin.cn/post/7245836790039445562
来源:juejin.cn/post/7245836790039445562