├── .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 | ![](picture/location_1.jpg) 36 | 37 | ![](picture/location_2.jpg) 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 | ![](picture/7.jpg) 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 | ![](picture/miui_1.jpg) 260 | 261 | ![](picture/miui_2.jpg) 262 | 263 | * 最大的问题在于:这两个界面是不同的 Activity,一个是小米定制的权限设置页,第二个是谷歌原生的权限设置页,当 miui 优化开启的时候,在小米定制的权限设置页授予权限才能有效果,当这个 miui 优化关闭的时候,在谷歌原生的权限设置页授予权限才能有效果。而跳转到国产手机页永远只会跳转到小米定制的那个权限设置页,所以就会导致当 miui 优化关闭的时候,使用代码跳转到小米权限设置页授予了权限之后返回仍然显示失败的问题。 264 | 265 | * 有人可能会说,解决这个问题的方式很简单,判断 miui 优化开关,如果是开启状态就跳转到小米定制的权限设置页,如果是关闭状态就跳转到谷歌原生的权限设置页,这样不就可以了?其实这个解决方案我也有尝试过,我曾委托联系到在小米工作的 miui 工程师,也有人帮我反馈这个问题给小米那边,最后得到答复都是一致的。 266 | 267 | ![](picture/miui_3.jpg) 268 | 269 | ![](picture/miui_4.jpg) 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 | ![](logo.png) 4 | 5 | * 项目地址:[Github](https://github.com/dahui888/HPermissions) 6 | 7 | * [点击此处可直接下载](https://github.com/dahui888/HPermissions/blob/master/HPermissions.apk) 8 | 9 | ![](picture/demo_code.png) 10 | 11 | ![](picture/1.jpg) ![](picture/2.jpg) ![](picture/3.jpg) 12 | 13 | ![](picture/4.jpg) ![](picture/5.jpg) ![](picture/6.jpg) 14 | 15 | ![](picture/7.jpg) ![](picture/8.jpg) ![](picture/9.jpg) 16 | 17 | ![](picture/10.jpg) ![](picture/11.jpg) 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://img.shields.io/github/issues/dahui888/HPermissions.svg)](https://github.com/dahui888/HPermissions/issues) | [![](https://img.shields.io/github/issues/yanzhenjie/AndPermission.svg)](https://github.com/yanzhenjie/AndPermission/issues) | [![](https://img.shields.io/github/issues/guolindev/PermissionX.svg)](https://github.com/guolindev/PermissionX/issues) | [![](https://img.shields.io/github/issues/Blankj/AndroidUtilCode.svg)](https://github.com/Blankj/AndroidUtilCode/issues) | [![](https://img.shields.io/github/issues/tbruyelle/RxPermissions.svg)](https://github.com/tbruyelle/RxPermissions/issues) | [![](https://img.shields.io/github/issues/permissions-dispatcher/PermissionsDispatcher.svg)](https://github.com/permissions-dispatcher/PermissionsDispatcher/issues) | [![](https://img.shields.io/github/issues/googlesamples/easypermissions.svg)](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 |