详解android6.0权限

介绍android6.0运行时权限,以及权限申请,权限适配

这是一篇迟到的文章。在6.0之前,在应用安装的时候,提示用户所需要用到的权限列表,同意之后安装,该app就被赋予所有的权限,我们暂且称它为安装时权限,安装后,被赋予的权限也无法取消,当然,国内的一些rom会在系统级别进行额外的一层权限管理,这个不在本文叙述范围之内;在6.0之后,google对权限进行了运行时的管理,而不是在安装时候,危险权限需要在运行时申请,我们暂且称它为运行时权限,非危险权限,在安装时由用户授予,这样简化了应用安装过程,因为用户在安装或更新应用时不需要授予权限,也给予了用户对app功能更多的控制

权限分组

系统权限主要分为两类,正常权限危险权限

正常权限不会直接危及用户的隐私,如果你的应用在它的Manifest中列出了正常权限,系统会自动授予权限

危险权限可以让app访问用户的机密数据,如果你的应用在它的Manifest列出了危险权限,用户必须明确批准你的app使用该权限

当然,不管哪个版本的android,你应用中所用到的所有权限,不管是正常权限还是危险权限,都需要在应用的Manifest中申明

如果你的设备运行Android 5.1以及5.1以下版本,或者你的应用的目标SDK是22以及22以下版本:如果你在应用的Manifest中申明了危险权限,用户在安装时必须授予权限,如果拒绝授予权限,那么系统就不会安装应用,也就是所谓的“一刀切”方式,不同意所有权限,就不能安装应用

如果你的设备运行Android 6.0以及6.0以上版本,或者你的目标SDK是23以及23以上版本:应用必须在Manifest中罗列出所有的权限,并且在程序运行时,它必须请求用户授予每一个危险权限,此时用户可以授予或者拒绝每一个权限,并且应用程序可以继续运行有限的功能,即使用户拒绝了权限请求
注意:从Android 6.0开始(API 23),用户可以在任何时候,对任何应用撤销权限,即使app申明的目标SDK低于23

正常权限

正常权限有以下几个特点

  1. 就是在安装时,由用户授予,后续运行时无需再次申请,无需再显示提醒用户,用户也不能取消这些权限(在Android 6.0及以上版本例外,用户可以通过管理界面撤销权限)
  2. 对用户隐私或安全没有较大影响

正常权限列表

  • ACCESS_LOCATION_EXTRA_COMMANDS
  • ACCESS_NETWORK_STATE
  • ACCESS_NOTIFICATION_POLICY
  • ACCESS_WIFI_STATE
  • BLUETOOTH
  • BLUETOOTH_ADMIN
  • BROADCAST_STICKY
  • CHANGE_NETWORK_STATE
  • CHANGE_WIFI_MULTICAST_STATE
  • CHANGE_WIFI_STATE
  • DISABLE_KEYGUARD
  • EXPAND_STATUS_BAR
  • GET_PACKAGE_SIZE
  • INSTALL_SHORTCUT
  • INTERNET
  • KILL_BACKGROUND_PROCESSES
  • MODIFY_AUDIO_SETTINGS
  • NFC
  • READ_SYNC_SETTINGS
  • READ_SYNC_STATS
  • RECEIVE_BOOT_COMPLETED
  • REORDER_TASKS
  • REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
  • REQUEST_INSTALL_PACKAGES
  • SET_ALARM
  • SET_TIME_ZONE
  • SET_WALLPAPER
  • SET_WALLPAPER_HINTS
  • TRANSMIT_IR
  • UNINSTALL_SHORTCUT
  • USE_FINGERPRINT
  • VIBRATE
  • WAKE_LOCK
  • WRITE_SYNC_SETTINGS

我们大概看一遍过去,可以总结归纳出正常权限有几类:蓝牙,网络状态,NFC,指纹,闹铃,快捷方式,震动等常用权限

危险权限

危险权限才是运行时权限的主要处理对象,因为这些权限可能引起隐私wenti或者影响其他程序运行,android中危险权限主要以组的形式出现,可以归纳为一下几个分组:

  • CALENDAR
  • CAMERA
  • CONTACTS
  • LOCATION
  • MICROPHONE
  • PHONE
  • SENSORS
  • SMS
  • STORAGE

具体请看下图:
android-permission-group

那么问题来了

