最近对自己负责的项目进行代码优化,其中一个任务就是将里面公共的业务逻辑或者是涉及到的功能逐个拆分成不同的类库。以求作为一个独立功能项目进行发布维护。后续新的App如果有同样的需求,那么只要导入相应的类库即可。
其中一块就是对Android6.0的动态权限申请做一个独立的封装。这一块后面会单独再写一篇博客,并会将项目开源出来。毕竟,不封装的6.0动态权限申请的处理方式确实导致代码变得很分散,不易阅读和维护。而且整体用起来较为麻烦,要是每个权限都这么去申请估计得累死。
但在介绍6.0动态权限申请的封装之前,还是得先总结一下相关的背景知识,以及基础的处理方式。
如果要直接看封装库,点这里:2步搞定动态权限:一个优雅的Android6.0动态权限申请库
1、Android6.0之前的权限处理方式
在Android6.0之前,对于权限的处理十分简单粗暴。即直接在Android项目的AndroidManifest文件中声明一下即可。需要什么权限,在这里写一下,后面就不用管了。而用户在安装App的时候,就默认要接受我们所声明的所有权限。
这对于开发者来说当然很不错,轻松方便的就把权限问题给处理掉了。但是对于用户来说,他们可就像被迫签了一纸协议。而且App很多涉及到隐私权限的操作用户是根本不知道的。这安全性可就不怎么样了。
2、Android6.0之后的权限处理方式
Android6.0之后,权限处理框架被重新设计。Android6.0将权限分为了两大类:普通权限 与 动态权限(运行时权限)。
(1)普通权限:他们的用法跟6.0之前毫无差别。只需要在AndroidManifest文件里面声明一下,后续就不用管了。因为这部分权限涉及到的隐私级别并不那么高。
以下是普通权限列表:
(2)动态权限:对于动态权限,你单单在Manifest文件里面声明一下是不行的。在需要该权限时要明确通知用户,并由用户授权。这类权限涉及到的用户隐私级别是很高的。
Android6.0为了开发者可以更方便的处理动态权限,对动态权限进行了分组。这样,只要成功申请了组中的任何一个权限,那么就同时成功申请了组中的其他权限。
以下是动态权限列表:
3、targetSdkVersion与手机系统Version问题
这些运行时权限只有当我们将targetSdkVersion设置大于23时,运行在Android6.0及以上的手机中才会生效。举个例子:比如我们把targetSdkVersion设置为23,我们的App跑在Android6.0的机子里。这时如果我们的App想要开启相机,但是我们又没有处理相机的动态权限,那么恭喜你,程序崩溃。
还是这个例子,如果targetSdkVersion小于23的话,会怎么样呢?答案是,仍然会使用旧版的权限规则,用户安装即默认接受所有权限。有朋友一看,觉得这样不是挺好的么。但是注意,用户是可以在应用设置中关闭相应权限的。那么如果用户在应用设置中关闭了相机权限。我们再去调用相机,会怎么样?一个好消息是程序不会被强制崩溃。一个坏消息是App这时会什么都不做,可能的一种情况就是返回null或0。但是代码上仍然可能会因此而崩溃。而且即便不崩溃,App什么都不干,徒留用户两眼一瞪。这用户体验想想也是醉了。
另外说一点,虽然Android6.0的权限更改给开发者带来了更多需要解决的问题。但是这对用户以及维护整个正常健康的Android环境是很有必要的。我们应该拥抱这种变化,而不是一味的抱怨麻烦。毕竟Android环境好了,我们Android开发者才会好。
4、在Activity中进行动态权限申请
主要涉及到4个方法:
(1)用于检查单个权限是否被授权
public int checkSelfPermission(String permission)
使用方式很简单,传入你要请求的权限,对比返回的int值即可知道是否申请了该权限。
int grantResult = checkSelfPermission(Manifest.permission.CAMERA);
if(grantResult != PackageManager.PERMISSION_GRANTED){
//没权限
}else{
//有权限
}
(2)用于请求多个权限
public final void requestPermissions(String[] permissions, int requestCode)
我们传入需要请求权限的数组以及一个请求码。例如这里我们请求3个权限:存储权限,相机权限,打电话权限。
String[] permissions = new String[]{
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.CAMERA,
Manifest.permission.CALL_PHONE
};
requestPermissions(permissions,1000);
之后由于调用了requestPermissions方法,在Android6.0系统中会对你所请求的动态权限依次弹出相应的申请弹窗。以此来告知用户当前App在申请一些涉及到隐私的动态权限。如下图:
这里可以看到这是第1项权限,权限共3项。后面两项就不贴了,样式都一样。
注意,这是App第1次申请该权限的弹窗样式。如果这里我们拒绝该权限,下次打开App进行第2次权限申请,权限申请框的样式就有些不一样了。如下图:
我们可以看到这里多了一个“点击拒绝后不再提示”的勾选框。如果用户勾选了他,并选择了拒绝。那么下次再进入App,就不会对该权限再弹出权限申请框了。那这种情况,我们该怎么办?不要担心,后面会单独说。
我们往下继续,这里我们对需要的权限进行了申请。那么这次权限申请的结果是怎样的呢?之后就由(3)来完成。
(3)请求多个权限的回调
public void onRequestPermissionsResult(
int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults
)
当我们调用requestPermissions方法申请权限后,系统最终会调用onRequestPermissionsResult方法的回调,并且将本次申请时用户处理的结果传到该方法中去。参数requestCode是我们调用requestPermissions方法时传入的requestCode。参数permissions是我们请求的权限列表。参数grantResults是我们请求权限的结果。
@Override
public void onRequestPermissionsResult(
int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if(requestCode == 1000) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
int length = grantResults.length;
for (int i = 0; i < length; i++) {
if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
//没权限
} else {
//有权限
}
}
}
}
}
这里展示了没权限和有权限两种结果。但是之前我们说的,如果用户勾选了不再提示,又拒绝了该权限,那我们该如何得知呢?
这时,就需要第4个方法出马了。
(4)是否会再次弹出动态权限请求框
public boolean shouldShowRequestPermissionRationale(String permission)
这个方法会返回一个boolean类型的值。
当App请求过某一权限,但是用户仅仅是选择了拒绝。那么该方法返回true。
当App请求过某一权限,但是用户勾选了不再提示并拒绝。那么该方法返回false。
这时,问题得到解决了。我们只需要在没权限时调用一下该方法,再做一次判断,就能清楚的区分用户是仅仅点击了拒绝,还是勾选了不再提示后并拒绝。如果我们得知用户勾选了不再提示并拒绝,那么我们应该告知用户为何我们需要申请该权限,并引导用户去应用设置里打开该权限。
最后贴一下Activity中完整的动态权限请求代码吧。可以根据业务需要在相应位置调用相关的业务代码,如果需要针对详细的被拒绝权限做相应处理,也可以在循环里记录一下是哪几个权限被拒绝了:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String[] permissions = new String[]{
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.CAMERA,
Manifest.permission.CALL_PHONE
};
requestPermissions(permissions,1000);
}
@Override
public void onRequestPermissionsResult(
int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if(requestCode == 1000) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
int length = grantResults.length;
boolean hasAllPermissions = false;
for (int i = 0; i < length; i++) {
if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
boolean shouldShowRequestPermissionRationale
= shouldShowRequestPermissionRationale(permissions[i]);
if(!shouldShowRequestPermissionRationale){
//没权限,且用户勾选了不再提示
Toast.makeText(
getApplicationContext(),
"告知用户想XXX必须开启YYY权限",
Toast.LENGTH_SHORT
).show();
return;
}else{
//没权限
hasAllPermissions = false;
break;
}
} else {
hasAllPermissions = true;
}
}
if(hasAllPermissions){
Toast.makeText(
getApplicationContext(),
"权限都有了",
Toast.LENGTH_SHORT
).show();
}else{
Toast.makeText(
getApplicationContext(),
"有权限被拒绝了",
Toast.LENGTH_SHORT
).show();
}
}
}
}
}
5、在Fragment中进行动态权限申请
弄清了Activity中的动态权限申请,Fragment中的动态权限申请其实就不需要多说了。为什么?因为Fragment除了第1个方法checkSelfPermission没有外,其他的3个方法全有,而且是一模一样。比葫芦画瓢再调用一遍就是了。
6、动态权限请求框架的封装
回过头来看看我们上面的代码吧,这还没有涉及到业务代码就已经有这么一大串了。而且如果权限申请处再多点,那别提多嗨皮了。而且由于涉及到回调方法,我们的业务难免会被分散,造成一个类里面出现在多个地方。作为一个有追求的程序员来说,我们是坚决拒绝的。
现在我们希望可以将动态权限的申请做一层封装。我只需要调用几句代码,告诉他有权限时做什么,没权限时做什么就可以了。而且最好是将回调函数也封装掉,这样保证我们的业务可以集中,防止在一个类中业务代码出现在多处。
鉴于本次篇幅有限,下篇再来讲详细的封装过程。