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代码去做版本判断,判断新老机型分别处理
基于第一个业务繁琐的问题,很多应用选择适配权限的时候,把所用到的敏感权限放在一个特定的页面去申请,比如欢迎页(某知名音乐播放器等),如果授权不成功,则会直接无法进入应用,这样虽然省事,但是用户体验不好,我在应用一打开,提示需要电话权限,用户会很疑惑。这样其实就违背了“运行时授权”的初衷,谷歌希望我们在真正调用的该功能的时候去请求,这样权限请求和用户的目的是一致的,也更容易授予权限成功。
那么能不能做到如下几个点呢?
对权限适配的期望:
-
基于用户体验考虑,我不希望在应用一打开就向用户索取一堆授权,异或是跳一个页面专门去授权、困扰我们宝贵的用户
-
不需要去重写onPermissionResult、甚至不需要Activity和Fragment。
-
去除版本判断。无论什么系统版本的新老手机,都是走一个方法
-
一行代码完成从权限检查、请求、到最终完我要做的事情
-
我不需要在原有项目中改太多代码
带着上述几个问题,我们今天的主角: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、不再需要Context、不再需要onPermissionResult
-
内部涵盖版本判断,一行代码解决权限相关操作,无需在调用业务方写权限适配代码,继而实现真正调用时请求的“真运行时权限”
-
接入成本低,零入侵,仅需要在gradle配置一行代码
工作流程:
如果我以在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也做了兼容,可以通过AppOps 检查权限,内部将权限名称做了相应的映射,它的大体流程就是下图: (这个检查结果不一定准确,但是即使不准确,也默认成功(A),保证我们回调能往下走,不会阻塞流程,有些在6.0以下自己实现了权限系统的手机(如vivo,魅族)等也是走此A的回调,最终会走到它们自己的权限申请流程)
最佳实践:
基于对于代码中对新老系统版本做了控制,而在权限拒绝里面很多处理也是又可以提取的部分,我们可以把回调再次封装一下,进一步减少重复代码:
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,我们再来看看效果:
主要功能的源码分析:
优雅的避掉onPermissionResult:
适配权限最大的痛点在于:项目业务页面繁多,如果你想实现“真运行时权限”的话就需要在业务的Activity或者Fragment中去重写权限请求回调方法,斟酌一番并且在参考了下RxPermission中对权限请求的处理,我决定用同样的方式—用一个没有界面的Fragment去完成我们权限请求的操作,下面贴上部分代码:
首先定义一个接口,用于封装权限请求的结果
public interface RequestPermissionListener {
/**
-
得到权限检查结果
-
@param permissions 封装权限的数组
*/
void onPermissionResult(Permission[] permissions);
}
复制代码
然后是我们的Fragment:
public class PermissionSupportFragment extends Fragment implements IPermissionActions {
/**
- 内部维护requestCode
*/
private static final int REQUEST_CODE = 11;
/**
- 传入的回调
*/
private RequestPermissionListener listener;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//当状态发生改变,比如设备旋转时候,Fragment不会被销毁
setRetainInstance(true);
}
/**
-
外部请求的最终调用方法
-
@param permissions 权限
-
@param listener 回调
*/
@TargetApi(M)
@Override
public void requestPermissions(String[] permissions, RequestPermissionListener listener) {
requestPermissions(permissions, REQUEST_CODE);
this.listener = listener;
}
@TargetApi(M)
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
Permission[] permissionResults = new Permission[permissions.length];
//拿到授权结果以后对结果做一些封装
if (requestCode == REQUEST_CODE) {
for (int i = 0; i < permissions.length; ++i) {
Permission permission = new Permission(permissions[i], grantResults[i], this.shouldShowRequestPermissionRationale(permissions[i]));
permissionResults[i] = permission;
}
}
if (listener != null && getActivity() != null && !getActivity().isDestroyed()) {
listener.onPermissionResult(permissionResults);
}
}
}
复制代码
其中Permission是我们的权限名称、授予结果、是否需要给用于一个解释的包装类:
public class Permission {
private static final String TAG = Permission.class.getSimpleName();
/**
- 权限名称
*/
public String permissionName;
/**
- 授予结果
*/
public int grantResult;
/**
- 是否需要给用户一个解释
*/
public boolean shouldRationale;
/**
- 权限是否已经被授予
*/
public boolean isGranted() {
return grantResult == PackageManager.PERMISSION_GRANTED;
}
//。。。
}
复制代码
至此,我们已经利用自己实现的一个没有界面的Fragment封装了运行时权限相关的请求、RequestCode的维护、以及onPermissionResult的回调、在我们真正调用的时候代码是这样的:
/**
-
@param activity 栈顶 Activity
-
@param permissionsToRequest 待请求的权限
-
@param listener 回调
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
最后
我见过很多技术leader在面试的时候,遇到处于迷茫期的大龄程序员,比面试官年龄都大。这些人有一些共同特征:可能工作了5、6年,还是每天重复给业务部门写代码,工作内容的重复性比较高,没有什么技术含量的工作。问到这些人的职业规划时,他们也没有太多想法。
其实30岁到40岁是一个人职业发展的黄金阶段,一定要在业务范围内的扩张,技术广度和深度提升上有自己的计划,才有助于在职业发展上有持续的发展路径,而不至于停滞不前。
不断奔跑,你就知道学习的意义所在!
《Android高级架构师面试指导+2021大厂面试真题》免费领取
程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**
因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-ahi6jmIZ-1710848497623)]
[外链图片转存中…(img-rTLjZZWe-1710848497624)]
[外链图片转存中…(img-MJjzs7D2-1710848497624)]
[外链图片转存中…(img-UOj71Ekv-1710848497624)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
[外链图片转存中…(img-1oQLYzFI-1710848497625)]
最后
我见过很多技术leader在面试的时候,遇到处于迷茫期的大龄程序员,比面试官年龄都大。这些人有一些共同特征:可能工作了5、6年,还是每天重复给业务部门写代码,工作内容的重复性比较高,没有什么技术含量的工作。问到这些人的职业规划时,他们也没有太多想法。
其实30岁到40岁是一个人职业发展的黄金阶段,一定要在业务范围内的扩张,技术广度和深度提升上有自己的计划,才有助于在职业发展上有持续的发展路径,而不至于停滞不前。
不断奔跑,你就知道学习的意义所在!
[外链图片转存中…(img-zyLBa955-1710848497625)]
《Android高级架构师面试指导+2021大厂面试真题》免费领取
[外链图片转存中…(img-F8WQJYXf-1710848497626)]