Android 6.0运行时权限

一、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 ;

处理授权结果:

@Override
public 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进行检测

       List<String> grantList = new ArrayList<>();
       List<String> diniedList = new ArrayList<>();
      
      for(int i=0;i<permissions.length;i++){

         String perm = permissions[i];
         if(PermissionChecker.checkSelfPermission(thisActivity,perm)){
            grantList.add(perm );
        }else{
           diniedList .add(perm );
        }
       
        boolean hasNext = ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
            perm);
       
        String op = AppOpsManagerCompat.permissionToOp(perm);
        if(TextUtils.isEmpty(op)) continue;

        SharePrefUtil.setBoolean(op,hasNext);  //记录下次是否允许再次申请
          
        
     }
   
    onPermissionGrants(grantList );  //分别处理不同的权限
    onPermissionDenied(diniedList );
}

五、补充

以上只是涉及到Android 6.0的系统,还有需要很多需要完善的地方:

  • Android6.0的之前的权限检测应该默认授权
  • Fragment或者Context的检测并没有实现
  • 小米和vivo通讯录兼容,小米无法询问,我们要让他去设置页面,vivo手机可以绕过检测,这也是需要处理的问题。

猜你喜欢

转载自my.oschina.net/ososchina/blog/1787953