0.前言
最近在研究所实习,我负责维护Android手机取证项目的Android客户端,有客户反映我们的APP在Android6.0无响应,经过调试发现SD卡读写权限权限被拒绝。但明明是在AndroidManifest.xml文件中声明过的。查了很多资料才知道Android6.0的很多权限申请机制发生了改变,可以说是Android6.0在安全机制上更进了一步吧,因此写下这篇文章以记录。
注:在运行程序时,对于某些权限向用户询问申请(后面会详细地讲)时因为我们知道客户在我们APP中不会点“拒绝”,因此我对此功能的实现也仅限于文章中的前部分直接做死循环直到用户同意授权该权限,因为当然是用最低的成本满足客户的需求最好啦,但是真正开发中,需要处理很多事情,我也并没有浅尝辄止,后面会用到onRequestPermissionsResult回调方法,shouldShowRequestPermissionRationale方法等,才能避免很多令用户困扰的情况。后面会详细地进行介绍。
我也花了整整两天的时间对兼容Android6.0权限管理机制的整个处理过程进行了理解和汇总,也方便大家遇到类似的问题少走弯路。
1.Android 6.0新的权限管理机制
Android 6.0 Marshmallow版本之后,系统对于一些危险级别的权限,在运行那些targetSdkVersion设置为23和23以上的应用并且需要这些权限时,会一个一个询问用户是否授予权限。若不询问直接使用这些权限,会出现类似java.lang.SecurityException: Permission Denial的异常日志。
应用targetSdkVersion如果没有设置为23版本或者以上,系统还是会使用旧规则:在安装的时候赋予该app所申请的所有权限。因此不会影响以前应用的正常使用,但是6.0以后,用户可以在<设置-权限>里将该APP的某些权限手动关闭,此时被用户禁止权限的API接口返回值都为null或者0,我们判空即可防止App Crash。
若APP在运行时,将设置里的权限手动关闭,那就会直接Crash。
2.危险权限列表
前面提到的危险级别的权限是我们需要格外关注的,因为这些权限在使用前需要进行特殊处理。
上图中有一个权限群的概念,同一组的任何一个权限被授权了,其他权限也自动被授权。
例如,一旦WRITE_EXTERNAL_STORAGE被授权了,APP同时也就有了READ_EXTERNAL_STORAGE权限。
3.Android 6.0运行时主动请求权限
3.1 检测和申请权限
下面的例子介绍上面列出的读写SD卡的使用例子,可以使用以下的方式解决:
public boolean isGrantExternalRW(Activity activity) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && activity.checkSelfPermission( Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { activity.requestPermissions(new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE }, 1); return false;//第一次开启应用并执行权限检查,虽然返回了false,但是已经调用过了申请权限的方法 } return true;//非第一次开启应用并执行权限检查,或者6.0以下的Android版本 }
这里需要说明的是,检查和申请权限的方法分别是Activity.checkSelfPermission()和Activity.requestPermissions,这两个方法是在 API 23中新增的。
Activity.checkSelfPermission()主要用于检测某个权限是否已经被授予,方法返回值为PackageManager.PERMISSION_DENIED或者PackageManager.PERMISSION_GRANTED。当返回DENIED就需要进行申请授权了。
Activity.requestPermissions该方法是异步的,第一个参数是需要申请的权限的字符串数组,第二个参数为requestCode,主要用于回调的时候检测。最前面的参数可以传入mActivity。好像不传也可以。
可以从方法名requestPermissions以及第二个参数看出,是支持一次性申请多个权限的,系统会通过对话框逐一询问用户是否授权。
然后在需要使用这个权限的时机,进行如下调用即可。
boolean isGrant= isGrantExternalRW(mActivity); if (isGrant) { //业务逻辑 } while (!isGrant) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } isGrant = isGrantExternalRW(mAct); if (isGrant) { //业务逻辑 } }
这里因为我们写的isGrantExternalRW方法的返回值只是判断的一开始检查权限时的状态,因此如果是第一次开启应用,返回的是false,并且申请了权限(如果用户同意的话),再调用一次该方法返回true,进入逻辑代码,并最后跳出while循环。如果用户已经授权过了,那么直接会走逻辑代码。这里比较流氓的是,如果用户一直不同意,会一直返回false,我们就阻塞在while循环里,一秒后继续申请,直到用户同意为止。显然这是不够友好的。
因此Google为了防止这种情况的发生,在用户拒绝授权时,下一次弹窗可以勾选“不再提醒”。如果这个选项被用户勾选了。下次为这个权限请求requestPermissions时,对话框就不弹出来了,系统会直接回调处理申请返回结果的回调方法onRequestPermissionsResult,回调结果为最后一次用户的选择。
3.2 处理权限申请回调
@Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { switch (requestCode) { case 1: { // 用户取消授权这个数组为空,如果你同时申请两个权限,那么grantResults的length就为2,分别记录你两个权限的申请结果 if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { //业务逻辑 } else { //授权被拒绝,不再进行基于该权限的功能 } return; } // 其他case处理其他权限的申请回调 } }
上面处理申请回调的方法已经写的很明白了。还有就是不管用户点击拒绝还是同意,都会回调该方法(别忘记用户拒绝并勾选“不再提醒”时也会回调)。
3.3 使用shouldShowRequestPermissionRationale方法
问题来了,如果第二次向用户申请权限被拒绝,并且用户勾选了“不再提醒”,那我们以后每次需要使用这个权限都会直接被拒绝,并不会弹出对话框。APP什么也不做会产生很差的用户体验。所以这种情况需要我们进行处理。这时候我们可以借助shouldShowRequestPermissionRationale方法。首先看一下该方法的返回值。
因此,我们在回调函数中做权限检测,如果返回DENIED,就调用上述方法,返回false,就弹出对话框引导用户手动开启权限,避免了用户的操作触发了权限申请机制(已被拒绝并勾选不再提醒或手动关闭权限),但是没有任何响应的尴尬。又因为我们事先向用户询问授权过了,因此不存在表格中的第一种情况。
因此只需要在上面回调代码的else代码段加入如下代码即可。
if (!mActivity.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { //用户已经完全拒绝,或手动关闭了权限 //开启此对话框缓解一下尴尬... AlertDialog dialog = new AlertDialog.Builder(this) .setMessage("不开启该权限将无法正常工作,请在设置中手动开启!") .setPositiveButton("确定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { finish(); } }) .setNegativeButton("取消", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { finish(); } }).create(); dialog.show(); return; }else{ //用户一直拒绝并一直不勾选“不再提醒” //Toast提醒一下即可 }
在需要权限的时候,只要执行类似于下面的检查和申请权限的代码即可(注意自己替换ContentCompat):
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(context, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE); } else { //Do the stuff that requires permission... }