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.
 
#### 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() {
......
});
```

#### 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);
```
 
 
* 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` 权限。
 
#### 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() {
......
});
```

#### 什么情况下需要适配分区存储特性
* 如果你的应用需要上架 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);
```
 
 
* 看到这里,我相信大家已经发现了一些差异,同样是跳转到安装 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

* 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)
  
  
  
  
  
  
 
#### 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 extends OnPermissionDescription> clazz);
// Set the permission request interceptor (Global setting)
XXPermissions.setPermissionInterceptor(Class extends OnPermissionInterceptor> 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://github.com/getActivity/XXPermissions/issues) | [](https://github.com/yanzhenjie/AndPermission/issues) | [](https://github.com/guolindev/PermissionX/issues) | [](https://github.com/Blankj/AndroidUtilCode/issues) | [](https://github.com/permissions-dispatcher/PermissionsDispatcher/issues) | [](https://github.com/tbruyelle/RxPermissions/issues) | [](https://github.com/googlesamples/easypermissions/issues) | [](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)
* Android middle office kt version: [AndroidProject-Kotlin](https://github.com/getActivity/AndroidProject-Kotlin)
* Toast framework: [Toaster](https://github.com/getActivity/Toaster)
* Network framework: [EasyHttp](https://github.com/getActivity/EasyHttp)
* Title bar framework: [TitleBar](https://github.com/getActivity/TitleBar)
* Floating window framework: [EasyWindow](https://github.com/getActivity/EasyWindow)
* Device compatibility framework:[DeviceCompat](https://github.com/getActivity/DeviceCompat)  
* Shape view framework: [ShapeView](https://github.com/getActivity/ShapeView)
* Shape drawable framework: [ShapeDrawable](https://github.com/getActivity/ShapeDrawable)
* Language switching framework: [Multi Languages](https://github.com/getActivity/MultiLanguages)
* Gson parsing fault tolerance: [GsonFactory](https://github.com/getActivity/GsonFactory)
* Logcat viewing framework: [Logcat](https://github.com/getActivity/Logcat)
* Nested scrolling layout framework:[NestedScrollLayout](https://github.com/getActivity/NestedScrollLayout)  
* Android version guide: [AndroidVersionAdapter](https://github.com/getActivity/AndroidVersionAdapter)
* Android code standard: [AndroidCodeStandard](https://github.com/getActivity/AndroidCodeStandard)
* Android resource summary:[AndroidIndex](https://github.com/getActivity/AndroidIndex)  
* Android open source leaderboard: [AndroidGithubBoss](https://github.com/getActivity/AndroidGithubBoss)
* Studio boutique plugins: [StudioPlugins](https://github.com/getActivity/StudioPlugins)
* Emoji collection: [EmojiPackage](https://github.com/getActivity/EmojiPackage)
* China provinces json: [ProvinceJson](https://github.com/getActivity/ProvinceJson)
* Markdown documentation:[MarkdownDoc](https://github.com/getActivity/MarkdownDoc)  
## 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)
# 权限请求框架

* 项目地址:[Github](https://github.com/getActivity/XXPermissions)
* 博文地址:[月下载 40 万次的框架是怎么练成的?](https://juejin.cn/post/7547408384585629711)
* 可以扫码下载 Demo 进行演示或者测试,如果扫码下载不了的,[点击此处可直接下载](https://github.com/getActivity/XXPermissions/releases/download/28.0/XXPermissions.apk)

  
  
  
  
  
  
  
#### 集成步骤
* 如果你的项目 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 extends OnPermissionDescription> clazz);
// 设置权限申请拦截器(全局设置)
XXPermissions.setPermissionInterceptor(Class extends OnPermissionInterceptor> 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://github.com/getActivity/XXPermissions/issues) | [](https://github.com/yanzhenjie/AndPermission/issues) | [](https://github.com/guolindev/PermissionX/issues) | [](https://github.com/Blankj/AndroidUtilCode/issues) | [](https://github.com/permissions-dispatcher/PermissionsDispatcher/issues) | [](https://github.com/tbruyelle/RxPermissions/issues) | [](https://github.com/googlesamples/easypermissions/issues) | [](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)  
* 安卓技术中台 Kt 版:[AndroidProject-Kotlin](https://github.com/getActivity/AndroidProject-Kotlin)  
* 吐司框架:[Toaster](https://github.com/getActivity/Toaster)  
* 网络框架:[EasyHttp](https://github.com/getActivity/EasyHttp)  
* 标题栏框架:[TitleBar](https://github.com/getActivity/TitleBar)  
* 悬浮窗框架:[EasyWindow](https://github.com/getActivity/EasyWindow)  
* 设备兼容框架:[DeviceCompat](https://github.com/getActivity/DeviceCompat)  
* ShapeView 框架:[ShapeView](https://github.com/getActivity/ShapeView)  
* ShapeDrawable 框架:[ShapeDrawable](https://github.com/getActivity/ShapeDrawable)  
* 语种切换框架:[MultiLanguages](https://github.com/getActivity/MultiLanguages)  
* Gson 解析容错:[GsonFactory](https://github.com/getActivity/GsonFactory)  
* 日志查看框架:[Logcat](https://github.com/getActivity/Logcat)  
* 嵌套滚动布局框架:[NestedScrollLayout](https://github.com/getActivity/NestedScrollLayout)  
* Android 版本适配:[AndroidVersionAdapter](https://github.com/getActivity/AndroidVersionAdapter)  
* Android 代码规范:[AndroidCodeStandard](https://github.com/getActivity/AndroidCodeStandard)  
* Android 资源大汇总:[AndroidIndex](https://github.com/getActivity/AndroidIndex)  
* Android 开源排行榜:[AndroidGithubBoss](https://github.com/getActivity/AndroidGithubBoss)  
* Studio 精品插件:[StudioPlugins](https://github.com/getActivity/StudioPlugins)  
* 表情包大集合:[EmojiPackage](https://github.com/getActivity/EmojiPackage)  
* AI 资源大汇总:[AiIndex](https://github.com/getActivity/AiIndex)  
* 省市区 Json 数据:[ProvinceJson](https://github.com/getActivity/ProvinceJson)  
* Markdown 语法文档:[MarkdownDoc](https://github.com/getActivity/MarkdownDoc)  
#### 微信公众号:Android轮子哥

#### Android 技术 Q 群:10047167
#### 如果您觉得我的开源库帮你节省了大量的开发时间,请扫描下方的二维码随意打赏,要是能打赏个 10.24 :monkey_face:就太:thumbsup:了。您的支持将鼓励我继续创作:octocat:([点击查看捐赠列表](https://github.com/getActivity/Donate))
 
## 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
================================================
================================================
FILE: app/src/main/res/layout/health_data_privacy_policy_activity.xml
================================================
================================================
FILE: app/src/main/res/layout/permission_description_popup.xml
================================================
================================================
FILE: app/src/main/res/values/colors.xml
================================================
#000000
#000000
#FFFFFF
================================================
FILE: app/src/main/res/values/strings_demo.xml
================================================
XXPermissions
Obtained %s successfully
Detected that you just returned from the permission settings interface
The current version is not Android 12 and above, the old version requires location permission to scan Bluetooth
The current version is not Android 13 and above, the old version needs location permission to scan WIFI
The current version is not Android 10 and above, the old version needs read storage permission to get media location permission
The current version is not Android 13 and above, it will automatically change to the old version of the request method
The current version is not Android 11 and above, it will automatically change to the old version of the request method
The current version is not Android 13 and above, it will automatically change to the old version of the request method
The current version is not Android 14 and above, it will automatically change to the old version of the request method
The current version is not Android 14 or above, and it will be replaced with sensor permissions to request
A new notification message was listened for with the title: %s and the content: %s
test
test notification channel name
Additional instructions for testing device management permissions
test description for accessibility services
Request single dangerous permission
Request multiple dangerous permission
Request location permission
Request fitness and wellness data permission
Request activity recognition permission
Request bluetooth permission
Request wifi permission
Request access media location information permission
Request media read permission
Request all file access permission
Request install unknown apps permission
Request display over other apps permission
Request modify system settings permission
Request allow notifications permission
Request allow send notifications permission (api 33)
Request allow notifications access permission
Request apps with usage access permission
Request alarms & reminders permission
Request do not disturb access permission
Request ignore battery optimize permission
Request picture-in-picture permission
Request vpn permission
Request full screen notifications permission
Request device admin permission
Request accessibility service permission
Request manage media permission
Request get installed application information permission
Request dangerous permission and special permission
Start app details activity
Health Data Privacy Policy
1. How We Collect and Use Your Health Data\n\n
Based on the permissions you granted in 「Health Data Sharing」 (e.g., step count, sleep, heart rate), application is only used to provide health management services. We will not use the data for unauthorized commercial purposes.\n\n
2. Data Storage and Protection\n\n
Your health data (e.g., medical treatment records, vaccination history) will be encrypted and stored with strict access restrictions to ensure data security.\n\n
3. Permission Changes and Withdrawal\n\n
You can adjust or disable health data access permissions at any time in 「App Permission Settings」, and we will immediately stop collecting and using the corresponding data.\n\n
4. Third-Party Cooperation Instructions\n\n
If involving third-party services (e.g., data analysis), we will ensure they follow the same privacy standards and will not disclose your sensitive health information (e.g., personal medical history, pregnancy status, etc.).\n\n
5. Minor Privacy Protection\n\n
If you are a minor, you need to authorize health data (e.g., height, weight related to growth and development) with the guardian’s consent. Guardians can assist in managing permissions.
================================================
FILE: app/src/main/res/values/strings_permission.xml
================================================
Permission description
Confirm
Authorization prompt
Failed to request permission, please grant \"%s\" manually
Failed to request permission, please agree to grant %s
Failed to request the %s,please select \"%s\" option
Failed to request the %s,please select \"%s\" option and enable the \"%s\" option
Failed to request the %s,please enable the \"%s\" option
Allow all
Allow all the time
Go to authorization
,
:
and
permission
you need to apply for this permission first before you can continue to use this service
calendar permission
allows creating, viewing, and managing schedules, such as syncing meeting reminders or automatically importing event times to keep your schedule updated.
camera permission
allows taking photos, recording videos, or scanning qr codes, such as uploading profile pictures, capturing moments, or quickly identifying information.
contacts permission
allows accessing contact information to quickly find friends, sync contacts to the app, or share content with contacts.
location permission
allows accessing real-time location to display nearby services, plan navigation routes, or recommend location-based content.
precise location permission
precise location
background location permission
allows continuous location access in the background for real-time track recording, background navigation, or emergency location sharing.
nearby devices permission
allows discovering and connecting to nearby devices (e.g., bluetooth, wi-fi) for wireless file transfer, smart device connection, or network sharing.
microphone permission
allows recording audio for voice chats, audio recordings, voice input, or voice recognition features.
phone permission
allows reading call status and phone numbers for caller id, quick dialing, or call statistics.
call logs permission
allows accessing call history for quick redialing, call duration tracking, or marking spam calls.
body sensors permission
allows accessing physiological data (e.g., heart rate, steps) for health monitoring, fitness tracking, or personalized health advice.
background body sensors permission
allows continuous physiological data access in the background for sleep monitoring, 24/7 health tracking, or emergency health alerts.
fitness and wellness data permission
@string/common_permission_body_sensors_description
background fitness and wellness data permission
Access fitness and wellness data in the background
@string/common_permission_body_sensors_background_description
past fitness and wellness data permission
Access past fitness and wellness data
access past fitness and health data to determine your current physical condition.
physical activity permission
physical activity permission
allows recognizing activities (e.g., walking, running) for automatic exercise mode switching, activity data statistics, or fitness plan creation.
access media location information permission
allows accessing the location metadata of photos and videos for organizing albums by location, displaying photo maps, or sharing media with location info.
sms permission
allows reading sms (e.g., verification codes), sending messages, or backing up sms records for automatic otp filling, bulk messaging, or sms archiving.
storage permission
allows reading and writing to device storage for saving images, downloading files, caching data, or backing up app settings.
send notification permission
allows sending message notifications (e.g., new messages, event alerts, important updates), with customizable notification types in settings.
image and video permission
allows accessing photos and videos in the gallery for uploading images, editing videos, or sharing media files within the app.
music and audio permission
allows accessing music and audio files on the device for playing local music, setting ringtones, or editing audio content.
get info about installed apps permission
allows accessing your installed apps to provide personalized recommendations. For example, suggesting related tools based on your installed apps or tailoring content to your app usage habits (your app list data is used anonymously and will not be shared with third parties).
all file access permission
@string/common_permission_storage_description
install unknown apps permission
allows the app to update itself automatically. When a new version is available, you can install it directly within the app without manually downloading from a browser or app store.
display over other apps permission
allows displaying content over other apps. Even when the app is in the background, it can show floating windows.
modify system settings permission
allows modifying system-level settings (e.g., volume, network mode, display settings).
allow notifications permission
@string/common_permission_post_notifications_description
allow notifications access permission
allows reading notification panel content for automatic otp recognition, notification summarization, or intelligent notification management.
apps with usage access permission
allows accessing app usage data (time and frequency) for screen time management, app usage statistics, or personalized recommendations.
alarms and reminders permission
allows creating and managing alarms and reminders for schedule alerts, task notifications, or recurring event reminders.
do not disturb access permission
allows managing do not disturb mode for automatic time switching, setting exception notifications, or temporarily blocking interruptions.
ignore battery optimize permission
allows keeping the app running in the background for real-time messaging, continuous location tracking, or background data sync.
picture-in-picture permission
allows enabling picture-in-picture mode for watching videos while using other apps, floating video windows, or multitasking.
full screen notifications permission
allows displaying notifications that occupy the entire screen when the device is locked, ensuring important information (e.g., incoming calls, emergency messages) is not missed. These notifications appear even when the device is locked.
device admin permission
allows device-level management functions (e.g., remote lock, data wipe, password policies).
accessibility service permission
allows assisting users with disabilities through features like voice announcements, gesture assistance, or automatic tapping. (please find and open it in the list of downloaded applications or services)
\tvpn\tpermission
allows creating and managing vpn connections for accessing internal networks, network proxy, or encrypted data transmission.
manage media permission
allows an application to modify and delete media files on this device or any connected storage device without user confirmation.
================================================
FILE: app/src/main/res/values/styles.xml
================================================
================================================
FILE: app/src/main/res/values-v21/styles.xml
================================================
================================================
FILE: app/src/main/res/values-zh/strings_demo.xml
================================================
XXPermissions
获取%s成功
检测到你刚刚从权限设置界面返回回来
当前版本不是 Android 12 及以上,旧版本的需要定位权限才能进行扫描蓝牙
当前版本不是 Android 13 及以上,旧版本的需要定位权限才能进行扫描 WIFI
当前版本不是 Android 10 及以上,旧版本的需要读取存储权限才能获取媒体位置权限
当前版本不是 Android 13 及以上,会自动变更为旧版的请求方式
当前版本不是 Android 11 及以上,会自动变更为旧版的请求方式
当前版本不是 Android 13 及以上,会自动变更为旧版的请求方式
当前版本不是 Android 14 及以上,会自动变更为旧版的请求方式
当前版本不是 Android 14 及以上,会更换成传感器权限来申请
监听到新的通知消息,标题为:%s,内容为:%s
test
用于测试的通知渠道名称
用于设备管理器权限的测试附加说明
用于无障碍服务的测试描述
申请单个危险权限
申请多个危险权限
申请定位权限
申请健康数据共享权限
申请身体活动权限
申请蓝牙权限
申请 WIFI 权限
申请访问媒体的位置信息权限
申请媒体文件读取权限
申请所有文件管理权限
申请安装包权限
申请悬浮窗权限
申请系统设置权限
申请通知权限
申请新版通知权限
申请通知栏监听权限
申请使用统计权限
申请闹钟提醒权限
申请勿扰权限
申请忽略电池优化权限
申请画中画权限
申请 VPN 权限
申请全屏通知权限
申请设备管理器权限
申请无障碍服务权限
申请管理媒体权限
申请读取应用列表
申请危险权限和特殊权限
跳转到应用详情页
健康数据隐私权政策
1. 我们如何收集和使用您的健康数据\n\n
基于您在「健康数据共享」中授予的权限(如步数、睡眠、心率等),应用仅用于为您提供健康管理服务。我们不会将数据用于未经授权的商业目的。\n\n
2. 数据存储与保护\n\n
您的健康数据(如就医情况、疫苗接种记录)将加密存储,严格限制访问权限,保障数据安全。\n\n
3. 权限变更与撤回\n\n
您可随时在「应用权限设置」中调整或关闭健康数据访问权限,我们将立即停止对应数据的收集与使用。\n\n
4. 第三方合作说明\n\n
若涉及第三方服务(如数据分析),我们会确保其遵守同等隐私标准,不会泄露您的敏感健康信息(如个人详细病史、怀孕情况等)。\n\n
5. 未成年人隐私保护\n\n
若您是未成年人,需在监护人同意下授权健康数据(如生长发育相关的身高、体重等),监护人可协助管理权限。
================================================
FILE: app/src/main/res/values-zh/strings_permission.xml
================================================
权限说明
确定
授权提醒
申请权限失败,请手动授予%s
申请权限失败,请正确授予%s
申请%s失败,请选择 \“%s\” 选项
申请%s失败,请选择 \“%s\” 并开启 \“%s\” 选项
申请%s失败,请开启 \“%s\” 选项
全部允许
始终允许
前往授权
、
:
、
权限
需要先申请该权限,才能继续使用该业务
日历权限
用于创建、查看和管理日程安排,例如同步会议提醒、自动导入活动时间,让您随时掌握行程动态。
相机权限
用于拍摄照片、录制视频或扫描二维码,例如上传头像、记录生活瞬间或快速识别信息。
通讯录权限
用于读取联系人信息,例如快速查找好友、同步联系人到应用或向通讯录好友分享内容。
定位权限
用于获取您的实时位置,例如显示附近的服务、导航路线规划或基于位置推荐内容。
精确定位权限
确切位置
后台定位权限
用于在后台持续获取位置信息,例如实时轨迹记录、后台导航或紧急情况下的位置共享。
附近设备权限
用于发现和连接蓝牙、Wi-Fi等附近设备,例如无线传输文件、连接智能设备或共享网络。
麦克风权限
用于录制声音,例如语音聊天、录制音频、语音输入或使用语音识别功能。
电话权限
用于读取通话状态和电话号码,例如来电自动识别、快速拨号或统计通话信息。
通话记录权限
用于读取通话历史,例如快速回拨号码、统计通话时长或标记骚扰电话。
身体传感器权限
用于获取心率、步数等生理数据,例如健康监测、运动记录或个性化健康建议。
后台身体传感器权限
用于在后台持续获取生理数据,例如睡眠监测、全天候健康追踪或紧急健康状况预警。
健康数据共享权限
@string/common_permission_body_sensors_description
后台健康数据共享权限
在后台访问健身与健康数据
@string/common_permission_body_sensors_background_description
过往的健身与健康数据权限
访问过往的健身与健康数据
访问过往的健身与健康数据以判断当前身体状态。
健身运动权限
身体活动权限
用于识别步行、跑步等运动状态,例如运动模式自动切换、运动数据统计或健身计划制定。
访问媒体的位置信息权限
用于获取照片、视频等媒体文件的拍摄位置,例如按地点整理相册、显示照片拍摄地图或分享带位置的媒体内容。
短信权限
用于读取短信内容(如验证码)、发送短信或备份短信记录,例如自动填充验证码、批量发送信息或短信归档。
存储权限
用于读取和写入手机存储,例如保存图片、下载文件、缓存数据或备份应用设置。
发送通知权限
用于向您发送消息通知,例如新消息提醒、活动通知或重要事项推送。
照片和视频权限
用于访问相册中的照片和视频,例如上传图片、编辑视频或分享媒体文件到应用。
音乐和音频权限
用于访问手机中的音乐和音频文件,例如播放本地音乐、设置铃声或编辑音频内容。
读取应用列表权限
用于获取已安装的应用信息,以提供个性化推荐。例如,根据您安装的应用推荐相关工具,或根据您的应用使用习惯定制内容(您的应用列表数据将匿名使用,不会分享给第三方)。
所有文件访问权限
@string/common_permission_storage_description
安装应用权限
用于应用自更新功能,当官方版本发布时可直接在App内下载并安装最新版本,避免手动到浏览器下载的繁琐。
悬浮窗权限
用于在其他应用上方显示内容,当应用处于后台状态时,依然能够展示悬浮窗口。
修改系统设置权限
用于修改系统级设置,例如调整音量、切换网络模式或自定义显示设置。
通知权限
@string/common_permission_post_notifications_description
通知栏监听权限
用于读取通知栏内容,例如自动识别验证码、汇总通知信息或智能管理通知提醒。
查看使用情况权限
用于获取应用使用时间和频率,例如屏幕时间管理、应用使用统计或个性化推荐。
闹钟和提醒权限
用于创建和管理闹钟、提醒事项,例如日程闹钟、任务提醒或周期性活动通知。
勿扰权限
用于管理手机勿扰模式,例如自动切换勿扰时段、设置例外通知或临时屏蔽干扰信息。
忽略电池优化权限
用于保持应用后台运行,例如实时接收消息、持续定位或后台数据同步。
画中画权限
用于开启画中画模式,例如边看视频边操作其他应用、悬浮视频窗口或多任务处理。
全屏通知权限
允许在设备锁定时显示占据整个屏幕空间的通知,确保重要信息(如来电、紧急消息)不会被错过,即使设备处于锁屏状态也能立即看到提醒。
设备管理器权限
用于提供设备级管理功能,例如远程锁定设备、擦除数据或强制密码策略。
无障碍服务权限
用于帮助残障用户使用手机,例如语音播报、手势辅助或自动点击。(请在已下载的应用或者服务列表中找到并开启)
\tVPN\t权限
用于创建和管理 VPN 连接,例如访问内网资源、网络代理或加密网络传输。
管理媒体权限
允许应用在无需用户确认的情况下修改和删除此设备或任何连接的存储设备上的媒体文件。
================================================
FILE: app/src/main/res/xml/accessibility_service_config.xml
================================================
================================================
FILE: app/src/main/res/xml/device_admin_config.xml
================================================
================================================
FILE: app/src/main/res/xml/locales_config.xml
================================================
================================================
FILE: build.gradle
================================================
plugins {
id 'com.android.application' version '8.4.2' apply false
id 'org.jetbrains.kotlin.android' version '1.6.21' apply false
id 'maven-publish'
}
tasks.register('clean', Delete) {
delete rootProject.buildDir
}
================================================
FILE: common.gradle
================================================
// 通用配置
android {
// 编译源码版本
compileSdk 36
defaultConfig {
// Android 版本适配指南:https://github.com/getActivity/AndroidVersionAdapter
targetSdk 36
versionCode 2800
versionName "28.0"
}
// 支持 Java JDK 8
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8
}
// 读取 local.properties 文件配置
def properties = new Properties()
def localPropertiesFile = rootProject.file("local.properties")
if (localPropertiesFile.exists()) {
localPropertiesFile.withInputStream { inputStream ->
properties.load(inputStream)
}
}
String buildDirPath = properties.getProperty("build.dir")
if (buildDirPath != null && buildDirPath != "") {
// 将构建文件统一输出到指定的目录下
project.buildDir = new File(buildDirPath, rootProject.name + "/build/${path.replaceAll(':', '/')}")
} else {
// 将构建文件统一输出到项目根目录下的 build 文件夹
project.buildDir = new File(rootDir, "build/${path.replaceAll(':', '/')}")
}
}
================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
zipStoreBase = GRADLE_USER_HOME
zipStorePath = wrapper/dists
distributionBase = GRADLE_USER_HOME
distributionPath = wrapper/dists
distributionUrl = https\://services.gradle.org/distributions/gradle-8.9-bin.zip
================================================
FILE: gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# Indicates the use of AndroidX
android.useAndroidX = true
# Indicates migration of third-party libraries to AndroidX
android.enableJetifier = true
================================================
FILE: gradlew
================================================
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# 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
#
# https://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.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
================================================
FILE: gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: jitpack.yml
================================================
jdk:
- openjdk17
================================================
FILE: library/build.gradle
================================================
apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
apply from : '../common.gradle'
android {
namespace 'com.hjq.permissions'
defaultConfig {
// 最低安装版本
minSdkVersion 17
// 框架的混淆配置
consumerProguardFiles 'proguard-permissions.pro'
}
libraryVariants.configureEach { variant ->
variant.packageLibraryProvider.configure { packageLib ->
// 剔除 META-INF 目录
packageLib.exclude 'META-INF/'
// 剔除 R.txt 文件
packageLib.exclude 'R.txt'
}
}
}
dependencies {
// noinspection GradleCompatible
implementation 'androidx.fragment:fragment:1.0.0'
// 设备兼容框架:https://github.com/getActivity/DeviceCompat
implementation 'com.github.getActivity:DeviceCompat:2.3'
}
publishing {
publications {
release(MavenPublication) {
groupId = 'com.github.getActivity'
artifactId = 'XXPermissions'
version = android.defaultConfig.versionName
// 指定 aar 文件的确切路径:https://cloud.tencent.com/developer/ask/sof/106381154
afterEvaluate {
artifact(bundleReleaseAar)
}
// 修正 Maven 插件在生成的 pom 文件没有携带 dependencies 信息的问题
// 用于测试 Gradle 命令:./gradlew publishToMavenLocal
// 相关 Github 地址:https://github.com/getActivity/XXPermissions/issues/371#issuecomment-3018735968
pom.withXml {
def dependencies = asNode().appendNode('dependencies')
configurations.implementation.allDependencies.each {
def dependency = dependencies.appendNode('dependency')
dependency.appendNode('groupId', it.group)
dependency.appendNode('artifactId', it.name)
dependency.appendNode('version', it.version)
dependency.appendNode('scope', 'runtime')
}
}
}
}
}
================================================
FILE: library/proguard-permissions.pro
================================================
# 禁止混淆 IStartActivityDelegate 和 IFragmentMethodNative 接口及实现类涉及的方法名称
# 这是因为框架拿 AndroidX 库中的 Fragment 和系统包下的 Fragment 实现了这两个接口,
# 这样做的目的是为了将不同 Fragment 抽象化,然后框架就可以不用关心和判断具体是哪个 Fragment 来申请权限,
# 但是那样做会出现一个问题,有人反馈在混淆后会报崩溃异常,经过排查后发现,外层传入的是普通的 Activity 对象,
# 而非 FragmentActivity 对象,框架会使用系统包下的 Fragment 来申请权限,从而引发了该问题,
# 如果外层传入的是 FragmentActivity 对象,框架会使用 AndroidX 库中的 Fragment 来申请权限,则不会出现该问题,
# 相关的 issue 地址:https://github.com/getActivity/XXPermissions/issues/371,
# 添加这个混淆规则是为了一件事,为了不让编译器混淆 AndroidX 库中的 Fragment 里面这些方法,
# 因为框架内部同时使用了系统包下的 Fragment 和 AndroidX 库中的 Fragment 来请求权限,
# 如果让编译器混淆了这些方法,会出现一个问题,那么就是 AndroidX 库中的 Fragment 这些方法虽然被混淆了,
# 但是系统包下的 Fragment 是系统类,是肯定不会被混淆的,那么就会导致调用的时候方法名对不上,从而报 AbstractMethodError,
# 要么不去混淆,要混淆就大家一起混淆,这样大家的方法名还能保持一致,调用方法的时候还能对得上(能找到对应的方法),
# 但问题就出在,有一方不能被混淆,这里的一方就是指系统包下的 Fragment 类,所以唯一的解决方案就是不混淆这些方法名。
-keepclassmembers interface com.hjq.permissions.start.IStartActivityDelegate {
;
}
-keepclassmembers interface com.hjq.permissions.fragment.IFragmentMethodNative {
;
}
# 禁止混淆 AndroidX 库中 Fragment 的 getActivity 方法名称
# 看到这里你可能会有一个疑问,我会在此处解答这个疑问:
# 前面对 IFragmentMethodNative 接口的混淆规则不是已经包含了 getActivity 方法?
# 在这里为什么还要单独添加一遍重复作用的混淆规则,这不是脱裤子放屁多此一举?
# 这是因为这个方法比较特殊,混淆编译的时候没有正确识别到这个方法,
# 从而导致把 AndroidX 库中 Fragment 类定义的 getActivity 方法给混淆了,
# 但是 IFragmentMethodNative 定义的 getActivity 方法没有给混淆;
# 这是因为 IFragmentMethodNative 接口定义是 A getActivity(),
# 而 AndroidX 库中 Fragment 类定义的是 FragmentActivity getActivity(),
# 如果不禁止混淆 Fragment 中 getActivity 方法名,会导致它被混淆掉。
-keepclassmembers class androidx.fragment.app.Fragment {
androidx.fragment.app.FragmentActivity getActivity();
}
================================================
FILE: library/src/main/AndroidManifest.xml
================================================
================================================
FILE: library/src/main/java/com/hjq/permissions/DefaultPermissionDescription.java
================================================
package com.hjq.permissions;
import android.app.Activity;
import androidx.annotation.NonNull;
import com.hjq.permissions.permission.base.IPermission;
import java.util.List;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/06/01
* desc : 权限说明默认实现
*/
final class DefaultPermissionDescription implements OnPermissionDescription {
@Override
public void askWhetherRequestPermission(@NonNull Activity activity,
@NonNull List requestList,
@NonNull Runnable continueRequestRunnable,
@NonNull Runnable breakRequestRunnable) {
// 继续执行请求任务
continueRequestRunnable.run();
}
@Override
public void onRequestPermissionStart(@NonNull Activity activity, @NonNull List requestList) {
// default implementation ignored
}
@Override
public void onRequestPermissionEnd(@NonNull Activity activity, @NonNull List requestList) {
// default implementation ignored
}
}
================================================
FILE: library/src/main/java/com/hjq/permissions/DefaultPermissionInterceptor.java
================================================
package com.hjq.permissions;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/06/09
* desc : 权限拦截器默认实现
*/
final class DefaultPermissionInterceptor implements OnPermissionInterceptor {}
================================================
FILE: library/src/main/java/com/hjq/permissions/OnPermissionCallback.java
================================================
package com.hjq.permissions;
import androidx.annotation.NonNull;
import com.hjq.permissions.permission.base.IPermission;
import java.util.List;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2018/06/15
* desc : 权限请求结果回调接口
*/
public interface OnPermissionCallback {
/**
* 权限请求结果回调
*
* @param grantedList 授予权限列表
* @param deniedList 拒绝权限列表
*/
void onResult(@NonNull List grantedList, @NonNull List deniedList);
}
================================================
FILE: library/src/main/java/com/hjq/permissions/OnPermissionDescription.java
================================================
package com.hjq.permissions;
import android.app.Activity;
import androidx.annotation.NonNull;
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 interface OnPermissionDescription {
/**
* 询问是否要发起权限请求
*
* @param requestList 请求的权限列表
* @param continueRequestRunnable 继续请求任务对象
* @param breakRequestRunnable 中断请求任务对象
*/
void askWhetherRequestPermission(@NonNull Activity activity,
@NonNull List requestList,
@NonNull Runnable continueRequestRunnable,
@NonNull Runnable breakRequestRunnable);
/**
* 权限请求开始
*
* @param requestList 请求的权限列表
*/
void onRequestPermissionStart(@NonNull Activity activity, @NonNull List requestList);
/**
* 权限请求结束
*
* @param requestList 请求的权限列表
*/
void onRequestPermissionEnd(@NonNull Activity activity, @NonNull List requestList);
}
================================================
FILE: library/src/main/java/com/hjq/permissions/OnPermissionInterceptor.java
================================================
package com.hjq.permissions;
import android.app.Activity;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hjq.permissions.core.PermissionRequestMainLogic;
import com.hjq.permissions.fragment.factory.PermissionFragmentFactory;
import com.hjq.permissions.permission.base.IPermission;
import java.util.List;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2020/12/26
* desc : 权限请求拦截器
*/
public interface OnPermissionInterceptor {
/**
* 发起权限申请(可在此处先弹 Dialog 再申请权限,如果用户已经授予权限,则不会触发此回调)
*
* @param requestList 请求的权限列表
* @param fragmentFactory 权限 Fragment 工厂类
* @param permissionDescription 权限描述器
* @param callback 权限申请回调
*/
default void onRequestPermissionStart(@NonNull Activity activity,
@NonNull List requestList,
@NonNull PermissionFragmentFactory, ?> fragmentFactory,
@NonNull OnPermissionDescription permissionDescription,
@Nullable OnPermissionCallback callback) {
dispatchPermissionRequest(activity, requestList, fragmentFactory, permissionDescription, callback);
}
/**
* 权限请求完成
*
* @param grantedList 授予权限列表
* @param deniedList 拒绝权限列表
* @param skipRequest 是否跳过了申请过程
* @param callback 权限申请回调
*/
default void onRequestPermissionEnd(@NonNull Activity activity, boolean skipRequest,
@NonNull List requestList,
@NonNull List grantedList,
@NonNull List deniedList,
@Nullable OnPermissionCallback callback) {
if (callback == null) {
return;
}
callback.onResult(grantedList, deniedList);
}
/**
* 派发权限请求
*
* @param requestList 请求的权限列表
* @param callback 权限申请回调
*/
default void dispatchPermissionRequest(@NonNull Activity activity,
@NonNull List requestList,
@NonNull PermissionFragmentFactory, ?> fragmentFactory,
@NonNull OnPermissionDescription permissionDescription,
@Nullable OnPermissionCallback callback) {
new PermissionRequestMainLogic(activity, requestList, fragmentFactory, this, permissionDescription, callback)
.request();
}
}
================================================
FILE: library/src/main/java/com/hjq/permissions/XXPermissions.java
================================================
package com.hjq.permissions;
import android.app.Activity;
import android.app.Fragment;
import android.content.Context;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import com.hjq.permissions.fragment.factory.PermissionFragmentFactory;
import com.hjq.permissions.fragment.factory.PermissionFragmentFactoryByAndroid;
import com.hjq.permissions.fragment.factory.PermissionFragmentFactoryByAndroidX;
import com.hjq.permissions.manifest.AndroidManifestParser;
import com.hjq.permissions.permission.PermissionChannel;
import com.hjq.permissions.permission.base.IPermission;
import com.hjq.permissions.start.StartActivityAgent;
import com.hjq.permissions.tools.PermissionApi;
import com.hjq.permissions.tools.PermissionChecker;
import com.hjq.permissions.tools.PermissionSettingPage;
import com.hjq.permissions.tools.PermissionUtils;
import java.util.ArrayList;
import java.util.List;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2018/06/15
* desc : Android 权限请求入口类
*/
@SuppressWarnings({"unused", "deprecation"})
public final class XXPermissions {
/** 权限设置页跳转请求码 */
public static final int REQUEST_CODE = 1024 + 1;
/** 权限申请拦截器的类型(全局生效) */
private static Class extends OnPermissionInterceptor> sPermissionInterceptorClass;
/** 权限请求描述器的类型(全局生效) */
private static Class extends OnPermissionDescription> sPermissionDescriptionClass;
/** 是否为检查模式(全局生效) */
private static Boolean sCheckMode;
/**
* 设置请求的对象
*
* @param context 当前 Activity,可以传入栈顶的 Activity
*/
public static XXPermissions with(@NonNull Context context) {
return new XXPermissions(context);
}
public static XXPermissions with(@NonNull Fragment fragment) {
return new XXPermissions(fragment);
}
public static XXPermissions with(@NonNull androidx.fragment.app.Fragment xFragment) {
return new XXPermissions(xFragment);
}
/**
* 设置是否开启错误检测模式(全局设置)
*/
public static void setCheckMode(boolean checkMode) {
sCheckMode = checkMode;
}
/**
* 设置权限申请拦截器(全局设置)
*/
public static void setPermissionInterceptor(Class extends OnPermissionInterceptor> clazz) {
sPermissionInterceptorClass = clazz;
}
/**
* 获取权限申请拦截器(全局)
*/
@NonNull
public static OnPermissionInterceptor getPermissionInterceptor() {
if (sPermissionInterceptorClass != null) {
try {
return sPermissionInterceptorClass.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
return new DefaultPermissionInterceptor();
}
/**
* 设置权限描述器(全局设置)
*
* 这里解释一下,为什么不开放普通对象,而是只开放 Class 对象,这是因为如果用普通对象,那么就会导致全局都复用这一个对象
* 而这个会带来一个后果,就是可能出现类内部字段的使用冲突,为了避免这一个问题,最好的解决方案是不去复用同一个对象
*/
public static void setPermissionDescription(Class extends OnPermissionDescription> clazz) {
sPermissionDescriptionClass = clazz;
}
/**
* 获取权限描述器(全局)
*/
@NonNull
public static OnPermissionDescription getPermissionDescription() {
if (sPermissionDescriptionClass != null) {
try {
return sPermissionDescriptionClass.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
return new DefaultPermissionDescription();
}
/** 申请的权限列表 */
@NonNull
private final List mRequestList = new ArrayList<>();
/** Context 对象 */
@Nullable
private final Context mContext;
/** 系统包下的 Fragment 对象 */
@Nullable
private Fragment mFragment;
/** AndroidX 库中的 Fragment 对象 */
@Nullable
private androidx.fragment.app.Fragment mXFragment;
/** 权限请求拦截器 */
@Nullable
private OnPermissionInterceptor mPermissionInterceptor;
/** 权限请求描述 */
@Nullable
private OnPermissionDescription mPermissionDescription;
/** 设置不检查 */
@Nullable
private Boolean mCheckMode;
private XXPermissions(@NonNull Context context) {
mContext = context;
}
private XXPermissions(@NonNull Fragment fragment) {
mFragment = fragment;
mContext = fragment.getActivity();
}
private XXPermissions(@NonNull androidx.fragment.app.Fragment xFragment) {
mXFragment = xFragment;
mContext = xFragment.getActivity();
}
/**
* 添加单个权限
*/
public XXPermissions permission(@NonNull IPermission permission) {
// 这种写法的作用:如果出现重复添加的权限,则以最后添加的权限为主
mRequestList.remove(permission);
mRequestList.add(permission);
return this;
}
/**
* 添加多个权限
*/
public XXPermissions permissions(@NonNull List permissions) {
if (permissions.isEmpty()) {
return this;
}
for (int i = 0; i < permissions.size(); i++) {
permission(permissions.get(i));
}
return this;
}
public XXPermissions permissions(@NonNull IPermission[] permissions) {
return permissions(PermissionUtils.asArrayList(permissions));
}
/**
* 设置权限请求拦截器
*/
public XXPermissions interceptor(@Nullable OnPermissionInterceptor permissionInterceptor) {
mPermissionInterceptor = permissionInterceptor;
return this;
}
/**
* 设置权限请求描述
*/
public XXPermissions description(@Nullable OnPermissionDescription permissionDescription) {
mPermissionDescription = permissionDescription;
return this;
}
/**
* 设置不触发错误检测机制
*/
public XXPermissions unchecked() {
mCheckMode = false;
return this;
}
/**
* 请求权限
*/
public void request(@Nullable OnPermissionCallback callback) {
if (mContext == null) {
return;
}
if (mPermissionInterceptor == null) {
mPermissionInterceptor = getPermissionInterceptor();
}
if (mPermissionDescription == null) {
mPermissionDescription = getPermissionDescription();
}
final Context context = mContext;
final Fragment fragment = mFragment;
final androidx.fragment.app.Fragment xFragment = mXFragment;
final OnPermissionInterceptor permissionInterceptor = mPermissionInterceptor;
final OnPermissionDescription permissionDescription = mPermissionDescription;
// 权限请求列表(为什么直接不用字段?因为框架要兼容新旧权限,在低版本下会自动添加旧权限申请,为了避免重复添加)
List requestList = new ArrayList<>(mRequestList);
// 从 Context 对象中获得 Activity 对象
Activity activity = PermissionUtils.findActivity(context);
if (isCheckMode(context)) {
// 检查传入的 Activity 或者 Fragment 状态是否正常
PermissionChecker.checkActivityStatus(activity);
if (fragment != null) {
PermissionChecker.checkAndroidFragmentStatus(fragment);
} else if (xFragment != null) {
PermissionChecker.checkAndroidXFragmentStatus(xFragment);
}
// 检查传入的权限是否正常
PermissionChecker.checkPermissionList(activity, requestList, AndroidManifestParser.getAndroidManifestInfo(context));
}
// 检查 Activity 是不是不可用
if (PermissionUtils.isActivityUnavailable(activity)) {
return;
}
// 优化所申请的权限列表
PermissionApi.addOldPermissionsByNewPermissions(activity, requestList);
// 判断要申请的权限是否都授予了
if (PermissionApi.isGrantedPermissions(context, requestList)) {
// 如果是的话,就不申请权限,而是通知权限申请成功
permissionInterceptor.onRequestPermissionEnd(activity, true, requestList, requestList, new ArrayList<>(), callback);
return;
}
final PermissionFragmentFactory, ?> fragmentFactory;
if (xFragment != null) {
if (PermissionUtils.isFragmentUnavailable(xFragment)) {
return;
}
fragmentFactory = generatePermissionFragmentFactory(activity, xFragment);
} else if (fragment != null) {
if (PermissionUtils.isFragmentUnavailable(fragment)) {
return;
}
fragmentFactory = generatePermissionFragmentFactory(activity, fragment);
} else {
fragmentFactory = generatePermissionFragmentFactory(activity);
}
// 申请没有授予过的权限
permissionInterceptor.onRequestPermissionStart(activity, requestList, fragmentFactory, permissionDescription, callback);
}
/**
* 当前是否为检测模式
*/
private boolean isCheckMode(@NonNull Context context) {
if (mCheckMode == null) {
if (sCheckMode == null) {
sCheckMode = PermissionUtils.isDebugMode(context);
}
mCheckMode = sCheckMode;
}
return mCheckMode;
}
/**
* 判断一个或多个权限是否全部授予了
*/
public static boolean isGrantedPermission(@NonNull Context context, @NonNull IPermission permission) {
return permission.isGrantedPermission(context);
}
public static boolean isGrantedPermissions(@NonNull Context context, @NonNull IPermission[] permissions) {
return isGrantedPermissions(context, PermissionUtils.asArrayList(permissions));
}
public static boolean isGrantedPermissions(@NonNull Context context, @NonNull List permissions) {
return PermissionApi.isGrantedPermissions(context, permissions);
}
/**
* 从权限列表中获取已授予的权限
*/
public static List getGrantedPermissions(@NonNull Context context, @NonNull IPermission[] permissions) {
return getGrantedPermissions(context, PermissionUtils.asArrayList(permissions));
}
public static List getGrantedPermissions(@NonNull Context context, @NonNull List permissions) {
return PermissionApi.getGrantedPermissions(context, permissions);
}
/**
* 从权限列表中获取没有授予的权限
*/
public static List getDeniedPermissions(@NonNull Context context, @NonNull IPermission[] permissions) {
return getDeniedPermissions(context, PermissionUtils.asArrayList(permissions));
}
public static List getDeniedPermissions(@NonNull Context context, @NonNull List permissions) {
return PermissionApi.getDeniedPermissions(context, permissions);
}
/**
* 判断两个权限是否相等
*/
public static boolean equalsPermission(@NonNull IPermission permission1, @NonNull IPermission permission2) {
return PermissionUtils.equalsPermission(permission1, permission2);
}
public static boolean equalsPermission(@NonNull IPermission permission1, @NonNull String permission2) {
return PermissionUtils.equalsPermission(permission1, permission2);
}
public static boolean equalsPermission(@NonNull String permissionName1, @NonNull String permission2) {
return PermissionUtils.equalsPermission(permissionName1, permission2);
}
/**
* 判断权限列表中是否包含某个权限
*/
public static boolean containsPermission(@NonNull List permissions, @NonNull IPermission permission) {
return PermissionUtils.containsPermission(permissions, permission);
}
public static boolean containsPermission(@NonNull List permissions, @NonNull String permissionName) {
return PermissionUtils.containsPermission(permissions, permissionName);
}
/**
* 判断某个权限是否为健康权限
*/
public static boolean isHealthPermission(@NonNull IPermission permission) {
return PermissionApi.isHealthPermission(permission);
}
/**
* 判断一个或多个权限是否被勾选了不再询问的选项
*
* 如果判断的权限中包含了危险权限,则需要特别注意:
* 2. 如果在应用启动后,没有向系统申请过这个危险权限,而是直接去判断它有没有勾选不再询问的选项,这样得到的结果是不准的,建议在权限回调方法中调用,除此之外没有更好的解决方法
* 3. 如果危险权限在申请的过程中,如果用户不是通过点击《不允许》选项来取消权限,而是通过点击返回键或者点击系统授权框外层区域来取消授权的,这样得到的结果是不准的,这个问题无解
*/
public static boolean isDoNotAskAgainPermission(@NonNull Activity activity, @NonNull IPermission permission) {
return permission.isDoNotAskAgainPermission(activity);
}
public static boolean isDoNotAskAgainPermissions(@NonNull Activity activity, @NonNull IPermission[] permissions) {
return isDoNotAskAgainPermissions(activity, PermissionUtils.asArrayList(permissions));
}
public static boolean isDoNotAskAgainPermissions(@NonNull Activity activity, @NonNull List permissions) {
return PermissionApi.isDoNotAskAgainPermissions(activity, permissions);
}
/* android.content.Context */
public static void startPermissionActivity(@NonNull Context context) {
startPermissionActivity(context, new ArrayList<>(0));
}
public static void startPermissionActivity(@NonNull Context context, @NonNull IPermission... permissions) {
startPermissionActivity(context, PermissionUtils.asArrayList(permissions));
}
/**
* 跳转到应用权限设置页
*
* @param permissions 没有授予或者被拒绝的权限组
*/
public static void startPermissionActivity(@NonNull Context context, @NonNull List permissions) {
Activity activity = PermissionUtils.findActivity(context);
if (activity != null) {
startPermissionActivity(activity, permissions);
return;
}
StartActivityAgent.startActivity(context, PermissionApi.getBestPermissionSettingIntent(context, permissions, true));
}
/* android.app.Activity */
public static void startPermissionActivity(@NonNull Activity activity) {
startPermissionActivity(activity, new ArrayList<>(0));
}
public static void startPermissionActivity(@NonNull Activity activity,
@NonNull IPermission... permissions) {
startPermissionActivity(activity, PermissionUtils.asArrayList(permissions));
}
public static void startPermissionActivity(@NonNull Activity activity,
@NonNull List permissions) {
startPermissionActivity(activity, permissions, REQUEST_CODE);
}
public static void startPermissionActivity(@NonNull Activity activity,
@NonNull List permissions,
@IntRange(from = 1, to = 65535) int requestCode) {
StartActivityAgent.startActivityForResult(activity,
PermissionApi.getBestPermissionSettingIntent(activity, permissions, true), requestCode);
}
public static void startPermissionActivity(@NonNull Activity activity,
@NonNull IPermission permission,
@Nullable OnPermissionCallback callback) {
startPermissionActivity(activity, PermissionUtils.asArrayList(permission), callback);
}
public static void startPermissionActivity(@NonNull Activity activity,
@NonNull List permissions,
@Nullable OnPermissionCallback callback) {
if (PermissionUtils.isActivityUnavailable(activity)) {
return;
}
if (permissions.isEmpty()) {
StartActivityAgent.startActivity(activity, PermissionSettingPage.getCommonPermissionSettingIntent(activity));
return;
}
PermissionFragmentFactory, ?> fragmentFactory = generatePermissionFragmentFactory(activity);
fragmentFactory.createAndCommitFragment(permissions, PermissionChannel.START_ACTIVITY, () -> {
if (PermissionUtils.isActivityUnavailable(activity)) {
return;
}
dispatchPermissionPageCallback(activity, permissions, callback);
});
}
/* android.app.Fragment */
public static void startPermissionActivity(@NonNull Fragment fragment) {
startPermissionActivity(fragment, new ArrayList<>(0));
}
public static void startPermissionActivity(@NonNull Fragment fragment,
@NonNull IPermission... permissions) {
startPermissionActivity(fragment, PermissionUtils.asArrayList(permissions));
}
public static void startPermissionActivity(@NonNull Fragment fragment,
@NonNull List permissions) {
startPermissionActivity(fragment, permissions, REQUEST_CODE);
}
public static void startPermissionActivity(@NonNull Fragment fragment,
@NonNull List permissions,
@IntRange(from = 1, to = 65535) int requestCode) {
if (PermissionUtils.isFragmentUnavailable(fragment)) {
return;
}
Activity activity = fragment.getActivity();
if (PermissionUtils.isActivityUnavailable(activity) || PermissionUtils.isFragmentUnavailable(fragment)) {
return;
}
if (permissions.isEmpty()) {
StartActivityAgent.startActivity(fragment, PermissionSettingPage.getCommonPermissionSettingIntent(activity));
return;
}
StartActivityAgent.startActivityForResult(fragment,
PermissionApi.getBestPermissionSettingIntent(activity, permissions, true), requestCode);
}
public static void startPermissionActivity(@NonNull Fragment fragment,
@NonNull IPermission permission,
@Nullable OnPermissionCallback callback) {
startPermissionActivity(fragment, PermissionUtils.asArrayList(permission), callback);
}
public static void startPermissionActivity(@NonNull Fragment fragment,
@NonNull List permissions,
@Nullable OnPermissionCallback callback) {
if (PermissionUtils.isFragmentUnavailable(fragment)) {
return;
}
Activity activity = fragment.getActivity();
if (PermissionUtils.isActivityUnavailable(activity) || PermissionUtils.isFragmentUnavailable(fragment)) {
return;
}
if (permissions.isEmpty()) {
StartActivityAgent.startActivity(fragment, PermissionSettingPage.getCommonPermissionSettingIntent(activity));
return;
}
PermissionFragmentFactory, ?> fragmentFactory = generatePermissionFragmentFactory(activity, fragment);
fragmentFactory.createAndCommitFragment(permissions, PermissionChannel.START_ACTIVITY, () -> {
if (PermissionUtils.isActivityUnavailable(activity) || PermissionUtils.isFragmentUnavailable(fragment)) {
return;
}
dispatchPermissionPageCallback(activity, permissions, callback);
});
}
/* androidx.fragment.app.Fragment */
public static void startPermissionActivity(@NonNull androidx.fragment.app.Fragment xFragment) {
startPermissionActivity(xFragment, new ArrayList<>());
}
public static void startPermissionActivity(@NonNull androidx.fragment.app.Fragment xFragment,
@NonNull IPermission... permissions) {
startPermissionActivity(xFragment, PermissionUtils.asArrayList(permissions));
}
public static void startPermissionActivity(@NonNull androidx.fragment.app.Fragment xFragment,
@NonNull List permissions) {
startPermissionActivity(xFragment, permissions, REQUEST_CODE);
}
public static void startPermissionActivity(@NonNull androidx.fragment.app.Fragment xFragment,
@NonNull List permissions,
@IntRange(from = 1, to = 65535) int requestCode) {
if (PermissionUtils.isFragmentUnavailable(xFragment)) {
return;
}
Activity activity = xFragment.getActivity();
if (PermissionUtils.isActivityUnavailable(activity) || PermissionUtils.isFragmentUnavailable(xFragment)) {
return;
}
if (permissions.isEmpty()) {
StartActivityAgent.startActivity(xFragment, PermissionSettingPage.getCommonPermissionSettingIntent(activity));
return;
}
StartActivityAgent.startActivityForResult(xFragment,
PermissionApi.getBestPermissionSettingIntent(activity, permissions, true), requestCode);
}
public static void startPermissionActivity(@NonNull androidx.fragment.app.Fragment xFragment,
@NonNull IPermission permission,
@Nullable OnPermissionCallback callback) {
startPermissionActivity(xFragment, PermissionUtils.asArrayList(permission), callback);
}
public static void startPermissionActivity(@NonNull androidx.fragment.app.Fragment xFragment,
@NonNull List permissions,
@Nullable OnPermissionCallback callback) {
if (PermissionUtils.isFragmentUnavailable(xFragment)) {
return;
}
Activity activity = xFragment.getActivity();
if (PermissionUtils.isActivityUnavailable(activity) || PermissionUtils.isFragmentUnavailable(xFragment)) {
return;
}
if (permissions.isEmpty()) {
StartActivityAgent.startActivity(xFragment, PermissionSettingPage.getCommonPermissionSettingIntent(activity));
return;
}
PermissionFragmentFactory, ?> fragmentFactory = generatePermissionFragmentFactory(activity, xFragment);
fragmentFactory.createAndCommitFragment(permissions, PermissionChannel.START_ACTIVITY, () -> {
if (PermissionUtils.isActivityUnavailable(activity) || PermissionUtils.isFragmentUnavailable(xFragment)) {
return;
}
dispatchPermissionPageCallback(activity, permissions, callback);
});
}
/**
* 创建 Fragment 工厂
*/
@NonNull
private static PermissionFragmentFactory, ?> generatePermissionFragmentFactory(@NonNull Activity activity) {
return generatePermissionFragmentFactory(activity, null, null);
}
@NonNull
private static PermissionFragmentFactory, ?> generatePermissionFragmentFactory(@NonNull Activity activity,
@Nullable androidx.fragment.app.Fragment xFragment) {
return generatePermissionFragmentFactory(activity, xFragment, null);
}
@NonNull
private static PermissionFragmentFactory, ?> generatePermissionFragmentFactory(@NonNull Activity activity,
@Nullable Fragment fragment) {
return generatePermissionFragmentFactory(activity, null, fragment);
}
private static PermissionFragmentFactory, ?> generatePermissionFragmentFactory(@NonNull Activity activity,
@Nullable androidx.fragment.app.Fragment xFragment,
@Nullable Fragment fragment) {
final PermissionFragmentFactory, ?> fragmentFactory;
if (xFragment != null) {
fragmentFactory = new PermissionFragmentFactoryByAndroidX(xFragment.getActivity(), xFragment.getChildFragmentManager());
} else if (fragment != null) {
fragmentFactory = new PermissionFragmentFactoryByAndroid(fragment.getActivity(), fragment.getChildFragmentManager());
} else if (activity instanceof FragmentActivity) {
FragmentActivity fragmentActivity = ((FragmentActivity) activity);
fragmentFactory = new PermissionFragmentFactoryByAndroidX(fragmentActivity, fragmentActivity.getSupportFragmentManager());
} else {
fragmentFactory = new PermissionFragmentFactoryByAndroid(activity, activity.getFragmentManager());
}
return fragmentFactory;
}
/**
* 派发权限设置页回调
*/
private static void dispatchPermissionPageCallback(@NonNull Context context,
@NonNull List permissions,
@Nullable OnPermissionCallback callback) {
if (callback == null) {
return;
}
List grantedList = new ArrayList<>(permissions.size());
List deniedList = new ArrayList<>(permissions.size());
// 遍历请求的权限,并且根据权限的授权状态进行分类
for (IPermission permission : permissions) {
if (permission.isGrantedPermission(context, false)) {
grantedList.add(permission);
} else {
deniedList.add(permission);
}
}
callback.onResult(grantedList, deniedList);
}
}
================================================
FILE: library/src/main/java/com/hjq/permissions/core/OnPermissionFragmentCallback.java
================================================
package com.hjq.permissions.core;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/05/30
* desc : 权限 Fragment 回调
*/
public interface OnPermissionFragmentCallback {
/**
* 权限请求时回调
*/
default void onRequestPermissionNow() {
// default implementation ignored
}
/**
* 权限请求完成回调
*/
void onRequestPermissionFinish();
/**
* 权限请求异常回调
*/
default void onRequestPermissionAnomaly() {
// default implementation ignored
}
}
================================================
FILE: library/src/main/java/com/hjq/permissions/core/PermissionChannelImpl.java
================================================
package com.hjq.permissions.core;
import android.app.Activity;
import android.os.Bundle;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.fragment.app.FragmentActivity;
import com.hjq.permissions.fragment.IFragmentCallback;
import com.hjq.permissions.fragment.IFragmentMethod;
import com.hjq.permissions.manager.ActivityOrientationManager;
import com.hjq.permissions.permission.base.IPermission;
import com.hjq.permissions.start.IStartActivityDelegate;
import com.hjq.permissions.tools.PermissionApi;
import com.hjq.permissions.tools.PermissionTaskHandler;
import com.hjq.permissions.tools.PermissionUtils;
import com.hjq.permissions.tools.PermissionVersion;
import java.util.List;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/05/20
* desc : 请求权限实现类
*/
public abstract class PermissionChannelImpl implements IFragmentCallback {
/** 请求的权限 */
public static final String REQUEST_PERMISSIONS = "request_permissions";
/** 请求码(自动生成)*/
public static final String REQUEST_CODE = "request_code";
/** 任务令牌 */
@NonNull
private final Object mTaskToken = new Object();
/** 非系统重启标记 */
private boolean mNonSystemRestartMark;
/** 权限请求是否已经发起 */
private boolean mAlreadyRequest;
/** 当前 Fragment 是否为手动解绑 */
private boolean mManualDetach;
/** Fragment 方法对象 */
@NonNull
private final IFragmentMethod, ?> mFragmentMethod;
/** 权限回调对象 */
@Nullable
private OnPermissionFragmentCallback mPermissionFragmentCallback;
protected PermissionChannelImpl(@NonNull IFragmentMethod, ?> fragmentMethod) {
mFragmentMethod = fragmentMethod;
}
public void setNonSystemRestartMark(boolean nonSystemRestartMark) {
mNonSystemRestartMark = nonSystemRestartMark;
}
public void setPermissionFragmentCallback(@Nullable OnPermissionFragmentCallback callback) {
mPermissionFragmentCallback = callback;
}
@Nullable
private OnPermissionFragmentCallback getPermissionFragmentCallback() {
return mPermissionFragmentCallback;
}
@Nullable
private Activity getActivity() {
return mFragmentMethod.getActivity();
}
private void commitFragmentDetach() {
mManualDetach = true;
mFragmentMethod.commitFragmentDetach();
}
private boolean isFragmentUnavailable() {
// 如果用户离开太久,会导致 Activity 被回收掉
// 所以这里要判断当前 Fragment 是否有被添加到 Activity
// 可在开发者模式中开启不保留活动来复现这个 Bug
return !mFragmentMethod.isAdded() || mFragmentMethod.isRemoving();
}
@RequiresApi(PermissionVersion.ANDROID_6)
protected void requestPermissions(@NonNull String[] permissions, @IntRange(from = 1, to = 65535) int requestCode) {
try {
mFragmentMethod.requestPermissions(permissions, requestCode);
} catch (Exception e1) {
// 在某些极端情况下,调用系统的 requestPermissions 方法时会出现崩溃,刚开始我还以为是 Android 6.0 以下的设备触发的 Bug,
// 结果发现 Android 6.0 及以上也有这个问题,你永远无法想象现实到底有多魔幻,经过分析得出结论,出现这种情况有以下几种可能:
// 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 成错误的,结果权限回调能正常回调。
// 如果真的出现这种极端情况,所有危险权限的申请必然会走失败的回调,但是框架要做的是:尽量让应用不要崩溃,并且能走完整个权限申请的流程。
// 涉及到此问题相关 Github issue 地址:
// 1. https://github.com/getActivity/XXPermissions/issues/153
// 2. https://github.com/getActivity/XXPermissions/issues/126
// 3. https://github.com/getActivity/XXPermissions/issues/327
// 4. https://github.com/getActivity/XXPermissions/issues/339
// 5. https://github.com/guolindev/PermissionX/issues/92
// 6. https://github.com/yanzhenjie/AndPermission/issues/72
// 7. https://github.com/yanzhenjie/AndPermission/issues/28
// 8. https://github.com/permissions-dispatcher/PermissionsDispatcher/issues/288
// 9. https://github.com/googlesamples/easypermissions/issues/342
// 10. https://github.com/HuanTanSheng/EasyPhotos/issues/256
// 11. https://github.com/oasisfeng/island/issues/67
// 12. https://github.com/Rakashazi/emu-ex-plus-alpha/issues/137
// 13. https://github.com/hyb1996-guest/AutoJsIssueReport/issues/1792
// 14. https://github.com/hyb1996-guest/AutoJsIssueReport/issues/1794
// 15. https://github.com/hyb1996-guest/AutoJsIssueReport/issues/1795
// 16. https://github.com/hyb1996-guest/AutoJsIssueReport/issues/2012
// 17. https://github.com/hyb1996-guest/AutoJsIssueReport/issues/18264
// android.content.ActivityNotFoundException: No Activity found to handle Intent
// { act=android.content.pm.action.REQUEST_PERMISSIONS pkg=com.android.packageinstaller (has extras) }
e1.printStackTrace();
Activity activity = mFragmentMethod.getActivity();
// 如果这个 Activity 是 FragmentActivity 类型的话,则不用 activity.requestPermissions 发起重试
// 这是因为如果外层传入的是 FragmentActivity 对象,则会创建的 AndroidX 库的 Fragment 对象,
// AndroidX 库的 Fragment 对象的 requestPermissions 方法,最终还是调用的 activity.requestPermissions,
// 所以当外层传入的是 FragmentActivity 对象时,就不进行重试,避免重复调用一次 activity.requestPermissions 方法,
// 另外你可能会有疑问,这样做不是太阳能手电筒?脱裤子纯放屁?实则不然,因为真的有人反馈过这种奇怪的情况,
// 调用 android.app.Fragment 对象的 requestPermissions 方法来申请权限在极少数机型上面可能会出现崩溃,
// 换成 ActivityCompat.requestPermissions 或者 activity.requestPermissions 就没有问题了,
// 目前猜测可能是某些厂商对 android.app.Fragment 这个类做了修改导致的,但是没有做严格的测试导致出现的 Bug,
// 所以换成 activity.requestPermissions 来发起重试,虽然这样做会导致权限请求和权限回调没有对应上的问题,
// 因为前面 fragment.requestPermissions 失败了就会先触发回调,这里再调用 activity.requestPermissions 就没有办法接收到回调,
// 但是针对这种极端场景处理就不可能 100% 完美,给厂商擦屁股也只能擦到这种程度了,当然最好的解决方案还是厂商自己修复这个问题。
// 相关问题 issue 地址:https://github.com/getActivity/XXPermissions/issues/339
if (activity instanceof FragmentActivity) {
return;
}
if (PermissionUtils.isActivityUnavailable(activity)) {
return;
}
try {
activity.requestPermissions(permissions, requestCode);
} catch (Exception e2) {
e2.printStackTrace();
}
}
}
@SuppressWarnings("deprecation")
@Nullable
protected List getPermissionRequestList() {
Bundle arguments = mFragmentMethod.getArguments();
if (arguments == null) {
return null;
}
if (PermissionVersion.isAndroid13()) {
return arguments.getParcelableArrayList(REQUEST_PERMISSIONS, IPermission.class);
} else {
return arguments.getParcelableArrayList(REQUEST_PERMISSIONS);
}
}
protected int getPermissionRequestCode() {
Bundle arguments = mFragmentMethod.getArguments();
if (arguments == null) {
return 0;
}
return arguments.getInt(REQUEST_CODE);
}
protected void sendTask(@NonNull Runnable runnable, long delayMillis) {
PermissionTaskHandler.sendTask(runnable, mTaskToken, delayMillis);
}
protected void cancelTask() {
PermissionTaskHandler.cancelTask(mTaskToken);
}
protected IStartActivityDelegate getStartActivityDelegate() {
return mFragmentMethod;
}
/**
* 开启权限请求
*/
protected abstract void startPermissionRequest(@NonNull Activity activity, @NonNull List permissions,
@IntRange(from = 1, to = 65535) int requestCode);
@Override
public void onFragmentResume() {
// 如果当前 Fragment 是通过系统重启应用触发的,则不进行权限申请
// 防止系统杀死应用后重新触发请求权限的问题
if (!mNonSystemRestartMark) {
mFragmentMethod.commitFragmentDetach();
return;
}
// 如果在 Activity 不可见的状态下添加 Fragment 并且去申请权限会导致授权对话框显示不出来
// 所以必须要在 Fragment 的 onResume 来申请权限,这样就可以保证应用回到前台的时候才去申请权限
if (mAlreadyRequest) {
return;
}
mAlreadyRequest = true;
Activity activity = getActivity();
// 检查 Activity 是不是不可用
if (PermissionUtils.isActivityUnavailable(activity)) {
return;
}
final int requestCode = getPermissionRequestCode();
if (requestCode <= 0) {
return;
}
List permissions = getPermissionRequestList();
if (permissions == null || permissions.isEmpty()) {
return;
}
startPermissionRequest(activity, permissions, requestCode);
OnPermissionFragmentCallback callback = getPermissionFragmentCallback();
if (callback == null) {
return;
}
callback.onRequestPermissionNow();
}
@Override
public void onFragmentDestroy() {
// 取消执行任务
cancelTask();
OnPermissionFragmentCallback callback = getPermissionFragmentCallback();
// 如果回调还没有置空,则证明前面没有回调权限回调完成
if (callback != null) {
// 告诉外层本次权限回调有异常
callback.onRequestPermissionAnomaly();
// 释放回调对象,避免内存泄漏
setPermissionFragmentCallback(null);
}
if (mManualDetach) {
return;
}
Activity activity = getActivity();
// 检查 Activity 是不是不可用
if (PermissionUtils.isActivityUnavailable(activity)) {
return;
}
// 如果不是手动解绑绑定,则证明是系统解除绑定,这里需要恢复 Activity 屏幕方向
// 如果是手动解除绑定,则会在所有的权限都申请完了之后恢复 Activity 屏幕方向
ActivityOrientationManager.unlockActivityOrientation(activity);
}
/**
* 通知权限回调
*/
protected void notificationPermissionCallback() {
Activity activity = getActivity();
if (PermissionUtils.isActivityUnavailable(activity)) {
return;
}
// 延迟处理权限请求的结果
sendTask(this::handlerPermissionCallback, PermissionApi.getMaxWaitTimeByPermissions(activity, getPermissionRequestList()));
}
/**
* 处理权限回调
*/
protected void handlerPermissionCallback() {
if (isFragmentUnavailable()) {
return;
}
Activity activity = getActivity();
if (PermissionUtils.isActivityUnavailable(activity)) {
return;
}
OnPermissionFragmentCallback callback = getPermissionFragmentCallback();
// 释放监听对象的引用
setPermissionFragmentCallback(null);
if (callback != null) {
callback.onRequestPermissionFinish();
}
// 将 Fragment 移除
commitFragmentDetach();
}
}
================================================
FILE: library/src/main/java/com/hjq/permissions/core/PermissionChannelImplByRequestPermissions.java
================================================
package com.hjq.permissions.core;
import android.app.Activity;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hjq.permissions.fragment.IFragmentMethod;
import com.hjq.permissions.manager.AlreadyRequestPermissionsManager;
import com.hjq.permissions.manager.PermissionRequestCodeManager;
import com.hjq.permissions.permission.base.IPermission;
import com.hjq.permissions.tools.PermissionUtils;
import com.hjq.permissions.tools.PermissionVersion;
import java.util.List;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/05/20
* desc : 请求权限实现类(通过 {@link android.app.Activity#requestPermissions(String[], int)} 实现)
*/
public final class PermissionChannelImplByRequestPermissions extends PermissionChannelImpl {
public PermissionChannelImplByRequestPermissions(@NonNull IFragmentMethod, ?> fragmentMethod) {
super(fragmentMethod);
}
@Override
protected void startPermissionRequest(@NonNull Activity activity,
@NonNull List permissions,
@IntRange(from = 1, to = 65535) int requestCode) {
if (!PermissionVersion.isAndroid6()) {
// 如果当前系统是 Android 6.0 以下,则没有危险权限的概念,则直接回调权限监听
sendTask(this::handlerPermissionCallback, 0);
return;
}
// 如果不需要的话就直接申请全部的危险权限
requestPermissions(PermissionUtils.convertPermissionArray(permissions), requestCode);
// 记录一下已申请过的权限(用于更加精准地判断用户是否勾选了《不再询问》)
AlreadyRequestPermissionsManager.addAlreadyRequestPermissions(permissions);
}
@Override
public void onFragmentRequestPermissionsResult(int requestCode, @Nullable String[] permissions, @Nullable int[] grantResults) {
// 如果回调中的请求码和请求时设置的请求码不一致,则证明回调有问题,则不往下执行代码
if (requestCode != getPermissionRequestCode()) {
return;
}
// 释放对这个请求码的占用
PermissionRequestCodeManager.releaseRequestCode(requestCode);
// 通知权限请求回调
notificationPermissionCallback();
}
}
================================================
FILE: library/src/main/java/com/hjq/permissions/core/PermissionChannelImplByStartActivity.java
================================================
package com.hjq.permissions.core;
import android.app.Activity;
import android.content.Intent;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hjq.permissions.fragment.IFragmentMethod;
import com.hjq.permissions.manager.AlreadyRequestPermissionsManager;
import com.hjq.permissions.manager.PermissionRequestCodeManager;
import com.hjq.permissions.permission.base.IPermission;
import com.hjq.permissions.start.StartActivityAgent;
import com.hjq.permissions.tools.PermissionApi;
import java.util.List;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/05/20
* desc : 请求权限实现类(通过 {@link android.app.Activity#startActivityForResult(Intent, int)} 实现)
*/
public final class PermissionChannelImplByStartActivity extends PermissionChannelImpl {
/** 忽略 onActivityResult 回调的总次数 */
private int mIgnoreActivityResultCount = 0;
public PermissionChannelImplByStartActivity(@NonNull IFragmentMethod, ?> fragmentMethod) {
super(fragmentMethod);
}
@Override
protected void startPermissionRequest(@NonNull Activity activity,
@NonNull List permissions,
@IntRange(from = 1, to = 65535) int requestCode) {
StartActivityAgent.startActivityForResult(activity, getStartActivityDelegate(),
PermissionApi.getBestPermissionSettingIntent(activity, permissions, false),
requestCode, () -> mIgnoreActivityResultCount++);
// 记录一下已申请过的权限
AlreadyRequestPermissionsManager.addAlreadyRequestPermissions(permissions);
}
@Override
public void onFragmentActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
// 如果回调中的请求码和请求时设置的请求码不一致,则证明回调有问题,则不往下执行代码
if (requestCode != getPermissionRequestCode()) {
return;
}
// 如果调用 startActivityForResult 出现跳转失败,框架会自动捕获跳转失败导致的 Exception,
// 这样做是为了以防应用程序出现崩溃,并且会自动拿下一个 Intent 进行重试,直到找到能跳转的 Intent 为止,
// 但是这样会出现一个问题,startActivityForResult 每次跳转失败就会导致系统触发一次 onActivityResult 回调,
// 这样就可能会出现触发多次 onActivityResult 回调的情况,从而导致权限实际还没有申请完,但是已经通知回调的尴尬局面,
// 在这种情况下只判断 requestCode 是否一样已经没有办法避免这个问题了,经过多轮思考,能想到一个比较好的解决方案,
// 就是要记录 startActivityForResult 跳转失败的次数,然后在 onActivityResult 回调中按次减掉,
// 也就是把那些 startActivityForResult 失败导致的回调给过滤掉,只有当这个次数减成 0 的时候,才能去回调权限请求的结果。
if (mIgnoreActivityResultCount > 0) {
mIgnoreActivityResultCount--;
return;
}
// 释放对这个请求码的占用
PermissionRequestCodeManager.releaseRequestCode(requestCode);
// 通知权限请求回调
notificationPermissionCallback();
}
}
================================================
FILE: library/src/main/java/com/hjq/permissions/core/PermissionRequestMainLogic.java
================================================
package com.hjq.permissions.core;
import android.app.Activity;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hjq.permissions.OnPermissionCallback;
import com.hjq.permissions.OnPermissionDescription;
import com.hjq.permissions.OnPermissionInterceptor;
import com.hjq.permissions.fragment.factory.PermissionFragmentFactory;
import com.hjq.permissions.manager.ActivityOrientationManager;
import com.hjq.permissions.permission.PermissionChannel;
import com.hjq.permissions.permission.base.IPermission;
import com.hjq.permissions.tools.PermissionApi;
import com.hjq.permissions.tools.PermissionTaskHandler;
import com.hjq.permissions.tools.PermissionUtils;
import com.hjq.permissions.tools.PermissionVersion;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2018/06/15
* desc : 权限申请主要逻辑实现类
*/
public final class PermissionRequestMainLogic {
@NonNull
private final Activity mActivity;
@NonNull
private final List mRequestList;
@NonNull
private final PermissionFragmentFactory, ?> mFragmentFactory;
@NonNull
private final OnPermissionInterceptor mPermissionInterceptor;
@NonNull
private final OnPermissionDescription mPermissionDescription;
@Nullable
private final OnPermissionCallback mCallBack;
public PermissionRequestMainLogic(@NonNull Activity activity,
@NonNull List requestList,
@NonNull PermissionFragmentFactory, ?> fragmentFactory,
@NonNull OnPermissionInterceptor permissionInterceptor,
@NonNull OnPermissionDescription permissionDescription,
@Nullable OnPermissionCallback callback) {
mActivity = activity;
mRequestList = requestList;
mFragmentFactory = fragmentFactory;
mPermissionInterceptor = permissionInterceptor;
mPermissionDescription = permissionDescription;
mCallBack = callback;
}
/**
* 开始权限请求
*/
public void request() {
if (mRequestList.isEmpty()) {
return;
}
List> unauthorizedList = getUnauthorizedList(mActivity, mRequestList);
if (unauthorizedList.isEmpty()) {
// 证明没有权限可以请求,直接处理权限请求结果
handlePermissionRequestResult();
return;
}
Iterator> iterator = unauthorizedList.iterator();
List firstPermissions = null;
while (iterator.hasNext() && (firstPermissions == null || firstPermissions.isEmpty())) {
firstPermissions = iterator.next();
}
if (firstPermissions == null || firstPermissions.isEmpty()) {
// 证明没有权限可以请求,直接处理权限请求结果
handlePermissionRequestResult();
return;
}
final Activity activity = mActivity;
final PermissionFragmentFactory, ?> fragmentFactory = mFragmentFactory;
final OnPermissionDescription permissionDescription = mPermissionDescription;
// 锁定 Activity 屏幕方向
ActivityOrientationManager.lockActivityOrientation(activity);
// 发起授权
requestPermissionsByFragment(activity, firstPermissions, fragmentFactory, permissionDescription, new Runnable() {
@Override
public void run() {
List nextPermissions = null;
while (iterator.hasNext()) {
nextPermissions = iterator.next();
if (nextPermissions == null || nextPermissions.isEmpty()) {
// 获取到的这个权限列表不符合要求,继续循环获取,虽然前面已经筛选过一波了,理论上不会走到这里来,但是为了代码的严谨性,这里还是要加一下判断
continue;
}
// 这里解释一下为什么之前已经判断过权限是否授予了,还要在这里再次判断,这难道不是多此一举吗?主要为了适配几种极端场景:
// 1. 用户发起相机权限和悬浮窗权限申请,在系统弹出相机权限授权框的时候,用户并没有授予,而是搞了一个骚操作,
// 直接跑去系统设置中,找到当前 App 的应用详情页的悬浮窗权限选项,然后就直接授予悬浮窗权限,最后再回到 App 上面,
// 此时系统还在傻傻等待用户授予相机权限,等用户授予了相机权限后,此时框架下一个申请的权限就是悬浮窗权限,
// 但是前面用户已经授予了悬浮窗权限,如果不在这里再次判断权限是否授予,就会导致一个问题,框架仍会跳转到悬浮窗设置页。
// 2. 偶然的一次测试,发现在 Android 12 的模拟器上面申请前台定位权限(包含模糊定位和精确定位)和后台定位权限有一个问题,
// 这个问题就是用户在授予定位权限的时候,故意选中《大致位置》(系统默认是帮你选中《确切位置》),这样前台定位权限其实不算申请成功,
// 这是因为选中《大致位置》会导致精确定位权限没有授予,反之则不会,如果用户选中的是《确切位置》,则精确定位权限和模糊定位权限会是都授予的状态,
// 此时下一个权限是后台定位权限,框架会引导用户去往权限设置页中重新授予定位权限,此时骚操作又来了,用户找到定位权限的选项,然后点进去,
// 此时用户勾选了《始终允许》选项,但是故意不勾选《使用确切位置》选项,然后返回到 App 中,此时用户回到 App 中重新发起权限申请,
// 这个时候系统会弹授权框让用户从《大致位置》更改为《确切位置》,然后用户选中《更改为切确切位置》,此时前台定位权限就算是申请完成了,
// 此时下一个权限就是后台定位权限了,但是前面说了用户勾选过了《始终允许》选项,如果不在这里再次判断权限是否授予,就会导致一个问题,
// 框架仍会发起一次新的权限申请,如果后台定位权限使用权限描述器的时候,是采用 Dialog 询问是否发起权限请求,就会出现一个奇怪的现象,
// App 用 Dialog 询问了用户要不要发起权限,结果用户选了《是》,但是实际上后台定位权限已经授予了,此时系统不会弹出任何授权框,而是告诉用户授权成功。
// 总结:之所以会出现这个问题,是因为第一个请求的权限列表中间不会有延迟,用户根本没有机会干其他事情,所以还能相信权限还是处于没有授予的状态,
// 但是到了第二个要请求的权限列表情况就复杂多了,因为你永远想不到用户在前面申请第一个权限列表的时候,那段时间干了什么骚操作。
if (PermissionApi.isGrantedPermissions(activity, nextPermissions)) {
// 将下一个要请求权限列表置空,表示不会请求它
nextPermissions = null;
// 上面的权限列表不符合请求的要求,继续循环获取
continue;
}
// 如果代码走到这里来,则证明下一个请求的权限列表是符合要求的,这里使用 break 跳出循环,然后进行下一步操作(权限请求)
break;
}
if (nextPermissions == null || nextPermissions.isEmpty()) {
// 证明请求已经全部完成,延迟发送权限处理结果
postDelayedHandlerRequestPermissionsResult();
return;
}
// 获取第下一次要申请权限列表中的首个权限
IPermission firstNextPermission = nextPermissions.get(0);
// 如果下一个请求的权限是后台权限
if (firstNextPermission.isBackgroundPermission(activity)) {
List foregroundPermissions = firstNextPermission.getForegroundPermissions(activity);
boolean grantedForegroundPermission = false;
// 如果这个后台权限对应的前台权限没有申请成功,则不要去申请后台权限,因为申请了也没有用,系统肯定不会给通过的
// 如果这种情况下还硬要去申请,等下还可能会触发权限说明弹窗,但是没有实际去申请权限的情况
if (foregroundPermissions != null && !foregroundPermissions.isEmpty()) {
for (IPermission foregroundPermission : foregroundPermissions) {
if (!foregroundPermission.isGrantedPermission(activity)) {
continue;
}
// 所有的前台权限中,只要有任一一个授权了,就算它是前台权限是申请通过的
grantedForegroundPermission = true;
}
} else {
// 如果某个权限是后台权限,但是没有返回它对应的前台权限,就默认它的前台权限是已经授予状态,然后申请后台权限
grantedForegroundPermission = true;
}
if (!grantedForegroundPermission) {
// 如果前台权限没有授予,就不去申请后台权限,直接进行下一轮申请
this.run();
return;
}
}
final List finalPermissions = nextPermissions;
int maxWaitTime = PermissionApi.getMaxIntervalTimeByPermissions(activity, nextPermissions);
if (maxWaitTime == 0) {
requestPermissionsByFragment(activity, finalPermissions, fragmentFactory, permissionDescription, this);
} else {
PermissionTaskHandler.sendTask(() ->
requestPermissionsByFragment(activity, finalPermissions, fragmentFactory, permissionDescription, this), maxWaitTime);
}
}
});
}
/**
* 获取未授权的权限列表
*/
@NonNull
private static List> getUnauthorizedList(@NonNull Activity activity, @NonNull List requestList) {
// 需要请求的权限列表
List> unauthorizedList = new ArrayList<>(requestList.size());
// 已处理的权限列表
List alreadyDoneList = new ArrayList<>(requestList.size());
// 遍历需要请求的权限列表
for (int i = 0; i < requestList.size(); i++) {
IPermission permission = requestList.get(i);
// 如果这个权限在前面已经处理过了,就不再处理
if (PermissionUtils.containsPermission(alreadyDoneList, permission)) {
continue;
}
alreadyDoneList.add(permission);
// 如果这个权限不支持申请,就不纳入申请的范围内
if (!permission.isSupportRequestPermission(activity)) {
continue;
}
// 如果这个权限已授权,就不纳入申请的范围内
if (permission.isGrantedPermission(activity)) {
continue;
}
// ------------ 下面是需要 startActivityForResult 才能授权的权限(一般为特殊权限)逻辑 ------------------ //
if (permission.getPermissionChannel(activity) == PermissionChannel.START_ACTIVITY) {
// 如果这是一个需要跳转页面才能授权的权限,那么就作为单独的一次权限进行处理
unauthorizedList.add(PermissionUtils.asArrayList(permission));
continue;
}
// ------------ 下面是需要 requestPermissions 才能授权的权限(一般为危险权限)逻辑 ------------------ //
// 查询危险权限所在的权限组类型
String permissionGroup = permission.getPermissionGroup(activity);
if (TextUtils.isEmpty(permissionGroup)) {
// 如果权限组为空,则证明这个权限被没有被定义权限组,就直接单独做为一次权限申请
unauthorizedList.add(PermissionUtils.asArrayList(permission));
continue;
}
List todoPermissions = null;
for (int j = i; j < requestList.size(); j++) {
IPermission todoPermission = requestList.get(j);
// 如果遍历到的权限对象不是同一个组别的,就继续找
if (!PermissionUtils.equalsString(todoPermission.getPermissionGroup(activity), permissionGroup)) {
continue;
}
// 判断当前权限是否支持申请
if (!todoPermission.isSupportRequestPermission(activity)) {
// 如果这个权限不支持申请,就不往下执行
continue;
}
// 判断要申请的权限是否授予了
if (todoPermission.isGrantedPermission(activity)) {
// 如果这个权限已经授予,就不往下执行
// Github issue 地址:https://github.com/getActivity/XXPermissions/issues/369
continue;
}
// 如果待处理的权限列表还没有初始化,就先进行初始化操作
if (todoPermissions == null) {
todoPermissions = new ArrayList<>();
}
// 添加到待处理的权限列表中
todoPermissions.add(todoPermission);
// 如果这个危险权限在前面已经处理过了,就不再添加
if (PermissionUtils.containsPermission(alreadyDoneList, todoPermission)) {
continue;
}
// 添加到已处理的权限列表中
alreadyDoneList.add(todoPermission);
}
// 如果这个待处理的权限列表为空,证明剩余的权限是在高版本系统才会出现,这里无需再次发起申请
if (todoPermissions == null || todoPermissions.isEmpty()) {
continue;
}
// 如果这个待处理的权限列表已经全部授权,就不纳入申请的范围内
if (PermissionApi.isGrantedPermissions(activity, todoPermissions)) {
continue;
}
// 判断申请的权限组是否包含后台权限(例如后台定位权限,后台传感器权限),如果有的话,不能在一起申请,需要进行拆分申请
List backgroundPermissions = null;
Iterator iterator = todoPermissions.iterator();
while (iterator.hasNext()) {
IPermission todoPermission = iterator.next();
// 先判断这个权限是不是后台权限,如果不是就继续找
if (!todoPermission.isBackgroundPermission(activity)) {
continue;
}
// 将后台权限拎出来放到另外一个集合中,然后作为单独的一次权限请求
iterator.remove();
backgroundPermissions = new ArrayList<>();
backgroundPermissions.add(todoPermission);
// 任务完成,跳过循环
break;
}
List foregroundPermissions = todoPermissions;
// 添加前台权限(前提得是没有授权)
if (!foregroundPermissions.isEmpty()) {
unauthorizedList.add(foregroundPermissions);
}
// 添加后台权限(前提得是没有授权)
if (backgroundPermissions != null && !backgroundPermissions.isEmpty()) {
unauthorizedList.add(backgroundPermissions);
}
}
return unauthorizedList;
}
/**
* 通过 Fragment 发起授权
*/
private static void requestPermissionsByFragment(@NonNull Activity activity,
@NonNull List permissions,
@NonNull PermissionFragmentFactory, ?> fragmentFactory,
@NonNull OnPermissionDescription permissionDescription,
@NonNull Runnable finishRunnable) {
if (permissions.isEmpty()) {
finishRunnable.run();
return;
}
PermissionChannel permissionChannel = PermissionChannel.REQUEST_PERMISSIONS;
for (IPermission permission : permissions) {
if (permission.getPermissionChannel(activity) == PermissionChannel.REQUEST_PERMISSIONS) {
continue;
}
permissionChannel = PermissionChannel.START_ACTIVITY;
break;
}
if (!PermissionVersion.isAndroid6() && permissionChannel == PermissionChannel.REQUEST_PERMISSIONS) {
// 如果是 Android 6.0 以下,则不能用 requestPermissions 来请求权限,所以直接跳过本次的权限请求,然后继续下一轮的权限请求
finishRunnable.run();
return;
}
PermissionChannel finalPermissionChannel = permissionChannel;
Runnable continueRequestRunnable = () ->
fragmentFactory.createAndCommitFragment(permissions, finalPermissionChannel, new OnPermissionFragmentCallback() {
@Override
public void onRequestPermissionNow() {
permissionDescription.onRequestPermissionStart(activity, permissions);
}
@Override
public void onRequestPermissionFinish() {
permissionDescription.onRequestPermissionEnd(activity, permissions);
finishRunnable.run();
}
@Override
public void onRequestPermissionAnomaly() {
permissionDescription.onRequestPermissionEnd(activity, permissions);
}
});
permissionDescription.askWhetherRequestPermission(activity, permissions, continueRequestRunnable, finishRunnable);
}
/**
* 延迟处理权限请求结果
*/
private void postDelayedHandlerRequestPermissionsResult() {
PermissionTaskHandler.sendTask(this::handlePermissionRequestResult, 100);
}
/**
* 延迟解锁 Activity 方向
*/
private void postDelayedUnlockActivityOrientation(@NonNull Activity activity) {
// 延迟执行是为了让外层回调中的代码能够顺序执行完成
PermissionTaskHandler.sendTask(() -> ActivityOrientationManager.unlockActivityOrientation(activity), 100);
}
/**
* 处理权限请求结果
*/
private void handlePermissionRequestResult() {
final Activity activity = mActivity;
final List requestList = mRequestList;
// 如果当前 Activity 不可用,就不继续往下执行代码
if (PermissionUtils.isActivityUnavailable(activity)) {
return;
}
List grantedList = new ArrayList<>(requestList.size());
List deniedList = new ArrayList<>(requestList.size());
// 遍历请求的权限,并且根据权限的授权状态进行分类
for (IPermission permission : requestList) {
if (permission.isGrantedPermission(activity, false)) {
grantedList.add(permission);
} else {
deniedList.add(permission);
}
}
// 权限申请结束
mPermissionInterceptor.onRequestPermissionEnd(activity, false, requestList, grantedList, deniedList, mCallBack);
// 延迟解锁 Activity 屏幕方向
postDelayedUnlockActivityOrientation(activity);
}
}
================================================
FILE: library/src/main/java/com/hjq/permissions/fragment/IFragmentCallback.java
================================================
package com.hjq.permissions.fragment;
import android.content.Intent;
import androidx.annotation.Nullable;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/05/20
* desc : Fragment 回调接口
*/
public interface IFragmentCallback {
/** Fragment 可见时回调 */
void onFragmentResume();
/** Fragment 解绑时回调 */
void onFragmentDestroy();
/** Fragment onRequestPermissionsResult 方法回调 */
default void onFragmentRequestPermissionsResult(int requestCode, @Nullable String[] permissions, @Nullable int[] grantResults) {
// default implementation ignored
}
/** Fragment onActivityResult 方法回调 */
default void onFragmentActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
// default implementation ignored
}
}
================================================
FILE: library/src/main/java/com/hjq/permissions/fragment/IFragmentMethod.java
================================================
package com.hjq.permissions.fragment;
import android.app.Activity;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/05/20
* desc : Fragment 接口方法
*/
public interface IFragmentMethod extends IFragmentMethodNative, IFragmentMethodExtension {}
================================================
FILE: library/src/main/java/com/hjq/permissions/fragment/IFragmentMethodExtension.java
================================================
package com.hjq.permissions.fragment;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hjq.permissions.core.OnPermissionFragmentCallback;
import com.hjq.permissions.core.PermissionChannelImpl;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/05/20
* desc : Fragment 扩展接口方法
*/
public interface IFragmentMethodExtension {
/**
* 获取权限请求通道的实现逻辑
*/
@NonNull
PermissionChannelImpl getPermissionChannelImpl();
/**
* 提交 Fragment 绑定
*/
void commitFragmentAttach(@Nullable M fragmentManager);
/**
* 提交 Fragment 解绑
*/
void commitFragmentDetach();
/**
* 设置权限请求流程回调
*/
void setPermissionFragmentCallback(@Nullable OnPermissionFragmentCallback callback);
/**
* 设置非系统重启标记
*/
void setNonSystemRestartMark(boolean nonSystemRestartMark);
}
================================================
FILE: library/src/main/java/com/hjq/permissions/fragment/IFragmentMethodNative.java
================================================
package com.hjq.permissions.fragment;
import android.app.Activity;
import android.os.Bundle;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hjq.permissions.start.IStartActivityDelegate;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/05/20
* desc : Fragment 原生接口方法
*/
public interface IFragmentMethodNative extends IStartActivityDelegate {
/** 获得 Activity 对象 */
@Nullable
A getActivity();
/** 请求权限 */
void requestPermissions(@NonNull String[] permissions, @IntRange(from = 1, to = 65535) int requestCode);
/** 获得参数集 */
@Nullable
Bundle getArguments();
/** 设置参数集 */
void setArguments(@Nullable Bundle arguments);
/** 设置是否保存实例,如果设置保存,则不会因为屏幕方向或配置变化而重新创建 */
void setRetainInstance(boolean retainInstance);
/** 当前 Fragment 是否已添加绑定 */
boolean isAdded();
/** 当前 Fragment 是否已移除 */
boolean isRemoving();
}
================================================
FILE: library/src/main/java/com/hjq/permissions/fragment/factory/PermissionFragmentFactory.java
================================================
package com.hjq.permissions.fragment.factory;
import android.app.Activity;
import android.os.Bundle;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hjq.permissions.core.OnPermissionFragmentCallback;
import com.hjq.permissions.core.PermissionChannelImpl;
import com.hjq.permissions.permission.PermissionChannel;
import com.hjq.permissions.permission.base.IPermission;
import java.util.ArrayList;
import java.util.List;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/05/20
* desc : 权限 Fragment 生产工厂
*/
public abstract class PermissionFragmentFactory {
/*
* 这里解释一下为什么要用抽象工厂模式来创建 Fragment,而不是沿用之前版本做法,直接用系统包下的 Fragment 来申请权限
*
* 问题一:直接使用系统包下的 Fragment,调用 fragment.requestPermissions 在极少数机型可能会出现崩溃,经过多轮排查最终确定问题原因
* 这是由于厂商修改了系统源码导致的 Bug,经过验证发现 ActivityCompat.requestPermissions(最终调用的是 activity.requestPermissions) 没问题
* 如果来连 activity.requestPermissions 申请权限都有问题,我也就没有办法了,负责这个功能改动的厂商的开发人员和测试人员可能要被公司打包送去祭天了
* 针对这个问题比较好的解决方案是:假设外层在 XXPermissions.with 传入的是 FragmentActivity 或者 AndroidX 库的 Fragment 对象
* 则换成 AndroidX 库中的 Fragment 来申请权限,其他情况则用系统包下的 Fragment 来申请权限,最大限度规避此类问题
* 相关的 Github issue:
* 1. https://github.com/getActivity/XXPermissions/issues/339
* 2. https://github.com/getActivity/XXPermissions/issues/126
* 3. https://github.com/getActivity/XXPermissions/issues/357
*
* 问题二:直接使用系统包下的 Fragment 来获取权限回调结果,如果外层是在 AndroidX 库 Fragment 发起的权限申请,在权限回调时
* 假设此时 AndroidX 库 Fragment 对象已销毁,但是仍然会触发回调给外层,这是因为不同 Fragment 对象的生命周期会不同步
* 这样就会就会导致权限回调给状态不正常的 Fragment,如果外层没有在权限回调中先对 Fragment 状态进行判断,就往下写代码逻辑
* 那么此时很可能会出现崩溃:java.lang.IllegalStateException: Fragment XxxFragment not attached to a context
* 针对这个问题比较好的解决方案是:对外层传入的宿主对象进行判断,然后创建依附于宿主的 Fragment 对象,以达到生命周期绑定的效果
* 1. 如果外层传入的宿主类型是 FragmentActivity,则创建 AndroidX 库中的 Fragment 对象,并且绑定到 FragmentActivity 对象上面
* 2. 如果外层传入的宿主类型是 Activity,则创建系统包下的 Fragment 对象,并且绑定到 Activity 对象上面
* 3. 如果外层传入的宿主类型是 AndroidX 库的 Fragment,则创建相同类型的 Fragment 对象,并且作为子 Fragment 绑定到父 Fragment 上面
* 4. 如果外层传入的宿主类型是系统包的 Fragment,则创建相同类型的 Fragment 对象,并且作为子 Fragment 绑定到父 Fragment 上面
* 相关的 Github issue:https://github.com/getActivity/XXPermissions/issues/365
*/
@NonNull
private final A mActivity;
@NonNull
private final M mFragmentManager;
protected PermissionFragmentFactory(@NonNull A activity, @NonNull M fragmentManager) {
mActivity = activity;
mFragmentManager = fragmentManager;
}
/**
* 获得 Activity 对象
*/
@NonNull
protected A getActivity() {
return mActivity;
}
/**
* 获得 FragmentManager 对象
*/
@NonNull
protected M getFragmentManager() {
return mFragmentManager;
}
/**
* 创建并提交 Fragment
*/
public abstract void createAndCommitFragment(@NonNull List permissions,
@NonNull PermissionChannel permissionChannel,
@Nullable OnPermissionFragmentCallback callback);
/**
* 生成权限请求的参数
*/
@NonNull
protected Bundle generatePermissionArguments(@NonNull List permissions, @IntRange(from = 1, to = 65535) int requestCode) {
Bundle bundle = new Bundle();
bundle.putInt(PermissionChannelImpl.REQUEST_CODE, requestCode);
if (permissions instanceof ArrayList) {
bundle.putParcelableArrayList(PermissionChannelImpl.REQUEST_PERMISSIONS, (ArrayList) permissions);
} else {
bundle.putParcelableArrayList(PermissionChannelImpl.REQUEST_PERMISSIONS, new ArrayList<>(permissions));
}
return bundle;
}
}
================================================
FILE: library/src/main/java/com/hjq/permissions/fragment/factory/PermissionFragmentFactoryByAndroid.java
================================================
package com.hjq.permissions.fragment.factory;
import android.app.Activity;
import android.app.FragmentManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hjq.permissions.core.OnPermissionFragmentCallback;
import com.hjq.permissions.fragment.IFragmentMethod;
import com.hjq.permissions.fragment.impl.android.PermissionAndroidFragmentByRequestPermissions;
import com.hjq.permissions.fragment.impl.android.PermissionAndroidFragmentByStartActivity;
import com.hjq.permissions.manager.PermissionRequestCodeManager;
import com.hjq.permissions.permission.PermissionChannel;
import com.hjq.permissions.permission.base.IPermission;
import java.util.List;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/05/20
* desc : 权限 Fragment 生产工厂( {@link android.app.Fragment} )
*/
@SuppressWarnings("deprecation")
public final class PermissionFragmentFactoryByAndroid extends PermissionFragmentFactory {
public PermissionFragmentFactoryByAndroid(@NonNull Activity activity, @NonNull FragmentManager fragmentManager) {
super(activity, fragmentManager);
}
@Override
public void createAndCommitFragment(@NonNull List permissions,
@NonNull PermissionChannel permissionChannel,
@Nullable OnPermissionFragmentCallback callback) {
IFragmentMethod fragment;
if (permissionChannel == PermissionChannel.REQUEST_PERMISSIONS) {
fragment = new PermissionAndroidFragmentByRequestPermissions();
} else {
fragment = new PermissionAndroidFragmentByStartActivity();
}
int maxRequestCode = PermissionRequestCodeManager.REQUEST_CODE_LIMIT_HIGH_VALUE;
int requestCode = PermissionRequestCodeManager.generateRandomRequestCode(maxRequestCode);
fragment.setArguments(generatePermissionArguments(permissions, requestCode));
fragment.setRetainInstance(true);
fragment.setNonSystemRestartMark(true);
fragment.setPermissionFragmentCallback(callback);
fragment.commitFragmentAttach(getFragmentManager());
}
}
================================================
FILE: library/src/main/java/com/hjq/permissions/fragment/factory/PermissionFragmentFactoryByAndroidX.java
================================================
package com.hjq.permissions.fragment.factory;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import com.hjq.permissions.core.OnPermissionFragmentCallback;
import com.hjq.permissions.fragment.IFragmentMethod;
import com.hjq.permissions.fragment.impl.androidx.PermissionAndroidXFragmentByRequestPermissions;
import com.hjq.permissions.fragment.impl.androidx.PermissionAndroidXFragmentByStartActivity;
import com.hjq.permissions.manager.PermissionRequestCodeManager;
import com.hjq.permissions.permission.PermissionChannel;
import com.hjq.permissions.permission.base.IPermission;
import java.util.List;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/05/20
* desc : 权限 Fragment 生产工厂( {@link androidx.fragment.app.Fragment} )
*/
public final class PermissionFragmentFactoryByAndroidX extends PermissionFragmentFactory {
public PermissionFragmentFactoryByAndroidX(@NonNull FragmentActivity activity, @NonNull FragmentManager fragmentManager) {
super(activity, fragmentManager);
}
@Override
public void createAndCommitFragment(@NonNull List permissions,
@NonNull PermissionChannel permissionChannel,
@Nullable OnPermissionFragmentCallback callback) {
IFragmentMethod fragment;
if (permissionChannel == PermissionChannel.REQUEST_PERMISSIONS) {
fragment = new PermissionAndroidXFragmentByRequestPermissions();
} else {
fragment = new PermissionAndroidXFragmentByStartActivity();
}
// 新版本的 AndroidX 库限制请求码必须小于 65536(不能包含 65536),所以实际的取值区间在:1 ~ 65535
// java.lang.IllegalArgumentException: Can only use lower 16 bits for requestCode
// 旧版本的 Support 库限制请求码必须小于 256(不能包含 256),所以实际的取值区间在:1 ~ 255
// java.lang.IllegalArgumentException: Can only use lower 8 bits for requestCode
// 相关问题地址:
// 1. https://stackoverflow.com/questions/33331073/android-what-to-choose-for-requestcode-values
// 2. https://github.com/domoticz/domoticz-android/issues/92
// 3. https://github.com/journeyapps/zxing-android-embedded/issues/117
int maxRequestCode;
// 判断当前是不是通过 requestPermissions 申请的权限
if (permissionChannel == PermissionChannel.REQUEST_PERMISSIONS) {
try {
FragmentActivity activity = getActivity();
// 检查一下大值的 requestCode 会不会超过 FragmentActivity 类中的设定
// 如果是,则证明当前的 Support 的版本是比较旧的,如果不是,则证明当前的 Support 的版本不是很旧
// 因为新版的 Support 版本已经纠错了这个问题,将 requestCode 最大值限制从 255 已经调整到了 65535
// 相关 Commit 地址:
// Github:https://github.com/androidx/androidx/commit/86f3b80ddf7f9aa5c5b7afe77217cb75632d62a2
// Google Git:https://android.googlesource.com/platform/frameworks/support/+/86f3b80ddf7f9aa5c5b7afe77217cb75632d62a2
activity.validateRequestPermissionsRequestCode(PermissionRequestCodeManager.REQUEST_CODE_LIMIT_HIGH_VALUE);
// 如果能安全走完 validateRequestPermissionsRequestCode 的调用,则证明了对传入 65535 的值是在限制范围内或者是没有限制的
maxRequestCode = PermissionRequestCodeManager.REQUEST_CODE_LIMIT_HIGH_VALUE;
} catch (IllegalArgumentException ignore) {
// 当 requestCode 的值超过了 FragmentActivity 类中的设定,会触发此异常报错,这里进行了捕获
// 则证明了对传入的 requestCode 的值是有限制的,并且只能用小一点的值,处理方案是将 maxRequestCode 换成小一点的值
// java.lang.IllegalArgumentException: Can only use lower 8 bits for requestCode
maxRequestCode = PermissionRequestCodeManager.REQUEST_CODE_LIMIT_LOW_VALUE;
} catch (Exception ignore) {
// 如果是其他的异常报错,很有可能是 validateRequestPermissionsRequestCode 这个 API 从 FragmentActivity 删除了
// 证明了 FragmentActivity 对传入的 requestCode 的值没有进行限制,所以 maxRequestCode 换成大一点的值
// 但是上面说的没有限制,并不是真的没有限制,而是 FragmentActivity 中没有限制,但是 Activity 本身就是有限制的
// Activity 的 requestPermissions 传入的 requestCode 参数都不能超过 65535,否则会无法申请权限
maxRequestCode = PermissionRequestCodeManager.REQUEST_CODE_LIMIT_HIGH_VALUE;
}
} else {
// 如果是特殊权限则没有这个限制,因为特殊权限是通过 startActivityForResult 实现的
// 新旧 Support 版本的 FragmentActivity 源码都对 startActivityForResult 传入的 requestCode 值没有限制
// 但是上面说的没有限制,并不是真的没有限制,而是 FragmentActivity 中没有限制,但是 Activity 本身就是有限制的
// Activity 的 startActivityForResult 传入的 requestCode 参数 requestCode 不能超过 65535,否则会无法进行页面跳转
maxRequestCode = PermissionRequestCodeManager.REQUEST_CODE_LIMIT_HIGH_VALUE;
}
int requestCode = PermissionRequestCodeManager.generateRandomRequestCode(maxRequestCode);
fragment.setArguments(generatePermissionArguments(permissions, requestCode));
fragment.setRetainInstance(true);
fragment.setNonSystemRestartMark(true);
fragment.setPermissionFragmentCallback(callback);
fragment.commitFragmentAttach(getFragmentManager());
}
}
================================================
FILE: library/src/main/java/com/hjq/permissions/fragment/impl/android/PermissionAndroidFragment.java
================================================
package com.hjq.permissions.fragment.impl.android;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hjq.permissions.core.OnPermissionFragmentCallback;
import com.hjq.permissions.fragment.IFragmentMethod;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/05/20
* desc : 权限 Fragment 类( {@link android.app.Fragment} )
*/
@SuppressWarnings("deprecation")
public abstract class PermissionAndroidFragment extends Fragment implements IFragmentMethod {
@Override
public void setPermissionFragmentCallback(@Nullable OnPermissionFragmentCallback callback) {
getPermissionChannelImpl().setPermissionFragmentCallback(callback);
}
@Override
public void setNonSystemRestartMark(boolean nonSystemRestartMark) {
getPermissionChannelImpl().setNonSystemRestartMark(nonSystemRestartMark);
}
@Override
public void commitFragmentAttach(@Nullable FragmentManager fragmentManager) {
if (fragmentManager == null) {
return;
}
fragmentManager.beginTransaction().add(this, this.toString()).commitAllowingStateLoss();
}
@Override
public void commitFragmentDetach() {
FragmentManager fragmentManager = getFragmentManager();
if (fragmentManager == null) {
return;
}
fragmentManager.beginTransaction().remove(this).commitAllowingStateLoss();
}
@Override
public void onResume() {
super.onResume();
getPermissionChannelImpl().onFragmentResume();
}
@Override
public void onDestroy() {
super.onDestroy();
getPermissionChannelImpl().onFragmentDestroy();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
getPermissionChannelImpl().onFragmentRequestPermissionsResult(requestCode, permissions, grantResults);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
getPermissionChannelImpl().onFragmentActivityResult(requestCode, resultCode, data);
}
}
================================================
FILE: library/src/main/java/com/hjq/permissions/fragment/impl/android/PermissionAndroidFragmentByRequestPermissions.java
================================================
package com.hjq.permissions.fragment.impl.android;
import androidx.annotation.NonNull;
import com.hjq.permissions.core.PermissionChannelImpl;
import com.hjq.permissions.core.PermissionChannelImplByRequestPermissions;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/05/20
* desc : 权限 Fragment 类( {@link android.app.Fragment} + {@link android.app.Activity#requestPermissions(String[], int)} )
*/
public final class PermissionAndroidFragmentByRequestPermissions extends PermissionAndroidFragment {
@NonNull
private final PermissionChannelImpl mPermissionChannelImpl = new PermissionChannelImplByRequestPermissions(this);
@NonNull
@Override
public PermissionChannelImpl getPermissionChannelImpl() {
return mPermissionChannelImpl;
}
}
================================================
FILE: library/src/main/java/com/hjq/permissions/fragment/impl/android/PermissionAndroidFragmentByStartActivity.java
================================================
package com.hjq.permissions.fragment.impl.android;
import android.content.Intent;
import androidx.annotation.NonNull;
import com.hjq.permissions.core.PermissionChannelImpl;
import com.hjq.permissions.core.PermissionChannelImplByStartActivity;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/05/20
* desc : 权限 Fragment 类( {@link android.app.Fragment} + {@link android.app.Activity#startActivityForResult(Intent, int)} )
*/
public final class PermissionAndroidFragmentByStartActivity extends PermissionAndroidFragment {
@NonNull
private final PermissionChannelImpl mPermissionChannelImpl = new PermissionChannelImplByStartActivity(this);
@NonNull
@Override
public PermissionChannelImpl getPermissionChannelImpl() {
return mPermissionChannelImpl;
}
}
================================================
FILE: library/src/main/java/com/hjq/permissions/fragment/impl/androidx/PermissionAndroidXFragment.java
================================================
package com.hjq.permissions.fragment.impl.androidx;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import com.hjq.permissions.core.OnPermissionFragmentCallback;
import com.hjq.permissions.fragment.IFragmentMethod;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/05/20
* desc : 权限 Fragment 类( {@link androidx.fragment.app.Fragment} )
*/
public abstract class PermissionAndroidXFragment extends Fragment implements IFragmentMethod {
/**
* 设置回调对象
*/
@Override
public void setPermissionFragmentCallback(@Nullable OnPermissionFragmentCallback callback) {
getPermissionChannelImpl().setPermissionFragmentCallback(callback);
}
/**
* 设置非系统重启标记
*/
@Override
public void setNonSystemRestartMark(boolean nonSystemRestartMark) {
getPermissionChannelImpl().setNonSystemRestartMark(nonSystemRestartMark);
}
/**
* 提交 Fragment 绑定
*/
@Override
public void commitFragmentAttach(@Nullable FragmentManager fragmentManager) {
if (fragmentManager == null) {
return;
}
fragmentManager.beginTransaction().add(this, this.toString()).commitAllowingStateLoss();
}
/**
* 提交 Fragment 解绑
*/
@Override
public void commitFragmentDetach() {
FragmentManager fragmentManager = getFragmentManager();
if (fragmentManager == null) {
return;
}
fragmentManager.beginTransaction().remove(this).commitAllowingStateLoss();
}
@Override
public void onResume() {
super.onResume();
getPermissionChannelImpl().onFragmentResume();
}
@Override
public void onDestroy() {
super.onDestroy();
getPermissionChannelImpl().onFragmentDestroy();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
getPermissionChannelImpl().onFragmentRequestPermissionsResult(requestCode, permissions, grantResults);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
getPermissionChannelImpl().onFragmentActivityResult(requestCode, resultCode, data);
}
}
================================================
FILE: library/src/main/java/com/hjq/permissions/fragment/impl/androidx/PermissionAndroidXFragmentByRequestPermissions.java
================================================
package com.hjq.permissions.fragment.impl.androidx;
import androidx.annotation.NonNull;
import com.hjq.permissions.core.PermissionChannelImpl;
import com.hjq.permissions.core.PermissionChannelImplByRequestPermissions;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/05/20
* desc : 权限 Fragment 类( {@link androidx.fragment.app.Fragment} + {@link android.app.Activity#requestPermissions(String[], int)} )
*/
public final class PermissionAndroidXFragmentByRequestPermissions extends PermissionAndroidXFragment {
@NonNull
private final PermissionChannelImpl mPermissionChannelImpl = new PermissionChannelImplByRequestPermissions(this);
@NonNull
@Override
public PermissionChannelImpl getPermissionChannelImpl() {
return mPermissionChannelImpl;
}
}
================================================
FILE: library/src/main/java/com/hjq/permissions/fragment/impl/androidx/PermissionAndroidXFragmentByStartActivity.java
================================================
package com.hjq.permissions.fragment.impl.androidx;
import android.content.Intent;
import androidx.annotation.NonNull;
import com.hjq.permissions.core.PermissionChannelImpl;
import com.hjq.permissions.core.PermissionChannelImplByStartActivity;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/05/20
* desc : 权限 Fragment 类( {@link androidx.fragment.app.Fragment} + {@link android.app.Activity#startActivityForResult(Intent, int)} )
*/
public final class PermissionAndroidXFragmentByStartActivity extends PermissionAndroidXFragment {
@NonNull
private final PermissionChannelImpl mPermissionChannelImpl = new PermissionChannelImplByStartActivity(this);
@NonNull
@Override
public PermissionChannelImpl getPermissionChannelImpl() {
return mPermissionChannelImpl;
}
}
================================================
FILE: library/src/main/java/com/hjq/permissions/manager/ActivityOrientationManager.java
================================================
package com.hjq.permissions.manager;
import android.app.Activity;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.view.Display;
import android.view.Surface;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import com.hjq.permissions.tools.PermissionVersion;
import java.util.HashMap;
import java.util.Map;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/05/20
* desc : Activity 屏幕方向管理类
*/
public final class ActivityOrientationManager {
/** 存放 Activity 屏幕方向集合 */
private static final Map ACTIVITY_ORIENTATION_MAP = new HashMap<>();
/** 私有化构造函数 */
private ActivityOrientationManager() {
// default implementation ignored
}
/**
* 锁定 Activity 方向
*/
public static synchronized void lockActivityOrientation(@NonNull Activity activity) {
// 如果当前没有锁定屏幕方向就获取当前屏幕方向并进行锁定
int sourceScreenOrientation = activity.getRequestedOrientation();
if (sourceScreenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
return;
}
int targetScreenOrientation;
// 锁定当前 Activity 方向
try {
// 兼容问题:在 Android 8.0 的手机上可以固定 Activity 的方向,但是这个 Activity 不能是透明的,否则就会抛出异常
// 复现场景:只需要给 Activity 主题设置 - true
属性即可
switch (activity.getResources().getConfiguration().orientation) {
case Configuration.ORIENTATION_LANDSCAPE:
targetScreenOrientation = isActivityReverse(activity) ?
ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE :
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
activity.setRequestedOrientation(targetScreenOrientation);
ACTIVITY_ORIENTATION_MAP.put(getIntKeyByActivity(activity), targetScreenOrientation);
break;
case Configuration.ORIENTATION_PORTRAIT:
targetScreenOrientation = isActivityReverse(activity) ?
ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT :
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
activity.setRequestedOrientation(targetScreenOrientation);
ACTIVITY_ORIENTATION_MAP.put(getIntKeyByActivity(activity), targetScreenOrientation);
break;
default:
break;
}
} catch (IllegalStateException e) {
// java.lang.IllegalStateException: Only fullscreen activities can request orientation
e.printStackTrace();
}
}
/**
* 解锁 Activity 方向
*/
public static synchronized void unlockActivityOrientation(@NonNull Activity activity) {
// 如果当前 Activity 没有锁定,就直接返回
if (activity.getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
return;
}
Integer targetScreenOrientation = ACTIVITY_ORIENTATION_MAP.get(getIntKeyByActivity(activity));
if (targetScreenOrientation == null) {
return;
}
// 判断 Activity 之前是不是设置的屏幕自适应(这个判断可能永远为 false,但是为了代码的严谨性,还是要做一下判断)
if (targetScreenOrientation == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
return;
}
// 为什么这里不用跟上面一样 try catch ?因为这里是把 Activity 方向取消固定,只有设置横屏或竖屏的时候才可能触发 crash
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
}
/**
* 判断 Activity 是否反方向旋转了
*/
@SuppressWarnings("deprecation")
private static boolean isActivityReverse(@NonNull Activity activity) {
Display display = null;
if (PermissionVersion.isAndroid11()) {
display = activity.getDisplay();
} else {
WindowManager windowManager = activity.getWindowManager();
if (windowManager != null) {
display = windowManager.getDefaultDisplay();
}
}
if (display == null) {
return false;
}
// 获取 Activity 旋转的角度
int activityRotation = display.getRotation();
switch (activityRotation) {
case Surface.ROTATION_180:
case Surface.ROTATION_270:
return true;
case Surface.ROTATION_0:
case Surface.ROTATION_90:
default:
return false;
}
}
/**
* 通过 Activity 获得一个 int 值的 key
*/
private static int getIntKeyByActivity(@NonNull Activity activity) {
// 这里取 Activity 的 hashCode 作为 key 值,这样就不会出现重复
return activity.hashCode();
}
}
================================================
FILE: library/src/main/java/com/hjq/permissions/manager/AlreadyRequestPermissionsManager.java
================================================
package com.hjq.permissions.manager;
import androidx.annotation.Nullable;
import com.hjq.permissions.permission.base.IPermission;
import com.hjq.permissions.tools.PermissionUtils;
import java.util.ArrayList;
import java.util.List;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/07/13
* desc : 已请求权限的管理类
*/
public final class AlreadyRequestPermissionsManager {
/** 已请求过的权限集 */
private static final List ALREADY_REQUEST_PERMISSIONS_LIST = new ArrayList<>();
/** 私有化构造函数 */
private AlreadyRequestPermissionsManager() {
// default implementation ignored
}
/**
* 添加已申请过的权限
*/
public static void addAlreadyRequestPermissions(@Nullable List permissions) {
if (permissions == null || permissions.isEmpty()) {
return;
}
for (IPermission permission : permissions) {
String permissionName = permission.getPermissionName();
if (PermissionUtils.containsPermission(ALREADY_REQUEST_PERMISSIONS_LIST, permissionName)) {
continue;
}
ALREADY_REQUEST_PERMISSIONS_LIST.add(permissionName);
}
}
/**
* 判断某些权限是否申请过
*/
public static boolean isAlreadyRequestPermissions(@Nullable IPermission permission) {
if (permission == null) {
return false;
}
return PermissionUtils.containsPermission(ALREADY_REQUEST_PERMISSIONS_LIST, permission.getPermissionName());
}
}
================================================
FILE: library/src/main/java/com/hjq/permissions/manager/PermissionRequestCodeManager.java
================================================
package com.hjq.permissions.manager;
import androidx.annotation.IntRange;
import com.hjq.permissions.XXPermissions;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/05/20
* desc : 权限请求码管理类
*/
public final class PermissionRequestCodeManager {
/** 请求码限制:低值 */
public static final int REQUEST_CODE_LIMIT_LOW_VALUE = 255;
/** 请求码限制:高值 */
public static final int REQUEST_CODE_LIMIT_HIGH_VALUE = 65535;
/** 权限请求码存放集合 */
private static final List REQUEST_CODE_ARRAY = new ArrayList<>();
/** 随机数对象 */
private static final Random RANDOM = new Random();
/** 私有化构造函数 */
private PermissionRequestCodeManager() {
// default implementation ignored
}
/**
* 随机生成一个请求码
*/
@IntRange(from = 1, to = 65535)
public static synchronized int generateRandomRequestCode(@IntRange(from = 1, to = 65535) int maxRequestCode) {
int requestCode;
// 请求码随机生成,避免随机产生之前的请求码,必须进行循环判断
// 1. 请求码不能为 0 和负数
// 2. 请求码不能等于 XXPermissions.REQUEST_CODE
// 3. 尽量避免和当前项目的请求码出现冲突,所以需要抛弃小值的请求码,经过测试,发现以下问题:
// a. 使用系统包下的 Fragment 进行权限申请,不会触发宿主 Activity 回调 onActivityResult 和 onRequestPermissionsResult
// b. 使用 AndroidX 库中的 Fragment 进行权限申请,会触发宿主 Activity 回调 onActivityResult 和 onRequestPermissionsResult
// 这是因为 AndroidX 库中的 Fragment 权限相关的回调是通过重写 Activity 类的 onActivityResult 和 onRequestPermissionsResult 实现的
// 而系统包下 Fragment 的 onActivityResult 和 onRequestPermissionsResult 回调是直接在 Activity 类中的 dispatchActivityResult 中实现的
do {
// maxRequestCode 目前只有两种值,255 和 65535
// 1. 如果外层传入的是 255(可能性较低),那么请求码的取值范围为:(255 / 2 + 1) ~ (255 - 1) = 128 ~ 254
// 2. 如果外层传入的是 65535(可能性较大),那么请求码的取值范围为:(65535 - 10000 + 1) ~ (65535 - 1) = 55536 ~ 65534
// 代码已经写得那么严谨了,但是仍然可能会出现请求码冲突的问题,虽然概率极低,但是这个时候就只能寄希望给外层的开发者,不要把请求码设定得那么大
// 否则会和框架的请求码相冲突,但是出现这种情况的可能性比较低,外层的开发者如果把请求码定太大,会发现调用 startActivityForResult 没反应
// 所以基于这个原因,一般设定大值请求码的可能性会比较小,退一万步讲,就算有这种情况,框架会随机从将近一万个数中选择一个,简称万里挑一
// 就算最终出现了问题,因为数量占比会很少,加上不是必现(因为是通过随机数生成的),这个问题的影响程度会大大降低,这也是目前能想到的最佳处理方案
int minRequestCode = maxRequestCode > 20000 ? maxRequestCode - 10000 : maxRequestCode / 2;
requestCode = RANDOM.nextInt(maxRequestCode - minRequestCode) + minRequestCode;
} while (requestCode == XXPermissions.REQUEST_CODE || REQUEST_CODE_ARRAY.contains(requestCode));
// 标记这个请求码已经被占用
REQUEST_CODE_ARRAY.add(requestCode);
return requestCode;
}
/**
* 释放对某个请求码的占用
*/
public static synchronized void releaseRequestCode(int requestCode) {
REQUEST_CODE_ARRAY.remove((Integer) requestCode);
}
}
================================================
FILE: library/src/main/java/com/hjq/permissions/manifest/AndroidManifestInfo.java
================================================
package com.hjq.permissions.manifest;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hjq.permissions.manifest.node.ActivityManifestInfo;
import com.hjq.permissions.manifest.node.ApplicationManifestInfo;
import com.hjq.permissions.manifest.node.BroadcastReceiverManifestInfo;
import com.hjq.permissions.manifest.node.PermissionManifestInfo;
import com.hjq.permissions.manifest.node.ServiceManifestInfo;
import com.hjq.permissions.manifest.node.UsesSdkManifestInfo;
import java.util.ArrayList;
import java.util.List;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2022/11/11
* desc : 清单文件解析 Bean 类
*/
public final class AndroidManifestInfo {
/** 应用包名 */
@NonNull
public String packageName = "";
/** 使用 sdk 信息 */
@Nullable
public UsesSdkManifestInfo usesSdkInfo;
/** 权限节点信息 */
@NonNull
public final List permissionInfoList = new ArrayList<>();
/** 查询包名列表 */
@NonNull
public final List queriesPackageList = new ArrayList<>();
/** Application 节点信息 */
@Nullable
public ApplicationManifestInfo applicationInfo;
/** Activity 节点信息 */
@NonNull
public final List activityInfoList = new ArrayList<>();
/** Service 节点信息 */
@NonNull
public final List serviceInfoList = new ArrayList<>();
/** BroadcastReceiver 节点信息 */
@NonNull
public final List receiverInfoList = new ArrayList<>();
}
================================================
FILE: library/src/main/java/com/hjq/permissions/manifest/AndroidManifestParser.java
================================================
package com.hjq.permissions.manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.AssetManager;
import android.content.res.XmlResourceParser;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hjq.permissions.manifest.node.ActivityManifestInfo;
import com.hjq.permissions.manifest.node.ApplicationManifestInfo;
import com.hjq.permissions.manifest.node.BroadcastReceiverManifestInfo;
import com.hjq.permissions.manifest.node.IntentFilterManifestInfo;
import com.hjq.permissions.manifest.node.MetaDataManifestInfo;
import com.hjq.permissions.manifest.node.PermissionManifestInfo;
import com.hjq.permissions.manifest.node.ServiceManifestInfo;
import com.hjq.permissions.manifest.node.UsesSdkManifestInfo;
import com.hjq.permissions.tools.PermissionUtils;
import com.hjq.permissions.tools.PermissionVersion;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import org.xmlpull.v1.XmlPullParserException;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2022/11/11
* desc : 清单文件解析器
*/
public final class AndroidManifestParser {
/** 清单文件的文件名称 */
private static final String ANDROID_MANIFEST_FILE_NAME = "AndroidManifest.xml";
/** Android 的命名空间 */
private static final String ANDROID_NAMESPACE_URI = "http://schemas.android.com/apk/res/android";
private static final String TAG_MANIFEST = "manifest";
private static final String TAG_USES_SDK = "uses-sdk";
private static final String TAG_USES_PERMISSION = "uses-permission";
private static final String TAG_USES_PERMISSION_SDK_23 = "uses-permission-sdk-23";
private static final String TAG_USES_PERMISSION_SDK_M = "uses-permission-sdk-m";
private static final String TAG_QUERIES = "queries";
private static final String TAG_APPLICATION = "application";
private static final String TAG_ACTIVITY = "activity";
private static final String TAG_ACTIVITY_ALIAS = "activity-alias";
private static final String TAG_SERVICE = "service";
private static final String TAG_RECEIVER = "receiver";
private static final String TAG_INTENT_FILTER = "intent-filter";
private static final String TAG_ACTION = "action";
private static final String TAG_CATEGORY = "category";
private static final String TAG_META_DATA = "meta-data";
private static final String ATTR_PACKAGE = "package";
private static final String ATTR_NAME = "name";
private static final String ATTR_VALUE = "value";
private static final String ATTR_RESOURCE = "resource";
private static final String ATTR_MAX_SDK_VERSION = "maxSdkVersion";
private static final String ATTR_MIN_SDK_VERSION = "minSdkVersion";
private static final String ATTR_USES_PERMISSION_FLAGS = "usesPermissionFlags";
private static final String ATTR_REQUEST_LEGACY_EXTERNAL_STORAGE = "requestLegacyExternalStorage";
private static final String ATTR_SUPPORTS_PICTURE_IN_PICTURE = "supportsPictureInPicture";
private static final String ATTR_PERMISSION = "permission";
/** 私有化构造函数 */
private AndroidManifestParser() {
// default implementation ignored
}
/**
* 获取当前应用的清单文件信息
*/
@Nullable
public static AndroidManifestInfo getAndroidManifestInfo(Context context) {
int apkPathCookie = AndroidManifestParser.findApkPathCookie(context, context.getApplicationInfo().sourceDir);
// 如果 cookie 为 0,证明获取失败
if (apkPathCookie == 0) {
return null;
}
AndroidManifestInfo manifestInfo = null;
try {
manifestInfo = AndroidManifestParser.parseAndroidManifest(context, apkPathCookie);
// 如果读取到的包名和当前应用的包名不是同一个的话,证明这个清单文件的内容不是当前应用的
// 具体案例:https://github.com/getActivity/XXPermissions/issues/102
if (!PermissionUtils.reverseEqualsString(context.getPackageName(), manifestInfo.packageName)) {
return null;
}
} catch (IOException | XmlPullParserException e) {
e.printStackTrace();
}
return manifestInfo;
}
/**
* 获取当前应用 Apk 在 AssetManager 中的 Cookie,如果获取失败,则为 0
*/
@SuppressWarnings("JavaReflectionMemberAccess")
@SuppressLint("PrivateApi")
public static int findApkPathCookie(@NonNull Context context, @NonNull String apkPath) {
AssetManager assets = context.getAssets();
Integer cookie;
try {
if (PermissionVersion.getTargetVersion(context) >= PermissionVersion.ANDROID_9 &&
PermissionVersion.getCurrentVersion() >= PermissionVersion.ANDROID_9 &&
PermissionVersion.getCurrentVersion() < PermissionVersion.ANDROID_11) {
// 反射套娃操作:实测这种方式只在 Android 9.0 和 Android 10.0 有效果,在 Android 11 上面就失效了
Method metaGetDeclaredMethod = Class.class.getDeclaredMethod(
"getDeclaredMethod", String.class, Class[].class);
metaGetDeclaredMethod.setAccessible(true);
// 注意 AssetManager.findCookieForPath 是 Android 9.0(API 28)的时候才添加的方法
// 而 Android 9.0 用的是 AssetManager.addAssetPath 来获取 cookie
// 具体可以参考 PackageParser.parseBaseApk 方法源码的实现
Method findCookieForPathMethod = (Method) metaGetDeclaredMethod.invoke(AssetManager.class,
"findCookieForPath", new Class[]{String.class});
if (findCookieForPathMethod != null) {
findCookieForPathMethod.setAccessible(true);
cookie = (Integer) findCookieForPathMethod.invoke(context.getAssets(), apkPath);
if (cookie != null) {
return cookie;
}
}
}
Method addAssetPathMethod = assets.getClass().getDeclaredMethod("addAssetPath", String.class);
cookie = (Integer) addAssetPathMethod.invoke(assets, apkPath);
if (cookie != null) {
return cookie;
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
// 获取失败直接返回 0
// 为什么不直接返回 Integer,而是返回 int 类型?
// 去看看 AssetManager.findCookieForPath 获取失败会返回什么就知道了
return 0;
}
/**
* 解析 apk 包中的清单文件
*
* @param context 上下文
* @param apkCookie 要解析 apk 的 cookie
*/
@NonNull
public static AndroidManifestInfo parseAndroidManifest(@NonNull Context context, int apkCookie) throws IOException, XmlPullParserException {
AndroidManifestInfo manifestInfo = new AndroidManifestInfo();
try (XmlResourceParser parser = context.getAssets().
openXmlResourceParser(apkCookie, ANDROID_MANIFEST_FILE_NAME)) {
do {
// 当前节点必须为标签头部
if (parser.getEventType() != XmlResourceParser.START_TAG) {
continue;
}
String tagName = parser.getName();
if (PermissionUtils.equalsString(TAG_MANIFEST, tagName)) {
manifestInfo.packageName = parsePackageFromXml(parser);
}
if (PermissionUtils.equalsString(TAG_USES_SDK, tagName)) {
manifestInfo.usesSdkInfo = parseUsesSdkFromXml(parser);
}
if (PermissionUtils.equalsString(TAG_USES_PERMISSION, tagName) ||
PermissionUtils.equalsString(TAG_USES_PERMISSION_SDK_23, tagName) ||
PermissionUtils.equalsString(TAG_USES_PERMISSION_SDK_M, tagName)) {
manifestInfo.permissionInfoList.add(parsePermissionFromXml(parser));
}
if (PermissionUtils.equalsString(TAG_QUERIES, tagName)) {
manifestInfo.queriesPackageList.add(parsePackageFromXml(parser));
}
if (PermissionUtils.equalsString(TAG_APPLICATION, tagName)) {
manifestInfo.applicationInfo = parseApplicationFromXml(parser);
}
if (PermissionUtils.equalsString(TAG_ACTIVITY, tagName) ||
PermissionUtils.equalsString(TAG_ACTIVITY_ALIAS, tagName)) {
manifestInfo.activityInfoList.add(parseActivityFromXml(parser));
}
if (PermissionUtils.equalsString(TAG_SERVICE, tagName)) {
manifestInfo.serviceInfoList.add(parseServerFromXml(parser));
}
if (PermissionUtils.equalsString(TAG_RECEIVER, tagName)) {
manifestInfo.receiverInfoList.add(parseBroadcastReceiverFromXml(parser));
}
if (PermissionUtils.equalsString(TAG_META_DATA, tagName) && manifestInfo.applicationInfo != null) {
if (manifestInfo.applicationInfo.metaDataInfoList == null) {
manifestInfo.applicationInfo.metaDataInfoList = new ArrayList<>();
}
manifestInfo.applicationInfo.metaDataInfoList.add(parseMetaDataFromXml(parser));
}
} while (parser.next() != XmlResourceParser.END_DOCUMENT);
}
return manifestInfo;
}
@NonNull
private static String parsePackageFromXml(@NonNull XmlResourceParser parser) {
String packageName = parser.getAttributeValue(null, ATTR_PACKAGE);
return packageName != null ? packageName : "";
}
@NonNull
private static UsesSdkManifestInfo parseUsesSdkFromXml(@NonNull XmlResourceParser parser) {
UsesSdkManifestInfo usesSdkInfo = new UsesSdkManifestInfo();
usesSdkInfo.minSdkVersion = parser.getAttributeIntValue(ANDROID_NAMESPACE_URI,
ATTR_MIN_SDK_VERSION, 0);
return usesSdkInfo;
}
@NonNull
private static PermissionManifestInfo parsePermissionFromXml(@NonNull XmlResourceParser parser) {
PermissionManifestInfo permissionInfo = new PermissionManifestInfo();
permissionInfo.name = parser.getAttributeValue(ANDROID_NAMESPACE_URI, ATTR_NAME);
permissionInfo.maxSdkVersion = parser.getAttributeIntValue(ANDROID_NAMESPACE_URI,
ATTR_MAX_SDK_VERSION, PermissionManifestInfo.DEFAULT_MAX_SDK_VERSION);
permissionInfo.usesPermissionFlags = parser.getAttributeIntValue(ANDROID_NAMESPACE_URI,
ATTR_USES_PERMISSION_FLAGS, 0);
return permissionInfo;
}
@NonNull
private static ApplicationManifestInfo parseApplicationFromXml(@NonNull XmlResourceParser parser) {
ApplicationManifestInfo applicationInfo = new ApplicationManifestInfo();
String applicationClassName = parser.getAttributeValue(ANDROID_NAMESPACE_URI, ATTR_NAME);
applicationInfo.name = applicationClassName != null ? applicationClassName : "";
applicationInfo.requestLegacyExternalStorage = parser.getAttributeBooleanValue(
ANDROID_NAMESPACE_URI, ATTR_REQUEST_LEGACY_EXTERNAL_STORAGE, false);
return applicationInfo;
}
@NonNull
private static ActivityManifestInfo parseActivityFromXml(@NonNull XmlResourceParser parser) throws IOException, XmlPullParserException {
ActivityManifestInfo activityInfo = new ActivityManifestInfo();
String activityClassName = parser.getAttributeValue(ANDROID_NAMESPACE_URI, ATTR_NAME);
activityInfo.name = activityClassName != null ? activityClassName : "";
activityInfo.supportsPictureInPicture = parser.getAttributeBooleanValue(
ANDROID_NAMESPACE_URI, ATTR_SUPPORTS_PICTURE_IN_PICTURE, false);
while (true) {
int nextTagType = parser.next();
String tagName = parser.getName();
if (nextTagType == XmlResourceParser.END_TAG &&
(PermissionUtils.equalsString(TAG_ACTIVITY, tagName) ||
PermissionUtils.equalsString(TAG_ACTIVITY_ALIAS, tagName))) {
break;
}
if (nextTagType == XmlResourceParser.START_TAG && PermissionUtils.equalsString(TAG_INTENT_FILTER, tagName)) {
if (activityInfo.intentFilterInfoList == null) {
activityInfo.intentFilterInfoList = new ArrayList<>();
}
activityInfo.intentFilterInfoList.add(parseIntentFilterFromXml(parser));
} else if (nextTagType == XmlResourceParser.START_TAG && PermissionUtils.equalsString(TAG_META_DATA, tagName)) {
if (activityInfo.metaDataInfoList == null) {
activityInfo.metaDataInfoList = new ArrayList<>();
}
activityInfo.metaDataInfoList.add(parseMetaDataFromXml(parser));
}
}
return activityInfo;
}
@NonNull
private static ServiceManifestInfo parseServerFromXml(@NonNull XmlResourceParser parser) throws IOException, XmlPullParserException {
ServiceManifestInfo serviceInfo = new ServiceManifestInfo();
String serviceClassName = parser.getAttributeValue(ANDROID_NAMESPACE_URI, ATTR_NAME);
serviceInfo.name = serviceClassName != null ? serviceClassName : "";
serviceInfo.permission = parser.getAttributeValue(ANDROID_NAMESPACE_URI, ATTR_PERMISSION);
while (true) {
int nextTagType = parser.next();
String tagName = parser.getName();
if (nextTagType == XmlResourceParser.END_TAG && PermissionUtils.equalsString(TAG_SERVICE, tagName)) {
break;
}
if (nextTagType == XmlResourceParser.START_TAG && PermissionUtils.equalsString(TAG_INTENT_FILTER, tagName)) {
if (serviceInfo.intentFilterInfoList == null) {
serviceInfo.intentFilterInfoList = new ArrayList<>();
}
serviceInfo.intentFilterInfoList.add(parseIntentFilterFromXml(parser));
} else if (nextTagType == XmlResourceParser.START_TAG && PermissionUtils.equalsString(TAG_META_DATA, tagName)) {
if (serviceInfo.metaDataInfoList == null) {
serviceInfo.metaDataInfoList = new ArrayList<>();
}
serviceInfo.metaDataInfoList.add(parseMetaDataFromXml(parser));
}
}
return serviceInfo;
}
@NonNull
private static BroadcastReceiverManifestInfo parseBroadcastReceiverFromXml(@NonNull XmlResourceParser parser) throws IOException, XmlPullParserException {
BroadcastReceiverManifestInfo receiverInfo = new BroadcastReceiverManifestInfo();
String broadcastReceiverClassName = parser.getAttributeValue(ANDROID_NAMESPACE_URI, ATTR_NAME);
receiverInfo.name = broadcastReceiverClassName != null ? broadcastReceiverClassName : "";
receiverInfo.permission = parser.getAttributeValue(ANDROID_NAMESPACE_URI, ATTR_PERMISSION);
while (true) {
int nextTagType = parser.next();
String tagName = parser.getName();
if (nextTagType == XmlResourceParser.END_TAG && PermissionUtils.equalsString(TAG_RECEIVER, tagName)) {
break;
}
if (nextTagType == XmlResourceParser.START_TAG && PermissionUtils.equalsString(TAG_INTENT_FILTER, tagName)) {
if (receiverInfo.intentFilterInfoList == null) {
receiverInfo.intentFilterInfoList = new ArrayList<>();
}
receiverInfo.intentFilterInfoList.add(parseIntentFilterFromXml(parser));
} else if (nextTagType == XmlResourceParser.START_TAG && PermissionUtils.equalsString(TAG_META_DATA, tagName)) {
if (receiverInfo.metaDataInfoList == null) {
receiverInfo.metaDataInfoList = new ArrayList<>();
}
receiverInfo.metaDataInfoList.add(parseMetaDataFromXml(parser));
}
}
return receiverInfo;
}
@NonNull
private static IntentFilterManifestInfo parseIntentFilterFromXml(@NonNull XmlResourceParser parser) throws IOException, XmlPullParserException {
IntentFilterManifestInfo intentFilterInfo = new IntentFilterManifestInfo();
while (true) {
int nextTagType = parser.next();
String tagName = parser.getName();
if (nextTagType == XmlResourceParser.END_TAG && PermissionUtils.equalsString(TAG_INTENT_FILTER, tagName)) {
break;
}
if (nextTagType != XmlResourceParser.START_TAG) {
continue;
}
if (PermissionUtils.equalsString(TAG_ACTION, tagName)) {
intentFilterInfo.actionList.add(parser.getAttributeValue(ANDROID_NAMESPACE_URI, ATTR_NAME));
} else if (PermissionUtils.equalsString(TAG_CATEGORY, tagName)) {
intentFilterInfo.categoryList.add(parser.getAttributeValue(ANDROID_NAMESPACE_URI, ATTR_NAME));
}
}
return intentFilterInfo;
}
@NonNull
private static MetaDataManifestInfo parseMetaDataFromXml(@NonNull XmlResourceParser parser) throws IOException, XmlPullParserException {
MetaDataManifestInfo metaDataInfo = new MetaDataManifestInfo();
metaDataInfo.name = parser.getAttributeValue(ANDROID_NAMESPACE_URI, ATTR_NAME);
metaDataInfo.value = parser.getAttributeValue(ANDROID_NAMESPACE_URI, ATTR_VALUE);
metaDataInfo.resource = parser.getAttributeResourceValue(ANDROID_NAMESPACE_URI, ATTR_RESOURCE, 0);
return metaDataInfo;
}
}
================================================
FILE: library/src/main/java/com/hjq/permissions/manifest/node/ActivityManifestInfo.java
================================================
package com.hjq.permissions.manifest.node;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.List;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2022/11/11
* desc : Activity 清单信息类
*/
public final class ActivityManifestInfo {
/** Activity 的类名 */
@NonNull
public String name = "";
/** 是否支持画中画 */
public boolean supportsPictureInPicture;
/** 意图过滤器列表 */
@Nullable
public List intentFilterInfoList;
/** MetaData 列表 */
@Nullable
public List metaDataInfoList;
}
================================================
FILE: library/src/main/java/com/hjq/permissions/manifest/node/ApplicationManifestInfo.java
================================================
package com.hjq.permissions.manifest.node;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.List;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2022/11/11
* desc : Application 清单信息类
*/
public final class ApplicationManifestInfo {
/** Application 的类名 */
@NonNull
public String name = "";
/** 是否忽略分区存储特性 */
public boolean requestLegacyExternalStorage;
/** MetaData 列表 */
@Nullable
public List metaDataInfoList;
}
================================================
FILE: library/src/main/java/com/hjq/permissions/manifest/node/BroadcastReceiverManifestInfo.java
================================================
package com.hjq.permissions.manifest.node;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.List;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2022/11/11
* desc : BroadcastReceiver 清单信息类
*/
public final class BroadcastReceiverManifestInfo {
/** BroadcastReceiver 的类名 */
@NonNull
public String name = "";
/** 所使用到的权限 */
@Nullable
public String permission;
/** 意图过滤器列表 */
@Nullable
public List intentFilterInfoList;
/** MetaData 列表 */
@Nullable
public List metaDataInfoList;
}
================================================
FILE: library/src/main/java/com/hjq/permissions/manifest/node/IntentFilterManifestInfo.java
================================================
package com.hjq.permissions.manifest.node;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.List;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/07/15
* desc : 意图过滤器
*/
public final class IntentFilterManifestInfo {
/** 动作列表 */
@NonNull
public final List actionList = new ArrayList<>();
/** 分类列表 */
@NonNull
public final List categoryList = new ArrayList<>();
}
================================================
FILE: library/src/main/java/com/hjq/permissions/manifest/node/MetaDataManifestInfo.java
================================================
package com.hjq.permissions.manifest.node;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/08/10
* desc : MetaData 清单信息类
*/
public final class MetaDataManifestInfo {
/** MetaData 的名称 */
@NonNull
public String name = "";
/** MetaData 的值 */
@Nullable
public String value;
/** MetaData 的资源 ID */
public int resource;
}
================================================
FILE: library/src/main/java/com/hjq/permissions/manifest/node/PermissionManifestInfo.java
================================================
package com.hjq.permissions.manifest.node;
import android.content.pm.PackageInfo;
import com.hjq.permissions.tools.PermissionVersion;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2022/11/11
* desc : 权限清单信息类
*/
public final class PermissionManifestInfo {
/** 默认最大生效 sdk 版本 */
public static final int DEFAULT_MAX_SDK_VERSION = Integer.MAX_VALUE;
/** 不需要请求地理位置标志 */
private static final int REQUESTED_PERMISSION_NEVER_FOR_LOCATION;
static {
if (PermissionVersion.isAndroid12()) {
REQUESTED_PERMISSION_NEVER_FOR_LOCATION = PackageInfo.REQUESTED_PERMISSION_NEVER_FOR_LOCATION;
} else {
REQUESTED_PERMISSION_NEVER_FOR_LOCATION = 0x00010000;
}
}
/** 权限名称 */
public String name;
/** 最大生效 sdk 版本 */
public int maxSdkVersion = DEFAULT_MAX_SDK_VERSION;
/** 权限使用标志 */
public int usesPermissionFlags;
/**
* 是否不会用当前权限需要推导地理位置
*/
public boolean neverForLocation() {
return (usesPermissionFlags & REQUESTED_PERMISSION_NEVER_FOR_LOCATION) != 0;
}
}
================================================
FILE: library/src/main/java/com/hjq/permissions/manifest/node/ServiceManifestInfo.java
================================================
package com.hjq.permissions.manifest.node;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.List;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2022/11/11
* desc : Service 清单信息类
*/
public final class ServiceManifestInfo {
/** Service 的类名 */
@NonNull
public String name = "";
/** 所使用到的权限 */
@Nullable
public String permission;
/** 意图过滤器列表 */
@Nullable
public List intentFilterInfoList;
/** MetaData 列表 */
@Nullable
public List metaDataInfoList;
}
================================================
FILE: library/src/main/java/com/hjq/permissions/manifest/node/UsesSdkManifestInfo.java
================================================
package com.hjq.permissions.manifest.node;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2022/11/11
* desc : 清单信息类
*/
public final class UsesSdkManifestInfo {
/** 最小安装版本要求 **/
public int minSdkVersion;
}
================================================
FILE: library/src/main/java/com/hjq/permissions/permission/PermissionChannel.java
================================================
package com.hjq.permissions.permission;
import android.content.Intent;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/08/24
* desc : 权限请求通道
*/
public enum PermissionChannel {
/** {@link android.app.Activity#requestPermissions(String[], int)} */
REQUEST_PERMISSIONS,
/** {@link android.app.Activity#startActivityForResult(Intent, int)} */
START_ACTIVITY
}
================================================
FILE: library/src/main/java/com/hjq/permissions/permission/PermissionGroups.java
================================================
package com.hjq.permissions.permission;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/06/14
* desc : 权限组的名称常量集
*/
public final class PermissionGroups {
/**
* 权限组的后缀名称
*/
public static final String SUFFIX = "_group";
/**
* 存储权限组,包含以下权限
*
* {@link PermissionNames#READ_EXTERNAL_STORAGE}
* {@link PermissionNames#WRITE_EXTERNAL_STORAGE}
*/
public static final String STORAGE = "storage" + SUFFIX;
/**
* 日历权限组,包含以下权限
*
* {@link PermissionNames#READ_CALENDAR}
* {@link PermissionNames#WRITE_CALENDAR}
*/
public static final String CALENDAR = "calendar" + SUFFIX;
/**
* 通讯录权限组,包含以下权限
*
* {@link PermissionNames#READ_CONTACTS}
* {@link PermissionNames#WRITE_CONTACTS}
* {@link PermissionNames#GET_ACCOUNTS}
*/
public static final String CONTACTS = "contacts" + SUFFIX;
/**
* 短信权限组,包含以下权限
*
* {@link PermissionNames#SEND_SMS}
* {@link PermissionNames#READ_SMS}
* {@link PermissionNames#RECEIVE_SMS}
* {@link PermissionNames#RECEIVE_WAP_PUSH}
* {@link PermissionNames#RECEIVE_MMS}
*/
public static final String SMS = "sms" + SUFFIX;
/**
* 位置权限组,包含以下权限
*
* {@link PermissionNames#ACCESS_COARSE_LOCATION}
* {@link PermissionNames#ACCESS_FINE_LOCATION}
* {@link PermissionNames#ACCESS_BACKGROUND_LOCATION}
*
* 注意:在 Android 12 的时候,蓝牙相关的权限已经归到附近设备的权限组了,但是在 Android 12 之前,蓝牙相关的权限归属定位权限组
* {@link PermissionNames#BLUETOOTH_SCAN}
* {@link PermissionNames#BLUETOOTH_CONNECT}
* {@link PermissionNames#BLUETOOTH_ADVERTISE}
*
* 注意:在 Android 13 的时候,WIFI 相关的权限已经归到附近设备的权限组了,但是在 Android 13 之前,WIFI 相关的权限归属定位权限组
* {@link PermissionNames#NEARBY_WIFI_DEVICES}
*/
public static final String LOCATION = "location" + SUFFIX;
/**
* 传感器权限组,包含以下权限
*
* {@link PermissionNames#BODY_SENSORS}
* {@link PermissionNames#BODY_SENSORS_BACKGROUND}
*/
public static final String SENSORS = "sensors" + SUFFIX;
/**
* 电话权限组,包含以下权限
*
* {@link PermissionNames#READ_PHONE_STATE}
* {@link PermissionNames#CALL_PHONE}
* {@link PermissionNames#ADD_VOICEMAIL}
* {@link PermissionNames#USE_SIP}
* {@link PermissionNames#READ_PHONE_NUMBERS}
* {@link PermissionNames#ANSWER_PHONE_CALLS}
* {@link PermissionNames#ACCEPT_HANDOVER}
*
* 注意:在 Android 9.0 的时候,读写通话记录权限已经归到一个单独的权限组了,但是在 Android 9.0 之前,读写通话记录权限归属电话权限组
* {@link PermissionNames#READ_CALL_LOG}
* {@link PermissionNames#WRITE_CALL_LOG}
* {@link PermissionNames#PROCESS_OUTGOING_CALLS}
*/
public static final String PHONE = "phone" + SUFFIX;
/**
* 通话记录权限组(在 Android 9.0 的时候,读写通话记录权限已经归到一个单独的权限组了,但是在 Android 9.0 之前,读写通话记录权限归属电话权限组),包含以下权限
*
* {@link PermissionNames#READ_CALL_LOG}
* {@link PermissionNames#WRITE_CALL_LOG}
* {@link PermissionNames#PROCESS_OUTGOING_CALLS}
*/
public static final String CALL_LOG = "call_log" + SUFFIX;
/**
* 附近设备权限组,包含以下权限
*
* 在 Android 12 的时候,蓝牙相关的权限已经归到附近设备的权限组了,但是在 Android 12 之前,蓝牙相关的权限归属定位权限组
* {@link PermissionNames#BLUETOOTH_SCAN}
* {@link PermissionNames#BLUETOOTH_CONNECT}
* {@link PermissionNames#BLUETOOTH_ADVERTISE}
*
* 注意:在 Android 13 的时候,WIFI 相关的权限已经归到附近设备的权限组了,但是在 Android 13 之前,WIFI 相关的权限归属定位权限组
* {@link PermissionNames#NEARBY_WIFI_DEVICES}
*/
public static final String NEARBY_DEVICES = "nearby_devices" + SUFFIX;
/**
* 照片和视频权限组(注意:不包含音频权限) ,包含以下权限
*
* {@link PermissionNames#READ_MEDIA_IMAGES}
* {@link PermissionNames#READ_MEDIA_VIDEO}
* {@link PermissionNames#READ_MEDIA_VISUAL_USER_SELECTED}
*/
public static final String IMAGE_AND_VIDEO_MEDIA = "image_and_video_media" + SUFFIX;
/**
* 健康权限组,包含以下权限
*
* {@link PermissionNames#READ_HEALTH_DATA_IN_BACKGROUND}
* {@link PermissionNames#READ_HEALTH_DATA_HISTORY}
*
* {@link PermissionNames#READ_ACTIVE_CALORIES_BURNED}
* {@link PermissionNames#WRITE_ACTIVE_CALORIES_BURNED}
* {@link PermissionNames#READ_ACTIVITY_INTENSITY}
* {@link PermissionNames#WRITE_ACTIVITY_INTENSITY}
* {@link PermissionNames#READ_BASAL_BODY_TEMPERATURE}
* {@link PermissionNames#WRITE_BASAL_BODY_TEMPERATURE}
* {@link PermissionNames#READ_BASAL_METABOLIC_RATE}
* {@link PermissionNames#WRITE_BASAL_METABOLIC_RATE}
* {@link PermissionNames#READ_BLOOD_GLUCOSE}
* {@link PermissionNames#WRITE_BLOOD_GLUCOSE}
* {@link PermissionNames#READ_BLOOD_PRESSURE}
* {@link PermissionNames#WRITE_BLOOD_PRESSURE}
* {@link PermissionNames#READ_BODY_FAT}
* {@link PermissionNames#WRITE_BODY_FAT}
* {@link PermissionNames#READ_BODY_TEMPERATURE}
* {@link PermissionNames#WRITE_BODY_TEMPERATURE}
* {@link PermissionNames#READ_BODY_WATER_MASS}
* {@link PermissionNames#WRITE_BODY_WATER_MASS}
* {@link PermissionNames#READ_BONE_MASS}
* {@link PermissionNames#WRITE_BONE_MASS}
* {@link PermissionNames#READ_CERVICAL_MUCUS}
* {@link PermissionNames#WRITE_CERVICAL_MUCUS}
* {@link PermissionNames#READ_DISTANCE}
* {@link PermissionNames#WRITE_DISTANCE}
* {@link PermissionNames#READ_ELEVATION_GAINED}
* {@link PermissionNames#WRITE_ELEVATION_GAINED}
* {@link PermissionNames#READ_EXERCISE}
* {@link PermissionNames#WRITE_EXERCISE}
* {@link PermissionNames#READ_EXERCISE_ROUTES}
* {@link PermissionNames#WRITE_EXERCISE_ROUTE}
* {@link PermissionNames#READ_FLOORS_CLIMBED}
* {@link PermissionNames#WRITE_FLOORS_CLIMBED}
* {@link PermissionNames#READ_HEART_RATE}
* {@link PermissionNames#WRITE_HEART_RATE}
* {@link PermissionNames#READ_HEART_RATE_VARIABILITY}
* {@link PermissionNames#WRITE_HEART_RATE_VARIABILITY}
* {@link PermissionNames#READ_HEIGHT}
* {@link PermissionNames#WRITE_HEIGHT}
* {@link PermissionNames#READ_HYDRATION}
* {@link PermissionNames#WRITE_HYDRATION}
* {@link PermissionNames#READ_INTERMENSTRUAL_BLEEDING}
* {@link PermissionNames#WRITE_INTERMENSTRUAL_BLEEDING}
* {@link PermissionNames#READ_LEAN_BODY_MASS}
* {@link PermissionNames#WRITE_LEAN_BODY_MASS}
* {@link PermissionNames#READ_MENSTRUATION}
* {@link PermissionNames#WRITE_MENSTRUATION}
* {@link PermissionNames#READ_MINDFULNESS}
* {@link PermissionNames#WRITE_MINDFULNESS}
* {@link PermissionNames#READ_NUTRITION}
* {@link PermissionNames#WRITE_NUTRITION}
* {@link PermissionNames#READ_OVULATION_TEST}
* {@link PermissionNames#WRITE_OVULATION_TEST}
* {@link PermissionNames#READ_OXYGEN_SATURATION}
* {@link PermissionNames#WRITE_OXYGEN_SATURATION}
* {@link PermissionNames#READ_PLANNED_EXERCISE}
* {@link PermissionNames#WRITE_PLANNED_EXERCISE}
* {@link PermissionNames#READ_POWER}
* {@link PermissionNames#WRITE_POWER}
* {@link PermissionNames#READ_RESPIRATORY_RATE}
* {@link PermissionNames#WRITE_RESPIRATORY_RATE}
* {@link PermissionNames#READ_RESTING_HEART_RATE}
* {@link PermissionNames#WRITE_RESTING_HEART_RATE}
* {@link PermissionNames#READ_SEXUAL_ACTIVITY}
* {@link PermissionNames#WRITE_SEXUAL_ACTIVITY}
* {@link PermissionNames#READ_SKIN_TEMPERATURE}
* {@link PermissionNames#WRITE_SKIN_TEMPERATURE}
* {@link PermissionNames#READ_SLEEP}
* {@link PermissionNames#WRITE_SLEEP}
* {@link PermissionNames#READ_SPEED}
* {@link PermissionNames#WRITE_SPEED}
* {@link PermissionNames#READ_STEPS}
* {@link PermissionNames#WRITE_STEPS}
* {@link PermissionNames#READ_TOTAL_CALORIES_BURNED}
* {@link PermissionNames#WRITE_TOTAL_CALORIES_BURNED}
* {@link PermissionNames#READ_VO2_MAX}
* {@link PermissionNames#WRITE_VO2_MAX}
* {@link PermissionNames#READ_WEIGHT}
* {@link PermissionNames#WRITE_WEIGHT}
* {@link PermissionNames#READ_WHEELCHAIR_PUSHES}
* {@link PermissionNames#WRITE_WHEELCHAIR_PUSHES}
*
* {@link PermissionNames#READ_MEDICAL_DATA_ALLERGIES_INTOLERANCES}
* {@link PermissionNames#READ_MEDICAL_DATA_CONDITIONS}
* {@link PermissionNames#READ_MEDICAL_DATA_LABORATORY_RESULTS}
* {@link PermissionNames#READ_MEDICAL_DATA_MEDICATIONS}
* {@link PermissionNames#READ_MEDICAL_DATA_PERSONAL_DETAILS}
* {@link PermissionNames#READ_MEDICAL_DATA_PRACTITIONER_DETAILS}
* {@link PermissionNames#READ_MEDICAL_DATA_PREGNANCY}
* {@link PermissionNames#READ_MEDICAL_DATA_PROCEDURES}
* {@link PermissionNames#READ_MEDICAL_DATA_SOCIAL_HISTORY}
* {@link PermissionNames#READ_MEDICAL_DATA_VACCINES}
* {@link PermissionNames#READ_MEDICAL_DATA_VISITS}
* {@link PermissionNames#READ_MEDICAL_DATA_VITAL_SIGNS}
* {@link PermissionNames#WRITE_MEDICAL_DATA}
*/
public static final String HEALTH = "health" + SUFFIX;
}
================================================
FILE: library/src/main/java/com/hjq/permissions/permission/PermissionLists.java
================================================
package com.hjq.permissions.permission;
import android.Manifest;
import android.accessibilityservice.AccessibilityService;
import android.app.admin.DeviceAdminReceiver;
import android.service.notification.NotificationListenerService;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.collection.LruCache;
import com.hjq.permissions.permission.base.IPermission;
import com.hjq.permissions.permission.dangerous.AccessBackgroundLocationPermission;
import com.hjq.permissions.permission.dangerous.AccessMediaLocationPermission;
import com.hjq.permissions.permission.dangerous.BluetoothAdvertisePermission;
import com.hjq.permissions.permission.dangerous.BluetoothConnectPermission;
import com.hjq.permissions.permission.dangerous.BluetoothScanPermission;
import com.hjq.permissions.permission.dangerous.BodySensorsBackgroundPermission;
import com.hjq.permissions.permission.dangerous.BodySensorsPermission;
import com.hjq.permissions.permission.dangerous.GetInstalledAppsPermission;
import com.hjq.permissions.permission.dangerous.NearbyWifiDevicesPermission;
import com.hjq.permissions.permission.dangerous.PostNotificationsPermission;
import com.hjq.permissions.permission.dangerous.ReadExternalStoragePermission;
import com.hjq.permissions.permission.dangerous.ReadHealthDataHistoryPermission;
import com.hjq.permissions.permission.dangerous.ReadHealthDataInBackgroundPermission;
import com.hjq.permissions.permission.dangerous.ReadHealthRatePermission;
import com.hjq.permissions.permission.dangerous.ReadMediaAudioPermission;
import com.hjq.permissions.permission.dangerous.ReadMediaImagesPermission;
import com.hjq.permissions.permission.dangerous.ReadMediaVideoPermission;
import com.hjq.permissions.permission.dangerous.ReadMediaVisualUserSelectedPermission;
import com.hjq.permissions.permission.dangerous.ReadPhoneNumbersPermission;
import com.hjq.permissions.permission.dangerous.StandardDangerousPermission;
import com.hjq.permissions.permission.dangerous.StandardFitnessAndWellnessDataPermission;
import com.hjq.permissions.permission.dangerous.StandardHealthRecordsPermission;
import com.hjq.permissions.permission.dangerous.WriteExternalStoragePermission;
import com.hjq.permissions.permission.special.AccessNotificationPolicyPermission;
import com.hjq.permissions.permission.special.BindAccessibilityServicePermission;
import com.hjq.permissions.permission.special.BindDeviceAdminPermission;
import com.hjq.permissions.permission.special.BindNotificationListenerServicePermission;
import com.hjq.permissions.permission.special.BindVpnServicePermission;
import com.hjq.permissions.permission.special.ManageExternalStoragePermission;
import com.hjq.permissions.permission.special.ManageMediaPermission;
import com.hjq.permissions.permission.special.NotificationServicePermission;
import com.hjq.permissions.permission.special.PackageUsageStatsPermission;
import com.hjq.permissions.permission.special.PictureInPicturePermission;
import com.hjq.permissions.permission.special.RequestIgnoreBatteryOptimizationsPermission;
import com.hjq.permissions.permission.special.RequestInstallPackagesPermission;
import com.hjq.permissions.permission.special.ScheduleExactAlarmPermission;
import com.hjq.permissions.permission.special.SystemAlertWindowPermission;
import com.hjq.permissions.permission.special.UseFullScreenIntentPermission;
import com.hjq.permissions.permission.special.WriteSettingsPermission;
import com.hjq.permissions.tools.PermissionVersion;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/06/11
* desc : 危险权限和特殊权限清单,参考 {@link Manifest.permission}
* doc : https://developer.android.google.cn/reference/android/Manifest.permission?hl=zh_cn
* https://developer.android.google.cn/reference/android/health/connect/HealthPermissions
* https://developer.android.google.cn/guide/topics/permissions/overview?hl=zh-cn#normal-dangerous
* http://www.taf.org.cn/upload/AssociationStandard/TTAF%20004-2017%20Android%E6%9D%83%E9%99%90%E8%B0%83%E7%94%A8%E5%BC%80%E5%8F%91%E8%80%85%E6%8C%87%E5%8D%97.pdf
*/
public final class PermissionLists {
/** 私有化构造函数 */
private PermissionLists() {
// default implementation ignored
}
/** 权限数量 */
private static final int PERMISSION_COUNT = 151;
/**
* 权限对象缓存集合
*
* 这里解释一下为什么将 IPermission 对象缓存到集合中?而不是定义成静态变量或者常量?有几个原因:
*
* 1. 如果直接定义成常量或静态变量,会有一个问题,如果项目开启混淆模式(minifyEnabled = true)情况下,
* 未使用到的常量或者静态变量仍然会被保留,不知道 Android Studio 为什么要那么做,但是问题终归还是个问题
* 目前我能找到的最好的解决方式是定义成静态方法,后续如果静态方法没有被调用,后续代码混淆的时候就会被剔除掉。
* 2. 如果直接定义成常量或静态变量,还有另外一个问题,就是一旦有谁第一次访问到本类,就会初始化很多对象,
* 不管这个权限有没有用到,都会在第一次访问的时候初始化完,这样对性能其实不太好的,虽然这点性能微不足道,
* 但是本着能省一点是一点的原则,所以搞了一个静态集合来存放这些权限对象,调用的时候发现没有再去创建。
*/
private static final LruCache PERMISSION_CACHE_MAP = new LruCache<>(PERMISSION_COUNT);
/**
* 获取缓存的权限对象
*
* @param permissionName 权限名称
*/
@Nullable
private static IPermission getCachePermission(@NonNull String permissionName) {
return PERMISSION_CACHE_MAP.get(permissionName);
}
/**
* 添加权限对象到缓存中
*
* @param permission 权限对象
*/
private static IPermission putCachePermission(@NonNull IPermission permission) {
PERMISSION_CACHE_MAP.put(permission.getPermissionName(), permission);
return permission;
}
/**
* 读取应用列表权限(危险权限,电信终端产业协会联合各大中国手机厂商搞的一个权限)
*
* Github issue 地址:https://github.com/getActivity/XXPermissions/issues/175
* 移动终端应用软件列表权限实施指南:http://www.taf.org.cn/StdDetail.aspx?uid=3A7D6656-43B8-4C46-8871-E379A3EA1D48&stdType=TAF
*
* 需要注意的是:
* 1. 需要在清单文件中注册 QUERY_ALL_PACKAGES 权限或者注册 节点,否则在 Android 11 上面就算申请成功也是获取不到已安装应用列表的
* 2. 这个权限在有的手机上面是授予状态,在有的手机上面是还没有授予,在有的手机上面是无法申请,要看手机厂商是否支持这个权限,支持了不一定默认授予
* 3. 如果你贪图方便在清单文件中注册了 QUERY_ALL_PACKAGES 权限,并且 App 需要上架 GooglePlay,注意看一下 GooglePlay 的政策:
* https://support.google.com/googleplay/android-developer/answer/9888170?hl=zh-Hans
*/
@NonNull
public static IPermission getGetInstalledAppsPermission() {
IPermission permission = getCachePermission(GetInstalledAppsPermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new GetInstalledAppsPermission());
}
/**
* 全屏通知权限(特殊权限,Android 14 新增的权限)
*
* 需要注意的是,如果你的应用需要上架 GooglePlay,请慎重添加此权限,相关文档介绍如下:
* 1. 了解前台服务和全屏 intent 要求:https://support.google.com/googleplay/android-developer/answer/13392821?hl=zh-Hans
* 2. GooglePlay 对 Android 14 全屏 Intent 的要求:https://orangeoma.zendesk.com/hc/en-us/articles/14126775576988-Google-Play-requirements-on-Full-screen-intent-for-Android-14
*/
@NonNull
public static IPermission getUseFullScreenIntentPermission() {
IPermission permission = getCachePermission(UseFullScreenIntentPermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new UseFullScreenIntentPermission());
}
/**
* 闹钟权限(特殊权限,Android 12 新增的权限)
*
* 需要注意的是:这个权限和其他特殊权限不同的是,默认已经是授予状态,用户也可以手动撤销授权
* 官方文档介绍:https://developer.android.google.cn/about/versions/12/behavior-changes-12?hl=zh_cn#exact-alarm-permission
* 应用只有在其核心功能支持精确闹钟需求的情况下才能声明此权限。请求此受限权限的应用需要接受审核;如果应用不符合可接受的用例标准,则不允许在 GooglePlay 上发布
* 查看 GooglePlay 对闹钟权限的要求:https://support.google.com/googleplay/android-developer/answer/9888170?hl=zh-Hans
*/
@NonNull
public static IPermission getScheduleExactAlarmPermission() {
IPermission permission = getCachePermission(ScheduleExactAlarmPermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new ScheduleExactAlarmPermission());
}
/**
* 管理媒体权限(特殊权限,Android 12 新增的权限)
*
* 用户可能会信任特定的应用来执行媒体管理,如频繁地修改媒体文件。
* 如果您的应用以 Android 11(API 级别 30)或更高版本为目标平台且不是设备的默认图库应用,
* 则每次您的应用尝试修改或删除文件时,您都必须向用户显示一个确认对话框。
* 如果您的应用以 Android 12 为目标平台,您可以请求用户向您的应用授予执行以下各项操作的权限,而无需针对每项文件操作提示用户。
* 请注意此权限不会直接授予读取或写入权限。它仅会阻止用户针对这些请求进行确认对话框。
*
* 具体介绍请看这里:https://developer.android.google.cn/reference/android/Manifest.permission#MANAGE_MEDIA
*/
@NonNull
public static IPermission getManageMediaPermission() {
IPermission permission = getCachePermission(ManageMediaPermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new ManageMediaPermission());
}
/**
* 所有文件访问权限(特殊权限,Android 11 新增的权限)
*
* 为了兼容 Android 11 以下版本,需要在清单文件中注册
* {@link PermissionNames#READ_EXTERNAL_STORAGE} 和 {@link PermissionNames#WRITE_EXTERNAL_STORAGE} 权限
*
* 如果你的应用需要上架 GooglePlay,那么需要详细阅读谷歌应用商店的政策:
* https://support.google.com/googleplay/android-developer/answer/9956427
*/
@NonNull
public static IPermission getManageExternalStoragePermission() {
IPermission permission = getCachePermission(ManageExternalStoragePermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new ManageExternalStoragePermission());
}
/**
* 安装应用权限(特殊权限,Android 8.0 新增的权限)
*
* Android 11 特性调整,安装外部来源应用需要重启 App:https://cloud.tencent.com/developer/news/637591
* 经过实践,Android 12 已经修复了此问题,授权或者取消授权后应用并不会重启
*/
@NonNull
public static IPermission getRequestInstallPackagesPermission() {
IPermission permission = getCachePermission(RequestInstallPackagesPermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new RequestInstallPackagesPermission());
}
/**
* 画中画权限(特殊权限,Android 8.0 新增的权限,注意此权限不需要在清单文件中注册也能申请)
*
* 需要注意的是:这个权限和其他特殊权限不同的是,默认已经是授予状态,用户也可以手动撤销授权
*/
@NonNull
public static IPermission getPictureInPicturePermission() {
IPermission permission = getCachePermission(PictureInPicturePermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new PictureInPicturePermission());
}
/**
* 悬浮窗权限(特殊权限,Android 6.0 新增的权限,但是有些国产的厂商在 Android 6.0 之前的设备就兼容了)
*
* 在 Android 10 及之前的版本能跳转到应用悬浮窗设置页面,而在 Android 11 及之后的版本只能跳转到系统设置悬浮窗管理列表了
* 官方解释:https://developer.android.google.cn/reference/android/provider/Settings#ACTION_MANAGE_OVERLAY_PERMISSION
*/
@NonNull
public static IPermission getSystemAlertWindowPermission() {
IPermission permission = getCachePermission(SystemAlertWindowPermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new SystemAlertWindowPermission());
}
/**
* 写入系统设置权限(特殊权限,Android 6.0 新增的权限)
*/
@SuppressWarnings("unused")
@NonNull
public static IPermission getWriteSettingsPermission() {
IPermission permission = getCachePermission(WriteSettingsPermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new WriteSettingsPermission());
}
/**
* 请求忽略电池优化选项权限(特殊权限,Android 6.0 新增的权限)
*/
@NonNull
public static IPermission getRequestIgnoreBatteryOptimizationsPermission() {
IPermission permission = getCachePermission(RequestIgnoreBatteryOptimizationsPermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new RequestIgnoreBatteryOptimizationsPermission());
}
/**
* 勿扰权限,可控制手机响铃模式【静音,震动】(特殊权限,Android 6.0 新增的权限)
*/
@NonNull
public static IPermission getAccessNotificationPolicyPermission() {
IPermission permission = getCachePermission(AccessNotificationPolicyPermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new AccessNotificationPolicyPermission());
}
/**
* 查看应用使用情况权限,简称使用统计权限(特殊权限,Android 5.0 新增的权限)
*/
@NonNull
public static IPermission getPackageUsageStatsPermission() {
IPermission permission = getCachePermission(PackageUsageStatsPermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new PackageUsageStatsPermission());
}
/**
* 通知栏监听权限(特殊权限,Android 4.3 新增的权限,注意此权限不需要在清单文件中注册也能申请)
*
* @param notificationListenerServiceClass 通知监听器的 Service 类型
*/
@NonNull
public static IPermission getBindNotificationListenerServicePermission(@NonNull Class extends NotificationListenerService> notificationListenerServiceClass) {
// 该对象不会纳入到缓存的集合中,这是它携带了具体的参数,只有无参的才能丢到缓存的集合中
return new BindNotificationListenerServicePermission(notificationListenerServiceClass);
}
/**
* VPN 权限(特殊权限,Android 4.0 新增的权限,注意此权限不需要在清单文件中注册也能申请)
*/
@NonNull
public static IPermission getBindVpnServicePermission() {
IPermission permission = getCachePermission(BindVpnServicePermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new BindVpnServicePermission());
}
/**
* 通知栏权限(特殊权限,只有 Android 4.4 及以上设备才能判断到权限状态,注意此权限不需要在清单文件中注册也能申请)
*
* @param channelId 通知渠道 id
*/
@NonNull
public static IPermission getNotificationServicePermission(@NonNull String channelId) {
// 该对象不会纳入到缓存的集合中,这是它携带了具体的参数,只有无参的才能丢到缓存的集合中
return new NotificationServicePermission(channelId);
}
/**
* 同上
*/
@NonNull
public static IPermission getNotificationServicePermission() {
IPermission permission = getCachePermission(NotificationServicePermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new NotificationServicePermission());
}
/**
* 无障碍服务权限(特殊权限,Android 4.1 新增的权限,注意此权限不需要在清单文件中注册也能申请)
*
* @param accessibilityServiceClass 无障碍 Service 类
*/
@NonNull
public static IPermission getBindAccessibilityServicePermission(@NonNull Class extends AccessibilityService> accessibilityServiceClass) {
return new BindAccessibilityServicePermission(accessibilityServiceClass);
}
/**
* 设备管理权限(特殊权限,Android 2.2 新增的权限,注意此权限不需要在清单文件中注册也能申请)
*
* @param deviceAdminReceiverClass 设备管理器的 BroadcastReceiver 类
* @param extraAddExplanation 申请设备管理器权限的附加说明
*/
@NonNull
public static IPermission getBindDeviceAdminPermission(@NonNull Class extends DeviceAdminReceiver> deviceAdminReceiverClass, @Nullable String extraAddExplanation) {
return new BindDeviceAdminPermission(deviceAdminReceiverClass, extraAddExplanation);
}
/**
* 同上
*/
@NonNull
public static IPermission getBindDeviceAdminPermission(@NonNull Class extends DeviceAdminReceiver> deviceAdminReceiverClass) {
return new BindDeviceAdminPermission(deviceAdminReceiverClass, null);
}
/* ------------------------------------ 我是一条华丽的分割线 ------------------------------------ */
/**
* 访问部分照片和视频的权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadMediaVisualUserSelectedPermission() {
IPermission permission = getCachePermission(ReadMediaVisualUserSelectedPermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new ReadMediaVisualUserSelectedPermission());
}
/**
* 发送通知权限(Android 13.0 新增的权限)
*
* 为了向下兼容,框架会自动在旧的安卓设备上自动添加 {@link PermissionLists#getNotificationServicePermission()} 权限进行动态申请,无需你手动添加
*/
@NonNull
public static IPermission getPostNotificationsPermission() {
IPermission permission = getCachePermission(PostNotificationsPermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new PostNotificationsPermission());
}
/**
* WIFI 权限(Android 13.0 新增的权限)
*
* 需要在清单文件中加入 android:usesPermissionFlags="neverForLocation" 属性(表示不推导设备地理位置)
* 否则就会导致在没有定位权限的情况下扫描不到附近的 WIFI 设备,这个是经过测试的,下面是清单权限注册案例,请参考以下进行注册
*
*
* 为了兼容 Android 13 以下版本,需要清单文件中注册 {@link PermissionNames#ACCESS_FINE_LOCATION} 权限
* 另外框架会自动在旧的安卓设备上自动添加 {@link PermissionLists#getAccessFineLocationPermission()} 权限进行动态申请,无需你手动添加
*/
@NonNull
public static IPermission getNearbyWifiDevicesPermission() {
IPermission permission = getCachePermission(NearbyWifiDevicesPermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new NearbyWifiDevicesPermission());
}
/**
* 后台传感器权限(Android 13.0 新增的权限)
*
* 需要注意的是:
* 1. 一旦你申请了该权限,在授权的时候,需要选择《始终允许》,而不能选择《仅在使用中允许》
* 2. 如果你的 App 只在前台状态下使用传感器功能,请不要申请该权限(后台传感器权限)
*/
@NonNull
public static IPermission getBodySensorsBackgroundPermission() {
IPermission permission = getCachePermission(BodySensorsBackgroundPermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new BodySensorsBackgroundPermission());
}
/**
* 读取图片权限(Android 13.0 新增的权限)
*
* 为了兼容 Android 13 以下版本,需要在清单文件中注册 {@link PermissionNames#READ_EXTERNAL_STORAGE} 权限
* 另外框架会自动在旧的安卓设备上自动添加 {@link PermissionLists#getReadExternalStoragePermission()} 权限进行动态申请,无需你手动添加
*/
@NonNull
public static IPermission getReadMediaImagesPermission() {
IPermission permission = getCachePermission(ReadMediaImagesPermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new ReadMediaImagesPermission());
}
/**
* 读取视频权限(Android 13.0 新增的权限)
*
* 为了兼容 Android 13 以下版本,需要在清单文件中注册 {@link PermissionNames#READ_EXTERNAL_STORAGE} 权限
* 另外框架会自动在旧的安卓设备上自动添加 {@link PermissionLists#getReadExternalStoragePermission()} 权限进行动态申请,无需你手动添加
*/
@NonNull
public static IPermission getReadMediaVideoPermission() {
IPermission permission = getCachePermission(ReadMediaVideoPermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new ReadMediaVideoPermission());
}
/**
* 读取音频权限(Android 13.0 新增的权限)
*
* 为了兼容 Android 13 以下版本,需要在清单文件中注册 {@link PermissionNames#READ_EXTERNAL_STORAGE} 权限
* 另外框架会自动在旧的安卓设备上自动添加 {@link PermissionLists#getReadExternalStoragePermission()} 权限进行动态申请,无需你手动添加
*/
@NonNull
public static IPermission getReadMediaAudioPermission() {
IPermission permission = getCachePermission(ReadMediaAudioPermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new ReadMediaAudioPermission());
}
/**
* 蓝牙扫描权限(Android 12.0 新增的权限)
*
* 需要在清单文件中加入 android:usesPermissionFlags="neverForLocation" 属性(表示不推导设备地理位置)
* 否则就会导致在没有定位权限的情况下扫描不到附近的蓝牙设备,这个是经过测试的,下面是清单权限注册案例,请参考以下进行注册
*
*
* 为了兼容 Android 12 以下版本,需要清单文件中注册 {@link Manifest.permission#BLUETOOTH_ADMIN} 和 {@link PermissionNames#ACCESS_FINE_LOCATION} 权限
* 另外框架会自动在旧的安卓设备上自动添加 {@link PermissionLists#getAccessFineLocationPermission()} 权限进行动态申请,无需你手动添加
*/
@NonNull
public static IPermission getBluetoothScanPermission() {
IPermission permission = getCachePermission(BluetoothScanPermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new BluetoothScanPermission());
}
/**
* 蓝牙连接权限(Android 12.0 新增的权限)
*
* 为了兼容 Android 12 以下版本,需要在清单文件中注册 {@link Manifest.permission#BLUETOOTH} 权限
*/
@NonNull
public static IPermission getBluetoothConnectPermission() {
IPermission permission = getCachePermission(BluetoothConnectPermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new BluetoothConnectPermission());
}
/**
* 蓝牙广播权限(Android 12.0 新增的权限)
*
* 将当前设备的蓝牙进行广播,供其他设备扫描时需要用到该权限
* 为了兼容 Android 12 以下版本,需要在清单文件中注册 {@link Manifest.permission#BLUETOOTH_ADMIN} 权限
*/
@NonNull
public static IPermission getBluetoothAdvertisePermission() {
IPermission permission = getCachePermission(BluetoothAdvertisePermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new BluetoothAdvertisePermission());
}
/**
* 在后台获取位置权限(Android 10.0 新增的权限)
*
* 需要注意的是:
* 1. 一旦你申请了该权限,在授权的时候,需要选择《始终允许》,而不能选择《仅在使用中允许》
* 2. 如果你的 App 只在前台状态下使用定位功能,没有在后台使用的场景,请不要申请该权限
*/
@NonNull
public static IPermission getAccessBackgroundLocationPermission() {
IPermission permission = getCachePermission(AccessBackgroundLocationPermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new AccessBackgroundLocationPermission());
}
/**
* 获取活动步数权限(Android 10.0 新增的权限)
*
* 需要注意的是:Android 10 以下不需要传感器(BODY_SENSORS)权限也能获取到步数
* Github issue 地址:https://github.com/getActivity/XXPermissions/issues/150
*/
@NonNull
public static IPermission getActivityRecognitionPermission() {
String permissionName = PermissionNames.ACTIVITY_RECOGNITION;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardDangerousPermission(permissionName, PermissionVersion.ANDROID_10));
}
/**
* 访问媒体的位置信息权限(Android 10.0 新增的权限)
*
* 需要注意的是:如果这个权限申请成功了但是不能正常读取照片的地理信息,那么需要先申请存储权限,具体可分别下面两种情况:
*
* 1. 如果适配了分区存储的情况下:
* 1) 如果项目 targetSdkVersion <= 32 需要申请 {@link PermissionLists#getReadExternalStoragePermission()}
* 2) 如果项目 targetSdkVersion >= 33 需要申请 {@link PermissionLists#getReadMediaImagesPermission()} 或者
* {@link PermissionLists#getReadMediaVideoPermission()},并且需要授予全部,不能选择授予部分
*
* 2. 如果没有适配分区存储的情况下:
* 1) 如果项目 targetSdkVersion <= 29 需要申请 {@link PermissionLists#getReadExternalStoragePermission()}
* 2) 如果项目 targetSdkVersion >= 30 需要申请 {@link PermissionLists#getManageExternalStoragePermission()}
*/
@NonNull
public static IPermission getAccessMediaLocationPermission() {
IPermission permission = getCachePermission(AccessMediaLocationPermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new AccessMediaLocationPermission());
}
/**
* 允许呼叫应用继续在另一个应用中启动的呼叫权限(Android 9.0 新增的权限)
*
* 需要注意:此权限在一些无法拨打电话的设备(例如:小米平板 5)上面申请,系统会直接回调失败,如有进行申请,请留意处理权限申请失败的情况
*/
@NonNull
public static IPermission getAcceptHandoverPermission() {
String permissionName = PermissionNames.ACCEPT_HANDOVER;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardDangerousPermission(permissionName, PermissionGroups.PHONE, PermissionVersion.ANDROID_9));
}
/**
* 读取手机号码权限(Android 8.0 新增的权限)
*
* 需要注意:此权限在一些无法拨打电话的设备(例如:小米平板 5)上面申请,系统会直接回调成功,但是这非必然,如有进行申请,还需留意处理权限申请失败的情况
*
* 为了兼容 Android 8.0 以下版本,需要在清单文件中注册 {@link PermissionNames#READ_PHONE_STATE} 权限
* 另外框架会自动在旧的安卓设备上自动添加 {@link PermissionLists#getReadPhoneStatePermission()} 权限进行动态申请,无需你手动添加
*/
@NonNull
public static IPermission getReadPhoneNumbersPermission() {
IPermission permission = getCachePermission(ReadPhoneNumbersPermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new ReadPhoneNumbersPermission());
}
/**
* 接听电话权限(Android 8.0 新增的权限,Android 8.0 以下可以采用模拟耳机按键事件来实现接听电话,这种方式不需要权限)
*
* 需要注意:此权限在一些无法拨打电话的设备(例如:小米平板 5)上面申请,系统会直接回调失败,如有进行申请,请留意处理权限申请失败的情况
*/
@NonNull
public static IPermission getAnswerPhoneCallsPermission() {
String permissionName = PermissionNames.ANSWER_PHONE_CALLS;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardDangerousPermission(permissionName, PermissionGroups.PHONE, PermissionVersion.ANDROID_8));
}
/**
* 读取外部存储权限
*/
@NonNull
public static IPermission getReadExternalStoragePermission() {
IPermission permission = getCachePermission(ReadExternalStoragePermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new ReadExternalStoragePermission());
}
/**
* 写入外部存储权限(注意:这个权限在 targetSdk >= Android 11 并且 Android 11 及以上的设备上面不起作用,请适配分区存储特性代替权限申请)
*/
@NonNull
public static IPermission getWriteExternalStoragePermission() {
IPermission permission = getCachePermission(WriteExternalStoragePermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new WriteExternalStoragePermission());
}
/**
* 相机权限
*/
@NonNull
public static IPermission getCameraPermission() {
String permissionName = PermissionNames.CAMERA;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardDangerousPermission(permissionName, PermissionVersion.ANDROID_6));
}
/**
* 麦克风权限
*/
@NonNull
public static IPermission getRecordAudioPermission() {
String permissionName = PermissionNames.RECORD_AUDIO;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardDangerousPermission(permissionName, PermissionVersion.ANDROID_6));
}
/**
* 获取精确位置权限
*/
@NonNull
public static IPermission getAccessFineLocationPermission() {
String permissionName = PermissionNames.ACCESS_FINE_LOCATION;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardDangerousPermission(permissionName, PermissionGroups.LOCATION, PermissionVersion.ANDROID_6));
}
/**
* 获取粗略位置权限
*/
@NonNull
public static IPermission getAccessCoarseLocationPermission() {
String permissionName = PermissionNames.ACCESS_COARSE_LOCATION;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardDangerousPermission(permissionName, PermissionGroups.LOCATION, PermissionVersion.ANDROID_6));
}
/**
* 读取联系人权限
*/
@NonNull
public static IPermission getReadContactsPermission() {
String permissionName = PermissionNames.READ_CONTACTS;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardDangerousPermission(permissionName, PermissionGroups.CONTACTS, PermissionVersion.ANDROID_6));
}
/**
* 修改联系人权限
*/
@NonNull
public static IPermission getWriteContactsPermission() {
String permissionName = PermissionNames.WRITE_CONTACTS;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardDangerousPermission(permissionName, PermissionGroups.CONTACTS, PermissionVersion.ANDROID_6));
}
/**
* 访问账户列表权限
*/
@NonNull
public static IPermission getGetAccountsPermission() {
String permissionName = PermissionNames.GET_ACCOUNTS;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardDangerousPermission(permissionName, PermissionGroups.CONTACTS, PermissionVersion.ANDROID_6));
}
/**
* 读取日历权限
*/
@NonNull
public static IPermission getReadCalendarPermission() {
String permissionName = PermissionNames.READ_CALENDAR;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardDangerousPermission(permissionName, PermissionGroups.CALENDAR, PermissionVersion.ANDROID_6));
}
/**
* 修改日历权限
*/
@NonNull
public static IPermission getWriteCalendarPermission() {
String permissionName = PermissionNames.WRITE_CALENDAR;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardDangerousPermission(permissionName, PermissionGroups.CALENDAR, PermissionVersion.ANDROID_6));
}
/**
* 读取电话状态权限,需要注意的是:
*
* 1. 这个权限在某些手机上面是没办法获取到的,因为某些系统禁止应用获得该权限
* 所以你要是申请了这个权限之后没有弹授权框,而是直接回调授权失败方法
* 请不要惊慌,这个不是 Bug、不是 Bug、不是 Bug,而是正常现象
* 后续情况汇报:有人反馈在 iQOO 手机上面获取不到该权限,在清单文件加入下面这个权限就可以了(这里只是做记录,并不代表这种方式就一定有效果)
*
* Github issue 地址:https://github.com/getActivity/XXPermissions/issues/98
*
* 2. 这个权限在某些手机上面申请是直接通过的,但是系统没有弹出授权对话框,实际上也是没有授权
* 这个也不是 Bug,而是系统故意就是这么做的,你要问我怎么办,我只能说胳膊拗不过大腿
* Github issue 地址:https://github.com/getActivity/XXPermissions/issues/369
*/
@NonNull
public static IPermission getReadPhoneStatePermission() {
String permissionName = PermissionNames.READ_PHONE_STATE;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardDangerousPermission(permissionName, PermissionGroups.PHONE, PermissionVersion.ANDROID_6));
}
/**
* 拨打电话权限
*
* 需要注意:此权限在一些无法拨打电话的设备(例如:小米平板 5)上面申请,系统会直接回调失败,如有进行申请,请留意处理权限申请失败的情况
*/
@NonNull
public static IPermission getCallPhonePermission() {
String permissionName = PermissionNames.CALL_PHONE;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardDangerousPermission(permissionName, PermissionGroups.PHONE, PermissionVersion.ANDROID_6));
}
/**
* 读取通话记录权限
*
* 需要注意:此权限在一些无法拨打电话的设备(例如:小米平板 5)上面申请,系统会直接回调失败,如有进行申请,请留意处理权限申请失败的情况
*/
@NonNull
public static IPermission getReadCallLogPermission() {
String permissionName = PermissionNames.READ_CALL_LOG;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
// 注意:在 Android 9.0 的时候,通话记录相关的权限已经归到一个单独的权限组了,但是在 Android 9.0 之前,读写通话记录权限归属电话权限组
String permissionGroup = PermissionVersion.isAndroid9() ? PermissionGroups.CALL_LOG : PermissionGroups.PHONE;
return putCachePermission(new StandardDangerousPermission(permissionName, permissionGroup, PermissionVersion.ANDROID_6));
}
/**
* 修改通话记录权限
*
* 需要注意:此权限在一些无法拨打电话的设备(例如:小米平板 5)上面申请,系统会直接回调失败,如有进行申请,请留意处理权限申请失败的情况
*/
@NonNull
public static IPermission getWriteCallLogPermission() {
String permissionName = PermissionNames.WRITE_CALL_LOG;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
// 注意:在 Android 9.0 的时候,通话记录相关的权限已经归到一个单独的权限组了,但是在 Android 9.0 之前,读写通话记录权限归属电话权限组
String permissionGroup = PermissionVersion.isAndroid9() ? PermissionGroups.CALL_LOG : PermissionGroups.PHONE;
return putCachePermission(new StandardDangerousPermission(permissionName, permissionGroup, PermissionVersion.ANDROID_6));
}
/**
* 添加语音邮件权限
*/
@NonNull
public static IPermission getAddVoicemailPermission() {
String permissionName = PermissionNames.ADD_VOICEMAIL;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardDangerousPermission(permissionName, PermissionGroups.PHONE, PermissionVersion.ANDROID_6));
}
/**
* 使用 SIP 视频权限
*/
@NonNull
public static IPermission getUseSipPermission() {
String permissionName = PermissionNames.USE_SIP;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardDangerousPermission(permissionName, PermissionGroups.PHONE, PermissionVersion.ANDROID_6));
}
/**
* 处理拨出电话权限
*
* 需要注意:此权限在一些无法拨打电话的设备(例如:小米平板 5)上面申请,系统会直接回调失败,如有进行申请,请留意处理权限申请失败的情况
*
* @deprecated 在 Android 10 已经过时,请见:https://developer.android.google.cn/reference/android/Manifest.permission?hl=zh_cn#PROCESS_OUTGOING_CALLS
*/
@NonNull
public static IPermission getProcessOutgoingCallsPermission() {
String permissionName = PermissionNames.PROCESS_OUTGOING_CALLS;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
// 注意:在 Android 9.0 的时候,通话记录相关的权限已经归到一个单独的权限组了,但是在 Android 9.0 之前,读写通话记录权限归属电话权限组
String permissionGroup = PermissionVersion.isAndroid9() ? PermissionGroups.CALL_LOG : PermissionGroups.PHONE;
return putCachePermission(new StandardDangerousPermission(permissionName, permissionGroup, PermissionVersion.ANDROID_6));
}
/**
* 使用传感器权限
*/
@NonNull
public static IPermission getBodySensorsPermission() {
IPermission permission = getCachePermission(BodySensorsPermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new BodySensorsPermission());
}
/**
* 发送短信权限
*
* 需要注意:此权限在一些无法发送短信的设备(例如:小米平板 5)上面申请,系统会直接回调失败,如有进行申请,请留意处理权限申请失败的情况
*/
@NonNull
public static IPermission getSendSmsPermission() {
String permissionName = PermissionNames.SEND_SMS;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardDangerousPermission(permissionName, PermissionGroups.SMS, PermissionVersion.ANDROID_6));
}
/**
* 接收短信权限
*
* 需要注意:此权限在一些无法发送短信的设备(例如:小米平板 5)上面申请,系统会直接回调失败,如有进行申请,请留意处理权限申请失败的情况
*/
@NonNull
public static IPermission getReceiveSmsPermission() {
String permissionName = PermissionNames.RECEIVE_SMS;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardDangerousPermission(permissionName, PermissionGroups.SMS, PermissionVersion.ANDROID_6));
}
/**
* 读取短信权限
*
* 需要注意:此权限在一些无法发送短信的设备(例如:小米平板 5)上面申请,系统会直接回调失败,如有进行申请,请留意处理权限申请失败的情况
*/
@NonNull
public static IPermission getReadSmsPermission() {
String permissionName = PermissionNames.READ_SMS;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardDangerousPermission(permissionName, PermissionGroups.SMS, PermissionVersion.ANDROID_6));
}
/**
* 接收 WAP 推送消息权限
*
* 需要注意:此权限在一些无法发送短信的设备(例如:小米平板 5)上面申请,系统会直接回调成功,但是这非必然,如有进行申请,还需留意处理权限申请失败的情况
*/
@NonNull
public static IPermission getReceiveWapPushPermission() {
String permissionName = PermissionNames.RECEIVE_WAP_PUSH;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardDangerousPermission(permissionName, PermissionGroups.SMS, PermissionVersion.ANDROID_6));
}
/**
* 接收彩信权限
*
* 需要注意:此权限在一些无法发送短信的设备(例如:小米平板 5)上面申请,系统会直接回调成功,但是这非必然,如有进行申请,还需留意处理权限申请失败的情况
*/
@NonNull
public static IPermission getReceiveMmsPermission() {
String permissionName = PermissionNames.RECEIVE_MMS;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardDangerousPermission(permissionName, PermissionGroups.SMS, PermissionVersion.ANDROID_6));
}
/* ------------------------------------ 我是一条华丽的分割线 ------------------------------------ */
/**
* 获取后台读取健康数据权限(Android 15.0 新增的权限)
*
* 为了兼容 Android 15 以下版本,需要在清单文件中注册 {@link PermissionNames#BODY_SENSORS_BACKGROUND} 权限
* 另外框架会自动在旧的安卓设备上自动添加 {@link PermissionLists#getReadHealthDataInBackgroundPermission()} ()} 权限进行动态申请,无需你手动添加
*/
@NonNull
public static IPermission getReadHealthDataInBackgroundPermission() {
String permissionName = ReadHealthDataInBackgroundPermission.PERMISSION_NAME;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new ReadHealthDataInBackgroundPermission());
}
/**
* 获取读取历史的健康数据权限(Android 15.0 新增的权限)
*
* Health Connect 可以读取授予权限前最多 30 天的数据。如果您希望应用读取 30 天之前的记录,请需要申请此权限,相关文档地址:
* https://developer.android.google.cn/health-and-fitness/guides/health-connect/develop/read-data?hl=zh-cn#read-older-data
*/
@NonNull
public static IPermission getReadHealthDataHistoryPermission() {
String permissionName = ReadHealthDataHistoryPermission.PERMISSION_NAME;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new ReadHealthDataHistoryPermission());
}
/**
* 获取读取运动消耗的卡路里数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadActiveCaloriesBurnedPermission() {
String permissionName = PermissionNames.READ_ACTIVE_CALORIES_BURNED;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入运动消耗的卡路里数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteActiveCaloriesBurnedPermission() {
String permissionName = PermissionNames.WRITE_ACTIVE_CALORIES_BURNED;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取活动强度数据权限(Android 16.0 新增的权限)
*/
@NonNull
public static IPermission getReadActivityIntensityPermission() {
String permissionName = PermissionNames.READ_ACTIVITY_INTENSITY;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_16));
}
/**
* 获取写入活动强度数据权限(Android 16.0 新增的权限)
*/
@NonNull
public static IPermission getWriteActivityIntensityPermission() {
String permissionName = PermissionNames.WRITE_ACTIVITY_INTENSITY;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_16));
}
/**
* 获取读取基础体温数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadBasalBodyTemperaturePermission() {
String permissionName = PermissionNames.READ_BASAL_BODY_TEMPERATURE;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入基础体温数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteBasalBodyTemperaturePermission() {
String permissionName = PermissionNames.WRITE_BASAL_BODY_TEMPERATURE;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取基础代谢率数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadBasalMetabolicRatePermission() {
String permissionName = PermissionNames.READ_BASAL_METABOLIC_RATE;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入基础代谢率数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteBasalMetabolicRatePermission() {
String permissionName = PermissionNames.WRITE_BASAL_METABOLIC_RATE;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取血糖数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadBloodGlucosePermission() {
String permissionName = PermissionNames.READ_BLOOD_GLUCOSE;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入血糖数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteBloodGlucosePermission() {
String permissionName = PermissionNames.WRITE_BLOOD_GLUCOSE;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取血压数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadBloodPressurePermission() {
String permissionName = PermissionNames.READ_BLOOD_PRESSURE;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入血压数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteBloodPressurePermission() {
String permissionName = PermissionNames.WRITE_BLOOD_PRESSURE;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取体脂数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadBodyFatPermission() {
String permissionName = PermissionNames.READ_BODY_FAT;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入体脂数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteBodyFatPermission() {
String permissionName = PermissionNames.WRITE_BODY_FAT;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取体温数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadBodyTemperaturePermission() {
String permissionName = PermissionNames.READ_BODY_TEMPERATURE;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入体温数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteBodyTemperaturePermission() {
String permissionName = PermissionNames.WRITE_BODY_TEMPERATURE;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取身体含水量数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadBodyWaterMassPermission() {
String permissionName = PermissionNames.READ_BODY_WATER_MASS;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入身体含水量数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteBodyWaterMassPermission() {
String permissionName = PermissionNames.WRITE_BODY_WATER_MASS;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取骨质密度数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadBoneMassPermission() {
String permissionName = PermissionNames.READ_BONE_MASS;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入骨质密度数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteBoneMassPermission() {
String permissionName = PermissionNames.WRITE_BONE_MASS;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取宫颈粘液数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadCervicalMucusPermission() {
String permissionName = PermissionNames.READ_CERVICAL_MUCUS;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入宫颈粘液数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteCervicalMucusPermission() {
String permissionName = PermissionNames.WRITE_CERVICAL_MUCUS;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取距离数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadDistancePermission() {
String permissionName = PermissionNames.READ_DISTANCE;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入距离数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteDistancePermission() {
String permissionName = PermissionNames.WRITE_DISTANCE;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取爬升高度数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadElevationGainedPermission() {
String permissionName = PermissionNames.READ_ELEVATION_GAINED;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入爬升高度数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteElevationGainedPermission() {
String permissionName = PermissionNames.WRITE_ELEVATION_GAINED;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取锻炼数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadExercisePermission() {
String permissionName = PermissionNames.READ_EXERCISE;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入锻炼数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteExercisePermission() {
String permissionName = PermissionNames.WRITE_EXERCISE;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取锻炼路线数据权限(Android 15.0 新增的权限)
*/
@NonNull
public static IPermission getReadExerciseRoutesPermission() {
String permissionName = PermissionNames.READ_EXERCISE_ROUTES;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_15));
}
/**
* 获取写入锻炼路线数据权限(Android 15.0 新增的权限)
*/
@NonNull
public static IPermission getWriteExerciseRoutePermission() {
String permissionName = PermissionNames.WRITE_EXERCISE_ROUTE;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_15));
}
/**
* 获取读取爬楼层数数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadFloorsClimbedPermission() {
String permissionName = PermissionNames.READ_FLOORS_CLIMBED;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入爬楼层数数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteFloorsClimbedPermission() {
String permissionName = PermissionNames.WRITE_FLOORS_CLIMBED;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取心率数据权限(Android 14.0 新增的权限)
*
* 为了兼容 Android 14 以下版本,需要在清单文件中注册 {@link PermissionNames#BODY_SENSORS} 权限
* 另外框架会自动在旧的安卓设备上自动添加 {@link PermissionLists#getBodySensorsPermission()} 权限进行动态申请,无需你手动添加
*/
@NonNull
public static IPermission getReadHeartRatePermission() {
IPermission permission = getCachePermission(ReadHealthRatePermission.PERMISSION_NAME);
if (permission != null) {
return permission;
}
return putCachePermission(new ReadHealthRatePermission());
}
/**
* 获取写入心率数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteHeartRatePermission() {
String permissionName = PermissionNames.WRITE_HEART_RATE;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取心率变异性数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadHeartRateVariabilityPermission() {
String permissionName = PermissionNames.READ_HEART_RATE_VARIABILITY;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入心率变异性数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteHeartRateVariabilityPermission() {
String permissionName = PermissionNames.WRITE_HEART_RATE_VARIABILITY;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取身高数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadHeightPermission() {
String permissionName = PermissionNames.READ_HEIGHT;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入身高数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteHeightPermission() {
String permissionName = PermissionNames.WRITE_HEIGHT;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取饮水量权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadHydrationPermission() {
String permissionName = PermissionNames.READ_HYDRATION;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入饮水量权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteHydrationPermission() {
String permissionName = PermissionNames.WRITE_HYDRATION;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取点状出血数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadIntermenstrualBleedingPermission() {
String permissionName = PermissionNames.READ_INTERMENSTRUAL_BLEEDING;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入点状出血数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteIntermenstrualBleedingPermission() {
String permissionName = PermissionNames.WRITE_INTERMENSTRUAL_BLEEDING;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取净体重数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadLeanBodyMassPermission() {
String permissionName = PermissionNames.READ_LEAN_BODY_MASS;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入净体重数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteLeanBodyMassPermission() {
String permissionName = PermissionNames.WRITE_LEAN_BODY_MASS;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取经期数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadMenstruationPermission() {
String permissionName = PermissionNames.READ_MENSTRUATION;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入经期数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteMenstruationPermission() {
String permissionName = PermissionNames.WRITE_MENSTRUATION;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取正念数据权限(Android 16.0 新增的权限)
*/
@NonNull
public static IPermission getReadMindfulnessPermission() {
String permissionName = PermissionNames.READ_MINDFULNESS;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_16));
}
/**
* 获取写入正念数据权限(Android 16.0 新增的权限)
*/
@NonNull
public static IPermission getWriteMindfulnessPermission() {
String permissionName = PermissionNames.WRITE_MINDFULNESS;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_16));
}
/**
* 获取读取营养数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadNutritionPermission() {
String permissionName = PermissionNames.READ_NUTRITION;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入营养数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteNutritionPermission() {
String permissionName = PermissionNames.WRITE_NUTRITION;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取排卵检测数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadOvulationTestPermission() {
String permissionName = PermissionNames.READ_OVULATION_TEST;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入排卵检测数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteOvulationTestPermission() {
String permissionName = PermissionNames.WRITE_OVULATION_TEST;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取血氧饱和度数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadOxygenSaturationPermission() {
String permissionName = PermissionNames.READ_OXYGEN_SATURATION;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入血氧饱和度数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteOxygenSaturationPermission() {
String permissionName = PermissionNames.WRITE_OXYGEN_SATURATION;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取训练计划数据权限(Android 15.0 新增的权限)
*/
@NonNull
public static IPermission getReadPlannedExercisePermission() {
String permissionName = PermissionNames.READ_PLANNED_EXERCISE;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_15));
}
/**
* 获取写入训练计划数据权限(Android 15.0 新增的权限)
*/
@NonNull
public static IPermission getWritePlannedExercisePermission() {
String permissionName = PermissionNames.WRITE_PLANNED_EXERCISE;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_15));
}
/**
* 获取读取体能数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadPowerPermission() {
String permissionName = PermissionNames.READ_POWER;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入体能数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWritePowerPermission() {
String permissionName = PermissionNames.WRITE_POWER;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取呼吸频率数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadRespiratoryRatePermission() {
String permissionName = PermissionNames.READ_RESPIRATORY_RATE;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入呼吸频率数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteRespiratoryRatePermission() {
String permissionName = PermissionNames.WRITE_RESPIRATORY_RATE;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取静息心率数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadRestingHeartRatePermission() {
String permissionName = PermissionNames.READ_RESTING_HEART_RATE;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入静息心率数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteRestingHeartRatePermission() {
String permissionName = PermissionNames.WRITE_RESTING_HEART_RATE;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取性活动数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadSexualActivityPermission() {
String permissionName = PermissionNames.READ_SEXUAL_ACTIVITY;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入性活动数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteSexualActivityPermission() {
String permissionName = PermissionNames.WRITE_SEXUAL_ACTIVITY;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取体表温度数据权限(Android 15.0 新增的权限)
*/
@NonNull
public static IPermission getReadSkinTemperaturePermission() {
String permissionName = PermissionNames.READ_SKIN_TEMPERATURE;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_15));
}
/**
* 获取写入体表温度数据权限(Android 15.0 新增的权限)
*/
@NonNull
public static IPermission getWriteSkinTemperaturePermission() {
String permissionName = PermissionNames.WRITE_SKIN_TEMPERATURE;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_15));
}
/**
* 获取读取睡眠数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadSleepPermission() {
String permissionName = PermissionNames.READ_SLEEP;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入睡眠数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteSleepPermission() {
String permissionName = PermissionNames.WRITE_SLEEP;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取速度数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadSpeedPermission() {
String permissionName = PermissionNames.READ_SPEED;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入速度数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteSpeedPermission() {
String permissionName = PermissionNames.WRITE_SPEED;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取步数数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadStepsPermission() {
String permissionName = PermissionNames.READ_STEPS;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入步数数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteStepsPermission() {
String permissionName = PermissionNames.WRITE_STEPS;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取消耗的卡路里总数数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadTotalCaloriesBurnedPermission() {
String permissionName = PermissionNames.READ_TOTAL_CALORIES_BURNED;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入消耗的卡路里总数数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteTotalCaloriesBurnedPermission() {
String permissionName = PermissionNames.WRITE_TOTAL_CALORIES_BURNED;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取最大摄氧量数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadVo2MaxPermission() {
String permissionName = PermissionNames.READ_VO2_MAX;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入最大摄氧量数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteVo2MaxPermission() {
String permissionName = PermissionNames.WRITE_VO2_MAX;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取体重数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadWeightPermission() {
String permissionName = PermissionNames.READ_WEIGHT;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入体重数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteWeightPermission() {
String permissionName = PermissionNames.WRITE_WEIGHT;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取读取推轮椅次数数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getReadWheelchairPushesPermission() {
String permissionName = PermissionNames.READ_WHEELCHAIR_PUSHES;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/**
* 获取写入推轮椅次数数据权限(Android 14.0 新增的权限)
*/
@NonNull
public static IPermission getWriteWheelchairPushesPermission() {
String permissionName = PermissionNames.WRITE_WHEELCHAIR_PUSHES;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardFitnessAndWellnessDataPermission(permissionName, PermissionVersion.ANDROID_14));
}
/* ------------------------------------ 我是一条华丽的分割线 ------------------------------------ */
/**
* 获取读取过敏反应数据权限(Android 16.0 新增的权限)
*/
@NonNull
public static IPermission getReadMedicalDataAllergiesIntolerancesPermission() {
String permissionName = PermissionNames.READ_MEDICAL_DATA_ALLERGIES_INTOLERANCES;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardHealthRecordsPermission(permissionName, PermissionVersion.ANDROID_16));
}
/**
* 获取读取病症数据权限(Android 16.0 新增的权限)
*/
@NonNull
public static IPermission getReadMedicalDataConditionsPermission() {
String permissionName = PermissionNames.READ_MEDICAL_DATA_CONDITIONS;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardHealthRecordsPermission(permissionName, PermissionVersion.ANDROID_16));
}
/**
* 获取读取化验结果数据权限(Android 16.0 新增的权限)
*/
@NonNull
public static IPermission getReadMedicalDataLaboratoryResultsPermission() {
String permissionName = PermissionNames.READ_MEDICAL_DATA_LABORATORY_RESULTS;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardHealthRecordsPermission(permissionName, PermissionVersion.ANDROID_16));
}
/**
* 获取读取用药情况数据权限(Android 16.0 新增的权限)
*/
@NonNull
public static IPermission getReadMedicalDataMedicationsPermission() {
String permissionName = PermissionNames.READ_MEDICAL_DATA_MEDICATIONS;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardHealthRecordsPermission(permissionName, PermissionVersion.ANDROID_16));
}
/**
* 获取读取个人详细信息数据权限(Android 16.0 新增的权限)
*/
@NonNull
public static IPermission getReadMedicalDataPersonalDetailsPermission() {
String permissionName = PermissionNames.READ_MEDICAL_DATA_PERSONAL_DETAILS;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardHealthRecordsPermission(permissionName, PermissionVersion.ANDROID_16));
}
/**
* 获取读取就医情况数据权限(Android 16.0 新增的权限)
*/
@NonNull
public static IPermission getReadMedicalDataPractitionerDetailsPermission() {
String permissionName = PermissionNames.READ_MEDICAL_DATA_PRACTITIONER_DETAILS;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardHealthRecordsPermission(permissionName, PermissionVersion.ANDROID_16));
}
/**
* 获取读取怀孕情况数据权限(Android 16.0 新增的权限)
*/
@NonNull
public static IPermission getReadMedicalDataPregnancyPermission() {
String permissionName = PermissionNames.READ_MEDICAL_DATA_PREGNANCY;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardHealthRecordsPermission(permissionName, PermissionVersion.ANDROID_16));
}
/**
* 获取读取医疗程序数据权限(Android 16.0 新增的权限)
*/
@NonNull
public static IPermission getReadMedicalDataProceduresPermission() {
String permissionName = PermissionNames.READ_MEDICAL_DATA_PROCEDURES;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardHealthRecordsPermission(permissionName, PermissionVersion.ANDROID_16));
}
/**
* 获取读取个人生活史数据权限(Android 16.0 新增的权限)
*/
@NonNull
public static IPermission getReadMedicalDataSocialHistoryPermission() {
String permissionName = PermissionNames.READ_MEDICAL_DATA_SOCIAL_HISTORY;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardHealthRecordsPermission(permissionName, PermissionVersion.ANDROID_16));
}
/**
* 获取读取疫苗接种数据权限(Android 16.0 新增的权限)
*/
@NonNull
public static IPermission getReadMedicalDataVaccinesPermission() {
String permissionName = PermissionNames.READ_MEDICAL_DATA_VACCINES;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardHealthRecordsPermission(permissionName, PermissionVersion.ANDROID_16));
}
/**
* 获取读取医师详细信息数据权限,包括地点、预约时间以及就诊组织名称等数据权限(Android 16.0 新增的权限)
*/
@NonNull
public static IPermission getReadMedicalDataVisitsPermission() {
String permissionName = PermissionNames.READ_MEDICAL_DATA_VISITS;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardHealthRecordsPermission(permissionName, PermissionVersion.ANDROID_16));
}
/**
* 获取读取生命体征数据权限(Android 16.0 新增的权限)
*/
@NonNull
public static IPermission getReadMedicalDataVitalSignsPermission() {
String permissionName = PermissionNames.READ_MEDICAL_DATA_VITAL_SIGNS;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardHealthRecordsPermission(permissionName, PermissionVersion.ANDROID_16));
}
/**
* 获取写入所有健康记录数据权限(Android 16.0 新增的权限)
*/
@NonNull
public static IPermission getWriteMedicalDataPermission() {
String permissionName = PermissionNames.WRITE_MEDICAL_DATA;
IPermission permission = getCachePermission(permissionName);
if (permission != null) {
return permission;
}
return putCachePermission(new StandardHealthRecordsPermission(permissionName, PermissionVersion.ANDROID_16));
}
}
================================================
FILE: library/src/main/java/com/hjq/permissions/permission/PermissionNames.java
================================================
package com.hjq.permissions.permission;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/06/11
* desc : 危险权限和特殊权限的名称常量集
*/
@SuppressWarnings("unused")
public final class PermissionNames {
/**
* 读取应用列表权限字符串常量,如需权限对象请调用 {@link PermissionLists#getGetInstalledAppsPermission()} 获取
*/
public static final String GET_INSTALLED_APPS = "com.android.permission.GET_INSTALLED_APPS";
/**
* 全屏通知权限字符串常量,如需权限对象请调用 {@link PermissionLists#getUseFullScreenIntentPermission()} 获取
*/
public static final String USE_FULL_SCREEN_INTENT = "android.permission.USE_FULL_SCREEN_INTENT";
/**
* 闹钟权限字符串常量,如需权限对象请调用 {@link PermissionLists#getScheduleExactAlarmPermission()} 获取
*/
public static final String SCHEDULE_EXACT_ALARM = "android.permission.SCHEDULE_EXACT_ALARM";
/**
* 管理媒体权限,如需权限对象请调用 {@link PermissionLists#getManageMediaPermission()} 获取
*/
public static final String MANAGE_MEDIA = "android.permission.MANAGE_MEDIA";
/**
* 所有文件访问权限字符串常量,如需权限对象请调用 {@link PermissionLists#getManageExternalStoragePermission()} 获取
*/
public static final String MANAGE_EXTERNAL_STORAGE = "android.permission.MANAGE_EXTERNAL_STORAGE";
/**
* 安装应用权限字符串常量,如需权限对象请调用 {@link PermissionLists#getRequestInstallPackagesPermission()} 获取
*/
public static final String REQUEST_INSTALL_PACKAGES = "android.permission.REQUEST_INSTALL_PACKAGES";
/**
* 画中画权限字符串常量,如需权限对象请调用 {@link PermissionLists#getPictureInPicturePermission()} 获取
*/
public static final String PICTURE_IN_PICTURE = "android.permission.PICTURE_IN_PICTURE";
/**
* 悬浮窗权限字符串常量,如需权限对象请调用 {@link PermissionLists#getSystemAlertWindowPermission()} 获取
*/
public static final String SYSTEM_ALERT_WINDOW = "android.permission.SYSTEM_ALERT_WINDOW";
/**
* 写入系统设置权限字符串常量,如需权限对象请调用 {@link PermissionLists#getWriteSettingsPermission()} 获取
*/
public static final String WRITE_SETTINGS = "android.permission.WRITE_SETTINGS";
/**
* 请求忽略电池优化选项权限字符串常量,如需权限对象请调用 {@link PermissionLists#getRequestIgnoreBatteryOptimizationsPermission()} 获取
*/
public static final String REQUEST_IGNORE_BATTERY_OPTIMIZATIONS = "android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS";
/**
* 勿扰权限字符串常量,如需权限对象请调用 {@link PermissionLists#getAccessNotificationPolicyPermission()} 获取
*/
public static final String ACCESS_NOTIFICATION_POLICY = "android.permission.ACCESS_NOTIFICATION_POLICY";
/**
* 查看应用使用情况权限字符串常量,如需权限对象请调用 {@link PermissionLists#getPackageUsageStatsPermission()} 获取
*/
public static final String PACKAGE_USAGE_STATS = "android.permission.PACKAGE_USAGE_STATS";
/**
* 通知栏监听权限字符串常量,如需权限对象请调用 {@link PermissionLists#getBindNotificationListenerServicePermission(Class)} 获取
*/
public static final String BIND_NOTIFICATION_LISTENER_SERVICE = "android.permission.BIND_NOTIFICATION_LISTENER_SERVICE";
/**
* VPN 权限字符串常量,如需权限对象请调用 {@link PermissionLists#getBindVpnServicePermission()} 获取
*/
public static final String BIND_VPN_SERVICE = "android.permission.BIND_VPN_SERVICE";
/**
* 通知栏权限字符串常量,如需权限对象请调用 {@link PermissionLists#getNotificationServicePermission(String)} 获取
*/
public static final String NOTIFICATION_SERVICE = "android.permission.NOTIFICATION_SERVICE";
/**
* 无障碍服务权限字符串常量,如需权限对象请调用 {@link PermissionLists#getBindAccessibilityServicePermission(Class)} 获取
*/
public static final String BIND_ACCESSIBILITY_SERVICE = "android.permission.BIND_ACCESSIBILITY_SERVICE";
/**
* 设备管理器权限字符串常量,如需权限对象请调用 {@link PermissionLists#getBindDeviceAdminPermission(Class, String)} 获取
*/
public static final String BIND_DEVICE_ADMIN = "android.permission.BIND_DEVICE_ADMIN";
/* ------------------------------------ 我是一条华丽的分割线 ------------------------------------ */
/**
* 访问部分照片和视频的权限字符串常量,如需权限对象请调用 {@link PermissionLists#getReadMediaVisualUserSelectedPermission()} 获取
*/
public static final String READ_MEDIA_VISUAL_USER_SELECTED = "android.permission.READ_MEDIA_VISUAL_USER_SELECTED";
/**
* 发送通知权限字符串常量,如需权限对象请调用 {@link PermissionLists#getPostNotificationsPermission()} 获取
*/
public static final String POST_NOTIFICATIONS = "android.permission.POST_NOTIFICATIONS";
/**
* WIFI 权限字符串常量,如需权限对象请调用 {@link PermissionLists#getNearbyWifiDevicesPermission()} 获取
*/
public static final String NEARBY_WIFI_DEVICES = "android.permission.NEARBY_WIFI_DEVICES";
/**
* 后台传感器权限字符串常量,如需权限对象请调用 {@link PermissionLists#getBodySensorsBackgroundPermission()} 获取
*/
public static final String BODY_SENSORS_BACKGROUND = "android.permission.BODY_SENSORS_BACKGROUND";
/**
* 读取图片权限字符串常量,如需权限对象请调用 {@link PermissionLists#getReadMediaImagesPermission()} 获取
*/
public static final String READ_MEDIA_IMAGES = "android.permission.READ_MEDIA_IMAGES";
/**
* 读取视频权限字符串常量,如需权限对象请调用 {@link PermissionLists#getReadMediaVideoPermission()} 获取
*/
public static final String READ_MEDIA_VIDEO = "android.permission.READ_MEDIA_VIDEO";
/**
* 读取音频权限字符串常量,如需权限对象请调用 {@link PermissionLists#getReadMediaAudioPermission()} 获取
*/
public static final String READ_MEDIA_AUDIO = "android.permission.READ_MEDIA_AUDIO";
/**
* 蓝牙扫描权限字符串常量,如需权限对象请调用 {@link PermissionLists#getBluetoothScanPermission()} 获取
*/
public static final String BLUETOOTH_SCAN = "android.permission.BLUETOOTH_SCAN";
/**
* 蓝牙连接权限字符串常量,如需权限对象请调用 {@link PermissionLists#getBluetoothConnectPermission()} 获取
*/
public static final String BLUETOOTH_CONNECT = "android.permission.BLUETOOTH_CONNECT";
/**
* 蓝牙广播权限字符串常量,如需权限对象请调用 {@link PermissionLists#getBluetoothAdvertisePermission()} 获取
*/
public static final String BLUETOOTH_ADVERTISE = "android.permission.BLUETOOTH_ADVERTISE";
/**
* 在后台获取位置权限字符串常量,如需权限对象请调用 {@link PermissionLists#getAccessBackgroundLocationPermission()} 获取
*/
public static final String ACCESS_BACKGROUND_LOCATION = "android.permission.ACCESS_BACKGROUND_LOCATION";
/**
* 获取活动步数权限字符串常量,如需权限对象请调用 {@link PermissionLists#getActivityRecognitionPermission()} 获取
*/
public static final String ACTIVITY_RECOGNITION = "android.permission.ACTIVITY_RECOGNITION";
/**
* 访问媒体的位置信息权限字符串常量,如需权限对象请调用 {@link PermissionLists#getAccessMediaLocationPermission()} 获取
*/
public static final String ACCESS_MEDIA_LOCATION = "android.permission.ACCESS_MEDIA_LOCATION";
/**
* 允许呼叫应用继续在另一个应用中启动的呼叫权限字符串常量,如需权限对象请调用 {@link PermissionLists#getAcceptHandoverPermission()} 获取
*/
public static final String ACCEPT_HANDOVER = "android.permission.ACCEPT_HANDOVER";
/**
* 读取手机号码权限字符串常量,如需权限对象请调用 {@link PermissionLists#getReadPhoneNumbersPermission()} 获取
*/
public static final String READ_PHONE_NUMBERS = "android.permission.READ_PHONE_NUMBERS";
/**
* 接听电话权限字符串常量,如需权限对象请调用 {@link PermissionLists#getAnswerPhoneCallsPermission()} 获取
*/
public static final String ANSWER_PHONE_CALLS = "android.permission.ANSWER_PHONE_CALLS";
/**
* 读取外部存储权限字符串常量,如需权限对象请调用 {@link PermissionLists#getReadExternalStoragePermission()} 获取
*/
public static final String READ_EXTERNAL_STORAGE = "android.permission.READ_EXTERNAL_STORAGE";
/**
* 写入外部存储权限字符串常量,如需权限对象请调用 {@link PermissionLists#getWriteExternalStoragePermission()} 获取
*/
public static final String WRITE_EXTERNAL_STORAGE = "android.permission.WRITE_EXTERNAL_STORAGE";
/**
* 相机权限字符串常量,如需权限对象请调用 {@link PermissionLists#getCameraPermission()} 获取
*/
public static final String CAMERA = "android.permission.CAMERA";
/**
* 麦克风权限字符串常量,如需权限对象请调用 {@link PermissionLists#getRecordAudioPermission()} 获取
*/
public static final String RECORD_AUDIO = "android.permission.RECORD_AUDIO";
/**
* 获取精确位置权限字符串常量,如需权限对象请调用 {@link PermissionLists#getAccessFineLocationPermission()} 获取
*/
public static final String ACCESS_FINE_LOCATION = "android.permission.ACCESS_FINE_LOCATION";
/**
* 获取粗略位置权限字符串常量,如需权限对象请调用 {@link PermissionLists#getAccessCoarseLocationPermission()} 获取
*/
public static final String ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION";
/**
* 读取联系人权限字符串常量,如需权限对象请调用 {@link PermissionLists#getReadContactsPermission()} 获取
*/
public static final String READ_CONTACTS = "android.permission.READ_CONTACTS";
/**
* 修改联系人权限字符串常量,如需权限对象请调用 {@link PermissionLists#getWriteContactsPermission()} 获取
*/
public static final String WRITE_CONTACTS = "android.permission.WRITE_CONTACTS";
/**
* 访问账户列表权限字符串常量,如需权限对象请调用 {@link PermissionLists#getGetAccountsPermission()} 获取
*/
public static final String GET_ACCOUNTS = "android.permission.GET_ACCOUNTS";
/**
* 读取日历权限字符串常量,如需权限对象请调用 {@link PermissionLists#getReadCalendarPermission()} 获取
*/
public static final String READ_CALENDAR = "android.permission.READ_CALENDAR";
/**
* 修改日历权限字符串常量,如需权限对象请调用 {@link PermissionLists#getWriteCalendarPermission()} 获取
*/
public static final String WRITE_CALENDAR = "android.permission.WRITE_CALENDAR";
/**
* 读取电话状态权限字符串常量,如需权限对象请调用 {@link PermissionLists#getReadPhoneStatePermission()} 获取
*/
public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE";
/**
* 拨打电话权限字符串常量,如需权限对象请调用 {@link PermissionLists#getCallPhonePermission()} 获取
*/
public static final String CALL_PHONE = "android.permission.CALL_PHONE";
/**
* 读取通话记录权限字符串常量,如需权限对象请调用 {@link PermissionLists#getReadCallLogPermission()} 获取
*/
public static final String READ_CALL_LOG = "android.permission.READ_CALL_LOG";
/**
* 修改通话记录权限字符串常量,如需权限对象请调用 {@link PermissionLists#getWriteCallLogPermission()} 获取
*/
public static final String WRITE_CALL_LOG = "android.permission.WRITE_CALL_LOG";
/**
* 添加语音邮件权限字符串常量,如需权限对象请调用 {@link PermissionLists#getAddVoicemailPermission()} 获取
*/
public static final String ADD_VOICEMAIL = "com.android.voicemail.permission.ADD_VOICEMAIL";
/**
* 使用 SIP 视频权限字符串常量,如需权限对象请调用 {@link PermissionLists#getUseSipPermission()} 获取
*/
public static final String USE_SIP = "android.permission.USE_SIP";
/**
* 处理拨出电话权限字符串常量,如需权限对象请调用 {@link PermissionLists#getProcessOutgoingCallsPermission()} 获取
*/
public static final String PROCESS_OUTGOING_CALLS = "android.permission.PROCESS_OUTGOING_CALLS";
/**
* 使用传感器权限字符串常量,如需权限对象请调用 {@link PermissionLists#getBodySensorsPermission()} 获取
*/
public static final String BODY_SENSORS = "android.permission.BODY_SENSORS";
/**
* 发送短信权限字符串常量,如需权限对象请调用 {@link PermissionLists#getSendSmsPermission()} 获取
*/
public static final String SEND_SMS = "android.permission.SEND_SMS";
/**
* 接收短信权限字符串常量,如需权限对象请调用 {@link PermissionLists#getReceiveSmsPermission()} 获取
*/
public static final String RECEIVE_SMS = "android.permission.RECEIVE_SMS";
/**
* 读取短信权限字符串常量,如需权限对象请调用 {@link PermissionLists#getReadSmsPermission()} ()} 获取
*/
public static final String READ_SMS = "android.permission.READ_SMS";
/**
* 接收 WAP 推送消息权限字符串常量,如需权限对象请调用 {@link PermissionLists#getReceiveWapPushPermission()} 获取
*/
public static final String RECEIVE_WAP_PUSH = "android.permission.RECEIVE_WAP_PUSH";
/**
* 接收彩信权限字符串常量,如需权限对象请调用 {@link PermissionLists#getReceiveMmsPermission()} 获取
*/
public static final String RECEIVE_MMS = "android.permission.RECEIVE_MMS";
/* ------------------------------------ 我是一条华丽的分割线 ------------------------------------ */
/**
* 在后台读取健康数据权限(任何类型),如需权限对象请调用 {@link PermissionLists#getReadHealthDataInBackgroundPermission()} 获取
*/
public static final String READ_HEALTH_DATA_IN_BACKGROUND = "android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND";
/**
* 读取以往的健康数据权限(任何类型),如需权限对象请调用 {@link PermissionLists#getReadHealthDataHistoryPermission()} 获取
*/
public static final String READ_HEALTH_DATA_HISTORY = "android.permission.health.READ_HEALTH_DATA_HISTORY";
/**
* 读取运动消耗的卡路里数据权限,如需权限对象请调用 {@link PermissionLists#getReadActiveCaloriesBurnedPermission()} 获取
*/
public static final String READ_ACTIVE_CALORIES_BURNED = "android.permission.health.READ_ACTIVE_CALORIES_BURNED";
/**
* 写入运动消耗的卡路里数据权限,如需权限对象请调用 {@link PermissionLists#getWriteActiveCaloriesBurnedPermission()} 获取
*/
public static final String WRITE_ACTIVE_CALORIES_BURNED = "android.permission.health.WRITE_ACTIVE_CALORIES_BURNED";
/**
* 读取活动强度数据权限,如需权限对象请调用 {@link PermissionLists#getReadActivityIntensityPermission()} 获取
*/
public static final String READ_ACTIVITY_INTENSITY = "android.permission.health.READ_ACTIVITY_INTENSITY";
/**
* 写入活动强度数据权限,如需权限对象请调用 {@link PermissionLists#getWriteActivityIntensityPermission()} 获取
*/
public static final String WRITE_ACTIVITY_INTENSITY = "android.permission.health.WRITE_ACTIVITY_INTENSITY";
/**
* 读取基础体温数据权限,如需权限对象请调用 {@link PermissionLists#getReadBasalBodyTemperaturePermission()} 获取
*/
public static final String READ_BASAL_BODY_TEMPERATURE = "android.permission.health.READ_BASAL_BODY_TEMPERATURE";
/**
* 写入基础体温数据权限,如需权限对象请调用 {@link PermissionLists#getWriteBasalBodyTemperaturePermission()} 获取
*/
public static final String WRITE_BASAL_BODY_TEMPERATURE = "android.permission.health.WRITE_BASAL_BODY_TEMPERATURE";
/**
* 读取基础代谢率数据权限,如需权限对象请调用 {@link PermissionLists#getReadBasalMetabolicRatePermission()} 获取
*/
public static final String READ_BASAL_METABOLIC_RATE = "android.permission.health.READ_BASAL_METABOLIC_RATE";
/**
* 写入基础代谢率数据权限,如需权限对象请调用 {@link PermissionLists#getWriteBasalMetabolicRatePermission()} 获取
*/
public static final String WRITE_BASAL_METABOLIC_RATE = "android.permission.health.WRITE_BASAL_METABOLIC_RATE";
/**
* 读取血糖数据权限,如需权限对象请调用 {@link PermissionLists#getReadBloodGlucosePermission()} 获取
*/
public static final String READ_BLOOD_GLUCOSE = "android.permission.health.READ_BLOOD_GLUCOSE";
/**
* 写入血糖数据权限,如需权限对象请调用 {@link PermissionLists#getWriteBloodGlucosePermission()} 获取
*/
public static final String WRITE_BLOOD_GLUCOSE = "android.permission.health.WRITE_BLOOD_GLUCOSE";
/**
* 读取血压数据权限,如需权限对象请调用 {@link PermissionLists#getReadBloodPressurePermission()} 获取
*/
public static final String READ_BLOOD_PRESSURE = "android.permission.health.READ_BLOOD_PRESSURE";
/**
* 写入血压数据权限,如需权限对象请调用 {@link PermissionLists#getWriteBloodPressurePermission()} 获取
*/
public static final String WRITE_BLOOD_PRESSURE = "android.permission.health.WRITE_BLOOD_PRESSURE";
/**
* 读取体脂数据权限,如需权限对象请调用 {@link PermissionLists#getReadBodyFatPermission()} 获取
*/
public static final String READ_BODY_FAT = "android.permission.health.READ_BODY_FAT";
/**
* 写入体脂数据权限,如需权限对象请调用 {@link PermissionLists#getWriteBodyFatPermission()} 获取
*/
public static final String WRITE_BODY_FAT = "android.permission.health.WRITE_BODY_FAT";
/**
* 读取体温数据权限,如需权限对象请调用 {@link PermissionLists#getReadBodyTemperaturePermission()} 获取
*/
public static final String READ_BODY_TEMPERATURE = "android.permission.health.READ_BODY_TEMPERATURE";
/**
* 写入体温数据权限,如需权限对象请调用 {@link PermissionLists#getWriteBodyTemperaturePermission()} 获取
*/
public static final String WRITE_BODY_TEMPERATURE = "android.permission.health.WRITE_BODY_TEMPERATURE";
/**
* 读取身体含水量数据权限,如需权限对象请调用 {@link PermissionLists#getReadBodyWaterMassPermission()} 获取
*/
public static final String READ_BODY_WATER_MASS = "android.permission.health.READ_BODY_WATER_MASS";
/**
* 写入身体含水量数据权限,如需权限对象请调用 {@link PermissionLists#getWriteBodyWaterMassPermission()} 获取
*/
public static final String WRITE_BODY_WATER_MASS = "android.permission.health.WRITE_BODY_WATER_MASS";
/**
* 读取骨质密度数据权限,如需权限对象请调用 {@link PermissionLists#getReadBoneMassPermission()} 获取
*/
public static final String READ_BONE_MASS = "android.permission.health.READ_BONE_MASS";
/**
* 写入骨质密度数据权限,如需权限对象请调用 {@link PermissionLists#getWriteBoneMassPermission()} 获取
*/
public static final String WRITE_BONE_MASS = "android.permission.health.WRITE_BONE_MASS";
/**
* 读取宫颈粘液数据权限,如需权限对象请调用 {@link PermissionLists#getReadCervicalMucusPermission()} 获取
*/
public static final String READ_CERVICAL_MUCUS = "android.permission.health.READ_CERVICAL_MUCUS";
/**
* 写入宫颈粘液数据权限,如需权限对象请调用 {@link PermissionLists#getWriteCervicalMucusPermission()} 获取
*/
public static final String WRITE_CERVICAL_MUCUS = "android.permission.health.WRITE_CERVICAL_MUCUS";
/**
* 读取距离数据权限,如需权限对象请调用 {@link PermissionLists#getReadDistancePermission()} 获取
*/
public static final String READ_DISTANCE = "android.permission.health.READ_DISTANCE";
/**
* 写入距离数据权限,如需权限对象请调用 {@link PermissionLists#getWriteDistancePermission()} 获取
*/
public static final String WRITE_DISTANCE = "android.permission.health.WRITE_DISTANCE";
/**
* 读取爬升高度数据权限,如需权限对象请调用 {@link PermissionLists#getReadElevationGainedPermission()} 获取
*/
public static final String READ_ELEVATION_GAINED = "android.permission.health.READ_ELEVATION_GAINED";
/**
* 写入爬升高度数据权限,如需权限对象请调用 {@link PermissionLists#getWriteElevationGainedPermission()} 获取
*/
public static final String WRITE_ELEVATION_GAINED = "android.permission.health.WRITE_ELEVATION_GAINED";
/**
* 读取锻炼数据权限,如需权限对象请调用 {@link PermissionLists#getReadExercisePermission()} 获取
*/
public static final String READ_EXERCISE = "android.permission.health.READ_EXERCISE";
/**
* 写入锻炼数据权限,如需权限对象请调用 {@link PermissionLists#getWriteExercisePermission()} 获取
*/
public static final String WRITE_EXERCISE = "android.permission.health.WRITE_EXERCISE";
/**
* 读取锻炼路线数据权限,如需权限对象请调用 {@link PermissionLists#getReadExerciseRoutesPermission()} 获取
*/
public static final String READ_EXERCISE_ROUTES = "android.permission.health.READ_EXERCISE_ROUTES";
/**
* 写入锻炼路线数据权限,如需权限对象请调用 {@link PermissionLists#getWriteExerciseRoutePermission()} 获取
*/
public static final String WRITE_EXERCISE_ROUTE = "android.permission.health.WRITE_EXERCISE_ROUTE";
/**
* 读取爬楼层数数据权限,如需权限对象请调用 {@link PermissionLists#getReadFloorsClimbedPermission()} 获取
*/
public static final String READ_FLOORS_CLIMBED = "android.permission.health.READ_FLOORS_CLIMBED";
/**
* 写入爬楼层数数据权限,如需权限对象请调用 {@link PermissionLists#getWriteFloorsClimbedPermission()} 获取
*/
public static final String WRITE_FLOORS_CLIMBED = "android.permission.health.WRITE_FLOORS_CLIMBED";
/**
* 读取心率数据权限,如需权限对象请调用 {@link PermissionLists#getReadHeartRatePermission()} 获取
*/
public static final String READ_HEART_RATE = "android.permission.health.READ_HEART_RATE";
/**
* 写入心率数据权限,如需权限对象请调用 {@link PermissionLists#getWriteHeartRatePermission()} 获取
*/
public static final String WRITE_HEART_RATE = "android.permission.health.WRITE_HEART_RATE";
/**
* 读取心率变异性数据权限,如需权限对象请调用 {@link PermissionLists#getReadHeartRateVariabilityPermission()} 获取
*/
public static final String READ_HEART_RATE_VARIABILITY = "android.permission.health.READ_HEART_RATE_VARIABILITY";
/**
* 写入心率变异性数据权限,如需权限对象请调用 {@link PermissionLists#getWriteHeartRateVariabilityPermission()} 获取
*/
public static final String WRITE_HEART_RATE_VARIABILITY = "android.permission.health.WRITE_HEART_RATE_VARIABILITY";
/**
* 读取身高数据权限,如需权限对象请调用 {@link PermissionLists#getReadHeightPermission()} 获取
*/
public static final String READ_HEIGHT = "android.permission.health.READ_HEIGHT";
/**
* 写入身高数据权限,如需权限对象请调用 {@link PermissionLists#getWriteHeightPermission()} 获取
*/
public static final String WRITE_HEIGHT = "android.permission.health.WRITE_HEIGHT";
/**
* 读取饮水量权限,如需权限对象请调用 {@link PermissionLists#getReadHydrationPermission()} 获取
*/
public static final String READ_HYDRATION = "android.permission.health.READ_HYDRATION";
/**
* 写入饮水量权限,如需权限对象请调用 {@link PermissionLists#getWriteHydrationPermission()} 获取
*/
public static final String WRITE_HYDRATION = "android.permission.health.WRITE_HYDRATION";
/**
* 读取点状出血数据权限,如需权限对象请调用 {@link PermissionLists#getReadIntermenstrualBleedingPermission()} 获取
*/
public static final String READ_INTERMENSTRUAL_BLEEDING = "android.permission.health.READ_INTERMENSTRUAL_BLEEDING";
/**
* 写入点状出血数据权限,如需权限对象请调用 {@link PermissionLists#getWriteIntermenstrualBleedingPermission()} 获取
*/
public static final String WRITE_INTERMENSTRUAL_BLEEDING = "android.permission.health.WRITE_INTERMENSTRUAL_BLEEDING";
/**
* 读取净体重数据权限,如需权限对象请调用 {@link PermissionLists#getReadLeanBodyMassPermission()} 获取
*/
public static final String READ_LEAN_BODY_MASS = "android.permission.health.READ_LEAN_BODY_MASS";
/**
* 写入净体重数据权限,如需权限对象请调用 {@link PermissionLists#getWriteLeanBodyMassPermission()} 获取
*/
public static final String WRITE_LEAN_BODY_MASS = "android.permission.health.WRITE_LEAN_BODY_MASS";
/**
* 读取经期数据权限,如需权限对象请调用 {@link PermissionLists#getReadMenstruationPermission()} 获取
*/
public static final String READ_MENSTRUATION = "android.permission.health.READ_MENSTRUATION";
/**
* 写入经期数据权限,如需权限对象请调用 {@link PermissionLists#getWriteMenstruationPermission()} 获取
*/
public static final String WRITE_MENSTRUATION = "android.permission.health.WRITE_MENSTRUATION";
/**
* 读取正念数据权限,如需权限对象请调用 {@link PermissionLists#getReadMindfulnessPermission()} 获取
*/
public static final String READ_MINDFULNESS = "android.permission.health.READ_MINDFULNESS";
/**
* 写入正念数据权限,如需权限对象请调用 {@link PermissionLists#getWriteMindfulnessPermission()} 获取
*/
public static final String WRITE_MINDFULNESS = "android.permission.health.WRITE_MINDFULNESS";
/**
* 读取营养数据权限,如需权限对象请调用 {@link PermissionLists#getReadNutritionPermission()} 获取
*/
public static final String READ_NUTRITION = "android.permission.health.READ_NUTRITION";
/**
* 写入营养数据权限,如需权限对象请调用 {@link PermissionLists#getWriteNutritionPermission()} 获取
*/
public static final String WRITE_NUTRITION = "android.permission.health.WRITE_NUTRITION";
/**
* 读取排卵检测数据权限,如需权限对象请调用 {@link PermissionLists#getReadOvulationTestPermission()} 获取
*/
public static final String READ_OVULATION_TEST = "android.permission.health.READ_OVULATION_TEST";
/**
* 写入排卵检测数据权限,如需权限对象请调用 {@link PermissionLists#getWriteOvulationTestPermission()} 获取
*/
public static final String WRITE_OVULATION_TEST = "android.permission.health.WRITE_OVULATION_TEST";
/**
* 读取血氧饱和度数据权限,如需权限对象请调用 {@link PermissionLists#getReadOxygenSaturationPermission()} 获取
*/
public static final String READ_OXYGEN_SATURATION = "android.permission.health.READ_OXYGEN_SATURATION";
/**
* 写入血氧饱和度数据权限,如需权限对象请调用 {@link PermissionLists#getWriteOxygenSaturationPermission()} 获取
*/
public static final String WRITE_OXYGEN_SATURATION = "android.permission.health.WRITE_OXYGEN_SATURATION";
/**
* 读取训练计划数据权限,如需权限对象请调用 {@link PermissionLists#getReadPlannedExercisePermission()} 获取
*/
public static final String READ_PLANNED_EXERCISE = "android.permission.health.READ_PLANNED_EXERCISE";
/**
* 写入训练计划数据权限,如需权限对象请调用 {@link PermissionLists#getWritePlannedExercisePermission()} 获取
*/
public static final String WRITE_PLANNED_EXERCISE = "android.permission.health.WRITE_PLANNED_EXERCISE";
/**
* 读取体能数据权限,如需权限对象请调用 {@link PermissionLists#getReadPowerPermission()} 获取
*/
public static final String READ_POWER = "android.permission.health.READ_POWER";
/**
* 写入体能数据权限,如需权限对象请调用 {@link PermissionLists#getWritePowerPermission()} 获取
*/
public static final String WRITE_POWER = "android.permission.health.WRITE_POWER";
/**
* 读取呼吸频率数据权限,如需权限对象请调用 {@link PermissionLists#getReadRespiratoryRatePermission()} 获取
*/
public static final String READ_RESPIRATORY_RATE = "android.permission.health.READ_RESPIRATORY_RATE";
/**
* 写入呼吸频率数据权限,如需权限对象请调用 {@link PermissionLists#getWriteRespiratoryRatePermission()} 获取
*/
public static final String WRITE_RESPIRATORY_RATE = "android.permission.health.WRITE_RESPIRATORY_RATE";
/**
* 读取静息心率数据权限,如需权限对象请调用 {@link PermissionLists#getReadRestingHeartRatePermission()} 获取
*/
public static final String READ_RESTING_HEART_RATE = "android.permission.health.READ_RESTING_HEART_RATE";
/**
* 写入静息心率数据权限,如需权限对象请调用 {@link PermissionLists#getWriteRestingHeartRatePermission()} 获取
*/
public static final String WRITE_RESTING_HEART_RATE = "android.permission.health.WRITE_RESTING_HEART_RATE";
/**
* 读取性活动数据权限,如需权限对象请调用 {@link PermissionLists#getReadSexualActivityPermission()} 获取
*/
public static final String READ_SEXUAL_ACTIVITY = "android.permission.health.READ_SEXUAL_ACTIVITY";
/**
* 写入性活动数据权限,如需权限对象请调用 {@link PermissionLists#getWriteSexualActivityPermission()} 获取
*/
public static final String WRITE_SEXUAL_ACTIVITY = "android.permission.health.WRITE_SEXUAL_ACTIVITY";
/**
* 读取体表温度数据权限,如需权限对象请调用 {@link PermissionLists#getReadSkinTemperaturePermission()} 获取
*/
public static final String READ_SKIN_TEMPERATURE = "android.permission.health.READ_SKIN_TEMPERATURE";
/**
* 写入体表温度数据权限,如需权限对象请调用 {@link PermissionLists#getWriteSkinTemperaturePermission()} 获取
*/
public static final String WRITE_SKIN_TEMPERATURE = "android.permission.health.WRITE_SKIN_TEMPERATURE";
/**
* 读取睡眠数据权限,如需权限对象请调用 {@link PermissionLists#getReadSleepPermission()} 获取
*/
public static final String READ_SLEEP = "android.permission.health.READ_SLEEP";
/**
* 写入睡眠数据权限,如需权限对象请调用 {@link PermissionLists#getWriteSleepPermission()} 获取
*/
public static final String WRITE_SLEEP = "android.permission.health.WRITE_SLEEP";
/**
* 读取速度数据权限,如需权限对象请调用 {@link PermissionLists#getReadSpeedPermission()} 获取
*/
public static final String READ_SPEED = "android.permission.health.READ_SPEED";
/**
* 写入速度数据权限,如需权限对象请调用 {@link PermissionLists#getWriteSpeedPermission()} 获取
*/
public static final String WRITE_SPEED = "android.permission.health.WRITE_SPEED";
/**
* 读取步数数据权限,如需权限对象请调用 {@link PermissionLists#getReadStepsPermission()} 获取
*/
public static final String READ_STEPS = "android.permission.health.READ_STEPS";
/**
* 写入步数数据权限,如需权限对象请调用 {@link PermissionLists#getWriteStepsPermission()} 获取
*/
public static final String WRITE_STEPS = "android.permission.health.WRITE_STEPS";
/**
* 读取消耗的卡路里总数数据权限,如需权限对象请调用 {@link PermissionLists#getReadTotalCaloriesBurnedPermission()} 获取
*/
public static final String READ_TOTAL_CALORIES_BURNED = "android.permission.health.READ_TOTAL_CALORIES_BURNED";
/**
* 写入消耗的卡路里总数数据权限,如需权限对象请调用 {@link PermissionLists#getWriteTotalCaloriesBurnedPermission()} 获取
*/
public static final String WRITE_TOTAL_CALORIES_BURNED = "android.permission.health.WRITE_TOTAL_CALORIES_BURNED";
/**
* 读取最大摄氧量数据权限,如需权限对象请调用 {@link PermissionLists#getReadVo2MaxPermission()} 获取
*/
public static final String READ_VO2_MAX = "android.permission.health.READ_VO2_MAX";
/**
* 写入最大摄氧量数据权限,如需权限对象请调用 {@link PermissionLists#getWriteVo2MaxPermission()} 获取
*/
public static final String WRITE_VO2_MAX = "android.permission.health.WRITE_VO2_MAX";
/**
* 读取体重数据权限,如需权限对象请调用 {@link PermissionLists#getReadWeightPermission()} 获取
*/
public static final String READ_WEIGHT = "android.permission.health.READ_WEIGHT";
/**
* 写入体重数据权限,如需权限对象请调用 {@link PermissionLists#getWriteWeightPermission()} 获取
*/
public static final String WRITE_WEIGHT = "android.permission.health.WRITE_WEIGHT";
/**
* 读取推轮椅次数数据权限,如需权限对象请调用 {@link PermissionLists#getReadWheelchairPushesPermission()} 获取
*/
public static final String READ_WHEELCHAIR_PUSHES = "android.permission.health.READ_WHEELCHAIR_PUSHES";
/**
* 写入推轮椅次数数据权限,如需权限对象请调用 {@link PermissionLists#getWriteWheelchairPushesPermission()} 获取
*/
public static final String WRITE_WHEELCHAIR_PUSHES = "android.permission.health.WRITE_WHEELCHAIR_PUSHES";
/* ------------------------------------ 我是一条华丽的分割线 ------------------------------------ */
/**
* 读取过敏反应数据权限,如需权限对象请调用 {@link PermissionLists#getReadMedicalDataAllergiesIntolerancesPermission()} 获取
*/
public static final String READ_MEDICAL_DATA_ALLERGIES_INTOLERANCES = "android.permission.health.READ_MEDICAL_DATA_ALLERGIES_INTOLERANCES";
/**
* 读取病症数据权限,如需权限对象请调用 {@link PermissionLists#getReadMedicalDataConditionsPermission()} 获取
*/
public static final String READ_MEDICAL_DATA_CONDITIONS = "android.permission.health.READ_MEDICAL_DATA_CONDITIONS";
/**
* 读取化验结果数据权限,如需权限对象请调用 {@link PermissionLists#getReadMedicalDataLaboratoryResultsPermission()} 获取
*/
public static final String READ_MEDICAL_DATA_LABORATORY_RESULTS = "android.permission.health.READ_MEDICAL_DATA_LABORATORY_RESULTS";
/**
* 读取用药情况数据权限,如需权限对象请调用 {@link PermissionLists#getReadMedicalDataMedicationsPermission()} 获取
*/
public static final String READ_MEDICAL_DATA_MEDICATIONS = "android.permission.health.READ_MEDICAL_DATA_MEDICATIONS";
/**
* 读取个人详细信息数据权限,如需权限对象请调用 {@link PermissionLists#getReadMedicalDataPersonalDetailsPermission()} 获取
*/
public static final String READ_MEDICAL_DATA_PERSONAL_DETAILS = "android.permission.health.READ_MEDICAL_DATA_PERSONAL_DETAILS";
/**
* 读取就医情况数据权限,如需权限对象请调用 {@link PermissionLists#getReadMedicalDataPractitionerDetailsPermission()} 获取
*/
public static final String READ_MEDICAL_DATA_PRACTITIONER_DETAILS = "android.permission.health.READ_MEDICAL_DATA_PRACTITIONER_DETAILS";
/**
* 读取怀孕情况数据权限,如需权限对象请调用 {@link PermissionLists#getReadMedicalDataPregnancyPermission()} 获取
*/
public static final String READ_MEDICAL_DATA_PREGNANCY = "android.permission.health.READ_MEDICAL_DATA_PREGNANCY";
/**
* 读取医疗程序数据权限,如需权限对象请调用 {@link PermissionLists#getReadMedicalDataProceduresPermission()} 获取
*/
public static final String READ_MEDICAL_DATA_PROCEDURES = "android.permission.health.READ_MEDICAL_DATA_PROCEDURES";
/**
* 读取个人生活史数据权限,如需权限对象请调用 {@link PermissionLists#getReadMedicalDataSocialHistoryPermission()} 获取
*/
public static final String READ_MEDICAL_DATA_SOCIAL_HISTORY = "android.permission.health.READ_MEDICAL_DATA_SOCIAL_HISTORY";
/**
* 读取疫苗接种数据权限,如需权限对象请调用 {@link PermissionLists#getReadMedicalDataVaccinesPermission()} 获取
*/
public static final String READ_MEDICAL_DATA_VACCINES = "android.permission.health.READ_MEDICAL_DATA_VACCINES";
/**
* 读取医师详细信息数据权限,包括地点、预约时间以及就诊组织名称等数据权限,如需权限对象请调用 {@link PermissionLists#getReadMedicalDataVisitsPermission()} 获取
*/
public static final String READ_MEDICAL_DATA_VISITS = "android.permission.health.READ_MEDICAL_DATA_VISITS";
/**
* 读取生命体征数据权限,如需权限对象请调用 {@link PermissionLists#getReadMedicalDataVitalSignsPermission()} 获取
*/
public static final String READ_MEDICAL_DATA_VITAL_SIGNS = "android.permission.health.READ_MEDICAL_DATA_VITAL_SIGNS";
/**
* 写入所有健康记录数据权限,如需权限对象请调用 {@link PermissionLists#getWriteMedicalDataPermission()} 获取
*/
public static final String WRITE_MEDICAL_DATA = "android.permission.health.WRITE_MEDICAL_DATA";
/** 私有化构造函数 */
private PermissionNames() {
// default implementation ignored
}
}
================================================
FILE: library/src/main/java/com/hjq/permissions/permission/PermissionPageType.java
================================================
package com.hjq.permissions.permission;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/07/13
* desc : 权限的页面类型
*/
public enum PermissionPageType {
/** 透明的 Activity */
TRANSPARENT_ACTIVITY,
/** 不透明的 Activity */
OPAQUE_ACTIVITY
}
================================================
FILE: library/src/main/java/com/hjq/permissions/permission/base/BasePermission.java
================================================
package com.hjq.permissions.permission.base;
import android.app.Activity;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Parcel;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.hjq.permissions.manifest.AndroidManifestInfo;
import com.hjq.permissions.manifest.node.PermissionManifestInfo;
import com.hjq.permissions.tools.PermissionSettingPage;
import com.hjq.permissions.tools.PermissionUtils;
import com.hjq.permissions.tools.PermissionVersion;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/06/11
* desc : 权限基类
*/
public abstract class BasePermission implements IPermission {
/** Op 权限模式:未知模式 */
public static final int MODE_UNKNOWN = -1;
protected BasePermission() {
// default implementation ignored
}
protected BasePermission(Parcel in) {
// default implementation ignored
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
// default implementation ignored
}
@NonNull
@Override
public String toString() {
return getPermissionName();
}
@Override
public boolean equals(@Nullable Object obj) {
// 如果要对比的对象和当前对象的内存地址一样,那么就返回 true
if (obj == this) {
return true;
}
// 重写 equals 方法是为了 List 和 Map 集合有能力辨别不同的权限对象是不是来自同一个权限
// 如果这两个权限对象的名称一样,那么就认为它们是同一个权限
if (obj instanceof IPermission) {
return PermissionUtils.equalsPermission(this, ((IPermission) obj));
} else if (obj instanceof String) {
return PermissionUtils.equalsPermission(this, ((String) obj));
}
return false;
}
@NonNull
protected Uri getPackageNameUri(@NonNull Context context) {
return PermissionUtils.getPackageNameUri(context);
}
@NonNull
protected Intent getApplicationDetailsSettingIntent(@NonNull Context context) {
return PermissionSettingPage.getApplicationDetailsSettingsIntent(context, this);
}
@NonNull
protected static Intent getManageApplicationSettingIntent() {
return PermissionSettingPage.getManageApplicationSettingsIntent();
}
@NonNull
protected static Intent getApplicationSettingIntent() {
return PermissionSettingPage.getApplicationSettingsIntent();
}
@NonNull
protected Intent getAndroidSettingIntent() {
return PermissionSettingPage.getAndroidSettingsIntent();
}
@Override
public void checkCompliance(@NonNull Activity activity,
@NonNull List requestList,
@Nullable AndroidManifestInfo manifestInfo) {
// 检查 targetSdkVersion 是否符合要求
checkSelfByTargetSdkVersion(activity);
// 检查 AndroidManifest.xml 是否符合要求
if (manifestInfo != null) {
List permissionInfoList = manifestInfo.permissionInfoList;
PermissionManifestInfo currentPermissionInfo = findPermissionInfoByList(permissionInfoList, getPermissionName());
checkSelfByManifestFile(activity, requestList, manifestInfo, permissionInfoList, currentPermissionInfo);
}
// 检查请求的权限列表是否符合要求
checkSelfByRequestPermissions(activity, requestList);
}
/**
* 检查 targetSdkVersion 是否符合要求,如果不合规则会抛出异常
*/
protected void checkSelfByTargetSdkVersion(@NonNull Context context) {
int minTargetSdkVersion = getMinTargetSdkVersion(context);
// 必须设置正确的 targetSdkVersion 才能正常检测权限
if (PermissionVersion.getTargetVersion(context) >= minTargetSdkVersion) {
return;
}
throw new IllegalStateException("Request \"" + getPermissionName() + "\" permission, " +
"The targetSdkVersion SDK must be " + minTargetSdkVersion +
" or more, if you do not want to upgrade targetSdkVersion, " +
"please apply with the old permission");
}
/**
* 当前权限是否在清单文件中静态注册
*/
protected abstract boolean isRegisterPermissionByManifestFile();
/**
* 检查 AndroidManifest.xml 是否符合要求,如果不合规则会抛出异常
*/
protected void checkSelfByManifestFile(@NonNull Activity activity,
@NonNull List requestList,
@NonNull AndroidManifestInfo manifestInfo,
@NonNull List permissionInfoList,
@Nullable PermissionManifestInfo currentPermissionInfo) {
if (!isRegisterPermissionByManifestFile()) {
return;
}
// 检查当前权限有没有在清单文件中静态注册,如果有注册,还要检查注册 maxSdkVersion 属性有没有问题
checkPermissionRegistrationStatus(currentPermissionInfo, getPermissionName());
}
/**
* 检查请求的权限列表是否符合要求,如果不合规则会抛出异常
*/
protected void checkSelfByRequestPermissions(@NonNull Activity activity, @NonNull List requestList) {
// default implementation ignored
// 默认无任何实现,交由子类自己去实现
}
/**
* 检查权限的注册状态,如果是则会抛出异常
*/
protected static void checkPermissionRegistrationStatus(@Nullable PermissionManifestInfo permissionInfo,
@NonNull String checkPermission) {
checkPermissionRegistrationStatus(permissionInfo, checkPermission, PermissionManifestInfo.DEFAULT_MAX_SDK_VERSION);
}
protected static void checkPermissionRegistrationStatus(@Nullable List permissionInfoList,
@NonNull String checkPermission) {
checkPermissionRegistrationStatus(permissionInfoList, checkPermission, PermissionManifestInfo.DEFAULT_MAX_SDK_VERSION);
}
protected static void checkPermissionRegistrationStatus(@Nullable List permissionInfoList,
@NonNull String checkPermission,
int lowestMaxSdkVersion) {
PermissionManifestInfo permissionInfo = null;
if (permissionInfoList != null) {
permissionInfo = findPermissionInfoByList(permissionInfoList, checkPermission);
}
checkPermissionRegistrationStatus(permissionInfo, checkPermission, lowestMaxSdkVersion);
}
protected static void checkPermissionRegistrationStatus(@Nullable PermissionManifestInfo permissionInfo,
@NonNull String checkPermission,
int lowestMaxSdkVersion) {
if (permissionInfo == null) {
// 动态申请的权限没有在清单文件中注册,分为以下两种情况:
// 1. 如果你的项目没有在清单文件中注册这个权限,请直接在清单文件中注册一下即可
// 2. 如果你的项目明明已注册这个权限,可以检查一下编译完成的 apk 包中是否包含该权限,如果里面没有,证明框架的判断是没有问题的
// 一般是第三方 sdk 或者框架在清单文件中注册了 导致的
// 解决方式也很简单,通过在项目中注册 即可替换掉原先的配置
// 具体案例:https://github.com/getActivity/XXPermissions/issues/98
throw new IllegalStateException("Please register permissions in the AndroidManifest.xml file " +
"");
}
int manifestMaxSdkVersion = permissionInfo.maxSdkVersion;
if (manifestMaxSdkVersion < lowestMaxSdkVersion) {
// 清单文件中所注册的权限 maxSdkVersion 大小不符合最低要求,分为以下两种情况:
// 1. 如果你的项目中注册了该属性,请根据报错提示修改 maxSdkVersion 属性值或者删除 maxSdkVersion 属性
// 2. 如果你明明没有注册过 maxSdkVersion 属性,可以检查一下编译完成的 apk 包中是否有该属性,如果里面存在,证明框架的判断是没有问题的
// 一般是第三方 sdk 或者框架在清单文件中注册了 导致的
// 解决方式也很简单,通过在项目中注册 即可替换掉原先的配置
throw new IllegalArgumentException("The AndroidManifest.xml file " +
" does not meet the requirements, " +
(lowestMaxSdkVersion != PermissionManifestInfo.DEFAULT_MAX_SDK_VERSION ?
"the minimum requirement for maxSdkVersion is " + lowestMaxSdkVersion :
"please delete the android:maxSdkVersion=\"" + manifestMaxSdkVersion + "\" attribute"));
}
}
/**
* 获得当前项目的 minSdkVersion
*/
protected static int getMinSdkVersion(@NonNull Context context, @Nullable AndroidManifestInfo manifestInfo) {
if (PermissionVersion.isAndroid7()) {
return context.getApplicationInfo().minSdkVersion;
}
if (manifestInfo == null || manifestInfo.usesSdkInfo == null) {
return PermissionVersion.ANDROID_4_2;
}
return manifestInfo.usesSdkInfo.minSdkVersion;
}
/**
* 从权限列表中获取指定的权限信息
*/
@Nullable
public static PermissionManifestInfo findPermissionInfoByList(@NonNull List permissionInfoList,
@NonNull String permissionName) {
PermissionManifestInfo permissionInfo = null;
for (PermissionManifestInfo info : permissionInfoList) {
if (PermissionUtils.equalsPermission(info.name, permissionName)) {
permissionInfo = info;
break;
}
}
return permissionInfo;
}
/**
* 判断某个危险权限是否授予了
*/
@RequiresApi(PermissionVersion.ANDROID_6)
public static boolean checkSelfPermission(@NonNull Context context, @NonNull String permission) {
return context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED;
}
/**
* 判断是否应该向用户显示请求权限的理由
*/
@RequiresApi(PermissionVersion.ANDROID_6)
@SuppressWarnings({"JavaReflectionMemberAccess", "ConstantConditions", "BooleanMethodIsAlwaysInverted"})
public static boolean shouldShowRequestPermissionRationale(@NonNull Activity activity, @NonNull String permission) {
// 解决 Android 12 调用 shouldShowRequestPermissionRationale 出现内存泄漏的问题
// Android 12L 和 Android 13 版本经过测试不会出现这个问题,证明 Google 在新版本上已经修复了这个问题
// 但是对于 Android 12 仍是一个历史遗留问题,这是我们所有 Android App 开发者不得不面对的一个事情
// issue 地址:https://github.com/getActivity/XXPermissions/issues/133
if (PermissionVersion.getCurrentVersion() == PermissionVersion.ANDROID_12) {
try {
// 另外针对这个问题,我还给谷歌的 AndroidX 项目无偿提供了解决方案,目前 Merge Request 已被合入主分支
// 我相信通过这一举措,将解决全球近 10 亿台 Android 12 设备出现的内存泄露问题
// Pull Request 地址:https://github.com/androidx/androidx/pull/435
PackageManager packageManager = activity.getApplication().getPackageManager();
Method method = PackageManager.class.getMethod("shouldShowRequestPermissionRationale", String.class);
return (boolean) method.invoke(packageManager, permission);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
e.printStackTrace();
}
}
return activity.shouldShowRequestPermissionRationale(permission);
}
/**
* 通过 AppOpsManager 判断某个权限是否授予
*
* @param opName 需要传入 {@link AppOpsManager} 类中的以 OPSTR 开头的字段
* @param defaultGranted 当判断不了该权限状态的时候,是否返回已授予状态
*/
@RequiresApi(PermissionVersion.ANDROID_4_4)
public static boolean checkOpPermission(@NonNull Context context, @NonNull String opName, boolean defaultGranted) {
int opMode = getOpPermissionMode(context, opName);
if (opMode == MODE_UNKNOWN) {
return defaultGranted;
}
return opMode == AppOpsManager.MODE_ALLOWED;
}
/**
* 通过 AppOpsManager 判断某个权限是否授予
*
* @param opFieldName 要反射 {@link AppOpsManager} 类中的字段名称
* @param opDefaultValue 当反射获取不到对应字段的值时,该值作为替补
* @param defaultGranted 当判断不了该权限状态的时候,是否返回已授予状态
*/
@RequiresApi(PermissionVersion.ANDROID_4_4)
public static boolean checkOpPermission(@NonNull Context context,
@NonNull String opFieldName,
int opDefaultValue,
boolean defaultGranted) {
int opMode = getOpPermissionMode(context, opFieldName, opDefaultValue);
if (opMode == MODE_UNKNOWN) {
return defaultGranted;
}
return opMode == AppOpsManager.MODE_ALLOWED;
}
/**
* 获取 AppOpsManager 某个权限的状态
*
* @param opName 需要传入 {@link AppOpsManager} 类中的以 OPSTR 开头的字段
*/
@RequiresApi(PermissionVersion.ANDROID_4_4)
@SuppressWarnings("deprecation")
public static int getOpPermissionMode(@NonNull Context context, @NonNull String opName) {
AppOpsManager appOpsManager;
if (PermissionVersion.isAndroid6()) {
appOpsManager = context.getSystemService(AppOpsManager.class);
} else {
appOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
}
// 虽然这个 SystemService 永远不为空,但是不怕一万,就怕万一,开展防御性编程
if (appOpsManager == null) {
return MODE_UNKNOWN;
}
try {
if (PermissionVersion.isAndroid10()) {
return appOpsManager.unsafeCheckOpNoThrow(opName, context.getApplicationInfo().uid, context.getPackageName());
} else {
return appOpsManager.checkOpNoThrow(opName, context.getApplicationInfo().uid, context.getPackageName());
}
} catch (Throwable e) {
e.printStackTrace();
return MODE_UNKNOWN;
}
}
/**
* 获取 AppOpsManager 某个权限的状态
*
* @param opName 要反射 {@link AppOpsManager} 类中的字段名称
* @param opDefaultValue 当反射获取不到对应字段的值时,该值作为替补
*/
@SuppressWarnings("ConstantConditions")
@RequiresApi(PermissionVersion.ANDROID_4_4)
public static int getOpPermissionMode(Context context, @NonNull String opName, int opDefaultValue) {
AppOpsManager appOpsManager;
if (PermissionVersion.isAndroid6()) {
appOpsManager = context.getSystemService(AppOpsManager.class);
} else {
appOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
}
// 虽然这个 SystemService 永远不为空,但是不怕一万,就怕万一,开展防御性编程
if (appOpsManager == null) {
return MODE_UNKNOWN;
}
try {
Class> appOpsClass = Class.forName(AppOpsManager.class.getName());
int opValue;
try {
Field opField = appOpsClass.getDeclaredField(opName);
opValue = (int) opField.get(Integer.class);
} catch (NoSuchFieldException e) {
opValue = opDefaultValue;
}
Method checkOpNoThrowMethod = appOpsClass.getMethod("checkOpNoThrow", Integer.TYPE, Integer.TYPE, String.class);
return ((int) checkOpNoThrowMethod.invoke(appOpsManager, opValue, context.getApplicationInfo().uid, context.getPackageName()));
} catch (Exception e) {
e.printStackTrace();
return MODE_UNKNOWN;
}
}
/**
* 判断 AppOpsManager 是否存在某个 Op 权限
*
* @param opName 要反射 {@link AppOpsManager} 类中的字段名称
*/
@RequiresApi(PermissionVersion.ANDROID_4_4)
public static boolean isExistOpPermission(String opName) {
try {
Class> appOpsClass = Class.forName(AppOpsManager.class.getName());
appOpsClass.getDeclaredField(opName);
// 证明有这个字段,返回 true
return true;
} catch (Exception ignored) {
// default implementation ignored
return false;
}
}
}
================================================
FILE: library/src/main/java/com/hjq/permissions/permission/base/IPermission.java
================================================
package com.hjq.permissions.permission.base;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.hjq.permissions.manifest.AndroidManifestInfo;
import com.hjq.permissions.permission.PermissionChannel;
import com.hjq.permissions.permission.PermissionPageType;
import com.hjq.permissions.tools.PermissionVersion;
import java.util.List;
/**
* author : Android 轮子哥
* github : https://github.com/getActivity/XXPermissions
* time : 2025/06/11
* desc : 权限接口
*/
public interface IPermission extends Parcelable {
/**
* 获取权限的名称
*/
@NonNull
String getPermissionName();
/**
* 获取权限请求通道
*/
@NonNull
PermissionChannel getPermissionChannel(@NonNull Context context);
/**
* 获取权限页面的类型
*/
@NonNull
PermissionPageType getPermissionPageType(@NonNull Context context);
/**
* 获取权限的组别
*/
@Nullable
default String getPermissionGroup(@NonNull Context context) {
// 返回空表示没有组别
return null;
}
/**
* 获取权限出现的 Android 版本
*/
int getFromAndroidVersion(@NonNull Context context);
/**
* 获取使用这个权限最低要求的 targetSdk 版本
*/
default int getMinTargetSdkVersion(@NonNull Context context) {
return getFromAndroidVersion(context);
}
/**
* 获取当前权限对应的旧权限
*/
@Nullable
default List getOldPermissions(Context context) {
// 表示没有旧权限
return null;
}
/**
* 获取当前权限对应的前台权限
*/
@Nullable
default List getForegroundPermissions(@NonNull Context context) {
// 表示没有前台权限
return null;
}
/**
* 当前权限是否为后台权限
*/
default boolean isBackgroundPermission(@NonNull Context context) {
List foregroundPermission = getForegroundPermissions(context);
if (foregroundPermission == null) {
return false;
}
return !foregroundPermission.isEmpty();
}
/**
* 当前是否支持请求该权限
*/
default boolean isSupportRequestPermission(@NonNull Context context) {
// 如果当前权限是否在低版本(不受支持的版本)上面运行,则证明不支持请求该权限
// 例如 MANAGE_EXTERNAL_STORAGE 权限是 Android 11 才出现的权限,在 Android 10 上面肯定是不支持申请
return getFromAndroidVersion(context) <= PermissionVersion.getCurrentVersion();
}
/**
* 判断当前权限是否授予
*/
default boolean isGrantedPermission(@NonNull Context context) {
return isGrantedPermission(context, true);
}
/**
* 判断当前权限是否授予
*
* @param skipRequest 是否跳过申请直接判断的权限状态
*/
boolean isGrantedPermission(@NonNull Context context, boolean skipRequest);
/**
* 判断当前权限是否被用户勾选了《不再询问的选项》
*/
boolean isDoNotAskAgainPermission(@NonNull Activity activity);
/**
* 获取当前权限所有可用的设置页意图
*/
@NonNull
default List getPermissionSettingIntents(@NonNull Context context) {
return getPermissionSettingIntents(context, true);
}
/**
* 获取当前权限所有可用的设置页意图
*
* 需要注意的是:无需在此方法中判断设置页的意图是否存在再添加,
* 因为框架在跳转的时候框架会先过滤一遍不存在的意图,
* 另外通过代码事先判断出来存在的意图也有可能会跳转失败,
* 如果出现跳转失败会自动使用下一个意图进行跳转,
* 总结:不存在的意图铁定会跳转失败,存在的意图不一定 100% 会跳转成功。
*
* @param skipRequest 是否跳过申请直接获取的 Intent
*/
@NonNull
List getPermissionSettingIntents(@NonNull Context context, boolean skipRequest);
/**
* 获取权限请求的间隔时间
*/
default int getRequestIntervalTime(@NonNull Context context) {
return 0;
}
/**
* 获取处理权限结果的等待时间
*/
default int getResultWaitTime(@NonNull Context context) {
return 0;
}
/**
* 检查权限请求是否合规
*/
default void checkCompliance(@NonNull Activity activity,
@NonNull List