|
| 1 | +# 目录 |
| 2 | + |
| 3 | +* [Android 11 定位权限适配](#Android 11 定位权限适配) |
| 4 | + |
| 5 | +* [Android 11 存储权限适配](#Android 11 存储权限适配) |
| 6 | + |
| 7 | +* [我想在申请前和申请后统一弹 Dialog 该怎么办?](#我想在申请前和申请后统一弹 Dialog 该怎么办?) |
| 8 | + |
| 9 | +* [框架为什么不兼容 Android 6.0 以下的权限申请?](#框架为什么不兼容 Android 6.0 以下的权限申请?) |
| 10 | + |
| 11 | +* [如何在 onDenied 回调中判断哪些权限被永久拒绝了?](#如何在 onDenied 回调中判断哪些权限被永久拒绝了?) |
| 12 | + |
| 13 | +* [新版 XXPermissions 为什么移除了自动申请清单权限 API?](#新版 XXPermissions 为什么移除了自动申请清单权限 API?) |
| 14 | + |
| 15 | +* [新版 XXPermissions 为什么移除了不断申请权限 API?](#新版 XXPermissions 为什么移除了不断申请权限 API?) |
| 16 | + |
| 17 | +* [新版 XXPermissions 为什么移除了国产手机权限设置页功能?](#新版 XXPermissions 为什么移除了国产手机权限设置页功能?) |
| 18 | + |
| 19 | +#### Android 11 定位权限适配 |
| 20 | + |
| 21 | +* 在 Android 10 上面,定位被划分为前台权限(精确和模糊)和后台权限,而到了 Android 11 上面,需要分别申请这两种权限,如果同时申请这两种权限会惨遭系统无情拒绝,连权限申请对话框都不会弹,立马被系统拒绝,直接导致定位权限申请失败。 |
| 22 | + |
| 23 | +* 如果你使用的是 XXPermissions 最新版本,那么恭喜你,直接将前台定位权限和后台定位权限全部传给框架即可,框架已经自动帮你把这两种权限分开申请了,整个适配过程零成本。 |
| 24 | + |
| 25 | +#### Android 11 存储权限适配 |
| 26 | + |
| 27 | +* 如果你的项目需要适配 Android 11 存储权限,那么需要先将 targetSdkVersion 进行升级 |
| 28 | + |
| 29 | +```groovy |
| 30 | +android |
| 31 | + defaultConfig { |
| 32 | + targetSdkVersion 30 |
| 33 | + } |
| 34 | +} |
| 35 | +``` |
| 36 | + |
| 37 | +* 再添加 Android 11 存储权限注册到清单文件中 |
| 38 | + |
| 39 | +```xml |
| 40 | +<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> |
| 41 | +``` |
| 42 | + |
| 43 | +* 需要注意的是,旧版的存储权限也需要在清单文件中注册,因为在低于 Android 11 的环境下申请存储权限,框架会自动切换到旧版的申请方式 |
| 44 | + |
| 45 | +```xml |
| 46 | +<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> |
| 47 | +<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> |
| 48 | +``` |
| 49 | + |
| 50 | +* 还需要在清单文件中加上这个属性,否则在 Android 10 的设备上将无法正常读写外部存储上的文件 |
| 51 | + |
| 52 | +```xml |
| 53 | +<application |
| 54 | + android:requestLegacyExternalStorage="true"> |
| 55 | +``` |
| 56 | + |
| 57 | +* 最后直接调用下面这句代码 |
| 58 | + |
| 59 | +```java |
| 60 | +XXPermissions.with(this) |
| 61 | + // 不适配 Android 11 可以这样写 |
| 62 | + //.permission(Permission.Group.STORAGE) |
| 63 | + // 适配 Android 11 需要这样写,这里无需再写 Permission.Group.STORAGE |
| 64 | + .permission(Permission.MANAGE_EXTERNAL_STORAGE) |
| 65 | + .request(new OnPermissionCallback() { |
| 66 | + |
| 67 | + @Override |
| 68 | + public void onGranted(List<String> permissions, boolean all) { |
| 69 | + if (all) { |
| 70 | + toast("获取存储权限成功"); |
| 71 | + } |
| 72 | + } |
| 73 | + |
| 74 | + @Override |
| 75 | + public void onDenied(List<String> permissions, boolean never) { |
| 76 | + if (never) { |
| 77 | + toast("被永久拒绝授权,请手动授予存储权限"); |
| 78 | + // 如果是被永久拒绝就跳转到应用权限系统设置页面 |
| 79 | + XXPermissions.startPermissionActivity(MainActivity.this, permissions); |
| 80 | + } else { |
| 81 | + toast("获取存储权限失败"); |
| 82 | + } |
| 83 | + } |
| 84 | + }); |
| 85 | +``` |
| 86 | + |
| 87 | + |
| 88 | + |
| 89 | +#### 我想在申请前和申请后统一弹 Dialog 该怎么办? |
| 90 | + |
| 91 | +* 在 Application 初始化的时候配置 |
| 92 | + |
| 93 | +```java |
| 94 | +public class MyApplication extends Application { |
| 95 | + |
| 96 | + @Override |
| 97 | + public void onCreate() { |
| 98 | + super.onCreate(); |
| 99 | + // 设置权限申请拦截器 |
| 100 | + XXPermissions.setPermissionInterceptor(new PermissionInterceptor()); |
| 101 | + } |
| 102 | +} |
| 103 | +``` |
| 104 | + |
| 105 | +* PermissionInterceptor 源码实现 |
| 106 | + |
| 107 | +```java |
| 108 | +public class PermissionInterceptor implements IPermissionInterceptor { |
| 109 | + |
| 110 | + @Override |
| 111 | + public void requestPermissions(FragmentActivity activity, OnPermissionCallback callback, List<String> permissions) { |
| 112 | + new AlertDialog.Builder(activity) |
| 113 | + .setTitle("授权提示") |
| 114 | + .setMessage("使用此功能需要先授予权限") |
| 115 | + .setPositiveButton("授予", new DialogInterface.OnClickListener() { |
| 116 | + |
| 117 | + @Override |
| 118 | + public void onClick(DialogInterface dialog, int which) { |
| 119 | + dialog.dismiss(); |
| 120 | + PermissionFragment.beginRequest(activity, new ArrayList<>(permissions), callback); |
| 121 | + } |
| 122 | + }) |
| 123 | + .setNegativeButton("取消", new DialogInterface.OnClickListener() { |
| 124 | + |
| 125 | + @Override |
| 126 | + public void onClick(DialogInterface dialog, int which) { |
| 127 | + dialog.dismiss(); |
| 128 | + } |
| 129 | + }) |
| 130 | + .show(); |
| 131 | + } |
| 132 | + |
| 133 | + @Override |
| 134 | + public void grantedPermissions(FragmentActivity activity, OnPermissionCallback callback, List<String> permissions, boolean all) { |
| 135 | + // 回调授权成功的方法 |
| 136 | + callback.onGranted(permissions, all); |
| 137 | + } |
| 138 | + |
| 139 | + @Override |
| 140 | + public void deniedPermissions(FragmentActivity activity, OnPermissionCallback callback, List<String> permissions, boolean never) { |
| 141 | + // 回调授权失败的方法 |
| 142 | + callback.onDenied(permissions, never); |
| 143 | + if (never) { |
| 144 | + showPermissionDialog(activity, permissions); |
| 145 | + return; |
| 146 | + } |
| 147 | + |
| 148 | + if (permissions.size() == 1 && Permission.ACCESS_BACKGROUND_LOCATION.equals(permissions.get(0))) { |
| 149 | + ToastUtils.show("没有授予后台定位权限,请您选择\"始终允许\""); |
| 150 | + return; |
| 151 | + } |
| 152 | + |
| 153 | + ToastUtils.show("授权失败,请正确授予权限"); |
| 154 | + } |
| 155 | + |
| 156 | + /** |
| 157 | + * 显示授权对话框 |
| 158 | + */ |
| 159 | + protected void showPermissionDialog(FragmentActivity activity, List<String> permissions) { |
| 160 | + new AlertDialog.Builder(activity) |
| 161 | + .setTitle("授权提醒") |
| 162 | + .setMessage(getPermissionHint(activity, permissions)) |
| 163 | + .setPositiveButton("前往授权", new DialogInterface.OnClickListener() { |
| 164 | + |
| 165 | + @Override |
| 166 | + public void onClick(DialogInterface dialog, int which) { |
| 167 | + dialog.dismiss(); |
| 168 | + XXPermissions.startPermissionActivity(activity, permissions); |
| 169 | + } |
| 170 | + }) |
| 171 | + .setNegativeButton("取消", new DialogInterface.OnClickListener() { |
| 172 | + |
| 173 | + @Override |
| 174 | + public void onClick(DialogInterface dialog, int which) { |
| 175 | + dialog.dismiss(); |
| 176 | + } |
| 177 | + }) |
| 178 | + .show(); |
| 179 | + } |
| 180 | + |
| 181 | + /** |
| 182 | + * 根据权限获取提示 |
| 183 | + */ |
| 184 | + protected String getPermissionHint(Context context, List<String> permissions) { |
| 185 | + // 具体实现请看 Demo 源码 |
| 186 | + } |
| 187 | +} |
| 188 | +``` |
| 189 | + |
| 190 | +#### 如何在 onDenied 回调中判断哪些权限被永久拒绝了? |
| 191 | + |
| 192 | +* 需求场景:假设同时申请日历权限和录音权限,结果都被用户拒绝了,但是这两组权限中有一组权限被永久拒绝了,如何判断某一组权限有没有被永久拒绝?这里给出代码示例: |
| 193 | + |
| 194 | +```java |
| 195 | +XXPermissions.with(this) |
| 196 | + .permission(Permission.RECORD_AUDIO) |
| 197 | + .permission(Permission.Group.CALENDAR) |
| 198 | + .request(new OnPermissionCallback() { |
| 199 | + |
| 200 | + @Override |
| 201 | + public void onGranted(List<String> permissions, boolean all) { |
| 202 | + if (all) { |
| 203 | + toast("获取录音和日历权限成功"); |
| 204 | + } |
| 205 | + } |
| 206 | + |
| 207 | + @Override |
| 208 | + public void onDenied(List<String> permissions, boolean never) { |
| 209 | + if (never && permissions.contains(Permission.RECORD_AUDIO) && |
| 210 | + XXPermissions.isPermissionPermanentDenied(MainActivity.this, Permission.RECORD_AUDIO)) { |
| 211 | + toast("录音权限被永久拒绝了"); |
| 212 | + } |
| 213 | + } |
| 214 | + }); |
| 215 | +``` |
| 216 | + |
| 217 | +#### 框架为什么不兼容 Android 6.0 以下的权限申请? |
| 218 | + |
| 219 | +* 因为 Android 6.0 以下的权限管理是手机厂商做的,那个时候谷歌还没有统一权限管理的方案,所以就算我们的应用没有适配也不会有任何问题,因为手机厂商对这块有自己的处理,但是有一点是肯定的,就算用户拒绝了授权,也不会导致应用崩溃,只会返回空白的通行证。 |
| 220 | + |
| 221 | +* 如果 XXPermissions 做这块的适配也可以做到,通过反射系统服务 AppOpsManager 类中的字段即可,但是并不能保证权限判断的准确性,可能会存在一定的误差,其次是适配的成本太高,因为国内手机厂商太多,对这块的改动参差不齐。 |
| 222 | + |
| 223 | +* 考虑到 Android 6.0 以下的设备占比很低,后续也会越来越少,会逐步退出历史的舞台,所以我的决定是不对这块做适配。 |
| 224 | + |
| 225 | +#### 新版 XXPermissions 为什么移除了自动申请清单权限 API? |
| 226 | + |
| 227 | +* 获取清单权限并申请的功能,这个虽然非常方便,但是存在一些隐患,因为 apk 中的清单文件最终是由多个 module 的清单文件合并而成,会变得不可控,这样会使我们无法预估申请的权限,并且还会掺杂一些不需要的权限,所以经过慎重考虑移除该功能。 |
| 228 | + |
| 229 | +#### 新版 XXPermissions 为什么移除了不断申请权限 API? |
| 230 | + |
| 231 | +* [【issue】建议恢复跳转权限设置页和获取AndroidManifest的所有权限两个实用功能](https://github.com/getActivity/XXPermissions/issues/54) |
| 232 | + |
| 233 | +* 假设用户拒绝了权限,如果框架再次申请,那么用户会授予的可能性也是比较小,同时某些应用商店已经禁用了这种行为,经过慎重考虑,对这个功能相关的 API 进行移除。 |
| 234 | + |
| 235 | +* 如果你还想用这种方式来申请权限,其实并不是没有办法,可以参考以下方式来实现 |
| 236 | + |
| 237 | +```java |
| 238 | +public class PermissionActivity extends AppCompatActivity implements OnPermissionCallback { |
| 239 | + |
| 240 | + @Override |
| 241 | + public void onClick(View view) { |
| 242 | + requestCameraPermission(); |
| 243 | + } |
| 244 | + |
| 245 | + private void requestCameraPermission() { |
| 246 | + XXPermissions.with(this) |
| 247 | + .permission(Permission.CAMERA) |
| 248 | + .request(this); |
| 249 | + } |
| 250 | + |
| 251 | + @Override |
| 252 | + public void onGranted(List<String> permissions, boolean all) { |
| 253 | + if (all) { |
| 254 | + toast("获取拍照权限成功"); |
| 255 | + } |
| 256 | + } |
| 257 | + |
| 258 | + @Override |
| 259 | + public void onDenied(List<String> permissions, boolean never) { |
| 260 | + if (never) { |
| 261 | + toast("被永久拒绝授权,请手动授予拍照权限"); |
| 262 | + // 如果是被永久拒绝就跳转到应用权限系统设置页面 |
| 263 | + XXPermissions.startPermissionActivity(MainActivity.this, permissions); |
| 264 | + } else { |
| 265 | + requestCameraPermission(); |
| 266 | + } |
| 267 | + } |
| 268 | + |
| 269 | + @Override |
| 270 | + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { |
| 271 | + super.onActivityResult(requestCode, resultCode, data); |
| 272 | + if (requestCode == XXPermissions.REQUEST_CODE) { |
| 273 | + toast("检测到你刚刚从权限设置界面返回回来"); |
| 274 | + } |
| 275 | + } |
| 276 | +} |
| 277 | +``` |
| 278 | + |
| 279 | +#### 新版 XXPermissions 为什么移除了国产手机权限设置页功能? |
| 280 | + |
| 281 | +* XXPermissions 9.0 及之前是有存在这一功能的,但是我在后续的版本上面将这个功能移除了,原因是有很多人跟我反馈这个功能其实存在很大的缺陷,例如在一些华为新机型上面可能跳转的页面不是应用的权限设置页,而是所有应用的权限管理列表界面。 |
| 282 | + |
| 283 | +* 其实不止华为有问题,小米同样有问题,有很多人跟我反馈过同一个问题,XXPermissions 跳转到国产手机权限设置页,用户正常授予了权限之后返回仍然检测到权限仍然是拒绝的状态,这个问题反馈的次数很多,但是迟迟不能排查到原因,终于在最后一次得到答案了,[有人](https://github.com/getActivity/XXPermissions/issues/38)帮我排查到是 miui 优化开关的问题(小米手机 ---> 开发者选项 ---> 启用 miui 优化),那么问题来了,这个开关有什么作用?是如何影响到 XXPermissions 的? |
| 284 | + |
| 285 | +* 首先这个问题要从 XXPermissions 跳转到国产手机设置页的原理讲起,从谷歌提供的原生 API 我们最多只能跳转到应用详情页,并不能直接跳转到权限设置页,而需要用户在应用详情页再次点击才能进入权限设置页。如果从用户体验的角度上看待这个问题,肯定是直接跳转到权限设置页是最好的,但是这种方式是不受谷歌支持的,当然也有方法实现,网上都有一个通用的答案,就是直接捕获某个品牌手机的权限设置页 `Activity` 包名然后进行跳转。这种想法的起点是好的,但是存在许多问题,并不能保证每个品牌的所有机型都能适配到位,手机产商更改这个 `Activity` 的包名的次数和频率比较高,在最近发布的一些新的华为机型上面几乎已经全部失效,也就是 `startActivity` 的时候会报 `ActivityNotFoundException` 或 `SecurityException ` 异常,当然这些异常是可以被捕捉到的,但是仅仅只能捕获到崩溃,一些非崩溃的行为我们并不能从中得知和处理,例如我刚刚讲过的华为和小米的问题,这些问题并不能导致崩溃,但是会导致功能出现异常。 |
| 286 | + |
| 287 | +* 而 miui 优化开关是小米工程师预留的切换 miui 和原生的功能开关,例如在这个开关开启的时候,在应用详情页点击权限管理会跳转到小米的权限设置页,如果这个开关是关闭状态(默认是开启状态),在应用详情页点击权限管理会跳转到谷歌原生的权限设置页,具体效果如图: |
| 288 | + |
| 289 | + |
| 290 | + |
| 291 | + |
| 292 | + |
| 293 | +* 最大的问题在于:这两个界面是不同的 Activity,一个是小米定制的权限设置页,第二个是谷歌原生的权限设置页,当 miui 优化开启的时候,在小米定制的权限设置页授予权限才能有效果,当这个 miui 优化关闭的时候,在谷歌原生的权限设置页授予权限才能有效果。而跳转到国产手机页永远只会跳转到小米定制的那个权限设置页,所以就会导致当 miui 优化关闭的时候,使用代码跳转到小米权限设置页授予了权限之后返回仍然显示失败的问题。 |
| 294 | + |
| 295 | +* 有人可能会说,解决这个问题的方式很简单,判断 miui 优化开关,如果是开启状态就跳转到小米定制的权限设置页,如果是关闭状态就跳转到谷歌原生的权限设置页,这样不就可以了?其实这个解决方案我也有尝试过,我曾委托联系到在小米工作的 miui 工程师,也有人帮我反馈这个问题给小米那边,最后得到答复都是一致的。 |
| 296 | + |
| 297 | + |
| 298 | + |
| 299 | + |
| 300 | + |
| 301 | +* 另外值得一提的是 [Android 11 对软件包可见性进行了限制](https://developer.android.google.cn/about/versions/11/privacy/package-visibility),所以这种跳包名的方式在未来将会完全不可行。 |
| 302 | + |
| 303 | +* 最终决定:这个功能的出发点是好的,但是我们没办法做好它,经过慎重考虑,决定将这个功能在 XXPermissions 9.2 版本及之后的版本进行移除。 |
| 304 | + |
0 commit comments