这也许是一句话Android权限适配的更优解决方案
背景
关于运行时的权限不用多说,这个概念已经很久,近期工信部在强推SDK26,我这边做了一些适配工作,其中有一项就是运行时权限,今天将对运行时权限提供一个更优雅的解决方案,如果你还不了解运行时权限,请移步:Android运行时权限浅谈
现状:(以直接调用打电话功能为例)
首先我们项目中可能会有这么一个方法:
/**
* 拨打指定电话
*/
public static void makeCall(Context context, String phoneNumber) {
Intent intent = new Intent(Intent.ACTION_CALL);
Uri data = Uri.parse("tel:" + phoneNumber);
intent.setData(data);
if (!(context instanceof Activity)) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
}
那么在适配动态权限以前,在我们任意用到打电话的业务页面我们可能就是这么用:
public void makeCall() {
Utils.makeCall(BeforeActivity.this, "10086");
}
于是乎,某一天,我们应用要适配targetSdk 26,首先我们要适配的就是动态权限,所以下面的代码就会变成这样:
public void makeCall() {
//6.0以下 直接即可拨打
if (android.os.Build.VERSION.SDK_INT < M) {
Utils.makeCall(BeforeActivity.this, "10086");
} else {
//6.0以上
if (ContextCompat.checkSelfPermission(BeforeActivity.this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(BeforeActivity.this, new String[]{Manifest.permission.CALL_PHONE},
REQUEST_CODE_CALL);
} else {
Utils.makeCall(BeforeActivity.this, "10086");
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE_CALL) {
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(BeforeActivity.this, "本次拨打电话授权失败,请手动去设置页打开权限,或者重试授权权限", Toast.LENGTH_SHORT).show();
} else {
Utils.makeCall(BeforeActivity.this, "10086");
}
}
}
以上就是拨打电话功能新老权限版本的基本实现(还不包括shouldShowRequestPermissionRationale的部分)。
目前也有一些知名的开源库,如PermissionsDispatcher,RXPermission等。虽然也能实现我们的功能,但无论自己适配还是现有开源库方案大体上都会或多或少有以下几个问题:
- 每个页面都要重写onPermissionResult方法、维护requestCode、或者第三方库封装的onPermissionResult方法,如果项目庞大,适配到每个业务点会非常繁琐
- 权限申请还区分Activity和Fragment,又要分别处理
- 每个权限都要写大量的if else代码去做版本判断,判断新老机型分别处理
基于第一个业务繁琐的问题,很多应用选择适配权限的时候,把所用到的敏感权限放在一个特定的页面去申请,比如欢迎页(某知名音乐播放器等),如果授权不成功,则会直接无法进入应用,这样虽然省事,但是用户体验不好,我在应用一打开,提示需要电话权限,用户会很疑惑。这样其实就违背了“运行时授权”的初衷,谷歌希望我们在真正调用的该功能的时候去请求,这样权限请求和用户的目的是一致的,也更容易授予权限成功。
那么能不能做到如下几个点呢?
- 不需要Activity和Fragment作为载体、不需要去重写onPermissionResult。
- 去除版本判断。只需要在一个工具类中把某个方法(如打电话)适配,然后全局调用,做到真正的运行时请求。
- 一行代码完成从权限检查、请求、到最终完成后做事情。
答案当然是有,下面是我们今天的主角:SoulPermission应运而生。
当使用了SoulPermission以后,最直观上看,我们上面的代码就变成了这样:
public void makeCall() {
SoulPermission.getInstance()
.checkAndRequestPermission(Manifest.permission.CALL_PHONE, new CheckRequestPermissionListener() {
@Override
public void onPermissionOk(Permission permission) {
Utils.makeCall(AfterActivity.this, "10086");
}
@Override
public void onPermissionDenied(Permission permission) {
Toast.makeText(AfterActivity.this, "本次拨打电话授权失败,请手动去设置页打开权限,或者重试授权权限", Toast.LENGTH_SHORT).show();
}
});
}
SoulPermission:
解决问题:
- 一行代码完成了对敏感权限功能功能的封装
- 涵盖了对版本的判断、权限请求
- 与Activity或者Fragment解耦,只需要我们传入申请的权限和回调即可。
- 更低成本的“真正运行时”权限的适配
大致工作流程:
如果我以在Android手机上要做一件事(doSomeThing),那么我最终可以有两个结果:
- A:可以做
- B:不能做
基于上述流程,那么SoulPermission的大致工作流程如下:
上图
在这里插入图片描述
从开始到结束展示了我们上述打电话的流程,A即直接拨打,B即toast提示用户,无法继续后续操作,绿色部分流程即可选部分,即对shouldShowRequestPermissionRationale的处理,那么完整权限流程下来,我们拨打电话的代码就是这么写:
public void makeCall() {
SoulPermission.getInstance()
.checkAndRequestPermission(Manifest.permission.CALL_PHONE, new CheckRequestPermissionListener() {
@Override
public void onPermissionOk(Permission permission) {
Utils.makeCall(AfterActivity.this, "10086");
}
@Override
public void onPermissionDenied(Permission permission) {
//绿色框中的流程
//用户第一次拒绝了权限且没有勾选"不再提示"的情况下这个值为true,此时告诉用户为什么需要这个权限。
if (permission.shouldRationale) {
new AlertDialog.Builder(AfterActivity.this)
.setTitle("提示")
.setMessage("如果你拒绝了权限,你将无法拨打电话,请点击授予权限")
.setPositiveButton("授予", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
//用户确定以后,重新执行请求原始流程
makeCall();
}
}).create().show();
} else {
Toast.makeText(AfterActivity.this, "本次拨打电话授权失败,请手动去设置页打开权限,或者重试授权权限", Toast.LENGTH_SHORT).show();
}
}
});
}
在这里插入图片描述
上述便是其在满足运行时权限下的完整工作流程。那么关于版本兼容呢?
针对部分手机6.0以下手机,SoulPermission也做了兼容,可以通过AppOpps 检查权限,内部将权限名称做了相应的映射,它的大体流程就是下图:
(这个检查结果不一定准确,但是即使不准确,也默认成功(A),保证我们回调能往下走,不会阻塞流程,其他自己实现了权限系统的手机,如vivo,魅族等也是走此方法,最终走他们自己的权限申请流程)
在这里插入图片描述
基于对于新老手机版本做了控制,在权限拒绝里面很多处理也是又可以提取的部分,我们可以把回调再次封装一下,进一步减少重复代码:
public abstract class CheckPermissionWithRationaleAdapter implements CheckRequestPermissionListener {
private String rationaleMessage;
private Runnable retryRunnable;
/**
* @param rationaleMessage 当用户首次拒绝弹框时候,根据权限不同给用户不同的文案解释
* @param retryRunnable 用户点重新授权的runnable 即重新执行原方法
*/
public CheckPermissionWithRationaleAdapter(String rationaleMessage, Runnable retryRunnable) {
this.rationaleMessage = rationaleMessage;
this.retryRunnable = retryRunnable;
}
@Override
public void onPermissionDenied(Permission permission) {
Activity activity = SoulPermission.getInstance().getTopActivity();
if (null == activity) {
return;
}
//绿色框中的流程
//用户第一次拒绝了权限、并且没有勾选"不再提示"这个值为true,此时告诉用户为什么需要这个权限。
if (permission.shouldRationale) {
new AlertDialog.Builder(activity)
.setTitle("提示")
.setMessage(rationaleMessage)
.setPositiveButton("授予", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
//用户确定以后,重新执行请求原始流程
retryRunnable.run();
}
}).create().show();
} else {
//此时请求权限会直接报未授予,需要用户手动去权限设置页,所以弹框引导用户跳转去设置页
String permissionDesc = permission.getPermissionNameDesc();
new AlertDialog.Builder(activity)
.setTitle("提示")
.setMessage(permissionDesc + "异常,请前往设置->权限管理,打开" + permissionDesc + "。")
.setPositiveButton("去设置", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
//去设置页
SoulPermission.getInstance().goPermissionSettings();
}
}).create().show();
}
}
}
然后我们在App所有打电话的入口处做一次调用:
/**
* 拨打指定电话
*/
public static void makeCall(final Context context, final String phoneNumber) {
SoulPermission.getInstance().checkAndRequestPermission(Manifest.permission.CALL_PHONE,
new CheckPermissionWithRationaleAdapter("如果你拒绝了权限,你将无法拨打电话,请点击授予权限",
new Runnable() {
@Override
public void run() {
//retry
makeCall(context, phoneNumber);
}
}) {
@Override
public void onPermissionOk(Permission permission) {
Intent intent = new Intent(Intent.ACTION_CALL);
Uri data = Uri.parse("tel:" + phoneNumber);
intent.setData(data);
if (!(context instanceof Activity)) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
}
});
}
那么这样下来,在Activity和任何业务页面的调用就只有一行代码了:
findViewById(R.id.bt_call).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
UtilsWithPermission.makeCall(getActivity(), "10086");
}
});
其中完全拒绝以后,SoulPermission 提供了跳转到系统权限设置页的方法,我们再来看看效果:
在这里插入图片描述
很多时候,其实绿色部分(shouldShowRequestPermissionRationale)其实并不一定必要,反复的弹框用户可能会厌烦,大多数情况,我们这么封装就好:
public abstract class CheckPermissionAdapter implements CheckRequestPermissionListener {
@Override
public void onPermissionDenied(Permission permission) {
//SoulPermission提供栈顶Activity
Activity activity = SoulPermission.getInstance().getTopActivity();
if (null == activity) {
return;
}
String permissionDesc = permission.getPermissionNameDesc();
new AlertDialog.Builder(activity)
.setTitle("提示")
.setMessage(permissionDesc + "异常,请前往设置->权限管理,打开" + permissionDesc + "。")
.setPositiveButton("去设置", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
//去设置页
SoulPermission.getInstance().goPermissionSettings();
}
}).create().show();
}
}
我们再写一个选择联系人的方法:
/**
* 选择联系人
*/
public static void chooseContact(final Activity activity, final int requestCode) {
SoulPermission.getInstance().checkAndRequestPermission(Manifest.permission.READ_CONTACTS,
new CheckPermissionAdapter() {
@Override
public void onPermissionOk(Permission permission) {
activity.startActivityForResult(new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI), requestCode);
}
});
}
在Activity中也是一行解决问题:
findViewById(R.id.bt_choose_contact).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
UtilsWithPermission.chooseContact(AfterActivity.this, REQUEST_CODE_CONTACT);
}
});
代码细节请参考demo,我们再来看看效果:
在这里插入图片描述
总结:
SoulPermission很好的适配了真运行时权限的要求做到了如下几点:
- 实现真正调用时请求的“真运行时权限”
- 解耦Activity和Fragment、不再需要Context
- 内部涵盖版本判断,一行代码封装权限请求和后续操作
- 接入成本低,可以在公共方法中声明以后,无需在调用业务方写权限适配代码
- 支持多项权限同时请求、支持系统权限页面跳转
- 支持检查通知权限
- 支持debug模式