一、Android 6.0运行时权限
在Android6.0之前,普遍意义上如果在Manifest中注册了权限,在安装过程中默认开启了权限,此后也无法关闭,这种方式相当不安全,尤其可能访问敏感信息。在Android 6.0到来了,为了解决此类不安全的问题,权限可以在系统设置中开启关闭,在Manifest中的声明只是作为权限申请“意向”,只是给了个清单,告诉系统我可能需要这些权限。在运行时,这些权限默认中一些安全级别较高的是关闭的,因此需要在运行时询问开启或者关闭。
新的权限机制更好的保护了用户的隐私,Google将权限分为两类,一类是正常权限,这类权限一般不涉及用户隐私,是不需要用户进行授权的,比如手机震动、访问网络等;另一类是风险权限,一般是涉及到用户隐私的,需要用户进行授权,比如读取sdcard、访问通讯录等。
二、面临的问题—系统碎片化
Android 是开放系统,由于系统的版本的更新控制权不是google独有,其他ROM厂家自己设计的ROM或多或少在兼容上出现了问题。小米 android 6.0,vivo、oppo权限检测并不是通过Google提供的方式,而是通过AppOpsManager来实现的,此外检测结果还有小米自家的【询问模式,mode值为4】。最坑的是vivo android 7.0在访问通讯录时可以绕过AppOpsManager和checkPermission,直接在cursor查询时提示,权限拒绝时cursor也没有触发异常,如果个人通讯录没有联系人的话,你永远无法知道授权成功了没有。其他手机还有定时自动禁止权限问题,这都是需要注意的地方。
三、权限检测方法
权限检测方法,google提供了比较完善的检测机制,当然开源的EasyPremission比较简陋,AndPermission相对要好一些。
public final class PermissionChecker { /** 已授权 */ public static final int PERMISSION_GRANTED = PackageManager.PERMISSION_GRANTED; /** 拒绝授权 */ public static final int PERMISSION_DENIED = PackageManager.PERMISSION_DENIED; /** 权限允许,权限操作被拒 */ public static final int PERMISSION_DENIED_APP_OP = PackageManager.PERMISSION_DENIED - 1; @IntDef({PERMISSION_GRANTED, PERMISSION_DENIED, PERMISSION_DENIED_APP_OP}) @Retention(RetentionPolicy.SOURCE) public @interface PermissionResult {} private PermissionChecker() { /* do nothing */ } /** * 检查指定Uid和pid进程的app权限 * * * @param context Context 上下文 * @param permission 权限名称 如Manifest.permission.READ_CONTACTS【注意:可以是权限组】 * @param pid 被检测app的进程id * @param uid 被检测app的uid * @param packageName 被检测app的包名 * @return 返回结果 {@link #PERMISSION_GRANTED} * or {@link #PERMISSION_DENIED} or {@link #PERMISSION_DENIED_APP_OP}. */ public static int checkPermission(@NonNull Context context, @NonNull String permission, int pid, int uid, String packageName) { if (context.checkPermission(permission, pid, uid) == PackageManager.PERMISSION_DENIED) { return PERMISSION_DENIED; } String op = AppOpsManagerCompat.permissionToOp(permission); if (op == null) { return PERMISSION_GRANTED; } if (packageName == null) { String[] packageNames = context.getPackageManager().getPackagesForUid(uid); if (packageNames == null || packageNames.length <= 0) { return PERMISSION_DENIED; } packageName = packageNames[0]; } if (AppOpsManagerCompat.noteProxyOp(context, op, packageName) != AppOpsManagerCompat.MODE_ALLOWED) { return PERMISSION_DENIED_APP_OP; } return PERMISSION_GRANTED; } /** * 检测当前app的权限 * * @param context 上下文资源 * @param permission 权限名称,也可以是权限组 * @return 返回结果 {@link #PERMISSION_GRANTED} * or {@link #PERMISSION_DENIED} or {@link #PERMISSION_DENIED_APP_OP}. */ public static int checkSelfPermission(@NonNull Context context, @NonNull String permission) { return checkPermission(context, permission, android.os.Process.myPid(), android.os.Process.myUid(), context.getPackageName()); } /** * 检测其他app是否具有通过ipc调用本应用的权限 * * @param context 上下文 * @param permission 要检查的权限 * @param packageName 调用者的包名,如果是null,则默认根据uid获取包名中的第一个包名 * @return The permission check result which is either {@link #PERMISSION_GRANTED} * or {@link #PERMISSION_DENIED} or {@link #PERMISSION_DENIED_APP_OP}. */ public static int checkCallingPermission(@NonNull Context context, @NonNull String permission, String packageName) { if (Binder.getCallingPid() == Process.myPid()) { // return PackageManager.PERMISSION_DENIED; } return checkPermission(context, permission, Binder.getCallingPid(), Binder.getCallingUid(), packageName); } /** * 检查是否所有应用和当前应用具有调用自身或者其他应用的权限 * * @param context 上下文 * @param permission 要检查的权限 * @return 返回值 {@link #PERMISSION_GRANTED} * or {@link #PERMISSION_DENIED} or {@link #PERMISSION_DENIED_APP_OP}. */ public static int checkCallingOrSelfPermission(@NonNull Context context, @NonNull String permission) { String packageName = (Binder.getCallingPid() == Process.myPid()) ? context.getPackageName() : null; return checkPermission(context, permission, Binder.getCallingPid(), Binder.getCallingUid(), packageName); }}
四、权限申请
权限申请是最复杂的过程,这个过程中涉及【系统授权对话框】问题。这里我们首先要探讨shouldShowRequestPermissionRationale方法。
这个方法的用法不能按照google官网例子来处理,否则你可能按照进入逻辑陷阱。我们先来分析一下返回值。
ActivityCompat.shouldShowRequestPermissionRationale(Context context, String permisssion)
返回值有已下几种情况
- 1、权限未开启的情况下,没有进行申请过permission,那么返回值为false
- 2、权限未开启的情况下,如果申请过权限,权限被拒绝,但是没有选择【记住不再提示】,那么会返回true
- 3、权限未开启的情况下,如果申请过权限,权限被拒绝,如果选择了【记住不再提示】,那么会返回位false
- 4、权限已开启,并且授权成功,返回位false。
那么如何正确使用此方法了?
正确的方式是在onRequestPermissionsResult中使用,我们可以将返回的结果存入SharedPreferences的boolean值。然后在我们申请权限的额时候取出此boolean值进行判断。
注意:就算缓存被清空,那么再一次请求权限,依然会有结果回调到onRequestPermisssionsResult中
申请授权:
if (PermissionChecker.checkSelfPermission(thisActivity, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { String op = AppOpsManagerCompat.permissionToOp(Manifest.permission.READ_CONTACTS); boolean hasNext = ActivityCompat.shouldShowRequestPermissionRationale(thisActivity, perm); boolean isShowRequestTip = !TextUtils.isEmpty(op) && SharePrefUtils.getBoolean(op,true) || hasNext ; //注意,SharePrefUtils默认值为true,首次应该返回true if (isShowRequestTip ) { ActivityCompat.requestPermissions(thisActivity, new String[]{Manifest.permission.READ_CONTACTS}, MY_PERMISSIONS_REQUEST_READ_CONTACTS); } else { showDialogForSystem();// 通过自己定义的dialog引导用户去设置页面授权 }}else{ Toast.make(thisActivity,"已授权,可以调用通讯录",Toast.LENGTH_SHORT).show();}
注意:考虑到用户授权之后有可能在设置页面关闭授权,因此,我们上面的请求权限应该做本地和系统两套判断较为合适,代码如下:
boolean hasNext = ActivityCompat.shouldShowRequestPermissionRationale(thisActivity, perm); boolean isShowRequestTip = !TextUtils.isEmpty(op) && SharePrefUtils.getBoolean(op,true) || hasNext ;
处理授权结果:
@Overridepublic void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { if(MY_PERMISSIONS_REQUEST_READ_CONTACTS!=requestCode) return; if(permissions==null || permissions.length==0) return; //在小米,vivo中,grantResults并不可靠,因此我们还需要使用PermissionChecker进行检测 ListgrantList = new ArrayList<>(); List diniedList = new ArrayList<>(); for(int i=0;i
五、补充
以上只是涉及到Android 6.0的系统,还有需要很多需要完善的地方:
- Android6.0的之前的权限检测应该默认授权
- Fragment或者Context的检测并没有实现
- 小米和vivo通讯录兼容,小米无法询问,我们要让他去设置页面,vivo手机可以绕过检测,这也是需要处理的问题。