Q1.我们的应用是否必须支持运行时权限?
Android为了应用兼容,实际上是可以不需要支持运行时权限的,只要设置targetSdkVersion低于23就可以了,意思是我的应用还乜有在API 23上面完全兼容,不要给我开启运行时权限新特性,当然了,早晚你还是要支持的,时间问题而已

Q2.如果不支持运行时权限,应用会崩溃么?
可能会奔溃,具体要根据你使用运行时权限在代码的什么地方,假设你应用启动就开始创建SDCard文件夹,但是在Android 6.0的设备上,系统或者用户在系统权限管理界面禁止了你的SDCard权限,如果应用代码处理不当,此时重新启动应用就会发生奔溃

这里我们还要注意的一点是,危险权限是以组的方式授予的,怎么理解呢?按照我们上面列出的9大分组,举个栗子,如果你申请读取SDCard权限,在用户授予权限后,你自动就获得了写入SDCard的权限,也就是说你获得了STORAGE分组的所有权限

运行时权限申请

API使用

申请运行时权限主要使用到的API有下面三个:

# 检测系统当前是否被授予某个运行时权限,传入参数是权限名称,比如Manifest.permission.READ_EXTERNAL_STORAGE
ContextCompat.checkSelfPermission(@NonNull Context context, @NonNull String permission)

# 是否要显示权限说明
# 1.用户第一次被拒绝(非永久拒绝)授予某个权限后,下次再次请求该权限,这个方法会返回true,用户有机会以某种方式对用户进行说明该权限用处
# 2.用户在第一次拒绝某个权限后,下次再次申请时,授权的dialog中将会出现“不再提醒”选项,一旦选中勾选了,那么下次申请将不会提示用户。
# 3.第二次请求权限时,用户拒绝了,并选择了“不再提醒”的选项,调用shouldShowRequestPermissionRationale()后返回false。
# 4.设备的策略禁止当前应用获取这个权限的授权:shouldShowRequestPermissionRationale()返回false 。
# 5.加这个提醒的好处在于,用户拒绝过一次权限后我们再次申请时可以提醒该权限的重要性,免得再次申请时用户勾选“不再提醒”并决绝,导致下次申请权限直接失败。
ActivityCompat.shouldShowRequestPermissionRationale(@NonNull Activity activity,@NonNull String permission)

# 请求权限授予,当然可以传入多个权限名称同时申请,用户会依次弹出多个提示框申请权限,App不能配置和修改这个对话框,如果需要提示用户这个权限相关的信息或说明,需要在调用 requestPermissions() 之前处理,该方法有两个参数: 
# int requestCode,会在回调onRequestPermissionsResult()时返回,用来判断是哪个授权申请的回调。
# String[] permissions,权限数组,你需要申请的的权限的数组。
# 由于该方法是异步的,所以无返回值,当用户处理完授权操作时,会回调Activity或者Fragment的onRequestPermissionsResult()方法。
ActivityCompat.requestPermissions(final @NonNull Activity activity,final @NonNull String[] permissions, final @IntRange(from = 0) int requestCode)

# 用户拒绝或者授予用户权限后的回调方法,该方法在Activity/Fragment中应该被重写,当用户处理完授权操作时,系统会自动回调该方法,该方法有三个参数: 
# int requestCode,在调用requestPermissions()时的第一个参数。
# String[] permissions,权限数组,在调用requestPermissions()时的第二个参数。
# int[] grantResults,授权结果数组,对应permissions,具体值和上方提到的PackageManager中的两个常量做比较。
onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)

我们以请求读写SDCard权限,来看一下一个标准的请求权限流程

# 判断当前系统版本是都是6.0以上
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    # 检测是否已经被用户授予该权限
    if (ContextCompat.checkSelfPermission(LaunchActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE) !=               PackageManager.PERMISSION_GRANTED) {
          # 没有被授予权限
          # 判断如果已经被用户拒绝过一次(非永久拒绝),则显示用户此权限的一些说明,这里可以用toast,当然也可以用自定义界面来显示给用户,如果是采用自定义界面来显示给用户,在有“确定”,“取消”等按钮的情况下,下面的“请求权限”步骤要酌情调用
          if (ActivityCompat.shouldShowRequestPermissionRationale(LaunchActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE)) {
                   Toast.makeText(LaunchActivity.this, "request read external storage", Toast.LENGTH_LONG).show();
          }
          # 请求权限,数组传入多个值,可以一次请求多个权限
          ActivityCompat.requestPermissions(LaunchActivity.this,
                  new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
                  PERMISSIONS_REQUEST_EXTERNAL_STORAGE);
    }
} else {
     # 6.0以下版本,直接使用权限
     // 做一些权限对应的操作
}

