Android 6.0运行权限解析(高级篇)

作者: Android轮子哥 | 来源:发表于2018-01-03 21:08 被阅读180次

    Android M对权限管理系统进行了改版,之前我们的App需要权限,只需在manifest中申明即可,用户安装后,一切申明的权限都可来去自如的使用。但是Android M把权限管理做了加强处理,在manifest申明了,在使用到相关功能时,还需重新授权方可使用。当然,不是所有权限都需重新授权,所以就把这些需要重新授权方可使用的权限称之为运行时权限

    PermissionLogo.png

    权限简介

    Android出于系统稳定性以及用户隐私方面的考虑,将应用程序访问权限限制在各自的沙盒内。程序可以随意访问所在沙盒内部的资源或者信息,访问沙盒外部的则必须明确的申请相关访问权限。应用程序所需要的权限需要在AndroidManifest.xml文件中申明。如:

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"    
            package="com.hjq.permission">
    
        <uses-permission android:name="android.permission.VIBRATE" />
        <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />  
    
        <application ...>    
        ...  
        </application>
    
    </manifest>
    

    系统权限根据敏感程度分为普通权限和危险权限两类。两类权限都需要在AndroidManifest.xml文件中申明。在Android 5.1 (API level 22) 及其以下,系统在APP安装时要求用户授权所有权限,否则APP不能安装;而在Android 6.0及其以上版本上,系统在APP安装时授权所有普通权限,危险权限需要在使用时动态让用户授权。这使得Android的权限管理更加灵活,用户可以根据需要在设置应用中对应用的各个危险权限授予不同的权限。

    普通权限

    android.permission.ACCESS_LOCATION_EXTRA_COMMANDS
    android.permission.ACCESS_NETWORK_STATE
    android.permission.ACCESS_NOTIFICATION_POLICY
    android.permission.ACCESS_WIFI_STATE
    android.permission.ACCESS_WIMAX_STATE
    android.permission.BLUETOOTH
    android.permission.BLUETOOTH_ADMIN
    android.permission.BROADCAST_STICKY
    android.permission.CHANGE_NETWORK_STATE
    android.permission.CHANGE_WIFI_MULTICAST_STATE
    android.permission.CHANGE_WIFI_STATE
    android.permission.CHANGE_WIMAX_STATE
    android.permission.DISABLE_KEYGUARD
    android.permission.EXPAND_STATUS_BAR
    android.permission.FLASHLIGHT
    android.permission.GET_ACCOUNTS
    android.permission.GET_PACKAGE_SIZE
    android.permission.INTERNET
    android.permission.KILL_BACKGROUND_PROCESSES
    android.permission.MODIFY_AUDIO_SETTINGS
    android.permission.NFC
    android.permission.READ_SYNC_SETTINGS
    android.permission.READ_SYNC_STATS
    android.permission.RECEIVE_BOOT_COMPLETED
    android.permission.REORDER_TASKS
    android.permission.REQUEST_INSTALL_PACKAGES
    android.permission.SET_TIME_ZONE
    android.permission.SET_WALLPAPER
    android.permission.SET_WALLPAPER_HINTS
    android.permission.SUBSCRIBED_FEEDS_READ
    android.permission.TRANSMIT_IR
    android.permission.USE_FINGERPRINT
    android.permission.VIBRATE
    android.permission.WAKE_LOCK
    android.permission.WRITE_SYNC_SETTINGS
    com.android.alarm.permission.SET_ALARM
    com.android.launcher.permission.INSTALL_SHORTCUT
    com.android.launcher.permission.UNINSTALL_SHORTCUT
    

    危险权限

    并不是所有的危险权限都能申请,有某些权限系统是默认禁止的,目前暂时没有任何办法获取

    涉及日历,摄像头,联系人,位置,话筒,电话,传感器,短信,存储

    //日历
    public static final String[] CALENDAR_GROUP = {Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR};
    //摄像头
    public static final String[] CAMERA_GROUP = {Manifest.permission.CAMERA};
    //联系人
    public static final String[] CONTACTS_GROUP = {Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS, Manifest.permission.GET_ACCOUNTS};
    //位置
    public static final String[] LOCATION_GROUP = {Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION};
    //话筒
    public static final String[] MICROPHONE_GROUP = {Manifest.permission.RECORD_AUDIO};
    //电话
    public static final String[] PHONE_GROUP = {Manifest.permission.READ_PHONE_STATE, Manifest.permission.CALL_PHONE, Manifest.permission.READ_CALL_LOG, Manifest.permission.WRITE_CALL_LOG, Manifest.permission.ADD_VOICEMAIL, Manifest.permission.USE_SIP, Manifest.permission.PROCESS_OUTGOING_CALLS};
    //传感器
    public static final String[] SENSORS_GROUP = {Manifest.permission.BODY_SENSORS};
    //短信
    public static final String[] SMS_GROUP = {Manifest.permission.SEND_SMS, Manifest.permission.RECEIVE_SMS, Manifest.permission.READ_SMS, Manifest.permission.RECEIVE_WAP_PUSH, Manifest.permission.RECEIVE_MMS};
    //存储
    public static final String[] STORAGE_GROUP = {Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE};
    
    DangerousPermissions.png

    需要注意

    Activity要继承不继承AppCompatActivity不重要,只要能找到ActivityCompat类即可,只要添加一条依赖即可,另外项目中的minSdkVersion大于等于API 23(安卓6.0),可以直接用Activity类中的方法,查看源码ActivityCompat得知,最后还是会调用Activity的方法,只不过做了一些判断,避免低版本Activity使用这些方法导致的崩溃

    compile 'com.android.support:appcompat-v7:25.3.1'
    

    危险权限在AndroidManifest.xml文件中也必须申明,否则动态申请会失败

    权限常量标识

    可以用于判断checkSelfPermission方法返回的数据

    也可以用于判断onRequestPermissionsResult方法中的grantResults参数

    /**
     * 授权了
     */
    public static final int PERMISSION_GRANTED = 0;
    
    /**
     * 拒绝了
     */
    public static final int PERMISSION_DENIED = -1;
    

    checkSelfPermission

    /**
     * 检测某个权限是否授予
     * @param context           Context对象
     * @param permission        需要检测的权限
     */
    ContextCompat.checkSelfPermission(Context context, String permission);
    //又或者使用子类的方法
    ActivityCompat.checkSelfPermission(Context context, String permission);
    //minSdkVersion >= 23 可以直接使用
    activity.checkSelfPermission(String permission);
    

    检查是否已经具有了相关权限。任何时候APP都要在执行需要危险权限的操作前去检查是否具有相关权限,即使刚刚执行过这项操作,因为用户很有可能去设置应用中关闭了相关权限

    举个栗子,如何判断这个权限有没有被授权

    if (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED){
        
    }
    

    requestPermissions

    /**
     * 申请相关权限
     * @param activity          Activity对象
     * @param permissions       请求的权限组
     * @param requestCode       本次请求码
     */
    ActivityCompat.requestPermissions(Activity activity, String[] permissions, int requestCode);
    //minSdkVersion >= 23 可以直接使用
    activity.requestPermissions(String[] permissions, int requestCode);
    

    申请相关权限。调用这个方法后会弹出一个系统对话框来向用户申请权限,APP不能自定义这个对话框的内容,这也就增加了上面提到的解释说明的必要性。这里还有一点也需要交代一下。从上面危险权限列表中也可以看出,这些权限都是有分组的。如,READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE权限就是属于STORAGE组的。分门别类不仅仅是为了方便容易阅读,组内权限在申请上也是有关联的

    • 在申请组内某个权限时,弹出的系统对话框会显示组名,而不是指明所申请的权限名。如,申请READ_EXTERNAL_STORAGE权限时,系统对话框提示请求“访问sd卡”权限,但不会说明是请求的sd卡读权限

    • 申请权限时,在使用每一条权限时都必须(不是应该)调用requestPermissions()方法来申请权限。如,在已经获取了READ_EXTERNAL_STORAGE权限的情况下,使用WRITE_EXTERNAL_STORAGE权限时依然需要调用requestPermissions()方法来申请,否则就会因为权限问题导致写sd卡失败

    经过一定的测试,得到以下结论

    • 第一次安装后请求权限:没有不再询问的选项

    • 被拒绝后再次请求权限,会有不再询问的选项

    • 被拒绝权限且不再询问,后面再请求是不会再弹框

    FristRequest.png CommonRequest.png

    shouldShowRequestPermissionRationale

    /**
     * 是否需要向用户解释
     * @param activity          Activity对象
     * @param permission        需要检测的权限
     */
    ActivityCompat.shouldShowRequestPermissionRationale(Activity activity, String permission);
    //minSdkVersion >= 23 可以直接使用
    activity.shouldShowRequestPermissionRationale(String permission);
    

    判断是否需要向用户解释,为什么需要这些权限。有时候用户会不理解应用程序为什么需要这些权限。如,相机应用申请摄像头使用权限用户容易理解,但是相机应用申请地理位置使用权限可能会让用户产生疑惑,因为用户很有能不知道相机需要保存每张照片的拍摄地点。这时候我们就需要做适当的解释说明了。这个方法只有在APP请求过某一权限且用户禁止APP使用该权限的时候返回true。在用户授权了权限和禁止权限时勾选了“Don't ask again”选项的情况下都会返回false。Android官方开发指导还提到一点,为避免给用户带来糟糕的用户体验,这里的解释说明应该是异步的,不要阻塞用户的操作。时下很多适配了6.0的APP在这点上处理的都不尽如人意,有的根本没有解释说明,有的是弹出对话框,用户体验都不是很好

    为了帮助查找用户可能需要解释的情形,Android 提供了一个实用程序方法,即 shouldShowRequestPermissionRationale()。如果应用之前请求过此权限但用户拒绝了请求,此方法将返回 true

    如果用户在过去拒绝了权限请求,并在权限请求系统对话框中选择了 Don’t ask again 选项,此方法将返回 false。如果设备规范禁止应用具有该权限,此方法也会返回 false

    shouldShowRequestPermissionRationale方法的源码

    public static boolean shouldShowRequestPermissionRationale(@NonNull Activity activity, @NonNull String permission) {
        if (Build.VERSION.SDK_INT >= 23) {
            return ActivityCompatApi23.shouldShowRequestPermissionRationale(activity, permission);
        }
        return false;
    }
    

    下面是不同应用场景调用的结果,已经过一定的测试

    • 之前没有拒绝过此权限的申请(第一次安装后请求权限前调用):false

    • 曾经被拒绝过权限后再调用:true

    • 曾经被拒绝过权限且不再询问后再调用:false

    • 系统不允许任何程序获取该权限:false

    • 查看源码得知安卓6.0以下返回:false

    • 总是允许权限后再次调用:false

    由此可以得出一个结论,只有曾经拒绝过才需要向用户解释,这句代码应该在Activity的onRequestPermissionsResult中调用比较合适,调用之前应该需要先判断是否为6.0以上设备

    onRequestPermissionsResult

    该方法在Activity或Fragment中应该被重写,当用户处理完授权操作时,系统会自动回调该方法

    /**
     * Activity处理权限结果回调
     * @param requestCode           权限请求码
     * @param permissions           请求的权限组
     * @param grantResults          请求的结果
     */
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {}
    

    该方法有三个参数,调用requestPermissions请求权限之后的回调

    • int requestCode: 权限请求码,和requestPermissions的同名参数对应

    • String[] permissions: 请求权限组,和requestPermissions的同名参数对应

    • int[] grantResults: 授权结果数组,用于区分上一个参数permissions中的权限有没有被授予,permissions和grantResults两个数组大小是一样的,具体值和上方提到的PackageManager中的两个常量做比较

    举个栗子,如何判断请求的这些权限有没有被全部授予

    for (int i = 0; i < grantResults.length ; i++) {
    
        if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
            return false;
        }
    }
    return true;
    

    如何处理被永久拒绝权限

    永久拒绝权限后从授权界面授权再取消授权会恢复到第一次请求的状态,即shouldShowRequestPermissionRationale会返回false,请求的弹窗没有不再询问的选项

    加入以下代码引导用户去系统设置界面开启权限

    Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
    intent.setData(Uri.fromParts("package", context.getPackageName(), null));
    startActivity(intent);
    
    gotoPermissionSettings1.png gotoPermissionSettings2.png

    安卓8.0权限适配

    在 Android 8.0 之前,如果应用在运行时请求权限并且被授予该权限,系统会错误地将属于同一权限组并且在清单中注册的其他权限也一起授予应用。

    对于针对 Android 8.0 的应用,此行为已被纠正。系统只会授予应用明确请求的权限。然而,一旦用户为应用授予某个权限,则所有后续对该权限组中权限的请求都将被自动批准。

    例如,假设某个应用在其清单中列出 READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE。应用请求 READ_EXTERNAL_STORAGE,并且用户授予了该权限。如果该应用针对的是 API 级别 24 或更低级别,系统还会同时授予 WRITE_EXTERNAL_STORAGE,因为该权限也属于同一 STORAGE 权限组并且也在清单中注册过。如果该应用针对的是 Android 8.0,则系统此时仅会授予 READ_EXTERNAL_STORAGE;不过,如果该应用后来又请求 WRITE_EXTERNAL_STORAGE,则系统会立即授予该权限,而不会提示用户

    Android 8.0 之前的版本,同一组的任何一个权限被授权了,组内的其他权限也自动被授权,但是Android 8.0之后的版本,需要更明确指定所使用的权限,并且系统只会授予申请的权限,不会授予没有组内的其他权限,这意味着,如果只申请了外部存储空间读取权限,在低版本下(API < 26)对外部存储空间使用写入操作是没有问题的,但是在高版本(API >= 26)下是会出现问题的,解决方案是需要两个将读和写的权限一起申请

    开源框架

    我自己写了一个权限请求框架,在我手上经过了大半年的维护和更新,简单易用、方便高效的权限请求框架Github地址,欢迎issues,欢迎star,一句代码搞定权限请求,从未如此简单,欢迎留言

    相关文章

      网友评论

      • mandypig:“第一次安装后请求权限:没有不再询问的选项” 这个结论不一定对,实测在华为honor上即使第一次请求权限也会有不再询问选项,国产手机改来改去各种适配问题 真的蛋疼
        Android轮子哥:@mandypig 原来如此
      • Android平头哥:楼主,写的不错

      本文标题:Android 6.0运行权限解析(高级篇)

      本文链接:https://www.haomeiwen.com/subject/jjuknxtx.html