Android 6.0 运行权限解析

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

权限简介

  • 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};

需要注意

  • Activity 要继承不继承 AppCompatActivity 不重要,只要能找到 ActivityCompat 类即可,只要添加一条依赖即可,另外项目中的 targetSdkVersion 大于等于 API 23(安卓6.0),可以直接用 Activity 类中的方法,查看源码 ActivityCompat 得知,最后还是会调用 Activity 的方法,只不过做了一些判断,避免低版本 Activity 使用这些方法导致的崩溃
implementation '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_STORAGEWRITE_EXTERNAL_STORAGE 权限就是属于 STORAGE 组的。分门别类不仅仅是为了方便容易阅读,组内权限在申请上也是有关联的

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

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

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

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

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

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

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);

Android 8.0 权限适配

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

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

  • 例如,假设某个应用在其清单中列出 READ_EXTERNAL_STORAGEWRITE_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)下是会出现问题的,解决方案是需要两个将读和写的权限一起申请。

开源框架

Android 技术讨论 Q 群:10047167