如果是在6.0及6.0以上版本时,执行 ActivityCompat.requestPermissions()方法请求权限后,如果用户同意或者拒绝后,会回调onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)方法,该方法的几个参数,我们做一下解释:

  1. requestCode 对应的是ActivityCompat.requestPermissions请求时的requestCode,这个与Activity中常用的onActivityResult方法中的requestCode类似,用来标识某一次或者某一个请求的回调对应关系
  2. permissions 对应ActivityCompat.requestPermissions()方法中请求的权限列表
  3. grantResults 对应2中permissions的每个权限的用户应答结果

好了,接下来就是根据requestCode,遍历permissions和grantResults中的结果,做对应的操作

@Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        switch (requestCode) {
            case PERMISSIONS_REQUEST_EXTERNAL_STORAGE: {
                // If request is cancelled, the result arrays are empty.
                if (grantResults.length > 0
                        && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    // permission was granted, yay! Do the
                    // contacts-related task you need to do.
                } else {
                    // permission denied, boo! Disable the
                    // functionality that depends on this permission.
                }
                return;
            }
        }
    }

注意事项

API问题

由于checkSelfPermission和requestPermissions从API 23才加入,低于23版本,需要在运行时判断 或者使用Support Library v4中提供的方法

  • ContextCompat.checkSelfPermission
  • ActivityCompat.requestPermissions
  • ActivityCompat.shouldShowRequestPermissionRationale

两个权限

运行时权限对于应用影响比较大的权限有两个,他们分别是

  • READ_PHONE_STATE
  • WRITE_EXTERNAL_STORAGE/READ_EXTERNAL_STORAGE

其中READ_PHONE_STATE用来获取deviceID,即IMEI号码。这是很多统计依赖计算设备唯一ID的参考。如果新的权限导致读取不到,避免导致统计的异常。建议在完全支持运行时权限之前,将对应的值写入到App本地数据中,对于新安装的,可以采取其他策略减少对统计的影响。

WRITE_EXTERNAL_STORAGE/READ_EXTERNAL_STORAGE这两个权限和外置存储(即sdcard)有关,对于下载相关的应用这一点还是比较重要的,我们应该尽可能的说明和引导用户授予该权限。

建议

  • 不要使用多余的权限,新增权限时要慎重
  • 使用Intent来替代某些权限,如拨打电话,选择图片(和你的产品经理PK去吧)
  • 对于使用权限获取的某些值,比如deviceId,尽量本地存储,下次访问直接使用本地的数据值

注意,由于用户可以撤销某些权限,所以不要使用应用本地的标志位来记录是否获取到某权限,在使用的时候要遵循流程实时判断后使用,避免不正确的使用导致应用崩溃

注意

  • 即使支持了运行时权限,也要在Manifest声明,因为市场应用会根据这个信息和硬件设备进行匹配,决定你的应用是否在该设备上显示。

  • 防止一次请求太多的权限或请求次数太多,用户可能对你的应用感到厌烦,在应用启动的时候,最好先请求应用必须的一些权限,非必须权限在使用的时候才请求,建议整理并按照上述分类管理自己的权限

  • 权限申请弹出的对话框不能自定义

  • 解释你的应用为什么需要这些权限:在你调用requestPermissions()之前,你为什么需要这个权限

  • 个人觉得Marshmallow的运行时权限对于用户来说绝对是一个好东西,但是目前想要支持需要做的事情还是比较多的。

  • 对于一个有很多依赖的宿主应用,想要做到支持还是有一些工作量的,因为你的权限申请受制于依赖。

最后,通过以上使用例子的代码我们看到,虽然原理和内容很简单,但是流程上我们还是要写很多代码,判断几个条件,稍后我将在另外一片文章中根据请求运行时权限的特点,封装出一个库EPermission ,建议大家使用这个库,简化权限申请流程

附录

权限结果常量

PackageManager.PERMISSION_DENIED:该权限是被拒绝的。
PackageManager.PERMISSION_GRANTED:该权限是被授权的。

权限名称

Manifest.permission.READ_EXTERNAL_STORAGE
Manifest.permission.CAMERA
等等Manifest.permission类中对应的常量