├── .gitignore
├── HPermissions.apk
├── HelpDoc.md
├── LICENSE
├── README.md
├── app
├── HPermissions.jks
├── build.gradle
├── gradle.properties
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── ph
│ │ └── permissions
│ │ ├── MainActivity.java
│ │ ├── NotificationMonitorService.java
│ │ └── PermissionInterceptor.java
│ └── res
│ ├── layout
│ └── activity_main.xml
│ ├── mipmap-xhdpi
│ └── ic_launcher.png
│ ├── mipmap-xxhdpi
│ └── ic_launcher.png
│ ├── mipmap-xxxhdpi
│ └── ic_launcher.png
│ ├── values-v21
│ └── styles.xml
│ └── values
│ ├── colors.xml
│ ├── strings.xml
│ └── styles.xml
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── library
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ └── com
│ └── ph
│ └── permissions
│ ├── AndroidVersion.java
│ ├── HPermissions.java
│ ├── IPermissionInterceptor.java
│ ├── OnPermissionCallback.java
│ ├── OnPermissionPageCallback.java
│ ├── Permission.java
│ ├── PermissionApi.java
│ ├── PermissionChecker.java
│ ├── PermissionFragment.java
│ ├── PermissionPageFragment.java
│ ├── PermissionPageIntent.java
│ └── PermissionUtils.java
├── logo.png
├── picture
├── 1.jpg
├── 10.jpg
├── 11.jpg
├── 2.jpg
├── 3.jpg
├── 4.jpg
├── 5.jpg
├── 6.jpg
├── 7.jpg
├── 8.jpg
├── 9.jpg
├── location_1.jpg
├── location_2.jpg
├── miui_1.jpg
└── miui_2.jpg
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | .idea
3 | .cxx
4 | .externalNativeBuild
5 | build
6 | captures
7 |
8 | ._*
9 | *.iml
10 | .DS_Store
11 | local.properties
--------------------------------------------------------------------------------
/HPermissions.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dahui888/HPermissions/dc84015c060bb41b138534ae8614b7f6b74dda7f/HPermissions.apk
--------------------------------------------------------------------------------
/HelpDoc.md:
--------------------------------------------------------------------------------
1 | #### 目录
2 |
3 | * [Android 11 定位权限适配](#android-11-定位权限适配)
4 |
5 | * [Android 11 存储权限适配](#android-11-存储权限适配)
6 |
7 | * [什么情况下需要适配分区存储特性](#什么情况下需要适配分区存储特性)
8 |
9 | * [为什么授予了存储权限但是权限设置页还是显示未授权](#为什么授予了存储权限但是权限设置页还是显示未授权)
10 |
11 | * [我想在申请前和申请后统一弹对话框该怎么处理](#我想在申请前和申请后统一弹对话框该怎么处理)
12 |
13 | * [如何在回调中判断哪些权限被永久拒绝了](#如何在回调中判断哪些权限被永久拒绝了)
14 |
15 | * [为什么不兼容 Android 6.0 以下的危险权限申请](#为什么不兼容-android-60-以下的危险权限申请)
16 |
17 | * [新版框架为什么移除了自动申请清单权限的功能](#新版框架为什么移除了自动申请清单权限的功能)
18 |
19 | * [新版框架为什么移除了不断申请权限的功能](#新版框架为什么移除了不断申请权限的功能)
20 |
21 | * [新版框架为什么移除了国产手机权限设置页功能](#新版框架为什么移除了国产手机权限设置页功能)
22 |
23 | * [为什么不用 ActivityResultContract 来申请权限](为什么不用-activityresultcontract-来申请权限)
24 |
25 | #### Android 11 定位权限适配
26 |
27 | * 在 Android 10 上面,定位权限被划分为前台权限(精确和模糊)和后台权限,而到了 Android 11 上面,需要分别申请这两种权限,如果同时申请这两种权限会**惨遭系统无情拒绝**,连权限申请对话框都不会弹,立马被系统拒绝,直接导致定位权限申请失败。
28 |
29 | * 如果你使用的是 HPermissions 最新版本,那么**恭喜你**,直接将前台定位权限和后台定位权限全部传给框架即可,框架已经自动帮你把这两种权限分开申请了,整个适配过程**零成本**。
30 |
31 | * 但是需要注意的是:申请过程分为两个步骤,第一步是申请前台定位权限,第二步是申请后台定位权限,用户必须要先同意前台定位权限才能进入后台定位权限的申请。同意前台定位权限的方式有两种:勾选 `仅在使用该应用时允许` 或 `仅限这一次`,而到了后台定位权限申请中,用户必须要勾选 `始终允许`,只有这样后台定位权限才能申请通过。
32 |
33 | * 还有如果你的应用只需要在前台使用定位功能, 而不需要在后台中使用定位功能,那么请不要连带申请 `Permission.ACCESS_BACKGROUND_LOCATION` 权限。
34 |
35 | 
36 |
37 | 
38 |
39 | #### Android 11 存储权限适配
40 |
41 | * 如果你的项目需要适配 Android 11 存储权限,那么需要先将 targetSdkVersion 进行升级
42 |
43 | ```groovy
44 | android
45 | defaultConfig {
46 | targetSdkVersion 30
47 | }
48 | }
49 | ```
50 |
51 | * 再添加 Android 11 存储权限注册到清单文件中
52 |
53 | ```xml
54 |
55 | ```
56 |
57 | * 需要注意的是,旧版的存储权限也需要在清单文件中注册,因为在低于 Android 11 的环境下申请存储权限,框架会自动切换到旧版的申请方式
58 |
59 | ```xml
60 |
61 |
62 | ```
63 |
64 | * 还需要在清单文件中加上这个属性,否则在 Android 10 的设备上将无法正常读写外部存储上的文件
65 |
66 | ```xml
67 |
69 | ```
70 |
71 | * 最后直接调用下面这句代码
72 |
73 | ```java
74 | HPermissions.with(MainActivity.this)
75 | // 适配 Android 11 分区存储这样写
76 | //.permission(Permission.Group.STORAGE)
77 | // 不适配 Android 11 分区存储这样写
78 | .permission(Permission.MANAGE_EXTERNAL_STORAGE)
79 | .request(new OnPermissionCallback() {
80 |
81 | @Override
82 | public void onGranted(List permissions, boolean all) {
83 | if (all) {
84 | toast("获取存储权限成功");
85 | }
86 | }
87 | });
88 | ```
89 |
90 | 
91 |
92 | #### 什么情况下需要适配分区存储特性
93 |
94 | * 如果你的应用需要上架 GooglePlay,那么需要详细查看:[谷歌应用商店政策(需要翻墙)](https://support.google.com/googleplay/android-developer/answer/9956427)
95 |
96 | * 分区存储的由来:谷歌之前收到了很多用户投诉,说很多应用都在 SD 卡下创建目录和文件,导致用户管理手机文件非常麻烦(强迫症的外国网友真多,哈哈),所以在 Android 10 版本更新中,谷歌要求所有开发者将媒体文件存放在自己内部目录或者 SD 卡内部目录中,不过谷歌在一版本上采取了宽松政策,在清单文件中加入 `android:requestLegacyExternalStorage="true"` 即可跳过这一特性的适配,不过在 Android 11 上面,你有两种选择:
97 |
98 | 1. 适配分区存储:这个是谷歌推荐的一种方式,但是会增加工作量,因为分区存储适配起来十分麻烦,我个人感觉是这样的。不过对于一些特定应用,例如文件管理器,文件备份工具,防病毒应用等这类应用它们就一定需要用到外部存储,这个时候就需要用第二种方式来实现了。
99 |
100 | 2. 申请外部存储权限:这个是谷歌不推荐的一种方式,只需要 `MANAGE_EXTERNAL_STORAGE` 权限即可,适配起来基本无压力,但是会存在一个问题,就是上架谷歌应用市场的时候,要经过 Google Play 审核和批准。
101 |
102 | * 这两种总结下来,我觉得各有好坏,不过我可以跟大家谈谈我的看法
103 |
104 | 1. 如果你的应用需要上架谷歌应用市场,需要尽快适配分区存储,因为谷歌这次来真的了
105 |
106 | 2. 如果你的应用只上架国内的应用市场,并且后续也没有上架谷歌应用市场的需要,那么你也可以直接申请 `MANAGE_EXTERNAL_STORAGE` 权限来读写外部存储
107 |
108 | #### 为什么授予了存储权限但是权限设置页还是显示未授权
109 |
110 | * 首先我需要先纠正大家一个错误的想法,`READ_EXTERNAL_STORAGE`、`WRITE_EXTERNAL_STORAGE` 这两个权限和 `MANAGE_EXTERNAL_STORAGE` 权限是两码事,虽然都叫存储权限,但是属于两种完全不同的权限,你如果申请的是 `MANAGE_EXTERNAL_STORAGE` 权限,并且授予了权限,但是在权限设置页并没有看到已授予,请注意这种情况是正常的,因为你在权限设置页看到的是存储授予状态是 `READ_EXTERNAL_STORAGE`、`WRITE_EXTERNAL_STORAGE` 权限状态的,而不是 `MANAGE_EXTERNAL_STORAGE` 权限状态的,但是这个时候已经获取到存储权限了,你大可不必管权限设置页显示的权限状态,直接读写文件即可,不会有权限问题的。
111 |
112 | * 还有一个问题,为什么只在 Android 11 以上的设备出现?首先 `MANAGE_EXTERNAL_STORAGE` 权限是 Android 11 才有权限,Android 10 及之前的版本是没有这个权限的,你如果在低版本设备上申请了 `MANAGE_EXTERNAL_STORAGE` 权限,那么框架会帮你做向下兼容,会自动帮你替换成 `READ_EXTERNAL_STORAGE`、`WRITE_EXTERNAL_STORAGE` 权限去申请,这个时候你看到权限设置页的存储权限状态肯定是正常的,这就是为什么你只在 Android 11 以上的设备才会看到这个问题。
113 |
114 | #### 我想在申请前和申请后统一弹对话框该怎么处理
115 |
116 | * 框架内部有提供一个拦截器接口,通过实现框架中提供的 [IPermissionInterceptor](/library/src/main/java/com/hjq/permissions/IPermissionInterceptor.java) 接口即可,具体实现可参考 Demo 中提供的 [PermissionInterceptor](app/src/main/java/com/hjq/permissions/demo/PermissionInterceptor.java) 类,建议下载源码后进行阅读,再将代码引入到项目中
117 |
118 | * 使用拦截的方式也很简单,具体有两种设置方式,一种针对局部设置,另外一种是全局设置
119 |
120 | ```java
121 | HPermissions.with(this)
122 | .permission(Permission.XXX)
123 | // 设置权限请求拦截器(局部设置)
124 | .interceptor(new PermissionInterceptor())
125 | .request(new OnPermissionCallback() {
126 |
127 | @Override
128 | public void onGranted(List permissions, boolean all) {
129 | ......
130 | }
131 |
132 | @Override
133 | public void onDenied(List permissions, boolean never) {
134 | ......
135 | }
136 | });
137 | ```
138 |
139 | ```java
140 | public class XxxApplication extends Application {
141 |
142 | @Override
143 | public void onCreate() {
144 | super.onCreate();
145 |
146 | // 设置权限请求拦截器(全局设置)
147 | HPermissions.setInterceptor(new PermissionInterceptor());
148 | }
149 | }
150 | ```
151 |
152 | #### 如何在回调中判断哪些权限被永久拒绝了
153 |
154 | * 需求场景:假设同时申请日历权限和录音权限,结果都被用户拒绝了,但是这两组权限中有一组权限被永久拒绝了,如何判断某一组权限有没有被永久拒绝?这里给出代码示例:
155 |
156 | ```java
157 | HPermissions.with(this)
158 | .permission(Permission.RECORD_AUDIO)
159 | .permission(Permission.Group.CALENDAR)
160 | .request(new OnPermissionCallback() {
161 |
162 | @Override
163 | public void onGranted(List permissions, boolean all) {
164 | if (all) {
165 | toast("获取录音和日历权限成功");
166 | }
167 | }
168 |
169 | @Override
170 | public void onDenied(List permissions, boolean never) {
171 | if (never && permissions.contains(Permission.RECORD_AUDIO) &&
172 | HPermissions.isPermanentDenied(MainActivity.this, Permission.RECORD_AUDIO)) {
173 | toast("录音权限被永久拒绝了");
174 | }
175 | }
176 | });
177 | ```
178 |
179 | #### 为什么不兼容 Android 6.0 以下的危险权限申请
180 |
181 | * 因为 Android 6.0 以下的危险权限管理是手机厂商做的,那个时候谷歌还没有统一危险权限管理的方案,所以就算我们的应用没有适配也不会有任何问题,因为手机厂商对这块有自己的处理,但是有一点是肯定的,就算用户拒绝了授权,也不会导致应用崩溃,只会返回空白的通行证。
182 |
183 | * 如果 HPermissions 做这块的适配也可以做到,通过反射系统服务 AppOpsManager 类中的字段即可,但是并不能保证权限判断的准确性,可能会存在一定的误差,其次是适配的成本太高,因为国内手机厂商太多,对这块的改动参差不齐。
184 |
185 | * 考虑到 Android 6.0 以下的设备占比很低,后续也会越来越少,会逐步退出历史的舞台,所以我的决定是不对这块做适配。
186 |
187 | #### 新版框架为什么移除了自动申请清单权限的功能
188 |
189 | > [【issue】建议恢复跳转权限设置页和获取AndroidManifest的所有权限两个实用功能](https://github.com/dahui888/HPermissions/issues/54)
190 |
191 | * 获取清单权限并申请的功能,这个虽然非常方便,但是存在一些隐患,因为 apk 中的清单文件最终是由多个 module 的清单文件合并而成,会变得不可控,这样会使我们无法预估申请的权限,并且还会掺杂一些不需要的权限,所以经过慎重考虑移除该功能。
192 |
193 | #### 新版框架为什么移除了不断申请权限的功能
194 |
195 | > [【issue】关于拒绝权限后一直请求获取的优化问题](https://github.com/dahui888/HPermissions/issues/39)
196 |
197 | * 假设用户拒绝了权限,如果框架再次申请,那么用户会授予的可能性也是比较小,同时某些应用商店已经禁用了这种行为,经过慎重考虑,对这个功能相关的 API 进行移除。
198 |
199 | * 如果你还想用这种方式来申请权限,其实并不是没有办法,可以参考以下方式来实现
200 |
201 | ```java
202 | public class PermissionActivity extends AppCompatActivity implements OnPermissionCallback {
203 |
204 | @Override
205 | public void onClick(View view) {
206 | requestCameraPermission();
207 | }
208 |
209 | private void requestCameraPermission() {
210 | HPermissions.with(this)
211 | .permission(Permission.CAMERA)
212 | .request(this);
213 | }
214 |
215 | @Override
216 | public void onGranted(List permissions, boolean all) {
217 | if (all) {
218 | toast("获取拍照权限成功");
219 | }
220 | }
221 |
222 | @Override
223 | public void onDenied(List permissions, boolean never) {
224 | if (never) {
225 | toast("被永久拒绝授权,请手动授予拍照权限");
226 | // 如果是被永久拒绝就跳转到应用权限系统设置页面
227 | HPermissions.startPermissionActivity(MainActivity.this, permissions);
228 | } else {
229 | requestCameraPermission();
230 | }
231 | }
232 |
233 | @Override
234 | protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
235 | super.onActivityResult(requestCode, resultCode, data);
236 | if (requestCode == HPermissions.REQUEST_CODE) {
237 | toast("检测到你刚刚从权限设置界面返回回来");
238 | }
239 | }
240 | }
241 | ```
242 |
243 | #### 新版框架为什么移除了国产手机权限设置页功能
244 |
245 | > [【issue】权限拒绝并不再提示的问题](https://github.com/dahui888/HPermissions/issues/99)
246 |
247 | > [【issue】小米手机权限拒绝后,库中判断的小米权限设置界面开启无效](https://github.com/dahui888/HPermissions/issues/38)
248 |
249 | > [【issue】正常申请存储权限时,永久拒绝,然后再应用设置页开启权限询问,系统权限申请弹窗未显示](https://github.com/dahui888/HPermissions/issues/100)
250 |
251 | * HPermissions1.0 及之前是有存在这一功能的,但是我在后续的版本上面将这个功能移除了,原因是有很多人跟我反馈这个功能其实存在很大的缺陷,例如在一些华为新机型上面可能跳转的页面不是应用的权限设置页,而是所有应用的权限管理列表界面。
252 |
253 | * 其实不止华为有问题,小米同样有问题,有很多人跟我反馈过同一个问题,HPermissions 跳转到国产手机权限设置页,用户正常授予了权限之后返回仍然检测到权限仍然是拒绝的状态,这个问题反馈的次数很多,但是迟迟不能排查到原因,终于在最后一次得到答案了,[有人](https://github.com/dahui888/HPermissions/issues/38)帮我排查到是 miui 优化开关的问题(小米手机 ---> 开发者选项 ---> 启用 miui 优化),那么问题来了,这个开关有什么作用?是如何影响到 HPermissions 的?
254 |
255 | * 首先这个问题要从 HPermissions 跳转到国产手机设置页的原理讲起,从谷歌提供的原生 API 我们最多只能跳转到应用详情页,并不能直接跳转到权限设置页,而需要用户在应用详情页再次点击才能进入权限设置页。如果从用户体验的角度上看待这个问题,肯定是直接跳转到权限设置页是最好的,但是这种方式是不受谷歌支持的,当然也有方法实现,网上都有一个通用的答案,就是直接捕获某个品牌手机的权限设置页 `Activity` 包名然后进行跳转。这种想法的起点是好的,但是存在许多问题,并不能保证每个品牌的所有机型都能适配到位,手机产商更改这个 `Activity` 的包名的次数和频率比较高,在最近发布的一些新的华为机型上面几乎已经全部失效,也就是 `startActivity` 的时候会报 `ActivityNotFoundException` 或 `SecurityException` 异常,当然这些异常是可以被捕捉到的,但是仅仅只能捕获到崩溃,一些非崩溃的行为我们并不能从中得知和处理,例如我刚刚讲过的华为和小米的问题,这些问题并不能导致崩溃,但是会导致功能出现异常。
256 |
257 | * 而 miui 优化开关是小米工程师预留的切换 miui 和原生的功能开关,例如在这个开关开启的时候,在应用详情页点击权限管理会跳转到小米的权限设置页,如果这个开关是关闭状态(默认是开启状态),在应用详情页点击权限管理会跳转到谷歌原生的权限设置页,具体效果如图:
258 |
259 | 
260 |
261 | 
262 |
263 | * 最大的问题在于:这两个界面是不同的 Activity,一个是小米定制的权限设置页,第二个是谷歌原生的权限设置页,当 miui 优化开启的时候,在小米定制的权限设置页授予权限才能有效果,当这个 miui 优化关闭的时候,在谷歌原生的权限设置页授予权限才能有效果。而跳转到国产手机页永远只会跳转到小米定制的那个权限设置页,所以就会导致当 miui 优化关闭的时候,使用代码跳转到小米权限设置页授予了权限之后返回仍然显示失败的问题。
264 |
265 | * 有人可能会说,解决这个问题的方式很简单,判断 miui 优化开关,如果是开启状态就跳转到小米定制的权限设置页,如果是关闭状态就跳转到谷歌原生的权限设置页,这样不就可以了?其实这个解决方案我也有尝试过,我曾委托联系到在小米工作的 miui 工程师,也有人帮我反馈这个问题给小米那边,最后得到答复都是一致的。
266 |
267 | 
268 |
269 | 
270 |
271 | * 另外值得一提的是 [Android 11 对软件包可见性进行了限制](https://developer.android.google.cn/about/versions/11/privacy/package-visibility),所以这种跳包名的方式在未来将会完全不可行。
272 |
273 | * 最终决定:这个功能的出发点是好的,但是我们没办法做好它,经过慎重考虑,决定将这个功能进行移除。
274 |
275 | #### 为什么不用 ActivityResultContract 来申请权限
276 |
277 | > [【issue】是否有考虑 onActivityResult 回调的权限申请切换成 ActivityResultContract](https://github.com/dahui888/HPermissions/issues/103)
278 |
279 | * ActivityResultContract 是 Activity `1.2.0-alpha02` 和 Fragment `1.3.0-alpha02` 中新追加的新 API,有一定的使用门槛,必须要求项目是基于 AndroidX,并且 AndroidX 的版本还要是 `1.3.0-alpha01` 以上才可以,如果替换成 `ActivityResultContract` 来实现,那么就会导致一部分开发者用不了 HPermissions,这是一个比较严重的问题,但实际上换成 ActivityResultContract 来实现本身没有带来任何的效益,例如我之前解决过的 Fragment 屏幕旋转及后台申请的问题,所以更换的意义又在哪里呢?有人可能会说官方已经将 onActivityResult 标记成过时,大家不必担心,之所以标记成过时只不过是谷歌为了推广新技术,但是可以明确说,官方是一定不会删掉这个 API 的,更准确来说是一定不敢,至于为什么?大家可以去看看 ActivityResultContract 是怎么实现的?它也是通过重写 Activity 的 `onActivityResult`、`onRequestPermissionsResult` 方法回调实现的,具体大家可以去看 `androidx.activity.ComponentActivity` 类中这两个方法的实现就会明白了,这里不再赘述。
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, June 2018
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright 2018 Huang JinQun
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 权限请求框架
2 |
3 | 
4 |
5 | * 项目地址:[Github](https://github.com/dahui888/HPermissions)
6 |
7 | * [点击此处可直接下载](https://github.com/dahui888/HPermissions/blob/master/HPermissions.apk)
8 |
9 | 
10 |
11 |   
12 |
13 |   
14 |
15 |   
16 |
17 |  
18 |
19 | #### 集成步骤
20 |
21 | * 如果你的项目 Gradle 配置是在 `7.0 以下`,需要在 `build.gradle` 文件中加入
22 |
23 | ```groovy
24 | allprojects {
25 | repositories {
26 | // JitPack 远程仓库:https://jitpack.io
27 | maven { url 'https://jitpack.io' }
28 | }
29 | }
30 | ```
31 |
32 | * 如果你的 Gradle 配置是 `7.0 及以上`,则需要在 `settings.gradle` 文件中加入
33 |
34 | ```groovy
35 | dependencyResolutionManagement {
36 | repositories {
37 | // JitPack 远程仓库:https://jitpack.io
38 | maven { url 'https://jitpack.io' }
39 | }
40 | }
41 | ```
42 |
43 | * 配置完远程仓库后,在项目 app 模块下的 `build.gradle` 文件中加入远程依赖
44 |
45 | ```groovy
46 | android {
47 | // 支持 JDK 1.8
48 | compileOptions {
49 | targetCompatibility JavaVersion.VERSION_1_8
50 | sourceCompatibility JavaVersion.VERSION_1_8
51 | }
52 | }
53 |
54 | dependencies {
55 | // 权限请求框架:https://github.com/dahui888/HPermissions
56 | implementation 'com.github.dahui888:HPermissions:1.2'
57 | }
58 | ```
59 |
60 | #### AndroidX
61 |
62 | * 如果项目是基于 **AndroidX** 包,请在项目 `gradle.properties` 文件中加入
63 |
64 | ```text
65 | # 表示将第三方库迁移到 AndroidX
66 | android.enableJetifier = true
67 | ```
68 |
69 | * 如果项目是基于 **Support** 包则不需要加入此配置
70 |
71 | #### 分区存储
72 |
73 | * 如果项目已经适配了 Android 10 分区存储特性,请在 `AndroidManifest.xml` 中加入
74 |
75 | ```xml
76 |
77 |
78 |
79 |
80 |
81 |
84 |
85 |
86 |
87 |
88 | ```
89 |
90 | * 如果当前项目没有适配这特性,那么这一步骤可以忽略
91 |
92 | * 需要注意的是:这个选项是框架用于判断当前项目是否适配了分区存储,需要注意的是,如果你的项目已经适配了分区存储特性,可以使用 `READ_EXTERNAL_STORAGE`、`WRITE_EXTERNAL_STORAGE` 来申请权限,如果你的项目还没有适配分区特性,就算申请了 `READ_EXTERNAL_STORAGE`、`WRITE_EXTERNAL_STORAGE` 权限也会导致无法正常读取外部存储上面的文件,如果你的项目没有适配分区存储,请使用 `MANAGE_EXTERNAL_STORAGE` 来申请权限,这样才能正常读取外部存储上面的文件,你如果想了解更多关于 Android 10 分区存储的特性,可以[点击此处查看和学习](https://www.jianshu.com/p/9a9d260e10b0)。
93 |
94 | #### 一句代码搞定权限请求,从未如此简单
95 |
96 | * Java 用法示例
97 |
98 | ```java
99 | HPermissions.with(this)
100 | // 申请单个权限
101 | .permission(Permission.RECORD_AUDIO)
102 | // 申请多个权限
103 | .permission(Permission.Group.CALENDAR)
104 | // 设置权限请求拦截器(局部设置)
105 | //.interceptor(new PermissionInterceptor())
106 | // 设置不触发错误检测机制(局部设置)
107 | //.unchecked()
108 | .request(new OnPermissionCallback() {
109 |
110 | @Override
111 | public void onGranted(List permissions, boolean all) {
112 | if (all) {
113 | toast("获取录音和日历权限成功");
114 | } else {
115 | toast("获取部分权限成功,但部分权限未正常授予");
116 | }
117 | }
118 |
119 | @Override
120 | public void onDenied(List permissions, boolean never) {
121 | if (never) {
122 | toast("被永久拒绝授权,请手动授予录音和日历权限");
123 | // 如果是被永久拒绝就跳转到应用权限系统设置页面
124 | HPermissions.startPermissionActivity(context, permissions);
125 | } else {
126 | toast("获取录音和日历权限失败");
127 | }
128 | }
129 | });
130 | ```
131 |
132 | * Kotlin 用法示例
133 |
134 | ```kotlin
135 | HPermissions.with(this)
136 | // 申请单个权限
137 | .permission(Permission.RECORD_AUDIO)
138 | // 申请多个权限
139 | .permission(Permission.Group.CALENDAR)
140 | // 设置权限请求拦截器(局部设置)
141 | //.interceptor(new PermissionInterceptor())
142 | // 设置不触发错误检测机制(局部设置)
143 | //.unchecked()
144 | .request(object : OnPermissionCallback {
145 |
146 | override fun onGranted(permissions: MutableList, all: Boolean) {
147 | if (all) {
148 | toast("获取录音和日历权限成功")
149 | } else {
150 | toast("获取部分权限成功,但部分权限未正常授予")
151 | }
152 | }
153 |
154 | override fun onDenied(permissions: MutableList, never: Boolean) {
155 | if (never) {
156 | toast("被永久拒绝授权,请手动授予录音和日历权限")
157 | // 如果是被永久拒绝就跳转到应用权限系统设置页面
158 | HPermissions.startPermissionActivity(context, permissions)
159 | } else {
160 | toast("获取录音和日历权限失败")
161 | }
162 | }
163 | })
164 | ```
165 |
166 | #### 从系统权限设置页返回判断
167 |
168 | ```java
169 | public class XxxActivity extends AppCompatActivity {
170 |
171 | @Override
172 | protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
173 | super.onActivityResult(requestCode, resultCode, data);
174 | if (requestCode == HPermissions.REQUEST_CODE) {
175 | if (HPermissions.isGranted(this, Permission.RECORD_AUDIO) &&
176 | HPermissions.isGranted(this, Permission.Group.CALENDAR)) {
177 | toast("用户已经在权限设置页授予了录音和日历权限");
178 | } else {
179 | toast("用户没有在权限设置页授予权限");
180 | }
181 | }
182 | }
183 | }
184 | ```
185 |
186 | #### 框架其他 API 介绍
187 |
188 | ```java
189 | // 判断一个或多个权限是否全部授予了
190 | HPermissions.isGranted(Context context, String... permissions);
191 |
192 | // 获取没有授予的权限
193 | HPermissions.getDenied(Context context, String... permissions);
194 |
195 | // 判断某个权限是否为特殊权限
196 | HPermissions.isSpecial(String permission);
197 |
198 | // 判断一个或多个权限是否被永久拒绝了
199 | HPermissions.isPermanentDenied(Activity activity, String... permissions);
200 |
201 | // 跳转到应用权限设置页
202 | HPermissions.startPermissionActivity(Context context, String... permissions);
203 | HPermissions.startPermissionActivity(Activity activity, String... permissions);
204 | HPermissions.startPermissionActivity(Activity activity, String... permission, OnPermissionPageCallback callback);
205 | HPermissions.startPermissionActivity(Fragment fragment, String... permissions);
206 | HPermissions.startPermissionActivity(Fragment fragment, String... permissions, OnPermissionPageCallback callback);
207 |
208 | // 设置不触发错误检测机制(全局设置)
209 | HPermissions.setCheckMode(false);
210 | // 设置权限申请拦截器(全局设置)
211 | HPermissions.setInterceptor(new IPermissionInterceptor() {});
212 | ```
213 |
214 | #### 关于权限监听回调参数说明
215 |
216 | * 我们都知道,如果用户全部授予只会调用 **onGranted** 方法,如果用户全部拒绝只会调用 **onDenied** 方法。
217 |
218 | * 但是还有一种情况,如果在请求多个权限的情况下,这些权限不是被全部授予或者全部拒绝了,而是部分授权部分拒绝这种情况,框架会如何处理回调呢?
219 |
220 | * 框架会先调用 **onDenied** 方法,再调用 **onGranted** 方法。其中我们可以通过 **onGranted** 方法中的 **all** 参数来判断权限是否全部授予了。
221 |
222 | * 如果想知道回调中的某个权限是否被授权或者拒绝,可以调用 **List** 类中的 **contains(Permission.XXX)** 方法来判断这个集合中是否包含了这个权限。
223 |
224 | ### [其他常见疑问请点击此处查看](HelpDoc.md)
225 |
226 | #### 同类权限请求框架之间的对比
227 |
228 | | 适配细节 | [HPermissions](https://github.com/dahui888/HPermissions) | [AndPermission](https://github.com/yanzhenjie/AndPermission) | [PermissionX](https://github.com/guolindev/PermissionX) | [AndroidUtilCode](https://github.com/Blankj/AndroidUtilCode) | [RxPermissions](https://github.com/tbruyelle/RxPermissions) | [PermissionsDispatcher](https://github.com/permissions-dispatcher/PermissionsDispatcher) | [EasyPermissions](https://github.com/googlesamples/easypermissions) |
229 | | :--------: | :------------: | :------------: | :------------: | :------------: | :------------: | :------------: | :------------: |
230 | | 对应版本 | 1.2 | 2.0.3 | 1.6.1 | 1.31.0 | 0.12 | 4.9.1 | 3.0.0 |
231 | | issues 数 | [](https://github.com/dahui888/HPermissions/issues) | [](https://github.com/yanzhenjie/AndPermission/issues) | [](https://github.com/guolindev/PermissionX/issues) | [](https://github.com/Blankj/AndroidUtilCode/issues) | [](https://github.com/tbruyelle/RxPermissions/issues) | [](https://github.com/permissions-dispatcher/PermissionsDispatcher/issues) | [](https://github.com/googlesamples/easypermissions/issues) |
232 | | 框架体积 | 37 KB | 127 KB | 78 KB | 500 KB | 28 KB | 91 KB | 48 KB |
233 | | 闹钟提醒权限 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
234 | | 所有文件管理权限 | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
235 | | 安装包权限 | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
236 | | 悬浮窗权限 | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ |
237 | | 系统设置权限 | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ |
238 | | 通知栏权限 | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
239 | | 通知栏监听权限 | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
240 | | 勿扰权限 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
241 | | 查看应用使用情况权限 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
242 | | Android 12 危险权限 | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
243 | | Android 11 危险权限 | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
244 | | Android 10 危险权限 | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ |
245 | | Android 9.0 危险权限 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ |
246 | | Android 8.0 危险权限 | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ |
247 | | 新权限自动兼容旧设备 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
248 | | 屏幕方向旋转场景适配 | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ |
249 | | 后台申请权限场景适配 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
250 | | 错误检测机制 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
251 |
252 | #### 新权限自动兼容旧设备介绍
253 |
254 | * 随着 Android 版本的不断更新,危险权限和特殊权限也在增加,那么这个时候会有一个版本兼容问题,高版本的安卓设备是支持申请低版本的权限,但是低版本的安卓设备是不支持申请高版本的权限,那么这个时候会出现一个兼容性的问题。
255 |
256 | * 经过核查,其他权限框架选择了一种最简单粗暴的方式,就是不去做兼容,而是交给外层的调用者做兼容,需要调用者在外层先判断安卓版本,在高版本上面传入新权限给框架,而在低版本上面传入旧权限给框架,这种方式看似简单粗暴,但是开发体验差,同时也暗藏了一个坑,外层的调用者他们知道这个新权限对应着的旧权限是哪个吗?我觉得不是每个人都知道,而一旦认知出现错误,必然会导致结果出现错误。
257 |
258 | * 我觉得最好的做法是交给框架来做,**HPermissions** 正是那么做的,外层调用者申请高版本权限的时候,那么在低版本设备上面,会自动添加低版本的权限进行申请,举个最简单的例子,Android 11 出现的 `MANAGE_EXTERNAL_STORAGE` 新权限,如果是在 Android 10 及以下的设备申请这个权限时,框架会自动添加 `READ_EXTERNAL_STORAGE` 和 `WRITE_EXTERNAL_STORAGE` 进行申请,在 Android 10 及以下的设备上面,我们可以直接把 `MANAGE_EXTERNAL_STORAGE` 当做 `READ_EXTERNAL_STORAGE` 和 `WRITE_EXTERNAL_STORAGE` 来用,因为 `MANAGE_EXTERNAL_STORAGE` 能干的事情,在 Android 10 及以下的设备上面,要用 `READ_EXTERNAL_STORAGE` 和 `WRITE_EXTERNAL_STORAGE` 才能做得了。
259 |
260 | * 所以大家在使用 **HPermissions** 的时候,直接拿新的权限去申请就可以了,完全不需要关心新旧权限的兼容问题,框架会自动帮你做处理的,与其他框架不同的是,我更想做的是让大家一句代码搞定权限请求,框架能做到的,统统交给框架做处理。
261 |
262 | #### 屏幕旋转场景适配介绍
263 |
264 | * 当系统权限申请对话框弹出后对 Activity 进行屏幕旋转,会导致权限申请回调失效,因为屏幕旋转会导致框架中的 Fragment 销毁重建,这样会导致里面的回调对象直接被回收,最终导致回调不正常。解决方案有几种,一是在清单文件中添加 `android:configChanges="orientation"` 属性,这样屏幕旋转时不会导致 Activity 和 Fragment 销毁重建,二是直接在清单文件中固定 Activity 显示的方向,但是以上两种方案都要使用框架的人处理,这样显然是不够灵活的,解铃还须系铃人,框架的问题应当由框架来解决,而 **RxPermissions** 的解决方式是给 PermissionFragment 对象设置 `fragment.setRetainInstance(true)`,这样就算屏幕旋转了,Activity 对象会销毁重建,而 Fragment 也不会跟着销毁重建,还是复用着之前那个对象,但是存在一个问题,如果 Activity 重写了 **onSaveInstanceState** 方法会直接导致这种方式失效,这样做显然只是治标不治本,而 **HPermissions** 的方式会更直接点,在 **PermissionFragment** 绑定到 Activity 上面时,把当前 Activity 的**屏幕方向固定住**,在权限申请结束后再把**屏幕方向还原回去**。
265 |
266 | * 在所有的权限请求框架中,只要使用了 Fragment 申请权限都会出现这个问题,而 AndPermission 其实是通过创建新的 Activity 来申请权限,所以不会出现这个问题,PermissionsDispatcher 则是采用了 APT 生成代码的形式来申请权限,所以也没有这个问题,而 PermissionX 则是直接借鉴了 HPermissions的解决方案
267 |
268 | #### 后台申请权限场景介绍
269 |
270 | * 当我们做耗时操作之后申请权限(例如在闪屏页获取隐私协议再申请权限),在网络请求的过程中将 Activity 返回桌面去(退到后台),然后会导致权限请求是在后台状态中进行,在这个时机上就可能会导致权限申请不正常,表现为不会显示授权对话框,处理不当的还会导致崩溃,例如 [RxPeremission/issues/249](https://github.com/tbruyelle/RxPermissions/issues/249)。原因在于框架中的 PermissionFragment 在 **commit / commitNow** 到 Activity 的时候会做一个检测,如果 Activity 的状态是不可见时则会抛出异常,而 **RxPeremission** 正是使用了 **commitNow** 才会导致崩溃 ,使用 **commitAllowingStateLoss / commitNowAllowingStateLoss** 则可以避开这个检测,虽然这样可以避免崩溃,但是会出现另外一个问题,系统提供的 **requestPermissions** API 在 Activity 不可见时调用也不会弹出授权对话框,**HPermissions** 的解决方式是将 **requestPermissions** 时机从 **create** 转移到了 **resume**,因为 Activity 和 Fragment 的生命周期方法是捆绑在一起的,如果 Activity 是不可见的,那么就算创建了 Fragment 也只会调用 **onCreate** 方法,而不会去调用它的 **onResume** 方法,最后当 Activity 从后台返回到前台时,不仅会触发 **Activity.onResume** 方法,同时也会触发 **PermissionFragment** 的 **onResume** 方法,在这个方法申请权限就可以保证最终 **requestPermissions** 申请的时机是在 Activity **处于可见状态的情况**下。
271 |
272 | #### 错误检测机制介绍
273 |
274 | * 在框架的日常维护中,有很多人跟我反馈过框架有 Bug,但是经过排查和定位发现,这其中有 95% 的问题来自于调用者一些不规范操作导致的,这不仅对我造成很大的困扰,同时也极大浪费了很多小伙伴的时间和精力,于是我在框架中加入了很多审查元素,在 **debug 模式**、**debug 模式**、**debug 模式** 下,一旦有某些操作不符合规范,那么框架会直接抛出异常给调用者,并在异常信息中正确指引调用者纠正错误,例如:
275 |
276 | * 传入的 Context 实例不是 Activity 对象,框架会抛出异常,又或者传入的 Activity 的状态异常(已经 **Finishing** 或者 **Destroyed**),这种情况一般是在异步申请权限导致的,框架也会抛出异常,请在合适的时机申请权限,如果申请的时机无法预估,请在外层做好 Activity 状态判断再进行权限申请。
277 |
278 | * 如果调用者没有传入任何权限就申请权限的话,框架会抛出异常,又或者如果调用者传入的权限不是危险权限或者特殊权限,框架也会抛出异常,因为有的人会把普通权限当做危险权限传给框架,系统会直接拒绝。
279 |
280 | * 如果当前项目在没有适配分区存储的情况下,申请 `READ_EXTERNAL_STORAGE` 和 `WRITE_EXTERNAL_STORAGE` 权限
281 |
282 | * 当项目的 `targetSdkVersion >= 29` 时,需要在清单文件中注册 `android:requestLegacyExternalStorage="true"` 属性,否则框架会抛出异常,如果不加会导致一个问题,明明已经获取到存储权限,但是无法在 Android 10 的设备上面正常读写外部存储上的文件。
283 |
284 | * 当项目的 `targetSdkVersion >= 30` 时,则不能申请 `READ_EXTERNAL_STORAGE` 和 `WRITE_EXTERNAL_STORAGE` 权限,而是应该申请 `MANAGE_EXTERNAL_STORAGE` 权限
285 |
286 | * 如果当前项目已经适配了分区存储,那么只需要在清单文件中注册一个 meta-data 属性即可: ``
287 |
288 | * 如果申请的权限中包含后台定位权限, 那么这里面则不能包含和定位无关的权限,否则框架会抛出异常,因为 `ACCESS_BACKGROUND_LOCATION` 和其他非定位权限定位掺杂在一起申请,在 Android 11 上会出现不申请直接被拒绝的情况。
289 |
290 | * 如果申请的权限和项目中的 **targetSdkVersion** 对不上,框架会抛出异常,是因为 **targetSdkVersion** 代表着项目适配到哪个 Android 版本,系统会自动做向下兼容,假设申请的权限是 Android 11 才出现的,但是 **targetSdkVersion** 还停留在 29,那么在某些机型上的申请,会出现授权异常的情况,也就是用户明明授权了,但是系统返回的始终是 false。
291 |
292 | * 如果动态申请的权限没有在 `AndroidManifest.xml` 中进行注册,框架会抛出异常,因为如果不这么做,是可以进行申请权限,但是不会出现授权弹窗,直接被系统拒绝,并且系统不会给出任何弹窗和提示,并且这个问题在每个机型上面都是**必现的**。
293 |
294 | * 如果动态申请的权限有在 `AndroidManifest.xml` 中进行注册,但是设定了不恰当的 `android:maxSdkVersion` 属性值,框架会抛出异常,举个例子:``,这样的设定会导致在 Android 11 (`Build.VERSION.SDK_INT >= 30`)及以上的设备申请权限,系统会认为这个权限没有在清单文件中注册,直接拒绝本次的权限申请,并且也是不会给出任何弹窗和提示,这个问题也是必现的。
295 |
296 | * 如果你同时申请了 `MANAGE_EXTERNAL_STORAGE`、`READ_EXTERNAL_STORAGE`、`WRITE_EXTERNAL_STORAGE` 这三个权限,框架会抛出异常,告诉你不要同时申请这三个权限,这是因为在 Android 11 及以上设备上面,申请了 `MANAGE_EXTERNAL_STORAGE` 权限,则没有申请 `READ_EXTERNAL_STORAGE`、`WRITE_EXTERNAL_STORAGE` 权限的必要,这是因为申请了 `MANAGE_EXTERNAL_STORAGE` 权限,就等于拥有了比 `READ_EXTERNAL_STORAGE`、`WRITE_EXTERNAL_STORAGE` 更加强大的能力,如果硬要那么做反而适得其反,假设框架允许的情况下,会同时出现两种授权方式,一种是弹窗授权,另一种是跳页面授权,用户要进行两次授权,但是实际上面有了 `MANAGE_EXTERNAL_STORAGE` 权限就满足使用了,这个时候大家可能心中有一个疑问了,你不申请 `READ_EXTERNAL_STORAGE`、`WRITE_EXTERNAL_STORAGE` 权限,Android 11 以下又没有 `MANAGE_EXTERNAL_STORAGE` 这个权限,那不是会有问题?关于这个问题大家可以放心,框架会做判断,如果你申请了 `MANAGE_EXTERNAL_STORAGE` 权限,在 Android 11 以下框架会自动添加 `READ_EXTERNAL_STORAGE`、`WRITE_EXTERNAL_STORAGE` 来申请,所以在低版本下也不会因为没有权限导致的无法使用。
297 |
298 | * 如果你不需要上面这些检测,可通过调用 `unchecked` 方法来关闭,但是需要注意的是,我并不建议你去关闭这个检测,因为在 **release 模式** 时它是关闭状态,不需要你手动关闭,而它只在 **debug 模式** 下才会触发这些检测。
299 |
300 | * 出现这些问题的原因是,我们对这些机制不太熟悉,而如果框架不加以限制,那么引发各种奇奇怪怪的问题出现,作为框架的作者,表示不仅你们很痛苦,作为框架作者表示也很受伤。因为这些问题不是框架导致的,而是调用者的某些操作不规范导致的。我觉得这个问题最好的解决方式是,由框架做统一的检查,因为我是框架的作者,对权限申请这块知识点有**较强的专业能力和足够的经验**,知道什么该做,什么不该做,这样就可以对这些骚操作进行一一拦截。
301 |
302 | * 当权限申请出现问题时,它会提醒你,告诉你哪里错了该怎么去纠正。帮助大家少走弯路。
303 |
304 | #### 框架亮点
305 |
306 | * 适配 Android 12 的权限请求框架
307 |
308 | * 适配所有 Android 版本的权限请求框架
309 |
310 | * 简洁易用:采用链式调用的方式,使用只需一句代码
311 |
312 | * 体积感人:功能在同类框架中是最全的,但是框架体积是垫底的
313 |
314 | * 适配极端情况:无论在多么极端恶劣的环境下申请权限,框架依然坚挺
315 |
316 | * 向下兼容属性:新权限在旧系统可以正常申请,框架会做自动适配,无需调用者适配
317 |
318 | * 自动检测错误:如果出现错误框架会主动抛出异常给调用者(仅在 Debug 下判断,把 Bug 扼杀在摇篮中)
319 |
320 | ## License
321 |
322 | ```text
323 | Copyright 2018 iXiao Hui
324 |
325 | Licensed under the Apache License, Version 2.0 (the "License");
326 | you may not use this file except in compliance with the License.
327 | You may obtain a copy of the License at
328 |
329 | http://www.apache.org/licenses/LICENSE-2.0
330 |
331 | Unless required by applicable law or agreed to in writing, software
332 | distributed under the License is distributed on an "AS IS" BASIS,
333 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
334 | See the License for the specific language governing permissions and
335 | limitations under the License.
336 | ```
337 |
--------------------------------------------------------------------------------
/app/HPermissions.jks:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dahui888/HPermissions/dc84015c060bb41b138534ae8614b7f6b74dda7f/app/HPermissions.jks
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 32
5 |
6 | defaultConfig {
7 | applicationId "com.ph.permissions"
8 | minSdkVersion 14
9 | targetSdkVersion 32
10 | versionCode 12
11 | versionName "1.2"
12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
13 | }
14 |
15 | // 使用 JDK 1.8
16 | compileOptions {
17 | targetCompatibility JavaVersion.VERSION_1_8
18 | sourceCompatibility JavaVersion.VERSION_1_8
19 | }
20 |
21 | // Apk 签名
22 | signingConfigs {
23 | config {
24 | storeFile file(StoreFile)
25 | storePassword StorePassword
26 | keyAlias KeyAlias
27 | keyPassword KeyPassword
28 | }
29 | }
30 |
31 | buildTypes {
32 | debug {
33 | minifyEnabled false
34 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
35 | signingConfig signingConfigs.config
36 | }
37 |
38 | release {
39 | minifyEnabled true
40 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
41 | signingConfig signingConfigs.config
42 | }
43 | }
44 |
45 | applicationVariants.all { variant ->
46 | // apk 输出文件名配置
47 | variant.outputs.all { output ->
48 | outputFileName = rootProject.getName() + '.apk'
49 | }
50 | }
51 | }
52 |
53 | dependencies {
54 | // 依赖 libs 目录下所有的 jar 和 aar 包
55 | implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs')
56 |
57 | implementation project(':library')
58 |
59 | // 谷歌兼容库:https://developer.android.google.cn/jetpack/androidx/releases/appcompat?hl=zh-cn
60 | // noinspection GradleCompatible
61 | implementation 'com.android.support:appcompat-v7:28.0.0'
62 | }
--------------------------------------------------------------------------------
/app/gradle.properties:
--------------------------------------------------------------------------------
1 | StoreFile = HPermissions.jks
2 | StorePassword = HPermissions
3 | KeyAlias = HPermissions
4 | KeyPassword = HPermissions
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | -keep class com.ph.permissions.** {*;}
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
56 |
57 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ph/permissions/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.ph.permissions;
2 |
3 | import android.content.ComponentName;
4 | import android.content.Intent;
5 | import android.content.pm.PackageManager;
6 | import android.os.Build;
7 | import android.os.Bundle;
8 | import android.support.annotation.Nullable;
9 | import android.support.annotation.RequiresApi;
10 | import android.support.v7.app.AppCompatActivity;
11 | import android.view.View;
12 | import android.widget.Toast;
13 |
14 | import com.ph.permissions.R;
15 |
16 | import java.util.List;
17 |
18 | /**
19 | * author : i小灰
20 | * github : https://github.com/dahui888/HPermissions
21 | * desc : 权限申请演示
22 | */
23 | public final class MainActivity extends AppCompatActivity implements View.OnClickListener {
24 |
25 | @Override
26 | protected void onCreate(Bundle savedInstanceState) {
27 | super.onCreate(savedInstanceState);
28 | setContentView(R.layout.activity_main);
29 |
30 | findViewById(R.id.btn_main_request_single).setOnClickListener(this);
31 | findViewById(R.id.btn_main_request_group).setOnClickListener(this);
32 | findViewById(R.id.btn_main_request_location).setOnClickListener(this);
33 | findViewById(R.id.btn_main_request_bluetooth).setOnClickListener(this);
34 | findViewById(R.id.btn_main_request_storage).setOnClickListener(this);
35 | findViewById(R.id.btn_main_request_install).setOnClickListener(this);
36 | findViewById(R.id.btn_main_request_window).setOnClickListener(this);
37 | findViewById(R.id.btn_main_request_setting).setOnClickListener(this);
38 | findViewById(R.id.btn_main_request_notification).setOnClickListener(this);
39 | findViewById(R.id.btn_main_request_notification_listener).setOnClickListener(this);
40 | findViewById(R.id.btn_main_request_package).setOnClickListener(this);
41 | findViewById(R.id.btn_main_request_alarm).setOnClickListener(this);
42 | findViewById(R.id.btn_main_request_not_disturb).setOnClickListener(this);
43 | findViewById(R.id.btn_main_app_details).setOnClickListener(this);
44 | }
45 |
46 | @Override
47 | public void onClick(View view) {
48 | int viewId = view.getId();
49 | if (viewId == R.id.btn_main_request_single) {
50 |
51 | HPermissions.with(this)
52 | .permission(Permission.CAMERA)
53 | .interceptor(new PermissionInterceptor())
54 | .request(new OnPermissionCallback() {
55 |
56 | @Override
57 | public void onGranted(List permissions, boolean all) {
58 | if (all) {
59 | toast("获取拍照权限成功");
60 | }
61 | }
62 | });
63 |
64 | } else if (viewId == R.id.btn_main_request_group) {
65 |
66 | HPermissions.with(this)
67 | .permission(Permission.RECORD_AUDIO)
68 | .permission(Permission.Group.CALENDAR)
69 | .interceptor(new PermissionInterceptor())
70 | .request(new OnPermissionCallback() {
71 |
72 | @Override
73 | public void onGranted(List permissions, boolean all) {
74 | if (all) {
75 | toast("获取录音和日历权限成功");
76 | }
77 | }
78 | });
79 |
80 | } else if (viewId == R.id.btn_main_request_location) {
81 |
82 | HPermissions.with(this)
83 | .permission(Permission.ACCESS_COARSE_LOCATION)
84 | .permission(Permission.ACCESS_FINE_LOCATION)
85 | // 如果不需要在后台使用定位功能,请不要申请此权限
86 | .permission(Permission.ACCESS_BACKGROUND_LOCATION)
87 | .interceptor(new PermissionInterceptor())
88 | .request(new OnPermissionCallback() {
89 |
90 | @Override
91 | public void onGranted(List permissions, boolean all) {
92 | if (all) {
93 | toast("获取定位权限成功");
94 | }
95 | }
96 | });
97 |
98 | } else if (viewId == R.id.btn_main_request_bluetooth) {
99 |
100 | long delayMillis = 0;
101 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
102 | delayMillis = 2000;
103 | toast("当前版本不是 Android 12 及以上,旧版本的需要定位权限才能进行扫描蓝牙");
104 | }
105 |
106 | view.postDelayed(new Runnable() {
107 |
108 | @Override
109 | public void run() {
110 | HPermissions.with(MainActivity.this)
111 | .permission(Permission.BLUETOOTH_SCAN)
112 | .permission(Permission.BLUETOOTH_CONNECT)
113 | .permission(Permission.BLUETOOTH_ADVERTISE)
114 | .interceptor(new PermissionInterceptor())
115 | .request(new OnPermissionCallback() {
116 |
117 | @Override
118 | public void onGranted(List permissions, boolean all) {
119 | if (all) {
120 | toast("获取蓝牙权限成功");
121 | }
122 | }
123 | });
124 | }
125 | }, delayMillis);
126 |
127 | } else if (viewId == R.id.btn_main_request_storage) {
128 |
129 | long delayMillis = 0;
130 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
131 | delayMillis = 2000;
132 | toast("当前版本不是 Android 11 及以上,会自动变更为旧版的请求方式");
133 | }
134 |
135 | view.postDelayed(new Runnable() {
136 |
137 | @Override
138 | public void run() {
139 | HPermissions.with(MainActivity.this)
140 | // 适配 Android 11 分区存储这样写
141 | //.permission(Permission.Group.STORAGE)
142 | // 不适配 Android 11 分区存储这样写
143 | .permission(Permission.MANAGE_EXTERNAL_STORAGE)
144 | .interceptor(new PermissionInterceptor())
145 | .request(new OnPermissionCallback() {
146 |
147 | @Override
148 | public void onGranted(List permissions, boolean all) {
149 | if (all) {
150 | toast("获取存储权限成功");
151 | }
152 | }
153 | });
154 | }
155 | }, delayMillis);
156 |
157 | } else if (viewId == R.id.btn_main_request_install) {
158 |
159 | HPermissions.with(this)
160 | .permission(Permission.REQUEST_INSTALL_PACKAGES)
161 | .interceptor(new PermissionInterceptor())
162 | .request(new OnPermissionCallback() {
163 |
164 | @Override
165 | public void onGranted(List permissions, boolean all) {
166 | toast("获取安装包权限成功");
167 | }
168 | });
169 |
170 | } else if (viewId == R.id.btn_main_request_window) {
171 |
172 | HPermissions.with(this)
173 | .permission(Permission.SYSTEM_ALERT_WINDOW)
174 | .interceptor(new PermissionInterceptor())
175 | .request(new OnPermissionCallback() {
176 |
177 | @Override
178 | public void onGranted(List permissions, boolean all) {
179 | toast("获取悬浮窗权限成功");
180 | }
181 | });
182 |
183 | } else if (viewId == R.id.btn_main_request_setting) {
184 |
185 | HPermissions.with(this)
186 | .permission(Permission.WRITE_SETTINGS)
187 | .interceptor(new PermissionInterceptor())
188 | .request(new OnPermissionCallback() {
189 |
190 | @Override
191 | public void onGranted(List permissions, boolean all) {
192 | toast("获取系统设置权限成功");
193 | }
194 | });
195 |
196 | } else if (viewId == R.id.btn_main_request_notification) {
197 |
198 | HPermissions.with(this)
199 | .permission(Permission.NOTIFICATION_SERVICE)
200 | .interceptor(new PermissionInterceptor())
201 | .request(new OnPermissionCallback() {
202 |
203 | @Override
204 | public void onGranted(List permissions, boolean all) {
205 | toast("获取通知栏权限成功");
206 | }
207 | });
208 |
209 | } else if (viewId == R.id.btn_main_request_notification_listener) {
210 |
211 | HPermissions.with(this)
212 | .permission(Permission.BIND_NOTIFICATION_LISTENER_SERVICE)
213 | .interceptor(new PermissionInterceptor())
214 | .request(new OnPermissionCallback() {
215 |
216 | @Override
217 | public void onGranted(List permissions, boolean all) {
218 | toast("获取通知栏监听权限成功");
219 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
220 | toggleNotificationListenerService();
221 | }
222 | }
223 | });
224 |
225 | } else if (viewId == R.id.btn_main_request_package) {
226 |
227 | HPermissions.with(this)
228 | .permission(Permission.PACKAGE_USAGE_STATS)
229 | .interceptor(new PermissionInterceptor())
230 | .request(new OnPermissionCallback() {
231 |
232 | @Override
233 | public void onGranted(List permissions, boolean all) {
234 | toast("获取使用统计权限成功");
235 | }
236 | });
237 |
238 | } else if (viewId == R.id.btn_main_request_alarm) {
239 |
240 | HPermissions.with(this)
241 | .permission(Permission.SCHEDULE_EXACT_ALARM)
242 | .interceptor(new PermissionInterceptor())
243 | .request(new OnPermissionCallback() {
244 |
245 | @Override
246 | public void onGranted(List permissions, boolean all) {
247 | toast("获取闹钟权限成功");
248 | }
249 | });
250 |
251 | } else if (viewId == R.id.btn_main_request_not_disturb) {
252 |
253 | HPermissions.with(this)
254 | .permission(Permission.ACCESS_NOTIFICATION_POLICY)
255 | .interceptor(new PermissionInterceptor())
256 | .request(new OnPermissionCallback() {
257 |
258 | @Override
259 | public void onGranted(List permissions, boolean all) {
260 | toast("获取勿扰权限成功");
261 | }
262 | });
263 |
264 | } else if (viewId == R.id.btn_main_app_details) {
265 |
266 | HPermissions.startPermissionActivity(this);
267 | }
268 | }
269 |
270 | @Override
271 | protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
272 | super.onActivityResult(requestCode, resultCode, data);
273 | if (requestCode == HPermissions.REQUEST_CODE) {
274 | toast("检测到你刚刚从权限设置界面返回回来");
275 | }
276 | }
277 |
278 | public void toast(CharSequence text) {
279 | Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
280 | }
281 |
282 | @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
283 | private void toggleNotificationListenerService() {
284 | PackageManager packageManager = getPackageManager();
285 | packageManager.setComponentEnabledSetting(
286 | new ComponentName(this, NotificationMonitorService.class),
287 | PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
288 |
289 | packageManager.setComponentEnabledSetting(
290 | new ComponentName(this, NotificationMonitorService.class),
291 | PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
292 | }
293 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ph/permissions/NotificationMonitorService.java:
--------------------------------------------------------------------------------
1 | package com.ph.permissions;
2 |
3 | import android.app.Notification;
4 | import android.os.Build;
5 | import android.os.Bundle;
6 | import android.service.notification.NotificationListenerService;
7 | import android.service.notification.StatusBarNotification;
8 | import android.support.annotation.RequiresApi;
9 | import android.widget.Toast;
10 |
11 | /**
12 | * author : i小灰
13 | * github : https://github.com/dahui888/HPermissions
14 | * desc : 通知消息监控服务
15 | */
16 | @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
17 | public final class NotificationMonitorService extends NotificationListenerService {
18 |
19 | /**
20 | * 当系统收到新的通知后出发回调
21 | */
22 | @Override
23 | public void onNotificationPosted(StatusBarNotification sbn) {
24 | super.onNotificationPosted(sbn);
25 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
26 | Bundle extras = sbn.getNotification().extras;
27 | if (extras != null) {
28 | //获取通知消息标题
29 | String title = extras.getString(Notification.EXTRA_TITLE);
30 | // 获取通知消息内容
31 | Object msgText = extras.getCharSequence(Notification.EXTRA_TEXT);
32 | Toast.makeText(this, "监听到新的通知消息,标题为:" + title + ",内容为:" + msgText, Toast.LENGTH_SHORT).show();
33 | }
34 | }
35 | }
36 |
37 | /**
38 | * 当系统通知被删掉后出发回调
39 | */
40 | @Override
41 | public void onNotificationRemoved(StatusBarNotification sbn) {
42 | super.onNotificationRemoved(sbn);
43 | }
44 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ph/permissions/PermissionInterceptor.java:
--------------------------------------------------------------------------------
1 | package com.ph.permissions;
2 |
3 | import android.app.Activity;
4 | import android.content.Context;
5 | import android.content.DialogInterface;
6 | import android.os.Build;
7 | import android.support.v7.app.AlertDialog;
8 | import android.widget.Toast;
9 |
10 | import com.ph.permissions.R;
11 |
12 | import java.util.ArrayList;
13 | import java.util.List;
14 |
15 | /**
16 | * author : i小灰
17 | * github : https://github.com/dahui888/HPermissions
18 | * desc : 权限申请拦截器
19 | */
20 | public final class PermissionInterceptor implements IPermissionInterceptor {
21 |
22 | // @Override
23 | // public void requestPermissions(Activity activity, OnPermissionCallback callback, List allPermissions) {
24 | // // 这里的 Dialog 只是示例,没有用 DialogFragment 来处理 Dialog 生命周期
25 | // new AlertDialog.Builder(activity)
26 | // .setTitle(R.string.common_permission_hint)
27 | // .setMessage(R.string.common_permission_message)
28 | // .setPositiveButton(R.string.common_permission_granted, new DialogInterface.OnClickListener() {
29 | //
30 | // @Override
31 | // public void onClick(DialogInterface dialog, int which) {
32 | // dialog.dismiss();
33 | // PermissionFragment.beginRequest(activity, new ArrayList<>(allPermissions), PermissionInterceptor.this, callback);
34 | // }
35 | // })
36 | // .setNegativeButton(R.string.common_permission_denied, new DialogInterface.OnClickListener() {
37 | //
38 | // @Override
39 | // public void onClick(DialogInterface dialog, int which) {
40 | // dialog.dismiss();
41 | // }
42 | // })
43 | // .show();
44 | // }
45 |
46 | @Override
47 | public void grantedPermissions(Activity activity, List allPermissions, List grantedPermissions,
48 | boolean all, OnPermissionCallback callback) {
49 | if (callback != null) {
50 | callback.onGranted(grantedPermissions, all);
51 | }
52 | }
53 |
54 | @Override
55 | public void deniedPermissions(Activity activity, List allPermissions, List deniedPermissions,
56 | boolean never, OnPermissionCallback callback) {
57 | if (callback != null) {
58 | callback.onDenied(deniedPermissions, never);
59 | }
60 |
61 | if (never) {
62 | showPermissionDialog(activity, allPermissions, deniedPermissions, callback);
63 | return;
64 | }
65 |
66 | if (deniedPermissions.size() == 1 && Permission.ACCESS_BACKGROUND_LOCATION.equals(deniedPermissions.get(0))) {
67 | Toast.makeText(activity, R.string.common_permission_fail_4, Toast.LENGTH_SHORT).show();
68 | return;
69 | }
70 | Toast.makeText(activity, R.string.common_permission_fail_1, Toast.LENGTH_SHORT).show();
71 | }
72 |
73 | /**
74 | * 显示授权对话框
75 | */
76 | protected void showPermissionDialog(Activity activity, List allPermissions,
77 | List deniedPermissions, OnPermissionCallback callback) {
78 | if (activity == null || activity.isFinishing() ||
79 | (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && activity.isDestroyed())) {
80 | return;
81 | }
82 | // 这里的 Dialog 只是示例,没有用 DialogFragment 来处理 Dialog 生命周期
83 | new AlertDialog.Builder(activity)
84 | .setTitle(R.string.common_permission_alert)
85 | .setMessage(getPermissionHint(activity, deniedPermissions))
86 | .setPositiveButton(R.string.common_permission_goto, new DialogInterface.OnClickListener() {
87 |
88 | @Override
89 | public void onClick(DialogInterface dialog, int which) {
90 | dialog.dismiss();
91 | HPermissions.startPermissionActivity(activity,
92 | deniedPermissions, new OnPermissionPageCallback() {
93 |
94 | @Override
95 | public void onGranted() {
96 | if (callback == null) {
97 | return;
98 | }
99 | callback.onGranted(allPermissions, true);
100 | }
101 |
102 | @Override
103 | public void onDenied() {
104 | showPermissionDialog(activity, allPermissions,
105 | HPermissions.getDenied(activity, allPermissions), callback);
106 | }
107 | });
108 | }
109 | })
110 | .show();
111 | }
112 |
113 | /**
114 | * 根据权限获取提示
115 | */
116 | protected String getPermissionHint(Context context, List permissions) {
117 | if (permissions == null || permissions.isEmpty()) {
118 | return context.getString(R.string.common_permission_fail_2);
119 | }
120 |
121 | List hints = new ArrayList<>();
122 | for (String permission : permissions) {
123 | switch (permission) {
124 | case Permission.READ_EXTERNAL_STORAGE:
125 | case Permission.WRITE_EXTERNAL_STORAGE:
126 | case Permission.MANAGE_EXTERNAL_STORAGE: {
127 | String hint = context.getString(R.string.common_permission_storage);
128 | if (!hints.contains(hint)) {
129 | hints.add(hint);
130 | }
131 | break;
132 | }
133 | case Permission.CAMERA: {
134 | String hint = context.getString(R.string.common_permission_camera);
135 | if (!hints.contains(hint)) {
136 | hints.add(hint);
137 | }
138 | break;
139 | }
140 | case Permission.RECORD_AUDIO: {
141 | String hint = context.getString(R.string.common_permission_microphone);
142 | if (!hints.contains(hint)) {
143 | hints.add(hint);
144 | }
145 | break;
146 | }
147 | case Permission.ACCESS_FINE_LOCATION:
148 | case Permission.ACCESS_COARSE_LOCATION:
149 | case Permission.ACCESS_BACKGROUND_LOCATION: {
150 | String hint;
151 | if (!permissions.contains(Permission.ACCESS_FINE_LOCATION) &&
152 | !permissions.contains(Permission.ACCESS_COARSE_LOCATION)) {
153 | hint = context.getString(R.string.common_permission_location_background);
154 | } else {
155 | hint = context.getString(R.string.common_permission_location);
156 | }
157 | if (!hints.contains(hint)) {
158 | hints.add(hint);
159 | }
160 | break;
161 | }
162 | case Permission.BLUETOOTH_SCAN:
163 | case Permission.BLUETOOTH_CONNECT:
164 | case Permission.BLUETOOTH_ADVERTISE: {
165 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
166 | String hint = context.getString(R.string.common_permission_bluetooth);
167 | if (!hints.contains(hint)) {
168 | hints.add(hint);
169 | }
170 | }
171 | break;
172 | }
173 | case Permission.READ_PHONE_STATE:
174 | case Permission.CALL_PHONE:
175 | case Permission.ADD_VOICEMAIL:
176 | case Permission.USE_SIP:
177 | case Permission.READ_PHONE_NUMBERS:
178 | case Permission.ANSWER_PHONE_CALLS: {
179 | String hint = context.getString(R.string.common_permission_phone);
180 | if (!hints.contains(hint)) {
181 | hints.add(hint);
182 | }
183 | break;
184 | }
185 | case Permission.GET_ACCOUNTS:
186 | case Permission.READ_CONTACTS:
187 | case Permission.WRITE_CONTACTS: {
188 | String hint = context.getString(R.string.common_permission_contacts);
189 | if (!hints.contains(hint)) {
190 | hints.add(hint);
191 | }
192 | break;
193 | }
194 | case Permission.READ_CALENDAR:
195 | case Permission.WRITE_CALENDAR: {
196 | String hint = context.getString(R.string.common_permission_calendar);
197 | if (!hints.contains(hint)) {
198 | hints.add(hint);
199 | }
200 | break;
201 | }
202 | case Permission.READ_CALL_LOG:
203 | case Permission.WRITE_CALL_LOG:
204 | case Permission.PROCESS_OUTGOING_CALLS: {
205 | String hint = context.getString(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ?
206 | R.string.common_permission_call_log : R.string.common_permission_phone);
207 | if (!hints.contains(hint)) {
208 | hints.add(hint);
209 | }
210 | break;
211 | }
212 | case Permission.BODY_SENSORS: {
213 | String hint = context.getString(R.string.common_permission_sensors);
214 | if (!hints.contains(hint)) {
215 | hints.add(hint);
216 | }
217 | break;
218 | }
219 | case Permission.ACTIVITY_RECOGNITION: {
220 | String hint = context.getString(R.string.common_permission_activity_recognition);
221 | if (!hints.contains(hint)) {
222 | hints.add(hint);
223 | }
224 | break;
225 | }
226 | case Permission.SEND_SMS:
227 | case Permission.RECEIVE_SMS:
228 | case Permission.READ_SMS:
229 | case Permission.RECEIVE_WAP_PUSH:
230 | case Permission.RECEIVE_MMS: {
231 | String hint = context.getString(R.string.common_permission_sms);
232 | if (!hints.contains(hint)) {
233 | hints.add(hint);
234 | }
235 | break;
236 | }
237 | case Permission.REQUEST_INSTALL_PACKAGES: {
238 | String hint = context.getString(R.string.common_permission_install);
239 | if (!hints.contains(hint)) {
240 | hints.add(hint);
241 | }
242 | break;
243 | }
244 | case Permission.SYSTEM_ALERT_WINDOW: {
245 | String hint = context.getString(R.string.common_permission_window);
246 | if (!hints.contains(hint)) {
247 | hints.add(hint);
248 | }
249 | break;
250 | }
251 | case Permission.WRITE_SETTINGS: {
252 | String hint = context.getString(R.string.common_permission_setting);
253 | if (!hints.contains(hint)) {
254 | hints.add(hint);
255 | }
256 | break;
257 | }
258 | case Permission.NOTIFICATION_SERVICE: {
259 | String hint = context.getString(R.string.common_permission_notification);
260 | if (!hints.contains(hint)) {
261 | hints.add(hint);
262 | }
263 | break;
264 | }
265 | case Permission.BIND_NOTIFICATION_LISTENER_SERVICE: {
266 | String hint = context.getString(R.string.common_permission_notification);
267 | if (!hints.contains(hint)) {
268 | hints.add(hint);
269 | }
270 | break;
271 | }
272 | case Permission.PACKAGE_USAGE_STATS: {
273 | String hint = context.getString(R.string.common_permission_task);
274 | if (!hints.contains(hint)) {
275 | hints.add(hint);
276 | }
277 | break;
278 | }
279 | case Permission.SCHEDULE_EXACT_ALARM: {
280 | String hint = context.getString(R.string.common_permission_alarm);
281 | if (!hints.contains(hint)) {
282 | hints.add(hint);
283 | }
284 | break;
285 | }
286 | case Permission.ACCESS_NOTIFICATION_POLICY: {
287 | String hint = context.getString(R.string.common_permission_not_disturb);
288 | if (!hints.contains(hint)) {
289 | hints.add(hint);
290 | }
291 | break;
292 | }
293 | default:
294 | break;
295 | }
296 | }
297 |
298 | if (!hints.isEmpty()) {
299 | StringBuilder builder = new StringBuilder();
300 | for (String text : hints) {
301 | if (builder.length() == 0) {
302 | builder.append(text);
303 | } else {
304 | builder.append("、")
305 | .append(text);
306 | }
307 | }
308 | builder.append(" ");
309 | return context.getString(R.string.common_permission_fail_3, builder.toString());
310 | }
311 |
312 | return context.getString(R.string.common_permission_fail_2);
313 | }
314 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
12 |
13 |
18 |
19 |
26 |
27 |
34 |
35 |
42 |
43 |
50 |
51 |
58 |
59 |
66 |
67 |
74 |
75 |
82 |
83 |
90 |
91 |
98 |
99 |
106 |
107 |
114 |
115 |
122 |
123 |
131 |
132 |
133 |
134 |
135 |
136 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dahui888/HPermissions/dc84015c060bb41b138534ae8614b7f6b74dda7f/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dahui888/HPermissions/dc84015c060bb41b138534ae8614b7f6b74dda7f/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dahui888/HPermissions/dc84015c060bb41b138534ae8614b7f6b74dda7f/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/values-v21/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #000000
4 | #000000
5 | #FFFFFF
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | HPermissions
4 |
5 | 授权提示
6 | 使用此功能需要先授予权限
7 | 授予
8 | 取消
9 |
10 | 授权提醒
11 | 授权失败,请正确授予权限
12 | 获取权限失败,请手动授予权限
13 | 获取权限失败,请手动授予%s
14 | 没有授予后台定位权限,请您选择"始终允许"
15 | 前往授权
16 |
17 | 日历权限
18 | 相机权限
19 | 通讯录权限
20 | 定位权限
21 | 后台定位权限
22 | 附近的设备权限
23 | 麦克风权限
24 | 电话权限
25 | 通话记录权限
26 | 身体传感器权限
27 | 健身运动权限
28 | 短信权限
29 | 存储权限
30 |
31 | 安装应用权限
32 | 悬浮窗权限
33 | 系统设置权限
34 | 通知栏权限
35 | 通知监听权限
36 | 查看使用情况权限
37 | 查看闹钟提醒权限
38 | 勿扰权限
39 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories {
3 | // 阿里云云效仓库:https://maven.aliyun.com/mvn/guide
4 | maven { url 'https://maven.aliyun.com/repository/public' }
5 | maven { url 'https://maven.aliyun.com/repository/google' }
6 | // 华为开源镜像:https://mirrors.huaweicloud.com/
7 | maven { url 'https://repo.huaweicloud.com/repository/maven/' }
8 | // JitPack 远程仓库:https://jitpack.io
9 | maven { url 'https://jitpack.io' }
10 | mavenCentral()
11 | google()
12 | // noinspection JcenterRepositoryObsolete
13 | jcenter()
14 | }
15 | dependencies {
16 | classpath 'com.android.tools.build:gradle:4.1.2'
17 | }
18 | }
19 |
20 | allprojects {
21 | repositories {
22 | maven { url 'https://maven.aliyun.com/repository/public' }
23 | maven { url 'https://maven.aliyun.com/repository/google' }
24 | maven { url 'https://repo.huaweicloud.com/repository/maven/' }
25 | maven { url 'https://jitpack.io' }
26 | mavenCentral()
27 | google()
28 | // noinspection JcenterRepositoryObsolete
29 | jcenter()
30 | }
31 |
32 | // 将构建文件统一输出到项目根目录下的 build 文件夹
33 | setBuildDir(new File(rootDir, "build/${path.replaceAll(':', '/')}"))
34 | }
35 |
36 | task clean(type: Delete) {
37 | delete rootProject.buildDir
38 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx1536m
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | # org.gradle.parallel=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dahui888/HPermissions/dc84015c060bb41b138534ae8614b7f6b74dda7f/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | zipStoreBase = GRADLE_USER_HOME
2 | zipStorePath = wrapper/dists
3 | distributionBase = GRADLE_USER_HOME
4 | distributionPath = wrapper/dists
5 | distributionUrl = https\://services.gradle.org/distributions/gradle-6.5-all.zip
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/library/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | android {
4 | compileSdkVersion 32
5 |
6 | defaultConfig {
7 | minSdkVersion 11
8 | versionCode 12
9 | versionName "1.2"
10 | }
11 |
12 | // 使用 JDK 1.8
13 | compileOptions {
14 | targetCompatibility JavaVersion.VERSION_1_8
15 | sourceCompatibility JavaVersion.VERSION_1_8
16 | }
17 |
18 | lintOptions {
19 | abortOnError false
20 | }
21 |
22 | android.libraryVariants.all { variant ->
23 | // aar 输出文件名配置
24 | variant.outputs.all { output ->
25 | outputFileName = "${rootProject.name}-${android.defaultConfig.versionName}.aar"
26 | }
27 | }
28 |
29 | packagingOptions {
30 | // 剔除这个包下的所有文件(不会移除签名信息)
31 | exclude 'META-INF/*******'
32 | }
33 | }
34 |
35 | afterEvaluate {
36 | // 排除 BuildConfig.class 和 R.class
37 | generateReleaseBuildConfig.enabled = false
38 | generateDebugBuildConfig.enabled = false
39 | generateReleaseResValues.enabled = false
40 | generateDebugResValues.enabled = false
41 | }
42 |
43 | dependencies {
44 | // noinspection GradleCompatible
45 | implementation 'com.android.support:support-fragment:24.2.0'
46 | }
47 |
48 | tasks.withType(Javadoc) {
49 | options.addStringOption('Xdoclint:none', '-quiet')
50 | options.addStringOption('encoding', 'UTF-8')
51 | options.addStringOption('charSet', 'UTF-8')
52 | }
53 |
54 | task sourcesJar(type: Jar) {
55 | from android.sourceSets.main.java.srcDirs
56 | classifier = 'sources'
57 | }
58 |
59 | task javadoc(type: Javadoc) {
60 | source = android.sourceSets.main.java.srcDirs
61 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
62 | }
63 |
64 | task javadocJar(type: Jar, dependsOn: javadoc) {
65 | classifier = 'javadoc'
66 | from javadoc.destinationDir
67 | }
68 |
69 | artifacts {
70 | archives javadocJar
71 | archives sourcesJar
72 | }
--------------------------------------------------------------------------------
/library/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in D:\SDK\Studio\sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
--------------------------------------------------------------------------------
/library/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/library/src/main/java/com/ph/permissions/AndroidVersion.java:
--------------------------------------------------------------------------------
1 | package com.ph.permissions;
2 |
3 | import android.os.Build;
4 |
5 | /**
6 | * author : i小灰
7 | * github : https://github.com/dahui888/HPermissions
8 | * desc : Android 版本判断
9 | */
10 | @SuppressWarnings("all")
11 | final class AndroidVersion {
12 |
13 | static final int ANDROID_12_L = Build.VERSION_CODES.S_V2;
14 | static final int ANDROID_12 = Build.VERSION_CODES.S;
15 | static final int ANDROID_11 = Build.VERSION_CODES.R;
16 | static final int ANDROID_10 = Build.VERSION_CODES.Q;
17 | static final int ANDROID_9 = Build.VERSION_CODES.P;
18 | static final int ANDROID_8_1 = Build.VERSION_CODES.O_MR1;
19 | static final int ANDROID_8 = Build.VERSION_CODES.O;
20 | static final int ANDROID_7_1 = Build.VERSION_CODES.N_MR1;
21 | static final int ANDROID_7 = Build.VERSION_CODES.N;
22 | static final int ANDROID_6 = Build.VERSION_CODES.M;
23 | static final int ANDROID_5_1 = Build.VERSION_CODES.LOLLIPOP_MR1;
24 | static final int ANDROID_5 = Build.VERSION_CODES.LOLLIPOP;
25 | static final int ANDROID_4_4 = Build.VERSION_CODES.KITKAT;
26 | static final int ANDROID_4_3 = Build.VERSION_CODES.JELLY_BEAN_MR2;
27 | static final int ANDROID_4_2 = Build.VERSION_CODES.JELLY_BEAN_MR1;
28 | static final int ANDROID_4_1 = Build.VERSION_CODES.JELLY_BEAN;
29 |
30 | /**
31 | * 是否是 Android 12 及以上版本
32 | */
33 | static boolean isAndroid12() {
34 | return Build.VERSION.SDK_INT >= ANDROID_12;
35 | }
36 |
37 | /**
38 | * 是否是 Android 11 及以上版本
39 | */
40 | static boolean isAndroid11() {
41 | return Build.VERSION.SDK_INT >= ANDROID_11;
42 | }
43 |
44 | /**
45 | * 是否是 Android 10 及以上版本
46 | */
47 | static boolean isAndroid10() {
48 | return Build.VERSION.SDK_INT >= ANDROID_10;
49 | }
50 |
51 | /**
52 | * 是否是 Android 9.0 及以上版本
53 | */
54 | static boolean isAndroid9() {
55 | return Build.VERSION.SDK_INT >= ANDROID_9;
56 | }
57 |
58 | /**
59 | * 是否是 Android 8.0 及以上版本
60 | */
61 | static boolean isAndroid8() {
62 | return Build.VERSION.SDK_INT >= ANDROID_8;
63 | }
64 |
65 | /**
66 | * 是否是 Android 6.0 及以上版本
67 | */
68 | static boolean isAndroid6() {
69 | return Build.VERSION.SDK_INT >= ANDROID_6;
70 | }
71 |
72 | /**
73 | * 是否是 Android 5.0 及以上版本
74 | */
75 | static boolean isAndroid5_1() {
76 | return Build.VERSION.SDK_INT >= ANDROID_5_1;
77 | }
78 |
79 | /**
80 | * 是否是 Android 5.0 及以上版本
81 | */
82 | static boolean isAndroid5() {
83 | return Build.VERSION.SDK_INT >= ANDROID_5;
84 | }
85 |
86 | /**
87 | * 是否是 Android 4.3 及以上版本
88 | */
89 | static boolean isAndroid4_3() {
90 | return Build.VERSION.SDK_INT >= ANDROID_4_3;
91 | }
92 |
93 | /**
94 | * 是否是 Android 4.2 及以上版本
95 | */
96 | static boolean isAndroid4_2() {
97 | return Build.VERSION.SDK_INT >= ANDROID_4_2;
98 | }
99 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/ph/permissions/HPermissions.java:
--------------------------------------------------------------------------------
1 | package com.ph.permissions;
2 |
3 | import android.app.Activity;
4 | import android.app.Fragment;
5 | import android.content.Context;
6 | import android.content.Intent;
7 | import android.os.Build;
8 |
9 | import java.util.ArrayList;
10 | import java.util.List;
11 |
12 | /**
13 | * author : i小灰
14 | * github : https://github.com/dahui888/HPermissions
15 | * desc : Android 危险权限请求类
16 | */
17 | @SuppressWarnings({"unused", "deprecation"})
18 | public final class HPermissions {
19 |
20 | /** 权限设置页跳转请求码 */
21 | public static final int REQUEST_CODE = 1024 + 1;
22 |
23 | /** 权限请求拦截器 */
24 | private static IPermissionInterceptor sInterceptor;
25 |
26 | /** 当前是否为检查模式 */
27 | private static Boolean sCheckMode;
28 |
29 | /**
30 | * 设置请求的对象
31 | *
32 | * @param context 当前 Activity,可以传入栈顶的 Activity
33 | */
34 | public static HPermissions with(Context context) {
35 | return new HPermissions(context);
36 | }
37 |
38 | public static HPermissions with(Fragment fragment) {
39 | return with(fragment.getActivity());
40 | }
41 |
42 | public static HPermissions with(android.support.v4.app.Fragment fragment) {
43 | return with(fragment.getActivity());
44 | }
45 |
46 | /**
47 | * 是否为检查模式
48 | */
49 | public static void setCheckMode(boolean checkMode) {
50 | sCheckMode = checkMode;
51 | }
52 |
53 | /**
54 | * 设置全局权限请求拦截器
55 | */
56 | public static void setInterceptor(IPermissionInterceptor interceptor) {
57 | sInterceptor = interceptor;
58 | }
59 |
60 | /**
61 | * 获取全局权限请求拦截器
62 | */
63 | public static IPermissionInterceptor getInterceptor() {
64 | if (sInterceptor == null) {
65 | sInterceptor = new IPermissionInterceptor() {};
66 | }
67 | return sInterceptor;
68 | }
69 |
70 | /** Context 对象 */
71 | private final Context mContext;
72 |
73 | /** 权限列表 */
74 | private List mPermissions;
75 |
76 | /** 权限请求拦截器 */
77 | private IPermissionInterceptor mInterceptor;
78 |
79 | /** 设置不检查 */
80 | private Boolean mCheckMode;
81 |
82 | /**
83 | * 私有化构造函数
84 | */
85 | private HPermissions(Context context) {
86 | mContext = context;
87 | }
88 |
89 | /**
90 | * 添加权限组
91 | */
92 | public HPermissions permission(String... permissions) {
93 | return permission(PermissionUtils.asArrayList(permissions));
94 | }
95 |
96 | public HPermissions permission(String[]... permissions) {
97 | return permission(PermissionUtils.asArrayLists(permissions));
98 | }
99 |
100 | public HPermissions permission(List permissions) {
101 | if (permissions == null || permissions.isEmpty()) {
102 | return this;
103 | }
104 | if (mPermissions == null) {
105 | mPermissions = new ArrayList<>(permissions);
106 | return this;
107 | }
108 |
109 | for (String permission : permissions) {
110 | if (mPermissions.contains(permission)) {
111 | continue;
112 | }
113 | mPermissions.add(permission);
114 | }
115 | return this;
116 | }
117 |
118 | /**
119 | * 设置权限请求拦截器
120 | */
121 | public HPermissions interceptor(IPermissionInterceptor interceptor) {
122 | mInterceptor = interceptor;
123 | return this;
124 | }
125 |
126 | /**
127 | * 设置不触发错误检测机制
128 | */
129 | public HPermissions unchecked() {
130 | mCheckMode = false;
131 | return this;
132 | }
133 |
134 | /**
135 | * 请求权限
136 | */
137 | public void request(OnPermissionCallback callback) {
138 | if (mContext == null) {
139 | return;
140 | }
141 |
142 | if (mInterceptor == null) {
143 | mInterceptor = getInterceptor();
144 | }
145 |
146 | // 权限请求列表(为什么直接不用字段?因为框架要兼容新旧权限,在低版本下会自动添加旧权限申请)
147 | List permissions = new ArrayList<>(mPermissions);
148 |
149 | if (mCheckMode == null) {
150 | if (sCheckMode == null) {
151 | sCheckMode = PermissionUtils.isDebugMode(mContext);
152 | }
153 | mCheckMode = sCheckMode;
154 | }
155 |
156 | // 检查当前 Activity 状态是否是正常的,如果不是则不请求权限
157 | Activity activity = PermissionUtils.findActivity(mContext);
158 | if (!PermissionChecker.checkActivityStatus(activity, mCheckMode)) {
159 | return;
160 | }
161 |
162 | // 必须要传入正常的权限或者权限组才能申请权限
163 | if (!PermissionChecker.checkPermissionArgument(permissions, mCheckMode)) {
164 | return;
165 | }
166 |
167 | if (mCheckMode) {
168 | // 检查申请的存储权限是否符合规范
169 | PermissionChecker.checkStoragePermission(mContext, permissions);
170 | // 检查申请的定位权限是否符合规范
171 | PermissionChecker.checkLocationPermission(mContext, permissions);
172 | // 检查申请的权限和 targetSdk 版本是否能吻合
173 | PermissionChecker.checkTargetSdkVersion(mContext, permissions);
174 | }
175 |
176 | if (mCheckMode) {
177 | // 检测权限有没有在清单文件中注册
178 | PermissionChecker.checkManifestPermissions(mContext, permissions);
179 | }
180 |
181 | // 优化所申请的权限列表
182 | PermissionChecker.optimizeDeprecatedPermission(permissions);
183 |
184 | if (PermissionApi.isGrantedPermissions(mContext, permissions)) {
185 | // 证明这些权限已经全部授予过,直接回调成功
186 | if (callback != null) {
187 | mInterceptor.grantedPermissions(activity, permissions, permissions, true, callback);
188 | }
189 | return;
190 | }
191 |
192 | // 申请没有授予过的权限
193 | mInterceptor.requestPermissions(activity, callback, permissions);
194 | }
195 |
196 | /**
197 | * 判断一个或多个权限是否全部授予了
198 | */
199 | public static boolean isGranted(Context context, String... permissions) {
200 | return isGranted(context, PermissionUtils.asArrayList(permissions));
201 | }
202 |
203 | public static boolean isGranted(Context context, String[]... permissions) {
204 | return isGranted(context, PermissionUtils.asArrayLists(permissions));
205 | }
206 |
207 | public static boolean isGranted(Context context, List permissions) {
208 | return PermissionApi.isGrantedPermissions(context, permissions);
209 | }
210 |
211 | /**
212 | * 获取没有授予的权限
213 | */
214 | public static List getDenied(Context context, String... permissions) {
215 | return getDenied(context, PermissionUtils.asArrayList(permissions));
216 | }
217 |
218 | public static List getDenied(Context context, String[]... permissions) {
219 | return getDenied(context, PermissionUtils.asArrayLists(permissions));
220 | }
221 |
222 | public static List getDenied(Context context, List permissions) {
223 | return PermissionApi.getDeniedPermissions(context, permissions);
224 | }
225 |
226 | /**
227 | * 判断某个权限是否为特殊权限
228 | */
229 | public static boolean isSpecial(String permission) {
230 | return PermissionApi.isSpecialPermission(permission);
231 | }
232 |
233 | /**
234 | * 判断一个或多个权限是否被永久拒绝了
235 | *
236 | * (注意不能在请求权限之前调用,应该在 {@link OnPermissionCallback#onDenied(List, boolean)} 方法中调用)
237 | */
238 | public static boolean isPermanentDenied(Activity activity, String... permissions) {
239 | return isPermanentDenied(activity, PermissionUtils.asArrayList(permissions));
240 | }
241 |
242 | public static boolean isPermanentDenied(Activity activity, String[]... permissions) {
243 | return isPermanentDenied(activity, PermissionUtils.asArrayLists(permissions));
244 | }
245 |
246 | public static boolean isPermanentDenied(Activity activity, List permissions) {
247 | return PermissionApi.isPermissionPermanentDenied(activity, permissions);
248 | }
249 |
250 | /* android.content.Context */
251 |
252 | public static void startPermissionActivity(Context context) {
253 | startPermissionActivity(context, (List) null);
254 | }
255 |
256 | public static void startPermissionActivity(Context context, String... permissions) {
257 | startPermissionActivity(context, PermissionUtils.asArrayList(permissions));
258 | }
259 |
260 | public static void startPermissionActivity(Context context, String[]... permissions) {
261 | startPermissionActivity(context, PermissionUtils.asArrayLists(permissions));
262 | }
263 |
264 | /**
265 | * 跳转到应用权限设置页
266 | *
267 | * @param permissions 没有授予或者被拒绝的权限组
268 | */
269 | public static void startPermissionActivity(Context context, List permissions) {
270 | Activity activity = PermissionUtils.findActivity(context);
271 | if (activity != null) {
272 | startPermissionActivity(activity, permissions);
273 | return;
274 | }
275 | Intent intent = PermissionPageIntent.getSmartPermissionIntent(context, permissions);
276 | if (!(context instanceof Activity)) {
277 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
278 | }
279 | context.startActivity(intent);
280 | }
281 |
282 | /* android.app.Activity */
283 |
284 | public static void startPermissionActivity(Activity activity) {
285 | startPermissionActivity(activity, (List) null);
286 | }
287 |
288 | public static void startPermissionActivity(Activity activity, String... permissions) {
289 | startPermissionActivity(activity, PermissionUtils.asArrayList(permissions));
290 | }
291 |
292 | public static void startPermissionActivity(Activity activity, String[]... permissions) {
293 | startPermissionActivity(activity, PermissionUtils.asArrayLists(permissions));
294 | }
295 |
296 | public static void startPermissionActivity(Activity activity, List permissions) {
297 | startPermissionActivity(activity, permissions, REQUEST_CODE);
298 | }
299 |
300 | public static void startPermissionActivity(Activity activity, List permissions, int requestCode) {
301 | activity.startActivityForResult(PermissionPageIntent.getSmartPermissionIntent(activity, permissions), requestCode);
302 | }
303 |
304 | public static void startPermissionActivity(Activity activity, String permission, OnPermissionPageCallback callback) {
305 | startPermissionActivity(activity, PermissionUtils.asArrayList(permission), callback);
306 | }
307 |
308 | public static void startPermissionActivity(Activity activity, String[] permissions, OnPermissionPageCallback callback) {
309 | startPermissionActivity(activity, PermissionUtils.asArrayLists(permissions), callback);
310 | }
311 |
312 | public static void startPermissionActivity(Activity activity, List permissions, OnPermissionPageCallback callback) {
313 | PermissionPageFragment.beginRequest(activity, (ArrayList) permissions, callback);
314 | }
315 |
316 | /* android.app.Fragment */
317 |
318 | public static void startPermissionActivity(Fragment fragment) {
319 | startPermissionActivity(fragment, (List) null);
320 | }
321 |
322 | public static void startPermissionActivity(Fragment fragment, String... permissions) {
323 | startPermissionActivity(fragment, PermissionUtils.asArrayList(permissions));
324 | }
325 |
326 | public static void startPermissionActivity(Fragment fragment, String[]... permissions) {
327 | startPermissionActivity(fragment, PermissionUtils.asArrayLists(permissions));
328 | }
329 |
330 | public static void startPermissionActivity(Fragment fragment, List permissions) {
331 | startPermissionActivity(fragment, permissions, REQUEST_CODE);
332 | }
333 |
334 | public static void startPermissionActivity(Fragment fragment, List permissions, int requestCode) {
335 | Activity activity = fragment.getActivity();
336 | if (activity == null) {
337 | return;
338 | }
339 | fragment.startActivityForResult(PermissionPageIntent.getSmartPermissionIntent(activity, permissions), requestCode);
340 | }
341 |
342 | public static void startPermissionActivity(Fragment fragment, String permission, OnPermissionPageCallback callback) {
343 | startPermissionActivity(fragment, PermissionUtils.asArrayList(permission), callback);
344 | }
345 |
346 | public static void startPermissionActivity(Fragment fragment, String[] permissions, OnPermissionPageCallback callback) {
347 | startPermissionActivity(fragment, PermissionUtils.asArrayLists(permissions), callback);
348 | }
349 |
350 | public static void startPermissionActivity(Fragment fragment, List permissions, OnPermissionPageCallback callback) {
351 | Activity activity = fragment.getActivity();
352 | if (activity == null || activity.isFinishing()) {
353 | return;
354 | }
355 | if (Build.VERSION.SDK_INT >= AndroidVersion.ANDROID_4_2 && activity.isDestroyed()) {
356 | return;
357 | }
358 | PermissionPageFragment.beginRequest(activity, (ArrayList) permissions, callback);
359 | }
360 |
361 | /* android.support.v4.app.Fragment */
362 |
363 | public static void startPermissionActivity(android.support.v4.app.Fragment fragment) {
364 | startPermissionActivity(fragment, (List) null);
365 | }
366 |
367 | public static void startPermissionActivity(android.support.v4.app.Fragment fragment, String... permissions) {
368 | startPermissionActivity(fragment, PermissionUtils.asArrayList(permissions));
369 | }
370 |
371 | public static void startPermissionActivity(android.support.v4.app.Fragment fragment, String[]... permissions) {
372 | startPermissionActivity(fragment, PermissionUtils.asArrayLists(permissions));
373 | }
374 |
375 | public static void startPermissionActivity(android.support.v4.app.Fragment fragment, List permissions) {
376 | startPermissionActivity(fragment, permissions, REQUEST_CODE);
377 | }
378 |
379 | public static void startPermissionActivity(android.support.v4.app.Fragment fragment, List permissions, int requestCode) {
380 | Activity activity = fragment.getActivity();
381 | if (activity == null) {
382 | return;
383 | }
384 | fragment.startActivityForResult(PermissionPageIntent.getSmartPermissionIntent(activity, permissions), requestCode);
385 | }
386 |
387 | public static void startPermissionActivity(android.support.v4.app.Fragment fragment, String permission, OnPermissionPageCallback callback) {
388 | startPermissionActivity(fragment, PermissionUtils.asArrayList(permission), callback);
389 | }
390 |
391 | public static void startPermissionActivity(android.support.v4.app.Fragment fragment, String[] permissions, OnPermissionPageCallback callback) {
392 | startPermissionActivity(fragment, PermissionUtils.asArrayLists(permissions), callback);
393 | }
394 |
395 | public static void startPermissionActivity(android.support.v4.app.Fragment fragment, List permissions, OnPermissionPageCallback callback) {
396 | Activity activity = fragment.getActivity();
397 | if (activity == null || activity.isFinishing()) {
398 | return;
399 | }
400 | if (Build.VERSION.SDK_INT >= AndroidVersion.ANDROID_4_2 && activity.isDestroyed()) {
401 | return;
402 | }
403 | PermissionPageFragment.beginRequest(activity, (ArrayList) permissions, callback);
404 | }
405 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/ph/permissions/IPermissionInterceptor.java:
--------------------------------------------------------------------------------
1 | package com.ph.permissions;
2 |
3 | import android.app.Activity;
4 |
5 | import java.util.ArrayList;
6 | import java.util.List;
7 |
8 | /**
9 | * author : i小灰
10 | * github : https://github.com/dahui888/HPermissions
11 | * desc : 权限请求拦截器
12 | */
13 | public interface IPermissionInterceptor {
14 |
15 | /**
16 | * 权限申请拦截,可在此处先弹 Dialog 再申请权限
17 | */
18 | default void requestPermissions(Activity activity, OnPermissionCallback callback, List allPermissions) {
19 | PermissionFragment.beginRequest(activity, new ArrayList<>(allPermissions), this, callback);
20 | }
21 |
22 | /**
23 | * 权限授予回调拦截,参见 {@link OnPermissionCallback#onGranted(List, boolean)}
24 | */
25 | default void grantedPermissions(Activity activity, List allPermissions,
26 | List grantedPermissions, boolean all,
27 | OnPermissionCallback callback) {
28 | if (callback == null) {
29 | return;
30 | }
31 | callback.onGranted(grantedPermissions, all);
32 | }
33 |
34 | /**
35 | * 权限拒绝回调拦截,参见 {@link OnPermissionCallback#onDenied(List, boolean)}
36 | */
37 | default void deniedPermissions(Activity activity, List allPermissions,
38 | List deniedPermissions, boolean never,
39 | OnPermissionCallback callback) {
40 | if (callback == null) {
41 | return;
42 | }
43 | callback.onDenied(deniedPermissions, never);
44 | }
45 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/ph/permissions/OnPermissionCallback.java:
--------------------------------------------------------------------------------
1 | package com.ph.permissions;
2 |
3 | import java.util.List;
4 |
5 | /**
6 | * author : i小灰
7 | * github : https://github.com/dahui888/HPermissions
8 | * desc : 权限请求结果回调接口
9 | */
10 | public interface OnPermissionCallback {
11 |
12 | /**
13 | * 有权限被同意授予时回调
14 | *
15 | * @param permissions 请求成功的权限组
16 | * @param all 是否全部授予了
17 | */
18 | void onGranted(List permissions, boolean all);
19 |
20 | /**
21 | * 有权限被拒绝授予时回调
22 | *
23 | * @param permissions 请求失败的权限组
24 | * @param never 是否有某个权限被永久拒绝了
25 | */
26 | default void onDenied(List permissions, boolean never) {}
27 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/ph/permissions/OnPermissionPageCallback.java:
--------------------------------------------------------------------------------
1 | package com.ph.permissions;
2 |
3 | /**
4 | * author : i小灰
5 | * github : https://github.com/dahui888/HPermissions
6 | * desc : 权限设置页结果回调接口
7 | */
8 | public interface OnPermissionPageCallback {
9 |
10 | /**
11 | * 权限已经授予
12 | */
13 | void onGranted();
14 |
15 | /**
16 | * 权限已经拒绝
17 | */
18 | void onDenied();
19 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/ph/permissions/Permission.java:
--------------------------------------------------------------------------------
1 | package com.ph.permissions;
2 |
3 | import android.Manifest;
4 |
5 | /**
6 | * author : i小灰
7 | * github : https://github.com/dahui888/HPermissions
8 | * desc : 危险权限和特殊权限常量集,参考 {@link Manifest.permission}
9 | * doc : https://developer.android.google.cn/reference/android/Manifest.permission?hl=zh_cn
10 | * https://developer.android.google.cn/guide/topics/permissions/overview?hl=zh-cn#normal-dangerous
11 | */
12 | @SuppressWarnings("unused")
13 | public final class Permission {
14 |
15 | private Permission() {}
16 |
17 | /**
18 | * 闹钟权限(特殊权限,需要 Android 12 及以上)
19 | *
20 | * 需要注意的是:这个权限和其他特殊权限不同的是,默认已经是授予状态,用户也可以手动撤销授权
21 | * 官方文档介绍:https://developer.android.google.cn/about/versions/12/behavior-changes-12?hl=zh_cn#exact-alarm-permission
22 | */
23 | public static final String SCHEDULE_EXACT_ALARM = "android.permission.SCHEDULE_EXACT_ALARM";
24 |
25 | /**
26 | * 文件管理权限(特殊权限,需要 Android 11 及以上)
27 | *
28 | * 为了兼容 Android 11 以下版本,需要在清单文件中注册
29 | * {@link Permission#READ_EXTERNAL_STORAGE} 和 {@link Permission#WRITE_EXTERNAL_STORAGE} 权限
30 | *
31 | * 如果你的应用需要上架 GooglePlay,那么需要详细查看:https://support.google.com/googleplay/android-developer/answer/9956427
32 | */
33 | public static final String MANAGE_EXTERNAL_STORAGE = "android.permission.MANAGE_EXTERNAL_STORAGE";
34 |
35 | /**
36 | * 安装应用权限(特殊权限,需要 Android 8.0 及以上)
37 | *
38 | * Android 11 特性调整,安装外部来源应用需要重启 App:https://cloud.tencent.com/developer/news/637591
39 | * 经过实践,Android 12 已经修复了此问题,授权或者取消授权后应用并不会重启
40 | */
41 | public static final String REQUEST_INSTALL_PACKAGES = "android.permission.REQUEST_INSTALL_PACKAGES";
42 |
43 | /**
44 | * 悬浮窗权限(特殊权限)
45 | *
46 | * 在 Android 10 及之前的版本能跳转到应用悬浮窗设置页面,而在 Android 11 及之后的版本只能跳转到系统设置悬浮窗管理列表了
47 | * 官方解释:https://developer.android.google.cn/reference/android/provider/Settings#ACTION_MANAGE_OVERLAY_PERMISSION
48 | */
49 | public static final String SYSTEM_ALERT_WINDOW = "android.permission.SYSTEM_ALERT_WINDOW";
50 |
51 | /** 系统设置权限(特殊权限) */
52 | public static final String WRITE_SETTINGS = "android.permission.WRITE_SETTINGS";
53 |
54 | /** 勿扰权限,可控制手机响铃模式(静音,震动)(特殊权限,Android 6.0 之后才有的权限)*/
55 | public static final String ACCESS_NOTIFICATION_POLICY = "android.permission.ACCESS_NOTIFICATION_POLICY";
56 |
57 | /** 通知栏监听权限(特殊权限,Android 4.3 之后才有的权限) */
58 | public static final String BIND_NOTIFICATION_LISTENER_SERVICE = "android.permission.BIND_NOTIFICATION_LISTENER_SERVICE";
59 |
60 | /** 查看应用使用情况权限,简称使用统计权限(特殊权限,Android 5.0 之后才有的权限) */
61 | public static final String PACKAGE_USAGE_STATS = "android.permission.PACKAGE_USAGE_STATS";
62 |
63 | /** 通知栏权限(特殊权限,注意此权限不需要在清单文件中注册也能申请) */
64 | public static final String NOTIFICATION_SERVICE = "android.permission.NOTIFICATION_SERVICE";
65 |
66 | /** 读取外部存储 */
67 | public static final String READ_EXTERNAL_STORAGE = "android.permission.READ_EXTERNAL_STORAGE";
68 | /** 写入外部存储 */
69 | public static final String WRITE_EXTERNAL_STORAGE = "android.permission.WRITE_EXTERNAL_STORAGE";
70 |
71 | /** 相机权限 */
72 | public static final String CAMERA = "android.permission.CAMERA";
73 |
74 | /** 麦克风权限 */
75 | public static final String RECORD_AUDIO = "android.permission.RECORD_AUDIO";
76 |
77 | /** 获取精确位置 */
78 | public static final String ACCESS_FINE_LOCATION = "android.permission.ACCESS_FINE_LOCATION";
79 | /** 获取粗略位置 */
80 | public static final String ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION";
81 | /**
82 | * 在后台获取位置(需要 Android 10.0 及以上)
83 | *
84 | * 需要注意的是:
85 | * 1. 一旦你申请了该权限,在授权的时候,需要选择 "始终允许",而不能选择 "仅在使用中允许"
86 | * 2. 如果你的 App 只在前台状态下使用定位功能,请不要申请该权限(后台定位权限)
87 | */
88 | public static final String ACCESS_BACKGROUND_LOCATION = "android.permission.ACCESS_BACKGROUND_LOCATION";
89 |
90 | /**
91 | * 蓝牙扫描权限(需要 Android 12.0 及以上)
92 | *
93 | * 为了兼容 Android 12 以下版本,需要清单文件中注册 {@link Manifest.permission#BLUETOOTH_ADMIN} 权限
94 | * 还有 Android 12 以下设备,获取蓝牙扫描结果需要模糊定位权限,框架会自动在旧的安卓设备上自动添加此权限进行动态申请
95 | */
96 | public static final String BLUETOOTH_SCAN = "android.permission.BLUETOOTH_SCAN";
97 | /**
98 | * 蓝牙连接权限(需要 Android 12.0 及以上)
99 | *
100 | * 为了兼容 Android 12 以下版本,需要在清单文件中注册 {@link Manifest.permission#BLUETOOTH} 权限
101 | */
102 | public static final String BLUETOOTH_CONNECT = "android.permission.BLUETOOTH_CONNECT";
103 | /**
104 | * 蓝牙广播权限(需要 Android 12.0 及以上)
105 | *
106 | * 将当前设备的蓝牙进行广播,供其他设备扫描时需要用到该权限
107 | * 为了兼容 Android 12 以下版本,需要在清单文件中注册 {@link Manifest.permission#BLUETOOTH_ADMIN} 权限
108 | */
109 | public static final String BLUETOOTH_ADVERTISE = "android.permission.BLUETOOTH_ADVERTISE";
110 |
111 | /** 读取联系人 */
112 | public static final String READ_CONTACTS = "android.permission.READ_CONTACTS";
113 | /** 修改联系人 */
114 | public static final String WRITE_CONTACTS = "android.permission.WRITE_CONTACTS";
115 | /** 访问账户列表 */
116 | public static final String GET_ACCOUNTS = "android.permission.GET_ACCOUNTS";
117 |
118 | /** 读取日历 */
119 | public static final String READ_CALENDAR = "android.permission.READ_CALENDAR";
120 | /** 修改日历 */
121 | public static final String WRITE_CALENDAR = "android.permission.WRITE_CALENDAR";
122 |
123 | /**
124 | * 读取照片中的地理位置(需要 Android 10.0 及以上)
125 | *
126 | * 需要注意的是:如果这个权限申请成功了但是不能正常读取照片的地理信息,那么需要先申请存储权限:
127 | *
128 | * 如果项目 targetSdkVersion <= 29 需要申请 {@link Permission.Group#STORAGE}
129 | * 如果项目 targetSdkVersion >= 30 需要申请 {@link Permission#MANAGE_EXTERNAL_STORAGE}
130 | */
131 | public static final String ACCESS_MEDIA_LOCATION = "android.permission.ACCESS_MEDIA_LOCATION";
132 |
133 | /**
134 | * 读取电话状态
135 | *
136 | * 需要注意的是:这个权限在某些手机上面是没办法获取到的,因为某些系统禁止应用获得该权限
137 | * 所以你要是申请了这个权限之后没有弹授权框,而是直接回调授权失败方法
138 | * 请不要惊慌,这个不是 Bug、不是 Bug、不是 Bug,而是正常现象
139 | *
140 | * 后续情况汇报:有人反馈在 iQOO 手机上面获取不到该权限,在清单文件加入下面这个权限就可以了(这里只是做记录,并不代表这种方式就一定有效果)
141 | *
142 | */
143 | public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE";
144 | /** 拨打电话 */
145 | public static final String CALL_PHONE = "android.permission.CALL_PHONE";
146 | /** 读取通话记录 */
147 | public static final String READ_CALL_LOG = "android.permission.READ_CALL_LOG";
148 | /** 修改通话记录 */
149 | public static final String WRITE_CALL_LOG = "android.permission.WRITE_CALL_LOG";
150 | /** 添加语音邮件 */
151 | public static final String ADD_VOICEMAIL = "com.android.voicemail.permission.ADD_VOICEMAIL";
152 | /** 使用SIP视频 */
153 | public static final String USE_SIP = "android.permission.USE_SIP";
154 | /**
155 | * 处理拨出电话
156 | *
157 | * @deprecated 在 Android 10 已经过时,请见:https://developer.android.google.cn/reference/android/Manifest.permission?hl=zh_cn#PROCESS_OUTGOING_CALLS
158 | */
159 | public static final String PROCESS_OUTGOING_CALLS = "android.permission.PROCESS_OUTGOING_CALLS";
160 | /**
161 | * 接听电话(需要 Android 8.0 及以上,Android 8.0 以下可以采用模拟耳机按键事件来实现接听电话,这种方式不需要权限)
162 | */
163 | public static final String ANSWER_PHONE_CALLS = "android.permission.ANSWER_PHONE_CALLS";
164 | /**
165 | * 读取手机号码(需要 Android 8.0 及以上)
166 | *
167 | * 为了兼容 Android 8.0 以下版本,需要在清单文件中注册 {@link Manifest.permission#READ_PHONE_STATE} 权限
168 | */
169 | public static final String READ_PHONE_NUMBERS = "android.permission.READ_PHONE_NUMBERS";
170 |
171 | /** 使用传感器 */
172 | public static final String BODY_SENSORS = "android.permission.BODY_SENSORS";
173 | /** 获取活动步数(需要 Android 10.0 及以上) */
174 | public static final String ACTIVITY_RECOGNITION = "android.permission.ACTIVITY_RECOGNITION";
175 |
176 | /** 发送短信 */
177 | public static final String SEND_SMS = "android.permission.SEND_SMS";
178 | /** 接收短信 */
179 | public static final String RECEIVE_SMS = "android.permission.RECEIVE_SMS";
180 | /** 读取短信 */
181 | public static final String READ_SMS = "android.permission.READ_SMS";
182 | /** 接收 WAP 推送消息 */
183 | public static final String RECEIVE_WAP_PUSH = "android.permission.RECEIVE_WAP_PUSH";
184 | /** 接收彩信 */
185 | public static final String RECEIVE_MMS = "android.permission.RECEIVE_MMS";
186 |
187 | /** 允许呼叫应用继续在另一个应用中启动的呼叫(需要 Android 9.0 及以上) */
188 | public static final String ACCEPT_HANDOVER = "android.permission.ACCEPT_HANDOVER";
189 |
190 | /**
191 | * 权限组
192 | */
193 | public static final class Group {
194 |
195 | /** 存储权限 */
196 | public static final String[] STORAGE = new String[] {
197 | Permission.READ_EXTERNAL_STORAGE,
198 | Permission.WRITE_EXTERNAL_STORAGE};
199 |
200 | /** 日历权限 */
201 | public static final String[] CALENDAR = new String[] {
202 | Permission.READ_CALENDAR,
203 | Permission.WRITE_CALENDAR};
204 |
205 | /** 联系人权限 */
206 | public static final String[] CONTACTS = new String[] {
207 | Permission.READ_CONTACTS,
208 | Permission.WRITE_CONTACTS,
209 | Permission.GET_ACCOUNTS};
210 |
211 | /** 传感器权限 */
212 | public static final String[] SENSORS = new String[] {
213 | Permission.BODY_SENSORS,
214 | Permission.ACTIVITY_RECOGNITION};
215 |
216 | /** 蓝牙权限 */
217 | public static final String[] BLUETOOTH = new String[] {
218 | Permission.BLUETOOTH_SCAN,
219 | Permission.BLUETOOTH_CONNECT,
220 | Permission.BLUETOOTH_ADVERTISE};
221 | }
222 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/ph/permissions/PermissionApi.java:
--------------------------------------------------------------------------------
1 | package com.ph.permissions;
2 |
3 | import android.app.Activity;
4 | import android.app.AlarmManager;
5 | import android.app.AppOpsManager;
6 | import android.app.NotificationManager;
7 | import android.content.Context;
8 | import android.content.pm.PackageManager;
9 | import android.os.Environment;
10 | import android.provider.Settings;
11 | import android.support.v4.app.NotificationManagerCompat;
12 |
13 | import java.util.ArrayList;
14 | import java.util.List;
15 | import java.util.Set;
16 |
17 | /**
18 | * author : i小灰
19 | * github : https://github.com/dahui888/HPermissions
20 | * desc : 权限判断类
21 | */
22 | final class PermissionApi {
23 |
24 | /**
25 | * 是否有存储权限
26 | */
27 | static boolean isGrantedStoragePermission(Context context) {
28 | if (AndroidVersion.isAndroid11()) {
29 | return Environment.isExternalStorageManager();
30 | }
31 | return isGrantedPermissions(context, PermissionUtils.asArrayList(Permission.Group.STORAGE));
32 | }
33 |
34 | /**
35 | * 是否有安装权限
36 | */
37 | static boolean isGrantedInstallPermission(Context context) {
38 | if (AndroidVersion.isAndroid8()) {
39 | return context.getPackageManager().canRequestPackageInstalls();
40 | }
41 | return true;
42 | }
43 |
44 | /**
45 | * 是否有悬浮窗权限
46 | */
47 | static boolean isGrantedWindowPermission(Context context) {
48 | if (AndroidVersion.isAndroid6()) {
49 | return Settings.canDrawOverlays(context);
50 | }
51 | return true;
52 | }
53 |
54 | /**
55 | * 是否有系统设置权限
56 | */
57 | static boolean isGrantedSettingPermission(Context context) {
58 | if (AndroidVersion.isAndroid6()) {
59 | return Settings.System.canWrite(context);
60 | }
61 | return true;
62 | }
63 |
64 | /**
65 | * 是否有通知栏权限
66 | */
67 | static boolean isGrantedNotifyPermission(Context context) {
68 | return NotificationManagerCompat.from(context).areNotificationsEnabled();
69 | }
70 |
71 | /**
72 | * 是否通知栏监听的权限
73 | */
74 | static boolean isGrantedNotificationListenerPermission(Context context) {
75 | if (AndroidVersion.isAndroid4_3()) {
76 | Set packageNames = NotificationManagerCompat.getEnabledListenerPackages(context);
77 | return packageNames.contains(context.getPackageName());
78 | }
79 | return true;
80 | }
81 |
82 | /**
83 | * 是否有使用统计权限
84 | */
85 | static boolean isGrantedPackagePermission(Context context) {
86 | if (AndroidVersion.isAndroid5()) {
87 | AppOpsManager appOps = (AppOpsManager)
88 | context.getSystemService(Context.APP_OPS_SERVICE);
89 | int mode;
90 | if (AndroidVersion.isAndroid10()) {
91 | mode = appOps.unsafeCheckOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS,
92 | context.getApplicationInfo().uid, context.getPackageName());
93 | } else {
94 | mode = appOps.checkOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS,
95 | context.getApplicationInfo().uid, context.getPackageName());
96 | }
97 | return mode == AppOpsManager.MODE_ALLOWED;
98 | }
99 | return true;
100 | }
101 |
102 | /**
103 | * 是否有闹钟权限
104 | */
105 | static boolean isGrantedAlarmPermission(Context context) {
106 | if (AndroidVersion.isAndroid12()) {
107 | return context.getSystemService(AlarmManager.class).canScheduleExactAlarms();
108 | }
109 | return true;
110 | }
111 |
112 | /**
113 | * 是否有勿扰模式权限
114 | */
115 | static boolean isGrantedNotDisturbPermission(Context context) {
116 | if (AndroidVersion.isAndroid6()) {
117 | return context.getSystemService(NotificationManager.class).isNotificationPolicyAccessGranted();
118 | }
119 | return true;
120 | }
121 |
122 | /**
123 | * 判断某个权限集合是否包含特殊权限
124 | */
125 | static boolean containsSpecialPermission(List permissions) {
126 | if (permissions == null || permissions.isEmpty()) {
127 | return false;
128 | }
129 |
130 | for (String permission : permissions) {
131 | if (isSpecialPermission(permission)) {
132 | return true;
133 | }
134 | }
135 | return false;
136 | }
137 |
138 | /**
139 | * 判断某个权限是否是特殊权限
140 | */
141 | static boolean isSpecialPermission(String permission) {
142 | return Permission.MANAGE_EXTERNAL_STORAGE.equals(permission) ||
143 | Permission.REQUEST_INSTALL_PACKAGES.equals(permission) ||
144 | Permission.SYSTEM_ALERT_WINDOW.equals(permission) ||
145 | Permission.WRITE_SETTINGS.equals(permission) ||
146 | Permission.NOTIFICATION_SERVICE.equals(permission) ||
147 | Permission.PACKAGE_USAGE_STATS.equals(permission) ||
148 | Permission.SCHEDULE_EXACT_ALARM.equals(permission) ||
149 | Permission.BIND_NOTIFICATION_LISTENER_SERVICE.equals(permission) ||
150 | Permission.ACCESS_NOTIFICATION_POLICY.equals(permission);
151 | }
152 |
153 | /**
154 | * 判断某些权限是否全部被授予
155 | */
156 | static boolean isGrantedPermissions(Context context, List permissions) {
157 | if (permissions == null || permissions.isEmpty()) {
158 | return false;
159 | }
160 |
161 | for (String permission : permissions) {
162 | if (!isGrantedPermission(context, permission)) {
163 | return false;
164 | }
165 | }
166 |
167 | return true;
168 | }
169 |
170 | /**
171 | * 获取已经授予的权限
172 | */
173 | static List getGrantedPermissions(Context context, List permissions) {
174 | List grantedPermission = new ArrayList<>(permissions.size());
175 | for (String permission : permissions) {
176 | if (isGrantedPermission(context, permission)) {
177 | grantedPermission.add(permission);
178 | }
179 | }
180 | return grantedPermission;
181 | }
182 |
183 | /**
184 | * 获取已经拒绝的权限
185 | */
186 | static List getDeniedPermissions(Context context, List permissions) {
187 | List deniedPermission = new ArrayList<>(permissions.size());
188 | for (String permission : permissions) {
189 | if (!isGrantedPermission(context, permission)) {
190 | deniedPermission.add(permission);
191 | }
192 | }
193 | return deniedPermission;
194 | }
195 |
196 | /**
197 | * 判断某个权限是否授予
198 | */
199 | static boolean isGrantedPermission(Context context, String permission) {
200 | // 检测通知栏权限
201 | if (Permission.NOTIFICATION_SERVICE.equals(permission)) {
202 | return isGrantedNotifyPermission(context);
203 | }
204 |
205 | // 检测获取使用统计权限
206 | if (Permission.PACKAGE_USAGE_STATS.equals(permission)) {
207 | return isGrantedPackagePermission(context);
208 | }
209 |
210 | // 检测通知栏监听权限
211 | if (Permission.BIND_NOTIFICATION_LISTENER_SERVICE.equals(permission)) {
212 | return isGrantedNotificationListenerPermission(context);
213 | }
214 |
215 | // 其他权限在 Android 6.0 以下版本就默认授予
216 | if (!AndroidVersion.isAndroid6()) {
217 | return true;
218 | }
219 |
220 | // 检测存储权限
221 | if (Permission.MANAGE_EXTERNAL_STORAGE.equals(permission)) {
222 | return isGrantedStoragePermission(context);
223 | }
224 |
225 | // 检测安装权限
226 | if (Permission.REQUEST_INSTALL_PACKAGES.equals(permission)) {
227 | return isGrantedInstallPermission(context);
228 | }
229 |
230 | // 检测悬浮窗权限
231 | if (Permission.SYSTEM_ALERT_WINDOW.equals(permission)) {
232 | return isGrantedWindowPermission(context);
233 | }
234 |
235 | // 检测系统权限
236 | if (Permission.WRITE_SETTINGS.equals(permission)) {
237 | return isGrantedSettingPermission(context);
238 | }
239 |
240 | // 检测闹钟权限
241 | if (Permission.SCHEDULE_EXACT_ALARM.equals(permission)) {
242 | return isGrantedAlarmPermission(context);
243 | }
244 |
245 | // 检测勿扰权限
246 | if (Permission.ACCESS_NOTIFICATION_POLICY.equals(permission)) {
247 | return isGrantedNotDisturbPermission(context);
248 | }
249 |
250 | // 检测 Android 12 的三个新权限
251 | if (!AndroidVersion.isAndroid12()) {
252 |
253 | if (Permission.BLUETOOTH_SCAN.equals(permission)) {
254 | return context.checkSelfPermission(Permission.ACCESS_COARSE_LOCATION) ==
255 | PackageManager.PERMISSION_GRANTED;
256 | }
257 |
258 | if (Permission.BLUETOOTH_CONNECT.equals(permission) ||
259 | Permission.BLUETOOTH_ADVERTISE.equals(permission)) {
260 | return true;
261 | }
262 | }
263 |
264 | // 检测 Android 10 的三个新权限
265 | if (!AndroidVersion.isAndroid10()) {
266 |
267 | if (Permission.ACCESS_BACKGROUND_LOCATION.equals(permission)) {
268 | return context.checkSelfPermission(Permission.ACCESS_FINE_LOCATION) ==
269 | PackageManager.PERMISSION_GRANTED;
270 | }
271 |
272 | if (Permission.ACTIVITY_RECOGNITION.equals(permission)) {
273 | return context.checkSelfPermission(Permission.BODY_SENSORS) ==
274 | PackageManager.PERMISSION_GRANTED;
275 | }
276 |
277 | if (Permission.ACCESS_MEDIA_LOCATION.equals(permission)) {
278 | return true;
279 | }
280 | }
281 |
282 | // 检测 Android 9.0 的一个新权限
283 | if (!AndroidVersion.isAndroid9()) {
284 |
285 | if (Permission.ACCEPT_HANDOVER.equals(permission)) {
286 | return true;
287 | }
288 | }
289 |
290 | // 检测 Android 8.0 的两个新权限
291 | if (!AndroidVersion.isAndroid8()) {
292 |
293 | if (Permission.ANSWER_PHONE_CALLS.equals(permission)) {
294 | return true;
295 | }
296 |
297 | if (Permission.READ_PHONE_NUMBERS.equals(permission)) {
298 | return context.checkSelfPermission(Permission.READ_PHONE_STATE) ==
299 | PackageManager.PERMISSION_GRANTED;
300 | }
301 | }
302 |
303 | return context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED;
304 | }
305 |
306 | /**
307 | * 在权限组中检查是否有某个权限是否被永久拒绝
308 | *
309 | * @param activity Activity对象
310 | * @param permissions 请求的权限
311 | */
312 | static boolean isPermissionPermanentDenied(Activity activity, List permissions) {
313 | for (String permission : permissions) {
314 | if (isPermissionPermanentDenied(activity, permission)) {
315 | return true;
316 | }
317 | }
318 | return false;
319 | }
320 |
321 | /**
322 | * 判断某个权限是否被永久拒绝
323 | *
324 | * @param activity Activity对象
325 | * @param permission 请求的权限
326 | */
327 | static boolean isPermissionPermanentDenied(Activity activity, String permission) {
328 | // 特殊权限不算,本身申请方式和危险权限申请方式不同,因为没有永久拒绝的选项,所以这里返回 false
329 | if (isSpecialPermission(permission)) {
330 | return false;
331 | }
332 |
333 | if (!AndroidVersion.isAndroid6()) {
334 | return false;
335 | }
336 |
337 | // 检测 Android 12 的三个新权限
338 | if (!AndroidVersion.isAndroid12()) {
339 |
340 | if (Permission.BLUETOOTH_SCAN.equals(permission)) {
341 | return !isGrantedPermission(activity, Permission.ACCESS_COARSE_LOCATION) &&
342 | !activity.shouldShowRequestPermissionRationale(Permission.ACCESS_COARSE_LOCATION);
343 | }
344 |
345 | if (Permission.BLUETOOTH_CONNECT.equals(permission) ||
346 | Permission.BLUETOOTH_ADVERTISE.equals(permission)) {
347 | return false;
348 | }
349 | }
350 |
351 | if (AndroidVersion.isAndroid10()) {
352 |
353 | // 重新检测后台定位权限是否永久拒绝
354 | if (Permission.ACCESS_BACKGROUND_LOCATION.equals(permission) &&
355 | !isGrantedPermission(activity, Permission.ACCESS_BACKGROUND_LOCATION) &&
356 | !isGrantedPermission(activity, Permission.ACCESS_FINE_LOCATION)) {
357 | return !activity.shouldShowRequestPermissionRationale(Permission.ACCESS_FINE_LOCATION);
358 | }
359 | }
360 |
361 | // 检测 Android 10 的三个新权限
362 | if (!AndroidVersion.isAndroid10()) {
363 |
364 | if (Permission.ACCESS_BACKGROUND_LOCATION.equals(permission)) {
365 | return !isGrantedPermission(activity, Permission.ACCESS_FINE_LOCATION) &&
366 | !activity.shouldShowRequestPermissionRationale(Permission.ACCESS_FINE_LOCATION);
367 | }
368 |
369 | if (Permission.ACTIVITY_RECOGNITION.equals(permission)) {
370 | return !isGrantedPermission(activity, Permission.BODY_SENSORS) &&
371 | !activity.shouldShowRequestPermissionRationale(Permission.BODY_SENSORS);
372 | }
373 |
374 | if (Permission.ACCESS_MEDIA_LOCATION.equals(permission)) {
375 | return false;
376 | }
377 | }
378 |
379 | // 检测 Android 9.0 的一个新权限
380 | if (!AndroidVersion.isAndroid9()) {
381 |
382 | if (Permission.ACCEPT_HANDOVER.equals(permission)) {
383 | return false;
384 | }
385 | }
386 |
387 | // 检测 Android 8.0 的两个新权限
388 | if (!AndroidVersion.isAndroid8()) {
389 |
390 | if (Permission.ANSWER_PHONE_CALLS.equals(permission)) {
391 | return false;
392 | }
393 |
394 | if (Permission.READ_PHONE_NUMBERS.equals(permission)) {
395 | return !isGrantedPermission(activity, Permission.READ_PHONE_STATE) &&
396 | !activity.shouldShowRequestPermissionRationale(Permission.READ_PHONE_STATE);
397 | }
398 | }
399 |
400 | return !isGrantedPermission(activity, permission) &&
401 | !activity.shouldShowRequestPermissionRationale(permission);
402 | }
403 |
404 | /**
405 | * 获取没有授予的权限
406 | *
407 | * @param permissions 需要请求的权限组
408 | * @param grantResults 允许结果组
409 | */
410 | static List getDeniedPermissions(List permissions, int[] grantResults) {
411 | List deniedPermissions = new ArrayList<>();
412 | for (int i = 0; i < grantResults.length; i++) {
413 | // 把没有授予过的权限加入到集合中
414 | if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
415 | deniedPermissions.add(permissions.get(i));
416 | }
417 | }
418 | return deniedPermissions;
419 | }
420 |
421 | /**
422 | * 获取已授予的权限
423 | *
424 | * @param permissions 需要请求的权限组
425 | * @param grantResults 允许结果组
426 | */
427 | static List getGrantedPermissions(List permissions, int[] grantResults) {
428 | List grantedPermissions = new ArrayList<>();
429 | for (int i = 0; i < grantResults.length; i++) {
430 | // 把授予过的权限加入到集合中
431 | if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
432 | grantedPermissions.add(permissions.get(i));
433 | }
434 | }
435 | return grantedPermissions;
436 | }
437 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/ph/permissions/PermissionChecker.java:
--------------------------------------------------------------------------------
1 | package com.ph.permissions;
2 |
3 | import android.Manifest;
4 | import android.app.Activity;
5 | import android.content.Context;
6 | import android.content.res.XmlResourceParser;
7 | import android.os.Build;
8 |
9 | import org.xmlpull.v1.XmlPullParserException;
10 |
11 | import java.io.IOException;
12 | import java.lang.reflect.Field;
13 | import java.util.ArrayList;
14 | import java.util.HashMap;
15 | import java.util.List;
16 |
17 | /**
18 | * author : i小灰
19 | * github : https://github.com/dahui888/HPermissions
20 | * desc : 权限错误检测类
21 | */
22 | final class PermissionChecker {
23 |
24 | /**
25 | * 检查 Activity 的状态是否正常
26 | *
27 | * @param checkMode 是否是检查模式
28 | * @return 是否检查通过
29 | */
30 | static boolean checkActivityStatus(Activity activity, boolean checkMode) {
31 | // 检查当前 Activity 状态是否是正常的,如果不是则不请求权限
32 | if (activity == null) {
33 | if (checkMode) {
34 | // Context 的实例必须是 Activity 对象
35 | throw new IllegalArgumentException("The instance of the context must be an activity object");
36 | }
37 | return false;
38 | }
39 |
40 | if (activity.isFinishing()) {
41 | if (checkMode) {
42 | // 这个 Activity 对象当前不能是关闭状态,这种情况常出现在执行异步请求后申请权限
43 | // 请自行在外层判断 Activity 状态是否正常之后再进入权限申请
44 | throw new IllegalStateException("The activity has been finishing, " +
45 | "please manually determine the status of the activity");
46 | }
47 | return false;
48 | }
49 |
50 | if (Build.VERSION.SDK_INT >= AndroidVersion.ANDROID_4_2 && activity.isDestroyed()) {
51 | if (checkMode) {
52 | // 这个 Activity 对象当前不能是销毁状态,这种情况常出现在执行异步请求后申请权限
53 | // 请自行在外层判断 Activity 状态是否正常之后再进入权限申请
54 | throw new IllegalStateException("The activity has been destroyed, " +
55 | "please manually determine the status of the activity");
56 | }
57 | return false;
58 | }
59 |
60 | return true;
61 | }
62 |
63 | /**
64 | * 检查传入的权限是否符合要求
65 | *
66 | * @param requestPermissions 请求的权限组
67 | * @param checkMode 是否是检查模式
68 | * @return 是否检查通过
69 | */
70 | static boolean checkPermissionArgument(List requestPermissions, boolean checkMode) {
71 | if (requestPermissions == null || requestPermissions.isEmpty()) {
72 | if (checkMode) {
73 | // 不传任何权限,就想动态申请权限?
74 | throw new IllegalArgumentException("The requested permission cannot be empty");
75 | }
76 | return false;
77 | }
78 |
79 | if (Build.VERSION.SDK_INT > AndroidVersion.ANDROID_12_L) {
80 | // 如果是 Android 12L 后面的版本,则不进行检查
81 | return true;
82 | }
83 |
84 | if (checkMode) {
85 | List allPermissions = new ArrayList<>();
86 | Field[] fields = Permission.class.getDeclaredFields();
87 | // 在开启代码混淆之后,反射 Permission 类中的字段会得到空的字段数组
88 | // 这个是因为编译后常量会在代码中直接引用,所以 Permission 常量字段在混淆的时候会被移除掉
89 | if (fields.length == 0) {
90 | return true;
91 | }
92 | for (Field field : fields) {
93 | if (!String.class.equals(field.getType())) {
94 | continue;
95 | }
96 | try {
97 | allPermissions.add((String) field.get(null));
98 | } catch (IllegalAccessException e) {
99 | e.printStackTrace();
100 | }
101 | }
102 | for (String permission : requestPermissions) {
103 | if (!allPermissions.contains(permission)) {
104 | // 请不要申请危险权限和特殊权限之外的权限
105 | throw new IllegalArgumentException("The " + permission +
106 | " is not a dangerous permission or special permission, " +
107 | "please do not apply dynamically");
108 | }
109 | }
110 | }
111 | return true;
112 | }
113 |
114 | /**
115 | * 检查存储权限
116 | *
117 | * @param requestPermissions 请求的权限组
118 | */
119 | static void checkStoragePermission(Context context, List requestPermissions) {
120 | // 如果请求的权限中没有包含外部存储相关的权限,那么就直接返回
121 | if (!requestPermissions.contains(Permission.MANAGE_EXTERNAL_STORAGE) &&
122 | !requestPermissions.contains(Permission.READ_EXTERNAL_STORAGE) &&
123 | !requestPermissions.contains(Permission.WRITE_EXTERNAL_STORAGE)) {
124 | return;
125 | }
126 |
127 | // 是否适配了分区存储
128 | boolean scopedStorage = PermissionUtils.isScopedStorage(context);
129 |
130 | XmlResourceParser parser = PermissionUtils.parseAndroidManifest(context);
131 | if (parser == null) {
132 | return;
133 | }
134 |
135 | try {
136 |
137 | do {
138 | // 当前节点必须为标签头部
139 | if (parser.getEventType() != XmlResourceParser.START_TAG) {
140 | continue;
141 | }
142 |
143 | // 当前标签必须为 application
144 | if (!"application".equals(parser.getName())) {
145 | continue;
146 | }
147 |
148 | int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
149 |
150 | boolean requestLegacyExternalStorage = parser.getAttributeBooleanValue(PermissionUtils.getAndroidNamespace(),
151 | "requestLegacyExternalStorage", false);
152 | // 如果在已经适配 Android 10 的情况下
153 | if (targetSdkVersion >= AndroidVersion.ANDROID_10 && !requestLegacyExternalStorage &&
154 | (requestPermissions.contains(Permission.MANAGE_EXTERNAL_STORAGE) || !scopedStorage)) {
155 | // 请在清单文件 Application 节点中注册 android:requestLegacyExternalStorage="true" 属性
156 | // 否则就算申请了权限,也无法在 Android 10 的设备上正常读写外部存储上的文件
157 | // 如果你的项目已经全面适配了分区存储,请在清单文件中注册一个 meta-data 属性
158 | // 来跳过该检查
159 | throw new IllegalStateException("Please register the android:requestLegacyExternalStorage=\"true\" " +
160 | "attribute in the AndroidManifest.xml file, otherwise it will cause incompatibility with the old version");
161 | }
162 |
163 | // 如果在已经适配 Android 11 的情况下
164 | if (targetSdkVersion >= AndroidVersion.ANDROID_11 &&
165 | !requestPermissions.contains(Permission.MANAGE_EXTERNAL_STORAGE) && !scopedStorage) {
166 | // 1. 适配分区存储的特性,并在清单文件中注册一个 meta-data 属性
167 | //
168 | // 2. 如果不想适配分区存储,则需要使用 Permission.MANAGE_EXTERNAL_STORAGE 来申请权限
169 | // 上面两种方式需要二选一,否则无法在 Android 11 的设备上正常读写外部存储上的文件
170 | throw new IllegalArgumentException("The storage permission application is abnormal. If you have adapted the scope storage, " +
171 | "please register the attribute in the AndroidManifest.xml file. " +
172 | "If there is no adaptation scope storage, please use MANAGE_EXTERNAL_STORAGE to apply for permission");
173 | }
174 |
175 | // 终止循环
176 | break;
177 |
178 | } while (parser.next() != XmlResourceParser.END_DOCUMENT);
179 |
180 | } catch (IOException | XmlPullParserException e) {
181 | e.printStackTrace();
182 | } finally {
183 | parser.close();
184 | }
185 | }
186 |
187 | /**
188 | * 检查定位权限
189 | *
190 | * @param requestPermissions 请求的权限组
191 | */
192 | static void checkLocationPermission(Context context, List requestPermissions) {
193 | if (context.getApplicationInfo().targetSdkVersion >= AndroidVersion.ANDROID_12) {
194 | if (requestPermissions.contains(Permission.ACCESS_FINE_LOCATION) &&
195 | !requestPermissions.contains(Permission.ACCESS_COARSE_LOCATION) ) {
196 | // 如果您的应用以 Android 12 为目标平台并且您请求 ACCESS_FINE_LOCATION 权限
197 | // 则还必须请求 ACCESS_COARSE_LOCATION 权限。您必须在单个运行时请求中包含这两项权限
198 | // 如果您尝试仅请求 ACCESS_FINE_LOCATION,则系统会忽略该请求并在 Logcat 中记录以下错误消息:
199 | // ACCESS_FINE_LOCATION must be requested with ACCESS_COARSE_LOCATION
200 | // 官方适配文档:https://developer.android.google.cn/about/versions/12/approximate-location
201 | throw new IllegalArgumentException("If your app targets Android 12 or higher " +
202 | "and requests the ACCESS_FINE_LOCATION runtime permission, " +
203 | "you must also request the ACCESS_COARSE_LOCATION permission. " +
204 | "You must include both permissions in a single runtime request.");
205 | }
206 | }
207 |
208 | // 判断是否包含后台定位权限
209 | if (!requestPermissions.contains(Permission.ACCESS_BACKGROUND_LOCATION)) {
210 | return;
211 | }
212 |
213 | if (requestPermissions.contains(Permission.ACCESS_COARSE_LOCATION) &&
214 | !requestPermissions.contains(Permission.ACCESS_FINE_LOCATION)) {
215 | // 申请后台定位权限可以不包含模糊定位权限,但是一定要包含精确定位权限,否则后台定位权限会无法申请
216 | // 也就是会导致无法弹出授权弹窗,经过实践,在 Android 12 上这个问题已经被解决了
217 | // 但是为了兼容 Android 12 以下的设备还是要那么做,否则在 Android 11 及以下设备会出现异常
218 | throw new IllegalArgumentException("The application for background location permissions " +
219 | "must include precise location permissions");
220 | }
221 |
222 | for (String permission : requestPermissions) {
223 | if (Permission.ACCESS_FINE_LOCATION.equals(permission)
224 | || Permission.ACCESS_COARSE_LOCATION.equals(permission)
225 | || Permission.ACCESS_BACKGROUND_LOCATION.equals(permission)) {
226 | continue;
227 | }
228 |
229 | // 因为包含了后台定位权限,所以请不要申请和定位无关的权限,因为在 Android 11 上面,后台定位权限不能和其他非定位的权限一起申请
230 | // 否则会出现只申请了后台定位权限,其他权限会被回绝掉的情况,因为在 Android 11 上面,后台定位权限是要跳 Activity,并非弹 Dialog
231 | // 另外如果你的应用没有后台定位的需求,请不要一同申请 Permission.ACCESS_BACKGROUND_LOCATION 权限
232 | throw new IllegalArgumentException("Because it includes background location permissions, " +
233 | "do not apply for permissions unrelated to location");
234 | }
235 | }
236 |
237 | /**
238 | * 检查targetSdkVersion 是否符合要求
239 | *
240 | * @param requestPermissions 请求的权限组
241 | */
242 | static void checkTargetSdkVersion(Context context, List requestPermissions) {
243 | // targetSdk 最低版本要求
244 | int targetSdkMinVersion;
245 | if (requestPermissions.contains(Permission.BLUETOOTH_SCAN) ||
246 | requestPermissions.contains(Permission.BLUETOOTH_CONNECT) ||
247 | requestPermissions.contains(Permission.BLUETOOTH_ADVERTISE) ||
248 | requestPermissions.contains(Permission.SCHEDULE_EXACT_ALARM)) {
249 | targetSdkMinVersion = AndroidVersion.ANDROID_12;
250 | } else if (requestPermissions.contains(Permission.MANAGE_EXTERNAL_STORAGE)) {
251 | // 必须设置 targetSdkVersion >= 30 才能正常检测权限,否则请使用 Permission.Group.STORAGE 来申请存储权限
252 | targetSdkMinVersion = AndroidVersion.ANDROID_11;
253 | } else if (requestPermissions.contains(Permission.ACCESS_BACKGROUND_LOCATION) ||
254 | requestPermissions.contains(Permission.ACTIVITY_RECOGNITION) ||
255 | requestPermissions.contains(Permission.ACCESS_MEDIA_LOCATION)) {
256 | targetSdkMinVersion = AndroidVersion.ANDROID_10;
257 | } else if (requestPermissions.contains(Permission.ACCEPT_HANDOVER)) {
258 | targetSdkMinVersion = AndroidVersion.ANDROID_9;
259 | } else if (requestPermissions.contains(Permission.REQUEST_INSTALL_PACKAGES) ||
260 | requestPermissions.contains(Permission.ANSWER_PHONE_CALLS) ||
261 | requestPermissions.contains(Permission.READ_PHONE_NUMBERS)) {
262 | targetSdkMinVersion = AndroidVersion.ANDROID_8;
263 | } else {
264 | targetSdkMinVersion = AndroidVersion.ANDROID_6;
265 | }
266 |
267 | // 必须设置正确的 targetSdkVersion 才能正常检测权限
268 | if (context.getApplicationInfo().targetSdkVersion < targetSdkMinVersion) {
269 | throw new RuntimeException("The targetSdkVersion SDK must be " + targetSdkMinVersion +
270 | " or more, if you do not want to upgrade targetSdkVersion, " +
271 | "please apply with the old permissions");
272 | }
273 | }
274 |
275 | /**
276 | * 检查清单文件中所注册的权限是否正常
277 | *
278 | * @param requestPermissions 请求的权限组
279 | */
280 | static void checkManifestPermissions(Context context, List requestPermissions) {
281 | HashMap manifestPermissions = PermissionUtils.getManifestPermissions(context);
282 | if (manifestPermissions.isEmpty()) {
283 | throw new IllegalStateException("No permissions are registered in the AndroidManifest.xml file");
284 | }
285 |
286 | int minSdkVersion = Build.VERSION.SDK_INT >= AndroidVersion.ANDROID_7 ?
287 | context.getApplicationInfo().minSdkVersion : AndroidVersion.ANDROID_6;
288 |
289 | for (String permission : requestPermissions) {
290 |
291 | if (Permission.NOTIFICATION_SERVICE.equals(permission)) {
292 | // 不检测通知栏权限有没有在清单文件中注册,因为这个权限是框架虚拟出来的,有没有在清单文件中注册都没关系
293 | continue;
294 | }
295 |
296 | if (Permission.BIND_NOTIFICATION_LISTENER_SERVICE.equals(permission)) {
297 | // 不检测通知栏监听权限有没有在清单文件中注册,因为这个权限是需要直接注册在清单文件中的 service 节点上面的
298 | continue;
299 | }
300 |
301 | if (minSdkVersion < AndroidVersion.ANDROID_12) {
302 |
303 | if (Permission.BLUETOOTH_SCAN.equals(permission)) {
304 | checkManifestPermission(manifestPermissions, Manifest.permission.BLUETOOTH_ADMIN, AndroidVersion.ANDROID_11);
305 | // 这是 Android 12 之前遗留的问题,获取扫描蓝牙的结果需要定位的权限
306 | checkManifestPermission(manifestPermissions, Manifest.permission.ACCESS_COARSE_LOCATION, AndroidVersion.ANDROID_11);
307 | }
308 |
309 | if (Permission.BLUETOOTH_CONNECT.equals(permission)) {
310 | checkManifestPermission(manifestPermissions, Manifest.permission.BLUETOOTH, AndroidVersion.ANDROID_11);
311 | }
312 |
313 | if (Permission.BLUETOOTH_ADVERTISE.equals(permission)) {
314 | checkManifestPermission(manifestPermissions, Manifest.permission.BLUETOOTH_ADMIN, AndroidVersion.ANDROID_11);
315 | }
316 | }
317 |
318 | if (minSdkVersion < AndroidVersion.ANDROID_11) {
319 |
320 | if (Permission.MANAGE_EXTERNAL_STORAGE.equals(permission)) {
321 | checkManifestPermission(manifestPermissions, Permission.READ_EXTERNAL_STORAGE, AndroidVersion.ANDROID_10);
322 | checkManifestPermission(manifestPermissions, Permission.WRITE_EXTERNAL_STORAGE, AndroidVersion.ANDROID_10);
323 | }
324 | }
325 |
326 | if (minSdkVersion < AndroidVersion.ANDROID_10) {
327 |
328 | if (Permission.ACTIVITY_RECOGNITION.equals(permission)) {
329 | checkManifestPermission(manifestPermissions, Permission.BODY_SENSORS, AndroidVersion.ANDROID_8);
330 | }
331 | }
332 |
333 | if (minSdkVersion < AndroidVersion.ANDROID_8) {
334 |
335 | if (Permission.READ_PHONE_NUMBERS.equals(permission)) {
336 | checkManifestPermission(manifestPermissions, Permission.READ_PHONE_STATE, AndroidVersion.ANDROID_7_1);
337 | }
338 | }
339 |
340 | checkManifestPermission(manifestPermissions, permission, Integer.MAX_VALUE);
341 | }
342 | }
343 |
344 | /**
345 | * 检查某个权限注册是否正常,如果是则会抛出异常
346 | *
347 | * @param manifestPermissions 清单权限组
348 | * @param checkPermission 被检查的权限
349 | * @param maxSdkVersion 最低要求的 maxSdkVersion
350 | */
351 | static void checkManifestPermission(HashMap manifestPermissions,
352 | String checkPermission, int maxSdkVersion) {
353 | if (!manifestPermissions.containsKey(checkPermission)) {
354 | // 动态申请的权限没有在清单文件中注册,分为以下两种情况:
355 | // 1. 如果你的项目没有在清单文件中注册这个权限,请直接在清单文件中注册一下即可
356 | // 2. 如果你的项目明明已注册这个权限,可以检查一下编译完成的 apk 包中是否包含该权限,如果里面没有,证明框架的判断是没有问题的
357 | // 一般是第三方 sdk 或者框架在清单文件中注册了 导致的
358 | // 解决方式也很简单,通过在项目中注册 即可替换掉原先的配置
359 | throw new IllegalStateException("Please register permissions in the AndroidManifest.xml file " +
360 | "");
361 | }
362 |
363 | Integer manifestMaxSdkVersion = manifestPermissions.get(checkPermission);
364 | if (manifestMaxSdkVersion == null) {
365 | return;
366 | }
367 |
368 | if (manifestMaxSdkVersion < maxSdkVersion) {
369 | // 清单文件中所注册的权限 maxSdkVersion 大小不符合最低要求,分为以下两种情况:
370 | // 1. 如果你的项目中注册了该属性,请根据报错提示修改 maxSdkVersion 属性值或者删除 maxSdkVersion 属性
371 | // 2. 如果你明明没有注册过 maxSdkVersion 属性,可以检查一下编译完成的 apk 包中是否有该属性,如果里面存在,证明框架的判断是没有问题的
372 | // 一般是第三方 sdk 或者框架在清单文件中注册了 导致的
373 | // 解决方式也很简单,通过在项目中注册 即可替换掉原先的配置
374 | throw new IllegalArgumentException("The AndroidManifest.xml file " +
375 | " does not meet the requirements, " +
378 | (maxSdkVersion != Integer.MAX_VALUE ?
379 | "the minimum requirement for maxSdkVersion is " + maxSdkVersion :
380 | "please delete the android:maxSdkVersion=\"" + manifestMaxSdkVersion + "\" attribute"));
381 | }
382 | }
383 |
384 | /**
385 | * 处理和优化已经过时的权限
386 | *
387 | * @param requestPermissions 请求的权限组
388 | */
389 | static void optimizeDeprecatedPermission(List requestPermissions) {
390 | // 如果本次申请包含了 Android 12 蓝牙扫描权限
391 | if (!AndroidVersion.isAndroid12() &&
392 | requestPermissions.contains(Permission.BLUETOOTH_SCAN) &&
393 | !requestPermissions.contains(Permission.ACCESS_COARSE_LOCATION)) {
394 | // 这是 Android 12 之前遗留的问题,扫描蓝牙需要定位的权限
395 | requestPermissions.add(Permission.ACCESS_COARSE_LOCATION);
396 | }
397 |
398 | // 如果本次申请包含了 Android 11 存储权限
399 | if (requestPermissions.contains(Permission.MANAGE_EXTERNAL_STORAGE)) {
400 |
401 | if (requestPermissions.contains(Permission.READ_EXTERNAL_STORAGE) ||
402 | requestPermissions.contains(Permission.WRITE_EXTERNAL_STORAGE)) {
403 | // 检测是否有旧版的存储权限,有的话直接抛出异常,请不要自己动态申请这两个权限
404 | throw new IllegalArgumentException("If you have applied for MANAGE_EXTERNAL_STORAGE permissions, " +
405 | "do not apply for the READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE permissions");
406 | }
407 |
408 | if (!AndroidVersion.isAndroid11()) {
409 | // 自动添加旧版的存储权限,因为旧版的系统不支持申请新版的存储权限
410 | requestPermissions.add(Permission.READ_EXTERNAL_STORAGE);
411 | requestPermissions.add(Permission.WRITE_EXTERNAL_STORAGE);
412 | }
413 | }
414 |
415 | if (!AndroidVersion.isAndroid8() &&
416 | requestPermissions.contains(Permission.READ_PHONE_NUMBERS) &&
417 | !requestPermissions.contains(Permission.READ_PHONE_STATE)) {
418 | // 自动添加旧版的读取电话号码权限,因为旧版的系统不支持申请新版的权限
419 | requestPermissions.add(Permission.READ_PHONE_STATE);
420 | }
421 |
422 | if (!AndroidVersion.isAndroid10() &&
423 | requestPermissions.contains(Permission.ACTIVITY_RECOGNITION) &&
424 | !requestPermissions.contains(Permission.BODY_SENSORS)) {
425 | // 自动添加传感器权限,因为 ACTIVITY_RECOGNITION 是从 Android 10 开始才从传感器权限中剥离成独立权限
426 | requestPermissions.add(Permission.BODY_SENSORS);
427 | }
428 | }
429 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/ph/permissions/PermissionFragment.java:
--------------------------------------------------------------------------------
1 | package com.ph.permissions;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.app.Activity;
5 | import android.app.Fragment;
6 | import android.content.Context;
7 | import android.content.Intent;
8 | import android.content.pm.ActivityInfo;
9 | import android.content.pm.PackageManager;
10 | import android.content.res.Configuration;
11 | import android.os.Bundle;
12 |
13 | import java.util.ArrayList;
14 | import java.util.Arrays;
15 | import java.util.List;
16 | import java.util.Random;
17 |
18 | /**
19 | * author : i小灰
20 | * github : https://github.com/dahui888/HPermissions
21 | * desc : 权限请求 Fragment
22 | */
23 | @SuppressWarnings("deprecation")
24 | public final class PermissionFragment extends Fragment implements Runnable {
25 |
26 | /** 请求的权限组 */
27 | private static final String REQUEST_PERMISSIONS = "request_permissions";
28 |
29 | /** 请求码(自动生成)*/
30 | private static final String REQUEST_CODE = "request_code";
31 |
32 | /** 权限请求码存放集合 */
33 | private static final List REQUEST_CODE_ARRAY = new ArrayList<>();
34 |
35 | /**
36 | * 开启权限申请
37 | */
38 | public static void beginRequest(Activity activity, ArrayList permissions,
39 | IPermissionInterceptor interceptor, OnPermissionCallback callback) {
40 | PermissionFragment fragment = new PermissionFragment();
41 | Bundle bundle = new Bundle();
42 | int requestCode;
43 | // 请求码随机生成,避免随机产生之前的请求码,必须进行循环判断
44 | do {
45 | // 新版本的 Support 库限制请求码必须小于 65536
46 | // 旧版本的 Support 库限制请求码必须小于 256
47 | requestCode = new Random().nextInt((int) Math.pow(2, 8));
48 | } while (REQUEST_CODE_ARRAY.contains(requestCode));
49 | // 标记这个请求码已经被占用
50 | REQUEST_CODE_ARRAY.add(requestCode);
51 | bundle.putInt(REQUEST_CODE, requestCode);
52 | bundle.putStringArrayList(REQUEST_PERMISSIONS, permissions);
53 | fragment.setArguments(bundle);
54 | // 设置保留实例,不会因为屏幕方向或配置变化而重新创建
55 | fragment.setRetainInstance(true);
56 | // 设置权限申请标记
57 | fragment.setRequestFlag(true);
58 | // 设置权限回调监听
59 | fragment.setCallBack(callback);
60 | // 设置权限请求拦截器
61 | fragment.setInterceptor(interceptor);
62 | // 绑定到 Activity 上面
63 | fragment.attachActivity(activity);
64 | }
65 |
66 | /** 是否申请了特殊权限 */
67 | private boolean mSpecialRequest;
68 |
69 | /** 是否申请了危险权限 */
70 | private boolean mDangerousRequest;
71 |
72 | /** 权限申请标记 */
73 | private boolean mRequestFlag;
74 |
75 | /** 权限回调对象 */
76 | private OnPermissionCallback mCallBack;
77 |
78 | /** 权限请求拦截器 */
79 | private IPermissionInterceptor mInterceptor;
80 |
81 | /** Activity 屏幕方向 */
82 | private int mScreenOrientation;
83 |
84 | /**
85 | * 绑定 Activity
86 | */
87 | public void attachActivity(Activity activity) {
88 | activity.getFragmentManager().beginTransaction().add(this, this.toString()).commitAllowingStateLoss();
89 | }
90 |
91 | /**
92 | * 解绑 Activity
93 | */
94 | public void detachActivity(Activity activity) {
95 | activity.getFragmentManager().beginTransaction().remove(this).commitAllowingStateLoss();
96 | }
97 |
98 | /**
99 | * 设置权限监听回调监听
100 | */
101 | public void setCallBack(OnPermissionCallback callback) {
102 | mCallBack = callback;
103 | }
104 |
105 | /**
106 | * 权限申请标记(防止系统杀死应用后重新触发请求的问题)
107 | */
108 | public void setRequestFlag(boolean flag) {
109 | mRequestFlag = flag;
110 | }
111 |
112 | /**
113 | * 设置权限请求拦截器
114 | */
115 | public void setInterceptor(IPermissionInterceptor interceptor) {
116 | mInterceptor = interceptor;
117 | }
118 |
119 | @SuppressLint("SourceLockedOrientationActivity")
120 | @Override
121 | public void onAttach(Context context) {
122 | super.onAttach(context);
123 | Activity activity = getActivity();
124 | if (activity == null) {
125 | return;
126 | }
127 | // 如果当前没有锁定屏幕方向就获取当前屏幕方向并进行锁定
128 | mScreenOrientation = activity.getRequestedOrientation();
129 | if (mScreenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
130 | return;
131 | }
132 |
133 | try {
134 | // 兼容问题:在 Android 8.0 的手机上可以固定 Activity 的方向,但是这个 Activity 不能是透明的,否则就会抛出异常
135 | // 复现场景:只需要给 Activity 主题设置 - true
属性即可
136 | switch (activity.getResources().getConfiguration().orientation) {
137 | case Configuration.ORIENTATION_LANDSCAPE:
138 | activity.setRequestedOrientation(PermissionUtils.isActivityReverse(activity) ?
139 | ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE :
140 | ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
141 | break;
142 | case Configuration.ORIENTATION_PORTRAIT:
143 | default:
144 | activity.setRequestedOrientation(PermissionUtils.isActivityReverse(activity) ?
145 | ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT :
146 | ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
147 | break;
148 | }
149 | } catch (IllegalStateException e) {
150 | // java.lang.IllegalStateException: Only fullscreen activities can request orientation
151 | e.printStackTrace();
152 | }
153 | }
154 |
155 | @Override
156 | public void onDetach() {
157 | super.onDetach();
158 | Activity activity = getActivity();
159 | if (activity == null || mScreenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
160 | return;
161 | }
162 | // 为什么这里不用跟上面一样 try catch ?因为这里是把 Activity 方向取消固定,只有设置横屏或竖屏的时候才可能触发 crash
163 | activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
164 | }
165 |
166 | @Override
167 | public void onDestroy() {
168 | super.onDestroy();
169 | // 取消引用监听器,避免内存泄漏
170 | mCallBack = null;
171 | }
172 |
173 | @Override
174 | public void onResume() {
175 | super.onResume();
176 |
177 | // 如果当前 Fragment 是通过系统重启应用触发的,则不进行权限申请
178 | if (!mRequestFlag) {
179 | detachActivity(getActivity());
180 | return;
181 | }
182 |
183 | // 如果在 Activity 不可见的状态下添加 Fragment 并且去申请权限会导致授权对话框显示不出来
184 | // 所以必须要在 Fragment 的 onResume 来申请权限,这样就可以保证应用回到前台的时候才去申请权限
185 | if (mSpecialRequest) {
186 | return;
187 | }
188 |
189 | mSpecialRequest = true;
190 | requestSpecialPermission();
191 | }
192 |
193 | /**
194 | * 申请特殊权限
195 | */
196 | public void requestSpecialPermission() {
197 | Bundle arguments = getArguments();
198 | Activity activity = getActivity();
199 | if (arguments == null || activity == null) {
200 | return;
201 | }
202 |
203 | List allPermissions = arguments.getStringArrayList(REQUEST_PERMISSIONS);
204 |
205 | // 是否需要申请特殊权限
206 | boolean requestSpecialPermission = false;
207 |
208 | // 判断当前是否包含特殊权限
209 | for (String permission : allPermissions) {
210 | if (PermissionApi.isSpecialPermission(permission)) {
211 | if (PermissionApi.isGrantedPermission(activity, permission)) {
212 | // 已经授予过了,可以跳过
213 | continue;
214 | }
215 | if (Permission.MANAGE_EXTERNAL_STORAGE.equals(permission) && !AndroidVersion.isAndroid11()) {
216 | // 当前必须是 Android 11 及以上版本,因为在旧版本上是拿旧权限做的判断
217 | continue;
218 | }
219 | // 跳转到特殊权限授权页面
220 | startActivityForResult(PermissionPageIntent.getSmartPermissionIntent(activity,
221 | PermissionUtils.asArrayList(permission)), getArguments().getInt(REQUEST_CODE));
222 | requestSpecialPermission = true;
223 | }
224 | }
225 |
226 | if (requestSpecialPermission) {
227 | return;
228 | }
229 | // 如果没有跳转到特殊权限授权页面,就直接申请危险权限
230 | requestDangerousPermission();
231 | }
232 |
233 | /**
234 | * 申请危险权限
235 | */
236 | public void requestDangerousPermission() {
237 | Activity activity = getActivity();
238 | Bundle arguments = getArguments();
239 | if (activity == null || arguments == null) {
240 | return;
241 | }
242 |
243 | final int requestCode = arguments.getInt(REQUEST_CODE);
244 |
245 | final ArrayList allPermissions = arguments.getStringArrayList(REQUEST_PERMISSIONS);
246 | if (allPermissions == null || allPermissions.isEmpty()) {
247 | return;
248 | }
249 |
250 | if (!AndroidVersion.isAndroid6()) {
251 | // 如果是 Android 6.0 以下,没有危险权限的概念,则直接回调监听
252 | int[] grantResults = new int[allPermissions.size()];
253 | for (int i = 0; i < grantResults.length; i++) {
254 | grantResults[i] = PermissionApi.isGrantedPermission(activity, allPermissions.get(i)) ?
255 | PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED;
256 | }
257 | onRequestPermissionsResult(requestCode, allPermissions.toArray(new String[0]), grantResults);
258 | return;
259 | }
260 |
261 | ArrayList locationPermission = null;
262 | // Android 10 定位策略发生改变,申请后台定位权限的前提是要有前台定位权限(授予了精确或者模糊任一权限)
263 | if (AndroidVersion.isAndroid10() && allPermissions.contains(Permission.ACCESS_BACKGROUND_LOCATION)) {
264 | locationPermission = new ArrayList<>();
265 | if (allPermissions.contains(Permission.ACCESS_COARSE_LOCATION)) {
266 | locationPermission.add(Permission.ACCESS_COARSE_LOCATION);
267 | }
268 |
269 | if (allPermissions.contains(Permission.ACCESS_FINE_LOCATION)) {
270 | locationPermission.add(Permission.ACCESS_FINE_LOCATION);
271 | }
272 | }
273 |
274 | if (!AndroidVersion.isAndroid10() || locationPermission == null || locationPermission.isEmpty()) {
275 | requestPermissions(allPermissions.toArray(new String[allPermissions.size() - 1]), getArguments().getInt(REQUEST_CODE));
276 | return;
277 | }
278 |
279 | // 在 Android 10 的机型上,需要先申请前台定位权限,再申请后台定位权限
280 | PermissionFragment.beginRequest(activity, locationPermission,
281 | new IPermissionInterceptor() {}, new OnPermissionCallback() {
282 |
283 | @Override
284 | public void onGranted(List permissions, boolean all) {
285 | if (!all || !isAdded()) {
286 | return;
287 | }
288 |
289 | // 前台定位权限授予了,现在申请后台定位权限
290 | PermissionFragment.beginRequest(activity,
291 | PermissionUtils.asArrayList(Permission.ACCESS_BACKGROUND_LOCATION)
292 | , new IPermissionInterceptor() {}, new OnPermissionCallback() {
293 |
294 | @Override
295 | public void onGranted(List permissions, boolean all) {
296 | if (!all || !isAdded()) {
297 | return;
298 | }
299 |
300 | // 前台定位权限和后台定位权限都授予了
301 | int[] grantResults = new int[allPermissions.size()];
302 | Arrays.fill(grantResults, PackageManager.PERMISSION_GRANTED);
303 | onRequestPermissionsResult(requestCode, allPermissions.toArray(new String[0]), grantResults);
304 | }
305 |
306 | @Override
307 | public void onDenied(List permissions, boolean never) {
308 | if (!isAdded()) {
309 | return;
310 | }
311 |
312 | // 后台定位授权失败,但是前台定位权限已经授予了
313 | int[] grantResults = new int[allPermissions.size()];
314 | for (int i = 0; i < allPermissions.size(); i++) {
315 | grantResults[i] = Permission.ACCESS_BACKGROUND_LOCATION.equals(allPermissions.get(i)) ?
316 | PackageManager.PERMISSION_DENIED : PackageManager.PERMISSION_GRANTED;
317 | }
318 | onRequestPermissionsResult(requestCode, allPermissions.toArray(new String[0]), grantResults);
319 | }
320 | });
321 | }
322 |
323 | @Override
324 | public void onDenied(List permissions, boolean never) {
325 | if (!isAdded()) {
326 | return;
327 | }
328 |
329 | // 前台定位授权失败,并且无法申请后台定位权限
330 | int[] grantResults = new int[allPermissions.size()];
331 | Arrays.fill(grantResults, PackageManager.PERMISSION_DENIED);
332 | onRequestPermissionsResult(requestCode, allPermissions.toArray(new String[0]), grantResults);
333 | }
334 | });
335 | }
336 |
337 | @Override
338 | public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
339 | if (permissions == null || permissions.length == 0 ||
340 | grantResults == null || grantResults.length == 0) {
341 | return;
342 | }
343 |
344 | Bundle arguments = getArguments();
345 | Activity activity = getActivity();
346 | if (activity == null || arguments == null || mInterceptor == null ||
347 | requestCode != arguments.getInt(REQUEST_CODE)) {
348 | return;
349 | }
350 |
351 | OnPermissionCallback callback = mCallBack;
352 | mCallBack = null;
353 |
354 | IPermissionInterceptor interceptor = mInterceptor;
355 | mInterceptor = null;
356 |
357 | // 优化权限回调结果
358 | PermissionUtils.optimizePermissionResults(activity, permissions, grantResults);
359 |
360 | // 将数组转换成 ArrayList
361 | List allPermissions = PermissionUtils.asArrayList(permissions);
362 |
363 | // 释放对这个请求码的占用
364 | REQUEST_CODE_ARRAY.remove((Integer) requestCode);
365 | // 将 Fragment 从 Activity 移除
366 | detachActivity(activity);
367 |
368 | // 获取已授予的权限
369 | List grantedPermissions = PermissionApi.getGrantedPermissions(allPermissions, grantResults);
370 |
371 | // 如果请求成功的权限集合大小和请求的数组一样大时证明权限已经全部授予
372 | if (grantedPermissions.size() == allPermissions.size()) {
373 | // 代表申请的所有的权限都授予了
374 | interceptor.grantedPermissions(activity, allPermissions, grantedPermissions, true, callback);
375 | return;
376 | }
377 |
378 | // 获取被拒绝的权限
379 | List deniedPermissions = PermissionApi.getDeniedPermissions(allPermissions, grantResults);
380 |
381 | // 代表申请的权限中有不同意授予的,如果有某个权限被永久拒绝就返回 true 给开发人员,让开发者引导用户去设置界面开启权限
382 | interceptor.deniedPermissions(activity, allPermissions, deniedPermissions,
383 | PermissionApi.isPermissionPermanentDenied(activity, deniedPermissions), callback);
384 |
385 | // 证明还有一部分权限被成功授予,回调成功接口
386 | if (!grantedPermissions.isEmpty()) {
387 | interceptor.grantedPermissions(activity, allPermissions, grantedPermissions, false, callback);
388 | }
389 | }
390 |
391 | @Override
392 | public void onActivityResult(int requestCode, int resultCode, Intent data) {
393 | Activity activity = getActivity();
394 | Bundle arguments = getArguments();
395 | if (activity == null || arguments == null || mDangerousRequest ||
396 | requestCode != arguments.getInt(REQUEST_CODE)) {
397 | return;
398 | }
399 |
400 | mDangerousRequest = true;
401 | // 需要延迟执行,不然有些华为机型授权了但是获取不到权限
402 | PermissionUtils.postDelayed(this, 300);
403 | }
404 |
405 | @Override
406 | public void run() {
407 | // 如果用户离开太久,会导致 Activity 被回收掉
408 | // 所以这里要判断当前 Fragment 是否有被添加到 Activity
409 | // 可在开发者模式中开启不保留活动来复现这个 Bug
410 | if (!isAdded()) {
411 | return;
412 | }
413 | // 请求其他危险权限
414 | requestDangerousPermission();
415 | }
416 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/ph/permissions/PermissionPageFragment.java:
--------------------------------------------------------------------------------
1 | package com.ph.permissions;
2 |
3 | import android.app.Activity;
4 | import android.app.Fragment;
5 | import android.content.Intent;
6 | import android.os.Bundle;
7 |
8 | import java.util.ArrayList;
9 | import java.util.List;
10 |
11 | /**
12 | * author : i小灰
13 | * github : https://github.com/dahui888/HPermissions
14 | * desc : 权限页跳转 Fragment
15 | */
16 | @SuppressWarnings("deprecation")
17 | public final class PermissionPageFragment extends Fragment implements Runnable {
18 |
19 | /** 权限设置页跳转请求码 */
20 | public static final int REQUEST_CODE = 1024 + 1;
21 |
22 | /** 请求的权限组 */
23 | private static final String REQUEST_PERMISSIONS = "request_permissions";
24 |
25 | /**
26 | * 开启权限申请
27 | */
28 | public static void beginRequest(Activity activity, ArrayList permissions, OnPermissionPageCallback callback) {
29 | PermissionPageFragment fragment = new PermissionPageFragment();
30 | Bundle bundle = new Bundle();
31 | bundle.putStringArrayList(REQUEST_PERMISSIONS, permissions);
32 | fragment.setArguments(bundle);
33 | // 设置保留实例,不会因为屏幕方向或配置变化而重新创建
34 | fragment.setRetainInstance(true);
35 | // 设置权限申请标记
36 | fragment.setRequestFlag(true);
37 | // 设置权限回调监听
38 | fragment.setCallBack(callback);
39 | // 绑定到 Activity 上面
40 | fragment.attachActivity(activity);
41 | }
42 |
43 | /**
44 | * 绑定 Activity
45 | */
46 | public void attachActivity(Activity activity) {
47 | activity.getFragmentManager().beginTransaction().add(this, this.toString()).commitAllowingStateLoss();
48 | }
49 |
50 | /**
51 | * 解绑 Activity
52 | */
53 | public void detachActivity(Activity activity) {
54 | activity.getFragmentManager().beginTransaction().remove(this).commitAllowingStateLoss();
55 | }
56 |
57 | /** 权限回调对象 */
58 | private OnPermissionPageCallback mCallBack;
59 |
60 | /** 权限申请标记 */
61 | private boolean mRequestFlag;
62 |
63 | /** 是否申请了权限 */
64 | private boolean mStartActivityFlag;
65 |
66 | @Override
67 | public void onResume() {
68 | super.onResume();
69 |
70 | // 如果当前 Fragment 是通过系统重启应用触发的,则不进行权限申请
71 | if (!mRequestFlag) {
72 | detachActivity(getActivity());
73 | return;
74 | }
75 |
76 | if (mStartActivityFlag) {
77 | return;
78 | }
79 |
80 | mStartActivityFlag = true;
81 |
82 | Bundle arguments = getArguments();
83 | Activity activity = getActivity();
84 | if (arguments == null || activity == null) {
85 | return;
86 | }
87 | List permissions = arguments.getStringArrayList(REQUEST_PERMISSIONS);
88 | startActivityForResult(PermissionPageIntent.getSmartPermissionIntent(getActivity(), permissions), HPermissions.REQUEST_CODE);
89 | }
90 |
91 | /**
92 | * 设置权限监听回调监听
93 | */
94 | public void setCallBack(OnPermissionPageCallback callback) {
95 | mCallBack = callback;
96 | }
97 |
98 | /**
99 | * 权限申请标记(防止系统杀死应用后重新触发请求的问题)
100 | */
101 | public void setRequestFlag(boolean flag) {
102 | mRequestFlag = flag;
103 | }
104 |
105 | @Override
106 | public void onActivityResult(int requestCode, int resultCode, Intent data) {
107 | super.onActivityResult(requestCode, resultCode, data);
108 | if (requestCode != REQUEST_CODE) {
109 | return;
110 | }
111 | // 需要延迟执行,不然有些华为机型授权了但是获取不到权限
112 | PermissionUtils.postDelayed(this, 300);
113 | }
114 |
115 | @Override
116 | public void run() {
117 | // 如果用户离开太久,会导致 Activity 被回收掉
118 | // 所以这里要判断当前 Fragment 是否有被添加到 Activity
119 | // 可在开发者模式中开启不保留活动来复现这个 Bug
120 | if (!isAdded()) {
121 | return;
122 | }
123 |
124 | Activity activity = getActivity();
125 | if (activity == null) {
126 | return;
127 | }
128 |
129 | OnPermissionPageCallback callback = mCallBack;
130 | mCallBack = null;
131 |
132 | if (callback == null) {
133 | detachActivity(getActivity());
134 | return;
135 | }
136 |
137 | Bundle arguments = getArguments();
138 | List allPermissions = arguments.getStringArrayList(REQUEST_PERMISSIONS);
139 |
140 | List grantedPermissions = PermissionApi.getGrantedPermissions(activity, allPermissions);
141 | if (grantedPermissions.size() == allPermissions.size()) {
142 | callback.onGranted();
143 | } else {
144 | callback.onDenied();
145 | }
146 | }
147 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/ph/permissions/PermissionPageIntent.java:
--------------------------------------------------------------------------------
1 | package com.ph.permissions;
2 |
3 | import android.content.Context;
4 | import android.content.Intent;
5 | import android.net.Uri;
6 | import android.provider.Settings;
7 |
8 | import java.util.List;
9 |
10 | /**
11 | * author : i小灰
12 | * github : https://github.com/dahui888/HPermissions
13 | * desc : 权限设置页
14 | */
15 | final class PermissionPageIntent {
16 |
17 | /**
18 | * 根据传入的权限自动选择最合适的权限设置页
19 | *
20 | * @param permissions 请求失败的权限
21 | */
22 | static Intent getSmartPermissionIntent(Context context, List permissions) {
23 | // 如果失败的权限里面不包含特殊权限
24 | if (permissions == null || permissions.isEmpty() ||
25 | !PermissionApi.containsSpecialPermission(permissions)) {
26 | return getApplicationDetailsIntent(context);
27 | }
28 |
29 | if (AndroidVersion.isAndroid11() && permissions.size() == 3 &&
30 | (permissions.contains(Permission.MANAGE_EXTERNAL_STORAGE) &&
31 | permissions.contains(Permission.READ_EXTERNAL_STORAGE) &&
32 | permissions.contains(Permission.WRITE_EXTERNAL_STORAGE))) {
33 | return getStoragePermissionIntent(context);
34 | }
35 |
36 | // 如果当前只有一个权限被拒绝了
37 | if (permissions.size() == 1) {
38 |
39 | String permission = permissions.get(0);
40 |
41 | if (Permission.MANAGE_EXTERNAL_STORAGE.equals(permission)) {
42 | return getStoragePermissionIntent(context);
43 | }
44 |
45 | if (Permission.REQUEST_INSTALL_PACKAGES.equals(permission)) {
46 | return getInstallPermissionIntent(context);
47 | }
48 |
49 | if (Permission.SYSTEM_ALERT_WINDOW.equals(permission)) {
50 | return getWindowPermissionIntent(context);
51 | }
52 |
53 | if (Permission.WRITE_SETTINGS.equals(permission)) {
54 | return getSettingPermissionIntent(context);
55 | }
56 |
57 | if (Permission.NOTIFICATION_SERVICE.equals(permission)) {
58 | return getNotifyPermissionIntent(context);
59 | }
60 |
61 | if (Permission.PACKAGE_USAGE_STATS.equals(permission)) {
62 | return getPackagePermissionIntent(context);
63 | }
64 |
65 | if (Permission.BIND_NOTIFICATION_LISTENER_SERVICE.equals(permission)) {
66 | return getNotificationListenerIntent(context);
67 | }
68 |
69 | if (Permission.SCHEDULE_EXACT_ALARM.equals(permission)) {
70 | return getAlarmPermissionIntent(context);
71 | }
72 |
73 | if (Permission.ACCESS_NOTIFICATION_POLICY.equals(permission)) {
74 | return getNotDisturbPermissionIntent(context);
75 | }
76 | }
77 |
78 | return getApplicationDetailsIntent(context);
79 | }
80 |
81 | /**
82 | * 获取应用详情界面意图
83 | */
84 | static Intent getApplicationDetailsIntent(Context context) {
85 | Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
86 | intent.setData(getPackageNameUri(context));
87 | return intent;
88 | }
89 |
90 | /**
91 | * 获取安装权限设置界面意图
92 | */
93 | static Intent getInstallPermissionIntent(Context context) {
94 | Intent intent = null;
95 | if (AndroidVersion.isAndroid8()) {
96 | intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
97 | intent.setData(getPackageNameUri(context));
98 | }
99 | if (intent == null || !PermissionUtils.areActivityIntent(context, intent)) {
100 | intent = getApplicationDetailsIntent(context);
101 | }
102 | return intent;
103 | }
104 |
105 | /**
106 | * 获取悬浮窗权限设置界面意图
107 | */
108 | static Intent getWindowPermissionIntent(Context context) {
109 | Intent intent = null;
110 | if (AndroidVersion.isAndroid6()) {
111 | intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
112 | // 在 Android 11 加包名跳转也是没有效果的,官方文档链接:
113 | // https://developer.android.google.cn/reference/android/provider/Settings#ACTION_MANAGE_OVERLAY_PERMISSION
114 | intent.setData(getPackageNameUri(context));
115 | }
116 |
117 | if (intent == null || !PermissionUtils.areActivityIntent(context, intent)) {
118 | intent = getApplicationDetailsIntent(context);
119 | }
120 | return intent;
121 | }
122 |
123 | /**
124 | * 获取通知栏权限设置界面意图
125 | */
126 | static Intent getNotifyPermissionIntent(Context context) {
127 | Intent intent = null;
128 | if (AndroidVersion.isAndroid8()) {
129 | intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
130 | intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName());
131 | //intent.putExtra(Settings.EXTRA_CHANNEL_ID, context.getApplicationInfo().uid);
132 | }
133 | if (intent == null || !PermissionUtils.areActivityIntent(context, intent)) {
134 | intent = getApplicationDetailsIntent(context);
135 | }
136 | return intent;
137 | }
138 |
139 | /**
140 | * 获取通知监听设置界面意图
141 | */
142 | static Intent getNotificationListenerIntent(Context context) {
143 | Intent intent;
144 | if (AndroidVersion.isAndroid5_1()) {
145 | intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS);
146 | } else {
147 | intent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS");
148 | }
149 |
150 | if (!PermissionUtils.areActivityIntent(context, intent)) {
151 | intent = getApplicationDetailsIntent(context);
152 | }
153 | return intent;
154 | }
155 |
156 | /**
157 | * 获取系统设置权限界面意图
158 | */
159 | static Intent getSettingPermissionIntent(Context context) {
160 | Intent intent = null;
161 | if (AndroidVersion.isAndroid6()) {
162 | intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS);
163 | intent.setData(getPackageNameUri(context));
164 | }
165 | if (intent == null || !PermissionUtils.areActivityIntent(context, intent)) {
166 | intent = getApplicationDetailsIntent(context);
167 | }
168 | return intent;
169 | }
170 |
171 | /**
172 | * 获取存储权限设置界面意图
173 | */
174 | static Intent getStoragePermissionIntent(Context context) {
175 | Intent intent = null;
176 | if (AndroidVersion.isAndroid11()) {
177 | intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
178 | intent.setData(getPackageNameUri(context));
179 | }
180 | if (intent == null || !PermissionUtils.areActivityIntent(context, intent)) {
181 | intent = getApplicationDetailsIntent(context);
182 | }
183 | return intent;
184 | }
185 |
186 | /**
187 | * 获取使用统计权限设置界面意图
188 | */
189 | static Intent getPackagePermissionIntent(Context context) {
190 | Intent intent = null;
191 | if (AndroidVersion.isAndroid5()) {
192 | intent = new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS);
193 | if (AndroidVersion.isAndroid10()) {
194 | // 经过测试,只有在 Android 10 及以上加包名才有效果
195 | // 如果在 Android 10 以下加包名会导致无法跳转
196 | intent.setData(getPackageNameUri(context));
197 | }
198 | }
199 | if (intent == null || !PermissionUtils.areActivityIntent(context, intent)) {
200 | intent = getApplicationDetailsIntent(context);
201 | }
202 | return intent;
203 | }
204 |
205 | /**
206 | * 获取勿扰模式设置界面意图
207 | */
208 | static Intent getNotDisturbPermissionIntent(Context context) {
209 | Intent intent = null;
210 | if (AndroidVersion.isAndroid6()) {
211 | intent = new Intent(Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS);
212 | }
213 | if (intent == null || !PermissionUtils.areActivityIntent(context, intent)) {
214 | intent = getApplicationDetailsIntent(context);
215 | }
216 | return intent;
217 | }
218 |
219 | /**
220 | * 获取闹钟权限设置界面意图
221 | */
222 | static Intent getAlarmPermissionIntent(Context context) {
223 | Intent intent = null;
224 | if (AndroidVersion.isAndroid12()) {
225 | intent = new Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM);
226 | intent.setData(getPackageNameUri(context));
227 | }
228 | if (intent == null || !PermissionUtils.areActivityIntent(context, intent)) {
229 | intent = getApplicationDetailsIntent(context);
230 | }
231 | return intent;
232 | }
233 |
234 | /**
235 | * 获取包名 Uri 对象
236 | */
237 | private static Uri getPackageNameUri(Context context) {
238 | return Uri.parse("package:" + context.getPackageName());
239 | }
240 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/ph/permissions/PermissionUtils.java:
--------------------------------------------------------------------------------
1 | package com.ph.permissions;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.app.Activity;
5 | import android.content.Context;
6 | import android.content.ContextWrapper;
7 | import android.content.Intent;
8 | import android.content.pm.ApplicationInfo;
9 | import android.content.pm.PackageManager;
10 | import android.content.res.AssetManager;
11 | import android.content.res.XmlResourceParser;
12 | import android.os.Bundle;
13 | import android.os.Handler;
14 | import android.os.Looper;
15 | import android.text.TextUtils;
16 | import android.view.Surface;
17 |
18 | import org.xmlpull.v1.XmlPullParserException;
19 |
20 | import java.io.IOException;
21 | import java.lang.reflect.InvocationTargetException;
22 | import java.lang.reflect.Method;
23 | import java.util.ArrayList;
24 | import java.util.HashMap;
25 |
26 | /**
27 | * author : i小灰
28 | * github : https://github.com/dahui888/HPermissions
29 | * desc : 权限相关工具类
30 | */
31 | final class PermissionUtils {
32 |
33 | /** Handler 对象 */
34 | private static final Handler HANDLER = new Handler(Looper.getMainLooper());
35 |
36 | /**
37 | * 延迟一段时间执行
38 | */
39 | public static void postDelayed(Runnable r, long delayMillis) {
40 | HANDLER.postDelayed(r, delayMillis);
41 | }
42 |
43 | /**
44 | * 获取 Android 属性命名空间
45 | */
46 | static String getAndroidNamespace() {
47 | return "http://schemas.android.com/apk/res/android";
48 | }
49 |
50 | /**
51 | * 当前是否处于 debug 模式
52 | */
53 | static boolean isDebugMode(Context context) {
54 | return (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
55 | }
56 |
57 | /**
58 | * 返回应用程序在清单文件中注册的权限
59 | */
60 | static HashMap getManifestPermissions(Context context) {
61 | HashMap manifestPermissions = new HashMap<>();
62 |
63 | XmlResourceParser parser = PermissionUtils.parseAndroidManifest(context);
64 |
65 | if (parser != null) {
66 | try {
67 |
68 | do {
69 | // 当前节点必须为标签头部
70 | if (parser.getEventType() != XmlResourceParser.START_TAG) {
71 | continue;
72 | }
73 |
74 | // 当前标签必须为 uses-permission
75 | if (!"uses-permission".equals(parser.getName())) {
76 | continue;
77 | }
78 |
79 | manifestPermissions.put(parser.getAttributeValue(getAndroidNamespace(), "name"),
80 | parser.getAttributeIntValue(getAndroidNamespace(), "maxSdkVersion", Integer.MAX_VALUE));
81 |
82 | } while (parser.next() != XmlResourceParser.END_DOCUMENT);
83 |
84 | } catch (IOException | XmlPullParserException e) {
85 | e.printStackTrace();
86 | } finally {
87 | parser.close();
88 | }
89 | }
90 |
91 | if (manifestPermissions.isEmpty()) {
92 | try {
93 | // 当清单文件没有注册任何权限的时候,那么这个数组对象就是空的
94 | String[] requestedPermissions = context.getPackageManager().getPackageInfo(
95 | context.getPackageName(), PackageManager.GET_PERMISSIONS).requestedPermissions;
96 | if (requestedPermissions != null) {
97 | for (String permission : requestedPermissions) {
98 | manifestPermissions.put(permission, Integer.MAX_VALUE);
99 | }
100 | }
101 | } catch (PackageManager.NameNotFoundException e) {
102 | e.printStackTrace();
103 | }
104 | }
105 |
106 | return manifestPermissions;
107 | }
108 |
109 | /**
110 | * 优化权限回调结果
111 | */
112 | static void optimizePermissionResults(Activity activity, String[] permissions, int[] grantResults) {
113 | for (int i = 0; i < permissions.length; i++) {
114 |
115 | boolean recheck = false;
116 |
117 | String permission = permissions[i];
118 |
119 | // 如果这个权限是特殊权限,那么就重新进行权限检测
120 | if (PermissionApi.isSpecialPermission(permission)) {
121 | recheck = true;
122 | }
123 |
124 | // 重新检查 Android 12 的三个新权限
125 | if (!AndroidVersion.isAndroid12() &&
126 | (Permission.BLUETOOTH_SCAN.equals(permission) ||
127 | Permission.BLUETOOTH_CONNECT.equals(permission) ||
128 | Permission.BLUETOOTH_ADVERTISE.equals(permission))) {
129 | recheck = true;
130 | }
131 |
132 | // 重新检查 Android 10.0 的三个新权限
133 | if (!AndroidVersion.isAndroid10() &&
134 | (Permission.ACCESS_BACKGROUND_LOCATION.equals(permission) ||
135 | Permission.ACTIVITY_RECOGNITION.equals(permission) ||
136 | Permission.ACCESS_MEDIA_LOCATION.equals(permission))) {
137 | recheck = true;
138 | }
139 |
140 | // 重新检查 Android 9.0 的一个新权限
141 | if (!AndroidVersion.isAndroid9() &&
142 | Permission.ACCEPT_HANDOVER.equals(permission)) {
143 | recheck = true;
144 | }
145 |
146 | // 重新检查 Android 8.0 的两个新权限
147 | if (!AndroidVersion.isAndroid8() &&
148 | (Permission.ANSWER_PHONE_CALLS.equals(permission) ||
149 | Permission.READ_PHONE_NUMBERS.equals(permission))) {
150 | recheck = true;
151 | }
152 |
153 | if (recheck) {
154 | grantResults[i] = PermissionApi.isGrantedPermission(activity, permission) ?
155 | PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED;
156 | }
157 | }
158 | }
159 |
160 | /**
161 | * 将数组转换成 ArrayList
162 | *
163 | * 这里解释一下为什么不用 Arrays.asList
164 | * 第一是返回的类型不是 java.util.ArrayList 而是 java.util.Arrays.ArrayList
165 | * 第二是返回的 ArrayList 对象是只读的,也就是不能添加任何元素,否则会抛异常
166 | */
167 | @SuppressWarnings("all")
168 | static ArrayList asArrayList(T... array) {
169 | ArrayList list = new ArrayList<>(array.length);
170 | if (array == null || array.length == 0) {
171 | return list;
172 | }
173 | for (T t : array) {
174 | list.add(t);
175 | }
176 | return list;
177 | }
178 |
179 | @SafeVarargs
180 | static ArrayList asArrayLists(T[]... arrays) {
181 | ArrayList list = new ArrayList<>();
182 | if (arrays == null || arrays.length == 0) {
183 | return list;
184 | }
185 | for (T[] ts : arrays) {
186 | list.addAll(asArrayList(ts));
187 | }
188 | return list;
189 | }
190 |
191 | /**
192 | * 寻找上下文中的 Activity 对象
193 | */
194 | static Activity findActivity(Context context) {
195 | do {
196 | if (context instanceof Activity) {
197 | return (Activity) context;
198 | } else if (context instanceof ContextWrapper){
199 | context = ((ContextWrapper) context).getBaseContext();
200 | } else {
201 | return null;
202 | }
203 | } while (context != null);
204 | return null;
205 | }
206 |
207 | /**
208 | * 获取当前应用 Apk 在 AssetManager 中的 Cookie,如果获取失败,则为 0
209 | */
210 | @SuppressWarnings("JavaReflectionMemberAccess")
211 | @SuppressLint("PrivateApi")
212 | static int findApkPathCookie(Context context) {
213 | AssetManager assets = context.getAssets();
214 | String apkPath = context.getApplicationInfo().sourceDir;
215 | try {
216 | // 为什么不直接通过反射 AssetManager.findCookieForPath 方法来判断?因为这个 API 属于反射黑名单,反射执行不了
217 | // 为什么不直接通过反射 AssetManager.addAssetPathInternal 这个非隐藏的方法来判断?因为这个也反射不了
218 | Method method = assets.getClass().getDeclaredMethod("addAssetPath", String.class);
219 | Integer cookie = (Integer) method.invoke(assets, apkPath);
220 | if (cookie != null) {
221 | return cookie;
222 | }
223 | } catch (NoSuchMethodException e) {
224 | e.printStackTrace();
225 | } catch (IllegalAccessException e) {
226 | e.printStackTrace();
227 | } catch (InvocationTargetException e) {
228 | e.printStackTrace();
229 | }
230 | // 获取失败
231 | return 0;
232 | }
233 |
234 | /**
235 | * 解析清单文件
236 | */
237 | static XmlResourceParser parseAndroidManifest(Context context) {
238 | int cookie = PermissionUtils.findApkPathCookie(context);
239 | if (cookie == 0) {
240 | // 如果 cookie 为 0,证明获取失败,直接 return
241 | return null;
242 | }
243 |
244 | try {
245 | XmlResourceParser parser = context.getAssets().openXmlResourceParser(cookie, "AndroidManifest.xml");
246 |
247 | do {
248 | // 当前节点必须为标签头部
249 | if (parser.getEventType() != XmlResourceParser.START_TAG) {
250 | continue;
251 | }
252 |
253 | if ("manifest".equals(parser.getName())) {
254 | // 如果读取到的包名和当前应用的包名不是同一个的话,证明这个清单文件的内容不是当前应用的
255 | if (TextUtils.equals(context.getPackageName(),
256 | parser.getAttributeValue(null, "package"))) {
257 | return parser;
258 | }
259 | }
260 |
261 | } while (parser.next() != XmlResourceParser.END_DOCUMENT);
262 |
263 | } catch (IOException e) {
264 | e.printStackTrace();
265 | } catch (XmlPullParserException e) {
266 | e.printStackTrace();
267 | }
268 | return null;
269 | }
270 |
271 | /**
272 | * 判断是否适配了分区存储
273 | */
274 | static boolean isScopedStorage(Context context) {
275 | try {
276 | String metaKey = "ScopedStorage";
277 | Bundle metaData = context.getPackageManager().getApplicationInfo(
278 | context.getPackageName(), PackageManager.GET_META_DATA).metaData;
279 | if (metaData != null && metaData.containsKey(metaKey)) {
280 | return Boolean.parseBoolean(String.valueOf(metaData.get(metaKey)));
281 | }
282 | } catch (PackageManager.NameNotFoundException e) {
283 | e.printStackTrace();
284 | }
285 | return false;
286 | }
287 |
288 | /**
289 | * 判断 Activity 是否反方向旋转了
290 | */
291 | static boolean isActivityReverse(Activity activity) {
292 | // 获取 Activity 旋转的角度
293 | int activityRotation;
294 | if (AndroidVersion.isAndroid11()) {
295 | activityRotation = activity.getDisplay().getRotation();
296 | } else {
297 | activityRotation = activity.getWindowManager().getDefaultDisplay().getRotation();
298 | }
299 | switch (activityRotation) {
300 | case Surface.ROTATION_180:
301 | case Surface.ROTATION_270:
302 | return true;
303 | case Surface.ROTATION_0:
304 | case Surface.ROTATION_90:
305 | default:
306 | return false;
307 | }
308 | }
309 |
310 | /**
311 | * 判断这个意图的 Activity 是否存在
312 | */
313 | static boolean areActivityIntent(Context context, Intent intent) {
314 | return !context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).isEmpty();
315 | }
316 | }
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dahui888/HPermissions/dc84015c060bb41b138534ae8614b7f6b74dda7f/logo.png
--------------------------------------------------------------------------------
/picture/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dahui888/HPermissions/dc84015c060bb41b138534ae8614b7f6b74dda7f/picture/1.jpg
--------------------------------------------------------------------------------
/picture/10.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dahui888/HPermissions/dc84015c060bb41b138534ae8614b7f6b74dda7f/picture/10.jpg
--------------------------------------------------------------------------------
/picture/11.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dahui888/HPermissions/dc84015c060bb41b138534ae8614b7f6b74dda7f/picture/11.jpg
--------------------------------------------------------------------------------
/picture/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dahui888/HPermissions/dc84015c060bb41b138534ae8614b7f6b74dda7f/picture/2.jpg
--------------------------------------------------------------------------------
/picture/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dahui888/HPermissions/dc84015c060bb41b138534ae8614b7f6b74dda7f/picture/3.jpg
--------------------------------------------------------------------------------
/picture/4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dahui888/HPermissions/dc84015c060bb41b138534ae8614b7f6b74dda7f/picture/4.jpg
--------------------------------------------------------------------------------
/picture/5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dahui888/HPermissions/dc84015c060bb41b138534ae8614b7f6b74dda7f/picture/5.jpg
--------------------------------------------------------------------------------
/picture/6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dahui888/HPermissions/dc84015c060bb41b138534ae8614b7f6b74dda7f/picture/6.jpg
--------------------------------------------------------------------------------
/picture/7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dahui888/HPermissions/dc84015c060bb41b138534ae8614b7f6b74dda7f/picture/7.jpg
--------------------------------------------------------------------------------
/picture/8.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dahui888/HPermissions/dc84015c060bb41b138534ae8614b7f6b74dda7f/picture/8.jpg
--------------------------------------------------------------------------------
/picture/9.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dahui888/HPermissions/dc84015c060bb41b138534ae8614b7f6b74dda7f/picture/9.jpg
--------------------------------------------------------------------------------
/picture/location_1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dahui888/HPermissions/dc84015c060bb41b138534ae8614b7f6b74dda7f/picture/location_1.jpg
--------------------------------------------------------------------------------
/picture/location_2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dahui888/HPermissions/dc84015c060bb41b138534ae8614b7f6b74dda7f/picture/location_2.jpg
--------------------------------------------------------------------------------
/picture/miui_1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dahui888/HPermissions/dc84015c060bb41b138534ae8614b7f6b74dda7f/picture/miui_1.jpg
--------------------------------------------------------------------------------
/picture/miui_2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dahui888/HPermissions/dc84015c060bb41b138534ae8614b7f6b74dda7f/picture/miui_2.jpg
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':library'
2 |
--------------------------------------------------------------------------------