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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341