Repository: getActivity/XXPermissions Branch: master Commit: 1384135be5a8 Files: 157 Total size: 942.7 KB Directory structure: gitextract_kzfq42ja/ ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── config.yml │ │ ├── issue_en_template_bug.yml │ │ ├── issue_en_template_question.yml │ │ ├── issue_en_template_suggest.yml │ │ ├── issue_zh_template_bug.yml │ │ ├── issue_zh_template_question.yml │ │ └── issue_zh_template_suggest.yml │ └── workflows/ │ └── android.yml ├── .gitignore ├── Details-en.md ├── Details-zh.md ├── HelpDoc-en.md ├── HelpDoc-zh.md ├── LICENSE ├── README-en.md ├── README.md ├── app/ │ ├── AppSignature.jks │ ├── build.gradle │ ├── gradle.properties │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── hjq/ │ │ └── permissions/ │ │ └── demo/ │ │ ├── AppApplication.java │ │ ├── HealthDataPrivacyPolicyActivity.java │ │ ├── MainActivity.java │ │ ├── WindowLifecycleManager.java │ │ ├── example/ │ │ │ ├── ExampleAccessibilityService.java │ │ │ ├── ExampleDeviceAdminReceiver.java │ │ │ ├── ExampleNotificationListenerService.java │ │ │ └── ExampleVpnService.java │ │ └── permission/ │ │ ├── PermissionConverter.java │ │ ├── PermissionDescription.java │ │ └── PermissionInterceptor.java │ └── res/ │ ├── drawable/ │ │ └── permission_description_popup_bg.xml │ ├── layout/ │ │ ├── activity_main.xml │ │ ├── health_data_privacy_policy_activity.xml │ │ └── permission_description_popup.xml │ ├── values/ │ │ ├── colors.xml │ │ ├── strings_demo.xml │ │ ├── strings_permission.xml │ │ └── styles.xml │ ├── values-v21/ │ │ └── styles.xml │ ├── values-zh/ │ │ ├── strings_demo.xml │ │ └── strings_permission.xml │ └── xml/ │ ├── accessibility_service_config.xml │ ├── device_admin_config.xml │ └── locales_config.xml ├── build.gradle ├── common.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── library/ │ ├── build.gradle │ ├── proguard-permissions.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── hjq/ │ └── permissions/ │ ├── DefaultPermissionDescription.java │ ├── DefaultPermissionInterceptor.java │ ├── OnPermissionCallback.java │ ├── OnPermissionDescription.java │ ├── OnPermissionInterceptor.java │ ├── XXPermissions.java │ ├── core/ │ │ ├── OnPermissionFragmentCallback.java │ │ ├── PermissionChannelImpl.java │ │ ├── PermissionChannelImplByRequestPermissions.java │ │ ├── PermissionChannelImplByStartActivity.java │ │ └── PermissionRequestMainLogic.java │ ├── fragment/ │ │ ├── IFragmentCallback.java │ │ ├── IFragmentMethod.java │ │ ├── IFragmentMethodExtension.java │ │ ├── IFragmentMethodNative.java │ │ ├── factory/ │ │ │ ├── PermissionFragmentFactory.java │ │ │ ├── PermissionFragmentFactoryByAndroid.java │ │ │ └── PermissionFragmentFactoryByAndroidX.java │ │ └── impl/ │ │ ├── android/ │ │ │ ├── PermissionAndroidFragment.java │ │ │ ├── PermissionAndroidFragmentByRequestPermissions.java │ │ │ └── PermissionAndroidFragmentByStartActivity.java │ │ └── androidx/ │ │ ├── PermissionAndroidXFragment.java │ │ ├── PermissionAndroidXFragmentByRequestPermissions.java │ │ └── PermissionAndroidXFragmentByStartActivity.java │ ├── manager/ │ │ ├── ActivityOrientationManager.java │ │ ├── AlreadyRequestPermissionsManager.java │ │ └── PermissionRequestCodeManager.java │ ├── manifest/ │ │ ├── AndroidManifestInfo.java │ │ ├── AndroidManifestParser.java │ │ └── node/ │ │ ├── ActivityManifestInfo.java │ │ ├── ApplicationManifestInfo.java │ │ ├── BroadcastReceiverManifestInfo.java │ │ ├── IntentFilterManifestInfo.java │ │ ├── MetaDataManifestInfo.java │ │ ├── PermissionManifestInfo.java │ │ ├── ServiceManifestInfo.java │ │ └── UsesSdkManifestInfo.java │ ├── permission/ │ │ ├── PermissionChannel.java │ │ ├── PermissionGroups.java │ │ ├── PermissionLists.java │ │ ├── PermissionNames.java │ │ ├── PermissionPageType.java │ │ ├── base/ │ │ │ ├── BasePermission.java │ │ │ └── IPermission.java │ │ ├── common/ │ │ │ ├── DangerousPermission.java │ │ │ └── SpecialPermission.java │ │ ├── dangerous/ │ │ │ ├── AccessBackgroundLocationPermission.java │ │ │ ├── AccessMediaLocationPermission.java │ │ │ ├── BluetoothAdvertisePermission.java │ │ │ ├── BluetoothConnectPermission.java │ │ │ ├── BluetoothScanPermission.java │ │ │ ├── BodySensorsBackgroundPermission.java │ │ │ ├── BodySensorsPermission.java │ │ │ ├── GetInstalledAppsPermission.java │ │ │ ├── HealthDataBasePermission.java │ │ │ ├── NearbyWifiDevicesPermission.java │ │ │ ├── PostNotificationsPermission.java │ │ │ ├── ReadExternalStoragePermission.java │ │ │ ├── ReadHealthDataHistoryPermission.java │ │ │ ├── ReadHealthDataInBackgroundPermission.java │ │ │ ├── ReadHealthRatePermission.java │ │ │ ├── ReadMediaAudioPermission.java │ │ │ ├── ReadMediaImagesPermission.java │ │ │ ├── ReadMediaVideoPermission.java │ │ │ ├── ReadMediaVisualUserSelectedPermission.java │ │ │ ├── ReadPhoneNumbersPermission.java │ │ │ ├── StandardDangerousPermission.java │ │ │ ├── StandardFitnessAndWellnessDataPermission.java │ │ │ ├── StandardHealthRecordsPermission.java │ │ │ └── WriteExternalStoragePermission.java │ │ └── special/ │ │ ├── AccessNotificationPolicyPermission.java │ │ ├── BindAccessibilityServicePermission.java │ │ ├── BindDeviceAdminPermission.java │ │ ├── BindNotificationListenerServicePermission.java │ │ ├── BindVpnServicePermission.java │ │ ├── ManageExternalStoragePermission.java │ │ ├── ManageMediaPermission.java │ │ ├── NotificationServicePermission.java │ │ ├── PackageUsageStatsPermission.java │ │ ├── PictureInPicturePermission.java │ │ ├── RequestIgnoreBatteryOptimizationsPermission.java │ │ ├── RequestInstallPackagesPermission.java │ │ ├── ScheduleExactAlarmPermission.java │ │ ├── SystemAlertWindowPermission.java │ │ ├── UseFullScreenIntentPermission.java │ │ └── WriteSettingsPermission.java │ ├── start/ │ │ ├── IStartActivityDelegate.java │ │ ├── StartActivityAgent.java │ │ ├── StartActivityDelegateByActivity.java │ │ ├── StartActivityDelegateByContext.java │ │ ├── StartActivityDelegateByFragmentAndroid.java │ │ └── StartActivityDelegateByFragmentAndroidX.java │ └── tools/ │ ├── PermissionApi.java │ ├── PermissionChecker.java │ ├── PermissionSettingPage.java │ ├── PermissionTaskHandler.java │ ├── PermissionUtils.java │ └── PermissionVersion.java └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: https://raw.githubusercontent.com/getActivity/Donate/master/picture/pay_ali.png ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false ================================================ FILE: .github/ISSUE_TEMPLATE/issue_en_template_bug.yml ================================================ name: Submit Bug description: Please let me know the issues with the framework, and I will assist you in resolving them! title: "[Bug]:" labels: ["bug"] body: - type: markdown attributes: value: | ## [Warning: Please make sure to fill in the issue template accurately. If an issue is found to be filled incorrectly, it will be closed without further notice.](https://github.com/getActivity/IssueTemplateGuide) - type: input id: input_id_1 attributes: label: Framework Version [Required] description: Please enter the version of the framework you are using. validations: required: true - type: textarea id: input_id_2 attributes: label: Issue Description [Required] description: Please describe the issue you are facing. validations: required: true - type: textarea id: input_id_3 attributes: label: Steps to Reproduce [Required] description: Please provide steps to reproduce the issue. validations: required: true - type: dropdown id: input_id_4 attributes: label: Is the Issue Reproducible? [Required] multiple: false options: - "Not Selected" - "Yes" - "No" validations: required: true - type: input id: input_id_5 attributes: label: Project targetSdkVersion [Required] validations: required: true - type: input id: input_id_6 attributes: label: Device Information [Required] description: Please provide the brand and model of the device where the issue occurred. validations: required: true - type: input id: input_id_7 attributes: label: Android Version [Required] description: Please provide the Android version where the issue occurred. validations: required: true - type: dropdown id: input_id_8 attributes: label: Issue Source Channel [Required] multiple: true options: - Encountered by myself - Identified in Bugly - User feedback - Other channels - type: input id: input_id_9 attributes: label: Is it specific to certain device models? [Required] description: Specify whether the issue is specific to certain devices (e.g., specific brand or Android version). validations: required: true - type: dropdown id: input_id_10 attributes: label: Does the latest version of the framework have this issue? [Required] description: If you are using an older version, it is recommended to upgrade and check if the issue still persists. multiple: false options: - "Not Selected" - "Yes" - "No" validations: required: true - type: dropdown id: input_id_11 attributes: label: Is the issue mentioned in the framework documentation? [Required] description: The documentation provides answers to frequently asked questions. Please check if the information you are looking for is already provided. multiple: false options: - "Not Selected" - "Yes" - "No" validations: required: true - type: dropdown id: input_id_12 attributes: label: Did you consult the framework documentation but couldn't find a solution? [Required] description: If you have consulted the documentation but still couldn't find a solution, you can select "Yes." multiple: false options: - "Not Selected" - "Yes" - "No" validations: required: true - type: dropdown id: input_id_13 attributes: label: Has a similar issue been reported in the issue list? [Required] description: You can search the issue list for keywords related to your problem and refer to the solutions provided by others. multiple: false options: - "Not Selected" - "Yes" - "No" validations: required: true - type: dropdown id: input_id_14 attributes: label: Have you searched the issue list but couldn't find a solution? [Required] description: If you have searched the issue list and couldn't find a solution, you can select "Yes." multiple: false options: - "Not Selected" - "Yes" - "No" validations: required: true - type: dropdown id: input_id_15 attributes: label: Can the issue be reproduced with a demo project? [Required] description: Check if the issue can be reproduced in a minimal demo project to isolate potential issues in your own code. multiple: false options: - "Not Selected" - "Yes" - "No" validations: required: true - type: textarea id: input_id_16 attributes: label: Provide Error Stack Trace description: If there is an error, please provide the stack trace. Note, Do not include obfuscated code in the stack trace. render: text validations: required: false - type: textarea id: input_id_17 attributes: label: Provide Screenshots or Videos description: Provide screenshots or videos if necessary. This field is optional. validations: required: false - type: textarea id: input_id_18 attributes: label: Provide a Solution description: If you have already found a solution, this field is optional. validations: required: false ================================================ FILE: .github/ISSUE_TEMPLATE/issue_en_template_question.yml ================================================ name: Ask a Question description: Ask your questions, and I will provide you with answers. title: "[Question]:" labels: ["question"] body: - type: markdown attributes: value: | ## [Warning: Please make sure to fill in the issue template accurately. If an issue is found to be filled incorrectly, it will be closed without further notice.](https://github.com/getActivity/IssueTemplateGuide) - type: textarea id: input_id_1 attributes: label: Question Description [Required] description: Please describe your question (Note, If it is a framework bug, please do not raise it here, as it will not be accepted). validations: required: true - type: dropdown id: input_id_2 attributes: label: Is the issue mentioned in the framework documentation? [Required] description: The documentation provides answers to frequently asked questions. Please check if the information you are looking for is already provided. multiple: false options: - "Not Selected" - "Yes" - "No" validations: required: true - type: dropdown id: input_id_3 attributes: label: Did you consult the framework documentation but couldn't find a solution? [Required] description: If you have consulted the documentation but still couldn't find a solution, you can select "Yes." multiple: false options: - "Not Selected" - "Yes" - "No" validations: required: true - type: dropdown id: input_id_4 attributes: label: Has a similar issue been reported in the issue list? [Required] description: You can search the issue list for keywords related to your problem and refer to the solutions provided by others. multiple: false options: - "Not Selected" - "Yes" - "No" validations: required: true - type: dropdown id: input_id_5 attributes: label: Have you searched the issue list but couldn't find a solution? [Required] description: If you have searched the issue list and couldn't find a solution, you can select "Yes." multiple: false options: - "Not Selected" - "Yes" - "No" validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/issue_en_template_suggest.yml ================================================ name: Submit Suggestion description: Please let me know the shortcomings of the framework, so that I can improve it! title: "[Suggestion]:" labels: ["help wanted"] body: - type: markdown attributes: value: | ## [Warning: Please make sure to fill in the issue template accurately. If an issue is found to be filled incorrectly, it will be closed without further notice.](https://github.com/getActivity/IssueTemplateGuide) - type: textarea id: input_id_1 attributes: label: What are the shortcomings you have noticed in the framework? [Required] description: You can describe any aspects of the framework that you are not satisfied with. validations: required: true - type: dropdown id: input_id_2 attributes: label: Has a similar suggestion been made in the issue list? [Required] description: If a similar suggestion has already been made, I will not address it again. multiple: false options: - "Not Selected" - "Yes" - "No" validations: required: true - type: dropdown id: input_id_3 attributes: label: Is the suggestion mentioned in the framework documentation? [Required] description: The documentation provides answers to frequently asked questions. Please check if the information you are looking for is already provided. multiple: false options: - "Not Selected" - "Yes" - "No" validations: required: true - type: dropdown id: input_id_4 attributes: label: Did you consult the framework documentation but couldn't find a solution? [Required] description: If you have consulted the documentation but still couldn't find a solution, you can select "Yes." multiple: false options: - "Not Selected" - "Yes" - "No" validations: required: true - type: textarea id: input_id_5 attributes: label: How do you suggest improving it? [Optional] description: You can provide your ideas or approaches for the author's reference. validations: required: false ================================================ FILE: .github/ISSUE_TEMPLATE/issue_zh_template_bug.yml ================================================ name: 提交 Bug description: 请告诉我框架存在的问题,我会协助你解决此问题! title: "[Bug]:" labels: ["bug"] body: - type: markdown attributes: value: | ## [【警告:请务必按照 issue 模板填写,不要抱有侥幸心理,一旦发现 issue 没有按照模板认真填写,一律直接关闭】](https://github.com/getActivity/IssueTemplateGuide) - type: input id: input_id_1 attributes: label: 框架版本【必填】 description: 请输入你使用的框架版本 validations: required: true - type: textarea id: input_id_2 attributes: label: 问题描述【必填】 description: 请输入你对这个问题的描述 validations: required: true - type: textarea id: input_id_3 attributes: label: 复现步骤【必填】 description: 请输入问题的复现步骤 validations: required: true - type: dropdown id: input_id_4 attributes: label: 是否必现【必填】 multiple: false options: - "未选择" - "是" - "否" validations: required: true - type: input id: input_id_5 attributes: label: 项目 targetSdkVersion【必填】 validations: required: true - type: input id: input_id_6 attributes: label: 出现问题的手机信息【必填】 description: 请填写出现问题的品牌和机型 validations: required: true - type: input id: input_id_7 attributes: label: 出现问题的安卓版本【必填】 description: 请填写出现问题的 Android 版本 validations: required: true - type: dropdown id: input_id_8 attributes: label: 问题信息的来源渠道【必填】 multiple: true options: - 自己遇到的 - Bugly 看到的 - 用户反馈 - 其他渠道 - type: input id: input_id_9 attributes: label: 是部分机型还是所有机型都会出现【必答】 description: 部分/全部(例如:某为,某 Android 版本会出现) validations: required: true - type: dropdown id: input_id_10 attributes: label: 框架最新的版本是否存在这个问题【必答】 description: 如果用的是旧版本的话,建议升级看问题是否还存在 multiple: false options: - "未选择" - "是" - "否" validations: required: true - type: dropdown id: input_id_11 attributes: label: 框架文档是否提及了该问题【必答】 description: 文档会提供最常见的问题解答,可以先看看是否有自己想要的 multiple: false options: - "未选择" - "是" - "否" validations: required: true - type: dropdown id: input_id_12 attributes: label: 是否已经查阅框架文档但还未能解决的【必答】 description: 如果查阅了文档但还是没有解决的话,可以选择是 multiple: false options: - "未选择" - "是" - "否" validations: required: true - type: dropdown id: input_id_13 attributes: label: issue 列表中是否有人曾提过类似的问题【必答】 description: 可以在 issue 列表在搜索问题关键字,参考一下别人的解决方案 multiple: false options: - "未选择" - "是" - "否" validations: required: true - type: dropdown id: input_id_14 attributes: label: 是否已经搜索过了 issue 列表但还未能解决的【必答】 description: 如果搜索过了 issue 列表但是问题没有解决的话,可以选择是 multiple: false options: - "未选择" - "是" - "否" validations: required: true - type: dropdown id: input_id_15 attributes: label: 是否可以通过 Demo 来复现该问题【必答】 description: 排查一下是不是自己的项目代码写得有问题导致的 multiple: false options: - "未选择" - "是" - "否" validations: required: true - type: textarea id: input_id_16 attributes: label: 提供报错堆栈 description: 如果有报错的话必填,注意不要拿被混淆过的代码堆栈上来 render: text validations: required: false - type: textarea id: input_id_17 attributes: label: 提供截图或视频 description: 根据需要提供,此项不强制 validations: required: false - type: textarea id: input_id_18 attributes: label: 提供解决方案 description: 如果已经解决了的话,此项不强制 validations: required: false ================================================ FILE: .github/ISSUE_TEMPLATE/issue_zh_template_question.yml ================================================ name: 提出疑问 description: 提出你的困惑,我会给你解答 title: "[疑惑]:" labels: ["question"] body: - type: markdown attributes: value: | ## [【警告:请务必按照 issue 模板填写,不要抱有侥幸心理,一旦发现 issue 没有按照模板认真填写,一律直接关闭】](https://github.com/getActivity/IssueTemplateGuide) - type: textarea id: input_id_1 attributes: label: 问题描述【必填】 description: 请描述一下你的问题(注意:如果确定是框架 bug 请不要在这里提,否则一概不受理) validations: required: true - type: dropdown id: input_id_2 attributes: label: 框架文档是否提及了该问题【必答】 description: 文档会提供最常见的问题解答,可以先看看是否有自己想要的 multiple: false options: - "未选择" - "是" - "否" validations: required: true - type: dropdown id: input_id_3 attributes: label: 是否已经查阅框架文档但还未能解决的【必答】 description: 如果查阅了文档但还是没有解决的话,可以选择是 multiple: false options: - "未选择" - "是" - "否" validations: required: true - type: dropdown id: input_id_4 attributes: label: issue 列表中是否有人曾提过类似的问题【必答】 description: 可以在 issue 列表在搜索问题关键字,参考一下别人的解决方案 multiple: false options: - "未选择" - "是" - "否" validations: required: true - type: dropdown id: input_id_5 attributes: label: 是否已经搜索过了 issue 列表但还未能解决的【必答】 description: 如果搜索过了 issue 列表但是问题没有解决的话,可以选择是 multiple: false options: - "未选择" - "是" - "否" validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/issue_zh_template_suggest.yml ================================================ name: 提交建议 description: 请告诉我框架的不足之处,让我做得更好! title: "[建议]:" labels: ["help wanted"] body: - type: markdown attributes: value: | ## [【警告:请务必按照 issue 模板填写,不要抱有侥幸心理,一旦发现 issue 没有按照模板认真填写,一律直接关闭】](https://github.com/getActivity/IssueTemplateGuide) - type: textarea id: input_id_1 attributes: label: 你觉得框架有什么不足之处?【必答】 description: 你可以描述框架有什么令你不满意的地方 validations: required: true - type: dropdown id: input_id_2 attributes: label: issue 是否有人曾提过类似的建议?【必答】 description: 一旦出现重复提问我将不会再次解答 multiple: false options: - "未选择" - "是" - "否" validations: required: true - type: dropdown id: input_id_3 attributes: label: 框架文档是否提及了该问题【必答】 description: 文档会提供最常见的问题解答,可以先看看是否有自己想要的 multiple: false options: - "未选择" - "是" - "否" validations: required: true - type: dropdown id: input_id_4 attributes: label: 是否已经查阅框架文档但还未能解决的【必答】 description: 如果查阅了文档但还是没有解决的话,可以选择是 multiple: false options: - "未选择" - "是" - "否" validations: required: true - type: textarea id: input_id_5 attributes: label: 你觉得该怎么去完善会比较好?【非必答】 description: 你可以提供一下自己的想法或者做法供作者参考 validations: required: false ================================================ FILE: .github/workflows/android.yml ================================================ name: Android CI on: [push] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: set up JDK 1.8 uses: actions/setup-java@v1 with: java-version: 1.8 ================================================ FILE: .gitignore ================================================ .gradle .idea .cxx .externalNativeBuild build captures ._* *.iml .DS_Store local.properties ================================================ FILE: Details-en.md ================================================ #### Table of Contents * [Intent Extreme Jump Fallback Mechanism](#intent-extreme-jump-fallback-mechanism) * [Compatibility with Permission Request API Crash Issues](#compatibility-with-permission-request-api-crash-issues) * [Avoiding System Permission Callback Null Pointer Issues](#avoiding-system-permission-callback-null-pointer-issues) * [Automatic Permission Split Requests](#automatic-permission-split-requests) * [Framework Completely Separates UI Layer](#framework-completely-separates-ui-layer) * [Core Logic and Specific Permissions Completely Decoupled](#core-logic-and-specific-permissions-completely-decoupled) * [Automatic Background Permission Adaptation](#automatic-background-permission-adaptation) * [Support for Cross-Platform Environment Calls](#support-for-cross-platform-environment-calls) * [Callback Lifecycle Synchronized with Host](#callback-lifecycle-synchronized-with-host) * [Support for Custom Permission Requests](#support-for-custom-permission-requests) * [New Version Permissions Support Backward Compatibility](#new-version-permissions-support-backward-compatibility) * [Screen Rotation Scenario Adaptation](#screen-rotation-scenario-adaptation) * [Background Permission Request Scenario Adaptation](#background-permission-request-scenario-adaptation) * [Fix Android 12 Memory Leak Issue](#fix-android-12-memory-leak-issue) * [Support for Code Error Detection](#support-for-code-error-detection) #### Intent Extreme Jump Fallback Mechanism * Before introducing this feature, let me ask you a question: please analyze if there's any problem with this code? ```java Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); intent.setData("package:" + getPackageName()); startActivityForResult(intent, 1024); ``` * You might say: It's simple, this is just code to jump to the application details page, what could be wrong with it? Are you trying to trick me? * This code seems to have no problems and runs fine, but it's actually a huge pitfall. Some manufacturers have directly removed the `ACTION_APPLICATION_DETAILS_SETTINGS` intent. Yes, you heard right - completely removed it. When this code runs on these devices, the application will crash. I'm not joking: ```text android.content.ActivityNotFoundException: No Activity found to handle Intent { act=android.settings.APPLICATION_DETAILS_SETTINGS dat=Package Name:com.xxx.xxx } ``` * If you still don't believe me, look here [Github Search `No Activity found to handle Intent act=android.settings.APPLICATION_DETAILS_SETTINGS`](https://github.com/search?q=No+Activity+found+to+handle+Intent++act%3Dandroid.settings.APPLICATION_DETAILS_SETTINGS&type=issues): * It's not just the `ACTION_APPLICATION_DETAILS_SETTINGS` intent; other intents have the same issue. If you don't believe me, check here [Github Search `No Activity found to handle Intent act=android`](https://github.com/search?q=No+Activity+found+to+handle+Intent++act%3Dandroid&type=issues). ``` android.content.ActivityNotFoundException: No Activity found to handle Intent { act=android.settings.MANAGE_UNKNOWN_APP_SOURCES (has data) } ``` * After reading this, you might want to complain, but the problem exists, and irrational complaints never solve problems. Only rational analysis and serious thinking are the way out. The issue is that the `Intent` can't be found. The simplest and most effective solution is to check if the `Intent` exists before jumping. If it exists, then jump; if not, don't jump. But if you think that's all there is to it, you're being too simplistic. Things are rarely as simple as they seem. Non-existent `Intent` jumps will fail, but have you considered that even existing `Intent` jumps don't guarantee success? If you don't believe me, look here [Github Search `Permission Denial: starting Intent`](https://github.com/search?q=Permission+Denial%3A+starting+Intent&type=issues). Now you understand why I called it a pitfall? ```text java.lang.SecurityException: Permission Denial: starting Intent { act=android.settings.MANAGE_UNKNOWN_APP_SOURCES (has data) cmp=xxxx/xxx } ``` * I'm not saying this to make you solve the problem, but to make you aware that it exists. Of course, the framework has already handled this issue. All the problems you can think of, the framework has already thought of and handled for you. Just one line of code, call the `XXPermissions.startPermissionActivity` method. If you're curious about how the framework implements this but too lazy to look at the source code, I've got you covered. The principle is actually very simple: when the framework gets the permission settings page, it puts all possible `Intent`s in a List collection, filters out non-existent `Intent`s, and then tries each `Intent` one by one. If one fails, it jumps to the next one, until it succeeds or there are no more `Intent`s left. #### Compatibility with Permission Request API Crash Issues * Before introducing this feature, let me ask you a question: please analyze if there's any problem with this code? ```java activity.requestPermissions(new String[]{Manifest.permission.CAMERA}, 1024); ``` * You might say: This is just a simple code using the system API to request permissions. What could be wrong with it? As long as you don't call it on devices below Android 6.0, it should be fine. * Theoretically, that's correct, but theory is just theory. In reality, calling this on Android 6.0 and above devices can also cause crashes. Yes, you didn't misread - Android 6.0 and above can crash. It sounds magical that such an important system API could crash. If you don't believe me, check here [XXPermissions/issues/153](https://github.com/getActivity/XXPermissions/issues/153), [XXPermissions/issues/126](https://github.com/getActivity/XXPermissions/issues/126), [XXPermissions/issues/327](https://github.com/getActivity/XXPermissions/issues/327), [XXPermissions/issues/339](https://github.com/getActivity/XXPermissions/issues/339), or if that's not enough, look here [Github Search `act=android.content.pm.action.REQUEST_PERMISSIONS`](https://github.com/search?q=act%3Dandroid.content.pm.action.REQUEST_PERMISSIONS&type=issues). Doesn't that instantly change your understanding? ```text android.content.ActivityNotFoundException: No Activity found to handle Intent { act=android.content.pm.action.REQUEST_PERMISSIONS pkg=com.android.packageinstaller (has extras) } ``` * This situation can occur for several reasons: 1. Manufacturer developers changed the package name of the `com.android.packageinstaller` system application but didn't test it properly before release (low probability) 2. Manufacturer developers deleted the `com.android.packageinstaller` system application but didn't test it properly before release (low probability) 3. Manufacturer developers modified Android system source code affecting the permission module but didn't test it properly before release (low probability) 4. Manufacturers actively cut the permission request function, for example on TV devices, indirectly causing apps requesting dangerous permissions to crash when requesting permissions (low probability) 5. Users have Root privileges and accidentally deleted the `com.android.packageinstaller` system application when streamlining system apps (higher probability) * After analyzing the source code of `Activity.requestPermissions`, it essentially still calls `startActivityForResult`, but the `Activity` can't be found. The best solution I can think of is to use `try catch` to prevent it from crashing. You might wonder if simply using `try catch` is enough? Won't it cause other problems? Won't it cause `onRequestPermissionsResult` not to be called, leading to the permission request process getting stuck? Although this problem can't be tested, theoretically it shouldn't happen. I experimented by using an incorrect `Intent` with `startActivityForResult` and `try catch`, and found that `onActivityResult` was still normally called by the system. This proves that using `try catch` with `startActivityForResult` doesn't affect the `onActivityResult` callback. I also analyzed the source code for `Activity` callbacks and found that both `onRequestPermissionsResult` and `onActivityResult` are called by the `dispatchActivityResult` method. In that extreme case, since `onActivityResult` can be called, it proves that `dispatchActivityResult` must have been normally called by the system, and similarly, `onRequestPermissionsResult` must also be normally called by `dispatchActivityResult`, forming a complete logical loop. * Additional test conclusion: I debugged the `Activity.requestPermissions` method and secretly modified the permission request `Intent`'s `Action` to an incorrect one, and the permission callback still worked normally. * If this extreme situation does occur, all dangerous permission requests will necessarily go through the failure callback, but what the framework can do is: try to prevent the application from crashing and ensure it completes the entire permission request process. #### Avoiding System Permission Callback Null Pointer Issues * Before introducing this feature, let me ask you a question: please analyze if there's any problem with this code? ```java public final class XxxActivity extends AppCompatActivity { @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (permissions.length == 0 || grantResults.length == 0) { return; } if (permissions[0].equals(Manifest.permission.CAMERA) && grantResults[0] == PackageManager.PERMISSION_GRANTED) { System.out.println("Camera permission granted successfully"); } else { System.out.println("Failed to get camera permission"); } } } ``` * You might say: This is normal handling of permission request results in the permission callback. I write it like this all the time. It looks fine to me. Are you just finding fault? * What if I told you that the `permissions` or `grantResults` array parameters returned by the system could be null? Would you believe it? I know you probably don't, because you see that both `permissions` and `grantResults` parameters have `@NonNull` annotations (and if you look at the Activity source code, you'll also see `@NonNull` annotations), which means the system should never return null. At this point, you probably think I'm deceiving you. * I know you don't believe me, so I've prepared evidence. Please look here [XXPermissions/issues/191](https://github.com/getActivity/XXPermissions/issues/191), [XXPermissions/issues/106](https://github.com/getActivity/XXPermissions/issues/106), [XXPermissions/issues/236](https://github.com/getActivity/XXPermissions/issues/236), or if that's not enough, look here [Github Search `NullPointerException onRequestPermissionsResult`](https://github.com/search?q=NullPointerException+onRequestPermissionsResult&type=issues); * After reading this, what are you thinking? What is the system trying to do? Marking parameters as non-null but returning null - isn't that deceiving us? The problem exists, and irrational complaints never solve problems. Only rational analysis and serious thinking are the way out. * Currently, the device brands reporting this issue include vivo, Xiaomi, and Lenovo, indicating that this problem is likely another pit dug by `Google` engineers. There are two approaches to solving this problem: 1. Still use the `permissions` and `grantResults` parameters to determine the permission status: Before using them, first check the array objects for null, then continue using them. 2. No longer use the `permissions` and `grantResults` parameters to determine the permission status: Switch to using `checkSelfPermission` to determine the permission status. * Although both can solve the problem, there are slight differences. The framework ultimately adopts the second approach. There's a Chinese saying: "Once unfaithful, never trusted again." Since it can do such unprincipled things, we must guard against other tricks it might have, such as: 1. The returned array objects are not null, but there are no elements in the arrays. If not checked in advance, calling them could trigger an `ArrayIndexOutOfBoundsException`. 2. The returned array objects are not null, there are elements in the arrays, but the lengths of the `permissions` and `grantResults` arrays are different. If not checked in advance, calling them could trigger an `ArrayIndexOutOfBoundsException`. 3. The returned array objects are not null, there are elements in the arrays, the lengths of the two arrays are normal, but the returned `grantResults` don't match reality. The user clearly granted the permission, but the array stores `-1` (`PackageManager.PERMISSION_DENIED`). * At this point, you might suddenly realize that solving this null pointer problem isn't as simple as just adding a null check? There's so much more to it. I want to tell everyone that no matter what the problem is, I will take it seriously, because what I pursue is never just solving the problem, but finding the optimal solution among all possible solutions. #### Automatic Permission Split Requests * In some scenarios, you need to request multiple permissions at once, such as microphone permission and calendar permission. In this case, product managers may want to split the permissions into two separate requests to display separate explanation dialogs for each permission. This design makes feature development more complex. Without splitting the requests, you would only need to add logic to show and close the dialog before and after the permission request. Now with split requests, you can't write it that way. You have to write it separately, which means writing various nested callbacks. Just thinking about doing this makes your head spin, almost making you want to throw up last night's midnight snack. * I understand everyone's pain and frustration, so I added a processing mechanism to the framework that automatically categorizes the permissions you pass in. For example, microphone permission is grouped into one category, calendar permission into another, and then they are split into two separate permission requests. When combined with the permission explanation interface opened by the framework, which tells you what permission is being requested, you can display the specific permission explanation dialog based on the permission. With this, the feature is completed easily and elegantly. While the iOS team is still struggling with implementation, you've already completed it and left work early. No delays, no pain, just the satisfaction of implementing the feature. #### Framework Completely Separates UI Layer * Some permission frameworks implement a set of permission explanation dialog UI and logic internally, requiring specific interfaces to be implemented for modification. I believe this design is unreasonable because displaying a permission explanation dialog is not a mandatory operation. Without it, calling the permission request API will still pop up the authorization box. Additionally, when it comes to UI, the UI designed within the framework inevitably cannot satisfy everyone's needs (it's a thankless task) because everyone receives different design drafts. So the best solution is for the framework not to write UI and logic internally, but to design relevant interfaces for this aspect and then hand it over to the outer layer for implementation. Of course, the framework's Demo module will also implement a case for the outer layer to reference (or directly copy the code). This not only solves the problem of inconsistent UI requirements but also reduces the size of the framework - killing two birds with one stone. #### Core Logic and Specific Permissions Completely Decoupled * The frameworks you see on the market that can support both dangerous permissions and special permissions have very high code coupling. This leads to a problem: for example, if you only use it to request dangerous permissions, when packaging, it will include special permissions code logic in the APK. It's like wanting to eat fried chicken, but the clerk tells you that you can only get fried chicken if you order a ten-person set meal. You think to yourself that even if you stuff yourself, you can't finish a ten-person set meal. Isn't this design clearly a trap? Although an app with more code won't "die from overeating" like a person, we shouldn't waste resources recklessly. A little waste here, a little waste there, and after development, you look at the APK size and it's 250 MB, and you have to consider size optimization. The key is that you can't optimize it because this part of the code is hardcoded in the framework, and the framework is remotely dependent. You'd have to switch to local dependencies to make changes, which means there might be bugs that increase a lot of self-testing workload. Importantly, the benefits of making changes are low, but the risks are extremely high, and you could easily end up on the layoff list while making changes. * For this problem, the framework has a brilliant design solution: encapsulate the implementation of different permissions into objects. You pass whatever permission object you request, and objects that aren't referenced will be removed during code obfuscation. This way, when packaging the official version, there won't be redundant code, and it won't occupy extra APK size. It truly achieves "pay for what you use." You no longer have to consider whether to buy a ten-person set meal just to eat a piece of fried chicken. No need to hesitate or waver. With XXPermissions, you can buy separately, buy what you want to eat, buy as much as you want to eat, suitable for all ages, honest and fair. * Of course, for some frameworks that don't support any special permissions or handle specific dangerous permissions separately, but simply use the system's API - using `context.requestPermissions` to request permissions and `context.checkSelfPermission` to check permissions - does this count as completely decoupled? Actually, it does, because they indeed don't directly depend on specific permissions in the core logic. But such frameworks don't meet the needs of real-world development because in a commercialized app, it's impossible to only request dangerous permissions. Need notification permission? Need installation package permission? Need floating window permission? As long as these frameworks support any special permission, this problem will exist. Of course, if they don't support it, then there's no problem. But the key is whether it's possible to both support these permissions and decouple the code? This is the key to the problem, which really tests the understanding of the framework and code design. As of now, only XXPermissions has truly achieved both support and decoupling. #### Automatic Background Permission Adaptation * Android 10 added [background location permission](https://developer.android.google.cn/about/versions/11/privacy/location?hl=zh-cn#background-location) and Android 13 added [background sensor permission](https://developer.android.google.cn/about/versions/13/behavior-changes-13?hl=zh-cn#body-sensors-background-permission). Don't think these background permissions are no different from ordinary dangerous permissions; the differences are very significant, and if you don't understand them clearly, it's easy to encounter bugs. 1. Foreground and background permissions cannot be requested together. You must request foreground permissions first, then background permissions. Requesting background permissions without foreground permissions will definitely be rejected by the system, which is beyond doubt. 2. After Android 11, there must be a certain time interval between foreground and background permission requests. That is, after splitting the two permission requests, you must ensure that there is a certain time interval between them, otherwise the request will fail. Testing shows it cannot be less than 150 milliseconds. 3. The background location permission corresponds to two foreground location permissions: precise location permission (`ACCESS_FINE_LOCATION`) and approximate location permission (`ACCESS_COARSE_LOCATION`). In versions `Android 10 ~ Android 11`, the background location permission is anchored to the precise location permission. Only when this permission is granted can you request the background location permission. In Android 12 and later versions, the background location permission can be anchored to either the precise location permission or the approximate location permission. When either of these permissions is granted, you can request the background location permission. * However, the framework has already handled these issues for you, and you don't need to handle them manually. The specific handling solutions are as follows: 1. Automatically identify background permissions and their corresponding foreground permissions, then automatically split them into two permission requests: first request foreground permissions, then request background permissions. 2. When requesting background permissions, add a time delay first, which is the 150 milliseconds mentioned earlier, then request the background permissions, thus avoiding this issue. 3. When judging background location permissions, different Android versions are handled differently for foreground location permissions. In versions `Android 10 ~ Android 11`, precise location permission is used; in `Android 12` and later versions, either precise location permission or approximate location permission is used, ensuring the same expected effect across different Android versions. #### Support for Cross-Platform Environment Calls * As we all know, [FlutterActivity](https://github.com/flutter/flutter/blob/03ef1ba910cac387f7b2af8ab09ca955d3974663/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java), [ComposeActivity](https://github.com/androidx/androidx/blob/8d08d42d60f7cc7ec0034d0b7ff6fd953516d96a/emoji2/emoji2-emojipicker/samples/src/main/java/androidx/emoji2/emojipicker/samples/ComposeActivity.kt), [UnityPlayerActivity](https://github.com/FabianTerhorst/PokemonGo/blob/d511b045f1492e0bae71778ef528f3d768d218cd/java/com/unity3d/player/UnityPlayerActivity.java), [Cocos2dxActivity](https://github.com/irontec/Ikasesteka/blob/master/Ikasesteka/cocos2d/cocos/platform/android/java/src/org/cocos2dx/lib/Cocos2dxActivity.java) are all subclasses of Activity, but they are not subclasses of [FragmentActivity](https://github.com/androidx/androidx/blob/8d08d42d60f7cc7ec0034d0b7ff6fd953516d96a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java). Their inheritance relationships are as follows: 1. `FlutterActivity extends Activity` 2. `ComposeActivity extends ComponentActivity extends Activity` 3. `UnityPlayerActivity extends Activity` 4. `Cocos2dxActivity extends Activity` 5. `FragmentActivity extends ComponentActivity extends Activity` * This creates a problem: some permission request frameworks use a transparent `Fragment` to get permission request callbacks. If the permission request framework uses `androidx.fragment.app.Fragment`, then it must require the outer layer to pass in an `androidx.fragment.app.FragmentActivity` object. If the `Activity` you're using is not a subclass of `androidx.fragment.app.FragmentActivity`, what should you do? Simple, you might say, just modify the current `Activity` to directly inherit from `androidx.fragment.app.FragmentActivity`, right? But what if your current `Activity` must inherit from `FlutterActivity`, `ComposeActivity`, `UnityPlayerActivity`, or `Cocos2dxActivity`? What should you do then? Modify their source code? Or modify the permission framework's source code? Whichever solution you choose, the cost of adaptation will be very high and difficult to maintain in the future. This is neither realistic nor scientific. Do you suddenly feel like heaven has blocked your path? Is writing permission request API code by hand the only way to implement permission requests? * Actually, the framework has already thought of this problem and has solved it for you without requiring any handling on your part. The specific implementation principle is: the framework will determine if the `Activity` object you pass in is a subclass of `androidx.fragment.app.FragmentActivity`. If it is, it will use `androidx.fragment.app.Fragment` for permission requests; if not, it will use `android.app.Fragment` for permission requests. This way, no matter which type of `Activity` you use, the framework can automatically adapt. #### Callback Lifecycle Synchronized with Host * Most permission request frameworks on the market use a single type of `Fragment` to handle permission request callbacks, but this leads to a problem. Suppose a framework uses `androidx.fragment.app.Fragment` to handle permission request callbacks, but you initiated the permission request in `android.app.Fragment`, or vice versa, you used `androidx.fragment.app.Fragment` but the framework used `android.app.Fragment`. You can't pass your own `Fragment` as the host to the permission request framework; you can only pass the `Activity` object to the framework through `fragment.getActivity()`. This makes the `Activity` the host object, leading to a lifecycle asynchronization problem: your own `Fragment` may have been destroyed, but the framework will still callback the permission request result listener, causing `Memory leak` at best and triggering `Exception` at worst. * The reason for this problem is that the `Fragment` used by the third-party framework and your `Fragment` are not actually the same type. Although they have the same class name, they are in different packages. Plus, as just mentioned, you can only pass the `Activity` object to the framework through `fragment.getActivity()`, so your own `Fragment` cannot form an effective lifecycle binding with the framework's `Fragment`. What you actually want is to bind to your own `Fragment`'s lifecycle, but the framework ultimately binds to the `Activity`'s lifecycle, which can easily trigger a crash. You can see the specific manifestation in this issue: [XXPermissions/issues/365](https://github.com/getActivity/XXPermissions/issues/365). * Actually, the framework has already thought of this problem and has solved it for you without requiring any handling on your part. The approach to solving this problem is: the framework will automatically select the best type of `Fragment` based on the type of object you pass in. If the host you pass in is an `androidx.fragment.app.FragmentActivity` or `androidx.fragment.app.Fragment` object, the framework will internally create an `androidx.fragment.app.Fragment` to receive permission request callbacks. If the host you pass in is a regular `Activity` or `android.app.Fragment`, the framework will internally create an `android.app.Fragment` to receive permission request callbacks. This way, no matter what host object you pass in, the framework will bind to its lifecycle, ensuring that when the permission request result is called back to the outermost layer, the host object is still in a normal state. * At this point, you might jump in and say, I can implement this without the framework. I can manually check the `Fragment`'s state in the permission callback method. Isn't it just a matter of two or three lines of code? Why does the framework make it so complicated? Your idea seems reasonable but doesn't stand up to scrutiny. If your project requests permissions in a dozen places, you would need to consider this issue in every callback method. Additionally, when requesting new permissions in the future, you would also need to consider this issue. Can you ensure that you won't miss anything when making changes? And what if this requirement was developed by your colleague, but only you know about this issue, and they are unaware? Do you know what might happen in that case? I believe you understand better than I do. The solution you provided, although it can temporarily solve the problem, treats the symptoms but not the root cause. The fundamental issue is that the approach to solving the problem is flawed, following a patch-the-hole mentality rather than thinking about blocking the leak at the source. Or perhaps you already knew how to completely cure it but chose the easiest way to handle it, which undoubtedly plants a time bomb in the project. #### Support for Custom Permission Requests * As the name suggests, developers can not only request permissions already supported by the framework but also request permissions they define themselves. This feature is very powerful and can meet the needs of the following scenarios: 1. You can define and request permissions not supported by the framework, such as boot auto-start permission, desktop shortcut permission, read clipboard permission, write clipboard permission, operate external storage `Android/data` permission, specific manufacturer permissions, and even [Bluetooth switch, WIFI switch, location switch](https://github.com/getActivity/XXPermissions/issues/170), etc. Let your imagination run wild here. Now you only need to inherit the `DangerousPermission` or `SpecialPermission` class provided by the framework to implement custom permissions. In previous versions, this could only be achieved by modifying the framework's source code, which was very cumbersome. You not only had to study the framework's source code but also had to do strict self-testing after modification. Now you don't need to do that anymore; the framework provides this extension interface, and implementing one interface can achieve it. 2. Developers no longer need to rely on the permission framework author to adapt new permissions. When Google releases a new Android version with new permissions, and the framework hasn't had time to adapt, but you urgently need to request this new permission, you can use this feature to adapt the new permission first. #### New Version Permissions Support Backward Compatibility * With the continuous update of the Android version, dangerous permissions and special permissions are also increasing, so there will be a version compatibility problem at this time. Higher version Android devices support applying for lower version permissions, but lower version Android devices do not support If you apply for a higher version of the permission, then there will be a compatibility problem at this time. * After verification, other permission frameworks chose the simplest and rude way, which is not to do compatibility, but to the caller of the outer layer for compatibility. The caller needs to judge the Android version in the outer layer first, and upload it on the higher version. Enter new permissions to the framework, and pass the old permissions to the framework on the lower version. This method seems simple and rude, but the development experience is poor. At the same time, it also hides a pit. The outer callers know that the new permissions correspond to Which is the old permission of ? I think not everyone knows it, and once the cognition is wrong, it will inevitably lead to wrong results. * I think the best way is to leave it to the framework. **XXPermissions** does exactly that. When the outer caller applies for a higher version of the permission, then the lower version of the device will automatically add the lower version of the permission. To apply, to give the simplest example, the new `MANAGE_EXTERNAL_STORAGE` permission that appeared in Android 11, if it is applied for this permission on Android 10 and below devices, the framework will automatically add `READ_EXTERNAL_STORAGE` and `WRITE_EXTERNAL_STORAGE` to apply, in Android On Android 10 and below devices, we can directly use `READ_EXTERNAL_STORAGE` and `WRITE_EXTERNAL_STORAGE` as `MANAGE_EXTERNAL_STORAGE`, because what `MANAGE_EXTERNAL_STORAGE` can do, on Android 10 and below devices, we need to use `READ_EXTERNAL_STORAGE` and `WRITE_EXTERNAL_STORAGE` Only then can it be done. * So when you use **XXPermissions**, you can directly apply for new permissions. You don’t need to care about the compatibility of old and new permissions. The framework will automatically handle it for you. Unlike other frameworks, What I want to do more is to let everyone handle the permission request with a single code, and let the framework handle everything that the framework can do. #### Screen Rotation Scenario Adaptation * When the system permission request dialog pops up and the Activity is rotated, it will cause the permission request callback to fail because screen rotation causes the Fragment in the framework to be destroyed and recreated, leading to the callback object being directly recycled and ultimately causing the callback to be abnormal. There are several solutions: one is to add the `android:configChanges="orientation"` attribute in the manifest file so that screen rotation won't cause the Activity and Fragment to be destroyed and recreated; another is to directly fix the display direction of the Activity in the manifest file. But both of these solutions require the framework user to handle them, which is obviously not flexible enough. The problem should be solved by those who created it, and framework problems should be solved by the framework. **RxPermissions**' solution is to set `fragment.setRetainInstance(true)` on the PermissionFragment object, so even if the screen rotates, the Activity object will be destroyed and recreated, but the Fragment won't be destroyed and recreated, still reusing the previous object. But there's a problem: if the Activity overrides the `onSaveInstanceState` method, it will directly cause this approach to fail. This approach obviously only treats the symptoms but not the root cause. **XXPermissions**' approach is more direct: when the **PermissionFragment** is bound to the Activity, it **fixes the screen orientation** of the current Activity, and after the permission request is completed, it **restores the screen orientation**. * In all permission request frameworks, as long as they use Activity/Fragment to request permissions, this problem will occur because as soon as the user rotates the screen, it will cause the permission callback to not callback normally. Currently, XXPermissions is one of the few frameworks that solves this problem, while PermissionX directly borrowed XXPermissions' solution. For details, see [XXPermissions/issues/49](https://github.com/getActivity/XXPermissions/issues/49) and [PermissionX/issues/51](https://github.com/guolindev/PermissionX/issues/51). #### Background Permission Request Scenario Adaptation * When we apply for permissions after doing time-consuming operations (such as obtaining the privacy agreement on the splash screen page and then applying for permissions), the activity will be returned to the desktop (retired to the background) during the network request process, and then the permission request will be in the background state At this time, the permission application may be abnormal, which means that the authorization dialog box will not be displayed, and if it is not handled properly, it will cause a crash, such as [ RxPermissions/issues/249](https://github.com/tbruyelle/RxPermissions/issues/249). The reason is that the PermissionFragment in the framework will do a detection when `commit`/ `commitNow` arrives at the Activity. If the state of the Activity is invisible, an exception will be thrown, and **RxPermissions** It is the use of `commitNow` that will cause the crash, and the use of `commitAllowingStateLoss`/ `commitNowAllowingStateLoss` can avoid Enable this detection, although this can avoid crashes, but there will be another problem. The `requestPermissions` API provided by the system will not pop up the authorization dialog when the Activity is not visible. **XXPermissions** was resolved by moving the `requestPermissions` timing from `onCreate` to `onResume`, because `Activity` It is bundled with the life cycle method of `Fragment`. If `Activity` is invisible, then even if `Fragment` is created, only The `onCreate` method will be called instead of its `onResume` method. Finally, when the Activity returns from the background to the foreground, not only will the `onResume` method of `Activity` be triggered, but also the `onResume` method of `PermissionFragment` will be triggered. Applying for permissions in this method can ensure that the timing of the final `requestPermissions` call is when `Activity` is in a visible state. #### Fix Android 12 Memory Leak Issue * Recently someone asked me about a memory leak[ XXPermissions/issues/133 ](https://github.com/getActivity/XXPermissions/issues/133). After practice, I confirmed that this problem really exists, but by looking at the code stack, I found that this problem is caused by the code of the system, which caused this problem The following conditions are required: 1. Use on Android 12 devices 2. Called `Activity.shouldShowRequestPermissionRationale` 3. After that, the activity.finish method is actively called in the code * The process of troubleshooting: After tracing the code, it is found that the code call stack is like this * Activity.shouldShowRequestPermissionRationale * PackageManager.shouldShowRequestPermissionRationale (implementation object is ApplicationPackageManager) * PermissionManager.shouldShowRequestPermissionRationale * new PermissionManager(Context context) * new PermissionUsageHelper(Context context) * AppOpsManager.startWatchingStarted * The culprit is that `PermissionUsageHelper` holds the `Context` object as a field, and calls `AppOpsManager.startWatchingStarted` in the constructor to start monitoring, so that PermissionUsageHelper The object will be added to the `AppOpsManager#mStartedWatchers` collection, so that when the Activity actively calls finish, it does not use `stopWatchingStarted` to remove the listener, resulting in object has been held in the `AppOpsManager#mStartedWatchers` collection, which indirectly causes the Activity object to be unable to be recycled by the system. * The solution to this problem is also very simple and rude, which is to replace the `Context` parameter passed in from the outer layer from the `Activity` object to the `Application` object That's right, some people may say, `Activity` only has the `shouldShowRequestPermissionRationale` method, but what should I do if there is no such method in Application? After looking at the implementation of this method, in fact, that method will eventually call the `PackageManager.shouldShowRequestPermissionRationale` method (**Hidden API, but not blacklisted**), so as long as you can get `PackageManager` object, and finally use reflection to execute this method, so that memory leaks can be avoided. * Fortunately, Google did not include `PackageManager.shouldShowRequestPermissionRationale` in the reflection blacklist, otherwise there is no way to clean up this mess this time, or it can only be implemented by modifying the system source code, but this way I can only wait for Google to fix it in the subsequent Android version, but fortunately, after the `Android 12 L` version, this problem has been fixed, [ The specific submission record can be viewed here](https://cs.android.com/android/_/android/platform/frameworks/base/+/0d47a03bfa8f4ca54b883ff3c664cd4ea4a624d9:core/java/android/permission/PermissionUsageHelper.java;dlc=cec069482f80019c12f3c06c817d33fc5ad6151f), but for `Android 12` This is still a historical issue. * It is worth noting that XXPermissions is the first and only framework of its kind to fix this problem. In addition, I also provided a solution to Google's [AndroidX](https://github.com/androidx/androidx/pull/435) project for free. At present, Merge Request has been merged into the main branch. I believe that through this move, the memory leak problem of nearly 1 billion Android 12 devices around the world will be solved. #### Support for Code Error Detection * In the daily maintenance of the framework, many people have reported to me that there are bugs in the framework, but after investigation and positioning, it is found that 95% of the problems come from some irregular operations of the caller, which not only caused great harm to me At the same time, it also greatly wasted the time and energy of many friends, so I added a lot of review elements to the framework, in **debug mode**, **debug mode**, **debug mode**, once some operations do not conform to the specification, the framework will directly throw an exception to the caller, and correctly guide the caller to correct the error in the exception information, for example: * If the caller applies for permissions without passing in any permissions, the framework will throw an exception. * The incoming Context instance is not an Activity object, the framework will throw an exception, or the state of the incoming Activity is abnormal (already **Finishing** or **Destroyed**), in this case Generally, it is caused by applying for permissions asynchronously, and the framework will also throw an exception. Please apply for permissions at the right time. If the timing of the application cannot be estimated, please make a good judgment on the activity status in the outer layer before applying for permissions. * If the current project is not adapted to partition storage, apply for `READ_EXTERNAL_STORAGE` and `WRITE_EXTERNAL_STORAGE` permissions * When the project's `targetSdkVersion >= 29`, you need to register the `android:requestLegacyExternalStorage="true"` attribute in the manifest file, otherwise the framework will throw an exception. If you don't add it, it will cause a problem, obviously it has been obtained Storage permissions, but the files on the external storage cannot be read and written normally on the Android 10 device. * When the project's `targetSdkVersion >= 30`, you cannot apply for `READ_EXTERNAL_STORAGE` and `WRITE_EXTERNAL_STORAGE` permissions, but should apply for `MANAGE_EXTERNAL_STORAGE` permissions * If the current project is already adapted to partitioned storage, you only need to register a meta-data attribute in the manifest file: `` * If the requested permissions do not match the **targetSdkVersion** in the project, the framework will throw an exception because **targetSdkVersion** represents which Android version the project is adapted to, and the system will Automatically do backward compatibility, assuming that the application permission only appeared on Android 11, but **targetSdkVersion** is still at 29, then the application on some models will have authorization exceptions, and also That is, the user has clearly authorized, but the system always returns false. * If the dynamically applied permission is not registered in `AndroidManifest.xml`, the framework will throw an exception, because if you don’t do this, you can apply for permission, but there will be no authorization pop-up window, and it will be directly rejected by the system, and the system will not give any pop-up windows and prompts, and this problem is **Must-have** on every phone model. * If the dynamic application permission is registered in `AndroidManifest.xml`, but an inappropriate `android:maxSdkVersion` attribute value is set, the framework will throw an exception, for example: ``, such a setting will lead to the application of permissions on Android 11 ( `Build.VERSION.SDK_INT >= 30`) and above devices, the system will think that this permission is not registered in the manifest file, and directly reject it This permission application will not give any pop-up windows and prompts. This problem is also inevitable. * If you apply for the three permissions `MANAGE_EXTERNAL_STORAGE`, `READ_EXTERNAL_STORAGE`, `WRITE_EXTERNAL_STORAGE` at the same time, the framework will throw an exception, telling you not to apply at the same time These three permissions are because on Android 11 and above devices, if `MANAGE_EXTERNAL_STORAGE` permission is applied, `READ_EXTERNAL_STORAGE`, `WRITE_EXTERNAL_STORAGE` The necessity of permission, this is because applying for `MANAGE_EXTERNAL_STORAGE` permission is equivalent to possessing a more powerful ability than `READ_EXTERNAL_STORAGE` and `WRITE_EXTERNAL_STORAGE`, If you insist on doing that, it will be counterproductive. Assuming that the framework allows it, there will be two authorization methods at the same time, one is pop-up authorization, and the other is page-jump authorization. The user needs to authorize twice, but in fact there are `MANAGE_EXTERNAL_STORAGE` permission is sufficient for use, at this time you may have a question in mind, you do not apply for `READ_EXTERNAL_STORAGE`, `WRITE_EXTERNAL_STORAGE` permission, Android There is no `MANAGE_EXTERNAL_STORAGE` permission below 11, isn't there a problem? Regarding this issue, you can rest assured that the framework will make judgments. If you apply for the `MANAGE_EXTERNAL_STORAGE` permission, the framework below Android 11 will automatically add `READ_EXTERNAL_STORAGE`, `WRITE_EXTERNAL_STORAGE` to apply, so it will not be unusable due to lack of permissions under lower versions. * If you don't need the above detections, you can turn them off by calling the `unchecked` method, but it should be noted that I don't recommend you to turn off this detection, because in **release mode** When it is closed, you don't need to close it manually, and it only triggers these detections under **debug mode**. * The reason for these problems is that we are not familiar with these mechanisms, and if the framework does not impose restrictions, then various strange problems will arise. As the author of the framework, not only you are suffering, but also as the framework author. Injuried. Because these problems are not caused by the framework, but by some irregular operations of the caller. I think the best way to solve this problem is to do a unified inspection by the framework, because I am the author of the framework, and I have **Strong professional ability and sufficient experience** knowledge about permission application, and know what to do and what not to do. It should be done, In this way, these irregular operations can be intercepted one by one. * When there is a problem with the permission application, do you hope that someone will come to remind you and tell you what is wrong? How to correct it? However, these XXPermissions have done it. Among all the permission request frameworks, I am the first person to do this. I think **make a frame** is not only to do a good job of function, but also to make complex The scene is handled well, and more importantly, **people oriented**, because the framework itself serves people, and what we need to do is not only to solve everyone's needs, but also to help everyone avoid detours in the process. ================================================ FILE: Details-zh.md ================================================ #### 目录 * [Intent 跳转极限兜底机制](#intent-跳转极限兜底机制) * [兼容请求权限 API 崩溃问题](#兼容请求权限-api-崩溃问题) * [规避系统权限回调空指针问题](#规避系统权限回调空指针问题) * [应用商店权限合规处理](#应用商店权限合规处理) * [自动拆分权限进行请求](#自动拆分权限进行请求) * [框架内部完全剥离 UI 层](#框架内部完全剥离-ui-层) * [核心逻辑和具体权限完全解耦](#核心逻辑和具体权限完全解耦) * [自动适配后台权限](#自动适配后台权限) * [支持在跨平台环境中调用](#支持在跨平台环境中调用) * [回调生命周期与宿主保持同步](#回调生命周期与宿主保持同步) * [支持自定义权限申请](#支持自定义权限申请) * [支持读取应用列表权限](#支持读取应用列表权限) * [新版本权限支持向下兼容](#新版本权限支持向下兼容) * [屏幕旋转场景适配](#屏幕旋转场景适配) * [后台申请权限场景适配](#后台申请权限场景适配) * [修复 Android 12 内存泄漏问题](修复-android-12-内存泄漏问题) * [第三方厂商兼容性优化](#第三方厂商兼容性优化) * [支持检测代码错误](#支持检测代码错误) #### Intent 跳转极限兜底机制 * 在介绍这个功能之前,我先问大家一个问题,请你分析一下这段代码是否有什么问题? ```java Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); intent.setData("package:" + getPackageName()); startActivityForResult(intent, 1024); ``` * 你可能会说:很简单啊,这不就是一个跳转到应用详情页的代码,还能有什么问题?你莫不是要找我的茬? * 这段代码看似没有问题,运行起来也没有问题,但实际上是一个天坑,你没有看到或者遇到并不代表不存在,有些厂商直接阉割了 `ACTION_APPLICATION_DETAILS_SETTINGS` 这个意图,是的你没有听错,就是直接阉割,这段代码在这些设备上面运行,应用就会闪崩,没有跟你开玩笑, ```text android.content.ActivityNotFoundException: No Activity found to handle Intent { act=android.settings.APPLICATION_DETAILS_SETTINGS dat=Package Name:com.xxx.xxx } ``` * 你如果还是不信,请看这里 [Github Search `No Activity found to handle Intent act=android.settings.APPLICATION_DETAILS_SETTINGS`](https://github.com/search?q=No+Activity+found+to+handle+Intent++act%3Dandroid.settings.APPLICATION_DETAILS_SETTINGS&type=issues): * 其实不止是 `ACTION_APPLICATION_DETAILS_SETTINGS` 这个意图,其他的意图也会,一个都跑不了,你如果不信可以看这里 [Github Search `No Activity found to handle Intent act=android`](https://github.com/search?q=No+Activity+found+to+handle+Intent++act%3Dandroid&type=issues)。 ``` android.content.ActivityNotFoundException: No Activity found to handle Intent { act=android.settings.MANAGE_UNKNOWN_APP_SOURCES (has data) } ``` * 看完你是不是想吐槽一下?但问题已经存在,非理性的抱怨永远解决不了问题,只有理性的分析和认真的思考才是唯一的出路。这个问题无非是 `Intent` 找不到了,最简单有效的方法,就是在跳转前对 `Intent` 进行判断,如果存在这个 `Intent` 再跳转,如果不存在就不跳转,你如果要是真的那么想问题就太片面了,事情往往没有你想得那么简单,不存在的 `Intent` 跳转会失败,那你有没有想过,存在的 `Intent` 也不代表一定能跳转成功,你如果不信可以看这里 [Github Search `Permission Denial: starting Intent`](https://github.com/search?q=Permission+Denial%3A+starting+Intent&type=issues),现在知道为什么叫天坑了吧? ```text java.lang.SecurityException: Permission Denial: starting Intent { act=android.settings.MANAGE_UNKNOWN_APP_SOURCES (has data) cmp=xxxx/xxx } ``` * 说这些并不是想让大家解决,而是让大家意识到有这个问题,当然框架内部已经处理好这个问题,你能想到的所有问题,框架已经提前想到了,并且已经帮你处理好了,只需要一句代码,调用 `XXPermissions.startPermissionActivity` 方法即可。假设你很好奇框架是怎么实现的,又懒得看源码实现,这点我也帮你想到了,在这里我介绍框架是怎么实现的,原理其实很简单,框架获取这个权限设置页的时候,把能想到的 `Intent` 写到 List 集合中,再筛选掉不存在的 `Intent`,然后挨个 `Intent` 进行跳转,如果失败就跳转到下一个,直到跳转成功或者没有下一个 `Intent` 了为止。 #### 兼容请求权限 API 崩溃问题 * 在介绍这个功能之前,我先问大家一个问题,请你分析一下这段代码是否有什么问题? ```java activity.requestPermissions(new String[]{Manifest.permission.CAMERA}, 1024); ``` * 你可能会说:这不就是一段再简单不过用系统 API 申请权限的代码吗?还能有什么问题,你只要不在 Android 6.0 以下的设备调用就肯定没有问题。 * 理论上是这样的,但是理论终究是理论,实际情况是在 Android 6.0 及以上的设备调用也有可能出现崩溃,对的你没有看错,Android 6.0 以上调用会出现崩溃,听着也太魔幻了,那么重要的系统 API 居然也会崩溃?如果不信可以看这里 [XXPermissions/issues/153](https://github.com/getActivity/XXPermissions/issues/153)、[XXPermissions/issues/126](https://github.com/getActivity/XXPermissions/issues/126)、[XXPermissions/issues/327](https://github.com/getActivity/XXPermissions/issues/327)、[XXPermissions/issues/339](https://github.com/getActivity/XXPermissions/issues/339),如果还不够看的话可以看这里 [Github Search `act=android.content.pm.action.REQUEST_PERMISSIONS`](https://github.com/search?q=act%3Dandroid.content.pm.action.REQUEST_PERMISSIONS&type=issues),看完是不是瞬间颠覆了你的认知? ```text android.content.ActivityNotFoundException: No Activity found to handle Intent { act=android.content.pm.action.REQUEST_PERMISSIONS pkg=com.android.packageinstaller (has extras) } ``` * 出现这种情况有以下几种可能: 1. 厂商开发工程师修改了 `com.android.packageinstaller` 系统应用的包名,但是没有自测好就上线了(概率较小) 2. 厂商开发工程师删除了 `com.android.packageinstaller` 这个系统应用,但是没有自测好就上线了(概率较小) 3. 厂商开发工程师在修改 Android 系统源码的时候,改动的代码影响到权限模块,但是没有自测好就上线了(概率较小) 4. 厂商主动阉割掉了权限申请功能,例如在电视 TV 设备上面,间接导致请求危险权限的 App 一请求权限就闪退(概率较小) 5. 用户有 Root 权限,在精简系统 App 的时候不小心删掉了 `com.android.packageinstaller` 这个系统应用(概率较大) * 经过分析 `Activity.requestPermissions` 的源码,它本质上还是调用 `startActivityForResult`,只不过 `Activity` 找不到了而已,目前能想到最好的解决方式,就是用 `try catch` 避免它出现崩溃,看到这里你可能会有一个疑问,就简单粗暴 `try catch`?你确定不会引发其他问题?会不会导致 `onRequestPermissionsResult` 没有回调?从而导致权限请求流程卡住的情况?虽然这个问题没有办法测试,但理论上是不会的,因为我用了错误的 `Intent` 进行 `startActivityForResult` 并进行 `try catch` 做实验,结果 `onActivityResult` 还是有被系统正常回调,证明对 `startActivityForResult` 进行 `try catch` 并不会影响 `onActivityResult` 的回调,我还分析了 `Activity` 回调方面的源码实现,发现无论是 `onRequestPermissionsResult` 还是 `onActivityResult`,回调它们的都是 `dispatchActivityResult` 方法,在那种极端情况下,既然 `onActivityResult` 能被回调,那么就证明 `dispatchActivityResult` 肯定有被系统正常调用的,同理 `onRequestPermissionsResult` 也肯定会被 `dispatchActivityResult` 正常调用,从而形成一个完整的逻辑闭环。 * 补充后续测试结论:我在 debug 了 `Activity.requestPermissions` 方法,偷偷修改权限请求 `Intent` 的 `Action` 成错误的,结果权限回调能正常回调。 * 如果真的出现这种极端情况,所有危险权限的申请必然会走失败的回调,但是框架能做的是:尽量让应用不要崩溃,并且能走完整个权限申请的流程。 #### 规避系统权限回调空指针问题 * 在介绍这个功能之前,我先问大家一个问题,请你分析一下这段代码是否有什么问题? ```java public final class XxxActivity extends AppCompatActivity { @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (permissions.length == 0 || grantResults.length == 0) { return; } if (permissions[0].equals(Manifest.permission.CAMERA) && grantResults[0] == PackageManager.PERMISSION_GRANTED) { System.out.println("获取相机权限成功"); } else { System.out.println("获取相机权限失败"); } } } ``` * 你可能会说:这不是正常在权限回调中处理权限请求结果,我平时就是那么写的,怎么看都没有什么毛病啊,你是不是没事找茬? * 如果我告诉你一件事,系统返回的参数 `permissions` 或 `grantResults` 数组对象有可能会为空,你会不会相信呢?我知道你肯定不信,因为你看到了 `permissions` 和 `grantResults` 参数上面都有 `@NonNull` 注解(点进去 Activity 源码里面看到的也是 `@NonNull` 注解),就代表系统返回的一定不为空,到这里你肯定认为我在欺骗你。 * 我知道你不信,所以证据给你备好了,请看这里 [XXPermissions/issues/191](https://github.com/getActivity/XXPermissions/issues/191)、[XXPermissions/issues/106](https://github.com/getActivity/XXPermissions/issues/106)、[XXPermissions/issues/236](https://github.com/getActivity/XXPermissions/issues/236),如果还不够看的话可以看这里 [Github Search `NullPointerException onRequestPermissionsResult`](https://github.com/search?q=NullPointerException+onRequestPermissionsResult&type=issues); * 看完是不是不知道你是何种想法?系统这是要闹哪样?把参数标记成不为空结果却给我返回空的,这难道不是在欺骗我的感情?问题已经存在,非理性的抱怨永远解决不了问题,只有理性的分析和认真的思考才是唯一的出路。 * 目前反馈这个问题的机型品牌有 vivo、小米、联想;就说明这个问题大概率又是 `Google` 工程师挖的坑,解决这个问题的思路有两种: 1. 仍然要用 `permissions` 和 `grantResults` 参数来判断权限的状态:使用之前需要先对数组对象进行防空判断,然后继续使用。 2. 不再使用 `permissions` 和 `grantResults` 参数来判断权限的状态:改用 `checkSelfPermission` 的方式来判断权限状态。 * 虽然两种都可以解决问题,但是两种略有区别,框架最终采用的是第二种,中国有一句老话叫:一次不忠终身不用,既然它能干这种毫无底线的事情,就不得不防它还有其他小动作,例如以下场景: 1. 返回的数组对象不为空,但是数组里面没有元素,如果事先不进行判断,一调用就可能会触发角标越界异常 `ArrayIndexOutOfBoundsException` 2. 返回的数组对象不为空,数组里面也有元素,但是 `permissions` 和 `grantResults` 两个数组的长度不一样,如果事先不进行判断,一调用就可能会触发角标越界异常 `ArrayIndexOutOfBoundsException` 3. 返回的数组对象不为空,数组里面也有元素,两个数组的长度也是正常的,但是返回的 `grantResults` 与实际不匹配,用户明明授予了权限,但是这个数组存的却是 `-1`(`PackageManager.PERMISSION_DENIED`) * 到这里你是不是瞬间觉得解决这个空指针的问题好像不是只是加一下防空判断那么简单?原来里面的学问那么多。在此我想跟大家说,无论是什么问题,我都会认真对待,因为我追求的从来不是能解决问题就好,而是在所有能想到的解决方案中找出最优解。 #### 应用商店权限合规处理 * 现在国内的应用商店,在申请权限的时候,需要同步告知权限申请的目的,否则会被拒绝上架或更新,框架已经帮你考虑到这点了,目前已经开放相应的接口,你可以实现接口来这一需求,具体效果如下图所示: * 虽然这个功能自己不需要框架提供接口也能实现,只需要在权限申请前显示弹窗,权限申请完成取消弹窗就行,但是这样会使你写的代码不优雅,因为这部分的代码是直接写死在 `Activity/Fragment` 类中的,不仅会增加 `Activity/Fragment` 类的复杂度,并且每个用到权限申请的 `Activity/Fragment` 类都要写一份这样的代码,后续会变得难以维护,正是考虑到这个问题,框架才开放了这个接口,还在 Demo 工程实现了一份完整的案例供你参考,你不仅可以轻而易举实现这个功能,过程无需操心实现的细节和是否有 Bug,因为你能想到的,我都帮你想到了,你没有想到的,我也帮你想到了。 #### 自动拆分权限进行请求 * 在一些需求场景下,需要同时申请多种权限,例如麦克风权限和日历权限,这个时候产品经理想要拆分成两次权限进行申请,以便能够分开显示两个权限的说明弹窗,这样的设计会导致功能开发比较复杂,如果不拆分申请的情况下只需要在请求权限前后加一下显示和关闭弹窗的逻辑就行了,现在要分成两次权限申请后就不能这样写了,只能分开写,分开写就意味着要写各种嵌套和回调,一想到要这样做就一个头两个大,差点就把昨晚吃的宵夜给呕出来。 * 大家的苦,大家的痛,不用多说,我都懂,所以我在框架加了一套处理机制,会自动将你传入的权限进行归类分组,例如麦克风权限归为一组,日历权限归为一组,然后会拆分成两次权限申请,这个时候在搭配上框架开放的权限说明接口,这个接口会告诉你申请什么权限,你再根据权限来展示具体的权限说明弹窗就行了,至此这个功能轻松又优雅地完成了,iOS 端还吭哧吭哧实现,你已经完成并提前下班了,没有延迟,没有痛苦,有的只是实现功能的爽感。 #### 框架内部完全剥离 UI 层 * 某些权限框架内部会实现一套权限说明弹窗的 UI 和逻辑,需要实现特定的接口才能修改,但是我认为这样的设计是不合理的,因为展示权限说明弹窗并不是一个必须的操作,没有它调用权限申请 API 照样会弹出授权框,另外涉及到 UI,框架内部设计的 UI 注定无法满足所有人的需求(吃力不讨好),因为每个人拿到的设计图都是不一样的,所以最好的方案是,框架自己不要在内部写 UI 和逻辑,而是设计好这方面相关的接口,然后全权交由外层实现,当然框架 Demo 模块也会实现一份案例供外层借鉴(供外层直接抄代码),这样不仅能解决 UI 需求不一致的问题,还能减少框架的体积,一箭双雕。 #### 核心逻辑和具体权限完全解耦 * 你在市面上能看到的能同时支持申请危险权限和特殊权限的框架,它们的代码耦合度非常高,这样会导致一个问题,例如你只拿它申请了危险权限,但是最终打包的时候,会连同特殊权限的代码逻辑一起给打包到 apk 中的,这就好比你现在想吃炸鸡,但店员告诉你只有点十个人的套餐才有炸鸡,你心想自己一个人就算撑死也没有办法吃完这十个人的套餐,这种设计不是明摆着坑人吗?虽然 app 多一些代码不会跟人一样被撑死,但是也不要肆意挥霍,这里浪费一点,那里浪费一点,开发完后一看 apk 体积 250 mb,还得考虑体积优化,关键是你还没有办法优化,因为这部分代码是写死在框架中,框架又是通过远程依赖,你就得换成本地依赖去改,改了就意味着可能有 bug 要增加很多自测的工作量,重要的是改了收益不高,但是风险极高,很容易改着改着将自己送上裁员名单。 * 针对这个问题,框架有一个堪称鬼才的设计方案,就是将不同权限的实现封装成对象,你申请什么权限就传什么对象,这样没有引用的对象就会在开启代码混淆的时候一并移除,这样打正式包的时候就不会有冗余的代码,更不会占用多余的 apk 的体积,真正做到了用多少算多少,再也不用为了想吃一块炸鸡而考虑要不要买个十个套餐,无需纠结,无需徘徊,在 XXPermissions 这里可以做到分开买,想吃什么买什么,想吃多少买多少,老少兼宜,童叟无欺。 * 当然对于某些框架,它既不支持任何特殊权限,也没有针对某个危险权限做单独的处理,只是简单套用系统的 API,请求权限就直接用 `context.requestPermissions`,判断权限就直接用 `context.checkSelfPermission`,这种算不算完全解耦呢?其实是算的,因为人家确实没有在核心逻辑中直接依赖具体某个权限,但是这种框架不符合现实开发的需求,因为在一个商业化的 app 中不可能只请求危险权限,通知权限要吧?安装包权限要吧?悬浮窗权限要吧?只要这些框架支持任何一个特殊权限,就会存在这个问题,当然不支持当然就没有这个问题,关键是能不能做到既能支持,又能对代码进行解耦呢?这才是问题的关键,非常考验对框架的理解和代码的设计,截止目前只有 XXPermissions 真正做到了既要又要。 #### 自动适配后台权限 * Android 10 上面新增了[后台定位权限](https://developer.android.google.cn/about/versions/11/privacy/location?hl=zh-cn#background-location) 和 Android 13 上面新增了[后台传感器权限](https://developer.android.google.cn/about/versions/13/behavior-changes-13?hl=zh-cn#body-sensors-background-permission),你可千万别认为这两个后台权限跟普通的危险权限没有区别,这里面的区别非常大,要是没有搞清楚容易出 Bug。 1. 前台权限和后台权限不能放在一起申请,必须先申请前台权限,才能申请后台权限,如果在没有前台权限的前提下申请后台权限是肯定会被系统拒绝的,这是毋庸置疑的。 2. 在 Android 11 之后,前台权限和后台权限申请必须保证一定的时间间隔,也就是拆分两次权限申请之后,还要保证这两次权限申请有一定的时间间隔,否则也会申请失败,经过测试不能低于 150 毫秒。 3. 后台定位权限对应的前台定位权限有两个,精确定位权限(`ACCESS_FINE_LOCATION`)和模糊定位权限(`ACCESS_COARSE_LOCATION`),在 `Android 10 ~ Android 11` 的版本,后台定位权限锚定的前台权限就是精确定位权限,只有这个权限同意的时候,才能申请后台定位权限,而到了 Android 12 及之后的版本,后台定位权限锚定的前台权限既可以是精确定位权限,也可以是模糊定位权限,这两个权限任一同意的时候,就可以申请后台定位权限。 * 然而上面这些问题,框架已经帮你处理了,你无需自己再手动处理,具体处理方案如下: 1. 自动识别后台权限和与之对应的前台权限,然后自动拆分成两次权限申请,先申请前台权限,再申请后台权限。 2. 在申请后台权限的时候,先加一段时间的延迟,也就是前面说的 150 毫秒,再进行申请后台权限,由此规避这个问题。 3. 在判断后台定位权限的时候,会针对不同的 Android 版本做前台定位权限判断,在 `Android 10 ~ Android 11` 的版本就用精确定位权限,`Android 12` 及之后的版本就用精确定位权限或者模糊定位权限,确保不同 Android 版本能达到同样的预期效果。 #### 支持在跨平台环境中调用 * 众所周知 [FlutterActivity](https://github.com/flutter/flutter/blob/03ef1ba910cac387f7b2af8ab09ca955d3974663/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java)、[ComposeActivity](https://github.com/androidx/androidx/blob/8d08d42d60f7cc7ec0034d0b7ff6fd953516d96a/emoji2/emoji2-emojipicker/samples/src/main/java/androidx/emoji2/emojipicker/samples/ComposeActivity.kt)、[UnityPlayerActivity](https://github.com/FabianTerhorst/PokemonGo/blob/d511b045f1492e0bae71778ef528f3d768d218cd/java/com/unity3d/player/UnityPlayerActivity.java)、[Cocos2dxActivity](https://github.com/irontec/Ikasesteka/blob/master/Ikasesteka/cocos2d/cocos/platform/android/java/src/org/cocos2dx/lib/Cocos2dxActivity.java) 这些都是 Activity 的子类,但是它们都不是 [FragmentActivity](https://github.com/androidx/androidx/blob/8d08d42d60f7cc7ec0034d0b7ff6fd953516d96a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java) 的子类,它们的继承关系是这样的: 1. `FlutterActivity extends Activity` 2. `ComposeActivity extends ComponentActivity extends Activity` 3. `UnityPlayerActivity extends Activity` 4. `Cocos2dxActivity extends Activity` 5. `FragmentActivity extends ComponentActivity extends Activity` * 这样就会出现一个问题,有些权限请求框架是用一个透明的 `Fragment` 获得权限申请的回调,如果这个权限请求框架用的是 `androidx.fragment.app.Fragment`,那么就必须要求外层传入 `androidx.fragment.app.FragmentActivity` 对象,假设这个时候你用的 `Activity` 并不是 `androidx.fragment.app.FragmentActivity` 的子类,请问你该怎么办?那简单,我就修改当前 `Activity` 直接继承 `androidx.fragment.app.FragmentActivity` 不就行了?那如果你目前的 `Activity` 是一定要继承 `FlutterActivity`、`ComposeActivity`、`UnityPlayerActivity`、`Cocos2dxActivity` 呢?请问你又该怎么改?难不成去改它们的源码?还是去改权限框架的源码?无论选哪种解决方案,改造的成本都会很大,后续也不好维护,这既不现实,也不科学。是不是突然感觉上天把路给你堵死了?难不成只能手写权限申请 API 的代码才能实现权限请求了? * 其实这个问题框架已经想到了,并且已经帮你解决了,无需你做任何处理,具体的实现原理是:框架会判断你传入的 `Activity` 对象是不是 `androidx.fragment.app.FragmentActivity` 的子类,如果是的话,则会用 `androidx.fragment.app.Fragment` 进行权限申请,如果不是的话,则会用 `android.app.Fragment` 进行权限申请,这样无论你用哪种 `Activity`,框架都能自动进行适配。 #### 回调生命周期与宿主保持同步 * 目前市面上大多数权限请求框架都会用单种 `Fragment` 处理权限请求回调,但是这样会导致一个问题,假设某个框架用的是 `androidx.fragment.app.Fragment` 处理权限请求回调,但是你却是在 `android.app.Fragment` 发起的权限请求,又或者反过来,你用 `androidx.fragment.app.Fragment` 框架用 `android.app.Fragment`,你无法把你自己的 `Fragment` 当做宿主,然后传给权限请求框架,只能通过 `fragment.getActivity()` 将 `Activity` 对象传给框架,这样 `Activity` 就成了宿主对象,这样都会导致一个生命周期不同步的问题,就是你自己的 `Fragment` 已经销毁的情况,但是框架仍会回调权限请求结果的监听器,轻则导致 `Memory leak`,重则会触发 `Exception`。 * 导致这个问题的原因是,第三方框架用的 `Fragment` 和你的 `Fragment` 实际上不是一个类型的,虽然它们的类名一样,但是它们所在的包名不一样,加上刚刚说的你只能通过 `fragment.getActivity()` 将 `Activity` 对象传给框架,这样你自己的 `Fragment` 无法和框架的 `Fragment` 之间无法形成一种有效的生命周期绑定,实际你想要的是绑定你自己 `Fragment` 的生命周期,但框架最终绑定的是 `Activity` 生命周期,这样很可能会触发 Crash,具体表现你可以看一下这个 issue:[XXPermissions/issues/365](https://github.com/getActivity/XXPermissions/issues/365)。 * 其实这个问题框架已经想到了,并且已经帮你解决了,无需你做任何处理,解决这个问题的思路是:框架会根据你传入的对象类型,自动选择最佳类型的 `Fragment`,假设你传入的宿主是 `androidx.fragment.app.FragmentActivity` 或者 `androidx.fragment.app.Fragment` 对象,框架内部则会创建 `androidx.fragment.app.Fragment` 来接收权限请求回调,假设你传入的宿主是普通的 `Activity` 或者 `android.app.Fragment`,框架内部则会创建 `android.app.Fragment` 来接收权限请求回调,这样无论你传入的是什么宿主对象,框架都会和它的生命周期做绑定,确保在回调权限请求结果给到最外层的时候,宿主对象仍处于正常的状态。 * 这个时候你可能会跳出来说,这个不用框架我也能实现,我自己在权限回调的方法中,自己手动判断一下 `Fragment` 的状态不就行了?不就是两三句代码的事情?框架为什么搞得那么麻烦?你的想法看似有道理,但实则经不起推敲,假设你的项目有十几处地方申请了权限,那么你需要在每个回调方法都考虑这个问题,另外后续申请新的权限,你也要考虑这个问题,你能确保自己改的时候不会出现漏网之鱼?还有假设这个需求是你的同事开发的,但是只有你知道这个事情,他并不知情的情况下,你知道这种情况下可能会发生什么吗?我相信你比我更懂。你提供的解决问题方法,虽然可以暂时解决问题,但是治标不治本,究其根本是解决的思路有问题,遵循的是有洞补洞的思维,而没有想从源头堵住漏洞。又或者你原本就知道怎么彻底根治,只不过选择了最轻松的方式来处理,但这无疑是给项目埋了一颗定时炸弹。 #### 支持自定义权限申请 * 顾名思义,开发者除了可以申请框架中已支持的权限,还支持申请开发者自己定义的权限,这个功能非常强大,此功能可以满足以下场景的需求: 1. 可以定义一些框架不支持的权限并进行申请,例如开机自启权限、桌面快捷方式权限、读取剪贴板权限、写入剪贴板权限、操作外部存储 `Android/data` 权限,特定厂商的一些权限等等适配,甚至是[蓝牙开关、WIFI 开关、定位开关](https://github.com/getActivity/XXPermissions/issues/170)等等,此处请尽情发挥你的想象力,现在只需要继承框架提供的 `DangerousPermission` 或 `SpecialPermission` 类即可实现自定义权限,要知道这个功能放在之前的版本只能通过修改框架的源码才能实现,过程十分麻烦,你不仅要研究框架的源码,又要在修改后做严格的自测,而现在不需要这样做了,框架对外提供了这个扩展接口,实现一个接口即可实现。 2. 开发者不需要再依赖权限框架作者来适配新的权限,当 Google 发布了新的 Android 版本,并且增加了新的权限,而框架来不及适配,而你又急需申请这个新的权限,那么这个时候可以使用这个功能,率先对新权限进行适配。 #### 支持读取应用列表权限 * 这个权限非常特殊,它不属于 Android 原生的权限,而是由[工信部](http://www.taf.org.cn/StdDetail.aspx?uid=3A7D6656-43B8-4C46-8871-E379A3EA1D48&stdType=TAF)牵头,联合各大中国手机厂商搞的一个权限,目前支持手机厂商有: | 品牌 | 版本要求 | 是否默认授予 | | :--------: | :------------------------------: | :--------: | | 华为 | HarmonyOS 3.0.0 及以上版本 | 否 | | 荣耀 | MagicOS 6.0 及以上版本 | 否 | | 小米 | MIUI 13 或 HyperOS 1.0.0 及以上版本 | MIUI 默认授予
HyperOS 默认没有授予 | | 红米 | 和小米雷同 | 和小米雷同 | | OPPO | (ColorOS 12 及以上版本 && Android 11+) 或者
(ColorOS 11.1 及以上版本 && Android 12+) | 否 | | VIVO | OriginOS 4 && Android 14 | 否 | | 一加 | 和 OPPO 雷同 | 和 OPPO 雷同 | | 真我 | RealmeUI 3.0 及以上版本 | 否 | * 目前不支持的手机厂商有: | 品牌 | 测试的手机机型 | 测试的版本 | 是否有申请该权限的入口 | | :------: | :---------------: | :---------------------------------: | :-----------------: | | 魅族 | 魅族 18x | Flyme 9.2.3.1A && Android 11 | 是 | | 锤子 | 坚果手机 Pro 2S | SmartisanOS 7.2.0.2 && Android 8.1 | 否 | | 奇虎 | 360 手机 N7 Lite | 360UI 3.0 && Android 8.1 | 否 | | 小辣椒 | 小辣椒S6 | 小辣椒 Os 3.0 && Android 7.1.1 | 否 | * 还有一些厂商没有列出来,并不是作者没有做测试,而是他们的系统本身就是直接用 Android 的,Android 原生目前不支持申请该权限 * 另外框架还做了一些特殊处理: * 在小米手机的 MIUI,但是这套机制只支持 MIUI 13 及以上的版本,然而框架内部做了一些兼容手段,目前已经适配了所有 MIUI 版本读取应用列表权限的申请。 * 三星手机从 OneUI 5.1.1 开始支持读取应用列表权限,但是这套机制完全不支持,然而框架内部做了一些兼容手段,目前已经适配了所有 OneUI 版本读取应用列表权限的申请。 * 魅族手机从 Flyme 8.x(不知道具体是哪个版本)开始支持读取应用列表权限,但是这套机制完全不支持,然而框架内部做了一些兼容手段,目前已经适配了 Flyme 9.x 及之后的版本读取应用列表权限的申请。 #### 新版本权限支持向下兼容 * 随着 Android 版本的不断更新,危险权限和特殊权限也在增加,那么这个时候会有一个版本兼容问题,高版本的安卓设备是支持申请低版本的权限,但是低版本的安卓设备是不支持申请高版本的权限,那么这个时候会出现一个兼容性的问题。 * 经过核查,其他权限框架选择了一种最简单粗暴的方式,就是不去做兼容,而是交给外层的调用者做兼容,需要调用者在外层先判断安卓版本,在高版本上面传入新权限给框架,而在低版本上面传入旧权限给框架,这种方式看似简单粗暴,但是开发体验差,同时也暗藏了一个坑,外层的调用者他们知道这个新权限对应着的旧权限是哪个吗?我觉得不是每个人都知道,而一旦认知出现错误,必然会导致结果出现错误。 * 我觉得最好的做法是交给框架来做,**XXPermissions** 正是那么做的,外层调用者申请高版本权限的时候,那么在低版本设备上面,会自动添加低版本的权限进行申请,举个最简单的例子,Android 11 出现的 `MANAGE_EXTERNAL_STORAGE` 新权限,如果是在 Android 10 及以下的设备申请这个权限时,框架会自动添加 `READ_EXTERNAL_STORAGE` 和 `WRITE_EXTERNAL_STORAGE` 进行申请,在 Android 10 及以下的设备上面,我们可以直接把 `READ_EXTERNAL_STORAGE` 和 `WRITE_EXTERNAL_STORAGE` 当做 `MANAGE_EXTERNAL_STORAGE` 来用,因为 `MANAGE_EXTERNAL_STORAGE` 能干的事情,在 Android 10 及以下的设备上面,要用 `READ_EXTERNAL_STORAGE` 和 `WRITE_EXTERNAL_STORAGE` 才能做得了。 * 所以大家在使用 **XXPermissions** 的时候,直接拿新的权限去申请就可以了,完全不需要关心新旧权限的兼容问题,框架会自动帮你做处理的,与其他框架不同的是,我更想做的是让大家一句代码搞定权限请求,框架能做到的,统统交给框架做处理。 #### 屏幕旋转场景适配 * 当系统权限申请对话框弹出后对 Activity 进行屏幕旋转,会导致权限申请回调失效,因为屏幕旋转会导致框架中的 Fragment 销毁重建,这样会导致里面的回调对象直接被回收,最终导致回调不正常。解决方案有几种,一是在清单文件中添加 `android:configChanges="orientation"` 属性,这样屏幕旋转时不会导致 Activity 和 Fragment 销毁重建,二是直接在清单文件中固定 Activity 显示的方向,但是以上两种方案都要使用框架的人处理,这样显然是不够灵活的,解铃还须系铃人,框架的问题应当由框架来解决,而 **RxPermissions** 的解决方式是给 PermissionFragment 对象设置 `fragment.setRetainInstance(true)`,这样就算屏幕旋转了,Activity 对象会销毁重建,而 Fragment 也不会跟着销毁重建,还是复用着之前那个对象,但是存在一个问题,如果 Activity 重写了 `onSaveInstanceState` 方法会直接导致这种方式失效,这样做显然只是治标不治本,而 **XXPermissions** 的方式会更直接点,在 **PermissionFragment** 绑定到 Activity 上面时,把当前 Activity 的**屏幕方向固定住**,在权限申请结束后再把**屏幕方向还原回去**。 * 在所有的权限请求框架中,只要使用了 Activity/Fragment 申请权限都会出现这个问题,因为只要用户一转动屏幕,就会导致权限回调无法正常回调,目前 XXPermissions 为数不多解决这个问题的框架,而 PermissionX 则是直接借鉴了 XXPermissions 的解决方案,详情请见 [XXPermissions/issues/49](https://github.com/getActivity/XXPermissions/issues/49) 、[PermissionX/issues/51](https://github.com/guolindev/PermissionX/issues/51)。 #### 后台申请权限场景适配 * 当我们做耗时操作之后申请权限(例如在闪屏页获取隐私协议再申请权限),在网络请求的过程中将 Activity 返回桌面去(退到后台),然后会导致权限请求是在后台状态中进行,在这个时机上就可能会导致权限申请不正常,表现为不会显示授权对话框,处理不当的还会导致崩溃,例如 [RxPermissions/issues/249](https://github.com/tbruyelle/RxPermissions/issues/249)。原因在于框架中的 PermissionFragment 在 `commit` / `commitNow` 到 Activity 的时候会做一个检测,如果 Activity 的状态是不可见时则会抛出异常,而 **RxPermissions** 正是使用了 `commitNow` 才会导致崩溃 ,使用 `commitAllowingStateLoss` / `commitNowAllowingStateLoss` 则可以避开这个检测,虽然这样可以避免崩溃,但是会出现另外一个问题,系统提供的 `requestPermissions` API 在 Activity 不可见时调用也不会弹出授权对话框,**XXPermissions** 的解决方式是将 `requestPermissions` 时机从 `onCreate` 转移到了 `onResume`,这是因为 `Activity` 和 `Fragment` 的生命周期方法是捆绑在一起的,如果 `Activity` 是不可见的,那么就算创建了 `Fragment` 也只会调用 `onCreate` 方法,而不会去调用它的 `onResume` 方法,最后当 Activity 从后台返回到前台时,不仅会触发 `Activity` 的 `onResume` 方法,也会触发 `PermissionFragment` 的 `onResume` 方法,在这个方法申请权限就可以保证最终 `requestPermissions` 调用的时机是在 `Activity` 处于可见状态的情况下。 #### 修复 Android 12 内存泄漏问题 * 最近有人跟我提了一个内存泄漏的问题 [XXPermissions/issues/133](https://github.com/getActivity/XXPermissions/issues/133) ,我经过实践后确认这个问题真实存在,但是通过查看代码堆栈,发现这个问题是系统的代码引起的,引发这个问题需要以下几个条件: 1. 在 Android 12 的设备上使用 2. 调用了 `Activity.shouldShowRequestPermissionRationale` 3. 在这之后又主动在代码调用了 activity.finish 方法 * 排查的过程:经过对代码的追踪,发现代码调用栈是这样的 * Activity.shouldShowRequestPermissionRationale * PackageManager.shouldShowRequestPermissionRationale(实现对象为 ApplicationPackageManager) * PermissionManager.shouldShowRequestPermissionRationale * new PermissionManager(Context context) * new PermissionUsageHelper(Context context) * AppOpsManager.startWatchingStarted * 罪魁祸首其实是 `PermissionUsageHelper` 将 `Context` 对象作为字段持有着,并在构造函数中调用 `AppOpsManager.startWatchingStarted` 开启监听,这样 PermissionUsageHelper 对象就会被添加进 `AppOpsManager#mStartedWatchers` 集合中,这样导致在 Activity 主动调用 finish 的时候,并没有使用 `stopWatchingStarted` 来移除监听,导致 `Activity` 对象一直被 `AppOpsManager#mStartedWatchers` 集合中持有着,所以间接导致了 Activity 对象无法被系统回收。 * 针对这个问题处理也很简单粗暴,就是将在外层传入的 `Context` 参数从 `Activity` 对象给替换成 `Application` 对象即可,有人可能会说了,`Activity` 里面才有 `shouldShowRequestPermissionRationale` 方法,而 Application 里面没有这个方法怎么办?看了一下这个方法的实现,其实那个方法最终会调用 `PackageManager.shouldShowRequestPermissionRationale` 方法(**隐藏 API,但是并不在黑名单中**)里面去,所以只要能获取到 `PackageManager` 对象即可,最后再使用反射去执行这个方法,这样就能避免出现内存泄漏。 * 幸好 Google 没有将 `PackageManager.shouldShowRequestPermissionRationale` 列入到反射黑名单中,否则这次想给 Google 擦屁股都没有办法了,要不然只能用修改系统源码实现的方式,但这种方式只能等谷歌在后续的 Android 版本上面修复了,不过庆幸的是,在 `Android 12 L` 的版本之后,这个问题被修复了,[具体的提交记录可以点击此处查看](https://cs.android.com/android/_/android/platform/frameworks/base/+/0d47a03bfa8f4ca54b883ff3c664cd4ea4a624d9:core/java/android/permission/PermissionUsageHelper.java;dlc=cec069482f80019c12f3c06c817d33fc5ad6151f),但是对于 `Android 12` 而言,这仍是一个历史遗留问题。 * 值得注意的是:XXPermissions 是目前同类框架第一款也是唯一一款修复这个问题的框架,另外针对这个问题,我还给谷歌的 [AndroidX](https://github.com/androidx/androidx/pull/435) 项目无偿提供了解决方案,目前 Merge Request 已被合入主分支,我相信通过这一举措,将解决全球近 10 亿台 Android 12 设备出现的内存泄露问题。 #### 第三方厂商兼容性优化 * 虽然我填的很多坑都是 Google 工程师给挖的,但是这里面也有国内厂商工程师的一份,他们的骚操作对比 Google 有过之而不及,真是应征了那句话,世界是一个草台班子,这里就不多说了,说多了全是泪,直接进入主题: 1. `GET_INSTALLED_APPS` 这个权限是工信部联合各大手机厂商推出的,框架除了按照这套标准进行适配,还做了额外的适配: * 三星的 OneUI 5.1.1 开始支持这个权限,但不是按照工信部出台的这套标准来做的,如果只是按照工信部的标准来适配,根本无法申请这个权限,但是框架内部针对 OneUI 进行了兼容,目前所有的 OneUI 版本都支持申请该权限。 * 小米的 MIUI 13 开始按照工信部的要求进行适配,如果是之前的机型则可能无法申请该权限,但是框架内部针对低版本的 MIUI 进行了兼容,目前所有的 MIUI 版本都支持申请该权限。 * 魅族 Flyme 8.x 有这个读取应用列表权限的入口,但是经过测试发现,Flyme 并没有按照工信部出台的这套标准来做的,也就是在这种情况是无法申请到这个权限的,然而框架内部做了一些兼容手段,目前已经适配了 Flyme 9.x 及之后的版本读取应用列表权限的申请。 2. 悬浮窗权限兼容 Android 6.0 及以下机型:众所周知 `SYSTEM_ALERT_WINDOW` 是 Android 6.0 才开始新增的特殊权限,在此之前的版本是不支持的,但是有一些国内厂商已经提前做了这个权限,这就会导致一个兼容问题,有部分 Android 6.0 以下的用户是无法申请悬浮窗权限,针对这个问题,框架目前进行了兼容,目前所有的机型都支持申请该权限。 3. `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` 权限针对小米机型的优化:请求忽略电池优化选项权限在用户授权后,发现在小米机型用代码判断权限状态还是 false,然后再次点击申请权限,却发现权限已经授予,经过排查后发现,在授权后不能立即判断这个权限的状态,否则是不准确的,需要延迟一段时间判断才是准确的,经过无数次实验,确定需要延迟 1000 毫秒才没问题,众所周知 1000 毫秒等于 1 秒,这个延迟也太大了,我都怀疑他们到底有没有自测就把代码发出来了,不过有一个好消息,后续发现澎湃 2.0 修复了这个 Bug,至于为什么会修复这个问题,我对比了一下澎湃 1.0 和澎湃 2.0,结果发现请求忽略电池优化选项权限的设置页 UI 有很大的改动,我想如果他们这辈子没有进行大改版,这个 Bug 将会永远存在。 4. `ACCESS_NOTIFICATION_POLICY` 针对华为或者荣耀机型兼容性处理:勿扰权限在华为或者荣耀机型上面无法跳转到当前应用的勿扰权限设置页,只能跳转到所有应用的勿扰权限列表页,再找到对应开启,到这里肯定有人站出来说了,这就是送分题,你在跳转之前,先判断 `Intent` 是否存在再跳不就解决了?你的想法很好,但是行不通,我用代码判断这个 `Intent` 是存在的,也能成功跳转,但是立马给你返回失败,具体情况你可以看一下 [XXPermissions/issues/190](https://github.com/getActivity/XXPermissions/issues/190),目前框架的解决方案是,如果当前厂商系统是 `HarmonyOS`、`MagicOS`、`EMUI` 中的任一一个,就不跳转到当前应用的勿扰权限设置页去授权,而是跳转到所有应用的勿扰权限列表页去授权,虽然这样麻烦一点,用户体验也会变差,但却是目前能想到的最好的解决方案。 * 针对以上问题,都在框架内部进行了处理,虽然无需你做任何处理,但是你仍需知道有这样一件事,所以哪里有什么岁月静好,只不过有前人替你把坑踩了个遍。虽然厂商们总是填坑少挖坑多,但是也有某些厂商在某个细节做得很不错,具体如下: 1. 小米有一个细节做得很好,就是支持当前应用跳转到具体的权限设置页,这是由于[小米自己开放](https://dev.mi.com/docs/appsmarket/technical_docs/adaptation_FAQ/#10) 了 `miui.intent.action.APP_PERM_EDITOR` 这个隐式意图才能实现,目前框架已经进行了适配,这样用户在小米机型上面手动授权的时候,就不需要先跳转到应用详情页上面,再点一下才能进去权限设置页了。 1. 魅族有一个细节做得很好,就是支持当前应用跳转到具体的权限设置页,这是由于魅族自己开放了 `com.meizu.safe.security.SHOW_APPSEC` 这个隐式意图才能实现,目前框架已经进行了适配,这样用户在魅族机型上面手动授权的时候,就不需要先跳转到应用详情页上面,再点一下才能进去权限设置页了。 2. OPPO 有一个细节做得很好,不仅可以直接跳转到具体的权限设置页 ,还能支持高亮要授权的权限选项,具体接入文档可以看这里 [OPPO 应用权限受阻跳转优化适配](https://open.oppomobile.com/new/developmentDoc/info?id=12983),目前框架已经进行了适配,这样用户在 OPPO 机型上面手动授权的时候,会自动滚动并高亮要设置的权限项,用户体验会大大提升,在此希望其他国内的厂商们跟进。 #### 支持检测代码错误 * 在框架的日常维护中,有很多人跟我反馈过框架有 Bug,但是经过排查和定位发现,这其中有 95% 的问题来自于调用者一些不规范操作导致的,这不仅对我造成很大的困扰,同时也极大浪费了很多小伙伴的时间和精力,于是我在框架中加入了很多审查元素,在 **debug 模式**、**debug 模式**、**debug 模式** 下,一旦有某些操作不符合规范,那么框架会直接抛出异常给调用者,并在异常信息中正确指引调用者纠正错误,例如: * 如果调用者没有传入任何权限就申请权限的话,框架会抛出异常。 * 传入的 Context 实例不是 Activity 对象,框架会抛出异常,又或者传入的 Activity 的状态异常(已经 **Finishing** 或者 **Destroyed**),这种情况一般是在异步申请权限导致的,框架也会抛出异常,请在合适的时机申请权限,如果申请的时机无法预估,请在外层做好 Activity 状态判断再进行权限申请。 * 如果当前项目在没有适配分区存储的情况下,申请 `READ_EXTERNAL_STORAGE` 和 `WRITE_EXTERNAL_STORAGE` 权限 * 当项目的 `targetSdkVersion >= 29` 时,需要在清单文件中注册 `android:requestLegacyExternalStorage="true"` 属性,否则框架会抛出异常,如果不加会导致一个问题,明明已经获取到存储权限,但是无法在 Android 10 的设备上面正常读写外部存储上的文件。 * 当项目的 `targetSdkVersion >= 30` 时,则不能申请 `READ_EXTERNAL_STORAGE` 和 `WRITE_EXTERNAL_STORAGE` 权限,而是应该申请 `MANAGE_EXTERNAL_STORAGE` 权限 * 如果当前项目已经适配了分区存储,那么只需要在清单文件中注册一个 meta-data 属性即可: `` * 如果申请的权限和项目中的 **targetSdkVersion** 对不上,框架会抛出异常,是因为 **targetSdkVersion** 代表着项目适配到哪个 Android 版本,系统会自动做向下兼容,假设申请的权限是 Android 11 才出现的,但是 **targetSdkVersion** 还停留在 29,那么在某些机型上的申请,会出现授权异常的情况,也就是用户明明授权了,但是系统返回的始终是 false。 * 如果动态申请的权限没有在 `AndroidManifest.xml` 中进行注册,框架会抛出异常,因为如果不这么做,是可以进行申请权限,但是不会出现授权弹窗,直接被系统拒绝,并且系统不会给出任何弹窗和提示,并且这个问题在每个机型上面都是**必现的**。 * 如果动态申请的权限有在 `AndroidManifest.xml` 中进行注册,但是设定了不恰当的 `android:maxSdkVersion` 属性值,框架会抛出异常,举个例子:``,这样的设定会导致在 Android 11 (`Build.VERSION.SDK_INT >= 30`)及以上的设备申请权限,系统会认为这个权限没有在清单文件中注册,直接拒绝本次的权限申请,并且也是不会给出任何弹窗和提示,这个问题也是必现的。 * 如果你同时申请了 `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` 来申请,所以在低版本下也不会因为没有权限导致的无法使用。 * 如果你不需要上面这些检测,可通过调用 `unchecked` 方法来关闭,但是需要注意的是,我并不建议你去关闭这个检测,因为在 **release 模式** 时它是关闭状态,不需要你手动关闭,而它只在 **debug 模式** 下才会触发这些检测。 * 出现这些问题的原因是,我们对这些机制不太熟悉,而如果框架不加以限制,那么引发各种奇奇怪怪的问题出现,作为框架的作者,表示不仅你们很痛苦,作为框架作者表示也很受伤。因为这些问题不是框架导致的,而是调用者的某些操作不规范导致的。我觉得这个问题最好的解决方式是,由框架做统一的检查,因为我是框架的作者,对权限申请这块知识点有**较强的专业能力和足够的经验**,知道什么该做,什么不该做,这样就可以对这些骚操作进行一一拦截。 * 当权限申请出现问题时,你希不希望能有个人过来提醒你,告诉你哪里错了?该怎么去纠正?然而这些 XXPermissions 都做到了,在所有的权限请求框架中,我算是第一个做这件事的人,我认为**做好一个框架**不仅仅是要把功能做好,把复杂的场景处理好,更重要的是要**以人为本**,因为框架本身就是为人服务的,要做的不仅仅是解决大家的需求,还要帮助大家在这个过程中少走弯路。 ================================================ FILE: HelpDoc-en.md ================================================ #### Catalog * [Android 11 location permission adaptation](#android-11-location-permission-adaptation) * [Android 11 storage permission adaptation](#android-11-storage-permission-adaptation) * [When do I need to adapt to the characteristics of partitioned storage](#when-do-i-need-to-adapt-to-the-characteristics-of-partitioned-storage) * [Why does the app restart after Android 11 grants the install permission](#why-does-the-app-restart-after-android-11-grants-the-install-permission) * [Why is the storage permission granted but the permission setting page still shows unauthorized](#why-is-the-storage-permission-granted-but-the-permission-setting-page-still-shows-unauthorized) * [What should I do if the dialog box pops up before and after the permission application](#what-should-i-do-if-the-dialog-box-pops-up-before-and-after-the-permission-application) * [How to know in the callback which permissions are permanently denied](#how-to-know-in-the-callback-which-permissions-are-permanently-denied) * [Why does the new version of the framework remove the function of automatically applying for AndroidManifest permissions](#why-does-the-new-version-of-the-framework-remove-the-function-of-automatically-applying-for-androidmanifest-permissions) * [Why does the new version of the framework remove the function of constantly applying for permissions](#why-does-the-new-version-of-the-framework-remove-the-function-of-constantly-applying-for-permissions) * [Why not use ActivityResultContract to request permission](#why-not-use-activityresultcontract-to-request-permission) * [How to deal with the problem that the permission request is successful but the blank pass is returned](#how-to-deal-with-the-problem-that-the-permission-request-is-successful-but-the-blank-pass-is-returned) * [Why cannot I access the files in the Android/data directory after authorization](#why-cannot-i-access-the-files-in-the-androiddata-directory-after-authorization) * [Is there any problem with skipping the installation permission application and installing the apk directly](#Is-there-any-problem-with-skipping-the-installation-permission-application-and-installing-the-apk-directly) #### Android 11 Location Permission Adaptation * On Android 10, positioning permissions are divided into foreground permissions (precise and fuzzy) and background permissions, while on Android 11, you need to apply for these two permissions separately. If you apply for these two permissions ** Ruthlessly rejected by the system ** at the same time, even the permission application dialog box will not pop up, and the system will reject it immediately. It directly leads to the failure of location permission application. * If you are using the latest version of **XXPermissions**, you ** Congratulations ** can directly pass the foreground and background positioning permissions to the framework. The framework has automatically applied for these two permissions separately for you. The whole adaptation process ** Zero cost **. * However, it should be noted that the application process is divided into two steps. The first step is to apply for the foreground location permission, and the second step is to apply for the background location permission. The user must first agree to the foreground location permission before entering the application for the background location permission. There are two ways to approve the foreground location permission: check `Allow only while using the app` or `Ask every time`. In the background location permission application, the user must check `Allow all the time`. Only in this way can the background location permission application be approved. * And if your application only needs to use the location function in the foreground, but does not need to use the location function in the background, please do not apply for `Permission.ACCESS_BACKGROUND_LOCATION` permission. ![](picture/en/help_doc_android_11_location_adapter_1.jpg) ![](picture/en/help_doc_android_11_location_adapter_2.jpg) #### Android 11 storage permission adaptation * If your project needs to adapt to Android 11 storage permissions, you need to upgrade targetSdkVersion first. ```groovy android defaultConfig { targetSdkVersion 30 } } ``` * Add Android 11storage permissions to register in the manifest file. ```xml ``` * It should be noted that the old version of the storage permissions also need to be registered in the manifest file, because the framework will automatically switch to the old version of the application mode when applying for storage permissions in an environment lower than Android 11. ```xml ``` * You also need to add this attribute to the manifest file, otherwise you won't be able to read and write files on external storage on Android 10 devices. ```xml ``` * Finally, call the following code directly. ```java XXPermissions.with(MainActivity.this) // The scoped storage that has been adapted to Android 11 needs to be called like this //.permission(PermissionLists.getReadExternalStoragePermission()) //.permission(PermissionLists.getWriteExternalStoragePermission()) // Not yet adapted to Android 11 scoped storage needs to be called like this .permission(PermissionLists.getManageExternalStoragePermission()) .request(new OnPermissionCallback() { ...... }); ``` ![](picture/en/demo_request_manage_storage_permission.jpg) #### When do I need to adapt to the characteristics of partitioned storage * If your app needs to be available on Google Play, you need to check it out in detail: [ Google App Store policy (need to climb over the wall) ](https://support.google.com/googleplay/android-developer/answer/9956427). [ Google Play notifications ](https://developer.android.google.cn/training/data-storage/manage-all-files#all-files-access-google-play) * The origin of scoped storage: Google has received many complaints from users before, saying that many applications create directories and files under the SD card, which makes it very troublesome for users to manage mobile phone files (there are so many foreign netizens with obsessive-compulsive disorder, ha ha), so in the Android 10 version update. Google requires all developers to store media files in their own internal directory or in the internal directory of the SD card, but Google has adopted a relaxed policy on one version, adding `android:requestLegacyExternalStorage="true"` the adaptation of this feature to the manifest file, but on Android 11, you have two options: 1. Adapting scoped storage: This is a method recommended by Google, but it will increase the workload, because it is very troublesome to adapt scoped storage, which is my personal feeling. However, for some specific applications, such as file managers, backup and recovery applications, anti-virus applications, document management applications, on-device file search, disk and file encryption, device-to-device data migration and so on, they must use external storage, which requires the second way to achieve. 2. Apply for external storage permissions: This is a way that Google does not recommend. It only needs `MANAGE_EXTERNAL_STORAGE` permissions, and there is basically no pressure to adapt. However, there will be a problem, that is, when it is put on the Google App Market, it must be reviewed and approved by Google Play. * To sum up, I think both are good and bad, but I can share my views with you. 1. If your app needs to be on the Google Apps Marketplace, you need to adapt to partitioned storage as soon as possible, because Google is really doing it this time. 2. If your application is only available in the china application market, and there is no subsequent need to be available in the Google application market, then you can also directly apply for `MANAGE_EXTERNAL_STORAGE` permission to read and write external storage. #### Why does the app restart after Android 11 grants the install permission * [Android 11 feature adjustment, installation of external source application requires restarting App](https://cloud.tencent.com/developer/news/637591) * First of all, this problem is a new feature of Android 11, not caused by the framework. Of course, there is no way to avoid this problem, because the application is killed by the system, and the level of the application is certainly not as high as that of the system. At present, there is no solution for this in the industry. If you have a good solution, you are welcome to provide it to me. * In addition, after practice, this problem will no longer appear on Android 12, proving that the problem has been fixed by Google. #### Why is the storage permission granted but the permission setting page still shows unauthorized * First of all, I need to correct a wrong idea. `READ_EXTERNAL_STORAGE` `WRITE_EXTERNAL_STORAGE` These two permissions and `MANAGE_EXTERNAL_STORAGE` permissions are two different things. Although they are both called storage permissions, they belong to two completely different permissions. If you apply for `MANAGE_EXTERNAL_STORAGE` permission and grant permission, However, you do not see that the permission has been granted on the permission setting page. Please note that this situation is normal, because what you see on the permission setting page is the storage grant status `READ_EXTERNAL_STORAGE` and `WRITE_EXTERNAL_STORAGE` permission status, not `MANAGE_EXTERNAL_STORAGE` the permission status, but at this time, the storage permission has been obtained. You don't have to worry about the permission status displayed on the permission setting page. You can read and write files directly. There will be no permission problem. * One more question, why only appear on devices above Android 11? First of all `MANAGE_EXTERNAL_STORAGE`, only Android 11 has permission. Android 10 and previous versions do not have this permission. If you apply for `MANAGE_EXTERNAL_STORAGE` permission on a lower version device, the framework will help you do downward compatibility. Will automatically help you replace `READ_EXTERNAL_STORAGE`, `WRITE_EXTERNAL_STORAGE` permissions to apply, this time you see the permission settings page of the storage permission status must be normal, which is why you only see this problem in Android 11 and above devices. #### What should I do if the dialog box pops up before and after the permission application * There are interfaces provided within the framework that can fulfill this requirement, It is enough to implement the interface provided [OnPermissionDescription](library/src/main/java/com/hjq/permissions/OnPermissionDescription.java) and [OnPermissionInterceptor](library/src/main/java/com/hjq/permissions/OnPermissionInterceptor.java) in the framework. For specific implementation, please refer to the [PermissionDescription](app/src/main/java/com/hjq/permissions/demo/PermissionDescription.java) and [PermissionInterceptor](app/src/main/java/com/hjq/permissions/demo/PermissionInterceptor.java) class provided in Demo. It is recommended to download the source code and read it, and then introduce the code into the project * The way to use is also very simple. There are two specific settings, one for local settings and the other for global settings. ```java XXPermissions.with(this) .permission(PermissionLists.getXxx()) // Set permission request description (local settings) .description(new PermissionDescription()) // Set permission request interceptor (local settings) .interceptor(new PermissionInterceptor()) .request(new OnPermissionCallback() { ...... }); ``` ```java public class XxxApplication extends Application { @Override public void onCreate() { super.onCreate(); // Set permission request description (global setting) XXPermissions.setPermissionDescription(PermissionDescription.class); // Set permission request interceptor (global setting) XXPermissions.setPermissionInterceptor(PermissionInterceptor.class); } } ``` #### How to know in the callback which permissions are permanently denied * Requirement scenario: Suppose you apply for calendar permission and recording permission at the same time, but both are rejected by the user. However, one of the two groups of permissions is permanently rejected. How to determine whether a certain group of permissions is permanently rejected? Here is a code example: ```java XXPermissions.with(this) .permission(PermissionLists.getRecordAudioPermission()) .permission(PermissionLists.getReadCalendarPermission()) .permission(PermissionLists.getWriteCalendarPermission()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { IPermission recordAudioPermission = PermissionLists.getRecordAudioPermission(); if (deniedList.contains(recordAudioPermission) && XXPermissions.isDoNotAskAgainPermission(activity, recordAudioPermission)) { toast("The recording permission request was denied, and the user checked Do not ask"); } return; } toast("Acquired recording and calendar permissions successfully"); } }); ``` #### Why does the new version of the framework remove the function of automatically applying for AndroidManifest permissions > [ [Issue] It is recommended to restore the two practical functions of jumping to the permission setting page and obtaining all permissions of AndroidManifest](https://github.com/getActivity/XXPermissions/issues/54) * The function of obtaining the list permission and applying. Although this is very convenient, there are some hidden dangers. Because the list file in apk is ultimately merged by the list files of multiple modules, it will become uncontrollable. This will make it impossible for us to predict the permissions applied for, and it will also mix some unnecessary permissions. Therefore, after careful consideration, this function will be removed. #### Why does the new version of the framework remove the function of constantly applying for permissions > [ [Issue] Optimization issue with keep requesting get after permission denied](https://github.com/getActivity/XXPermissions/issues/39) * Assuming that the user refuses the permission, if the framework applies again, the possibility that the user will grant it is relatively small. At the same time, some app stores have disabled this behavior. After careful consideration, the API related to this function will be removed. * If you still want to use this way to apply for permission, in fact, there is no way, you can refer to the following ways to achieve. ```java public class PermissionActivity extends AppCompatActivity implements OnPermissionCallback { @Override public void onClick(View view) { requestCameraPermission(); } private void requestCameraPermission() { XXPermissions.with(this) .permission(PermissionLists.getCameraPermission()) .request(this); } @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { boolean doNotAskAgain = XXPermissions.isDoNotAskAgainPermissions(activity, deniedList); if (doNotAskAgain) { toast("Authorization is permanently denied, please manually grant permission to take camera"); // If it is permanently denied, jump to the application permission system settings page XXPermissions.startPermissionActivity(activity, deniedList); } else { requestCameraPermission(); } return; } toast("Successfully obtained permission to take camera"); } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode != XXPermissions.REQUEST_CODE) { return; } toast("Detected that you just returned from the permission settings interface"); } } ``` #### Why not use ActivityResultContract to request permission > [ [Issue] Whether the permission application for onActivityResult callback has been considered and switched to ActivityResultContract](https://github.com/getActivity/XXPermissions/issues/103) * Activity ResultContract is a new API added in Activity `1.2.0-alpha02` and Fragment `1.3.0-alpha02`, which has a certain threshold for use, and the project must be based on Android X. And the version of Android X must be `1.3.0-alpha01` above. If it is replaced `ActivityResultContract`, some developers will not be able to use **XXPermissions**, which is a serious problem. But in fact, changing to Activity ResultContract does not bring any benefits. For example, I have solved the problems of Fragment screen rotation and background application before, so what is the significance of changing? Some people may say that the official onActivityResult has been marked as obsolete. Don't worry. The reason why it is marked as obsolete is just for Google to promote new technology. But it can be clearly said that the official will not delete this API. More accurately, it will not dare. Why? You can see how Activity ResultContract is implemented? It is also implemented by rewriting the `onRequestPermissionsResult` method callback of the Activity `onActivityResult`. You can see the implementation of these two methods in the `androidx.activity.ComponentActivity` class, which will not be repeated here. #### How to deal with the problem that the permission request is successful but the blank pass is returned * There is no solution to this problem. The permission request framework can only help you apply for permission. As for what you do when you apply for permission, the framework cannot know or intervene. The return of the blank pass is the manufacturer's own behavior. The purpose is to protect the user's privacy, because it cannot be used without permission in some applications. The return of the blank pass is to avoid this situation. You want to ask me what to do? I can only say that the arm can't resist the thigh, so don't make some unnecessary resistance. #### Why cannot I access the files in the Android/data directory after authorization * First of all, no matter what kind of storage permission you apply for, you cannot directly read the android/data directory on Android 11. This is a new feature on Android 11, and you need to make additional adaptation. You can refer to this open source project for the specific adaptation process. #### Is there any problem with skipping the installation permission application and installing the apk directly * If you are careful, you may find that you can install apk without installation permissions. So why should I apply for `REQUEST_INSTALL_PACKAGES` permissions? Isn't that unnecessary? * Here I want to say, is not what you imagine, next let us experiment, here selected `Google piexl 3XL (Android 12)` and `Xiaomi phone 12 (Android 12)` respectively do a test ```java Intent intent = new Intent(Intent.ACTION_VIEW); Uri uri; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { uri = FileProvider.getUriForFile(context, context.getPackageName() + ".provider", file); } else { uri = Uri.fromFile(file); } intent.setDataAndType(uri, "application/vnd.android.package-archive"); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); context.startActivity(intent); ``` ![](picture/en/help_doc_install_package_android_1.jpg) ![](picture/en/help_doc_install_package_android_2.jpg) ![](picture/en/help_doc_install_package_miui_1.jpg) ![](picture/en/help_doc_install_package_miui_2.jpg) * See here, I believe you have noticed some differences, also jump to install apk page, on the Android native system, will show the `Cancel` and `Settings` option, click `Cancel` option will cancel the installation, only click `Settings` option, will let you grant the installation package permissions, On top of MIUI, the `Allow` and `Restrict` options are displayed, as well as a `Don't show again` option. If the user checks `Don't show again` and clicks the `Restrict` option, The next time the application goes to the install apk page, it will be directly rejected by the system, and only a toast prompt will be displayed. The conclusion of the problem is: You can directly jump to the page of installing apk, but it is not recommended to do so, because on some mobile phones, the system may directly reject the request to install apk, so the standard writing should be, first judge whether there is no installation permission, if not, apply for, if there is, then jump to the page of installing apk, Of course, if you apply for installation permissions using this framework, you don't need to determine whether there are permissions or not to apply directly. Whether there is authorization or not, it will inform you through the callback, and you can then handle it from the callback. ================================================ FILE: HelpDoc-zh.md ================================================ #### 目录 * [Android 11 定位权限适配](#android-11-定位权限适配) * [Android 11 存储权限适配](#android-11-存储权限适配) * [什么情况下需要适配分区存储特性](#什么情况下需要适配分区存储特性) * [Android 11 授予了安装权限之后为什么应用重启了](#android-11-授予了安装权限之后为什么应用重启了) * [为什么授予了存储权限但是权限设置页还是显示未授权](#为什么授予了存储权限但是权限设置页还是显示未授权) * [我想在申请前和申请后统一弹对话框该怎么处理](#我想在申请前和申请后统一弹对话框该怎么处理) * [如何在回调中知道哪些权限被永久拒绝了](#如何在回调中知道哪些权限被永久拒绝了) * [为什么不兼容 Android 6.0 以下的危险权限申请](#为什么不兼容-android-60-以下的危险权限申请) * [新版框架为什么移除了自动申请清单权限的功能](#新版框架为什么移除了自动申请清单权限的功能) * [新版框架为什么移除了不断申请权限的功能](#新版框架为什么移除了不断申请权限的功能) * [新版框架为什么移除了国产手机权限设置页功能](#新版框架为什么移除了国产手机权限设置页功能) * [为什么不用 ActivityResultContract 来申请权限](#为什么不用-activityresultcontract-来申请权限) * [怎么处理权限请求成功但是返回空白通行证的问题](#怎么处理权限请求成功但是返回空白通行证的问题) * [为什么授权了还是无法访问 Android/data 目录下的文件](#为什么授权了还是无法访问-android-data-目录下的文件) * [跳过安装权限申请然后直接安装 apk 会有什么问题吗](#跳过安装权限申请然后直接安装-apk-会有什么问题吗) * [如何应对国内某些应用商店在明确拒绝权限后 48 小时内不允许再次申请的问题](#如何应对国内某些应用商店在明确拒绝权限后-48-小时内不允许再次申请的问题) #### Android 11 定位权限适配 * 在 Android 10 上面,定位权限被划分为前台权限(精确和模糊)和后台权限,而到了 Android 11 上面,需要分别申请这两种权限,如果同时申请这两种权限会**惨遭系统无情拒绝**,连权限申请对话框都不会弹,立马被系统拒绝,直接导致定位权限申请失败。 * 如果你使用的是 **XXPermissions** 最新版本,那么**恭喜你**,直接将前台定位权限和后台定位权限全部传给框架即可,框架已经自动帮你把这两种权限分开申请了,整个适配过程**零成本**。 * 但是需要注意的是:申请过程分为两个步骤,第一步是申请前台定位权限,第二步是申请后台定位权限,用户必须要先同意前台定位权限才能进入后台定位权限的申请。同意前台定位权限的方式有两种:勾选 `仅在使用该应用时允许` 或 `仅限这一次`,而到了后台定位权限申请中,用户必须要勾选 `始终允许`,只有这样后台定位权限才能申请通过。 * 还有如果你的应用只需要在前台使用定位功能, 而不需要在后台中使用定位功能,那么请不要连带申请 `Permission.ACCESS_BACKGROUND_LOCATION` 权限。 ![](picture/zh/help_doc_android_11_location_adapter_1.jpg) ![](picture/zh/help_doc_android_11_location_adapter_2.jpg) #### Android 11 存储权限适配 * 如果你的项目需要适配 Android 11 存储权限,那么需要先将 targetSdkVersion 进行升级 ```groovy android defaultConfig { targetSdkVersion 30 } } ``` * 再添加 Android 11 存储权限注册到清单文件中 ```xml ``` * 需要注意的是,旧版的存储权限也需要在清单文件中注册,因为在低于 Android 11 的环境下申请存储权限,框架会自动切换到旧版的申请方式 ```xml ``` * 还需要在清单文件中加上这个属性,否则在 Android 10 的设备上将无法正常读写外部存储上的文件 ```xml ``` * 最后直接调用下面这句代码 ```java XXPermissions.with(MainActivity.this) // 适配 Android 11 分区存储这样写 //.permission(PermissionLists.getReadExternalStoragePermission()) //.permission(PermissionLists.getWriteExternalStoragePermission()) // 不适配 Android 11 分区存储这样写 .permission(PermissionLists.getManageExternalStoragePermission()) .request(new OnPermissionCallback() { ...... }); ``` ![](picture/zh/demo_request_manage_storage_permission.jpg) #### 什么情况下需要适配分区存储特性 * 如果你的应用需要上架 GooglePlay,那么需要详细查看:[谷歌应用商店政策(需要翻墙)](https://support.google.com/googleplay/android-developer/answer/9956427)、[Google Play 通知](https://developer.android.google.cn/training/data-storage/manage-all-files#all-files-access-google-play) * 分区存储的由来:谷歌之前收到了很多用户投诉,说很多应用都在 SD 卡下创建目录和文件,导致用户管理手机文件非常麻烦(强迫症的外国网友真多,哈哈),所以在 Android 10 版本更新中,谷歌要求所有开发者将媒体文件存放在自己内部目录或者 SD 卡内部目录中,不过谷歌在一版本上采取了宽松政策,在清单文件中加入 `android:requestLegacyExternalStorage="true"` 即可跳过这一特性的适配,不过在 Android 11 上面,你有两种选择: 1. 适配分区存储:这个是谷歌推荐的一种方式,但是会增加工作量,因为分区存储适配起来十分麻烦,我个人感觉是这样的。不过对于一些特定应用,例如文件管理器、备份和恢复应用、防病毒应用、文档管理应用、设备上的文件搜索、磁盘和文件加密、设备到设备数据迁移等这类应用它们就一定需要用到外部存储,这个时候就需要用第二种方式来实现了。 2. 申请外部存储权限:这个是谷歌不推荐的一种方式,只需要 `MANAGE_EXTERNAL_STORAGE` 权限即可,适配起来基本无压力,但是会存在一个问题,就是上架谷歌应用市场的时候,要经过 Google Play 审核和批准。 * 这两种总结下来,我觉得各有好坏,不过我可以跟大家谈谈我的看法 1. 如果你的应用需要上架谷歌应用市场,需要尽快适配分区存储,因为谷歌这次来真的了 2. 如果你的应用只上架国内的应用市场,并且后续也没有上架谷歌应用市场的需要,那么你也可以直接申请 `MANAGE_EXTERNAL_STORAGE` 权限来读写外部存储 #### Android 11 授予了安装权限之后为什么应用重启了 * [Android 11 特性调整,安装外部来源应用需要重启 App](https://cloud.tencent.com/developer/news/637591) * 先说结论,这个问题是 Android 11 的新特性,并非框架的问题导致的,当然这个问题是没有办法规避的,因为应用是被系统杀死的,应用的等级肯定不如系统的高,目前行业对这块也没有解决方案,如果你有好的解决方案,欢迎你提供给我。 * 另外经过实践,这个问题在 Android 12 上面已经不会再出现,证明问题已经被谷歌修复了。 #### 为什么授予了存储权限但是权限设置页还是显示未授权 * 首先我需要先纠正大家一个错误的想法,`READ_EXTERNAL_STORAGE`、`WRITE_EXTERNAL_STORAGE` 这两个权限和 `MANAGE_EXTERNAL_STORAGE` 权限是两码事,虽然都叫存储权限,但是属于两种完全不同的权限,你如果申请的是 `MANAGE_EXTERNAL_STORAGE` 权限,并且授予了权限,但是在权限设置页并没有看到已授予,请注意这种情况是正常的,因为你在权限设置页看到的是存储授予状态是 `READ_EXTERNAL_STORAGE`、`WRITE_EXTERNAL_STORAGE` 权限状态的,而不是 `MANAGE_EXTERNAL_STORAGE` 权限状态的,但是这个时候已经获取到存储权限了,你大可不必管权限设置页显示的权限状态,直接读写文件即可,不会有权限问题的。 * 还有一个问题,为什么只在 Android 11 以上的设备出现?首先 `MANAGE_EXTERNAL_STORAGE` 权限是 Android 11 才有权限,Android 10 及之前的版本是没有这个权限的,你如果在低版本设备上申请了 `MANAGE_EXTERNAL_STORAGE` 权限,那么框架会帮你做向下兼容,会自动帮你替换成 `READ_EXTERNAL_STORAGE`、`WRITE_EXTERNAL_STORAGE` 权限去申请,这个时候你看到权限设置页的存储权限状态肯定是正常的,这就是为什么你只在 Android 11 以上的设备才会看到这个问题。 #### 我想在申请前和申请后统一弹对话框该怎么处理 * 框架内部有提供接口可以实现这一需求,通过实现框架中提供的 [OnPermissionDescription](library/src/main/java/com/hjq/permissions/OnPermissionDescription.java) 和 [OnPermissionInterceptor](library/src/main/java/com/hjq/permissions/OnPermissionInterceptor.java) 接口即可,具体实现可参考 Demo 中提供的 [PermissionDescription](app/src/main/java/com/hjq/permissions/demo/PermissionDescription.java) 和 [PermissionInterceptor](app/src/main/java/com/hjq/permissions/demo/PermissionInterceptor.java) 类,建议下载源码后进行阅读,再将代码引入到项目中 * 使用方式也很简单,具体有两种设置方式,一种针对局部设置,另外一种是全局设置 ```java XXPermissions.with(this) .permission(PermissionLists.getXxx()) // 设置权限说明(局部设置) .description(new PermissionDescription()) // 设置权限请求拦截器(局部设置) .interceptor(new PermissionInterceptor()) .request(new OnPermissionCallback() { ...... }); ``` ```java public class XxxApplication extends Application { @Override public void onCreate() { super.onCreate(); // 设置权限说明(全局设置) XXPermissions.setPermissionDescription(PermissionDescription.class); // 设置权限请求拦截器(全局设置) XXPermissions.setPermissionInterceptor(PermissionInterceptor.class); } } ``` #### 如何在回调中知道哪些权限被永久拒绝了 * 需求场景:假设同时申请日历权限和录音权限,结果都被用户拒绝了,但是这两组权限中有一组权限被永久拒绝了,如何判断某一组权限有没有被永久拒绝?这里给出代码示例: ```java XXPermissions.with(this) .permission(PermissionLists.getRecordAudioPermission()) .permission(PermissionLists.getReadCalendarPermission()) .permission(PermissionLists.getWriteCalendarPermission()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { IPermission recordAudioPermission = PermissionLists.getRecordAudioPermission(); if (deniedList.contains(recordAudioPermission) && XXPermissions.isDoNotAskAgainPermission(activity, recordAudioPermission)) { toast("录音权限请求被拒绝了,并且用户勾选了不再询问"); } return; } toast("获取录音和日历权限成功"); } }); ``` #### 为什么不兼容 Android 6.0 以下的危险权限申请 * 因为 Android 6.0 以下的危险权限管理是手机厂商做的,那个时候谷歌还没有统一危险权限管理的方案,所以就算我们的应用没有适配也不会有任何问题,因为手机厂商对这块有自己的处理,但是有一点是肯定的,就算用户拒绝了授权,也不会导致应用崩溃,只会返回空白的通行证。 * 如果 **XXPermissions** 做这块的适配也可以做到,通过反射系统服务 AppOpsManager 类中的字段即可,但是并不能保证权限判断的准确性,可能会存在一定的误差,其次是适配的成本太高,因为国内手机厂商太多,对这块的改动参差不齐。 * 考虑到 Android 6.0 以下的设备占比很低,后续也会越来越少,会逐步退出历史的舞台,所以我的决定是不对这块做适配。 #### 新版框架为什么移除了自动申请清单权限的功能 > [【issue】建议恢复跳转权限设置页和获取AndroidManifest的所有权限两个实用功能](https://github.com/getActivity/XXPermissions/issues/54) * 获取清单权限并申请的功能,这个虽然非常方便,但是存在一些隐患,因为 apk 中的清单文件最终是由多个 module 的清单文件合并而成,会变得不可控,这样会使我们无法预估申请的权限,并且还会掺杂一些不需要的权限,所以经过慎重考虑移除该功能。 #### 新版框架为什么移除了不断申请权限的功能 > [【issue】关于拒绝权限后一直请求获取的优化问题](https://github.com/getActivity/XXPermissions/issues/39) * 假设用户拒绝了权限,如果框架再次申请,那么用户会授予的可能性也是比较小,同时某些应用商店已经禁用了这种行为,经过慎重考虑,对这个功能相关的 API 进行移除。 * 如果你还想用这种方式来申请权限,其实并不是没有办法,可以参考以下方式来实现 ```java public class PermissionActivity extends AppCompatActivity implements OnPermissionCallback { @Override public void onClick(View view) { requestCameraPermission(); } private void requestCameraPermission() { XXPermissions.with(this) .permission(PermissionLists.getCameraPermission()) .request(this); } @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { boolean doNotAskAgain = XXPermissions.isDoNotAskAgainPermissions(activity, deniedList); if (doNotAskAgain) { toast("被永久拒绝授权,请手动授予拍照权限""); // 如果是被永久拒绝就跳转到应用权限系统设置页面 XXPermissions.startPermissionActivity(activity, deniedList); } else { requestCameraPermission(); } return; } toast("获取拍照权限成功"); } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode != XXPermissions.REQUEST_CODE) { return; } toast("检测到你刚刚从权限设置界面返回回来"); } } ``` #### 新版框架为什么移除了国产手机权限设置页功能 > [【issue】权限拒绝并不再提示的问题](https://github.com/getActivity/XXPermissions/issues/99) > [【issue】正常申请存储权限时,永久拒绝,然后再应用设置页开启权限询问,系统权限申请弹窗未显示](https://github.com/getActivity/XXPermissions/issues/100) * **XXPermissions** 9.0 及之前是有存在这一功能的,但是我在后续的版本上面将这个功能移除了,原因是有很多人跟我反馈这个功能其实存在很大的缺陷,例如在一些华为新机型上面可能跳转的页面不是应用的权限设置页,而是所有应用的权限管理列表界面。 * 首先这个问题要从 **XXPermissions** 跳转到国产手机设置页的原理讲起,从谷歌提供的原生 API 我们最多只能跳转到应用详情页,并不能直接跳转到权限设置页,而需要用户在应用详情页再次点击才能进入权限设置页。如果从用户体验的角度上看待这个问题,肯定是直接跳转到权限设置页是最好的,但是这种方式是不受谷歌支持的,当然也有方法实现,网上都有一个通用的答案,就是直接捕获某个品牌手机的权限设置页 `Activity` 包名然后进行跳转。这种想法的起点是好的,但是存在许多问题,并不能保证每个品牌的所有机型都能适配到位,手机产商更改这个 `Activity` 的包名的次数和频率比较高,在最近发布的一些新的华为机型上面几乎已经全部失效,也就是 `startActivity` 的时候会报 `ActivityNotFoundException` 或 `SecurityException` 异常,当然这些异常是可以被捕捉到的,但是仅仅只能捕获到崩溃,一些非崩溃的行为我们并不能从中得知和处理,例如我刚刚讲过的华为的问题,这些问题并不能导致崩溃,但是会导致功能出现异常。 * 另外值得一提的是 [Android 11 对软件包可见性进行了限制](https://developer.android.google.cn/about/versions/11/privacy/package-visibility),所以这种跳包名的方式在未来将会完全不可行。 * 最终决定:这个功能的出发点是好的,但是我们没办法做好它,经过慎重考虑,决定将这个功能在 [9.2](https://github.com/getActivity/XXPermissions/releases/tag/9.2) 及之后的版本进行移除。 #### 为什么不用 ActivityResultContract 来申请权限 > [【issue】是否有考虑 onActivityResult 回调的权限申请切换成 ActivityResultContract](https://github.com/getActivity/XXPermissions/issues/103) * ActivityResultContract 是 Activity `1.2.0-alpha02` 和 Fragment `1.3.0-alpha02` 中新追加的新 API,有一定的使用门槛,必须要求项目是基于 AndroidX,并且 AndroidX 的版本还要是 `1.3.0-alpha01` 以上才可以,如果替换成 `ActivityResultContract` 来实现,那么就会导致一部分开发者用不了 **XXPermissions**,这是一个比较严重的问题,但实际上换成 ActivityResultContract 来实现本身没有带来任何的效益,例如我之前解决过的 Fragment 屏幕旋转及后台申请的问题,所以更换的意义又在哪里呢?有人可能会说官方已经将 onActivityResult 标记成过时,大家不必担心,之所以标记成过时只不过是谷歌为了推广新技术,但是可以明确说,官方是一定不会删掉这个 API 的,更准确来说是一定不敢,至于为什么?大家可以去看看 ActivityResultContract 是怎么实现的?它也是通过重写 Activity 的 `onActivityResult`、`onRequestPermissionsResult` 方法回调实现的,具体大家可以去看 `androidx.activity.ComponentActivity` 类中这两个方法的实现就会明白了,这里不再赘述。 #### 怎么处理权限请求成功但是返回空白通行证的问题 * 此问题无解,权限请求框架只能帮你申请权限,至于你申请权限做什么操作,框架无法知道,也无法干预,还有返回空白通行证是厂商自己的行为,目的就是为了保护用户的隐私,因为在某些应用上面不给权限就不能用,返回空白通行证是为了规避这种情况的发生。你要问我怎么办?我只能说胳膊拗不过大腿,别做一些无谓的抵抗。 #### 为什么授权了还是无法访问 Android/data 目录下的文件 * 首先无论你申请了哪种存储权限,在 Android 11 上面就是无法直接读取 Android/data 目录的,这个是 Android 11 上的新特性,需要你进行额外适配,具体适配流程可以参考这个开源项目 [https://github.com/getActivity/AndroidVersionAdapter](https://github.com/getActivity/AndroidVersionAdapter) #### 跳过安装权限申请然后直接安装 apk 会有什么问题吗 * 有细心的同学可能发现了,不需要安装权限也可以直接调起安装 apk,那我为什么还要申请 `REQUEST_INSTALL_PACKAGES` 权限?这不是脱裤子放屁,多此一举吗? * 在这里我想说的是,并不是你想象的那样,接下来让我们试验一下,这里选用了 `Google piexl 3XL(Android 12)` 和 `小米手机 12(Android 12)` 分别做一下测试 ```java Intent intent = new Intent(Intent.ACTION_VIEW); Uri uri; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { uri = FileProvider.getUriForFile(context, context.getPackageName() + ".provider", file); } else { uri = Uri.fromFile(file); } intent.setDataAndType(uri, "application/vnd.android.package-archive"); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); context.startActivity(intent); ``` ![](picture/zh/help_doc_install_package_android_1.jpg) ![](picture/zh/help_doc_install_package_android_2.jpg) ![](picture/zh/help_doc_install_package_miui_1.jpg) ![](picture/zh/help_doc_install_package_miui_2.jpg) * 看到这里,我相信大家已经发现了一些差异,同样是跳转到安装 apk 页面,在 Android 原生系统上面,会显示 `取消` 和 `设置` 的选项,点击 `取消` 的选项会取消安装,只有点击 `设置` 的选项,才会让你授予安装包权限,授予了才能进行安装,而在 MIUI 上面,会显示 `允许` 和 `禁止` 的选项,另外还有一个 `记住我的选择` 的选项,如果用户勾选了这个 `记住我的选择` 并且点击了 `禁止` 的选项,那么应用下次跳转到安装 apk 页面会被系统直接拒绝,并且只会显示一个 toast 提示,问题结论是:可以直接跳转到安装 apk 页面,但是不建议那么做,因为在有些手机上面,系统可能会直接拒绝这个安装 apk 的请求,针对这个问题,所以标准的写法应该是,先判断有没有安装权限,没有的话就申请,有的话再去跳转到安装 apk 的页面,当然你如果用的是本框架申请的安装权限,可以不需要判断有没有权限直接申请,有授权和没有授权都会通过回调告诉你,你再从回调中做处理。 #### 如何应对国内某些应用商店在明确拒绝权限后 48 小时内不允许再次申请的问题 * 首先这种属于业务逻辑的问题,框架本身是不会做这种事情的,但并非不能实现,这得益于框架良好的设计,框架内部提供了一个叫 OnPermissionInterceptor 的拦截器类,当前有权限申请的时候,会走 requestPermissions 方法的回调,你可以重写这个方法的逻辑,先去判断要申请的权限是否在 48 小时内已经申请过了一次了,如果没有的话,就走权限申请的流程,如果有的话,那么就直接回调权限申请失败的方法。 ```java public final class PermissionInterceptor implements OnPermissionInterceptor { private static final String SP_NAME_PERMISSION_REQUEST_TIME_RECORD = "permission_request_time_record"; @Override public void onRequestPermissionStart(@NonNull Activity activity, @NonNull List requestList, @NonNull PermissionFragmentFactory fragmentFactory, @NonNull OnPermissionDescription permissionDescription, @Nullable OnPermissionCallback callback) { SharedPreferences sharedPreferences = activity.getSharedPreferences(SP_NAME_PERMISSION_REQUEST_TIME_RECORD, Context.MODE_PRIVATE); String permissionKey = String.valueOf(requestList); long lastRequestPermissionTime = sharedPreferences.getLong(permissionKey, 0); if (System.currentTimeMillis() - lastRequestPermissionTime <= 1000 * 60 * 60 * 24 * 2) { List deniedList = XXPermissions.getDeniedPermissions(activity, requestList); List grantedList = new ArrayList<>(requestList); grantedList.removeAll(deniedList); onRequestPermissionEnd(activity, true, requestList, grantedList, deniedList, callback); return; } sharedPreferences.edit().putLong(permissionKey, System.currentTimeMillis()).apply(); // 如果之前没有申请过权限,或者距离上次申请已经超过了 48 个小时,则进行申请权限 dispatchPermissionRequest(activity, requestList, fragmentFactory, permissionDescription, callback); } } ``` ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, June 2018 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2018 Huang JinQun Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README-en.md ================================================ # [中文文档](README.md) # Permission request framework ![](logo.png) * project address: [Github](https://github.com/getActivity/XXPermissions) * [Click here to download demo apk directly](https://github.com/getActivity/XXPermissions/releases/download/28.0/XXPermissions.apk) ![](picture/en/demo_request_permission_activity.jpg) ![](picture/en/demo_request_single_permission.jpg) ![](picture/en/demo_request_group_permission.jpg) ![](picture/en/demo_request_system_alert_window_permission.jpg) ![](picture/en/demo_request_notification_service_permission.jpg) ![](picture/en/demo_request_notification_service_channel_permission.jpg) ![](picture/en/demo_request_full_screen_notifications_permission.jpg) ![](picture/en/demo_request_write_settings_permission.jpg) ![](picture/en/demo_request_manage_storage_permission.jpg) ![](picture/en/demo_request_usage_stats_permission.jpg) ![](picture/en/demo_request_schedule_exact_alarm_permission.jpg) ![](picture/en/demo_request_bind_notification_listener_permission.jpg) ![](picture/en/demo_request_access_notification_policy_permission.jpg) ![](picture/en/demo_request_ignore_battery_optimizations_permission.jpg) ![](picture/en/demo_request_bind_vpn_service_permission.jpg) ![](picture/en/demo_request_accessibility_service_permission.jpg) ![](picture/en/demo_request_device_admin_permission.jpg) ![](picture/en/demo_request_picture_in_picture_permission.jpg) ![](picture/en/demo_request_health_data_permission_1.jpg) ![](picture/en/demo_request_health_data_permission_2.jpg) #### Integration steps * If your project Gradle configuration is in `7.0` below, needs to be in `build.gradle` file added ```groovy allprojects { repositories { // JitPack remote repository:https://jitpack.io maven { url 'https://jitpack.io' } } } ``` * If your Gradle configuration is `7.0` or above, needs to be in `settings.gradle` file added ```groovy dependencyResolutionManagement { repositories { // JitPack remote repository:https://jitpack.io maven { url 'https://jitpack.io' } } } ``` * After configuring the remote warehouse, under the project app module `build.gradle` Add remote dependencies to the file ```groovy android { // Support JDK 1.8 compileOptions { targetCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8 } } dependencies { // Device compatibility framework:https://github.com/getActivity/DeviceCompat implementation 'com.github.getActivity:DeviceCompat:2.3' // Permission request framework:https://github.com/getActivity/XXPermissions implementation 'com.github.getActivity:XXPermissions:28.0' } ``` #### Support library compatible * Option 1: Use remote dependencies of the old version framework ```groovy dependencies { // Device compatibility framework:https://github.com/getActivity/DeviceCompat implementation 'com.github.getActivity:DeviceCompat:2.3' // Permission request framework:https://github.com/getActivity/XXPermissions implementation 'com.github.getActivity:XXPermissions:26.8' } ``` * Option 2: If your project is still in the Support phase and it's not convenient to migrate to **AndroidX** yet, but you want to use the latest version of the framework, you can use the [JetifierStandalone](https://developer.android.com/tools/jetifier#install) tool provided by **Google** to convert the **aar** packages from the released Release versions into **Support-compatible aar** packages using reverse mode. * You can choose either of the above two options, but it's still not recommended. These are only stopgap measures, not long-term solutions. Subsequent versions of the framework will no longer support **Support** projects. The best approach is to migrate your project to **AndroidX**. #### scoped storage * If the project has been adapted to the Android 10 scoped storage feature, please go to`AndroidManifest.xml`join in ```xml ``` * If the current project does not adapt to this feature, then this step can be ignored * It should be noted that this option is used by the framework to determine whether the current project is adapted to scoped storage. It should be noted that if your project has been adapted to the scoped storage feature, you can use`READ_EXTERNAL_STORAGE`、`WRITE_EXTERNAL_STORAGE`To apply for permission, if your project has not yet adapted to the partition feature, even if you apply`READ_EXTERNAL_STORAGE`、`WRITE_EXTERNAL_STORAGE`The permissions will also cause the files on the external storage to be unable to be read normally. If your project is not suitable for scoped storage, please use`MANAGE_EXTERNAL_STORAGE`To apply for permission, so that the files on the external storage can be read normally. If you want to know more about the features of Android 10 partition storage, you can[Click here to view and learn](https://github.com/getActivity/AndroidVersionAdapter#android-100). #### Frame obfuscation rules The framework has automatically added the framework's obfuscation rules for you internally. When you add the framework's dependent remote libraries, the framework's obfuscation rules will also be carried into your project. You don't need to add them manually yourself. Specific obfuscation rule content [Click here to view](library/proguard-permissions.pro) #### One code to get permission request has never been easier * Java code example ```java XXPermissions.with(this) // Request multiple permission .permission(PermissionLists.getRecordAudioPermission()) .permission(PermissionLists.getCameraPermission()) // Setting does not trigger error detection mechanism (local setting) //.unchecked() .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { // Determine whether the permissions that failed requests have been checked by the user to no longer ask boolean doNotAskAgain = XXPermissions.isDoNotAskAgainPermissions(activity, deniedList); // The logic for failing to handle permission requests here ...... return; } // The logic for handling permission requests here is successful ...... } }); ``` * Kotlin code example ```kotlin XXPermissions.with(this) // Request multiple permission .permission(PermissionLists.getRecordAudioPermission()) .permission(PermissionLists.getCameraPermission()) // Setting does not trigger error detection mechanism (local setting) //.unchecked() .request(object : OnPermissionCallback { override fun onResult(grantedList: MutableList, deniedList: MutableList) { val allGranted = deniedList.isEmpty() if (!allGranted) { // Determine whether the permissions that failed requests have been checked by the user to no longer ask val doNotAskAgain = XXPermissions.isDoNotAskAgainPermissions(activity, deniedList) // The logic for failing to handle permission requests here // ...... return } // The logic for handling permission requests here is successful // ...... } }) ``` #### Introduction to other APIs of the framework ```java // Check if a single permission is granted XXPermissions.isGrantedPermission(@NonNull Context context, @NonNull IPermission permission); XXPermissions.isGrantedPermissions(@NonNull Context context, @NonNull IPermission[] permissions); XXPermissions.isGrantedPermissions(@NonNull Context context, @NonNull List permissions); // Get the granted permissions from a permission list XXPermissions.getGrantedPermissions(@NonNull Context context, @NonNull IPermission[] permissions); XXPermissions.getGrantedPermissions(@NonNull Context context, @NonNull List permissions); // Get the denied permissions from a permission list XXPermissions.getDeniedPermissions(@NonNull Context context, @NonNull IPermission[] permissions); XXPermissions.getDeniedPermissions(@NonNull Context context, @NonNull List permissions); // Determine whether the two permissions are equal XXPermissions.equalsPermission(@NonNull IPermission permission, @NonNull IPermission permission2); XXPermissions.equalsPermission(@NonNull IPermission permission, @NonNull String permissionName); XXPermissions.equalsPermission(@NonNull String permissionName1, @NonNull String permissionName2); // Determine whether a certain permission is included in the permission list XXPermissions.containsPermission(@NonNull List permissions, @NonNull IPermission permission); XXPermissions.containsPermission(@NonNull List permissions, @NonNull String permissionName); // Check if a permission is a health permission XXPermissions.isHealthPermission(@NonNull IPermission permission); // Check if a permission has been denied with the "Never ask again" option selected // (Must be called within the permission request callback to be effective) XXPermissions.isDoNotAskAgainPermission(@NonNull Activity activity, @NonNull IPermission permission); XXPermissions.isDoNotAskAgainPermissions(@NonNull Activity activity, @NonNull IPermission[] permissions); XXPermissions.isDoNotAskAgainPermissions(@NonNull Activity activity, @NonNull List permissions); // Navigate to the permission settings page (Context version) XXPermissions.startPermissionActivity(@NonNull Context context); XXPermissions.startPermissionActivity(@NonNull Context context, @NonNull IPermission... permissions); XXPermissions.startPermissionActivity(@NonNull Context context, @NonNull List permissions); // Navigate to the permission settings page (Activity version) XXPermissions.startPermissionActivity(@NonNull Activity activity); XXPermissions.startPermissionActivity(@NonNull Activity activity, @NonNull IPermission... permissions); XXPermissions.startPermissionActivity(@NonNull Activity activity, @NonNull List permissions); XXPermissions.startPermissionActivity(@NonNull Activity activity, @NonNull List permissions, @IntRange(from = 1, to = 65535) int requestCode); XXPermissions.startPermissionActivity(@NonNull Activity activity, @NonNull IPermission permission, @Nullable OnPermissionCallback callback); XXPermissions.startPermissionActivity(@NonNull Activity activity, @NonNull List permissions, @Nullable OnPermissionCallback callback); // Navigate to the permission settings page (Android Fragment version) XXPermissions.startPermissionActivity(@NonNull Fragment fragment); XXPermissions.startPermissionActivity(@NonNull Fragment fragment, @NonNull IPermission... permissions); XXPermissions.startPermissionActivity(@NonNull Fragment fragment, @NonNull List permissions); XXPermissions.startPermissionActivity(@NonNull Fragment fragment, @NonNull List permissions, @IntRange(from = 1, to = 65535) int requestCode); XXPermissions.startPermissionActivity(@NonNull Fragment fragment, @NonNull IPermission permission, @Nullable OnPermissionCallback callback); XXPermissions.startPermissionActivity(@NonNull Fragment fragment, @NonNull List permissions, @Nullable OnPermissionCallback callback); // Navigate to the permission settings page (AndroidX Fragment version) XXPermissions.startPermissionActivity(@NonNull androidx.fragment.app.Fragment xFragment); XXPermissions.startPermissionActivity(@NonNull androidx.fragment.app.Fragment xFragment, @NonNull IPermission... permissions); XXPermissions.startPermissionActivity(@NonNull androidx.fragment.app.Fragment xFragment, @NonNull List permissions); XXPermissions.startPermissionActivity(@NonNull androidx.fragment.app.Fragment xFragment, @NonNull List permissions, @IntRange(from = 1, to = 65535) int requestCode); XXPermissions.startPermissionActivity(@NonNull androidx.fragment.app.Fragment xFragment, @NonNull IPermission permission, @Nullable OnPermissionCallback callback); XXPermissions.startPermissionActivity(@NonNull androidx.fragment.app.Fragment xFragment, @NonNull List permissions, @Nullable OnPermissionCallback callback); // Set the permission description provider (Global setting) XXPermissions.setPermissionDescription(Class clazz); // Set the permission request interceptor (Global setting) XXPermissions.setPermissionInterceptor(Class clazz); // Set whether to enable error detection mode (Global setting) XXPermissions.setCheckMode(boolean checkMode); ``` #### Comparison between similar permission request frameworks | Adaptation details | [XXPermissions](https://github.com/getActivity/XXPermissions) | [AndPermission](https://github.com/yanzhenjie/AndPermission) | [PermissionX](https://github.com/guolindev/PermissionX) | [AndroidUtilCode-PermissionUtils](https://github.com/Blankj/AndroidUtilCode) | [PermissionsDispatcher](https://github.com/permissions-dispatcher/PermissionsDispatcher) | [RxPermissions](https://github.com/tbruyelle/RxPermissions) | [EasyPermissions](https://github.com/googlesamples/easypermissions) | [Dexter](https://github.com/Karumi/Dexter) | |:------------------------------------------------------------:| :------------: | :------------: | :------------: | :------------: | :------------: | :------------: | :------------: | :------------: | | Corresponding version | 28.0 | 2.0.3 | 1.8.1 | 1.31.0 | 4.9.2 | 0.12 | 3.0.0 | 6.2.3 | | Number of issues | [![](https://img.shields.io/github/issues/getActivity/XXPermissions.svg)](https://github.com/getActivity/XXPermissions/issues) | [![](https://img.shields.io/github/issues/yanzhenjie/AndPermission.svg)](https://github.com/yanzhenjie/AndPermission/issues) | [![](https://img.shields.io/github/issues/guolindev/PermissionX.svg)](https://github.com/guolindev/PermissionX/issues) | [![](https://img.shields.io/github/issues/Blankj/AndroidUtilCode.svg)](https://github.com/Blankj/AndroidUtilCode/issues) | [![](https://img.shields.io/github/issues/permissions-dispatcher/PermissionsDispatcher.svg)](https://github.com/permissions-dispatcher/PermissionsDispatcher/issues) | [![](https://img.shields.io/github/issues/tbruyelle/RxPermissions.svg)](https://github.com/tbruyelle/RxPermissions/issues) | [![](https://img.shields.io/github/issues/googlesamples/easypermissions.svg)](https://github.com/googlesamples/easypermissions/issues) | [![](https://img.shields.io/github/issues/Karumi/Dexter.svg)](https://github.com/Karumi/Dexter/issues) | | Framework Maintenance Status |**In maintenance**| stop maintenance | stop maintenance | stop maintenance | stop maintenance | stop maintenance | stop maintenance | stop maintenance | | `SCHEDULE_EXACT_ALARM` | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | `MANAGE_EXTERNAL_STORAGE` | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | | `REQUEST_INSTALL_PACKAGES` | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | | `PICTURE_IN_PICTURE` | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | `SYSTEM_ALERT_WINDOW` | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | | `WRITE_SETTINGS` | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | | `NOTIFICATION_SERVICE` | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | `NOTIFICATION_SERVICE`(Channel) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | `BIND_NOTIFICATION_LISTENER_SERVICE` | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | `ACCESS_NOTIFICATION_POLICY` | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | `PACKAGE_USAGE_STATS` | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | `USE_FULL_SCREEN_INTENT` | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | `BIND_VPN_SERVICE` | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | `BIND_ACCESSIBILITY_SERVICE` | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | `BIND_DEVICE_ADMIN` | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | `MANAGE_MEDIA` | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | Intent Extreme Jump Fallback Mechanism | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | Compatibility with Permission Request API Crash Issues | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | Avoiding System Permission Callback Null Pointer Issues | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | Automatic Permission Split Requests | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | Framework Completely Separates UI Layer | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | Core Logic and Specific Permissions Completely Decoupled | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | | Automatic Background Permission Adaptation | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | Support for Cross-Platform Environment Calls | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | | Callback Lifecycle Synchronized with Host | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | Support for Custom Permission Requests | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | New Version Permissions Support Backward Compatibility | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | Screen Rotation Scenario Adaptation | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | | Background Permission Request Scenario Adaptation | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | Fix Android 12 Memory Leak Issue | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | Support for Code Error Detection | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | #### [For specific implementation details, please click here to view](Details-en.md) #### [For other frequently asked questions, please click here](HelpDoc-en.md) #### Framework highlights * Take the lead: the first permission request framework adapted to Android 16 * Concise and easy to use: using the method of chain call, only one line of code is needed to use * Comprehensive support: the first and only permission request framework that adapts to all Android versions * Overcoming technical difficulties: the first framework to solve system memory leaks in Android 12 for permission applications * Adapt to extreme situations: No matter how extreme and harsh the environment is to apply for permissions, the framework is still strong * Downward Compatibility: New permissions can be applied normally in the old system, and the framework will automatically adapt without the caller's adaptation * Automatic error detection: If an error occurs, the framework will actively throw an exception to the caller (only judged under Debug, and kill the bug in the cradle) #### Author's other open source projects * Android middle office: [AndroidProject](https://github.com/getActivity/AndroidProject)![](https://img.shields.io/github/stars/getActivity/AndroidProject.svg)![](https://img.shields.io/github/forks/getActivity/AndroidProject.svg) * Android middle office kt version: [AndroidProject-Kotlin](https://github.com/getActivity/AndroidProject-Kotlin)![](https://img.shields.io/github/stars/getActivity/AndroidProject-Kotlin.svg)![](https://img.shields.io/github/forks/getActivity/AndroidProject-Kotlin.svg) * Toast framework: [Toaster](https://github.com/getActivity/Toaster)![](https://img.shields.io/github/stars/getActivity/Toaster.svg)![](https://img.shields.io/github/forks/getActivity/Toaster.svg) * Network framework: [EasyHttp](https://github.com/getActivity/EasyHttp)![](https://img.shields.io/github/stars/getActivity/EasyHttp.svg)![](https://img.shields.io/github/forks/getActivity/EasyHttp.svg) * Title bar framework: [TitleBar](https://github.com/getActivity/TitleBar)![](https://img.shields.io/github/stars/getActivity/TitleBar.svg)![](https://img.shields.io/github/forks/getActivity/TitleBar.svg) * Floating window framework: [EasyWindow](https://github.com/getActivity/EasyWindow)![](https://img.shields.io/github/stars/getActivity/EasyWindow.svg)![](https://img.shields.io/github/forks/getActivity/EasyWindow.svg) * Device compatibility framework:[DeviceCompat](https://github.com/getActivity/DeviceCompat) ![](https://img.shields.io/github/stars/getActivity/DeviceCompat.svg) ![](https://img.shields.io/github/forks/getActivity/DeviceCompat.svg) * Shape view framework: [ShapeView](https://github.com/getActivity/ShapeView)![](https://img.shields.io/github/stars/getActivity/ShapeView.svg)![](https://img.shields.io/github/forks/getActivity/ShapeView.svg) * Shape drawable framework: [ShapeDrawable](https://github.com/getActivity/ShapeDrawable)![](https://img.shields.io/github/stars/getActivity/ShapeDrawable.svg)![](https://img.shields.io/github/forks/getActivity/ShapeDrawable.svg) * Language switching framework: [Multi Languages](https://github.com/getActivity/MultiLanguages)![](https://img.shields.io/github/stars/getActivity/MultiLanguages.svg)![](https://img.shields.io/github/forks/getActivity/MultiLanguages.svg) * Gson parsing fault tolerance: [GsonFactory](https://github.com/getActivity/GsonFactory)![](https://img.shields.io/github/stars/getActivity/GsonFactory.svg)![](https://img.shields.io/github/forks/getActivity/GsonFactory.svg) * Logcat viewing framework: [Logcat](https://github.com/getActivity/Logcat)![](https://img.shields.io/github/stars/getActivity/Logcat.svg)![](https://img.shields.io/github/forks/getActivity/Logcat.svg) * Nested scrolling layout framework:[NestedScrollLayout](https://github.com/getActivity/NestedScrollLayout) ![](https://img.shields.io/github/stars/getActivity/NestedScrollLayout.svg) ![](https://img.shields.io/github/forks/getActivity/NestedScrollLayout.svg) * Android version guide: [AndroidVersionAdapter](https://github.com/getActivity/AndroidVersionAdapter)![](https://img.shields.io/github/stars/getActivity/AndroidVersionAdapter.svg)![](https://img.shields.io/github/forks/getActivity/AndroidVersionAdapter.svg) * Android code standard: [AndroidCodeStandard](https://github.com/getActivity/AndroidCodeStandard)![](https://img.shields.io/github/stars/getActivity/AndroidCodeStandard.svg)![](https://img.shields.io/github/forks/getActivity/AndroidCodeStandard.svg) * Android resource summary:[AndroidIndex](https://github.com/getActivity/AndroidIndex) ![](https://img.shields.io/github/stars/getActivity/AndroidIndex.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidIndex.svg) * Android open source leaderboard: [AndroidGithubBoss](https://github.com/getActivity/AndroidGithubBoss)![](https://img.shields.io/github/stars/getActivity/AndroidGithubBoss.svg)![](https://img.shields.io/github/forks/getActivity/AndroidGithubBoss.svg) * Studio boutique plugins: [StudioPlugins](https://github.com/getActivity/StudioPlugins)![](https://img.shields.io/github/stars/getActivity/StudioPlugins.svg)![](https://img.shields.io/github/forks/getActivity/StudioPlugins.svg) * Emoji collection: [EmojiPackage](https://github.com/getActivity/EmojiPackage)![](https://img.shields.io/github/stars/getActivity/EmojiPackage.svg)![](https://img.shields.io/github/forks/getActivity/EmojiPackage.svg) * China provinces json: [ProvinceJson](https://github.com/getActivity/ProvinceJson)![](https://img.shields.io/github/stars/getActivity/ProvinceJson.svg)![](https://img.shields.io/github/forks/getActivity/ProvinceJson.svg) * Markdown documentation:[MarkdownDoc](https://github.com/getActivity/MarkdownDoc) ![](https://img.shields.io/github/stars/getActivity/MarkdownDoc.svg) ![](https://img.shields.io/github/forks/getActivity/MarkdownDoc.svg) ## License ```text Copyright 2018 Huang JinQun Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ``` ================================================ FILE: README.md ================================================ # [English Doc](README-en.md) # 权限请求框架 ![](logo.png) * 项目地址:[Github](https://github.com/getActivity/XXPermissions) * 博文地址:[月下载 40 万次的框架是怎么练成的?](https://juejin.cn/post/7547408384585629711) * 可以扫码下载 Demo 进行演示或者测试,如果扫码下载不了的,[点击此处可直接下载](https://github.com/getActivity/XXPermissions/releases/download/28.0/XXPermissions.apk) ![](picture/zh/download_demo_apk_qr_code.png) ![](picture/zh/demo_request_permission_activity.jpg) ![](picture/zh/demo_request_single_permission.jpg) ![](picture/zh/demo_request_group_permission.jpg) ![](picture/zh/demo_request_system_alert_window_permission.jpg) ![](picture/zh/demo_request_notification_service_permission.jpg) ![](picture/zh/demo_request_notification_service_channel_permission.jpg) ![](picture/zh/demo_request_full_screen_notifications_permission.jpg) ![](picture/zh/demo_request_write_settings_permission.jpg) ![](picture/zh/demo_request_manage_storage_permission.jpg) ![](picture/zh/demo_request_usage_stats_permission.jpg) ![](picture/zh/demo_request_schedule_exact_alarm_permission.jpg) ![](picture/zh/demo_request_bind_notification_listener_permission.jpg) ![](picture/zh/demo_request_access_notification_policy_permission.jpg) ![](picture/zh/demo_request_ignore_battery_optimizations_permission.jpg) ![](picture/zh/demo_request_bind_vpn_service_permission.jpg) ![](picture/zh/demo_request_picture_in_picture_permission.jpg) ![](picture/zh/demo_request_accessibility_service_permission.jpg) ![](picture/zh/demo_request_device_admin_permission.jpg) ![](picture/zh/demo_request_get_installed_apps_permission.jpg) ![](picture/zh/demo_request_health_data_permission_1.jpg) ![](picture/zh/demo_request_health_data_permission_2.jpg) #### 集成步骤 * 如果你的项目 Gradle 配置是在 `7.0` 以下,需要在 `build.gradle` 文件中加入 ```groovy allprojects { repositories { // JitPack 远程仓库:https://jitpack.io maven { url 'https://jitpack.io' } } } ``` * 如果你的 Gradle 配置是 `7.0` 及以上,则需要在 `settings.gradle` 文件中加入 ```groovy dependencyResolutionManagement { repositories { // JitPack 远程仓库:https://jitpack.io maven { url 'https://jitpack.io' } } } ``` * 配置完远程仓库后,在项目 app 模块下的 `build.gradle` 文件中加入远程依赖 ```groovy android { // 支持 JDK 1.8 及以上 compileOptions { targetCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8 } } dependencies { // 设备兼容框架:https://github.com/getActivity/DeviceCompat implementation 'com.github.getActivity:DeviceCompat:2.3' // 权限请求框架:https://github.com/getActivity/XXPermissions implementation 'com.github.getActivity:XXPermissions:28.0' } ``` #### Support 库兼容 * 方案一:沿用旧版本框架的远程依赖 ``` dependencies { // 设备兼容框架:https://github.com/getActivity/DeviceCompat implementation 'com.github.getActivity:DeviceCompat:2.3' // 权限请求框架:https://github.com/getActivity/XXPermissions implementation 'com.github.getActivity:XXPermissions:26.8' } ``` * 方案二:如果你的项目仍处于 Support 阶段,目前不方便转到 **AndroidX** 中来,但又想用最新版本的框架,可以使用 **Google** 提供的 [JetifierStandalone](https://developer.android.google.cn/tools/jetifier?hl=zh-cn#install) 工具将已发布版本 [Release](https://github.com/getActivity/XXPermissions/releases) 中的 **aar** 包通过反向模式转成 **Support** 版本的 **aar** 包来使用。 * 上述两种方案任选其一即可,但是仍旧不推荐你那样做,因为这些只是权宜之计,并非长久之计,框架后续的版本已不再支持 **Support** 项目,最好的方案是将项目迁移到 **AndroidX**。 * 将项目从 **Support** 迁移 **AndroidX** 相关的教程:[AndroidX 踩坑指南](https://juejin.cn/post/7053773917495754782) #### 分区存储 * 如果项目已经适配了 Android 10 分区存储特性,请在 `AndroidManifest.xml` 中加入 ```xml ``` * 如果当前项目没有适配这特性,那么这一步骤可以忽略 * 需要注意的是:这个选项是框架用于判断当前项目是否适配了分区存储,需要注意的是,如果你的项目已经适配了分区存储特性,可以使用 `READ_EXTERNAL_STORAGE`、`WRITE_EXTERNAL_STORAGE` 来申请权限,如果你的项目还没有适配分区特性,就算申请了 `READ_EXTERNAL_STORAGE`、`WRITE_EXTERNAL_STORAGE` 权限也会导致无法正常读取外部存储上面的文件,如果你的项目没有适配分区存储,请使用 `MANAGE_EXTERNAL_STORAGE` 来申请权限,这样才能正常读取外部存储上面的文件,你如果想了解更多关于 Android 10 分区存储的特性,可以[点击此处查看和学习](https://github.com/getActivity/AndroidVersionAdapter#android-100)。 #### 框架混淆规则 * 框架已经在内部自动帮你添加了框架的混淆规则,在你添加框架的依赖远程库的时候,框架的混淆规则也会一同携带到你的项目中,你无需自己手动添加,具体的混淆规则内容 [可点击此处查看](library/proguard-permissions.pro) #### 一句代码搞定权限请求,从未如此简单 * Java 用法示例 ```java XXPermissions.with(this) // 申请多个权限 .permission(PermissionLists.getRecordAudioPermission()) .permission(PermissionLists.getCameraPermission()) // 设置不触发错误检测机制(局部设置) //.unchecked() .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { // 判断请求失败的权限是否被用户勾选了不再询问的选项 boolean doNotAskAgain = XXPermissions.isDoNotAskAgainPermissions(activity, deniedList); // 在这里处理权限请求失败的逻辑 ...... return; } // 在这里处理权限请求成功的逻辑 ...... } }); ``` * Kotlin 用法示例 ```kotlin XXPermissions.with(this) // 申请多个权限 .permission(PermissionLists.getRecordAudioPermission()) .permission(PermissionLists.getCameraPermission()) // 设置不触发错误检测机制(局部设置) //.unchecked() .request(object : OnPermissionCallback { override fun onResult(grantedList: MutableList, deniedList: MutableList) { val allGranted = deniedList.isEmpty() if (!allGranted) { // 判断请求失败的权限是否被用户勾选了不再询问的选项 val doNotAskAgain = XXPermissions.isDoNotAskAgainPermissions(activity, deniedList) // 在这里处理权限请求失败的逻辑 // ...... return } // 在这里处理权限请求成功的逻辑 // ...... } }) ``` #### 框架其他 API 介绍 ```java // 判断一个或多个权限是否全部授予了 XXPermissions.isGrantedPermission(@NonNull Context context, @NonNull IPermission permission); XXPermissions.isGrantedPermissions(@NonNull Context context, @NonNull IPermission[] permissions); XXPermissions.isGrantedPermissions(@NonNull Context context, @NonNull List permissions); // 从权限列表中获取已授予的权限 XXPermissions.getGrantedPermissions(@NonNull Context context, @NonNull IPermission[] permissions); XXPermissions.getGrantedPermissions(@NonNull Context context, @NonNull List permissions); // 从权限列表中获取没有授予的权限 XXPermissions.getDeniedPermissions(@NonNull Context context, @NonNull IPermission[] permissions); XXPermissions.getDeniedPermissions(@NonNull Context context, @NonNull List permissions); // 判断两个权限是否相等 XXPermissions.equalsPermission(@NonNull IPermission permission, @NonNull IPermission permission2); XXPermissions.equalsPermission(@NonNull IPermission permission, @NonNull String permissionName); XXPermissions.equalsPermission(@NonNull String permissionName1, @NonNull String permissionName2); // 判断权限列表中是否包含某个权限 XXPermissions.containsPermission(@NonNull List permissions, @NonNull IPermission permission); XXPermissions.containsPermission(@NonNull List permissions, @NonNull String permissionName); // 判断某个权限是否为健康权限 XXPermissions.isHealthPermission(@NonNull IPermission permission); // 判断一个或多个权限是否被勾选了《不再询问》的选项(一定要在权限申请的回调方法中调用才有效果) XXPermissions.isDoNotAskAgainPermission(@NonNull Activity activity, @NonNull IPermission permission); XXPermissions.isDoNotAskAgainPermissions(@NonNull Activity activity, @NonNull IPermission[] permissions); XXPermissions.isDoNotAskAgainPermissions(@NonNull Activity activity, @NonNull List permissions); // 跳转到权限设置页(Context 版本) XXPermissions.startPermissionActivity(@NonNull Context context); XXPermissions.startPermissionActivity(@NonNull Context context, @NonNull IPermission... permissions); XXPermissions.startPermissionActivity(@NonNull Context context, @NonNull List permissions); // 跳转到权限设置页(Activity 版本) XXPermissions.startPermissionActivity(@NonNull Activity activity); XXPermissions.startPermissionActivity(@NonNull Activity activity, @NonNull IPermission... permissions); XXPermissions.startPermissionActivity(@NonNull Activity activity, @NonNull List permissions); XXPermissions.startPermissionActivity(@NonNull Activity activity, @NonNull List permissions, @IntRange(from = 1, to = 65535) int requestCode); XXPermissions.startPermissionActivity(@NonNull Activity activity, @NonNull IPermission permission, @Nullable OnPermissionCallback callback); XXPermissions.startPermissionActivity(@NonNull Activity activity, @NonNull List permissions, @Nullable OnPermissionCallback callback); // 跳转到权限设置页(Android Fragment 版本) XXPermissions.startPermissionActivity(@NonNull Fragment fragment); XXPermissions.startPermissionActivity(@NonNull Fragment fragment, @NonNull IPermission... permissions); XXPermissions.startPermissionActivity(@NonNull Fragment fragment, @NonNull List permissions); XXPermissions.startPermissionActivity(@NonNull Fragment fragment, @NonNull List permissions, @IntRange(from = 1, to = 65535) int requestCode); XXPermissions.startPermissionActivity(@NonNull Fragment fragment, @NonNull IPermission permission, @Nullable OnPermissionCallback callback); XXPermissions.startPermissionActivity(@NonNull Fragment fragment, @NonNull List permissions, @Nullable OnPermissionCallback callback); // 跳转到权限设置页(AndroidX Fragment 版本) XXPermissions.startPermissionActivity(@NonNull androidx.fragment.app.Fragment xFragment); XXPermissions.startPermissionActivity(@NonNull androidx.fragment.app.Fragment xFragment, @NonNull IPermission... permissions); XXPermissions.startPermissionActivity(@NonNull androidx.fragment.app.Fragment xFragment, @NonNull List permissions); XXPermissions.startPermissionActivity(@NonNull androidx.fragment.app.Fragment xFragment, @NonNull List permissions, @IntRange(from = 1, to = 65535) int requestCode); XXPermissions.startPermissionActivity(@NonNull androidx.fragment.app.Fragment xFragment, @NonNull IPermission permission, @Nullable OnPermissionCallback callback); XXPermissions.startPermissionActivity(@NonNull androidx.fragment.app.Fragment xFragment, @NonNull List permissions, @Nullable OnPermissionCallback callback); // 设置权限描述器(全局设置) XXPermissions.setPermissionDescription(Class clazz); // 设置权限申请拦截器(全局设置) XXPermissions.setPermissionInterceptor(Class clazz); // 设置是否开启错误检测模式(全局设置) XXPermissions.setCheckMode(boolean checkMode); ``` #### 同类权限请求框架之间的对比 | 适配细节 | [XXPermissions](https://github.com/getActivity/XXPermissions) | [AndPermission](https://github.com/yanzhenjie/AndPermission) | [PermissionX](https://github.com/guolindev/PermissionX) | [AndroidUtilCode-PermissionUtils](https://github.com/Blankj/AndroidUtilCode) | [PermissionsDispatcher](https://github.com/permissions-dispatcher/PermissionsDispatcher) | [RxPermissions](https://github.com/tbruyelle/RxPermissions) | [EasyPermissions](https://github.com/googlesamples/easypermissions) | [Dexter](https://github.com/Karumi/Dexter) | |:--------------------:| :------------: | :------------: | :------------: | :------------: | :------------: | :------------: | :------------: | :------------: | | 对应版本 | 28.0 | 2.0.3 | 1.8.1 | 1.31.0 | 4.9.2 | 0.12 | 3.0.0 | 6.2.3 | | issues 数 | [![](https://img.shields.io/github/issues/getActivity/XXPermissions.svg)](https://github.com/getActivity/XXPermissions/issues) | [![](https://img.shields.io/github/issues/yanzhenjie/AndPermission.svg)](https://github.com/yanzhenjie/AndPermission/issues) | [![](https://img.shields.io/github/issues/guolindev/PermissionX.svg)](https://github.com/guolindev/PermissionX/issues) | [![](https://img.shields.io/github/issues/Blankj/AndroidUtilCode.svg)](https://github.com/Blankj/AndroidUtilCode/issues) | [![](https://img.shields.io/github/issues/permissions-dispatcher/PermissionsDispatcher.svg)](https://github.com/permissions-dispatcher/PermissionsDispatcher/issues) | [![](https://img.shields.io/github/issues/tbruyelle/RxPermissions.svg)](https://github.com/tbruyelle/RxPermissions/issues) | [![](https://img.shields.io/github/issues/googlesamples/easypermissions.svg)](https://github.com/googlesamples/easypermissions/issues) | [![](https://img.shields.io/github/issues/Karumi/Dexter.svg)](https://github.com/Karumi/Dexter/issues) | | 框架维护状态 |**维护中**| 停止维护 | 停止维护 | 停止维护 | 停止维护 | 停止维护 | 停止维护 | 停止维护 | | 读取应用列表权限 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | 闹钟提醒权限 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | 所有文件管理权限 | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | | 安装包权限 | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | | 画中画权限 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | 悬浮窗权限 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | | 系统设置权限 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | | 通知栏权限 | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | 通知栏渠道权限 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | 通知栏监听权限 | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | 勿扰权限 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | 忽略电池优化权限 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | 查看应用使用情况权限 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | 全屏通知权限 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | VPN 权限 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | 无障碍权限 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | 设备管理器权限 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | 管理媒体权限 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | Intent 跳转极限兜底机制 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | 兼容请求权限 API 崩溃问题 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | 规避系统权限回调空指针问题 | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | 应用商店权限合规处理 | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | | 自动拆分权限进行请求 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | 框架内部完全剥离 UI 层 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | 核心逻辑和具体权限完全解耦 | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | | 自动适配后台权限 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | 支持在跨平台环境中调用 | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | | 回调生命周期与宿主保持同步 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | 支持自定义权限申请 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | 支持读取应用列表权限 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | 新版本权限支持向下兼容 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | 屏幕旋转场景适配 | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | | 后台申请权限场景适配 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | 修复 Android 12 内存泄漏问题 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | 第三方厂商兼容性优化 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | 支持检测代码错误 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | #### [具体实现细节请点击这里查看](Details-zh.md) #### [其他常见疑问请点击此处查看](HelpDoc-zh.md) #### 框架亮点 * 一马当先:首款适配 Android 16 的权限请求框架 * 简洁易用:采用链式调用的方式,使用只需一句代码 * 支持全面:首款也是唯一一款适配所有 Android 版本的权限请求框架 * 技术难题攻坚:首款解决权限申请在 Android 12 出现系统内存泄漏的框架 * 适配极端情况:无论在多么极端恶劣的环境下申请权限,框架依然坚挺 * 向下兼容属性:新权限在旧系统可以正常申请,框架会做自动适配,无需调用者适配 * 自动检测错误:如果出现错误框架会主动抛出异常给调用者(仅在 Debug 下判断,把 Bug 扼杀在摇篮中) #### 作者的其他开源项目 * 安卓技术中台:[AndroidProject](https://github.com/getActivity/AndroidProject) ![](https://img.shields.io/github/stars/getActivity/AndroidProject.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidProject.svg) * 安卓技术中台 Kt 版:[AndroidProject-Kotlin](https://github.com/getActivity/AndroidProject-Kotlin) ![](https://img.shields.io/github/stars/getActivity/AndroidProject-Kotlin.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidProject-Kotlin.svg) * 吐司框架:[Toaster](https://github.com/getActivity/Toaster) ![](https://img.shields.io/github/stars/getActivity/Toaster.svg) ![](https://img.shields.io/github/forks/getActivity/Toaster.svg) * 网络框架:[EasyHttp](https://github.com/getActivity/EasyHttp) ![](https://img.shields.io/github/stars/getActivity/EasyHttp.svg) ![](https://img.shields.io/github/forks/getActivity/EasyHttp.svg) * 标题栏框架:[TitleBar](https://github.com/getActivity/TitleBar) ![](https://img.shields.io/github/stars/getActivity/TitleBar.svg) ![](https://img.shields.io/github/forks/getActivity/TitleBar.svg) * 悬浮窗框架:[EasyWindow](https://github.com/getActivity/EasyWindow) ![](https://img.shields.io/github/stars/getActivity/EasyWindow.svg) ![](https://img.shields.io/github/forks/getActivity/EasyWindow.svg) * 设备兼容框架:[DeviceCompat](https://github.com/getActivity/DeviceCompat) ![](https://img.shields.io/github/stars/getActivity/DeviceCompat.svg) ![](https://img.shields.io/github/forks/getActivity/DeviceCompat.svg) * ShapeView 框架:[ShapeView](https://github.com/getActivity/ShapeView) ![](https://img.shields.io/github/stars/getActivity/ShapeView.svg) ![](https://img.shields.io/github/forks/getActivity/ShapeView.svg) * ShapeDrawable 框架:[ShapeDrawable](https://github.com/getActivity/ShapeDrawable) ![](https://img.shields.io/github/stars/getActivity/ShapeDrawable.svg) ![](https://img.shields.io/github/forks/getActivity/ShapeDrawable.svg) * 语种切换框架:[MultiLanguages](https://github.com/getActivity/MultiLanguages) ![](https://img.shields.io/github/stars/getActivity/MultiLanguages.svg) ![](https://img.shields.io/github/forks/getActivity/MultiLanguages.svg) * Gson 解析容错:[GsonFactory](https://github.com/getActivity/GsonFactory) ![](https://img.shields.io/github/stars/getActivity/GsonFactory.svg) ![](https://img.shields.io/github/forks/getActivity/GsonFactory.svg) * 日志查看框架:[Logcat](https://github.com/getActivity/Logcat) ![](https://img.shields.io/github/stars/getActivity/Logcat.svg) ![](https://img.shields.io/github/forks/getActivity/Logcat.svg) * 嵌套滚动布局框架:[NestedScrollLayout](https://github.com/getActivity/NestedScrollLayout) ![](https://img.shields.io/github/stars/getActivity/NestedScrollLayout.svg) ![](https://img.shields.io/github/forks/getActivity/NestedScrollLayout.svg) * Android 版本适配:[AndroidVersionAdapter](https://github.com/getActivity/AndroidVersionAdapter) ![](https://img.shields.io/github/stars/getActivity/AndroidVersionAdapter.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidVersionAdapter.svg) * Android 代码规范:[AndroidCodeStandard](https://github.com/getActivity/AndroidCodeStandard) ![](https://img.shields.io/github/stars/getActivity/AndroidCodeStandard.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidCodeStandard.svg) * Android 资源大汇总:[AndroidIndex](https://github.com/getActivity/AndroidIndex) ![](https://img.shields.io/github/stars/getActivity/AndroidIndex.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidIndex.svg) * Android 开源排行榜:[AndroidGithubBoss](https://github.com/getActivity/AndroidGithubBoss) ![](https://img.shields.io/github/stars/getActivity/AndroidGithubBoss.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidGithubBoss.svg) * Studio 精品插件:[StudioPlugins](https://github.com/getActivity/StudioPlugins) ![](https://img.shields.io/github/stars/getActivity/StudioPlugins.svg) ![](https://img.shields.io/github/forks/getActivity/StudioPlugins.svg) * 表情包大集合:[EmojiPackage](https://github.com/getActivity/EmojiPackage) ![](https://img.shields.io/github/stars/getActivity/EmojiPackage.svg) ![](https://img.shields.io/github/forks/getActivity/EmojiPackage.svg) * AI 资源大汇总:[AiIndex](https://github.com/getActivity/AiIndex) ![](https://img.shields.io/github/stars/getActivity/AiIndex.svg) ![](https://img.shields.io/github/forks/getActivity/AiIndex.svg) * 省市区 Json 数据:[ProvinceJson](https://github.com/getActivity/ProvinceJson) ![](https://img.shields.io/github/stars/getActivity/ProvinceJson.svg) ![](https://img.shields.io/github/forks/getActivity/ProvinceJson.svg) * Markdown 语法文档:[MarkdownDoc](https://github.com/getActivity/MarkdownDoc) ![](https://img.shields.io/github/stars/getActivity/MarkdownDoc.svg) ![](https://img.shields.io/github/forks/getActivity/MarkdownDoc.svg) #### 微信公众号:Android轮子哥 ![](https://raw.githubusercontent.com/getActivity/Donate/master/picture/official_ccount.png) #### Android 技术 Q 群:10047167 #### 如果您觉得我的开源库帮你节省了大量的开发时间,请扫描下方的二维码随意打赏,要是能打赏个 10.24 :monkey_face:就太:thumbsup:了。您的支持将鼓励我继续创作:octocat:([点击查看捐赠列表](https://github.com/getActivity/Donate)) ![](https://raw.githubusercontent.com/getActivity/Donate/master/picture/pay_ali.png) ![](https://raw.githubusercontent.com/getActivity/Donate/master/picture/pay_wechat.png) ## License ```text Copyright 2018 Huang JinQun Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ``` ================================================ FILE: app/build.gradle ================================================ apply plugin: 'com.android.application' apply from : '../common.gradle' android { namespace 'com.hjq.permissions.demo' defaultConfig { applicationId "com.hjq.permissions.demo" // 最低安装版本 minSdkVersion 18 } // Apk 签名的那些事:https://www.jianshu.com/p/a1f8e5896aa2 signingConfigs { config { storeFile file(StoreFile) storePassword StorePassword keyAlias KeyAlias keyPassword KeyPassword } } buildTypes { debug { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' signingConfig signingConfigs.config } release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' signingConfig signingConfigs.config } } applicationVariants.configureEach { variant -> // apk 输出文件名配置 variant.outputs.configureEach { output -> outputFileName = rootProject.getName() + '.apk' } } } dependencies { // 依赖 libs 目录下所有的 jar 和 aar 包 implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs') implementation project(':library') // 谷歌兼容库:https://developer.android.google.cn/jetpack/androidx/releases/appcompat?hl=zh-cn // noinspection GradleCompatible implementation 'androidx.appcompat:appcompat:1.0.0' // 设备兼容框架:https://github.com/getActivity/DeviceCompat implementation 'com.github.getActivity:DeviceCompat:2.3' // 吐司框架:https://github.com/getActivity/Toaster implementation 'com.github.getActivity:Toaster:13.8' // 标题栏框架:https://github.com/getActivity/TitleBar implementation 'com.github.getActivity:TitleBar:10.8' // 内存泄漏检测:https://github.com/square/leakcanary debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12' } ================================================ FILE: app/gradle.properties ================================================ StoreFile = AppSignature.jks StorePassword = AndroidProject KeyAlias = AndroidProject KeyPassword = AndroidProject ================================================ FILE: app/proguard-rules.pro ================================================ ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/com/hjq/permissions/demo/AppApplication.java ================================================ package com.hjq.permissions.demo; import android.app.Application; import com.hjq.toast.Toaster; import com.hjq.toast.style.WhiteToastStyle; /** * author : Android 轮子哥 * github : https://github.com/getActivity/XXPermissions * time : 2021/01/04 * desc : 应用入口 */ public final class AppApplication extends Application { @Override public void onCreate() { super.onCreate(); // 初始化吐司工具类 Toaster.init(this, new WhiteToastStyle()); } } ================================================ FILE: app/src/main/java/com/hjq/permissions/demo/HealthDataPrivacyPolicyActivity.java ================================================ package com.hjq.permissions.demo; import android.graphics.Insets; import android.os.Build; import android.os.Bundle; import android.view.View; import android.view.View.OnApplyWindowInsetsListener; import android.view.WindowInsets; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import com.hjq.bar.OnTitleBarListener; import com.hjq.bar.TitleBar; /** * author : Android 轮子哥 * github : https://github.com/getActivity/XXPermissions * time : 2025/07/28 * desc : 健康数据隐私政策界面 */ public class HealthDataPrivacyPolicyActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.health_data_privacy_policy_activity); TitleBar titleBar = findViewById(R.id.tb_health_data_privacy_policy_bar); titleBar.setOnTitleBarListener(new OnTitleBarListener() { @Override public void onLeftClick(TitleBar titleBar) { finish(); } }); // 适配 Android 15 EdgeToEdge 特性 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { titleBar.setOnApplyWindowInsetsListener(new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsets onApplyWindowInsets(@NonNull View v, @NonNull WindowInsets insets) { Insets systemBars = insets.getInsets(WindowInsets.Type.systemBars()); // v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); v.setPadding(0, systemBars.top, 0, 0); return insets; } }); } } } ================================================ FILE: app/src/main/java/com/hjq/permissions/demo/MainActivity.java ================================================ package com.hjq.permissions.demo; import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.ComponentName; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Insets; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.health.connect.HealthConnectException; import android.health.connect.HealthConnectManager; import android.health.connect.ReadRecordsRequest; import android.health.connect.ReadRecordsRequestUsingFilters; import android.health.connect.ReadRecordsResponse; import android.health.connect.TimeInstantRangeFilter; import android.health.connect.TimeRangeFilter; import android.health.connect.datatypes.HeartRateRecord; import android.location.Address; import android.location.Geocoder; import android.media.ExifInterface; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.OutcomeReceiver; import android.provider.MediaStore; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.view.View.OnApplyWindowInsetsListener; import android.view.WindowInsets; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; import com.hjq.bar.OnTitleBarListener; import com.hjq.bar.TitleBar; import com.hjq.device.compat.DeviceBrand; import com.hjq.device.compat.DeviceOs; import com.hjq.permissions.OnPermissionCallback; import com.hjq.permissions.XXPermissions; import com.hjq.permissions.demo.example.ExampleAccessibilityService; import com.hjq.permissions.demo.example.ExampleDeviceAdminReceiver; import com.hjq.permissions.demo.example.ExampleNotificationListenerService; import com.hjq.permissions.demo.permission.PermissionConverter; import com.hjq.permissions.demo.permission.PermissionDescription; import com.hjq.permissions.demo.permission.PermissionInterceptor; import com.hjq.permissions.permission.PermissionLists; import com.hjq.permissions.permission.base.IPermission; import com.hjq.toast.Toaster; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.concurrent.Executors; /** * author : Android 轮子哥 * github : https://github.com/getActivity/XXPermissions * time : 2018/06/15 * desc : 权限申请演示 */ public final class MainActivity extends AppCompatActivity implements View.OnClickListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); TitleBar titleBar = findViewById(R.id.tb_main_bar); titleBar.setOnTitleBarListener(new OnTitleBarListener() { @Override public void onTitleClick(TitleBar titleBar) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse("https://github.com/getActivity/XXPermissions")); startActivity(intent); } }); // 适配 Android 15 EdgeToEdge 特性 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { titleBar.setOnApplyWindowInsetsListener(new OnApplyWindowInsetsListener() { @NonNull @Override public WindowInsets onApplyWindowInsets(@NonNull View v, @NonNull WindowInsets insets) { Insets systemBars = insets.getInsets(WindowInsets.Type.systemBars()); // v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); v.setPadding(0, systemBars.top, 0, 0); return insets; } }); } findViewById(R.id.btn_main_request_single_permission).setOnClickListener(this); findViewById(R.id.btn_main_request_group_permission).setOnClickListener(this); findViewById(R.id.btn_main_request_multiple_type_permission).setOnClickListener(this); findViewById(R.id.btn_main_request_location_permission).setOnClickListener(this); findViewById(R.id.btn_main_request_activity_recognition_permission).setOnClickListener(this); findViewById(R.id.btn_main_request_bluetooth_permission).setOnClickListener(this); findViewById(R.id.btn_main_request_wifi_devices_permission).setOnClickListener(this); findViewById(R.id.btn_main_request_read_media_location_information_permission).setOnClickListener(this); findViewById(R.id.btn_main_request_read_media_permission).setOnClickListener(this); findViewById(R.id.btn_main_request_health_permission).setOnClickListener(this); findViewById(R.id.btn_main_request_manage_storage_permission).setOnClickListener(this); findViewById(R.id.btn_main_request_install_packages_permission).setOnClickListener(this); findViewById(R.id.btn_main_request_system_alert_window_permission).setOnClickListener(this); findViewById(R.id.btn_main_request_write_settings_permission).setOnClickListener(this); findViewById(R.id.btn_main_request_notification_service_permission).setOnClickListener(this); findViewById(R.id.btn_main_request_post_notifications_permission).setOnClickListener(this); findViewById(R.id.btn_main_request_bind_notification_listener_permission).setOnClickListener(this); findViewById(R.id.btn_main_request_usage_stats_permission).setOnClickListener(this); findViewById(R.id.btn_main_request_schedule_exact_alarm_permission).setOnClickListener(this); findViewById(R.id.btn_main_request_access_notification_policy_permission).setOnClickListener(this); findViewById(R.id.btn_main_request_ignore_battery_optimizations_permission).setOnClickListener(this); findViewById(R.id.btn_main_request_picture_in_picture_permission).setOnClickListener(this); findViewById(R.id.btn_main_request_bind_vpn_service_permission).setOnClickListener(this); findViewById(R.id.btn_main_request_full_screen_notifications_permission).setOnClickListener(this); findViewById(R.id.btn_main_request_device_admin_permission).setOnClickListener(this); findViewById(R.id.btn_main_request_accessibility_service_permission).setOnClickListener(this); findViewById(R.id.btn_main_request_manage_media_permission).setOnClickListener(this); findViewById(R.id.btn_main_request_get_installed_apps_permission).setOnClickListener(this); findViewById(R.id.btn_main_start_permission_activity).setOnClickListener(this); if (!TextUtils.isEmpty(DeviceOs.getOsName())) { TextView deviceInfoView = findViewById(R.id.tv_main_device_info); deviceInfoView.setVisibility(View.VISIBLE); StringBuilder stringBuilder = new StringBuilder() .append("BrandName: " + DeviceBrand.getBrandName()) .append("\nOsName: " + DeviceOs.getOsName()) .append("\nOsVersionName: " + DeviceOs.getOsVersionName()) .append("\nAndroidVersion: Android " + Build.VERSION.RELEASE) .append("\nAndroidApiLevel: " + Build.VERSION.SDK_INT); deviceInfoView.setText(stringBuilder); } } @Override public void onClick(View view) { int viewId = view.getId(); if (viewId == R.id.btn_main_request_single_permission) { XXPermissions.with(this) .permission(PermissionLists.getCameraPermission()) .interceptor(new PermissionInterceptor()) .description(new PermissionDescription()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { return; } showGrantedPermissionsToast(grantedList); } }); } else if (viewId == R.id.btn_main_request_group_permission) { XXPermissions.with(this) .permission(PermissionLists.getRecordAudioPermission()) .permission(PermissionLists.getReadCalendarPermission()) .permission(PermissionLists.getWriteCalendarPermission()) .interceptor(new PermissionInterceptor()) .description(new PermissionDescription()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { return; } showGrantedPermissionsToast(grantedList); } }); } else if (viewId == R.id.btn_main_request_location_permission) { XXPermissions.with(this) .permission(PermissionLists.getAccessCoarseLocationPermission()) .permission(PermissionLists.getAccessFineLocationPermission()) // 如果不需要在后台使用定位功能,请不要申请此权限 .permission(PermissionLists.getAccessBackgroundLocationPermission()) .interceptor(new PermissionInterceptor()) .description(new PermissionDescription()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { return; } showGrantedPermissionsToast(grantedList); } }); } else if (viewId == R.id.btn_main_request_health_permission) { long delayMillis = 0; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { delayMillis = 2000; toast(getString(R.string.demo_android_14_health_permission_hint)); } view.postDelayed(new Runnable() { @Override public void run() { XXPermissions.with(MainActivity.this) .permission(PermissionLists.getReadSleepPermission()) .permission(PermissionLists.getReadActiveCaloriesBurnedPermission()) .permission(PermissionLists.getReadExercisePermission()) .permission(PermissionLists.getReadHeartRatePermission()) .permission(PermissionLists.getWriteHeartRatePermission()) .permission(PermissionLists.getReadHealthDataHistoryPermission()) .permission(PermissionLists.getReadHealthDataInBackgroundPermission()) .interceptor(new PermissionInterceptor()) .description(new PermissionDescription()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { return; } showGrantedPermissionsToast(grantedList); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { HealthConnectManager healthConnectManager = (HealthConnectManager) getSystemService(Context.HEALTHCONNECT_SERVICE); ZonedDateTime lastDay = ZonedDateTime.now() .truncatedTo(ChronoUnit.DAYS) .minusDays(1) .withHour(12); ZonedDateTime firstDay = lastDay.minusDays(7); TimeRangeFilter timeRangeFilter = new TimeInstantRangeFilter.Builder() .setStartTime(firstDay.toInstant()) .setEndTime(lastDay.toInstant()) .build(); ReadRecordsRequest readRecordsRequest = new ReadRecordsRequestUsingFilters.Builder<>( HeartRateRecord.class) .setTimeRangeFilter(timeRangeFilter) .setAscending(false) .build(); healthConnectManager.readRecords(readRecordsRequest, Executors.newSingleThreadExecutor(), new OutcomeReceiver, HealthConnectException>() { @Override public void onResult(ReadRecordsResponse result) { Log.i("XXPermissions", "获取到的健康数据数量为:" + result.getRecords().size()); } @Override public void onError(@NonNull HealthConnectException e) { Log.e("XXPermissions", "获取健康数据失败", e); } }); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE); Sensor heartRateSensor = sensorManager.getDefaultSensor(Sensor.TYPE_HEART_RATE); if (heartRateSensor != null) { Log.i("XXPermissions", "获取心率传感器成功"); } else { Log.i("XXPermissions", "获取心率传感器失败"); } } } }); } }, delayMillis); } else if (viewId == R.id.btn_main_request_activity_recognition_permission) { XXPermissions.with(this) .permission(PermissionLists.getActivityRecognitionPermission()) .interceptor(new PermissionInterceptor()) .description(new PermissionDescription()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { return; } showGrantedPermissionsToast(grantedList); addCountStepListener(); } }); } else if (viewId == R.id.btn_main_request_bluetooth_permission) { long delayMillis = 0; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { delayMillis = 2000; toast(getString(R.string.demo_android_12_bluetooth_permission_hint)); } view.postDelayed(new Runnable() { @Override public void run() { XXPermissions.with(MainActivity.this) .permission(PermissionLists.getBluetoothScanPermission()) .permission(PermissionLists.getBluetoothConnectPermission()) .permission(PermissionLists.getBluetoothAdvertisePermission()) .interceptor(new PermissionInterceptor()) .description(new PermissionDescription()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { return; } showGrantedPermissionsToast(grantedList); } }); } }, delayMillis); } else if (viewId == R.id.btn_main_request_wifi_devices_permission) { long delayMillis = 0; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { delayMillis = 2000; toast(getString(R.string.demo_android_13_wifi_permission_hint)); } view.postDelayed(new Runnable() { @Override public void run() { XXPermissions.with(MainActivity.this) .permission(PermissionLists.getNearbyWifiDevicesPermission()) .interceptor(new PermissionInterceptor()) .description(new PermissionDescription()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { return; } showGrantedPermissionsToast(grantedList); } }); } }, delayMillis); } else if (viewId == R.id.btn_main_request_read_media_location_information_permission) { long delayMillis = 0; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { delayMillis = 2000; toast(getString(R.string.demo_android_10_read_media_location_permission_hint)); } view.postDelayed(new Runnable() { @Override public void run() { XXPermissions.with(MainActivity.this) // 申请 ACCESS_MEDIA_LOCATION 的前提条件: // 1. 如果 targetSdk >= 33,有两种方案选择(二选一): // a. 申请 READ_MEDIA_IMAGES 或 READ_MEDIA_VIDEO 权限,需要注意的点是 // 如果是在 Android 14 申请,只能选择允许访问全部的照片和视频,不能选择部分 // b. 申请 MANAGE_EXTERNAL_STORAGE 权限 // 2. 如果 targetSdk < 33,,有两种方案选择(二选一): // a. 则添加 READ_EXTERNAL_STORAGE // b. MANAGE_EXTERNAL_STORAGE 二选一 .permission(PermissionLists.getReadMediaImagesPermission()) .permission(PermissionLists.getReadMediaVideoPermission()) .permission(PermissionLists.getAccessMediaLocationPermission()) .interceptor(new PermissionInterceptor()) .description(new PermissionDescription()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { return; } showGrantedPermissionsToast(grantedList); new Thread(new Runnable() { @Override public void run() { getAllImagesFromGallery(true); } }).start(); } }); } }, delayMillis); } else if (viewId == R.id.btn_main_request_read_media_permission) { long delayMillis = 0; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { delayMillis = 2000; toast(getString(R.string.demo_android_13_read_media_permission_hint)); } view.postDelayed(new Runnable() { @Override public void run() { XXPermissions.with(MainActivity.this) // 不适配分区存储应该这样写 //.permission(PermissionLists.getManageExternalStoragePermission()) // 适配分区存储应该这样写 .permission(PermissionLists.getReadMediaImagesPermission()) .permission(PermissionLists.getReadMediaVideoPermission()) .permission(PermissionLists.getReadMediaAudioPermission()) .permission(PermissionLists.getReadMediaVisualUserSelectedPermission()) .permission(PermissionLists.getWriteExternalStoragePermission()) .interceptor(new PermissionInterceptor()) .description(new PermissionDescription()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { return; } showGrantedPermissionsToast(grantedList); getAllImagesFromGallery(false); } }); } }, delayMillis); } else if (viewId == R.id.btn_main_request_manage_storage_permission) { long delayMillis = 0; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { delayMillis = 2000; toast(getString(R.string.demo_android_11_manage_storage_permission_hint)); } view.postDelayed(new Runnable() { @Override public void run() { XXPermissions.with(MainActivity.this) // 适配分区存储应该这样写 //.permission(PermissionLists.getReadExternalStoragePermission()) //.permission(PermissionLists.getWriteExternalStoragePermission()) // 不适配分区存储应该这样写 .permission(PermissionLists.getManageExternalStoragePermission()) .interceptor(new PermissionInterceptor()) .description(new PermissionDescription()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { return; } showGrantedPermissionsToast(grantedList); } }); } }, delayMillis); } else if (viewId == R.id.btn_main_request_install_packages_permission) { XXPermissions.with(this) .permission(PermissionLists.getRequestInstallPackagesPermission()) .interceptor(new PermissionInterceptor()) .description(new PermissionDescription()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { return; } showGrantedPermissionsToast(grantedList); } }); } else if (viewId == R.id.btn_main_request_system_alert_window_permission) { XXPermissions.with(this) .permission(PermissionLists.getSystemAlertWindowPermission()) .interceptor(new PermissionInterceptor()) .description(new PermissionDescription()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { return; } showGrantedPermissionsToast(grantedList); } }); } else if (viewId == R.id.btn_main_request_write_settings_permission) { XXPermissions.with(this) .permission(PermissionLists.getWriteSettingsPermission()) .interceptor(new PermissionInterceptor()) .description(new PermissionDescription()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { return; } showGrantedPermissionsToast(grantedList); } }); } else if (viewId == R.id.btn_main_request_notification_service_permission) { String channelId = getString(R.string.test_notification_channel_id); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){ NotificationChannel channel = new NotificationChannel(channelId, getString(R.string.test_notification_channel_name), NotificationManager.IMPORTANCE_DEFAULT); NotificationManager manager = getSystemService(NotificationManager.class); manager.createNotificationChannel(channel); } XXPermissions.with(this) // 不需要指定通知渠道 id 这样写(两种写法只能二选一,不可以两种都写) //.permission(PermissionLists.getNotificationServicePermission()) // 需要指定通知渠道 id 这样写(两种写法只能二选一,不可以两种都写) .permission(PermissionLists.getNotificationServicePermission(channelId)) .interceptor(new PermissionInterceptor()) .description(new PermissionDescription()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { return; } showGrantedPermissionsToast(grantedList); } }); } else if (viewId == R.id.btn_main_request_post_notifications_permission) { long delayMillis = 0; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { delayMillis = 2000; toast(getString(R.string.demo_android_13_post_notification_permission_hint)); } view.postDelayed(new Runnable() { @Override public void run() { XXPermissions.with(MainActivity.this) .permission(PermissionLists.getPostNotificationsPermission()) .interceptor(new PermissionInterceptor()) .description(new PermissionDescription()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { return; } showGrantedPermissionsToast(grantedList); } }); } }, delayMillis); } else if (viewId == R.id.btn_main_request_bind_notification_listener_permission) { XXPermissions.with(this) .permission(PermissionLists.getBindNotificationListenerServicePermission( ExampleNotificationListenerService.class)) .interceptor(new PermissionInterceptor()) .description(new PermissionDescription()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { return; } showGrantedPermissionsToast(grantedList); toggleNotificationListenerService(); } }); } else if (viewId == R.id.btn_main_request_usage_stats_permission) { XXPermissions.with(this) .permission(PermissionLists.getPackageUsageStatsPermission()) .interceptor(new PermissionInterceptor()) .description(new PermissionDescription()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { return; } showGrantedPermissionsToast(grantedList); } }); } else if (viewId == R.id.btn_main_request_schedule_exact_alarm_permission) { XXPermissions.with(this) .permission(PermissionLists.getScheduleExactAlarmPermission()) .interceptor(new PermissionInterceptor()) .description(new PermissionDescription()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { return; } showGrantedPermissionsToast(grantedList); } }); } else if (viewId == R.id.btn_main_request_access_notification_policy_permission) { XXPermissions.with(this) .permission(PermissionLists.getAccessNotificationPolicyPermission()) .interceptor(new PermissionInterceptor()) .description(new PermissionDescription()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { return; } showGrantedPermissionsToast(grantedList); } }); } else if (viewId == R.id.btn_main_request_ignore_battery_optimizations_permission) { XXPermissions.with(this) .permission(PermissionLists.getRequestIgnoreBatteryOptimizationsPermission()) .interceptor(new PermissionInterceptor()) .description(new PermissionDescription()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { return; } showGrantedPermissionsToast(grantedList); } }); } else if (viewId == R.id.btn_main_request_picture_in_picture_permission) { XXPermissions.with(this) .permission(PermissionLists.getPictureInPicturePermission()) .interceptor(new PermissionInterceptor()) .description(new PermissionDescription()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { return; } showGrantedPermissionsToast(grantedList); } }); } else if (viewId == R.id.btn_main_request_bind_vpn_service_permission) { XXPermissions.with(this) .permission(PermissionLists.getBindVpnServicePermission()) .interceptor(new PermissionInterceptor()) .description(new PermissionDescription()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { return; } showGrantedPermissionsToast(grantedList); } }); } else if (viewId == R.id.btn_main_request_full_screen_notifications_permission) { long delayMillis = 0; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { delayMillis = 2000; toast(getString(R.string.demo_android_14_full_screen_notifications_permission_hint)); } view.postDelayed(new Runnable() { @Override public void run() { XXPermissions.with(MainActivity.this) // 请求全屏通知权限需要携带通知权限(发送通知权限或者通知服务权限任意一个即可)同时申请 .permission(PermissionLists.getPostNotificationsPermission()) //.permission(PermissionLists.getNotificationServicePermission()) .permission(PermissionLists.getUseFullScreenIntentPermission()) .interceptor(new PermissionInterceptor()) .description(new PermissionDescription()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { return; } showGrantedPermissionsToast(grantedList); } }); } }, delayMillis); } else if (viewId == R.id.btn_main_request_device_admin_permission) { XXPermissions.with(this) .permission(PermissionLists.getBindDeviceAdminPermission( ExampleDeviceAdminReceiver.class, getString(R.string.test_device_admin_extra_add_explanation))) .interceptor(new PermissionInterceptor()) .description(new PermissionDescription()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { return; } showGrantedPermissionsToast(grantedList); } }); } else if (viewId == R.id.btn_main_request_accessibility_service_permission) { XXPermissions.with(this) .permission(PermissionLists.getBindAccessibilityServicePermission(ExampleAccessibilityService.class)) .interceptor(new PermissionInterceptor()) .description(new PermissionDescription()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { return; } showGrantedPermissionsToast(grantedList); } }); } else if (viewId == R.id.btn_main_request_manage_media_permission) { XXPermissions.with(this) .permission(PermissionLists.getManageMediaPermission()) .interceptor(new PermissionInterceptor()) .description(new PermissionDescription()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { return; } showGrantedPermissionsToast(grantedList); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { return; } ContentResolver contentResolver = getContentResolver(); // 适配 Android 10 分区存储特性 ContentValues values = new ContentValues(); // 设置显示的文件名 values.put(MediaStore.Images.Media.DISPLAY_NAME, "XXPermissionsLogo.png"); // 生成一个新的 uri 路径 Uri outputUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); if (outputUri == null) { return; } // 生成图片到本地 try { Drawable drawable = ContextCompat.getDrawable(MainActivity.this, R.mipmap.ic_launcher); // w:写入模式,如果文件存在则覆盖,如果文件不存在则创建 // wa:追加模式,如果文件存在则追加到文件末尾,如果文件不存在则创建 OutputStream outputStream = contentResolver.openOutputStream(outputUri, "w"); if (outputStream == null) { return; } if (!(drawable instanceof BitmapDrawable)) { return; } BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable; if (bitmapDrawable.getBitmap().compress(Bitmap.CompressFormat.PNG, 100, outputStream)) { outputStream.flush(); } MediaStore.createWriteRequest(contentResolver, Collections.singletonList(outputUri)); } catch (IOException e) { e.printStackTrace(); } } }); } else if (viewId == R.id.btn_main_request_get_installed_apps_permission) { XXPermissions.with(this) .permission(PermissionLists.getGetInstalledAppsPermission()) .interceptor(new PermissionInterceptor()) .description(new PermissionDescription()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { return; } showGrantedPermissionsToast(grantedList); getAppList(); } }); } else if (viewId == R.id.btn_main_request_multiple_type_permission) { XXPermissions.with(this) .permission(PermissionLists.getReadCallLogPermission()) .permission(PermissionLists.getWriteCallLogPermission()) .permission(PermissionLists.getSystemAlertWindowPermission()) .interceptor(new PermissionInterceptor()) .description(new PermissionDescription()) .request(new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { boolean allGranted = deniedList.isEmpty(); if (!allGranted) { return; } showGrantedPermissionsToast(grantedList); } }); } else if (viewId == R.id.btn_main_start_permission_activity) { XXPermissions.startPermissionActivity(this); } } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode != XXPermissions.REQUEST_CODE) { return; } toast(getString(R.string.demo_return_activity_result_hint)); } public void showGrantedPermissionsToast(List grantedList) { toast(String.format(getString(R.string.demo_obtain_permission_success_hint), PermissionConverter.getNickNamesByPermissions(MainActivity.this, grantedList))); } public void toast(CharSequence text) { Toaster.show(text); } private void toggleNotificationListenerService() { PackageManager packageManager = getPackageManager(); packageManager.setComponentEnabledSetting( new ComponentName(this, ExampleNotificationListenerService.class), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); packageManager.setComponentEnabledSetting( new ComponentName(this, ExampleNotificationListenerService.class), PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP); } /** * 获取所有图片媒体 * * @param acquireLatitudeAndLongitude 是否获取图片拍摄时的经纬度 */ private void getAllImagesFromGallery(boolean acquireLatitudeAndLongitude) { String[] projection = {MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA, MediaStore.MediaColumns.TITLE, MediaStore.Images.Media.SIZE, MediaStore.Images.ImageColumns.LATITUDE, MediaStore.Images.ImageColumns.LONGITUDE}; final String orderBy = MediaStore.Video.Media.DATE_TAKEN; Cursor cursor = getApplicationContext().getContentResolver() .query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null, orderBy + " DESC"); int idIndex = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID); int pathIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DATA); int titleIndex = cursor.getColumnIndex(MediaStore.MediaColumns.TITLE); while (cursor.moveToNext()) { String filePath = cursor.getString(pathIndex); float[] latLong = new float[2]; // 谷歌官方文档:https://developer.android.google.cn/training/data-storage/shared/media?hl=zh-cn#location-media-captured Uri photoUri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, cursor.getString(idIndex)); String photoTitle = cursor.getString(titleIndex); Log.i("XXPermissions", photoTitle + " = " + filePath); if (acquireLatitudeAndLongitude) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { photoUri = MediaStore.setRequireOriginal(photoUri); try { InputStream inputStream = getApplicationContext() .getContentResolver().openInputStream(photoUri); if (inputStream == null) { continue; } ExifInterface exifInterface = new ExifInterface(inputStream); // 获取图片的经纬度 exifInterface.getLatLong(latLong); inputStream.close(); } catch (IOException e) { e.printStackTrace(); } catch (UnsupportedOperationException e) { // java.lang.UnsupportedOperationException: // Caller must hold ACCESS_MEDIA_LOCATION permission to access original // 经过测试,在部分手机上面申请获取媒体位置权限,如果用户选择的是 "仅在使用中允许" // 那么就会导致权限是授予状态,但是调用 openInputStream 时会抛出此异常 e.printStackTrace(); } } else { int latitudeIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.LATITUDE); int longitudeIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.LONGITUDE); latLong = new float[]{cursor.getFloat(latitudeIndex), cursor.getFloat(longitudeIndex)}; } } if (latLong[0] != 0 && latLong[1] != 0) { Log.i("XXPermissions", "获取到图片的经纬度:" + filePath + "," + Arrays.toString(latLong)); Log.i("XXPermissions", "图片经纬度所在的地址:" + latLongToAddressString(latLong[0], latLong[1])); } else { Log.i("XXPermissions", "该图片获取不到经纬度:" + filePath); } } cursor.close(); } /** * 将经纬度转换成地址 */ private String latLongToAddressString(float latitude, float longitude) { String addressString = ""; Geocoder geocoder = new Geocoder(this, Locale.getDefault()); try { List
addresses = geocoder.getFromLocation(latitude, longitude, 1); if (addresses != null) { Address returnedAddress = addresses.get(0); StringBuilder strReturnedAddress = new StringBuilder(""); for (int i = 0; i <= returnedAddress.getMaxAddressLineIndex(); i++) { strReturnedAddress.append(returnedAddress.getAddressLine(i)).append("\n"); } addressString = strReturnedAddress.toString(); } else { Log.w("XXPermissions", "没有返回地址"); } } catch (Exception e) { e.printStackTrace(); Log.w("XXPermissions", "无法获取到地址"); } return addressString; } private final SensorEventListener mSensorEventListener = new SensorEventListener() { @Override public void onSensorChanged(SensorEvent event) { Log.w("onSensorChanged", "event = " + event); switch (event.sensor.getType()) { case Sensor.TYPE_STEP_COUNTER: Log.w("XXPermissions", "开机以来当天总步数:" + event.values[0]); break; case Sensor.TYPE_STEP_DETECTOR: if (event.values[0] == 1) { Log.w("XXPermissions", "当前走了一步"); } break; default: break; } } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { Log.w("onAccuracyChanged", String.valueOf(accuracy)); } }; /** * 添加步数监听 */ private void addCountStepListener() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { return; } SensorManager manager = (SensorManager) getSystemService(SENSOR_SERVICE); Sensor stepSensor = manager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER); Sensor detectorSensor = manager.getDefaultSensor(Sensor.TYPE_STEP_DETECTOR); if (stepSensor != null) { manager.registerListener(mSensorEventListener, stepSensor, SensorManager.SENSOR_DELAY_NORMAL); } if (detectorSensor != null) { manager.registerListener(mSensorEventListener, detectorSensor, SensorManager.SENSOR_DELAY_NORMAL); } } private void getAppList() { try { PackageManager packageManager = getPackageManager(); int flags = PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES; List packageInfoList; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { packageInfoList = packageManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(flags)); } else { packageInfoList = packageManager.getInstalledPackages(flags); } for (PackageInfo info : packageInfoList) { Log.i("XXPermissions", "应用包名:" + info.packageName); } } catch (Throwable t) { t.printStackTrace(); } } } ================================================ FILE: app/src/main/java/com/hjq/permissions/demo/WindowLifecycleManager.java ================================================ package com.hjq.permissions.demo; import android.app.Activity; import android.app.Application.ActivityLifecycleCallbacks; import android.app.Dialog; import android.os.Build; import android.os.Bundle; import android.widget.PopupWindow; import androidx.annotation.NonNull; import androidx.annotation.Nullable; /** * author : Android 轮子哥 * github : https://github.com/getActivity/XXPermissions * time : 2025/05/30 * desc : 窗口生命周期管理 */ public final class WindowLifecycleManager { /** * 将 Activity 和 Dialog 的生命周期绑定在一起 */ public static void bindDialogLifecycle(@NonNull Activity activity, @NonNull Dialog dialog) { WindowLifecycleCallbacks windowLifecycleCallbacks = new WindowLifecycleCallbacks(activity) { @Override public void onWindowDismiss() { if (!dialog.isShowing()) { return; } dialog.dismiss(); } }; registerWindowLifecycleCallbacks(activity, windowLifecycleCallbacks); } /** * 将 Activity 和 PopupWindow 的生命周期绑定在一起 */ public static void bindPopupWindowLifecycle(@NonNull Activity activity, @NonNull PopupWindow popupWindow) { WindowLifecycleCallbacks windowLifecycleCallbacks = new WindowLifecycleCallbacks(activity) { @Override public void onWindowDismiss() { if (!popupWindow.isShowing()) { return; } popupWindow.dismiss(); } }; registerWindowLifecycleCallbacks(activity, windowLifecycleCallbacks); } /** * 注册窗口回调 */ private static void registerWindowLifecycleCallbacks(@NonNull Activity activity, @NonNull WindowLifecycleCallbacks callbacks) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { activity.registerActivityLifecycleCallbacks(callbacks); } else { activity.getApplication().registerActivityLifecycleCallbacks(callbacks); } } /** * 反注册窗口回调 */ private static void unregisterWindowLifecycleCallbacks(@NonNull Activity activity, @NonNull WindowLifecycleCallbacks callbacks) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { activity.unregisterActivityLifecycleCallbacks(callbacks); } else { activity.getApplication().unregisterActivityLifecycleCallbacks(callbacks); } } /** * 窗口生命周期回调 */ private abstract static class WindowLifecycleCallbacks implements ActivityLifecycleCallbacks { @Nullable private Activity mActivity; private WindowLifecycleCallbacks(@NonNull Activity activity) { mActivity = activity; } public abstract void onWindowDismiss(); @Override public final void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) { // default implementation ignored } @Override public final void onActivityStarted(@NonNull Activity activity) { // default implementation ignored } @Override public final void onActivityResumed(@NonNull Activity activity) { // default implementation ignored } @Override public final void onActivityPaused(@NonNull Activity activity) { // default implementation ignored } @Override public final void onActivityStopped(@NonNull Activity activity) { // default implementation ignored } @Override public final void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) { // default implementation ignored } @Override public final void onActivityDestroyed(@NonNull Activity activity) { if (activity != mActivity) { return; } // 释放 Activity 对象 mActivity = null; // 反注册窗口监听 unregisterWindowLifecycleCallbacks(activity, this); // 通知外层销毁窗口 onWindowDismiss(); } } } ================================================ FILE: app/src/main/java/com/hjq/permissions/demo/example/ExampleAccessibilityService.java ================================================ package com.hjq.permissions.demo.example; import android.accessibilityservice.AccessibilityService; import android.util.Log; import android.view.KeyEvent; import android.view.accessibility.AccessibilityEvent; import androidx.annotation.NonNull; /** * author : Android 轮子哥 * github : https://github.com/getActivity/XXPermissions * time : 2025/06/15 * desc : 无障碍服务案例类 */ public final class ExampleAccessibilityService extends AccessibilityService { /** * 无障碍服务的生命周期,表明服务已经连接成功 */ @Override public void onServiceConnected() { super.onServiceConnected(); } /** * 当系统想要中断您的服务正在提供的反馈(通常是为了响应将焦点移到其他 * 控件等用户操作)时,就会调用此方法。此方法可能会在您的服务的整个生命 * 周期内被调用多次。 */ @Override public void onInterrupt() { // default implementation ignored } /** * 当用户在触摸屏上执行特定手势时由系统调用。注意:为了接收手势, * 辅助服务必须通过设置AccessibilityServiceInfo请求设备处于触摸探索模式FLAG_REQUEST_TOUCH_EXPLORATION_MOD */ @Override public boolean onGesture(int gestureId) { log("onGesture: " + gestureId); return super.onGesture(gestureId); } /** * 当系统检测到与无障碍服务指定的事件过滤参数匹配的 AccessibilityEvent * 时,就会回调此方法。例如,当用户点击按钮,或者聚焦于某个应用(无障碍 * 服务正在为该应用提供反馈)中的界面控件时。出现这种情况时,系统会调用 * 此方法,并传递关联的 AccessibilityEvent,然后服务会对该类进行解释并 * 使用它来向用户提供反馈。此方法可能会在您的服务的整个生命周期内被调用多次。 */ @Override public void onAccessibilityEvent(AccessibilityEvent event) { log("无障碍服务 onAccessibilityEvent:" + event); switch (event.getEventType()) { case AccessibilityEvent.TYPE_ANNOUNCEMENT: log("应用程序发布公告的事件"); break; case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: log("View 的焦点"); break; case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: log("View 的焦点清除"); break; case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED: log("通知栏状态更新"); break; case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER: log("View 的鼠标悬停选中"); break; case AccessibilityEvent.TYPE_VIEW_HOVER_EXIT: log("View 的鼠标悬停离开"); break; case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START: log("开始触摸探索手势的事件"); break; case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END: log("结束触摸探索手势的事件"); break; case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: log("窗口内容更新"); break; case AccessibilityEvent.TYPE_VIEW_SCROLLED: log("滚动类View"); break; case AccessibilityEvent.TYPE_VIEW_SELECTED: log("表示通常在 android.widget.AdapterView 的上下文中选择项的事件"); break; case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED: log("EditText 视图选中内容改变"); break; case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED: log("EditText 视图内容改变"); break; case AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY: log("表示以给定的移动粒度遍历视图文本的事件"); break; case AccessibilityEvent.TYPE_VIEW_CLICKED: log("点击事件"); break; case AccessibilityEvent.TYPE_VIEW_LONG_CLICKED: log("长按点击事件"); break; case AccessibilityEvent.TYPE_VIEW_CONTEXT_CLICKED: log("表示在 android.view.View 上的上下文单击事件"); break; case AccessibilityEvent.TYPE_GESTURE_DETECTION_START: log("开始手势检测"); break; case AccessibilityEvent.TYPE_GESTURE_DETECTION_END: log("结束手势检测"); break; case AccessibilityEvent.TYPE_TOUCH_INTERACTION_START: log("表示用户开始触摸屏幕的事件"); break; case AccessibilityEvent.TYPE_TOUCH_INTERACTION_END: log("表示用户结束触摸屏幕的事件"); break; case AccessibilityEvent.TYPE_ASSIST_READING_CONTEXT: log("表示助手当前正在读取用户屏幕上下文的事件。"); break; } } /** * 按键事件 */ @Override public boolean onKeyEvent(KeyEvent event) { log("onKeyEvent: " + event); return super.onKeyEvent(event); } private void log(@NonNull String message) { Log.i("XXPermissions", message); } } ================================================ FILE: app/src/main/java/com/hjq/permissions/demo/example/ExampleDeviceAdminReceiver.java ================================================ package com.hjq.permissions.demo.example; import android.app.admin.DeviceAdminReceiver; import android.content.Context; import android.content.Intent; import android.os.UserHandle; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; /** * author : Android 轮子哥 * github : https://github.com/getActivity/XXPermissions * time : 2025/06/15 * desc : 设备管理器广播案例类 */ public final class ExampleDeviceAdminReceiver extends DeviceAdminReceiver { @Override public void onEnabled(@NonNull Context context, @NonNull Intent intent) { super.onEnabled(context, intent); // Toaster.show("设备管理器:可用"); log("设备管理器:可用"); } @Override public void onDisabled(@NonNull Context context, @NonNull Intent intent) { super.onDisabled(context, intent); // Toaster.show("设备管理器:不可用"); log("设备管理器:不可用"); } @Nullable @Override public CharSequence onDisableRequested(@NonNull Context context, @NonNull Intent intent) { return "这是一个可选的消息,警告有关禁止用户的请求"; } @Override public void onPasswordChanged(@NonNull Context context, @NonNull Intent intent, @NonNull UserHandle user) { super.onPasswordChanged(context, intent, user); // Toaster.show("设备管理器:密码己经改变"); log("设备管理器:密码己经改变"); } @Override public void onPasswordFailed(@NonNull Context context, @NonNull Intent intent) { super.onPasswordFailed(context, intent); // Toaster.show("设备管理器:改变密码失败"); log("设备管理器:改变密码失败"); } @Override public void onPasswordSucceeded(@NonNull Context context, @NonNull Intent intent, @NonNull UserHandle user) { super.onPasswordSucceeded(context, intent, user); // Toaster.show("设备管理器:改变密码成功"); log("设备管理器:改变密码成功"); } private void log(@NonNull String message) { Log.i("XXPermissions", message); } } ================================================ FILE: app/src/main/java/com/hjq/permissions/demo/example/ExampleNotificationListenerService.java ================================================ package com.hjq.permissions.demo.example; import android.app.Notification; import android.os.Build; import android.os.Bundle; import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; import android.util.Log; import com.hjq.permissions.demo.R; /** * author : Android 轮子哥 * github : https://github.com/getActivity/XXPermissions * time : 2022/01/22 * desc : 通知消息监控服务案例类 */ public final class ExampleNotificationListenerService extends NotificationListenerService { /** * 当系统收到新的通知后出发回调 */ @Override public void onNotificationPosted(StatusBarNotification sbn) { // 需要注释掉回调 super.onNotificationPosted 的调用,测试在原生 Android 4.3 版本会触发崩溃 // 但是测试在原生 Android 5.0 的版本却没有这个问题,证明这个是一个历史遗留问题 // java.lang.AbstractMethodError: abstract method not implemented // super.onNotificationPosted(sbn); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { return; } Bundle extras = sbn.getNotification().extras; if (extras == null) { return; } //获取通知消息标题 String title = extras.getString(Notification.EXTRA_TITLE); // 获取通知消息内容 Object msgText = extras.getCharSequence(Notification.EXTRA_TEXT); // Toaster.show(String.format(getString(R.string.demo_notification_listener_toast), title, msgText)); // 这里选择打 Log,而不是弹 Toast,是为了避免影响 Demo 工程的使用体验 Log.i("XXPermissions", String.format(getString(R.string.demo_notification_listener_toast), title, msgText)); } /** * 当系统通知被删掉后出发回调 */ @Override public void onNotificationRemoved(StatusBarNotification sbn) { // 需要注释掉回调 super.onNotificationRemoved 的调用,测试在原生 Android 4.3 版本会触发崩溃 // 但是测试在原生 Android 5.0 的版本却没有这个问题,证明这个是一个历史遗留问题 // java.lang.AbstractMethodError: abstract method not implemented // super.onNotificationRemoved(sbn); } } ================================================ FILE: app/src/main/java/com/hjq/permissions/demo/example/ExampleVpnService.java ================================================ package com.hjq.permissions.demo.example; import android.net.VpnService; public final class ExampleVpnService extends VpnService { } ================================================ FILE: app/src/main/java/com/hjq/permissions/demo/permission/PermissionConverter.java ================================================ package com.hjq.permissions.demo.permission; import android.content.Context; import android.os.Build; import android.text.TextUtils; import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.hjq.permissions.demo.R; import com.hjq.permissions.permission.PermissionGroups; import com.hjq.permissions.permission.PermissionNames; import com.hjq.permissions.permission.base.IPermission; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * author : Android 轮子哥 * github : https://github.com/getActivity/XXPermissions * time : 2025/05/30 * desc : 权限转换器(根据权限获取对应的名称和说明) */ public final class PermissionConverter { /** 权限名称映射(为了适配多语种,这里存储的是 StringId,而不是 String) */ private static final Map PERMISSION_NAME_MAP = new HashMap<>(); /** 权限描述映射(为了适配多语种,这里存储的是 StringId,而不是 String) */ private static final Map PERMISSION_DESCRIPTION_MAP = new HashMap<>(); static { PERMISSION_NAME_MAP.put(PermissionGroups.STORAGE, R.string.common_permission_storage); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_storage, R.string.common_permission_storage_description); PERMISSION_NAME_MAP.put(PermissionGroups.IMAGE_AND_VIDEO_MEDIA, R.string.common_permission_image_and_video); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_image_and_video, R.string.common_permission_image_and_video_description); PERMISSION_NAME_MAP.put(PermissionNames.READ_MEDIA_AUDIO, R.string.common_permission_music_and_audio); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_music_and_audio, R.string.common_permission_music_and_audio_description); PERMISSION_NAME_MAP.put(PermissionNames.CAMERA, R.string.common_permission_camera); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_camera, R.string.common_permission_camera_description); PERMISSION_NAME_MAP.put(PermissionNames.RECORD_AUDIO, R.string.common_permission_microphone); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_microphone, R.string.common_permission_microphone_description); PERMISSION_NAME_MAP.put(PermissionGroups.NEARBY_DEVICES, R.string.common_permission_nearby_devices); // 注意:在 Android 13 的时候,WIFI 相关的权限已经归到附近设备的权限组了,但是在 Android 13 之前,WIFI 相关的权限归属定位权限组 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // 需要填充文案:蓝牙权限描述 + WIFI 权限描述 PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_nearby_devices, R.string.common_permission_nearby_devices_description); } else { // 需要填充文案:蓝牙权限描述 PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_nearby_devices, R.string.common_permission_nearby_devices_description); } PERMISSION_NAME_MAP.put(PermissionGroups.LOCATION, R.string.common_permission_location); // 注意:在 Android 12 的时候,蓝牙相关的权限已经归到附近设备的权限组了,但是在 Android 12 之前,蓝牙相关的权限归属定位权限组 // 注意:在 Android 13 的时候,WIFI 相关的权限已经归到附近设备的权限组了,但是在 Android 13 之前,WIFI 相关的权限归属定位权限组 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // 需要填充文案:前台定位权限描述 PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_location, R.string.common_permission_location_description); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // 需要填充文案:前台定位权限描述 + WIFI 权限描述 PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_location, R.string.common_permission_location_description); } else { // 需要填充文案:前台定位权限描述 + 蓝牙权限描述 + WIFI 权限描述 PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_location, R.string.common_permission_location_description); } // 后台定位权限虽然属于定位权限组,但是只要是属于后台权限,都有独属于自己的一套规则 PERMISSION_NAME_MAP.put(PermissionNames.ACCESS_BACKGROUND_LOCATION, R.string.common_permission_location_background); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_location_background, R.string.common_permission_location_background_description); int sensorsPermissionNameStringId; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) { sensorsPermissionNameStringId = R.string.common_permission_health_data; } else { sensorsPermissionNameStringId = R.string.common_permission_body_sensors; } PERMISSION_NAME_MAP.put(PermissionGroups.SENSORS, sensorsPermissionNameStringId); PERMISSION_DESCRIPTION_MAP.put(sensorsPermissionNameStringId, R.string.common_permission_body_sensors_description); // 后台传感器权限虽然属于传感器权限组,但是只要是属于后台权限,都有独属于自己的一套规则 int bodySensorsBackgroundPermissionNameStringId; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) { bodySensorsBackgroundPermissionNameStringId = R.string.common_permission_health_data_background; } else { bodySensorsBackgroundPermissionNameStringId = R.string.common_permission_body_sensors_background; } PERMISSION_NAME_MAP.put(PermissionNames.BODY_SENSORS_BACKGROUND, bodySensorsBackgroundPermissionNameStringId); PERMISSION_DESCRIPTION_MAP.put(bodySensorsBackgroundPermissionNameStringId, R.string.common_permission_body_sensors_background_description); // Android 16 这个版本开始,传感器权限被进行了精细化拆分,拆分成了无数个健康权限 PERMISSION_NAME_MAP.put(PermissionGroups.HEALTH, R.string.common_permission_health_data); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_health_data, R.string.common_permission_health_data_description); PERMISSION_NAME_MAP.put(PermissionNames.READ_HEALTH_DATA_IN_BACKGROUND, R.string.common_permission_health_data_background); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_health_data_background, R.string.common_permission_health_data_background_description); PERMISSION_NAME_MAP.put(PermissionNames.READ_HEALTH_DATA_HISTORY, R.string.common_permission_health_data_past); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_health_data_past, R.string.common_permission_health_data_past_description); PERMISSION_NAME_MAP.put(PermissionGroups.CALL_LOG, R.string.common_permission_call_logs); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_call_logs, R.string.common_permission_call_logs_description); PERMISSION_NAME_MAP.put(PermissionGroups.PHONE, R.string.common_permission_phone); // 注意:在 Android 9.0 的时候,读写通话记录权限已经归到一个单独的权限组了,但是在 Android 9.0 之前,读写通话记录权限归属电话权限组 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { // 需要填充文案:电话权限描述 PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_phone, R.string.common_permission_phone_description); } else { // 需要填充文案:电话权限描述 + 通话记录权限描述 PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_phone, R.string.common_permission_phone_description); } PERMISSION_NAME_MAP.put(PermissionGroups.CONTACTS, R.string.common_permission_contacts); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_contacts, R.string.common_permission_contacts_description); PERMISSION_NAME_MAP.put(PermissionGroups.CALENDAR, R.string.common_permission_calendar); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_calendar, R.string.common_permission_calendar_description); // 注意:在 Android 10 的版本,这个权限的名称为《健身运动权限》,但是到了 Android 11 的时候,这个权限的名称被修改成了《身体活动权限》 // 没错就改了一下权限的叫法,其他的一切没有变,Google 产品经理真的是闲的蛋疼,但是吐槽归吐槽,框架也要灵活应对一下,避免小白用户跳转到设置页找不到对应的选项 int activityRecognitionPermissionNameStringId = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ? R.string.common_permission_activity_recognition_api30 : R.string.common_permission_activity_recognition_api29; PERMISSION_NAME_MAP.put(PermissionNames.ACTIVITY_RECOGNITION, activityRecognitionPermissionNameStringId); PERMISSION_DESCRIPTION_MAP.put(activityRecognitionPermissionNameStringId, R.string.common_permission_activity_recognition_description); PERMISSION_NAME_MAP.put(PermissionNames.ACCESS_MEDIA_LOCATION, R.string.common_permission_access_media_location_information); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_access_media_location_information, R.string.common_permission_access_media_location_information_description); PERMISSION_NAME_MAP.put(PermissionGroups.SMS, R.string.common_permission_sms); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_sms, R.string.common_permission_sms_description); PERMISSION_NAME_MAP.put(PermissionNames.GET_INSTALLED_APPS, R.string.common_permission_get_installed_apps); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_get_installed_apps, R.string.common_permission_get_installed_apps_description); PERMISSION_NAME_MAP.put(PermissionNames.MANAGE_EXTERNAL_STORAGE, R.string.common_permission_all_file_access); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_all_file_access, R.string.common_permission_all_file_access_description); PERMISSION_NAME_MAP.put(PermissionNames.REQUEST_INSTALL_PACKAGES, R.string.common_permission_install_unknown_apps); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_install_unknown_apps, R.string.common_permission_install_unknown_apps_description); PERMISSION_NAME_MAP.put(PermissionNames.SYSTEM_ALERT_WINDOW, R.string.common_permission_display_over_other_apps); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_display_over_other_apps, R.string.common_permission_display_over_other_apps_description); PERMISSION_NAME_MAP.put(PermissionNames.WRITE_SETTINGS, R.string.common_permission_modify_system_settings); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_modify_system_settings, R.string.common_permission_modify_system_settings_description); PERMISSION_NAME_MAP.put(PermissionNames.NOTIFICATION_SERVICE, R.string.common_permission_allow_notifications); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_allow_notifications, R.string.common_permission_allow_notifications_description); PERMISSION_NAME_MAP.put(PermissionNames.POST_NOTIFICATIONS, R.string.common_permission_post_notifications); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_post_notifications, R.string.common_permission_post_notifications_description); PERMISSION_NAME_MAP.put(PermissionNames.BIND_NOTIFICATION_LISTENER_SERVICE, R.string.common_permission_allow_notifications_access); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_allow_notifications_access, R.string.common_permission_allow_notifications_access_description); PERMISSION_NAME_MAP.put(PermissionNames.PACKAGE_USAGE_STATS, R.string.common_permission_apps_with_usage_access); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_apps_with_usage_access, R.string.common_permission_apps_with_usage_access_description); PERMISSION_NAME_MAP.put(PermissionNames.SCHEDULE_EXACT_ALARM, R.string.common_permission_alarms_reminders); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_alarms_reminders, R.string.common_permission_alarms_reminders_description); PERMISSION_NAME_MAP.put(PermissionNames.ACCESS_NOTIFICATION_POLICY, R.string.common_permission_do_not_disturb_access); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_do_not_disturb_access, R.string.common_permission_do_not_disturb_access_description); PERMISSION_NAME_MAP.put(PermissionNames.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, R.string.common_permission_ignore_battery_optimize); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_ignore_battery_optimize, R.string.common_permission_ignore_battery_optimize_description); PERMISSION_NAME_MAP.put(PermissionNames.BIND_VPN_SERVICE, R.string.common_permission_vpn); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_vpn, R.string.common_permission_vpn_description); PERMISSION_NAME_MAP.put(PermissionNames.PICTURE_IN_PICTURE, R.string.common_permission_picture_in_picture); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_picture_in_picture, R.string.common_permission_picture_in_picture_description); PERMISSION_NAME_MAP.put(PermissionNames.USE_FULL_SCREEN_INTENT, R.string.common_permission_full_screen_notifications); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_full_screen_notifications, R.string.common_permission_full_screen_notifications_description); PERMISSION_NAME_MAP.put(PermissionNames.BIND_DEVICE_ADMIN, R.string.common_permission_device_admin); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_device_admin, R.string.common_permission_device_admin_description); PERMISSION_NAME_MAP.put(PermissionNames.BIND_ACCESSIBILITY_SERVICE, R.string.common_permission_accessibility_service); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_accessibility_service, R.string.common_permission_accessibility_service_description); PERMISSION_NAME_MAP.put(PermissionNames.MANAGE_MEDIA, R.string.common_permission_manage_media); PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_manage_media, R.string.common_permission_manage_media_description); } /** * 通过权限获得名称 */ @NonNull public static String getNickNamesByPermissions(@NonNull Context context, @NonNull List permissions) { List permissionNameList = getNickNameListByPermissions(context, permissions, true); StringBuilder builder = new StringBuilder(); for (String permissionName : permissionNameList) { if (TextUtils.isEmpty(permissionName)) { continue; } if (builder.length() == 0) { builder.append(permissionName); } else { builder.append(context.getString(R.string.common_permission_comma)) .append(permissionName); } } if (builder.length() == 0) { // 如果没有获得到任何信息,则返回一个默认的文本 return context.getString(R.string.common_permission_unknown); } return builder.toString(); } @NonNull public static List getNickNameListByPermissions(@NonNull Context context, @NonNull List permissions, boolean filterHighVersionPermissions) { List permissionNickNameList = new ArrayList<>(); for (IPermission permission : permissions) { // 如果当前设置了过滤高版本权限,并且这个权限是高版本系统才出现的权限,则不继续往下执行 // 避免出现在低版本上面执行拒绝权限后,连带高版本的名称也一起显示出来,但是在低版本上面是没有这个权限的 if (filterHighVersionPermissions && permission.getFromAndroidVersion(context) > Build.VERSION.SDK_INT) { continue; } String permissionName = getNickNameByPermission(context, permission); if (TextUtils.isEmpty(permissionName)) { continue; } if (permissionNickNameList.contains(permissionName)) { continue; } permissionNickNameList.add(permissionName); } return permissionNickNameList; } public static String getNickNameByPermission(@NonNull Context context, @NonNull IPermission permission) { Integer permissionNameStringId = getPermissionNickNameStringId(context, permission); if (permissionNameStringId == null || permissionNameStringId == 0) { return ""; } return context.getString(permissionNameStringId); } /** * 通过权限获得描述 */ @NonNull public static String getDescriptionsByPermissions(@NonNull Context context, @NonNull List permissions) { List descriptionList = getDescriptionListByPermissions(context, permissions); StringBuilder builder = new StringBuilder(); for (String description : descriptionList) { if (TextUtils.isEmpty(description)) { continue; } if (builder.length() == 0) { builder.append(description); } else { builder.append("\n") .append(description); } } return builder.toString(); } @NonNull public static List getDescriptionListByPermissions(@NonNull Context context, @NonNull List permissions) { List descriptionList = new ArrayList<>(); for (IPermission permission : permissions) { String permissionDescription = getDescriptionByPermission(context, permission); if (TextUtils.isEmpty(permissionDescription)) { continue; } if (descriptionList.contains(permissionDescription)) { continue; } descriptionList.add(permissionDescription); } return descriptionList; } /** * 通过权限获得描述 */ @NonNull public static String getDescriptionByPermission(@NonNull Context context, @NonNull IPermission permission) { Integer permissionNameStringId = getPermissionNickNameStringId(context, permission); if (permissionNameStringId == null || permissionNameStringId == 0) { return ""; } String permissionNickName = context.getString(permissionNameStringId); Integer permissionDescriptionStringId = getPermissionDescriptionStringId(permissionNameStringId); String permissionDescription; if (permissionDescriptionStringId == null || permissionDescriptionStringId == 0) { permissionDescription = ""; } else { permissionDescription = context.getString(permissionDescriptionStringId); } return permissionNickName + context.getString(R.string.common_permission_colon) + permissionDescription; } /** * 获取这个权限对应的别名 StringId */ @Nullable public static Integer getPermissionNickNameStringId(@NonNull Context context, @NonNull IPermission permission) { String permissionName = permission.getPermissionName(); String permissionGroup = permission.getPermissionGroup(context); Integer permissionNameStringId = PERMISSION_NAME_MAP.get(permissionName); if (permissionNameStringId != null && permissionNameStringId > 0) { return permissionNameStringId; } Integer permissionGroupStringId = PERMISSION_NAME_MAP.get(permissionGroup); if (permissionGroupStringId != null && permissionGroupStringId > 0) { return permissionGroupStringId; } return permissionNameStringId; } /** * 获取这个权限对应的描述 StringId */ @Nullable public static Integer getPermissionDescriptionStringId(@IdRes int permissionNickNameStringId) { return PERMISSION_DESCRIPTION_MAP.get(permissionNickNameStringId); } } ================================================ FILE: app/src/main/java/com/hjq/permissions/demo/permission/PermissionDescription.java ================================================ package com.hjq.permissions.demo.permission; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.res.Configuration; import android.graphics.Color; import android.graphics.Point; import android.graphics.drawable.ColorDrawable; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; import android.util.DisplayMetrics; import android.view.Display; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.PopupWindow; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import com.hjq.permissions.OnPermissionDescription; import com.hjq.permissions.demo.R; import com.hjq.permissions.demo.WindowLifecycleManager; import com.hjq.permissions.permission.PermissionPageType; import com.hjq.permissions.permission.base.IPermission; import java.util.List; /** * author : Android 轮子哥 * github : https://github.com/getActivity/XXPermissions * time : 2025/05/30 * desc : 权限请求描述实现 */ public final class PermissionDescription implements OnPermissionDescription { /** 消息处理 Handler 对象 */ public static final Handler HANDLER = new Handler(Looper.getMainLooper()); /** 权限请求描述弹窗显示类型:Dialog */ private static final int DESCRIPTION_WINDOW_TYPE_DIALOG = 0; /** 权限请求描述弹窗显示类型:PopupWindow */ private static final int DESCRIPTION_WINDOW_TYPE_POPUP = 1; /** 权限请求描述弹窗显示类型 */ private int mDescriptionWindowType = DESCRIPTION_WINDOW_TYPE_DIALOG; /** 消息 Token */ @NonNull private final Object mHandlerToken = new Object(); /** 权限申请说明弹窗 */ @Nullable private PopupWindow mPermissionPopupWindow; /** 权限申请说明对话框 */ @Nullable private Dialog mPermissionDialog; @Override public void askWhetherRequestPermission(@NonNull Activity activity, @NonNull List requestList, @NonNull Runnable continueRequestRunnable, @NonNull Runnable breakRequestRunnable) { // 以下情况使用 Dialog 来展示权限说明弹窗,否则使用 PopupWindow 来展示权限说明弹窗 // 1. 如果请求的权限显示的系统界面是不透明的 Activity // 2. 如果当前 Activity 的屏幕是横屏状态的话,要求物理尺寸要够大,否则显示的顶部弹窗会被遮挡住, // 设备的物理屏幕尺寸还小于 8.5 寸(目前大多数小屏平板大多数集中在 8、8.7、8.8、10 寸), // 实测 8 寸的平板获取到的物理尺寸到只有 7.958788793906728,所以这里的代码判断基本上是针对 8.5 寸及以上的平板做优化。 if (isActivityLandscape(activity) && getPhysicalScreenSize(activity) < 8.5) { mDescriptionWindowType = DESCRIPTION_WINDOW_TYPE_DIALOG; } else { mDescriptionWindowType = DESCRIPTION_WINDOW_TYPE_POPUP; for (IPermission permission : requestList) { if (permission.getPermissionPageType(activity) == PermissionPageType.OPAQUE_ACTIVITY) { mDescriptionWindowType = DESCRIPTION_WINDOW_TYPE_DIALOG; } } } if (mDescriptionWindowType == DESCRIPTION_WINDOW_TYPE_POPUP) { continueRequestRunnable.run(); return; } showDialog(activity, activity.getString(R.string.common_permission_description_title), generatePermissionDescription(activity, requestList), activity.getString(R.string.common_permission_confirm), (dialog, which) -> { dialog.dismiss(); continueRequestRunnable.run(); }); } @Override public void onRequestPermissionStart(@NonNull Activity activity, @NonNull List requestList) { if (mDescriptionWindowType != DESCRIPTION_WINDOW_TYPE_POPUP) { return; } Runnable showPopupRunnable = () -> showPopupWindow(activity, generatePermissionDescription(activity, requestList)); // 这里解释一下为什么要延迟一段时间再显示 PopupWindow,这是因为系统没有开放任何 API 给外层直接获取权限是否永久拒绝 // 目前只有申请过了权限才能通过 shouldShowRequestPermissionRationale 判断是不是永久拒绝,如果此前没有申请过权限,则无法判断 // 针对这个问题能想到最佳的解决方案是:先申请权限,如果极短的时间内,权限申请没有结束,则证明权限之前没有被用户勾选了《不再询问》 // 此时系统的权限弹窗正在显示给用户,这个时候再去显示应用的 PopupWindow 权限说明弹窗给用户看,所以这个 PopupWindow 是在发起权限申请后才显示的 // 这样做是为了避免 PopupWindow 显示了又马上消失,这样就不会出现 PopupWindow 一闪而过的效果,提升用户的视觉体验 // 最后补充一点:350 毫秒只是一个经验值,经过测试可覆盖大部分机型,具体可根据实际情况进行调整,这里不做强制要求 // 相关 Github issue 地址:https://github.com/getActivity/XXPermissions/issues/366 HANDLER.postAtTime(showPopupRunnable, mHandlerToken, SystemClock.uptimeMillis() + 350); } @Override public void onRequestPermissionEnd(@NonNull Activity activity, @NonNull List requestList) { // 移除跟这个 Token 有关但是没有还没有执行的消息 HANDLER.removeCallbacksAndMessages(mHandlerToken); // 销毁当前正在显示的弹窗 dismissPopupWindow(); dismissDialog(); } /** * 生成权限描述文案 */ private String generatePermissionDescription(@NonNull Activity activity, @NonNull List requestList) { return PermissionConverter.getDescriptionsByPermissions(activity, requestList); } /** * 显示 Dialog * * @param dialogTitle 对话框标题 * @param dialogMessage 对话框消息 * @param confirmButtonText 对话框确认按钮文本 * @param confirmListener 对话框确认按钮点击事件 */ private void showDialog(@NonNull Activity activity, @Nullable String dialogTitle, @Nullable String dialogMessage, @Nullable String confirmButtonText, @Nullable DialogInterface.OnClickListener confirmListener) { if (mPermissionDialog != null) { dismissDialog(); } if (activity.isFinishing() || activity.isDestroyed()) { return; } // 另外这里需要判断 Activity 的类型来申请权限,这是因为只有 AppCompatActivity 才能调用 AndroidX 库的 AlertDialog 来显示,否则会出现报错 // java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity // 为什么不直接用系统包 AlertDialog 来显示,而是两套规则?因为系统包 AlertDialog 是系统自带的类,不同 Android 版本展现的样式可能不太一样 // 如果这个 Android 版本比较低,那么这个对话框的样式就会变得很丑,准确来讲也不能说丑,而是当时系统的 UI 设计就是那样,它只是跟随系统的样式而已 if (activity instanceof AppCompatActivity) { mPermissionDialog = new androidx.appcompat.app.AlertDialog.Builder(activity) .setTitle(dialogTitle) .setMessage(dialogMessage) // 对话框一定要设置成不可取消的 .setCancelable(false) .setPositiveButton(confirmButtonText, confirmListener) .create(); } else { mPermissionDialog = new AlertDialog.Builder(activity) .setTitle(dialogTitle) .setMessage(dialogMessage) // 对话框一定要设置成不可取消的 .setCancelable(false) .setPositiveButton(confirmButtonText, confirmListener) .create(); } mPermissionDialog.show(); // 将 Activity 和 Dialog 生命周期绑定在一起,避免可能会出现的内存泄漏 // 当然如果上面创建的 Dialog 已经有做了生命周期管理,则不需要执行下面这行代码 WindowLifecycleManager.bindDialogLifecycle(activity, mPermissionDialog); } /** * 销毁 Dialog */ private void dismissDialog() { if (mPermissionDialog == null) { return; } if (!mPermissionDialog.isShowing()) { return; } mPermissionDialog.dismiss(); mPermissionDialog = null; } /** * 显示 PopupWindow * * @param content 弹窗显示的内容 */ private void showPopupWindow(@NonNull Activity activity, @NonNull String content) { if (mPermissionPopupWindow != null) { dismissPopupWindow(); } if (activity.isFinishing() || activity.isDestroyed()) { return; } ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView(); View contentView = LayoutInflater.from(activity) .inflate(R.layout.permission_description_popup, decorView, false); mPermissionPopupWindow = new PopupWindow(activity); mPermissionPopupWindow.setContentView(contentView); mPermissionPopupWindow.setWidth(WindowManager.LayoutParams.MATCH_PARENT); mPermissionPopupWindow.setHeight(WindowManager.LayoutParams.WRAP_CONTENT); mPermissionPopupWindow.setAnimationStyle(android.R.style.Animation_Dialog); mPermissionPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); mPermissionPopupWindow.setTouchable(true); mPermissionPopupWindow.setOutsideTouchable(true); TextView messageView = mPermissionPopupWindow.getContentView().findViewById(R.id.tv_permission_description_message); messageView.setText(content); mPermissionPopupWindow.showAtLocation(decorView, Gravity.TOP, 0, 0); // 将 Activity 和 PopupWindow 生命周期绑定在一起,避免可能会出现的内存泄漏 // 当然如果上面创建的 PopupWindow 已经有做了生命周期管理,则不需要执行下面这行代码 WindowLifecycleManager.bindPopupWindowLifecycle(activity, mPermissionPopupWindow); } /** * 销毁 PopupWindow */ private void dismissPopupWindow() { if (mPermissionPopupWindow == null) { return; } if (!mPermissionPopupWindow.isShowing()) { return; } mPermissionPopupWindow.dismiss(); mPermissionPopupWindow = null; } /** * 判断当前 Activity 是否是横盘显示 */ public static boolean isActivityLandscape(@NonNull Activity activity) { return activity.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; } /** * 获取当前设备的物理屏幕尺寸 */ @SuppressWarnings("deprecation") public static double getPhysicalScreenSize(@NonNull Context context) { WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); Display defaultDisplay = windowManager.getDefaultDisplay(); if (defaultDisplay == null) { return 0; } DisplayMetrics metrics = new DisplayMetrics(); defaultDisplay.getMetrics(metrics); float screenWidthInInches; float screenHeightInInches; Point point = new Point(); defaultDisplay.getRealSize(point); screenWidthInInches = point.x / metrics.xdpi; screenHeightInInches = point.y / metrics.ydpi; // 勾股定理:直角三角形的两条直角边的平方和等于斜边的平方 return Math.sqrt(Math.pow(screenWidthInInches, 2) + Math.pow(screenHeightInInches, 2)); } } ================================================ FILE: app/src/main/java/com/hjq/permissions/demo/permission/PermissionInterceptor.java ================================================ package com.hjq.permissions.demo.permission; import android.app.Activity; import android.app.AlertDialog.Builder; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.pm.PackageManager; import android.os.Build; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import com.hjq.permissions.OnPermissionCallback; import com.hjq.permissions.OnPermissionInterceptor; import com.hjq.permissions.XXPermissions; import com.hjq.permissions.demo.R; import com.hjq.permissions.demo.WindowLifecycleManager; import com.hjq.permissions.permission.PermissionGroups; import com.hjq.permissions.permission.PermissionNames; import com.hjq.permissions.permission.base.IPermission; import com.hjq.toast.Toaster; import java.util.List; /** * author : Android 轮子哥 * github : https://github.com/getActivity/XXPermissions * time : 2021/01/04 * desc : 权限申请拦截器 */ public final class PermissionInterceptor implements OnPermissionInterceptor { @Override public void onRequestPermissionEnd(@NonNull Activity activity, boolean skipRequest, @NonNull List requestList, @NonNull List grantedList, @NonNull List deniedList, @Nullable OnPermissionCallback callback) { if (callback != null) { callback.onResult(grantedList, deniedList); } if (deniedList.isEmpty()) { return; } boolean doNotAskAgain = XXPermissions.isDoNotAskAgainPermissions(activity, deniedList); String permissionHint = generatePermissionHint(activity, deniedList, doNotAskAgain); if (!doNotAskAgain) { // 如果没有勾选不再询问选项,就弹 Toast 提示给用户 Toaster.show(permissionHint); return; } // 如果勾选了不再询问选项,就弹 Dialog 引导用户去授权 showPermissionSettingDialog(activity, requestList, deniedList, callback, permissionHint); } private void showPermissionSettingDialog(@NonNull Activity activity, @NonNull List requestList, @NonNull List deniedList, @Nullable OnPermissionCallback callback, @NonNull String permissionHint) { if (activity.isFinishing() || activity.isDestroyed()) { return; } String dialogTitle = activity.getString(R.string.common_permission_alert); String confirmButtonText = activity.getString(R.string.common_permission_go_to_authorization); DialogInterface.OnClickListener confirmListener = (dialog, which) -> { dialog.dismiss(); XXPermissions.startPermissionActivity(activity, deniedList, new OnPermissionCallback() { @Override public void onResult(@NonNull List grantedList, @NonNull List deniedList) { List latestDeniedList = XXPermissions.getDeniedPermissions(activity, requestList); boolean allGranted = latestDeniedList.isEmpty(); if (!allGranted) { // 递归显示对话框,让提示用户授权,只不过对话框是可取消的,用户不想授权了,随时可以点击返回键或者对话框蒙层来取消显示 showPermissionSettingDialog(activity, requestList, latestDeniedList, callback, generatePermissionHint(activity, latestDeniedList, true)); return; } if (callback == null) { return; } // 用户全部授权了,回调成功给外层监听器,免得用户还要再发起权限申请 callback.onResult(requestList, latestDeniedList); } }); }; // 另外这里需要判断 Activity 的类型来申请权限,这是因为只有 AppCompatActivity 才能调用 AndroidX 库的 AlertDialog 来显示,否则会出现报错 // java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity // 为什么不直接用系统包 AlertDialog 来显示,而是两套规则?因为系统包 AlertDialog 是系统自带的类,不同 Android 版本展现的样式可能不太一样 // 如果这个 Android 版本比较低,那么这个对话框的样式就会变得很丑,准确来讲也不能说丑,而是当时系统的 UI 设计就是那样,它只是跟随系统的样式而已 Dialog dialog; if (activity instanceof AppCompatActivity) { dialog = new AlertDialog.Builder(activity) .setTitle(dialogTitle) .setMessage(permissionHint) // 这里需要设置成可取消的,这样用户不想授权了,随时可以点击返回键或者对话框蒙层来取消显示 Dialog .setCancelable(true) .setPositiveButton(confirmButtonText, confirmListener) .create(); } else { dialog = new Builder(activity) .setTitle(dialogTitle) .setMessage(permissionHint) // 这里需要设置成可取消的,这样用户不想授权了,随时可以点击返回键或者对话框蒙层来取消显示 Dialog .setCancelable(true) .setPositiveButton(confirmButtonText, confirmListener) .create(); } dialog.show(); // 将 Activity 和 Dialog 生命周期绑定在一起,避免可能会出现的内存泄漏 // 当然如果上面创建的 Dialog 已经有做了生命周期管理,则不需要执行下面这行代码 WindowLifecycleManager.bindDialogLifecycle(activity, dialog); } /** * 生成权限提示文案 */ @NonNull private String generatePermissionHint(@NonNull Activity activity, @NonNull List deniedList, boolean doNotAskAgain) { int deniedPermissionCount = deniedList.size(); int deniedLocationPermissionCount = 0; int deniedSensorsPermissionCount = 0; int deniedHealthPermissionCount = 0; for (IPermission deniedPermission : deniedList) { String permissionGroup = deniedPermission.getPermissionGroup(activity); if (TextUtils.isEmpty(permissionGroup)) { continue; } if (PermissionGroups.LOCATION.equals(permissionGroup)) { deniedLocationPermissionCount++; } else if (PermissionGroups.SENSORS.equals(permissionGroup)) { deniedSensorsPermissionCount++; } else if (XXPermissions.isHealthPermission(deniedPermission)) { deniedHealthPermissionCount++; } } if (deniedLocationPermissionCount == deniedPermissionCount && VERSION.SDK_INT >= VERSION_CODES.Q) { if (deniedLocationPermissionCount == 1) { if (XXPermissions.equalsPermission(deniedList.get(0), PermissionNames.ACCESS_BACKGROUND_LOCATION)) { return activity.getString(R.string.common_permission_fail_hint_1, activity.getString(R.string.common_permission_location_background), getBackgroundPermissionOptionLabel(activity)); } else if (VERSION.SDK_INT >= VERSION_CODES.S && XXPermissions.equalsPermission(deniedList.get(0), PermissionNames.ACCESS_FINE_LOCATION)) { // 如果请求的定位权限中,既包含了精确定位权限,又包含了模糊定位权限或者后台定位权限, // 但是用户只同意了模糊定位权限的情况或者后台定位权限,并没有同意精确定位权限的情况,就提示用户开启确切位置选项 // 需要注意的是 Android 12 才将模糊定位权限和精确定位权限的授权选项进行分拆,之前的版本没有区分得那么仔细 return activity.getString(R.string.common_permission_fail_hint_3, activity.getString(R.string.common_permission_location_fine), activity.getString(R.string.common_permission_location_fine_option)); } } else { if (XXPermissions.containsPermission(deniedList, PermissionNames.ACCESS_BACKGROUND_LOCATION)) { if (VERSION.SDK_INT >= VERSION_CODES.S && XXPermissions.containsPermission(deniedList, PermissionNames.ACCESS_FINE_LOCATION)) { return activity.getString(R.string.common_permission_fail_hint_2, activity.getString(R.string.common_permission_location), getBackgroundPermissionOptionLabel(activity), activity.getString(R.string.common_permission_location_fine_option)); } else { return activity.getString(R.string.common_permission_fail_hint_1, activity.getString(R.string.common_permission_location), getBackgroundPermissionOptionLabel(activity)); } } } } else if (deniedSensorsPermissionCount == deniedPermissionCount && VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { if (deniedPermissionCount == 1) { if (XXPermissions.equalsPermission(deniedList.get(0), PermissionNames.BODY_SENSORS_BACKGROUND)) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) { return activity.getString(R.string.common_permission_fail_hint_1, activity.getString(R.string.common_permission_health_data_background), activity.getString(R.string.common_permission_health_data_background_option)); } else { return activity.getString(R.string.common_permission_fail_hint_1, activity.getString(R.string.common_permission_body_sensors_background), getBackgroundPermissionOptionLabel(activity)); } } } else { if (doNotAskAgain) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) { return activity.getString(R.string.common_permission_fail_hint_1, activity.getString(R.string.common_permission_health_data), activity.getString(R.string.common_permission_allow_all_option)); } else { return activity.getString(R.string.common_permission_fail_hint_1, activity.getString(R.string.common_permission_body_sensors), getBackgroundPermissionOptionLabel(activity)); } } } } else if (deniedHealthPermissionCount == deniedPermissionCount && VERSION.SDK_INT >= VERSION_CODES.BAKLAVA) { switch (deniedPermissionCount) { case 1: if (XXPermissions.equalsPermission(deniedList.get(0), PermissionNames.READ_HEALTH_DATA_IN_BACKGROUND)) { return activity.getString(R.string.common_permission_fail_hint_3, activity.getString(R.string.common_permission_health_data_background), activity.getString(R.string.common_permission_health_data_background_option)); } else if (XXPermissions.equalsPermission(deniedList.get(0), PermissionNames.READ_HEALTH_DATA_HISTORY)) { return activity.getString(R.string.common_permission_fail_hint_3, activity.getString(R.string.common_permission_health_data_past), activity.getString(R.string.common_permission_health_data_past_option)); } break; case 2: if (XXPermissions.containsPermission(deniedList, PermissionNames.READ_HEALTH_DATA_HISTORY) && XXPermissions.containsPermission(deniedList, PermissionNames.READ_HEALTH_DATA_IN_BACKGROUND)) { return activity.getString(R.string.common_permission_fail_hint_3, activity.getString(R.string.common_permission_health_data_past) + activity.getString(R.string.common_permission_and) + activity.getString(R.string.common_permission_health_data_background), activity.getString(R.string.common_permission_health_data_past_option) + activity.getString(R.string.common_permission_and) + activity.getString(R.string.common_permission_health_data_background_option)); } else if (XXPermissions.containsPermission(deniedList, PermissionNames.READ_HEALTH_DATA_HISTORY)) { return activity.getString(R.string.common_permission_fail_hint_2, activity.getString(R.string.common_permission_health_data) + activity.getString(R.string.common_permission_and) + activity.getString(R.string.common_permission_health_data_past), activity.getString(R.string.common_permission_allow_all_option), activity.getString(R.string.common_permission_health_data_background_option)); } else if (XXPermissions.containsPermission(deniedList, PermissionNames.READ_HEALTH_DATA_IN_BACKGROUND)) { return activity.getString(R.string.common_permission_fail_hint_2, activity.getString(R.string.common_permission_health_data) + activity.getString(R.string.common_permission_and) + activity.getString(R.string.common_permission_health_data_background), activity.getString(R.string.common_permission_allow_all_option), activity.getString(R.string.common_permission_health_data_background_option)); } break; default: if (XXPermissions.containsPermission(deniedList, PermissionNames.READ_HEALTH_DATA_HISTORY) && XXPermissions.containsPermission(deniedList, PermissionNames.READ_HEALTH_DATA_IN_BACKGROUND)) { return activity.getString(R.string.common_permission_fail_hint_2, activity.getString(R.string.common_permission_health_data) + activity.getString(R.string.common_permission_and) + activity.getString(R.string.common_permission_health_data_past) + activity.getString(R.string.common_permission_and) + activity.getString(R.string.common_permission_health_data_background), activity.getString(R.string.common_permission_allow_all_option), activity.getString(R.string.common_permission_health_data_past_option) + activity.getString(R.string.common_permission_and) + activity.getString(R.string.common_permission_health_data_background_option)); } break; } return activity.getString(R.string.common_permission_fail_hint_1, activity.getString(R.string.common_permission_health_data), activity.getString(R.string.common_permission_allow_all_option)); } return activity.getString(doNotAskAgain ? R.string.common_permission_fail_assign_hint_1 : R.string.common_permission_fail_assign_hint_2, PermissionConverter.getNickNamesByPermissions(activity, deniedList)); } /** * 获取后台权限的《始终允许》选项的文案 */ @NonNull private String getBackgroundPermissionOptionLabel(Context context) { PackageManager packageManager = context.getPackageManager(); if (packageManager != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { CharSequence backgroundPermissionOptionLabel = packageManager.getBackgroundPermissionOptionLabel(); if (!TextUtils.isEmpty(backgroundPermissionOptionLabel)) { return backgroundPermissionOptionLabel.toString(); } } return context.getString(R.string.common_permission_allow_all_the_time_option); } } ================================================ FILE: app/src/main/res/drawable/permission_description_popup_bg.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_main.xml ================================================