Android 6.0 (API level 23) 已经发布很长一段时间了,其中一个很大的改进就是运行时权限。我之前就在知乎回答过一个问题iPhone 到底爽在哪里?

我说,iPhone上的App都是默认下载安装的,然后运行App时需要什么权限就弹窗向我申请,这对用户来说就非常好。因为我不想给App权限就不给,而Android 6.0以前是这样的,我下载了一个App安装,系统就弹出这个App需要使用的全部的权限,就给我看一下,我需要这个App 的话,只能同意所有的权限都给这个App,要么我不安装这个App

Android 6.0以前的权限管理应该是我对Android最不满的功能,6.0中 Google终于来解决这个问题了。

下面我就来好好聊聊这个运行时权限管理是怎么回事

权限分类

在6.0中Android把所有的权限从逻辑上分成了两类:常规(normal)和危险(dangerous)

  • 常规权限指的是那些不会直接获取你隐私的权限,如果你在AndroidManifest.xml文件中列出了这些权限的话,系统会自动授权给你。这里是normal权限列表,有很多
  • 危险权限就是那些能获取你隐私信息,或者可能会影响你的文件等的操作。比如读取你的联系人、使用你的摄像头和读取你的日历等等

权限组

这里是危险权限列表:
此处输入图片的描述

从上图中我们可以看到,Android系统把危险权限分了9大组,这样也是为了简化权限的申请机制。如果你申请了android.permission.READ_CONTACTS读取联系人的权限,那么6.0 系统就会把这一组中其他的权限也打包给你。我觉得这个和iOS的隐私管理机制非常相似,在iOS系统设置的“隐私->通讯录”中可以看到,如果你给一个App通讯录的权限,那么这个App既可以读也可以写的

Android 6.0里面只有危险权限才需要运行时获取的

申请权限

tartgetSdkVersion

说到申请权限,先要说一下targetSdkVersion这个字段,这个字段一般定义在build.gradle文件中的。这个对App来说很重要!但是是什么意思呢?

假如说targetSdkVersion 22,安装好以后Android系统就知道了这个App在系统API 22以下都测试过了并且能正确运行的,但是在23以上并不可以正确运行的,假如说这个App运行在了Android 6.0系统上,那么Android就会对这个App很“照顾”,兼容它正确运行。比如,6.0系统会把App申请的所有权限都默认给这个App,处理的逻辑和6.0一下的系统是一样的

ActivityCompat

android.support.v4.app.ActivityCompat这个类是App 向系统申请权限主要的工具,而且兼容了各种系统版本

  • ActivityCompat.requestPermissions向系统申请一个或一组权限
  • ActivityCompat.checkSelfPermissionApp检查自己是否有某个权限
  • ActivityCompat.shouldShowRequestPermissionRationale判断弹出对话框中是否含有“不再询问”的选择框

申请权限的步骤:

  1. 你要有一个运行Android 6.0系统的设备
  2. 将App的targetSdkVersion设置为23
  3. AndroidManifest.xml中申请的并且是危险的所有权限都列出来,用ActivityCompat.requestPermissions方法向系统申请权限
  4. 在所在的Activity中OverrideonRequestPermissionsResult方法接受系统权限申请的回调
  5. 处理回调,比如用户拒绝了某个权限,这时App可以弹出一个对话框描述一下App为何需要这个权限等等

targetSdkVersion 小于23

假如你的App的targetSdkVersion小于23,但是安装到了Android 6.0系统上了,会怎么样呢?会崩溃吗?

别担心,Android开发团队已经考虑到这一点了,如果targetSdkVersion小于23的话,就表示你的App并没有在新的运行时权限系统上测试过,此时Android系统会把你申请的全部权限都给你

但是!!!用户依然可以进入App的设置界面把权限关闭!!

cancel permission

此时你还能用这个权限么?经过我的测试,是不可以了。

所以,如果App的targetSdkVersion小于23并且运行在Android 6.0系统上,怎么去检测用户关闭了权限呢?伟大的stackoverflow告诉我们:android.support.v4.content.PermissionChecker可以帮我们解决这个问题。这个类的文档中有这个一段:

For apps targeting API lower than android.os.Build.VERSION_CODES.M these permissions are always granted as such apps do not expect permission revocations and would crash. Therefore, when the user disables a permission for a legacy app in the UI the platform disables the APIs guarded by this permission making them a no-op which is doing nothing or returning an empty result or default error.

PermissionChecker.checkSelfPermission方法就是用于检查App自身有没有某一个权限,这个方法的返回结果只有三种:

  • PERMISSION_GRANTED: 已授权
  • PERMISSION_DENIED: 没有被授权
  • PERMISSION_DENIED_APP_OP: 没有被授权

PERMISSION_DENIEDPERMISSION_DENIED_APP_OP都表示没有被授权,但是它们的区别就在于targetSdkVersion的值,如果targetSdkVersion小于23,就返回PERMISSION_DENIED_APP_OP,否则就返回PERMISSION_DENIED

因此,如果你的App的targetSdkVersion小于23,但是运行在Android 6.0及以后的系统上,你可以用PermissionChecker.checkSelfPermission(context, permission) == PermissionChecker. PERMISSION_DENIED_APP_OP来检查App是否有某一个权限

总结

如果App的targetSdkVersion小于23(Android 6.0以前),那么ContextCompat#checkSelfPermissionContext#checkSelfPermission方法的返回结果都是错误的,因为它们总是返回0(PERMISSION_GRANTED)。即使App运行在Android 6.0上并且用户在设置中关闭了App的权限,上面两个方法返回的结果也是0

上面也说到,Android 6.0系统上,用户是可以关闭App权限的,所以并不是说App的targetSdkVersion小于23就可以不用关心权限问题了:

  • Android < 6.0:系统会给App所有的权限
  • Android >= 6.0 && targetSdkVersion < 23:系统会默认给予App所有的权限,但是用户可以去设置中关闭权限。这时你需要使用PermissionChecker.checkSelfPermission来检测App是否有某一个权限
public boolean selfPermissionGranted(Context context, String permission) {
    // Android 6.0 以前,全部默认授权
    boolean result = true;

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
       if (targetSdkVersion >= Build.VERSION_CODES.M) {
            // targetSdkVersion >= 23, 使用Context#checkSelfPermission
            result = context.checkSelfPermission(permission)
                    == PackageManager.PERMISSION_GRANTED;
        } else {
            // targetSdkVersion < 23, 需要使用 PermissionChecker
            result = PermissionChecker.checkSelfPermission(context, permission)
                    == PermissionChecker.PERMISSION_GRANTED;
        }
    }
    return result;
}

获取App的targetSdkVersion值:

 try {
    final PackageInfo info = context.getPackageManager().getPackageInfo(
        context.getPackageName(), 0);
    targetSdkVersion = info.applicationInfo.targetSdkVersion;
} catch (PackageManager.NameNotFoundException e) {
    e.printStackTrace();
}

最后

不得不说,慢慢的从Android 5.0开始,Google慢慢的缩减了Android开放策略,以前的Android真的是可以为所欲为,监听系统各种变化,甚至一个App被切换到后台,它任然可以获取到当前正在运行的App(用户正在使用的),这个Api可以轻松的获取用户的隐私信息啊,太可怕了。

从Android 6.0开始,运行时权限、Doze模式以及App Standby,Android 7.0中对Doze模式加强,以及取消了很多比如CONNTENCTIVITY_ACTIONACTION_NEW_PICTUREACTION_NEW_VIDEO广播

参考