Skip to content

Commit a080b4d

Browse files
committed
适配 Android 13 权限
新增支持撤销权限新特性 优化和补充框架使用文档 优化权限拦截器 API 设计 优化权限委托接口的代码设计 优化权限委托实现类的代码逻辑 优化权限字符串 equals 执行效率 优化权限集合 contains 执行效率 优化所有文件的管理权限在 Android 10 上的判断逻辑 优化获取图片位置权限在 Android 10 及以上的判断逻辑 优化后台定位权限在 Android 12 上的判断逻辑 优化在 Android 9 获取 Android 清单文件 cookie 的逻辑 修正拆分两次请求权限在 Android 13 会出现失败的问题
1 parent 3f13f68 commit a080b4d

27 files changed

+1472
-493
lines changed

HelpDoc.md

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
* [什么情况下需要适配分区存储特性](#什么情况下需要适配分区存储特性)
88

9+
* [Android 11 授予了安装权限之后为什么应用重启了](#android-11-授予了安装权限之后为什么应用重启了)
10+
911
* [为什么授予了存储权限但是权限设置页还是显示未授权](#为什么授予了存储权限但是权限设置页还是显示未授权)
1012

1113
* [我想在申请前和申请后统一弹对话框该怎么处理](#我想在申请前和申请后统一弹对话框该怎么处理)
@@ -24,6 +26,10 @@
2426

2527
* [怎么处理权限请求成功但是返回空白通行证的问题](#怎么处理权限请求成功但是返回空白通行证的问题)
2628

29+
* [为什么授权了还是无法访问 Android/data 目录下的文件](#为什么授权了还是无法访问-android-data-目录下的文件)
30+
31+
* [如何应对国内某些应用商店在明确拒绝权限后 48 小时内不允许再次申请的问题](#如何应对国内某些应用商店在明确拒绝权限后-48-小时内不允许再次申请的问题)
32+
2733
#### Android 11 定位权限适配
2834

2935
* 在 Android 10 上面,定位权限被划分为前台权限(精确和模糊)和后台权限,而到了 Android 11 上面,需要分别申请这两种权限,如果同时申请这两种权限会**惨遭系统无情拒绝**,连权限申请对话框都不会弹,立马被系统拒绝,直接导致定位权限申请失败。
@@ -93,11 +99,11 @@ XXPermissions.with(MainActivity.this)
9399

94100
#### 什么情况下需要适配分区存储特性
95101

96-
* 如果你的应用需要上架 GooglePlay,那么需要详细查看:[谷歌应用商店政策(需要翻墙)](https://support.google.com/googleplay/android-developer/answer/9956427)
102+
* 如果你的应用需要上架 GooglePlay,那么需要详细查看:[谷歌应用商店政策(需要翻墙)](https://support.google.com/googleplay/android-developer/answer/9956427)[Google Play 通知](https://developer.android.google.cn/training/data-storage/manage-all-files#all-files-access-google-play)
97103

98104
* 分区存储的由来:谷歌之前收到了很多用户投诉,说很多应用都在 SD 卡下创建目录和文件,导致用户管理手机文件非常麻烦(强迫症的外国网友真多,哈哈),所以在 Android 10 版本更新中,谷歌要求所有开发者将媒体文件存放在自己内部目录或者 SD 卡内部目录中,不过谷歌在一版本上采取了宽松政策,在清单文件中加入 `android:requestLegacyExternalStorage="true"` 即可跳过这一特性的适配,不过在 Android 11 上面,你有两种选择:
99105

100-
1. 适配分区存储:这个是谷歌推荐的一种方式,但是会增加工作量,因为分区存储适配起来十分麻烦,我个人感觉是这样的。不过对于一些特定应用,例如文件管理器,文件备份工具,防病毒应用等这类应用它们就一定需要用到外部存储,这个时候就需要用第二种方式来实现了。
106+
1. 适配分区存储:这个是谷歌推荐的一种方式,但是会增加工作量,因为分区存储适配起来十分麻烦,我个人感觉是这样的。不过对于一些特定应用,例如文件管理器、备份和恢复应用、防病毒应用、文档管理应用、设备上的文件搜索、磁盘和文件加密、设备到设备数据迁移等这类应用它们就一定需要用到外部存储,这个时候就需要用第二种方式来实现了。
101107

102108
2. 申请外部存储权限:这个是谷歌不推荐的一种方式,只需要 `MANAGE_EXTERNAL_STORAGE` 权限即可,适配起来基本无压力,但是会存在一个问题,就是上架谷歌应用市场的时候,要经过 Google Play 审核和批准。
103109

@@ -107,6 +113,14 @@ XXPermissions.with(MainActivity.this)
107113

108114
2. 如果你的应用只上架国内的应用市场,并且后续也没有上架谷歌应用市场的需要,那么你也可以直接申请 `MANAGE_EXTERNAL_STORAGE` 权限来读写外部存储
109115

116+
#### Android 11 授予了安装权限之后为什么应用重启了
117+
118+
* [Android 11 特性调整,安装外部来源应用需要重启 App](https://cloud.tencent.com/developer/news/637591)
119+
120+
* 先说结论,这个问题是 Android 11 的新特性,并非框架的问题导致的,当然这个问题是没有办法规避的,因为应用是被系统杀死的,应用的等级肯定不如系统的高,目前行业对这块也没有解决方案,如果你有好的解决方案,欢迎你提供给我。
121+
122+
* 另外经过实践,这个问题在 Android 12 上面已经不会再出现,证明问题已经被谷歌修复了。
123+
110124
#### 为什么授予了存储权限但是权限设置页还是显示未授权
111125

112126
* 首先我需要先纠正大家一个错误的想法,`READ_EXTERNAL_STORAGE``WRITE_EXTERNAL_STORAGE` 这两个权限和 `MANAGE_EXTERNAL_STORAGE` 权限是两码事,虽然都叫存储权限,但是属于两种完全不同的权限,你如果申请的是 `MANAGE_EXTERNAL_STORAGE` 权限,并且授予了权限,但是在权限设置页并没有看到已授予,请注意这种情况是正常的,因为你在权限设置页看到的是存储授予状态是 `READ_EXTERNAL_STORAGE``WRITE_EXTERNAL_STORAGE` 权限状态的,而不是 `MANAGE_EXTERNAL_STORAGE` 权限状态的,但是这个时候已经获取到存储权限了,你大可不必管权限设置页显示的权限状态,直接读写文件即可,不会有权限问题的。
@@ -284,3 +298,53 @@ public class PermissionActivity extends AppCompatActivity implements OnPermissio
284298

285299
* 此问题无解,权限请求框架只能帮你申请权限,至于你申请权限做什么操作,框架无法知道,也无法干预,还有返回空白通行证是厂商自己的行为,目的就是为了保护用户的隐私,因为在某些应用上面不给权限就不能用,返回空白通行证是为了规避这种情况的发生。你要问我怎么办?我只能说胳膊拗不过大腿,别做一些无谓的抵抗。
286300

301+
#### 为什么授权了还是无法访问 Android/data 目录下的文件
302+
303+
* 首先无论你申请了哪种存储权限,在 Android 11 上面就是无法直接读取 Android/data 目录的,这个是 Android 11 上的新特性,需要你进行额外适配,具体适配流程可以参考这个开源项目 [https://github.com/getActivity/AndroidVersionAdapter](https://github.com/getActivity/AndroidVersionAdapter)
304+
305+
#### 如何应对国内某些应用商店在明确拒绝权限后 48 小时内不允许再次申请的问题
306+
307+
* 首先这种属于业务逻辑的问题,框架本身是不会做这种事情的,但并非不能实现,这得益于框架良好的设计,框架内部提供了一个叫 IPermissionInterceptor 的拦截器类,当前有权限申请的时候,会走 requestPermissions 方法的回调,你可以重写这个方法的逻辑,先去判断要申请的权限是否在 48 小时内已经申请过了一次了,如果没有的话,就走权限申请的流程,如果有的话,那么就直接回调权限申请失败的方法。
308+
309+
```java
310+
public final class PermissionInterceptor implements IPermissionInterceptor {
311+
312+
private static final String SP_NAME_PERMISSION_REQUEST_TIME_RECORD = "permission_request_time_record";
313+
314+
@Override
315+
public void requestPermissions(Activity activity, List<String> allPermissions, OnPermissionCallback callback) {
316+
SharedPreferences sharedPreferences = activity.getSharedPreferences(SP_NAME_PERMISSION_REQUEST_TIME_RECORD, Context.MODE_PRIVATE);
317+
String permissionKey = String.valueOf(allPermissions);
318+
long lastRequestPermissionTime = sharedPreferences.getLong(permissionKey, 0);
319+
if (System.currentTimeMillis() - lastRequestPermissionTime <= 1000 * 60 * 60 * 24 * 2) {
320+
List<String> deniedPermissions = XXPermissions.getDenied(activity, allPermissions);
321+
List<String> grantedPermissions = new ArrayList<>(allPermissions);
322+
grantedPermissions.removeAll(deniedPermissions);
323+
deniedPermissions(activity, allPermissions, deniedPermissions, true, callback);
324+
if (!grantedPermissions.isEmpty()) {
325+
grantedPermissions(activity, allPermissions, grantedPermissions, false, callback);
326+
}
327+
return;
328+
}
329+
sharedPreferences.edit().putLong(permissionKey, System.currentTimeMillis()).apply();
330+
// 如果之前没有申请过权限,或者距离上次申请已经超过了 48 个小时,则进行申请权限
331+
IPermissionInterceptor.super.requestPermissions(activity, allPermissions, callback);
332+
}
333+
334+
@Override
335+
public void grantedPermissions(Activity activity, List<String> allPermissions, List<String> grantedPermissions, boolean all, OnPermissionCallback callback) {
336+
if (callback == null) {
337+
return;
338+
}
339+
callback.onGranted(grantedPermissions, all);
340+
}
341+
342+
@Override
343+
public void deniedPermissions(Activity activity, List<String> allPermissions, List<String> deniedPermissions, boolean never, OnPermissionCallback callback) {
344+
if (callback == null) {
345+
return;
346+
}
347+
callback.onDenied(deniedPermissions, never);
348+
}
349+
}
350+
```

README.md

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
* 博文地址:[一句代码搞定权限请求,从未如此简单](https://www.jianshu.com/p/c69ff8a445ed)
88

9-
* 可以扫码下载 Demo 进行演示或者测试,如果扫码下载不了的,[点击此处可直接下载](https://github.com/getActivity/XXPermissions/releases/download/15.0/XXPermissions.apk)
9+
* 可以扫码下载 Demo 进行演示或者测试,如果扫码下载不了的,[点击此处可直接下载](https://github.com/getActivity/XXPermissions/releases/download/16.0/XXPermissions.apk)
1010

1111
![](picture/demo_code.png)
1212

@@ -57,7 +57,7 @@ android {
5757
5858
dependencies {
5959
// 权限请求框架:https://github.com/getActivity/XXPermissions
60-
implementation 'com.github.getActivity:XXPermissions:15.0'
60+
implementation 'com.github.getActivity:XXPermissions:16.0'
6161
}
6262
```
6363

@@ -113,11 +113,11 @@ XXPermissions.with(this)
113113

114114
@Override
115115
public void onGranted(List<String> permissions, boolean all) {
116-
if (all) {
117-
toast("获取录音和日历权限成功");
118-
} else {
116+
if (!all) {
119117
toast("获取部分权限成功,但部分权限未正常授予");
118+
return;
120119
}
120+
toast("获取录音和日历权限成功");
121121
}
122122

123123
@Override
@@ -149,10 +149,10 @@ XXPermissions.with(this)
149149

150150
override fun onGranted(permissions: MutableList<String>, all: Boolean) {
151151
if (all) {
152-
toast("获取录音和日历权限成功")
153-
} else {
154152
toast("获取部分权限成功,但部分权限未正常授予")
153+
return
155154
}
155+
toast("获取录音和日历权限成功")
156156
}
157157

158158
override fun onDenied(permissions: MutableList<String>, never: Boolean) {
@@ -211,9 +211,10 @@ XXPermissions.setInterceptor(new IPermissionInterceptor() {});
211211

212212
| 适配细节 | [XXPermissions](https://github.com/getActivity/XXPermissions) | [AndPermission](https://github.com/yanzhenjie/AndPermission) | [PermissionX](https://github.com/guolindev/PermissionX) | [AndroidUtilCode](https://github.com/Blankj/AndroidUtilCode) | [PermissionsDispatcher](https://github.com/permissions-dispatcher/PermissionsDispatcher) | [RxPermissions](https://github.com/tbruyelle/RxPermissions) | [EasyPermissions](https://github.com/googlesamples/easypermissions) |
213213
| :--------: | :------------: | :------------: | :------------: | :------------: | :------------: | :------------: | :------------: |
214-
| 对应版本 | 15.0 | 2.0.3 | 1.6.4 | 1.31.0 | 4.9.2 | 0.12 | 3.0.0 |
214+
| 对应版本 | 16.0 | 2.0.3 | 1.6.4 | 1.31.0 | 4.9.2 | 0.12 | 3.0.0 |
215215
| issues 数 | [![](https://img.shields.io/github/issues/getActivity/XXPermissions.svg)](https://github.com/getActivity/XXPermissions/issues) | [![](https://img.shields.io/github/issues/yanzhenjie/AndPermission.svg)](https://github.com/yanzhenjie/AndPermission/issues) | [![](https://img.shields.io/github/issues/guolindev/PermissionX.svg)](https://github.com/guolindev/PermissionX/issues) | [![](https://img.shields.io/github/issues/Blankj/AndroidUtilCode.svg)](https://github.com/Blankj/AndroidUtilCode/issues) | [![](https://img.shields.io/github/issues/permissions-dispatcher/PermissionsDispatcher.svg)](https://github.com/permissions-dispatcher/PermissionsDispatcher/issues) | [![](https://img.shields.io/github/issues/tbruyelle/RxPermissions.svg)](https://github.com/tbruyelle/RxPermissions/issues) | [![](https://img.shields.io/github/issues/googlesamples/easypermissions.svg)](https://github.com/googlesamples/easypermissions/issues) |
216-
| 框架体积 | 45 KB | 127 KB | 90 KB | 500 KB | 99 KB | 28 KB | 48 KB |
216+
| 框架体积 | 51 KB | 127 KB | 90 KB | 500 KB | 99 KB | 28 KB | 48 KB |
217+
| 框架维护状态 |**维护中**| 停止维护 |**维护中**| 停止维护 |**维护中**| 停止维护 | 停止维护 |
217218
| 闹钟提醒权限 ||||||||
218219
| 所有文件管理权限 ||||||||
219220
| 安装包权限 ||||||||
@@ -225,6 +226,7 @@ XXPermissions.setInterceptor(new IPermissionInterceptor() {});
225226
| 忽略电池优化权限 ||||||||
226227
| 查看应用使用情况权限 ||||||||
227228
| VPN 权限 ||||||||
229+
| Android 13 危险权限 ||||||||
228230
| Android 12 危险权限 ||||||||
229231
| Android 11 危险权限 ||||||||
230232
| Android 10 危险权限 ||||||||
@@ -260,15 +262,15 @@ XXPermissions.setInterceptor(new IPermissionInterceptor() {});
260262

261263
* 最近有人跟我提了一个内存泄漏的问题 [XXPermissions/issues/133](https://github.com/getActivity/XXPermissions/issues/133) ,我经过实践后确认这个问题真实存在,但是通过查看代码堆栈,发现这个问题是系统的代码引起的,引发这个问题需要以下几个条件:
262264

263-
1. 在 Android 12 及以上的设备上使用
265+
1. 在 Android 12 的设备上使用
264266

265267
2. 调用了 `Activity.shouldShowRequestPermissionRationale`
266268

267269
3. 在这之后又主动在代码调用了 activity.finish 方法
268270

269271
* 排查的过程:经过对代码的追踪,发现代码调用栈是这样的
270272

271-
* Activity.shouldShowRequestPermissionRationale
273+
* Activity.shouldShowRequestPermissionRationale
272274

273275
* PackageManager.shouldShowRequestPermissionRationale(实现对象为 ApplicationPackageManager)
274276

@@ -284,7 +286,9 @@ XXPermissions.setInterceptor(new IPermissionInterceptor() {});
284286

285287
* 针对这个问题处理也很简单粗暴,就是将在外层传入的 **Context** 参数从 **Activity** 对象给替换成 **Application** 对象即可,有人可能会说了,Activity 里面才有 `shouldShowRequestPermissionRationale` 方法,而 Application 里面没有这个方法怎么办?看了一下这个方法的实现,其实那个方法最终会调用 `PackageManager.shouldShowRequestPermissionRationale` 方法(**隐藏 API,但是并不在黑名单中**)里面去,所以只要能获取到 **PackageManager** 对象即可,最后再使用反射去执行这个方法,这样就能避免出现内存泄漏。
286288

287-
* 幸好 Google 没有将 PackageManager.shouldShowRequestPermissionRationale 列入到反射黑名单中,否则这次想给 Google 擦屁股都没有办法了,要不然只能用修改系统源码实现的方式,但这种方式只能等谷歌在后续的 Android 版本上面修复了。
289+
* 幸好 Google 没有将 PackageManager.shouldShowRequestPermissionRationale 列入到反射黑名单中,否则这次想给 Google 擦屁股都没有办法了,要不然只能用修改系统源码实现的方式,但这种方式只能等谷歌在后续的 Android 版本上面修复了,不过庆幸的是,在 Android 12 L 的版本之后,这个问题被修复了,[具体的提交记录可以点击此处查看](https://cs.android.com/android/_/android/platform/frameworks/base/+/0d47a03bfa8f4ca54b883ff3c664cd4ea4a624d9:core/java/android/permission/PermissionUsageHelper.java;dlc=cec069482f80019c12f3c06c817d33fc5ad6151f),但是对于 Android 12 而言,这仍是一个历史遗留问题。
290+
291+
* 值得注意的是:XXPermissions 是目前同类框架第一款也是唯一一款修复这个问题的框架,另外针对这个问题,我还给谷歌的 [AndroidX](https://github.com/androidx/androidx/pull/435) 项目提供了解决方案。
288292

289293
#### 错误检测机制介绍
290294

@@ -320,7 +324,7 @@ XXPermissions.setInterceptor(new IPermissionInterceptor() {});
320324

321325
#### 框架亮点
322326

323-
* 首款适配 Android 11 的权限请求框架
327+
* 首款适配 Android 13 的权限请求框架
324328

325329
* 首款也是唯一一款适配所有 Android 版本的权限请求框架
326330

app/build.gradle

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
apply plugin: 'com.android.application'
22

33
android {
4-
compileSdkVersion 32
4+
compileSdkVersion 33
55

66
defaultConfig {
77
applicationId "com.hjq.permissions.demo"
88
minSdkVersion 14
9-
targetSdkVersion 32
10-
versionCode 1500
11-
versionName "15.0"
9+
targetSdkVersion 33
10+
versionCode 1600
11+
versionName "16.0"
1212
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
1313
}
1414

@@ -64,5 +64,5 @@ dependencies {
6464
implementation 'com.github.getActivity:ToastUtils:10.5'
6565

6666
// 内存泄漏检测:https://github.com/square/leakcanary
67-
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1'
67+
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
6868
}

app/src/main/AndroidManifest.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@
5151

5252
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
5353

54+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
55+
56+
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" android:usesPermissionFlags="neverForLocation" />
57+
58+
<uses-permission android:name="android.permission.BODY_SENSORS_BACKGROUND" />
59+
60+
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
61+
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
62+
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
63+
5464
<application
5565
android:name=".AppApplication"
5666
android:icon="@mipmap/ic_launcher"

0 commit comments

Comments
 (0)