From e60a35704e801f6281b9305135d1911e6278056b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Android=20=E8=BD=AE=E5=AD=90=E5=93=A5?= Date: Sun, 11 Dec 2022 15:06:52 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=8B=B1=E6=96=87=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E7=9A=84=E6=96=87=E6=A1=A3=E5=8F=8A=20demo=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=AF=B7=E6=B1=82=E5=89=8D=E5=BC=B9=E5=87=BA?= =?UTF-8?q?=E6=9D=83=E9=99=90=E8=AF=B4=E6=98=8E=E5=BC=B9=E7=AA=97=E6=A1=88?= =?UTF-8?q?=E4=BE=8B=20=E6=96=B0=E5=A2=9E=E9=80=9A=E7=9F=A5=E6=A0=8F?= =?UTF-8?q?=E7=9B=91=E5=90=AC=E6=9D=83=E9=99=90=E6=B8=85=E5=8D=95=E6=A3=80?= =?UTF-8?q?=E6=B5=8B=E6=9C=BA=E5=88=B6=20=E8=A1=A5=E5=85=85=E7=94=BB?= =?UTF-8?q?=E4=B8=AD=E7=94=BB=E6=9D=83=E9=99=90=E6=B8=85=E5=8D=95=E6=A3=80?= =?UTF-8?q?=E6=B5=8B=E6=9C=BA=E5=88=B6=20=E4=BC=98=E5=8C=96=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E6=B8=85=E5=8D=95=E6=96=87=E4=BB=B6=E7=9A=84=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=20=E4=BC=98=E5=8C=96=E6=A1=86=E6=9E=B6=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E9=83=A8=E5=88=86=E4=BB=A3=E7=A0=81=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=20=E4=BC=98=E5=8C=96=E6=9D=83=E9=99=90=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E6=8B=A6=E6=88=AA=E5=99=A8=E7=9A=84=E6=96=B9=E6=B3=95=E5=91=BD?= =?UTF-8?q?=E5=90=8D=20=E4=BC=98=E5=8C=96=E8=8E=B7=E5=8F=96=E9=80=9A?= =?UTF-8?q?=E7=9F=A5=E7=9B=91=E5=90=AC=E8=AE=BE=E7=BD=AE=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=20Intent=20=E9=80=BB=E8=BE=91=20=E4=BF=AE=E6=AD=A3=20Android?= =?UTF-8?q?=2012=20=E5=AE=9A=E4=BD=8D=E6=9D=83=E9=99=90=E6=A3=80=E6=B5=8B?= =?UTF-8?q?=E6=9C=BA=E5=88=B6=20=E4=BF=AE=E6=AD=A3=20Android=2013=20?= =?UTF-8?q?=E5=AA=92=E4=BD=93=E6=9D=83=E9=99=90=E5=90=91=E4=B8=8B=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=E7=9A=84=E6=96=B9=E5=BC=8F=20=E4=BF=AE=E6=AD=A3=20Per?= =?UTF-8?q?missionPageFragment=20=E4=BD=BF=E7=94=A8=E5=AE=8C=E6=B2=A1?= =?UTF-8?q?=E6=9C=89=E7=A7=BB=E9=99=A4=E7=9A=84=E9=97=AE=E9=A2=98=20?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E5=9C=A8=E6=B2=A1=E6=9C=89=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E4=BB=BB=E4=BD=95=E6=9D=83=E9=99=90=E7=9A=84=E6=83=85=E5=86=B5?= =?UTF-8?q?=E4=B8=8B=E8=B0=83=E7=94=A8=20request=20=E5=87=BA=E7=8E=B0?= =?UTF-8?q?=E5=B4=A9=E6=BA=83=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ISSUE_TEMPLATE/issue_en_template_bug.md | 48 +++ .../issue_en_template_question.md | 19 + .../issue_en_template_suggest.md | 19 + ...mplate_bug.md => issue_zh_template_bug.md} | 10 +- ...stion.md => issue_zh_template_question.md} | 2 +- ...uggest.md => issue_zh_template_suggest.md} | 6 +- HelpDoc-en.md | 358 ++++++++++++++++ HelpDoc.md => HelpDoc-zh.md | 64 +-- README-en.md | 394 ++++++++++++++++++ README.md | 64 +-- app/build.gradle | 10 +- app/src/main/AndroidManifest.xml | 6 +- .../hjq/permissions/demo/MainActivity.java | 136 +++--- .../demo/NotificationMonitorService.java | 2 +- .../demo/PermissionInterceptor.java | 200 ++++++--- .../demo/PermissionNameConvert.java | 38 +- .../permission_description_popup_bg.xml | 13 + app/src/main/res/layout/activity_main.xml | 50 +-- .../layout/permission_description_popup.xml | 42 ++ app/src/main/res/values-v21/styles.xml | 2 + app/src/main/res/values-zh/string_demo.xml | 41 ++ .../string_permission.xml} | 40 +- app/src/main/res/values/string_demo.xml | 41 ++ app/src/main/res/values/string_permission.xml | 52 +++ app/src/main/res/values/styles.xml | 2 + app/src/main/res/xml/locales_config.xml | 6 + library/build.gradle | 4 +- .../hjq/permissions/AndroidManifestInfo.java | 73 ++++ .../permissions/AndroidManifestParser.java | 149 +++++++ .../com/hjq/permissions/AndroidVersion.java | 18 +- .../permissions/IPermissionInterceptor.java | 50 ++- .../hjq/permissions/OnPermissionCallback.java | 8 +- .../permissions/OnPermissionPageCallback.java | 2 +- .../java/com/hjq/permissions/Permission.java | 22 +- .../com/hjq/permissions/PermissionApi.java | 24 +- .../hjq/permissions/PermissionChecker.java | 368 +++++++++------- .../hjq/permissions/PermissionDelegate.java | 7 +- .../PermissionDelegateImplV14.java | 66 ++- .../PermissionDelegateImplV23.java | 31 +- .../PermissionDelegateImplV26.java | 19 +- .../PermissionDelegateImplV28.java | 5 +- .../PermissionDelegateImplV29.java | 8 +- .../PermissionDelegateImplV30.java | 9 +- .../PermissionDelegateImplV31.java | 11 +- .../PermissionDelegateImplV33.java | 5 +- .../hjq/permissions/PermissionFragment.java | 48 ++- .../permissions/PermissionPageFragment.java | 44 +- .../com/hjq/permissions/PermissionUtils.java | 173 +++----- .../com/hjq/permissions/XXPermissions.java | 248 +++++++---- picture/en/1.jpg | Bin 0 -> 34189 bytes picture/en/10.jpg | Bin 0 -> 19786 bytes picture/en/11.jpg | Bin 0 -> 12290 bytes picture/en/12.jpg | Bin 0 -> 29055 bytes picture/en/13.jpg | Bin 0 -> 28834 bytes picture/en/14.jpg | Bin 0 -> 21164 bytes picture/en/15.jpg | Bin 0 -> 18632 bytes picture/en/2.jpg | Bin 0 -> 25605 bytes picture/en/3.jpg | Bin 0 -> 19636 bytes picture/en/4.jpg | Bin 0 -> 17796 bytes picture/en/5.jpg | Bin 0 -> 16413 bytes picture/en/6.jpg | Bin 0 -> 15643 bytes picture/en/7.jpg | Bin 0 -> 18112 bytes picture/en/8.jpg | Bin 0 -> 17138 bytes picture/en/9.jpg | Bin 0 -> 20982 bytes picture/en/location_1.jpg | Bin 0 -> 28055 bytes picture/en/location_2.jpg | Bin 0 -> 22400 bytes picture/en/miui_1.jpg | Bin 0 -> 76287 bytes picture/en/miui_2.jpg | Bin 0 -> 87350 bytes picture/en/miui_3.jpg | Bin 0 -> 56298 bytes picture/miui_1.jpg | Bin 76038 -> 0 bytes picture/miui_2.jpg | Bin 40476 -> 0 bytes picture/miui_3.jpg | Bin 185964 -> 0 bytes picture/miui_4.jpg | Bin 147253 -> 185964 bytes picture/miui_5.jpg | Bin 0 -> 147253 bytes picture/{ => zh}/1.jpg | Bin picture/{ => zh}/10.jpg | Bin picture/{ => zh}/11.jpg | Bin picture/{ => zh}/12.jpg | Bin picture/{ => zh}/13.jpg | Bin picture/{ => zh}/14.jpg | Bin picture/{ => zh}/15.jpg | Bin picture/{ => zh}/2.jpg | Bin picture/{ => zh}/3.jpg | Bin picture/{ => zh}/4.jpg | Bin picture/{ => zh}/5.jpg | Bin picture/{ => zh}/6.jpg | Bin picture/{ => zh}/7.jpg | Bin picture/{ => zh}/8.jpg | Bin picture/{ => zh}/9.jpg | Bin picture/{ => zh}/location_1.jpg | Bin picture/{ => zh}/location_2.jpg | Bin picture/zh/miui_1.jpg | Bin 0 -> 71063 bytes picture/zh/miui_2.jpg | Bin 0 -> 69358 bytes picture/zh/miui_3.jpg | Bin 0 -> 49845 bytes 94 files changed, 2319 insertions(+), 738 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/issue_en_template_bug.md create mode 100644 .github/ISSUE_TEMPLATE/issue_en_template_question.md create mode 100644 .github/ISSUE_TEMPLATE/issue_en_template_suggest.md rename .github/ISSUE_TEMPLATE/{issue_template_bug.md => issue_zh_template_bug.md} (78%) rename .github/ISSUE_TEMPLATE/{issue_template_question.md => issue_zh_template_question.md} (79%) rename .github/ISSUE_TEMPLATE/{issue_template_suggest.md => issue_zh_template_suggest.md} (54%) create mode 100644 HelpDoc-en.md rename HelpDoc.md => HelpDoc-zh.md (77%) create mode 100644 README-en.md create mode 100644 app/src/main/res/drawable/permission_description_popup_bg.xml create mode 100644 app/src/main/res/layout/permission_description_popup.xml create mode 100644 app/src/main/res/values-zh/string_demo.xml rename app/src/main/res/{values/strings.xml => values-zh/string_permission.xml} (57%) create mode 100644 app/src/main/res/values/string_demo.xml create mode 100644 app/src/main/res/values/string_permission.xml create mode 100644 app/src/main/res/xml/locales_config.xml create mode 100644 library/src/main/java/com/hjq/permissions/AndroidManifestInfo.java create mode 100644 library/src/main/java/com/hjq/permissions/AndroidManifestParser.java create mode 100644 picture/en/1.jpg create mode 100644 picture/en/10.jpg create mode 100644 picture/en/11.jpg create mode 100644 picture/en/12.jpg create mode 100644 picture/en/13.jpg create mode 100644 picture/en/14.jpg create mode 100644 picture/en/15.jpg create mode 100644 picture/en/2.jpg create mode 100644 picture/en/3.jpg create mode 100644 picture/en/4.jpg create mode 100644 picture/en/5.jpg create mode 100644 picture/en/6.jpg create mode 100644 picture/en/7.jpg create mode 100644 picture/en/8.jpg create mode 100644 picture/en/9.jpg create mode 100644 picture/en/location_1.jpg create mode 100644 picture/en/location_2.jpg create mode 100644 picture/en/miui_1.jpg create mode 100644 picture/en/miui_2.jpg create mode 100644 picture/en/miui_3.jpg delete mode 100644 picture/miui_1.jpg delete mode 100644 picture/miui_2.jpg delete mode 100644 picture/miui_3.jpg create mode 100644 picture/miui_5.jpg rename picture/{ => zh}/1.jpg (100%) rename picture/{ => zh}/10.jpg (100%) rename picture/{ => zh}/11.jpg (100%) rename picture/{ => zh}/12.jpg (100%) rename picture/{ => zh}/13.jpg (100%) rename picture/{ => zh}/14.jpg (100%) rename picture/{ => zh}/15.jpg (100%) rename picture/{ => zh}/2.jpg (100%) rename picture/{ => zh}/3.jpg (100%) rename picture/{ => zh}/4.jpg (100%) rename picture/{ => zh}/5.jpg (100%) rename picture/{ => zh}/6.jpg (100%) rename picture/{ => zh}/7.jpg (100%) rename picture/{ => zh}/8.jpg (100%) rename picture/{ => zh}/9.jpg (100%) rename picture/{ => zh}/location_1.jpg (100%) rename picture/{ => zh}/location_2.jpg (100%) create mode 100644 picture/zh/miui_1.jpg create mode 100644 picture/zh/miui_2.jpg create mode 100644 picture/zh/miui_3.jpg diff --git a/.github/ISSUE_TEMPLATE/issue_en_template_bug.md b/.github/ISSUE_TEMPLATE/issue_en_template_bug.md new file mode 100644 index 0000000..1bb115f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_en_template_bug.md @@ -0,0 +1,48 @@ +--- +name: Submit Bug +about: Please tell me the problem with the framework and I will help you fix it! +labels: bug +assignees: getActivity + +--- + + +## [Warning: Please be sure to fill in according to the issue template. Don't take any chances. Once you find that the issue is not filled in carefully according to the template, you will close it directly.] + +#### Description of the problem + +* Framework Version [Required]: Please enter the framework version you are using + +* Problem Description [Required]: Please enter your description of the problem + +* Reproduction step [Required]: Please enter the steps to reproduce the problem (Note: Bug without reproduction step is not accepted at present) + +* Whether the problem can be reproduced [Required]: Yes/No + +* Phone information in question [Required]: Please fill in the phone brand and model in question + +* Android version in question [Required]: Please fill in the Android version in question + +* Source of problem information [Required]: Please fill in the source of the problem (for example: encountered by yourself / see on firebase crashlytics / user feedback, etc.) + +#### Please respond + +* Is it a specific phone or all phones will appear [Must answer]: Some/All (for example: google phones, certain Android versions will appear) + +* Does the latest version of the framework have this problem [Must answer]: Yes/No (if you are using an older version, it is recommended to upgrade to see if the problem still exists) + +* Have you consulted the framework documentation but have not resolved [Must answer]: Yes/No (the documentation will provide answers to the most common questions, and you can see if there is anything you want) + +* Has anyone ever asked a similar question [Must answer]: Yes/No (see if anyone has ever asked a similar question, and refer to how others have solved it first) + +* Whether the question can be reproduced through Demo [Must answer]: Yes/No (it is used to check whether it is caused by a problem with the writing of your own project code.) + +* Does this issue also occur using the system-provided permissions API [Must answer]: Yes/No (it is used to check whether there is a problem with the code of the framework.) + +#### Other + +* Provide the error stack (if it is a crash, it needs to be provided, be careful not to take the obfuscated code stack) + +* Provide screenshots or videos (if available, it is recommended to fill in) + +* Provide solutions (if any, it is recommended to fill in) \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/issue_en_template_question.md b/.github/ISSUE_TEMPLATE/issue_en_template_question.md new file mode 100644 index 0000000..2b8975e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_en_template_question.md @@ -0,0 +1,19 @@ +--- +name: Ask questions +about: Ask questions and I'll answer them +labels: question +assignees: getActivity + +--- + +## [Warning: Please be sure to fill in according to the issue template. Don't take any chances. Once you find that the issue is not filled in carefully according to the template, you will close it directly.] + +#### Description of the problem + +* Problem Description [Required]: Please describe your problem (Note: If it is determined to be a framework bug, please do not mention it here, otherwise it will not be accepted) + +#### Please respond + +* Have you consulted the framework documentation but have not resolved [Must answer]: Yes/No (the documentation will provide answers to the most common questions, and you can see if there is anything you want) + +* Has anyone ever asked a similar question [Must answer]: Yes/No (see if anyone has ever asked a similar question, and refer to how others have solved it first) diff --git a/.github/ISSUE_TEMPLATE/issue_en_template_suggest.md b/.github/ISSUE_TEMPLATE/issue_en_template_suggest.md new file mode 100644 index 0000000..1c61a84 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_en_template_suggest.md @@ -0,0 +1,19 @@ +--- +name: Submit a suggestion +about: Please tell me the shortcomings of the framework, so that I can do better! +labels: help wanted +assignees: getActivity + +--- + +## [Warning: Please be sure to fill in according to the issue template. Don't take any chances. Once you find that the issue is not filled in carefully according to the template, you will close it directly.] + +#### Suggest collection + +* Issue Has anyone asked a similar question before? [Must answer]: (Once there are repeated questions, I will not answer them again) + +* Does the framework documentation mention this? [Must answer]: Yes/No (please read the documentation of the framework before making suggestions) + +* What do you think the framework is lacking in? [Must answer]: (You can describe what makes you dissatisfied with the framework) + +* What do you think would be better to improve? [Optional]: (You can provide your own ideas or practices for the author's reference) \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/issue_template_bug.md b/.github/ISSUE_TEMPLATE/issue_zh_template_bug.md similarity index 78% rename from .github/ISSUE_TEMPLATE/issue_template_bug.md rename to .github/ISSUE_TEMPLATE/issue_zh_template_bug.md index c28bcbb..39e6817 100644 --- a/.github/ISSUE_TEMPLATE/issue_template_bug.md +++ b/.github/ISSUE_TEMPLATE/issue_zh_template_bug.md @@ -10,13 +10,13 @@ assignees: getActivity #### 问题描述 -* 框架版本【必填】:XXX +* 框架版本【必填】:请输入你使用的框架版本 -* 问题描述【必填】:XXX +* 问题描述【必填】:请输入你对这个问题的描述 -* 复现步骤【必填】:XXX(注意:目前不受理没有复现步骤的 Bug 单) +* 复现步骤【必填】:请输入问题的复现步骤(注意:目前不受理没有复现步骤的 Bug 单) -* 是否必现【必填】:填是/否 +* 是否必现【必填】:是/否 * 出现问题的手机信息【必填】:请填写出现问题的品牌和机型 @@ -30,7 +30,7 @@ assignees: getActivity * 框架最新的版本是否存在这个问题【必答】:是/否(如果用的是旧版本的话,建议升级看问题是否还存在) -* 是否已经查阅框架文档还未能解决的【必答】:是/否(文档会提供最常见的问题解答,可以看看是否有自己想要的) +* 是否已经查阅框架文档但还未能解决的【必答】:是/否(文档会提供最常见的问题解答,可以看看是否有自己想要的) * issue 是否有人曾提过类似的问题【必答】:是/否(看看曾经有人提过类似的问题,先参考一下别人是怎么解决的) diff --git a/.github/ISSUE_TEMPLATE/issue_template_question.md b/.github/ISSUE_TEMPLATE/issue_zh_template_question.md similarity index 79% rename from .github/ISSUE_TEMPLATE/issue_template_question.md rename to .github/ISSUE_TEMPLATE/issue_zh_template_question.md index 1101285..e1b6212 100644 --- a/.github/ISSUE_TEMPLATE/issue_template_question.md +++ b/.github/ISSUE_TEMPLATE/issue_zh_template_question.md @@ -10,7 +10,7 @@ assignees: getActivity #### 问题描述 -* 请描述一下你的疑惑【必填】:XXX(注意:如果是框架 bug 请不要在这里提,否则一概不受理) +* 问题描述【必填】:请描述一下你的问题(注意:如果确定是框架 bug 请不要在这里提,否则一概不受理) #### 请回答 diff --git a/.github/ISSUE_TEMPLATE/issue_template_suggest.md b/.github/ISSUE_TEMPLATE/issue_zh_template_suggest.md similarity index 54% rename from .github/ISSUE_TEMPLATE/issue_template_suggest.md rename to .github/ISSUE_TEMPLATE/issue_zh_template_suggest.md index 20da57b..d789f4c 100644 --- a/.github/ISSUE_TEMPLATE/issue_template_suggest.md +++ b/.github/ISSUE_TEMPLATE/issue_zh_template_suggest.md @@ -10,10 +10,10 @@ assignees: getActivity #### 建议收集 -* issue 是否有人曾提过类似的问题?【必答】(一旦出现重复提问我将不会再次解答) +* issue 是否有人曾提过类似的问题?【必答】:(一旦出现重复提问我将不会再次解答) * 框架文档是否有提及到此问题?【必答】:是/否(请先看完框架的文档后再来提建议) -* 你觉得框架有什么不足之处?【必答】(你可以描述框架有什么令你不满意的地方) +* 你觉得框架有什么不足之处?【必答】:(你可以描述框架有什么令你不满意的地方) -* 你觉得该怎么去完善会比较好?【非必答】(你可以提供一下自己的想法或者做法供作者参考) \ No newline at end of file +* 你觉得该怎么去完善会比较好?【非必答】:(你可以提供一下自己的想法或者做法供作者参考) \ No newline at end of file diff --git a/HelpDoc-en.md b/HelpDoc-en.md new file mode 100644 index 0000000..1a7a9da --- /dev/null +++ b/HelpDoc-en.md @@ -0,0 +1,358 @@ +#### 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 is it not compatible with dangerous permission applications below Android 6.0](#why-is-it-not-compatible-with-dangerous-permission-applications-below-android-60) + +* [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 does the new version of the framework remove the function of the china mobile phone permission setting page](#why-does-the-new-version-of-the-framework-remove-the-function-of-the-china-mobile-phone-permission-setting-page) + +* [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) + +* [How to deal with the problem that some china application stores are not allowed to apply again within 48 hours after they explicitly refuse permission](#how-to-deal-with-the-problem-that-some-china-application-stores-are-not-allowed-to-apply-again-within-48-hours-after-they-explicitly-refuse-permission) + +#### Android 11 Location Permission Adaptation + +* On Android 10, positioning permissions are divided into foreground permissions (precise and fuzzy) and background permissions, while on Android 11, you need to apply for these two permissions separately. If you apply for these two permissions ** Ruthlessly rejected by the system ** at the same time, even the permission application dialog box will not pop up, and the system will reject it immediately. It directly leads to the failure of location permission application. + +* If you are using the latest version of **XXPermissions**, you ** Congratulations ** can directly pass the foreground and background positioning permissions to the framework. The framework has automatically applied for these two permissions separately for you. The whole adaptation process ** Zero cost **. + +* However, it should be noted that the application process is divided into two steps. The first step is to apply for the foreground location permission, and the second step is to apply for the background location permission. The user must first agree to the foreground location permission before entering the application for the background location permission. There are two ways to approve the foreground location permission: check `Allow only while using the app` or `Ask every time`. In the background location permission application, the user must check `Allow all the time`. Only in this way can the background location permission application be approved. + +* And if your application only needs to use the location function in the foreground, but does not need to use the location function in the background, please do not apply for `Permission.ACCESS_BACKGROUND_LOCATION` permission. + +![](picture/en/location_1.jpg) + +![](picture/en/location_2.jpg) + +#### Android 11 storage permission adaptation + +* If your project needs to adapt to Android 11storage 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(Permission.Group.STORAGE) + // Not yet adapted to Android 11 scoped storage needs to be called like this + .permission(Permission.MANAGE_EXTERNAL_STORAGE) + .request(new OnPermissionCallback() { + + @Override + public void onGranted(@NonNull List permissions, boolean all) { + if (all) { + toast("获取存储权限成功"); + } + } + }); +``` + +![](picture/en/7.jpg) + +#### When do I need to adapt to the characteristics of partitioned storage + +* If your app needs to be available on Google Play, you need to check it out in detail: [ Google App Store policy (need to climb over the wall) ](https://support.google.com/googleplay/android-developer/answer/9956427). [ Google Play notifications ](https://developer.android.google.cn/training/data-storage/manage-all-files#all-files-access-google-play) + +* The origin of scoped storage: Google has received many complaints from users before, saying that many applications create directories and files under the SD card, which makes it very troublesome for users to manage mobile phone files (there are so many foreign netizens with obsessive-compulsive disorder, ha ha), so in the Android 10 version update. Google requires all developers to store media files in their own internal directory or in the internal directory of the SD card, but Google has adopted a relaxed policy on one version, adding `android:requestLegacyExternalStorage="true"` the adaptation of this feature to the manifest file, but on Android 11, you have two options: + + 1. Adapting scoped storage: This is a method recommended by Google, but it will increase the workload, because it is very troublesome to adapt scoped storage, which is my personal feeling. However, for some specific applications, such as file managers, backup and recovery applications, anti-virus applications, document management applications, on-device file search, disk and file encryption, device-to-device data migration and so on, they must use external storage, which requires the second way to achieve. + + 2. Apply for external storage permissions: This is a way that Google does not recommend. It only needs `MANAGE_EXTERNAL_STORAGE` permissions, and there is basically no pressure to adapt. However, there will be a problem, that is, when it is put on the Google App Market, it must be reviewed and approved by Google Play. + +* To sum up, I think both are good and bad, but I can share my views with you. + + 1. If your app needs to be on the Google Apps Marketplace, you need to adapt to partitioned storage as soon as possible, because Google is really doing it this time. + + 2. If your application is only available in the china application market, and there is no subsequent need to be available in the Google application market, then you can also directly apply for `MANAGE_EXTERNAL_STORAGE` permission to read and write external storage. + +#### Why does the app restart after Android 11 grants the install permission + +* [Android 11 feature adjustment, installation of external source application requires restarting App](https://cloud.tencent.com/developer/news/637591) + +* First of all, this problem is a new feature of Android 11, not caused by the framework. Of course, there is no way to avoid this problem, because the application is killed by the system, and the level of the application is certainly not as high as that of the system. At present, there is no solution for this in the industry. If you have a good solution, you are welcome to provide it to me. + +* In addition, after practice, this problem will no longer appear on Android 12, proving that the problem has been fixed by Google. + +#### Why is the storage permission granted but the permission setting page still shows unauthorized + +* First of all, I need to correct a wrong idea. `READ_EXTERNAL_STORAGE` `WRITE_EXTERNAL_STORAGE` These two permissions and `MANAGE_EXTERNAL_STORAGE` permissions are two different things. Although they are both called storage permissions, they belong to two completely different permissions. If you apply for `MANAGE_EXTERNAL_STORAGE` permission and grant permission, However, you do not see that the permission has been granted on the permission setting page. Please note that this situation is normal, because what you see on the permission setting page is the storage grant status `READ_EXTERNAL_STORAGE` and `WRITE_EXTERNAL_STORAGE` permission status, not `MANAGE_EXTERNAL_STORAGE` the permission status, but at this time, the storage permission has been obtained. You don't have to worry about the permission status displayed on the permission setting page. You can read and write files directly. There will be no permission problem. + +* One more question, why only appear on devices above Android 11? First of all `MANAGE_EXTERNAL_STORAGE`, only Android 11 has permission. Android 10 and previous versions do not have this permission. If you apply for `MANAGE_EXTERNAL_STORAGE` permission on a lower version device, the framework will help you do downward compatibility. Will automatically help you replace `READ_EXTERNAL_STORAGE`, `WRITE_EXTERNAL_STORAGE` permissions to apply, this time you see the permission settings page of the storage permission status must be normal, which is why you only see this problem in Android 11 and above devices. + +#### What should I do if the dialog box pops up before and after the permission application + +* An interceptor interface is provided inside the framework. It is enough to implement the interface provided [ IPermissionInterceptor ](/library/src/main/java/com/hjq/permissions/IPermissionInterceptor.java) in the framework. For specific implementation, please refer to the [ 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 interception is also very simple. There are two specific settings, one for local settings and the other for global settings. + +```java +XXPermissions.with(this) + .permission(Permission.XXX) + // Set permission request interceptor (local settings) + .interceptor(new PermissionInterceptor()) + .request(new OnPermissionCallback() { + + @Override + public void onGranted(@NonNull List permissions, boolean all) { + ...... + } + + @Override + public void onDenied(@NonNull List permissions, boolean never) { + ...... + } + }); +``` + +```java +public class XxxApplication extends Application { + + @Override + public void onCreate() { + super.onCreate(); + + // Set permission request interceptor (global setting) + XXPermissions.setInterceptor(new PermissionInterceptor()); + } +} +``` + +#### 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(Permission.RECORD_AUDIO) + .permission(Permission.Group.CALENDAR) + .request(new OnPermissionCallback() { + + @Override + public void onGranted(@NonNull List permissions, boolean all) { + if (all) { + toast("Acquired recording and calendar permissions successfully"); + } + } + + @Override + public void onDenied(@NonNull List permissions, boolean never) { + if (never && permissions.contains(Permission.RECORD_AUDIO) && + XXPermissions.isPermanentDenied(MainActivity.this, Permission.RECORD_AUDIO)) { + toast("Recording permission has been permanently denied"); + } + } + }); +``` + +#### Why is it not compatible with dangerous permission applications below Android 6.0 + +* Because the dangerous rights management below Android 6.0 was done by china mobile phone manufacturers, at that time Google did not have a unified scheme for dangerous rights management, so even if our application did not adapt, there would be no problem, because mobile phone manufacturers have their own handling of this area, but one thing is certain, even if the user refused authorization. It will not cause the application to crash, but will only return a blank pass. + +* If **XXPermissions** does this adaptation, it can also be done by reflecting the field in the AppOpsManager class of the system service, but it can not guarantee the accuracy of the permission judgment, and there may be some errors. Secondly, the cost of adaptation is too high, because there are too many china mobile phone manufacturers, and the changes to this area are uneven. + +* Considering that the proportion of devices below Android 6.0 is very low, there will be fewer and fewer devices in the future, and they will gradually withdraw from the stage of history, so my decision is not to adapt to this area. + +#### 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(Permission.CAMERA) + .request(this); + } + + @Override + public void onGranted(@NonNull List permissions, boolean all) { + if (all) { + toast("Successfully obtained permission to take camera"); + } + } + + @Override + public void onDenied(@NonNull List permissions, boolean never) { + if (never) { + 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(MainActivity.this, permissions); + } else { + requestCameraPermission(); + } + } + + @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 does the new version of the framework remove the function of the china mobile phone permission setting page + +> [ [Issue] Permission denied and no longer prompted](https://github.com/getActivity/XXPermissions/issues/99) + +> [ [Issue] After the permission of the Xiaomi mobile phone is denied, the opening of the Xiaomi permission setting interface judged in the library is invalid](https://github.com/getActivity/XXPermissions/issues/38) + +> [When [issue] applies for storage permission normally, it is rejected permanently, and then the permission inquiry is opened on the application setting page, but the system permission application pop-up window is not displayed](https://github.com/getActivity/XXPermissions/issues/100) + +* This function exists in **XXPermissions** 9.0 and before, but I removed this function in the subsequent version, because many people told me that this function actually has a big defect, for example, the page that may jump on some new Huawei models is not the permission setting page of the application. Instead, it is a permission management list interface for all applications. + +* In fact, not only Huawei has problems, but also millet has problems. Many people have fed back the same problem with me. **XXPermissions** jumps to the china mobile phone permission setting page. After the user grants the permission normally, it still detects that the permission is still rejected. There are many times of feedback on this problem, but the reason can not be found out. Finally, I got the answer for the last time, which [someone](https://github.com/getActivity/XXPermissions/issues/38) helped me to find out the problem of MIUI optimization switch (Xiaomi mobile phone --> developer options --> Turn on MIUl optimization). So the question is, what is the function of this switch? How does this affect **XXPermissions**? + +![](picture/en/miui_1.jpg) + +* First of all, this problem should start with the principle of **XXPermissions** jumping to the china mobile phone settings page. From the native API provided by Google, we can only jump to the application details page at most, not directly to the permission settings page, but need users to click again on the application details page to enter the permission settings page. If we look at this problem from the perspective of user experience, it is definitely best to jump directly to the permission setting page, but this way is not supported by Google. Of course, there are ways to achieve it. There is a common answer on the Internet, which is to directly capture the name of the permission setting page `Activity` of a certain brand of mobile phone and then jump. The starting point of this idea is good, but there are many problems, which can not guarantee that all models of each brand can be adapted in place. The number and frequency of mobile phone manufacturers changing `Activity` the package name are relatively high, and almost all of the new Huawei models released recently have failed. That is to say `startActivity`, there will be reports `ActivityNotFoundException` or `SecurityException` exceptions. Of course, these exceptions can be captured, but only crashes can be captured. We can't learn and deal with some non-crash behaviors, such as the problems of Huawei and Xiaomi I just talked about. These problems do not cause crashes, but they do cause functional anomalies. + +* The MIUI optimization switch is a switch reserved by millet engineers to switch MIUI and native functions. For example, when the switch is turned on, clicking on the permission management on the application details page will jump to the permission setting page of millet. If the switch is off (the default is on), Clicking on permission management on the application details page will jump to Google's native permission setting page, as shown in the figure: + +![](picture/en/miui_2.jpg) + +![](picture/en/miui_3.jpg) + +* The biggest problem is that the two interfaces are different activities. One is the permission setting page customized by Xiaomi, and the second is the native permission setting page of Google. When the MIUI optimization is turned on, the permission granted on the permission setting page customized by Xiaomi can be effective. When the MIUI optimization is turned off, Permission can only be granted in Google's native permission settings page. Jumping to the china mobile phone page will only jump to the permission setting page customized by Xiaomi forever, so it will lead to the problem that when the MIUI optimization is turned off, the return after using the code to jump to the Xiaomi permission setting page to grant permission still fails. + +* Some people may say that the way to solve this problem is very simple, judge the MIUI optimization switch, if it is open, jump to the Xiaomi customized permission setting page, if it is closed, jump to Google's native permission setting page, so it is not possible? In fact, I have tried this solution, I have commissioned to contact the MIUI engineer working in Xiaomi, and someone helped me to feedback this problem to Xiaomi, and finally got the same answer. + +![](picture/miui_4.jpg) + +![](picture/miui_5.jpg) + +* It is also worth mentioning [Android 11 puts restrictions on package visibility](https://developer.android.google.cn/about/versions/11/privacy/package-visibility) that this way of skipping package names will not be feasible in the future. + +* Final decision: The intent of this feature is good, but we can't do it well. After careful consideration, we decided to remove this feature in [**XXPermissions** Version 9.2 ](https://github.com/getActivity/XXPermissions/releases/tag/9.2) and after versions. + +#### 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. + +#### How to deal with the problem that some china application stores are not allowed to apply again within 48 hours after they explicitly refuse permission + +* First of all, this is a business logic problem. The framework itself will not do this kind of thing, but it is not impossible to achieve. This is due to the good design of the framework. The framework provides an interceptor class called IPermissionInterceptor. You can use the callback of the Request Permissions method. You can rewrite the logic of this method to determine whether the permission you want to apply for has been applied once within 48 hours. If not, you can use the permission application process. If yes, you can directly call back the method of permission application failure. + +```java +public final class PermissionInterceptor implements IPermissionInterceptor { + + private static final String SP_NAME_PERMISSION_REQUEST_TIME_RECORD = "permission_request_time_record"; + + @Override + public void launchPermissionRequest(@NonNull Activity activity, @NonNull List allPermissions, + @Nullable OnPermissionCallback callback) { + SharedPreferences sharedPreferences = activity.getSharedPreferences(SP_NAME_PERMISSION_REQUEST_TIME_RECORD, Context.MODE_PRIVATE); + String permissionKey = String.valueOf(allPermissions); + long lastRequestPermissionTime = sharedPreferences.getLong(permissionKey, 0); + if (System.currentTimeMillis() - lastRequestPermissionTime <= 1000 * 60 * 60 * 24 * 2) { + List deniedPermissions = XXPermissions.getDenied(activity, allPermissions); + List grantedPermissions = new ArrayList<>(allPermissions); + grantedPermissions.removeAll(deniedPermissions); + deniedPermissions(activity, allPermissions, deniedPermissions, true, callback); + if (!grantedPermissions.isEmpty()) { + grantedPermissions(activity, allPermissions, grantedPermissions, false, callback); + } + return; + } + sharedPreferences.edit().putLong(permissionKey, System.currentTimeMillis()).apply(); + // If you have not applied for permission before, or it has been more than 48 hours since the last application, apply for permission + IPermissionInterceptor.super.requestPermissions(activity, allPermissions, callback); + } + + @Override + public void grantedPermissionRequest(@NonNull Activity activity, @NonNull List allPermissions, + @NonNull List grantedPermissions, boolean all, + @Nullable OnPermissionCallback callback) { + if (callback == null) { + return; + } + callback.onGranted(grantedPermissions, all); + } + + @Override + public void deniedPermissionRequest(@NonNull Activity activity, @NonNull List allPermissions, + @NonNull List deniedPermissions, boolean never, + @Nullable OnPermissionCallback callback) { + if (callback == null) { + return; + } + callback.onDenied(deniedPermissions, never); + } +} +``` \ No newline at end of file diff --git a/HelpDoc.md b/HelpDoc-zh.md similarity index 77% rename from HelpDoc.md rename to HelpDoc-zh.md index 2937823..f18f50e 100644 --- a/HelpDoc.md +++ b/HelpDoc-zh.md @@ -12,7 +12,7 @@ * [我想在申请前和申请后统一弹对话框该怎么处理](#我想在申请前和申请后统一弹对话框该怎么处理) -* [如何在回调中判断哪些权限被永久拒绝了](#如何在回调中判断哪些权限被永久拒绝了) +* [如何在回调中知道哪些权限被永久拒绝了](#如何在回调中知道哪些权限被永久拒绝了) * [为什么不兼容 Android 6.0 以下的危险权限申请](#为什么不兼容-android-60-以下的危险权限申请) @@ -34,15 +34,15 @@ * 在 Android 10 上面,定位权限被划分为前台权限(精确和模糊)和后台权限,而到了 Android 11 上面,需要分别申请这两种权限,如果同时申请这两种权限会**惨遭系统无情拒绝**,连权限申请对话框都不会弹,立马被系统拒绝,直接导致定位权限申请失败。 -* 如果你使用的是 XXPermissions 最新版本,那么**恭喜你**,直接将前台定位权限和后台定位权限全部传给框架即可,框架已经自动帮你把这两种权限分开申请了,整个适配过程**零成本**。 +* 如果你使用的是 **XXPermissions** 最新版本,那么**恭喜你**,直接将前台定位权限和后台定位权限全部传给框架即可,框架已经自动帮你把这两种权限分开申请了,整个适配过程**零成本**。 * 但是需要注意的是:申请过程分为两个步骤,第一步是申请前台定位权限,第二步是申请后台定位权限,用户必须要先同意前台定位权限才能进入后台定位权限的申请。同意前台定位权限的方式有两种:勾选 `仅在使用该应用时允许` 或 `仅限这一次`,而到了后台定位权限申请中,用户必须要勾选 `始终允许`,只有这样后台定位权限才能申请通过。 * 还有如果你的应用只需要在前台使用定位功能, 而不需要在后台中使用定位功能,那么请不要连带申请 `Permission.ACCESS_BACKGROUND_LOCATION` 权限。 -![](picture/location_1.jpg) +![](picture/zh/location_1.jpg) -![](picture/location_2.jpg) +![](picture/zh/location_2.jpg) #### Android 11 存储权限适配 @@ -87,7 +87,7 @@ XXPermissions.with(MainActivity.this) .request(new OnPermissionCallback() { @Override - public void onGranted(List permissions, boolean all) { + public void onGranted(@NonNull List permissions, boolean all) { if (all) { toast("获取存储权限成功"); } @@ -95,7 +95,7 @@ XXPermissions.with(MainActivity.this) }); ``` -![](picture/7.jpg) +![](picture/zh/7.jpg) #### 什么情况下需要适配分区存储特性 @@ -141,12 +141,12 @@ XXPermissions.with(this) .request(new OnPermissionCallback() { @Override - public void onGranted(List permissions, boolean all) { + public void onGranted(@NonNull List permissions, boolean all) { ...... } @Override - public void onDenied(List permissions, boolean never) { + public void onDenied(@NonNull List permissions, boolean never) { ...... } }); @@ -165,7 +165,7 @@ public class XxxApplication extends Application { } ``` -#### 如何在回调中判断哪些权限被永久拒绝了 +#### 如何在回调中知道哪些权限被永久拒绝了 * 需求场景:假设同时申请日历权限和录音权限,结果都被用户拒绝了,但是这两组权限中有一组权限被永久拒绝了,如何判断某一组权限有没有被永久拒绝?这里给出代码示例: @@ -176,14 +176,14 @@ XXPermissions.with(this) .request(new OnPermissionCallback() { @Override - public void onGranted(List permissions, boolean all) { + public void onGranted(@NonNull List permissions, boolean all) { if (all) { toast("获取录音和日历权限成功"); } } @Override - public void onDenied(List permissions, boolean never) { + public void onDenied(@NonNull List permissions, boolean never) { if (never && permissions.contains(Permission.RECORD_AUDIO) && XXPermissions.isPermanentDenied(MainActivity.this, Permission.RECORD_AUDIO)) { toast("录音权限被永久拒绝了"); @@ -196,7 +196,7 @@ XXPermissions.with(this) * 因为 Android 6.0 以下的危险权限管理是手机厂商做的,那个时候谷歌还没有统一危险权限管理的方案,所以就算我们的应用没有适配也不会有任何问题,因为手机厂商对这块有自己的处理,但是有一点是肯定的,就算用户拒绝了授权,也不会导致应用崩溃,只会返回空白的通行证。 -* 如果 XXPermissions 做这块的适配也可以做到,通过反射系统服务 AppOpsManager 类中的字段即可,但是并不能保证权限判断的准确性,可能会存在一定的误差,其次是适配的成本太高,因为国内手机厂商太多,对这块的改动参差不齐。 +* 如果 **XXPermissions** 做这块的适配也可以做到,通过反射系统服务 AppOpsManager 类中的字段即可,但是并不能保证权限判断的准确性,可能会存在一定的误差,其次是适配的成本太高,因为国内手机厂商太多,对这块的改动参差不齐。 * 考虑到 Android 6.0 以下的设备占比很低,后续也会越来越少,会逐步退出历史的舞台,所以我的决定是不对这块做适配。 @@ -229,14 +229,14 @@ public class PermissionActivity extends AppCompatActivity implements OnPermissio } @Override - public void onGranted(List permissions, boolean all) { + public void onGranted(@NonNull List permissions, boolean all) { if (all) { toast("获取拍照权限成功"); } } @Override - public void onDenied(List permissions, boolean never) { + public void onDenied(@NonNull List permissions, boolean never) { if (never) { toast("被永久拒绝授权,请手动授予拍照权限"); // 如果是被永久拒绝就跳转到应用权限系统设置页面 @@ -249,9 +249,10 @@ public class PermissionActivity extends AppCompatActivity implements OnPermissio @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); - if (requestCode == XXPermissions.REQUEST_CODE) { - toast("检测到你刚刚从权限设置界面返回回来"); + if (requestCode != XXPermissions.REQUEST_CODE) { + return; } + toast("检测到你刚刚从权限设置界面返回回来"); } } ``` @@ -264,35 +265,37 @@ public class PermissionActivity extends AppCompatActivity implements OnPermissio > [【issue】正常申请存储权限时,永久拒绝,然后再应用设置页开启权限询问,系统权限申请弹窗未显示](https://github.com/getActivity/XXPermissions/issues/100) -* XXPermissions 9.0 及之前是有存在这一功能的,但是我在后续的版本上面将这个功能移除了,原因是有很多人跟我反馈这个功能其实存在很大的缺陷,例如在一些华为新机型上面可能跳转的页面不是应用的权限设置页,而是所有应用的权限管理列表界面。 +* **XXPermissions** 9.0 及之前是有存在这一功能的,但是我在后续的版本上面将这个功能移除了,原因是有很多人跟我反馈这个功能其实存在很大的缺陷,例如在一些华为新机型上面可能跳转的页面不是应用的权限设置页,而是所有应用的权限管理列表界面。 + +* 其实不止华为有问题,小米同样有问题,有很多人跟我反馈过同一个问题,**XXPermissions** 跳转到国产手机权限设置页,用户正常授予了权限之后返回仍然检测到权限仍然是拒绝的状态,这个问题反馈的次数很多,但是迟迟不能排查到原因,终于在最后一次得到答案了,[有人](https://github.com/getActivity/XXPermissions/issues/38)帮我排查到是 miui 优化开关的问题(小米手机 ---> 开发者选项 ---> 启用 miui 优化),那么问题来了,这个开关有什么作用?是如何影响到 **XXPermissions** 的? -* 其实不止华为有问题,小米同样有问题,有很多人跟我反馈过同一个问题,XXPermissions 跳转到国产手机权限设置页,用户正常授予了权限之后返回仍然检测到权限仍然是拒绝的状态,这个问题反馈的次数很多,但是迟迟不能排查到原因,终于在最后一次得到答案了,[有人](https://github.com/getActivity/XXPermissions/issues/38)帮我排查到是 miui 优化开关的问题(小米手机 ---> 开发者选项 ---> 启用 miui 优化),那么问题来了,这个开关有什么作用?是如何影响到 XXPermissions 的? +![](picture/zh/miui_1.jpg) -* 首先这个问题要从 XXPermissions 跳转到国产手机设置页的原理讲起,从谷歌提供的原生 API 我们最多只能跳转到应用详情页,并不能直接跳转到权限设置页,而需要用户在应用详情页再次点击才能进入权限设置页。如果从用户体验的角度上看待这个问题,肯定是直接跳转到权限设置页是最好的,但是这种方式是不受谷歌支持的,当然也有方法实现,网上都有一个通用的答案,就是直接捕获某个品牌手机的权限设置页 `Activity` 包名然后进行跳转。这种想法的起点是好的,但是存在许多问题,并不能保证每个品牌的所有机型都能适配到位,手机产商更改这个 `Activity` 的包名的次数和频率比较高,在最近发布的一些新的华为机型上面几乎已经全部失效,也就是 `startActivity` 的时候会报 `ActivityNotFoundException` 或 `SecurityException` 异常,当然这些异常是可以被捕捉到的,但是仅仅只能捕获到崩溃,一些非崩溃的行为我们并不能从中得知和处理,例如我刚刚讲过的华为和小米的问题,这些问题并不能导致崩溃,但是会导致功能出现异常。 +* 首先这个问题要从 **XXPermissions** 跳转到国产手机设置页的原理讲起,从谷歌提供的原生 API 我们最多只能跳转到应用详情页,并不能直接跳转到权限设置页,而需要用户在应用详情页再次点击才能进入权限设置页。如果从用户体验的角度上看待这个问题,肯定是直接跳转到权限设置页是最好的,但是这种方式是不受谷歌支持的,当然也有方法实现,网上都有一个通用的答案,就是直接捕获某个品牌手机的权限设置页 `Activity` 包名然后进行跳转。这种想法的起点是好的,但是存在许多问题,并不能保证每个品牌的所有机型都能适配到位,手机产商更改这个 `Activity` 的包名的次数和频率比较高,在最近发布的一些新的华为机型上面几乎已经全部失效,也就是 `startActivity` 的时候会报 `ActivityNotFoundException` 或 `SecurityException` 异常,当然这些异常是可以被捕捉到的,但是仅仅只能捕获到崩溃,一些非崩溃的行为我们并不能从中得知和处理,例如我刚刚讲过的华为和小米的问题,这些问题并不能导致崩溃,但是会导致功能出现异常。 * 而 miui 优化开关是小米工程师预留的切换 miui 和原生的功能开关,例如在这个开关开启的时候,在应用详情页点击权限管理会跳转到小米的权限设置页,如果这个开关是关闭状态(默认是开启状态),在应用详情页点击权限管理会跳转到谷歌原生的权限设置页,具体效果如图: -![](picture/miui_1.jpg) +![](picture/zh/miui_2.jpg) -![](picture/miui_2.jpg) +![](picture/zh/miui_3.jpg) * 最大的问题在于:这两个界面是不同的 Activity,一个是小米定制的权限设置页,第二个是谷歌原生的权限设置页,当 miui 优化开启的时候,在小米定制的权限设置页授予权限才能有效果,当这个 miui 优化关闭的时候,在谷歌原生的权限设置页授予权限才能有效果。而跳转到国产手机页永远只会跳转到小米定制的那个权限设置页,所以就会导致当 miui 优化关闭的时候,使用代码跳转到小米权限设置页授予了权限之后返回仍然显示失败的问题。 * 有人可能会说,解决这个问题的方式很简单,判断 miui 优化开关,如果是开启状态就跳转到小米定制的权限设置页,如果是关闭状态就跳转到谷歌原生的权限设置页,这样不就可以了?其实这个解决方案我也有尝试过,我曾委托联系到在小米工作的 miui 工程师,也有人帮我反馈这个问题给小米那边,最后得到答复都是一致的。 -![](picture/miui_3.jpg) - ![](picture/miui_4.jpg) +![](picture/miui_5.jpg) + * 另外值得一提的是 [Android 11 对软件包可见性进行了限制](https://developer.android.google.cn/about/versions/11/privacy/package-visibility),所以这种跳包名的方式在未来将会完全不可行。 -* 最终决定:这个功能的出发点是好的,但是我们没办法做好它,经过慎重考虑,决定将这个功能在 [XXPermissions 9.2 版本](https://github.com/getActivity/XXPermissions/releases/tag/9.2)及之后的版本进行移除。 +* 最终决定:这个功能的出发点是好的,但是我们没办法做好它,经过慎重考虑,决定将这个功能在 [**XXPermissions** 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` 类中这两个方法的实现就会明白了,这里不再赘述。 +* 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` 类中这两个方法的实现就会明白了,这里不再赘述。 #### 怎么处理权限请求成功但是返回空白通行证的问题 @@ -312,7 +315,8 @@ public final class PermissionInterceptor implements IPermissionInterceptor { private static final String SP_NAME_PERMISSION_REQUEST_TIME_RECORD = "permission_request_time_record"; @Override - public void requestPermissions(Activity activity, List allPermissions, OnPermissionCallback callback) { + public void launchPermissionRequest(@NonNull Activity activity, @NonNull List allPermissions, + @Nullable OnPermissionCallback callback) { SharedPreferences sharedPreferences = activity.getSharedPreferences(SP_NAME_PERMISSION_REQUEST_TIME_RECORD, Context.MODE_PRIVATE); String permissionKey = String.valueOf(allPermissions); long lastRequestPermissionTime = sharedPreferences.getLong(permissionKey, 0); @@ -332,7 +336,9 @@ public final class PermissionInterceptor implements IPermissionInterceptor { } @Override - public void grantedPermissions(Activity activity, List allPermissions, List grantedPermissions, boolean all, OnPermissionCallback callback) { + public void grantedPermissionRequest(@NonNull Activity activity, @NonNull List allPermissions, + @NonNull List grantedPermissions, boolean all, + @Nullable OnPermissionCallback callback) { if (callback == null) { return; } @@ -340,7 +346,9 @@ public final class PermissionInterceptor implements IPermissionInterceptor { } @Override - public void deniedPermissions(Activity activity, List allPermissions, List deniedPermissions, boolean never, OnPermissionCallback callback) { + public void deniedPermissionRequest(@NonNull Activity activity, @NonNull List allPermissions, + @NonNull List deniedPermissions, boolean never, + @Nullable OnPermissionCallback callback) { if (callback == null) { return; } diff --git a/README-en.md b/README-en.md new file mode 100644 index 0000000..eb0f70e --- /dev/null +++ b/README-en.md @@ -0,0 +1,394 @@ +## [中文文档](README.md) + +# Permission Request Framework + +![](logo.png) + +* project address: [Github](https://github.com/getActivity/XXPermissions) + +* You can scan the code to download the Demo for demonstration or test. If the scan code cannot be downloaded, [Click here to download directly](https://github.com/getActivity/XXPermissions/releases/download/16.2/XXPermissions.apk) + +![](picture/demo_code.png) + +![](picture/en/1.jpg) ![](picture/en/2.jpg) ![](picture/en/3.jpg) + +![](picture/en/4.jpg) ![](picture/en/5.jpg) ![](picture/en/6.jpg) + +![](picture/en/7.jpg) ![](picture/en/8.jpg) ![](picture/en/9.jpg) + +![](picture/en/10.jpg) ![](picture/en/11.jpg) ![](picture/en/12.jpg) + +![](picture/en/13.jpg) ![](picture/en/14.jpg) ![](picture/en/15.jpg) + +#### Integration steps + +* If your project Gradle configuration is in below `7.0`, needs to be in `build.gradle` file added + +```groovy +allprojects { + repositories { + // JitPack 远程仓库:https://jitpack.io + maven { url 'https://jitpack.io' } + } +} +``` + +* If your Gradle configuration is `7.0` or above, you need to `settings.gradle` file added + +```groovy +dependencyResolutionManagement { + repositories { + // JitPack 远程仓库: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 { + // Permission Request Framework:https://github.com/getActivity/XXPermissions + implementation 'com.github.getActivity:XXPermissions:16.5' +} +``` + +#### AndroidX compatible + +* If the project is based on **AndroidX** package, please in the item `gradle.properties` file added + +```text +# Indicates migration of third-party libraries to AndroidX +android.enableJetifier = true +``` + +* If the project is based on **Support** Packages do not need to be added to this configuration + +#### 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). + +#### One code to get permission request has never been easier + +* Java code example + +```java +XXPermissions.with(this) + // Request single permission + .permission(Permission.RECORD_AUDIO) + // Request multiple permission + .permission(Permission.Group.CALENDAR) + // Set permission request interceptor (local settings) + //.interceptor(new PermissionInterceptor()) + // Setting does not trigger error detection mechanism (local setting) + //.unchecked() + .request(new OnPermissionCallback() { + + @Override + public void onGranted(@NonNull List permissions, boolean all) { + if (!all) { + toast("Some permissions were obtained successfully, but some permissions were not granted normally"); + return; + } + toast("Acquired recording and calendar permissions successfully"); + } + + @Override + public void onDenied(@NonNull List permissions, boolean never) { + if (never) { + toast("Authorization denied permanently, please grant recording and calendar permissions manually"); + // If it is permanently denied, jump to the application permission system settings page + XXPermissions.startPermissionActivity(context, permissions); + } else { + toast("Failed to get recording and calendar permissions"); + } + } + }); +``` + +* Kotlin code example + +```kotlin +XXPermissions.with(this) + // Request single permission + .permission(Permission.RECORD_AUDIO) + // Request multiple permission + .permission(Permission.Group.CALENDAR) + // Set permission request interceptor (local settings) + //.interceptor(new PermissionInterceptor()) + // Setting does not trigger error detection mechanism (local setting) + //.unchecked() + .request(object : OnPermissionCallback { + + override fun onGranted(permissions: MutableList, all: Boolean) { + if (!all) { + toast("Some permissions were obtained successfully, but some permissions were not granted normally") + return + } + toast("Acquired recording and calendar permissions successfully") + } + + override fun onDenied(permissions: MutableList, never: Boolean) { + if (never) { + toast("Authorization denied permanently, please grant recording and calendar permissions manually") + // If it is permanently denied, jump to the application permission system settings page + XXPermissions.startPermissionActivity(context, permissions) + } else { + toast("Failed to get recording and calendar permissions") + } + } + }) +``` + +#### Introduction to other APIs of the framework + +```java +// Determine if one or more permissions are all granted +XXPermissions.isGranted(Context context, String... permissions); + +// Get permission not granted +XXPermissions.getDenied(Context context, String... permissions); + +// Determine whether a permission is a special permission +XXPermissions.isSpecial(String permission); + +// Determine if one or more permissions have been permanently denied (It must be called in the callback method of the permission application to have an effect) +XXPermissions.isPermanentDenied(Activity activity, String... permissions); + +// Start app details activity +XXPermissions.startPermissionActivity(Context context, String... permissions); +XXPermissions.startPermissionActivity(Activity activity, String... permissions); +XXPermissions.startPermissionActivity(Activity activity, String... permission, OnPermissionPageCallback callback); +XXPermissions.startPermissionActivity(Fragment fragment, String... permissions); +XXPermissions.startPermissionActivity(Fragment fragment, String... permissions, OnPermissionPageCallback callback); + +// Setting not to trigger error detection mechanism (global setting) +XXPermissions.setCheckMode(false); +// Set permission request interceptor (global setting) +XXPermissions.setInterceptor(new IPermissionInterceptor() {}); +``` + +#### About the permission monitoring callback parameter description + +* We all know that if the user grants all it will only call `onGranted` method, which will only be called if the user rejects all `onDenied` method. + +* But there is another situation. If multiple permissions are requested, these permissions are not all granted or all denied, but some of the authorizations are partially denied. How will the framework handle the callback? + +* The framework will call first `onDenied` method, then call `onGranted` method. of which we can pass `onGranted` in the method `all` parameters to determine whether all permissions are granted. + +* If you want to know whether a permission in the callback is granted or denied, you can call `List` in class `contains(Permission.XXX)` method to determine whether this permission is included in this collection. + +## [For other frequently asked questions, please click here](HelpDoc-en.md) + +#### 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) | +| :-----------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------: | +| corresponding version | 16.5 | 2.0.3 | 1.7.1 | 1.31.0 | 4.9.2 | 0.12 | 3.0.0 | +| number of issues | [![](https://img.shields.io/github/issues/getActivity/XXPermissions.svg)](https://github.com/getActivity/XXPermissions/issues) | [![](https://img.shields.io/github/issues/yanzhenjie/AndPermission.svg)](https://github.com/yanzhenjie/AndPermission/issues) | [![](https://img.shields.io/github/issues/guolindev/PermissionX.svg)](https://github.com/guolindev/PermissionX/issues) | [![](https://img.shields.io/github/issues/Blankj/AndroidUtilCode.svg)](https://github.com/Blankj/AndroidUtilCode/issues) | [![](https://img.shields.io/github/issues/permissions-dispatcher/PermissionsDispatcher.svg)](https://github.com/permissions-dispatcher/PermissionsDispatcher/issues) | [![](https://img.shields.io/github/issues/tbruyelle/RxPermissions.svg)](https://github.com/tbruyelle/RxPermissions/issues) | [![](https://img.shields.io/github/issues/googlesamples/easypermissions.svg)](https://github.com/googlesamples/easypermissions/issues) | +| framework volume | 59 KB | 127 KB | 97 KB | 500 KB | 99 KB | 28 KB | 48 KB | +| Framework Maintenance Status | **In maintenance** | stop maintenance | **In maintenance** | stop maintenance | stop maintenance | stop maintenance | stop maintenance | +| Alarm reminder permission | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| All file management permissions | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | +| Install package permissions | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | +| Picture-in-picture permissions | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Floating window permissions | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | +| System setting permissions | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | +| Notification bar permissions | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Notification bar monitoring permission | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Do not disturb permission | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Ignore battery optimization permission | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| View app usage permission | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| VPN permissions | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Android 13 Dangerous Permissions | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | +| Android 12 Dangerous Permissions | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | +| Android 11 Dangerous Permissions | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | +| Android 10 Dangerous Permissions | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | +| Android 9.0 Dangerous Permissions | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | +| Android 8.0 Dangerous Permissions | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | +| New permissions are automatically compatible with old devices | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Screen orientation rotation scene adaptation | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | +| Background application permission scenario adaptation | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Android 12 memory leak bug fix | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Error detection mechanism | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | + +#### New permissions are automatically compatible with old devices + +* 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 `MANAGE_EXTERNAL_STORAGE` as `READ_EXTERNAL_STORAGE` and `WRITE_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 scene adaptation introduction + +* Rotating the screen of the activity after the system permission application dialog box pops up will cause the permission application callback to fail, because the screen rotation will cause the Fragment in the framework to be destroyed and rebuilt, which will cause the callback object in it to be recycled directly, and eventually cause the callback to be abnormal. There are several solutions, one is to add in the manifest file`android:configChanges="orientation"`Attribute, so that the Activity and Fragment will not be destroyed and rebuilt when the screen is rotated. The second is to fix the direction of the Activity display directly in the manifest file, but the above two solutions must be handled by people who use the framework, which is obviously not flexible enough. The only one who can fix the problem is the one who created the problem, the problem of the framework should be solved by the frame, and **RxPermissions** The solution is to set the PermissionFragment object`fragment.setRetainInstance(true)`, so even if the screen is rotated, the Activity object will be destroyed and rebuilt, and the Fragment will not be destroyed and rebuilt, and the previous object will still be reused, but there is a problem, if the Activity is rewritten **onSaveInstanceState** The method will directly lead to the failure of this method, which is obviously only a temporary solution, but not the root cause. **XXPermissions** way would be more direct, in **PermissionFragment** When bound to an Activity, the current Activity's **Fixed screen orientation**, after the permission application ends, **reset the screen orientation**. + +* In all permission request frameworks, this problem occurs as long as Fragment is used to apply for permissions, and AndPermission actually applies for permissions by creating a new Activity, so this problem does not occur. PermissionsDispatcher uses APT to generate code. Apply for permission, so there is no such problem, and PermissionX directly draws on the solution of **XXPermissions**, please see [XXPermissions/issues/49](https://github.com/getActivity/XXPermissions/issues/49)、[PermissionX/issues/51](https://github.com/guolindev/PermissionX/issues/51). + +#### Background application permission scenario introduction + +* 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 [ RxPeremission/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 **RxPeremission** 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. + +#### Android 12 memory leak problem repair introduction + +* 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. + +#### Introduction to Error Detection Mechanism + +* 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: + + * 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 caller applies for permissions without passing in any permissions, the framework will throw an exception, or if the permissions passed in by the caller are not dangerous permissions or special permissions, the framework will also throw an exception, because some people will pass ordinary permissions When passed to the framework as a dangerous permission, the system will directly reject it. + + * 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 permission includes background location permission, then it cannot include permission not related to location, otherwise the framework will throw an exception, because `ACCESS_BACKGROUND_LOCATION` is applied together with other non-location permission location, on Android 11, there will be a situation where no application is directly rejected. + + * 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. + +#### Framework Highlights + +* Take the lead: the first permission request framework adapted to Android 13 + +* Concise and easy to use: using the method of chain call, only one line of code is needed to use + +* Impressive volume: The functions are the most complete among similar frames, but the frame volume is at the bottom + +* Comprehensive support: the first and only permission request framework that adapts to all Android versions + +* Overcoming technical difficulties: the first framework to solve system memory leaks in Android 12 for permission applications + +* Adapt to extreme situations: No matter how extreme and harsh the environment is to apply for permissions, the framework is still strong + +* Downward Compatibility: New permissions can be applied normally in the old system, and the framework will automatically adapt without the caller's adaptation + +* Automatic error detection: If an error occurs, the framework will actively throw an exception to the caller (only judged under Debug, and kill the bug in the cradle) + +#### Author's other open source projects + +* Android middle office: [AndroidProject](https://github.com/getActivity/AndroidProject)![](https://img.shields.io/github/stars/getActivity/AndroidProject.svg)![](https://img.shields.io/github/forks/getActivity/AndroidProject.svg) + +* Android middle office Kt version: [AndroidProject-Kotlin](https://github.com/getActivity/AndroidProject-Kotlin)![](https://img.shields.io/github/stars/getActivity/AndroidProject-Kotlin.svg)![](https://img.shields.io/github/forks/getActivity/AndroidProject-Kotlin.svg) + +* Toast framework: [ToastUtils](https://github.com/getActivity/ToastUtils)![](https://img.shields.io/github/stars/getActivity/ToastUtils.svg)![](https://img.shields.io/github/forks/getActivity/ToastUtils.svg) + +* Network framework: [EasyHttp](https://github.com/getActivity/EasyHttp)![](https://img.shields.io/github/stars/getActivity/EasyHttp.svg)![](https://img.shields.io/github/forks/getActivity/EasyHttp.svg) + +* Title bar framework: [TitleBar](https://github.com/getActivity/TitleBar)![](https://img.shields.io/github/stars/getActivity/TitleBar.svg)![](https://img.shields.io/github/forks/getActivity/TitleBar.svg) + +* Floating window framework: [XToast](https://github.com/getActivity/XToast)![](https://img.shields.io/github/stars/getActivity/XToast.svg)![](https://img.shields.io/github/forks/getActivity/XToast.svg) + +* Shape view framework: [ShapeView](https://github.com/getActivity/ShapeView)![](https://img.shields.io/github/stars/getActivity/ShapeView.svg)![](https://img.shields.io/github/forks/getActivity/ShapeView.svg) + +* Language switching framework: [Multi Languages](https://github.com/getActivity/MultiLanguages)![](https://img.shields.io/github/stars/getActivity/MultiLanguages.svg)![](https://img.shields.io/github/forks/getActivity/MultiLanguages.svg) + +* Gson parsing fault tolerance: [GsonFactory](https://github.com/getActivity/GsonFactory)![](https://img.shields.io/github/stars/getActivity/GsonFactory.svg)![](https://img.shields.io/github/forks/getActivity/GsonFactory.svg) + +* Logcat viewing framework: [Logcat](https://github.com/getActivity/Logcat)![](https://img.shields.io/github/stars/getActivity/Logcat.svg)![](https://img.shields.io/github/forks/getActivity/Logcat.svg) + +* Android version guide: [AndroidVersionAdapter](https://github.com/getActivity/AndroidVersionAdapter)![](https://img.shields.io/github/stars/getActivity/AndroidVersionAdapter.svg)![](https://img.shields.io/github/forks/getActivity/AndroidVersionAdapter.svg) + +* Android code standard: [AndroidCodeStandard](https://github.com/getActivity/AndroidCodeStandard)![](https://img.shields.io/github/stars/getActivity/AndroidCodeStandard.svg)![](https://img.shields.io/github/forks/getActivity/AndroidCodeStandard.svg) + +* Android open source leaderboard: [AndroidGithubBoss](https://github.com/getActivity/AndroidGithubBoss)![](https://img.shields.io/github/stars/getActivity/AndroidGithubBoss.svg)![](https://img.shields.io/github/forks/getActivity/AndroidGithubBoss.svg) + +* Studio boutique plugins: [StudioPlugins](https://github.com/getActivity/StudioPlugins)![](https://img.shields.io/github/stars/getActivity/StudioPlugins.svg)![](https://img.shields.io/github/forks/getActivity/StudioPlugins.svg) + +* Emoji collection: [emoji pa c shadow](https://github.com/getActivity/EmojiPackage)![](https://img.shields.io/github/stars/getActivity/EmojiPackage.svg)![](https://img.shields.io/github/forks/getActivity/EmojiPackage.svg) + +* China provinces json: [ProvinceJson](https://github.com/getActivity/ProvinceJson)![](https://img.shields.io/github/stars/getActivity/ProvinceJson.svg)![](https://img.shields.io/github/forks/getActivity/ProvinceJson.svg) + +## 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. +``` \ No newline at end of file diff --git a/README.md b/README.md index 372c9f6..bf9ab1d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +## [English Doc](README-en.md) + # 权限请求框架 ![](logo.png) @@ -6,25 +8,25 @@ * 博文地址:[一句代码搞定权限请求,从未如此简单](https://www.jianshu.com/p/c69ff8a445ed) -* 可以扫码下载 Demo 进行演示或者测试,如果扫码下载不了的,[点击此处可直接下载](https://github.com/getActivity/XXPermissions/releases/download/16.2/XXPermissions.apk) +* 可以扫码下载 Demo 进行演示或者测试,如果扫码下载不了的,[点击此处可直接下载](https://github.com/getActivity/XXPermissions/releases/download/16.5/XXPermissions.apk) ![](picture/demo_code.png) * 另外想对 Android 6.0 权限需要深入了解的,可以看这篇文章[Android 6.0 运行权限解析](https://www.jianshu.com/p/6a4dff744031) -![](picture/1.jpg) ![](picture/2.jpg) ![](picture/3.jpg) +![](picture/zh/1.jpg) ![](picture/zh/2.jpg) ![](picture/zh/3.jpg) -![](picture/4.jpg) ![](picture/5.jpg) ![](picture/6.jpg) +![](picture/zh/4.jpg) ![](picture/zh/5.jpg) ![](picture/zh/6.jpg) -![](picture/7.jpg) ![](picture/8.jpg) ![](picture/9.jpg) +![](picture/zh/7.jpg) ![](picture/zh/8.jpg) ![](picture/zh/9.jpg) -![](picture/10.jpg) ![](picture/11.jpg) ![](picture/12.jpg) +![](picture/zh/10.jpg) ![](picture/zh/11.jpg) ![](picture/zh/12.jpg) -![](picture/13.jpg) ![](picture/14.jpg) ![](picture/15.jpg) +![](picture/zh/13.jpg) ![](picture/zh/14.jpg) ![](picture/zh/15.jpg) #### 集成步骤 -* 如果你的项目 Gradle 配置是在 `7.0 以下`,需要在 `build.gradle` 文件中加入 +* 如果你的项目 Gradle 配置是在 `7.0` 以下,需要在 `build.gradle` 文件中加入 ```groovy allprojects { @@ -35,7 +37,7 @@ allprojects { } ``` -* 如果你的 Gradle 配置是 `7.0 及以上`,则需要在 `settings.gradle` 文件中加入 +* 如果你的 Gradle 配置是 `7.0` 及以上,则需要在 `settings.gradle` 文件中加入 ```groovy dependencyResolutionManagement { @@ -59,7 +61,7 @@ android { dependencies { // 权限请求框架:https://github.com/getActivity/XXPermissions - implementation 'com.github.getActivity:XXPermissions:16.2' + implementation 'com.github.getActivity:XXPermissions:16.5' } ``` @@ -83,7 +85,7 @@ android.enableJetifier = true - + @@ -114,7 +116,7 @@ XXPermissions.with(this) .request(new OnPermissionCallback() { @Override - public void onGranted(List permissions, boolean all) { + public void onGranted(@NonNull List permissions, boolean all) { if (!all) { toast("获取部分权限成功,但部分权限未正常授予"); return; @@ -123,7 +125,7 @@ XXPermissions.with(this) } @Override - public void onDenied(List permissions, boolean never) { + public void onDenied(@NonNull List permissions, boolean never) { if (never) { toast("被永久拒绝授权,请手动授予录音和日历权限"); // 如果是被永久拒绝就跳转到应用权限系统设置页面 @@ -181,7 +183,7 @@ XXPermissions.getDenied(Context context, String... permissions); // 判断某个权限是否为特殊权限 XXPermissions.isSpecial(String permission); -// 判断一个或多个权限是否被永久拒绝了 +// 判断一个或多个权限是否被永久拒绝了(一定要在权限申请的回调方法中调用才有效果) XXPermissions.isPermanentDenied(Activity activity, String... permissions); // 跳转到应用权限设置页 @@ -199,23 +201,23 @@ XXPermissions.setInterceptor(new IPermissionInterceptor() {}); #### 关于权限监听回调参数说明 -* 我们都知道,如果用户全部授予只会调用 **onGranted** 方法,如果用户全部拒绝只会调用 **onDenied** 方法。 +* 我们都知道,如果用户全部授予只会调用 `onGranted` 方法,如果用户全部拒绝只会调用 `onDenied` 方法。 * 但是还有一种情况,如果在请求多个权限的情况下,这些权限不是被全部授予或者全部拒绝了,而是部分授权部分拒绝这种情况,框架会如何处理回调呢? -* 框架会先调用 **onDenied** 方法,再调用 **onGranted** 方法。其中我们可以通过 **onGranted** 方法中的 **all** 参数来判断权限是否全部授予了。 +* 框架会先调用 `onDenied` 方法,再调用 `onGranted` 方法。其中我们可以通过 `onGranted` 方法中的 `all` 参数来判断权限是否全部授予了。 -* 如果想知道回调中的某个权限是否被授权或者拒绝,可以调用 **List** 类中的 **contains(Permission.XXX)** 方法来判断这个集合中是否包含了这个权限。 +* 如果想知道回调中的某个权限是否被授权或者拒绝,可以调用 `List` 类中的 `contains(Permission.XXX)` 方法来判断这个集合中是否包含了这个权限。` -## [其他常见疑问请点击此处查看](HelpDoc.md) +## [其他常见疑问请点击此处查看](HelpDoc-zh.md) #### 同类权限请求框架之间的对比 -| 适配细节 | [XXPermissions](https://github.com/getActivity/XXPermissions) | [AndPermission](https://github.com/yanzhenjie/AndPermission) | [PermissionX](https://github.com/guolindev/PermissionX) | [AndroidUtilCode](https://github.com/Blankj/AndroidUtilCode) | [PermissionsDispatcher](https://github.com/permissions-dispatcher/PermissionsDispatcher) | [RxPermissions](https://github.com/tbruyelle/RxPermissions) | [EasyPermissions](https://github.com/googlesamples/easypermissions) | +| 适配细节 | [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) | | :--------: | :------------: | :------------: | :------------: | :------------: | :------------: | :------------: | :------------: | -| 对应版本 | 16.2 | 2.0.3 | 1.6.4 | 1.31.0 | 4.9.2 | 0.12 | 3.0.0 | +| 对应版本 | 16.5 | 2.0.3 | 1.7.1 | 1.31.0 | 4.9.2 | 0.12 | 3.0.0 | | issues 数 | [![](https://img.shields.io/github/issues/getActivity/XXPermissions.svg)](https://github.com/getActivity/XXPermissions/issues) | [![](https://img.shields.io/github/issues/yanzhenjie/AndPermission.svg)](https://github.com/yanzhenjie/AndPermission/issues) | [![](https://img.shields.io/github/issues/guolindev/PermissionX.svg)](https://github.com/guolindev/PermissionX/issues) | [![](https://img.shields.io/github/issues/Blankj/AndroidUtilCode.svg)](https://github.com/Blankj/AndroidUtilCode/issues) | [![](https://img.shields.io/github/issues/permissions-dispatcher/PermissionsDispatcher.svg)](https://github.com/permissions-dispatcher/PermissionsDispatcher/issues) | [![](https://img.shields.io/github/issues/tbruyelle/RxPermissions.svg)](https://github.com/tbruyelle/RxPermissions/issues) | [![](https://img.shields.io/github/issues/googlesamples/easypermissions.svg)](https://github.com/googlesamples/easypermissions/issues) | -| 框架体积 | 52 KB | 127 KB | 90 KB | 500 KB | 99 KB | 28 KB | 48 KB | +| 框架体积 | 59 KB | 127 KB | 97 KB | 500 KB | 99 KB | 28 KB | 48 KB | | 框架维护状态 |**维护中**| 停止维护 |**维护中**| 停止维护 | 停止维护 | 停止维护 | 停止维护 | | 闹钟提醒权限 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | 所有文件管理权限 | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | @@ -229,7 +231,7 @@ XXPermissions.setInterceptor(new IPermissionInterceptor() {}); | 忽略电池优化权限 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | 查看应用使用情况权限 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | VPN 权限 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | -| Android 13 危险权限 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Android 13 危险权限 | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | | Android 12 危险权限 | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | | Android 11 危险权限 | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | | Android 10 危险权限 | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | @@ -253,13 +255,13 @@ XXPermissions.setInterceptor(new IPermissionInterceptor() {}); #### 屏幕旋转场景适配介绍 -* 当系统权限申请对话框弹出后对 Activity 进行屏幕旋转,会导致权限申请回调失效,因为屏幕旋转会导致框架中的 Fragment 销毁重建,这样会导致里面的回调对象直接被回收,最终导致回调不正常。解决方案有几种,一是在清单文件中添加 `android:configChanges="orientation"` 属性,这样屏幕旋转时不会导致 Activity 和 Fragment 销毁重建,二是直接在清单文件中固定 Activity 显示的方向,但是以上两种方案都要使用框架的人处理,这样显然是不够灵活的,解铃还须系铃人,框架的问题应当由框架来解决,而 **RxPermissions** 的解决方式是给 PermissionFragment 对象设置 `fragment.setRetainInstance(true)`,这样就算屏幕旋转了,Activity 对象会销毁重建,而 Fragment 也不会跟着销毁重建,还是复用着之前那个对象,但是存在一个问题,如果 Activity 重写了 **onSaveInstanceState** 方法会直接导致这种方式失效,这样做显然只是治标不治本,而 **XXPermissions** 的方式会更直接点,在 **PermissionFragment** 绑定到 Activity 上面时,把当前 Activity 的**屏幕方向固定住**,在权限申请结束后再把**屏幕方向还原回去**。 +* 当系统权限申请对话框弹出后对 Activity 进行屏幕旋转,会导致权限申请回调失效,因为屏幕旋转会导致框架中的 Fragment 销毁重建,这样会导致里面的回调对象直接被回收,最终导致回调不正常。解决方案有几种,一是在清单文件中添加 `android:configChanges="orientation"` 属性,这样屏幕旋转时不会导致 Activity 和 Fragment 销毁重建,二是直接在清单文件中固定 Activity 显示的方向,但是以上两种方案都要使用框架的人处理,这样显然是不够灵活的,解铃还须系铃人,框架的问题应当由框架来解决,而 **RxPermissions** 的解决方式是给 PermissionFragment 对象设置 `fragment.setRetainInstance(true)`,这样就算屏幕旋转了,Activity 对象会销毁重建,而 Fragment 也不会跟着销毁重建,还是复用着之前那个对象,但是存在一个问题,如果 Activity 重写了 `onSaveInstanceState` 方法会直接导致这种方式失效,这样做显然只是治标不治本,而 **XXPermissions** 的方式会更直接点,在 **PermissionFragment** 绑定到 Activity 上面时,把当前 Activity 的**屏幕方向固定住**,在权限申请结束后再把**屏幕方向还原回去**。 * 在所有的权限请求框架中,只要使用了 Fragment 申请权限都会出现这个问题,而 AndPermission 其实是通过创建新的 Activity 来申请权限,所以不会出现这个问题,PermissionsDispatcher 则是采用了 APT 生成代码的形式来申请权限,所以也没有这个问题,而 PermissionX 则是直接借鉴了 XXPermissions 的解决方案,详情请见 [XXPermissions/issues/49](https://github.com/getActivity/XXPermissions/issues/49) 、[PermissionX/issues/51](https://github.com/guolindev/PermissionX/issues/51)。 #### 后台申请权限场景介绍 -* 当我们做耗时操作之后申请权限(例如在闪屏页获取隐私协议再申请权限),在网络请求的过程中将 Activity 返回桌面去(退到后台),然后会导致权限请求是在后台状态中进行,在这个时机上就可能会导致权限申请不正常,表现为不会显示授权对话框,处理不当的还会导致崩溃,例如 [RxPeremission/issues/249](https://github.com/tbruyelle/RxPermissions/issues/249)。原因在于框架中的 PermissionFragment 在 **commit / commitNow** 到 Activity 的时候会做一个检测,如果 Activity 的状态是不可见时则会抛出异常,而 **RxPeremission** 正是使用了 **commitNow** 才会导致崩溃 ,使用 **commitAllowingStateLoss / commitNowAllowingStateLoss** 则可以避开这个检测,虽然这样可以避免崩溃,但是会出现另外一个问题,系统提供的 **requestPermissions** API 在 Activity 不可见时调用也不会弹出授权对话框,**XXPermissions** 的解决方式是将 **requestPermissions** 时机从 **create** 转移到了 **resume**,因为 Activity 和 Fragment 的生命周期方法是捆绑在一起的,如果 Activity 是不可见的,那么就算创建了 Fragment 也只会调用 **onCreate** 方法,而不会去调用它的 **onResume** 方法,最后当 Activity 从后台返回到前台时,不仅会触发 **Activity.onResume** 方法,同时也会触发 **PermissionFragment** 的 **onResume** 方法,在这个方法申请权限就可以保证最终 **requestPermissions** 调用的时机是在 Activity **处于可见状态的情况**下。 +* 当我们做耗时操作之后申请权限(例如在闪屏页获取隐私协议再申请权限),在网络请求的过程中将 Activity 返回桌面去(退到后台),然后会导致权限请求是在后台状态中进行,在这个时机上就可能会导致权限申请不正常,表现为不会显示授权对话框,处理不当的还会导致崩溃,例如 [RxPeremission/issues/249](https://github.com/tbruyelle/RxPermissions/issues/249)。原因在于框架中的 PermissionFragment 在 `commit` / `commitNow` 到 Activity 的时候会做一个检测,如果 Activity 的状态是不可见时则会抛出异常,而 **RxPeremission** 正是使用了 `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 内存泄漏问题修复介绍 @@ -285,11 +287,11 @@ XXPermissions.setInterceptor(new IPermissionInterceptor() {}); * AppOpsManager.startWatchingStarted -* 罪魁祸首其实是 **PermissionUsageHelper** 将 Context 对象作为字段持有着,并在构造函数中调用 `AppOpsManager.startWatchingStarted` 开启监听,这样 PermissionUsageHelper 对象就会被添加进 `AppOpsManager#mStartedWatchers` 集合中,这样导致在 Activity 主动调用 finish 的时候,并没有使用 stopWatchingStarted 来移除监听,导致 Activity 对象一直被 `AppOpsManager#mStartedWatchers` 集合中持有着,所以间接导致了 Activity 对象无法被系统回收。 +* 罪魁祸首其实是 `PermissionUsageHelper` 将 `Context` 对象作为字段持有着,并在构造函数中调用 `AppOpsManager.startWatchingStarted` 开启监听,这样 PermissionUsageHelper 对象就会被添加进 `AppOpsManager#mStartedWatchers` 集合中,这样导致在 Activity 主动调用 finish 的时候,并没有使用 `stopWatchingStarted` 来移除监听,导致 `Activity` 对象一直被 `AppOpsManager#mStartedWatchers` 集合中持有着,所以间接导致了 Activity 对象无法被系统回收。 -* 针对这个问题处理也很简单粗暴,就是将在外层传入的 **Context** 参数从 **Activity** 对象给替换成 **Application** 对象即可,有人可能会说了,Activity 里面才有 `shouldShowRequestPermissionRationale` 方法,而 Application 里面没有这个方法怎么办?看了一下这个方法的实现,其实那个方法最终会调用 `PackageManager.shouldShowRequestPermissionRationale` 方法(**隐藏 API,但是并不在黑名单中**)里面去,所以只要能获取到 **PackageManager** 对象即可,最后再使用反射去执行这个方法,这样就能避免出现内存泄漏。 +* 针对这个问题处理也很简单粗暴,就是将在外层传入的 `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 而言,这仍是一个历史遗留问题。 +* 幸好 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 设备出现的内存泄露问题。 @@ -327,14 +329,16 @@ XXPermissions.setInterceptor(new IPermissionInterceptor() {}); #### 框架亮点 -* 首款适配 Android 13 的权限请求框架 - -* 首款也是唯一一款适配所有 Android 版本的权限请求框架 +* 一马当先:首款适配 Android 13 的权限请求框架 * 简洁易用:采用链式调用的方式,使用只需一句代码 * 体积感人:功能在同类框架中是最全的,但是框架体积是垫底的 +* 支持全面:首款也是唯一一款适配所有 Android 版本的权限请求框架 + +* 技术难题攻坚:首款解决权限申请在 Android 12 出现系统内存泄漏的框架 + * 适配极端情况:无论在多么极端恶劣的环境下申请权限,框架依然坚挺 * 向下兼容属性:新权限在旧系统可以正常申请,框架会做自动适配,无需调用者适配 @@ -367,6 +371,8 @@ XXPermissions.setInterceptor(new IPermissionInterceptor() {}); * Android 代码规范:[AndroidCodeStandard](https://github.com/getActivity/AndroidCodeStandard) ![](https://img.shields.io/github/stars/getActivity/AndroidCodeStandard.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidCodeStandard.svg) +* Android 资源大汇总:[AndroidIndex](https://github.com/getActivity/AndroidIndex) ![](https://img.shields.io/github/stars/getActivity/AndroidIndex.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidIndex.svg) + * Android 开源排行榜:[AndroidGithubBoss](https://github.com/getActivity/AndroidGithubBoss) ![](https://img.shields.io/github/stars/getActivity/AndroidGithubBoss.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidGithubBoss.svg) * Studio 精品插件:[StudioPlugins](https://github.com/getActivity/StudioPlugins) ![](https://img.shields.io/github/stars/getActivity/StudioPlugins.svg) ![](https://img.shields.io/github/forks/getActivity/StudioPlugins.svg) diff --git a/app/build.gradle b/app/build.gradle index 4ae88e0..3d51686 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,10 +5,10 @@ android { defaultConfig { applicationId "com.hjq.permissions.demo" - minSdkVersion 14 + minSdkVersion 16 targetSdkVersion 33 - versionCode 1620 - versionName "16.2" + versionCode 1650 + versionName "16.5" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } @@ -61,8 +61,8 @@ dependencies { implementation 'com.android.support:appcompat-v7:28.0.0' // 吐司框架:https://github.com/getActivity/ToastUtils - implementation 'com.github.getActivity:ToastUtils:10.5' + implementation 'com.github.getActivity:ToastUtils:11.2' // 内存泄漏检测:https://github.com/square/leakcanary - debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' + debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2c19059..d46fb12 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -61,10 +61,14 @@ + + + @@ -86,7 +90,7 @@ + android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" > diff --git a/app/src/main/java/com/hjq/permissions/demo/MainActivity.java b/app/src/main/java/com/hjq/permissions/demo/MainActivity.java index 0ef7750..943538d 100644 --- a/app/src/main/java/com/hjq/permissions/demo/MainActivity.java +++ b/app/src/main/java/com/hjq/permissions/demo/MainActivity.java @@ -15,6 +15,7 @@ import android.os.Build; import android.os.Bundle; import android.provider.MediaStore; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.support.v7.app.AppCompatActivity; @@ -52,8 +53,8 @@ protected void onCreate(Bundle savedInstanceState) { findViewById(R.id.btn_main_request_activity_recognition).setOnClickListener(this); findViewById(R.id.btn_main_request_bluetooth).setOnClickListener(this); findViewById(R.id.btn_main_request_wifi).setOnClickListener(this); - findViewById(R.id.btn_main_request_media_location).setOnClickListener(this); - findViewById(R.id.btn_main_request_media_storage).setOnClickListener(this); + findViewById(R.id.btn_main_request_read_media_location).setOnClickListener(this); + findViewById(R.id.btn_main_request_media_read).setOnClickListener(this); findViewById(R.id.btn_main_request_manage_storage).setOnClickListener(this); findViewById(R.id.btn_main_request_install).setOnClickListener(this); findViewById(R.id.btn_main_request_window).setOnClickListener(this); @@ -81,11 +82,12 @@ public void onClick(View view) { .request(new OnPermissionCallback() { @Override - public void onGranted(List permissions, boolean all) { + public void onGranted(@NonNull List permissions, boolean all) { if (!all) { return; } - toast("获取" + PermissionNameConvert.getPermissionString(MainActivity.this, permissions) + "成功"); + toast(String.format(getString(R.string.demo_obtain_permission_success_hint), + PermissionNameConvert.getPermissionString(MainActivity.this, permissions))); } }); @@ -98,11 +100,12 @@ public void onGranted(List permissions, boolean all) { .request(new OnPermissionCallback() { @Override - public void onGranted(List permissions, boolean all) { + public void onGranted(@NonNull List permissions, boolean all) { if (!all) { return; } - toast("获取" + PermissionNameConvert.getPermissionString(MainActivity.this, permissions) + "成功"); + toast(String.format(getString(R.string.demo_obtain_permission_success_hint), + PermissionNameConvert.getPermissionString(MainActivity.this, permissions))); } }); @@ -117,11 +120,12 @@ public void onGranted(List permissions, boolean all) { .request(new OnPermissionCallback() { @Override - public void onGranted(List permissions, boolean all) { + public void onGranted(@NonNull List permissions, boolean all) { if (!all) { return; } - toast("获取" + PermissionNameConvert.getPermissionString(MainActivity.this, permissions) + "成功"); + toast(String.format(getString(R.string.demo_obtain_permission_success_hint), + PermissionNameConvert.getPermissionString(MainActivity.this, permissions))); } }); @@ -134,11 +138,12 @@ public void onGranted(List permissions, boolean all) { .request(new OnPermissionCallback() { @Override - public void onGranted(List permissions, boolean all) { + public void onGranted(@NonNull List permissions, boolean all) { if (!all) { return; } - toast("获取" + PermissionNameConvert.getPermissionString(MainActivity.this, permissions) + "成功"); + toast(String.format(getString(R.string.demo_obtain_permission_success_hint), + PermissionNameConvert.getPermissionString(MainActivity.this, permissions))); } }); @@ -150,11 +155,12 @@ public void onGranted(List permissions, boolean all) { .request(new OnPermissionCallback() { @Override - public void onGranted(List permissions, boolean all) { + public void onGranted(@NonNull List permissions, boolean all) { if (!all) { return; } - toast("获取" + PermissionNameConvert.getPermissionString(MainActivity.this, permissions) + "成功"); + toast(String.format(getString(R.string.demo_obtain_permission_success_hint), + PermissionNameConvert.getPermissionString(MainActivity.this, permissions))); addCountStepListener(); } }); @@ -164,7 +170,7 @@ public void onGranted(List permissions, boolean all) { long delayMillis = 0; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { delayMillis = 2000; - toast("当前版本不是 Android 12 及以上,旧版本的需要定位权限才能进行扫描蓝牙"); + toast(getString(R.string.demo_android_12_bluetooth_permission_hint)); } view.postDelayed(new Runnable() { @@ -179,11 +185,12 @@ public void run() { .request(new OnPermissionCallback() { @Override - public void onGranted(List permissions, boolean all) { + public void onGranted(@NonNull List permissions, boolean all) { if (!all) { return; } - toast("获取" + PermissionNameConvert.getPermissionString(MainActivity.this, permissions) + "成功"); + toast(String.format(getString(R.string.demo_obtain_permission_success_hint), + PermissionNameConvert.getPermissionString(MainActivity.this, permissions))); } }); } @@ -194,7 +201,7 @@ public void onGranted(List permissions, boolean all) { long delayMillis = 0; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { delayMillis = 2000; - toast("当前版本不是 Android 13 及以上,旧版本的需要定位权限才能进行扫描 WIFI"); + toast(getString(R.string.demo_android_13_wifi_permission_hint)); } view.postDelayed(new Runnable() { @@ -207,22 +214,23 @@ public void run() { .request(new OnPermissionCallback() { @Override - public void onGranted(List permissions, boolean all) { + public void onGranted(@NonNull List permissions, boolean all) { if (!all) { return; } - toast("获取" + PermissionNameConvert.getPermissionString(MainActivity.this, permissions) + "成功"); + toast(String.format(getString(R.string.demo_obtain_permission_success_hint), + PermissionNameConvert.getPermissionString(MainActivity.this, permissions))); } }); } }, delayMillis); - } else if (viewId == R.id.btn_main_request_media_location) { + } else if (viewId == R.id.btn_main_request_read_media_location) { long delayMillis = 0; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { delayMillis = 2000; - toast("当前版本不是 Android 10 及以上,旧版本的需要读取存储权限才能获取媒体位置权限"); + toast(getString(R.string.demo_android_10_read_media_location_permission_hint)); } view.postDelayed(new Runnable() { @@ -239,11 +247,12 @@ public void run() { .request(new OnPermissionCallback() { @Override - public void onGranted(List permissions, boolean all) { + public void onGranted(@NonNull List permissions, boolean all) { if (!all) { return; } - toast("获取" + PermissionNameConvert.getPermissionString(MainActivity.this, permissions) + "成功"); + toast(String.format(getString(R.string.demo_obtain_permission_success_hint), + PermissionNameConvert.getPermissionString(MainActivity.this, permissions))); new Thread(new Runnable() { @Override public void run() { @@ -255,12 +264,12 @@ public void run() { } }, delayMillis); - } else if (viewId == R.id.btn_main_request_media_storage) { + } else if (viewId == R.id.btn_main_request_media_read) { long delayMillis = 0; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { delayMillis = 2000; - toast("当前版本不是 Android 13 及以上,会自动变更为旧版的请求方式"); + toast(getString(R.string.demo_android_13_read_media_permission_hint)); } view.postDelayed(new Runnable() { @@ -278,11 +287,12 @@ public void run() { .request(new OnPermissionCallback() { @Override - public void onGranted(List permissions, boolean all) { + public void onGranted(@NonNull List permissions, boolean all) { if (!all) { return; } - toast("获取" + PermissionNameConvert.getPermissionString(MainActivity.this, permissions) + "成功"); + toast(String.format(getString(R.string.demo_obtain_permission_success_hint), + PermissionNameConvert.getPermissionString(MainActivity.this, permissions))); } }); } @@ -293,7 +303,7 @@ public void onGranted(List permissions, boolean all) { long delayMillis = 0; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { delayMillis = 2000; - toast("当前版本不是 Android 11 及以上,会自动变更为旧版的请求方式"); + toast(getString(R.string.demo_android_11_manage_storage_permission_hint)); } view.postDelayed(new Runnable() { @@ -309,11 +319,12 @@ public void run() { .request(new OnPermissionCallback() { @Override - public void onGranted(List permissions, boolean all) { + public void onGranted(@NonNull List permissions, boolean all) { if (!all) { return; } - toast("获取" + PermissionNameConvert.getPermissionString(MainActivity.this, permissions) + "成功"); + toast(String.format(getString(R.string.demo_obtain_permission_success_hint), + PermissionNameConvert.getPermissionString(MainActivity.this, permissions))); } }); } @@ -327,11 +338,12 @@ public void onGranted(List permissions, boolean all) { .request(new OnPermissionCallback() { @Override - public void onGranted(List permissions, boolean all) { + public void onGranted(@NonNull List permissions, boolean all) { if (!all) { return; } - toast("获取" + PermissionNameConvert.getPermissionString(MainActivity.this, permissions) + "成功"); + toast(String.format(getString(R.string.demo_obtain_permission_success_hint), + PermissionNameConvert.getPermissionString(MainActivity.this, permissions))); } }); @@ -343,11 +355,12 @@ public void onGranted(List permissions, boolean all) { .request(new OnPermissionCallback() { @Override - public void onGranted(List permissions, boolean all) { + public void onGranted(@NonNull List permissions, boolean all) { if (!all) { return; } - toast("获取" + PermissionNameConvert.getPermissionString(MainActivity.this, permissions) + "成功"); + toast(String.format(getString(R.string.demo_obtain_permission_success_hint), + PermissionNameConvert.getPermissionString(MainActivity.this, permissions))); } }); @@ -359,11 +372,12 @@ public void onGranted(List permissions, boolean all) { .request(new OnPermissionCallback() { @Override - public void onGranted(List permissions, boolean all) { + public void onGranted(@NonNull List permissions, boolean all) { if (!all) { return; } - toast("获取" + PermissionNameConvert.getPermissionString(MainActivity.this, permissions) + "成功"); + toast(String.format(getString(R.string.demo_obtain_permission_success_hint), + PermissionNameConvert.getPermissionString(MainActivity.this, permissions))); } }); @@ -375,11 +389,12 @@ public void onGranted(List permissions, boolean all) { .request(new OnPermissionCallback() { @Override - public void onGranted(List permissions, boolean all) { + public void onGranted(@NonNull List permissions, boolean all) { if (!all) { return; } - toast("获取" + PermissionNameConvert.getPermissionString(MainActivity.this, permissions) + "成功"); + toast(String.format(getString(R.string.demo_obtain_permission_success_hint), + PermissionNameConvert.getPermissionString(MainActivity.this, permissions))); } }); @@ -388,7 +403,7 @@ public void onGranted(List permissions, boolean all) { long delayMillis = 0; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { delayMillis = 2000; - toast("当前版本不是 Android 13 及以上,会自动变更为旧版的请求方式"); + toast(getString(R.string.demo_android_13_post_notification_permission_hint)); } view.postDelayed(new Runnable() { @@ -401,11 +416,12 @@ public void run() { .request(new OnPermissionCallback() { @Override - public void onGranted(List permissions, boolean all) { + public void onGranted(@NonNull List permissions, boolean all) { if (!all) { return; } - toast("获取" + PermissionNameConvert.getPermissionString(MainActivity.this, permissions) + "成功"); + toast(String.format(getString(R.string.demo_obtain_permission_success_hint), + PermissionNameConvert.getPermissionString(MainActivity.this, permissions))); } }); } @@ -419,11 +435,12 @@ public void onGranted(List permissions, boolean all) { .request(new OnPermissionCallback() { @Override - public void onGranted(List permissions, boolean all) { + public void onGranted(@NonNull List permissions, boolean all) { if (!all) { return; } - toast("获取" + PermissionNameConvert.getPermissionString(MainActivity.this, permissions) + "成功"); + toast(String.format(getString(R.string.demo_obtain_permission_success_hint), + PermissionNameConvert.getPermissionString(MainActivity.this, permissions))); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { toggleNotificationListenerService(); } @@ -438,11 +455,12 @@ public void onGranted(List permissions, boolean all) { .request(new OnPermissionCallback() { @Override - public void onGranted(List permissions, boolean all) { + public void onGranted(@NonNull List permissions, boolean all) { if (!all) { return; } - toast("获取" + PermissionNameConvert.getPermissionString(MainActivity.this, permissions) + "成功"); + toast(String.format(getString(R.string.demo_obtain_permission_success_hint), + PermissionNameConvert.getPermissionString(MainActivity.this, permissions))); } }); @@ -454,11 +472,12 @@ public void onGranted(List permissions, boolean all) { .request(new OnPermissionCallback() { @Override - public void onGranted(List permissions, boolean all) { + public void onGranted(@NonNull List permissions, boolean all) { if (!all) { return; } - toast("获取" + PermissionNameConvert.getPermissionString(MainActivity.this, permissions) + "成功"); + toast(String.format(getString(R.string.demo_obtain_permission_success_hint), + PermissionNameConvert.getPermissionString(MainActivity.this, permissions))); } }); @@ -470,11 +489,12 @@ public void onGranted(List permissions, boolean all) { .request(new OnPermissionCallback() { @Override - public void onGranted(List permissions, boolean all) { + public void onGranted(@NonNull List permissions, boolean all) { if (!all) { return; } - toast("获取" + PermissionNameConvert.getPermissionString(MainActivity.this, permissions) + "成功"); + toast(String.format(getString(R.string.demo_obtain_permission_success_hint), + PermissionNameConvert.getPermissionString(MainActivity.this, permissions))); } }); @@ -486,11 +506,12 @@ public void onGranted(List permissions, boolean all) { .request(new OnPermissionCallback() { @Override - public void onGranted(List permissions, boolean all) { + public void onGranted(@NonNull List permissions, boolean all) { if (!all) { return; } - toast("获取" + PermissionNameConvert.getPermissionString(MainActivity.this, permissions) + "成功"); + toast(String.format(getString(R.string.demo_obtain_permission_success_hint), + PermissionNameConvert.getPermissionString(MainActivity.this, permissions))); } }); @@ -502,11 +523,12 @@ public void onGranted(List permissions, boolean all) { .request(new OnPermissionCallback() { @Override - public void onGranted(List permissions, boolean all) { + public void onGranted(@NonNull List permissions, boolean all) { if (!all) { return; } - toast("获取" + PermissionNameConvert.getPermissionString(MainActivity.this, permissions) + "成功"); + toast(String.format(getString(R.string.demo_obtain_permission_success_hint), + PermissionNameConvert.getPermissionString(MainActivity.this, permissions))); } }); @@ -518,11 +540,12 @@ public void onGranted(List permissions, boolean all) { .request(new OnPermissionCallback() { @Override - public void onGranted(List permissions, boolean all) { + public void onGranted(@NonNull List permissions, boolean all) { if (!all) { return; } - toast("获取" + PermissionNameConvert.getPermissionString(MainActivity.this, permissions) + "成功"); + toast(String.format(getString(R.string.demo_obtain_permission_success_hint), + PermissionNameConvert.getPermissionString(MainActivity.this, permissions))); } }); @@ -535,9 +558,10 @@ public void onGranted(List permissions, boolean all) { @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); - if (requestCode == XXPermissions.REQUEST_CODE) { - toast("检测到你刚刚从权限设置界面返回回来"); + if (requestCode != XXPermissions.REQUEST_CODE) { + return; } + toast(getString(R.string.demo_return_activity_result_hint)); } public void toast(CharSequence text) { diff --git a/app/src/main/java/com/hjq/permissions/demo/NotificationMonitorService.java b/app/src/main/java/com/hjq/permissions/demo/NotificationMonitorService.java index 382b6e8..d1ffcd3 100644 --- a/app/src/main/java/com/hjq/permissions/demo/NotificationMonitorService.java +++ b/app/src/main/java/com/hjq/permissions/demo/NotificationMonitorService.java @@ -31,7 +31,7 @@ public void onNotificationPosted(StatusBarNotification sbn) { String title = extras.getString(Notification.EXTRA_TITLE); // 获取通知消息内容 Object msgText = extras.getCharSequence(Notification.EXTRA_TEXT); - ToastUtils.show("监听到新的通知消息,标题为:" + title + ",内容为:" + msgText); + ToastUtils.show(String.format(getString(R.string.demo_notification_listener_toast), title, msgText)); } } } diff --git a/app/src/main/java/com/hjq/permissions/demo/PermissionInterceptor.java b/app/src/main/java/com/hjq/permissions/demo/PermissionInterceptor.java index 611db98..68f99cd 100644 --- a/app/src/main/java/com/hjq/permissions/demo/PermissionInterceptor.java +++ b/app/src/main/java/com/hjq/permissions/demo/PermissionInterceptor.java @@ -2,16 +2,32 @@ import android.app.Activity; import android.content.DialogInterface; +import android.content.res.Configuration; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v7.app.AlertDialog; +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 com.hjq.permissions.IPermissionInterceptor; import com.hjq.permissions.OnPermissionCallback; import com.hjq.permissions.OnPermissionPageCallback; import com.hjq.permissions.Permission; +import com.hjq.permissions.PermissionFragment; import com.hjq.permissions.XXPermissions; import com.hjq.toast.ToastUtils; +import java.util.ArrayList; import java.util.List; /** @@ -22,40 +38,78 @@ */ public final class PermissionInterceptor implements IPermissionInterceptor { -// @Override -// public void requestPermissions(Activity activity, List allPermissions, OnPermissionCallback callback) { -// List deniedPermissions = XXPermissions.getDenied(activity, allPermissions); -// String permissionString = PermissionNameConvert.getPermissionString(activity, deniedPermissions); -// -// // 这里的 Dialog 只是示例,没有用 DialogFragment 来处理 Dialog 生命周期 -// new AlertDialog.Builder(activity) -// .setTitle(R.string.common_permission_hint) -// .setMessage(activity.getString(R.string.common_permission_message, permissionString)) -// .setPositiveButton(R.string.common_permission_granted, new DialogInterface.OnClickListener() { -// -// @Override -// public void onClick(DialogInterface dialog, int which) { -// dialog.dismiss(); -// PermissionFragment.beginRequest(activity, new ArrayList<>(allPermissions), PermissionInterceptor.this, callback); -// } -// }) -// .setNegativeButton(R.string.common_permission_denied, new DialogInterface.OnClickListener() { -// -// @Override -// public void onClick(DialogInterface dialog, int which) { -// dialog.dismiss(); -// if (callback != null) { -// callback.onDenied(deniedPermissions, false); -// } -// } -// }) -// .show(); -// } + public static final Handler HANDLER = new Handler(Looper.getMainLooper()); + /** 权限申请标记 */ + private boolean mRequestFlag; + + /** 权限申请说明 Popup */ + private PopupWindow mPermissionPopup; @Override - public void grantedPermissions(Activity activity, List allPermissions, List grantedPermissions, - boolean all, OnPermissionCallback callback) { + public void launchPermissionRequest(@NonNull Activity activity, @NonNull List allPermissions, @Nullable OnPermissionCallback callback) { + mRequestFlag = true; + List deniedPermissions = XXPermissions.getDenied(activity, allPermissions); + String message = activity.getString(R.string.common_permission_message, PermissionNameConvert.getPermissionString(activity, deniedPermissions)); + + ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView(); + int activityOrientation = activity.getResources().getConfiguration().orientation; + + boolean showPopupWindow = activityOrientation == Configuration.ORIENTATION_PORTRAIT; + for (String permission : allPermissions) { + if (!XXPermissions.isSpecial(permission)) { + continue; + } + if (XXPermissions.isGranted(activity, permission)) { + continue; + } + // 如果申请的权限带有特殊权限,并且还没有授予的话 + // 就不用 PopupWindow 对话框来显示,而是用 Dialog 来显示 + showPopupWindow = false; + break; + } + + if (showPopupWindow) { + + PermissionFragment.launch(activity, new ArrayList<>(allPermissions), this, callback); + // 延迟 300 毫秒是为了避免出现 PopupWindow 显示然后立马消失的情况 + // 因为框架没有办法在还没有申请权限的情况下,去判断权限是否永久拒绝了,必须要在发起权限申请之后 + // 所以只能通过延迟显示 PopupWindow 来做这件事,如果 300 毫秒内权限申请没有结束,证明本次申请的权限没有永久拒绝 + HANDLER.postDelayed(() -> { + if (!mRequestFlag) { + return; + } + if (activity.isFinishing() || + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && activity.isDestroyed())) { + return; + } + showPopupWindow(activity, decorView, message); + }, 300); + + } else { + // 注意:这里的 Dialog 只是示例,没有用 DialogFragment 来处理 Dialog 生命周期 + new AlertDialog.Builder(activity) + .setTitle(R.string.common_permission_description) + .setMessage(message) + .setPositiveButton(R.string.common_permission_granted, (dialog, which) -> { + dialog.dismiss(); + PermissionFragment.launch(activity, new ArrayList<>(allPermissions), + PermissionInterceptor.this, callback); + }) + .setNegativeButton(R.string.common_permission_denied, (dialog, which) -> { + dialog.dismiss(); + if (callback != null) { + callback.onDenied(deniedPermissions, false); + } + }) + .show(); + } + } + + @Override + public void grantedPermissionRequest(@NonNull Activity activity, @NonNull List allPermissions, + @NonNull List grantedPermissions, boolean all, + @Nullable OnPermissionCallback callback) { if (callback == null) { return; } @@ -63,8 +117,9 @@ public void grantedPermissions(Activity activity, List allPermissions, L } @Override - public void deniedPermissions(Activity activity, List allPermissions, List deniedPermissions, - boolean never, OnPermissionCallback callback) { + public void deniedPermissionRequest(@NonNull Activity activity, @NonNull List allPermissions, + @NonNull List deniedPermissions, boolean never, + @Nullable OnPermissionCallback callback) { if (callback != null) { callback.onDenied(deniedPermissions, never); } @@ -104,9 +159,42 @@ public void deniedPermissions(Activity activity, List allPermissions, Li ToastUtils.show(message); } - /** - * 显示授权对话框 - */ + @Override + public void finishPermissionRequest(@NonNull Activity activity, @NonNull List allPermissions, + boolean skipRequest, @Nullable OnPermissionCallback callback) { + mRequestFlag = false; + dismissPopupWindow(); + } + + private void showPopupWindow(Activity activity, ViewGroup decorView, String message) { + if (mPermissionPopup == null) { + View contentView = LayoutInflater.from(activity) + .inflate(R.layout.permission_description_popup, decorView, false); + mPermissionPopup = new PopupWindow(activity); + mPermissionPopup.setContentView(contentView); + mPermissionPopup.setWidth(WindowManager.LayoutParams.MATCH_PARENT); + mPermissionPopup.setHeight(WindowManager.LayoutParams.WRAP_CONTENT); + mPermissionPopup.setAnimationStyle(android.R.style.Animation_Dialog); + mPermissionPopup.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + mPermissionPopup.setTouchable(true); + mPermissionPopup.setOutsideTouchable(true); + } + TextView messageView = mPermissionPopup.getContentView().findViewById(R.id.tv_permission_description_message); + messageView.setText(message); + // 注意:这里的 PopupWindow 只是示例,没有监听 Activity onDestroy 来处理 PopupWindow 生命周期 + mPermissionPopup.showAtLocation(decorView, Gravity.TOP, 0, 0); + } + + private void dismissPopupWindow() { + if (mPermissionPopup == null) { + return; + } + if (!mPermissionPopup.isShowing()) { + return; + } + mPermissionPopup.dismiss(); + } + private void showPermissionSettingDialog(Activity activity, List allPermissions, List deniedPermissions, OnPermissionCallback callback) { if (activity == null || activity.isFinishing() || @@ -127,29 +215,25 @@ private void showPermissionSettingDialog(Activity activity, List allPerm new AlertDialog.Builder(activity) .setTitle(R.string.common_permission_alert) .setMessage(message) - .setPositiveButton(R.string.common_permission_goto_setting_page, new DialogInterface.OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - XXPermissions.startPermissionActivity(activity, - deniedPermissions, new OnPermissionPageCallback() { - - @Override - public void onGranted() { - if (callback == null) { - return; - } - callback.onGranted(allPermissions, true); - } - - @Override - public void onDenied() { - showPermissionSettingDialog(activity, allPermissions, - XXPermissions.getDenied(activity, allPermissions), callback); + .setPositiveButton(R.string.common_permission_goto_setting_page, (dialog, which) -> { + dialog.dismiss(); + XXPermissions.startPermissionActivity(activity, + deniedPermissions, new OnPermissionPageCallback() { + + @Override + public void onGranted() { + if (callback == null) { + return; } - }); - } + callback.onGranted(allPermissions, true); + } + + @Override + public void onDenied() { + showPermissionSettingDialog(activity, allPermissions, + XXPermissions.getDenied(activity, allPermissions), callback); + } + }); }) .show(); } diff --git a/app/src/main/java/com/hjq/permissions/demo/PermissionNameConvert.java b/app/src/main/java/com/hjq/permissions/demo/PermissionNameConvert.java index d8a3437..6b6f2fe 100644 --- a/app/src/main/java/com/hjq/permissions/demo/PermissionNameConvert.java +++ b/app/src/main/java/com/hjq/permissions/demo/PermissionNameConvert.java @@ -78,7 +78,7 @@ public static List permissionsToNames(Context context, List perm } case Permission.READ_MEDIA_AUDIO: { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - String hint = context.getString(R.string.common_permission_audio); + String hint = context.getString(R.string.common_permission_music_and_audio); if (!permissionNames.contains(hint)) { permissionNames.add(hint); } @@ -120,9 +120,9 @@ public static List permissionsToNames(Context context, List perm String hint; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !permissions.contains(Permission.BODY_SENSORS)) { - hint = context.getString(R.string.common_permission_sensors_background); + hint = context.getString(R.string.common_permission_body_sensors_background); } else { - hint = context.getString(R.string.common_permission_sensors); + hint = context.getString(R.string.common_permission_body_sensors); } if (!permissionNames.contains(hint)) { permissionNames.add(hint); @@ -133,7 +133,7 @@ public static List permissionsToNames(Context context, List perm case Permission.BLUETOOTH_CONNECT: case Permission.BLUETOOTH_ADVERTISE: { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - String hint = context.getString(R.string.common_permission_wireless_devices); + String hint = context.getString(R.string.common_permission_nearby_devices); if (!permissionNames.contains(hint)) { permissionNames.add(hint); } @@ -142,7 +142,7 @@ public static List permissionsToNames(Context context, List perm } case Permission.NEARBY_WIFI_DEVICES: { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - String hint = context.getString(R.string.common_permission_wireless_devices); + String hint = context.getString(R.string.common_permission_nearby_devices); if (!permissionNames.contains(hint)) { permissionNames.add(hint); } @@ -182,7 +182,7 @@ public static List permissionsToNames(Context context, List perm case Permission.WRITE_CALL_LOG: case Permission.PROCESS_OUTGOING_CALLS: { String hint = context.getString(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? - R.string.common_permission_call_log : + R.string.common_permission_call_logs : R.string.common_permission_phone); if (!permissionNames.contains(hint)) { permissionNames.add(hint); @@ -191,8 +191,8 @@ public static List permissionsToNames(Context context, List perm } case Permission.ACTIVITY_RECOGNITION: { String hint = context.getString(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ? - R.string.common_permission_activity_recognition_30 : - R.string.common_permission_activity_recognition_29); + R.string.common_permission_activity_recognition_api30 : + R.string.common_permission_activity_recognition_api29); if (!permissionNames.contains(hint)) { permissionNames.add(hint); } @@ -200,7 +200,7 @@ public static List permissionsToNames(Context context, List perm } case Permission.ACCESS_MEDIA_LOCATION: { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - String hint = context.getString(R.string.common_permission_media_location); + String hint = context.getString(R.string.common_permission_access_media_location); if (!permissionNames.contains(hint)) { permissionNames.add(hint); } @@ -220,7 +220,7 @@ public static List permissionsToNames(Context context, List perm } case Permission.MANAGE_EXTERNAL_STORAGE: { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - String hint = context.getString(R.string.common_permission_manage_storage); + String hint = context.getString(R.string.common_permission_all_file_access); if (!permissionNames.contains(hint)) { permissionNames.add(hint); } @@ -228,28 +228,28 @@ public static List permissionsToNames(Context context, List perm break; } case Permission.REQUEST_INSTALL_PACKAGES: { - String hint = context.getString(R.string.common_permission_install); + String hint = context.getString(R.string.common_permission_install_unknown_apps); if (!permissionNames.contains(hint)) { permissionNames.add(hint); } break; } case Permission.SYSTEM_ALERT_WINDOW: { - String hint = context.getString(R.string.common_permission_window); + String hint = context.getString(R.string.common_permission_display_over_other_apps); if (!permissionNames.contains(hint)) { permissionNames.add(hint); } break; } case Permission.WRITE_SETTINGS: { - String hint = context.getString(R.string.common_permission_setting); + String hint = context.getString(R.string.common_permission_modify_system_settings); if (!permissionNames.contains(hint)) { permissionNames.add(hint); } break; } case Permission.NOTIFICATION_SERVICE: { - String hint = context.getString(R.string.common_permission_notification); + String hint = context.getString(R.string.common_permission_allow_notifications); if (!permissionNames.contains(hint)) { permissionNames.add(hint); } @@ -265,35 +265,35 @@ public static List permissionsToNames(Context context, List perm break; } case Permission.BIND_NOTIFICATION_LISTENER_SERVICE: { - String hint = context.getString(R.string.common_permission_notification_listener); + String hint = context.getString(R.string.common_permission_allow_notifications_access); if (!permissionNames.contains(hint)) { permissionNames.add(hint); } break; } case Permission.PACKAGE_USAGE_STATS: { - String hint = context.getString(R.string.common_permission_task); + String hint = context.getString(R.string.common_permission_apps_with_usage_access); if (!permissionNames.contains(hint)) { permissionNames.add(hint); } break; } case Permission.SCHEDULE_EXACT_ALARM: { - String hint = context.getString(R.string.common_permission_alarm); + String hint = context.getString(R.string.common_permission_alarms_reminders); if (!permissionNames.contains(hint)) { permissionNames.add(hint); } break; } case Permission.ACCESS_NOTIFICATION_POLICY: { - String hint = context.getString(R.string.common_permission_not_disturb); + String hint = context.getString(R.string.common_permission_do_not_disturb_access); if (!permissionNames.contains(hint)) { permissionNames.add(hint); } break; } case Permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS: { - String hint = context.getString(R.string.common_permission_ignore_battery); + String hint = context.getString(R.string.common_permission_ignore_battery_optimize); if (!permissionNames.contains(hint)) { permissionNames.add(hint); } diff --git a/app/src/main/res/drawable/permission_description_popup_bg.xml b/app/src/main/res/drawable/permission_description_popup_bg.xml new file mode 100644 index 0000000..b33fb6a --- /dev/null +++ b/app/src/main/res/drawable/permission_description_popup_bg.xml @@ -0,0 +1,13 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 40e7454..6e9c333 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -22,7 +22,7 @@ android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginTop="10dp" - android:text="申请单个危险权限" /> + android:text="@string/demo_request_single_dangerous_permission" />