diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..9466725e --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: https://raw.githubusercontent.com/getActivity/Donate/master/picture/pay_ali.png diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..ec4bb386 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/issue_en_template_bug.yml b/.github/ISSUE_TEMPLATE/issue_en_template_bug.yml new file mode 100644 index 00000000..f1ecd2ca --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_en_template_bug.yml @@ -0,0 +1,173 @@ +name: Submit Bug +description: Please let me know the issues with the framework, and I will assist you in resolving them! +title: "[Bug]:" +labels: ["bug"] + +body: + - type: markdown + attributes: + value: | + ## [Warning: Please make sure to fill in the issue template accurately. If an issue is found to be filled incorrectly, it will be closed without further notice.](https://github.com/getActivity/IssueTemplateGuide) + - type: input + id: input_id_1 + attributes: + label: Framework Version [Required] + description: Please enter the version of the framework you are using. + validations: + required: true + - type: textarea + id: input_id_2 + attributes: + label: Issue Description [Required] + description: Please describe the issue you are facing. + validations: + required: true + - type: textarea + id: input_id_3 + attributes: + label: Steps to Reproduce [Required] + description: Please provide steps to reproduce the issue. + validations: + required: true + - type: dropdown + id: input_id_4 + attributes: + label: Is the Issue Reproducible? [Required] + multiple: false + options: + - "Not Selected" + - "Yes" + - "No" + validations: + required: true + - type: input + id: input_id_5 + attributes: + label: Project targetSdkVersion [Required] + validations: + required: true + - type: input + id: input_id_6 + attributes: + label: Device Information [Required] + description: Please provide the brand and model of the device where the issue occurred. + validations: + required: true + - type: input + id: input_id_7 + attributes: + label: Android Version [Required] + description: Please provide the Android version where the issue occurred. + validations: + required: true + - type: dropdown + id: input_id_8 + attributes: + label: Issue Source Channel [Required] + multiple: true + options: + - Encountered by myself + - Identified in Bugly + - User feedback + - Other channels + - type: input + id: input_id_9 + attributes: + label: Is it specific to certain device models? [Required] + description: Specify whether the issue is specific to certain devices (e.g., specific brand or Android version). + validations: + required: true + - type: dropdown + id: input_id_10 + attributes: + label: Does the latest version of the framework have this issue? [Required] + description: If you are using an older version, it is recommended to upgrade and check if the issue still persists. + multiple: false + options: + - "Not Selected" + - "Yes" + - "No" + validations: + required: true + - type: dropdown + id: input_id_11 + attributes: + label: Is the issue mentioned in the framework documentation? [Required] + description: The documentation provides answers to frequently asked questions. Please check if the information you are looking for is already provided. + multiple: false + options: + - "Not Selected" + - "Yes" + - "No" + validations: + required: true + - type: dropdown + id: input_id_12 + attributes: + label: Did you consult the framework documentation but couldn't find a solution? [Required] + description: If you have consulted the documentation but still couldn't find a solution, you can select "Yes." + multiple: false + options: + - "Not Selected" + - "Yes" + - "No" + validations: + required: true + - type: dropdown + id: input_id_13 + attributes: + label: Has a similar issue been reported in the issue list? [Required] + description: You can search the issue list for keywords related to your problem and refer to the solutions provided by others. + multiple: false + options: + - "Not Selected" + - "Yes" + - "No" + validations: + required: true + - type: dropdown + id: input_id_14 + attributes: + label: Have you searched the issue list but couldn't find a solution? [Required] + description: If you have searched the issue list and couldn't find a solution, you can select "Yes." + multiple: false + options: + - "Not Selected" + - "Yes" + - "No" + validations: + required: true + - type: dropdown + id: input_id_15 + attributes: + label: Can the issue be reproduced with a demo project? [Required] + description: Check if the issue can be reproduced in a minimal demo project to isolate potential issues in your own code. + multiple: false + options: + - "Not Selected" + - "Yes" + - "No" + validations: + required: true + - type: textarea + id: input_id_16 + attributes: + label: Provide Error Stack Trace + description: If there is an error, please provide the stack trace. Note, Do not include obfuscated code in the stack trace. + render: text + validations: + required: false + - type: textarea + id: input_id_17 + attributes: + label: Provide Screenshots or Videos + description: Provide screenshots or videos if necessary. This field is optional. + validations: + required: false + - type: textarea + id: input_id_18 + attributes: + label: Provide a Solution + description: If you have already found a solution, this field is optional. + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/issue_en_template_question.yml b/.github/ISSUE_TEMPLATE/issue_en_template_question.yml new file mode 100644 index 00000000..663218e3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_en_template_question.yml @@ -0,0 +1,65 @@ +name: Ask a Question +description: Ask your questions, and I will provide you with answers. +title: "[Question]:" +labels: ["question"] + +body: + - type: markdown + attributes: + value: | + ## [Warning: Please make sure to fill in the issue template accurately. If an issue is found to be filled incorrectly, it will be closed without further notice.](https://github.com/getActivity/IssueTemplateGuide) + - type: textarea + id: input_id_1 + attributes: + label: Question Description [Required] + description: Please describe your question (Note, If it is a framework bug, please do not raise it here, as it will not be accepted). + validations: + required: true + - type: dropdown + id: input_id_2 + attributes: + label: Is the issue mentioned in the framework documentation? [Required] + description: The documentation provides answers to frequently asked questions. Please check if the information you are looking for is already provided. + multiple: false + options: + - "Not Selected" + - "Yes" + - "No" + validations: + required: true + - type: dropdown + id: input_id_3 + attributes: + label: Did you consult the framework documentation but couldn't find a solution? [Required] + description: If you have consulted the documentation but still couldn't find a solution, you can select "Yes." + multiple: false + options: + - "Not Selected" + - "Yes" + - "No" + validations: + required: true + - type: dropdown + id: input_id_4 + attributes: + label: Has a similar issue been reported in the issue list? [Required] + description: You can search the issue list for keywords related to your problem and refer to the solutions provided by others. + multiple: false + options: + - "Not Selected" + - "Yes" + - "No" + validations: + required: true + - type: dropdown + id: input_id_5 + attributes: + label: Have you searched the issue list but couldn't find a solution? [Required] + description: If you have searched the issue list and couldn't find a solution, you can select "Yes." + multiple: false + options: + - "Not Selected" + - "Yes" + - "No" + validations: + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/issue_en_template_suggest.yml b/.github/ISSUE_TEMPLATE/issue_en_template_suggest.yml new file mode 100644 index 00000000..742e2b92 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_en_template_suggest.yml @@ -0,0 +1,60 @@ +name: Submit Suggestion +description: Please let me know the shortcomings of the framework, so that I can improve it! +title: "[Suggestion]:" +labels: ["help wanted"] + +body: + - type: markdown + attributes: + value: | + ## [Warning: Please make sure to fill in the issue template accurately. If an issue is found to be filled incorrectly, it will be closed without further notice.](https://github.com/getActivity/IssueTemplateGuide) + - type: textarea + id: input_id_1 + attributes: + label: What are the shortcomings you have noticed in the framework? [Required] + description: You can describe any aspects of the framework that you are not satisfied with. + validations: + required: true + - type: dropdown + id: input_id_2 + attributes: + label: Has a similar suggestion been made in the issue list? [Required] + description: If a similar suggestion has already been made, I will not address it again. + multiple: false + options: + - "Not Selected" + - "Yes" + - "No" + validations: + required: true + - type: dropdown + id: input_id_3 + attributes: + label: Is the suggestion mentioned in the framework documentation? [Required] + description: The documentation provides answers to frequently asked questions. Please check if the information you are looking for is already provided. + multiple: false + options: + - "Not Selected" + - "Yes" + - "No" + validations: + required: true + - type: dropdown + id: input_id_4 + attributes: + label: Did you consult the framework documentation but couldn't find a solution? [Required] + description: If you have consulted the documentation but still couldn't find a solution, you can select "Yes." + multiple: false + options: + - "Not Selected" + - "Yes" + - "No" + validations: + required: true + - type: textarea + id: input_id_5 + attributes: + label: How do you suggest improving it? [Optional] + description: You can provide your ideas or approaches for the author's reference. + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/issue_zh_template_bug.yml b/.github/ISSUE_TEMPLATE/issue_zh_template_bug.yml new file mode 100644 index 00000000..af107d80 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_zh_template_bug.yml @@ -0,0 +1,173 @@ +name: 提交 Bug +description: 请告诉我框架存在的问题,我会协助你解决此问题! +title: "[Bug]:" +labels: ["bug"] + +body: + - type: markdown + attributes: + value: | + ## [【警告:请务必按照 issue 模板填写,不要抱有侥幸心理,一旦发现 issue 没有按照模板认真填写,一律直接关闭】](https://github.com/getActivity/IssueTemplateGuide) + - type: input + id: input_id_1 + attributes: + label: 框架版本【必填】 + description: 请输入你使用的框架版本 + validations: + required: true + - type: textarea + id: input_id_2 + attributes: + label: 问题描述【必填】 + description: 请输入你对这个问题的描述 + validations: + required: true + - type: textarea + id: input_id_3 + attributes: + label: 复现步骤【必填】 + description: 请输入问题的复现步骤 + validations: + required: true + - type: dropdown + id: input_id_4 + attributes: + label: 是否必现【必填】 + multiple: false + options: + - "未选择" + - "是" + - "否" + validations: + required: true + - type: input + id: input_id_5 + attributes: + label: 项目 targetSdkVersion【必填】 + validations: + required: true + - type: input + id: input_id_6 + attributes: + label: 出现问题的手机信息【必填】 + description: 请填写出现问题的品牌和机型 + validations: + required: true + - type: input + id: input_id_7 + attributes: + label: 出现问题的安卓版本【必填】 + description: 请填写出现问题的 Android 版本 + validations: + required: true + - type: dropdown + id: input_id_8 + attributes: + label: 问题信息的来源渠道【必填】 + multiple: true + options: + - 自己遇到的 + - Bugly 看到的 + - 用户反馈 + - 其他渠道 + - type: input + id: input_id_9 + attributes: + label: 是部分机型还是所有机型都会出现【必答】 + description: 部分/全部(例如:某为,某 Android 版本会出现) + validations: + required: true + - type: dropdown + id: input_id_10 + attributes: + label: 框架最新的版本是否存在这个问题【必答】 + description: 如果用的是旧版本的话,建议升级看问题是否还存在 + multiple: false + options: + - "未选择" + - "是" + - "否" + validations: + required: true + - type: dropdown + id: input_id_11 + attributes: + label: 框架文档是否提及了该问题【必答】 + description: 文档会提供最常见的问题解答,可以先看看是否有自己想要的 + multiple: false + options: + - "未选择" + - "是" + - "否" + validations: + required: true + - type: dropdown + id: input_id_12 + attributes: + label: 是否已经查阅框架文档但还未能解决的【必答】 + description: 如果查阅了文档但还是没有解决的话,可以选择是 + multiple: false + options: + - "未选择" + - "是" + - "否" + validations: + required: true + - type: dropdown + id: input_id_13 + attributes: + label: issue 列表中是否有人曾提过类似的问题【必答】 + description: 可以在 issue 列表在搜索问题关键字,参考一下别人的解决方案 + multiple: false + options: + - "未选择" + - "是" + - "否" + validations: + required: true + - type: dropdown + id: input_id_14 + attributes: + label: 是否已经搜索过了 issue 列表但还未能解决的【必答】 + description: 如果搜索过了 issue 列表但是问题没有解决的话,可以选择是 + multiple: false + options: + - "未选择" + - "是" + - "否" + validations: + required: true + - type: dropdown + id: input_id_15 + attributes: + label: 是否可以通过 Demo 来复现该问题【必答】 + description: 排查一下是不是自己的项目代码写得有问题导致的 + multiple: false + options: + - "未选择" + - "是" + - "否" + validations: + required: true + - type: textarea + id: input_id_16 + attributes: + label: 提供报错堆栈 + description: 如果有报错的话必填,注意不要拿被混淆过的代码堆栈上来 + render: text + validations: + required: false + - type: textarea + id: input_id_17 + attributes: + label: 提供截图或视频 + description: 根据需要提供,此项不强制 + validations: + required: false + - type: textarea + id: input_id_18 + attributes: + label: 提供解决方案 + description: 如果已经解决了的话,此项不强制 + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/issue_zh_template_question.yml b/.github/ISSUE_TEMPLATE/issue_zh_template_question.yml new file mode 100644 index 00000000..ba56beb7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_zh_template_question.yml @@ -0,0 +1,65 @@ +name: 提出疑问 +description: 提出你的困惑,我会给你解答 +title: "[疑惑]:" +labels: ["question"] + +body: + - type: markdown + attributes: + value: | + ## [【警告:请务必按照 issue 模板填写,不要抱有侥幸心理,一旦发现 issue 没有按照模板认真填写,一律直接关闭】](https://github.com/getActivity/IssueTemplateGuide) + - type: textarea + id: input_id_1 + attributes: + label: 问题描述【必填】 + description: 请描述一下你的问题(注意:如果确定是框架 bug 请不要在这里提,否则一概不受理) + validations: + required: true + - type: dropdown + id: input_id_2 + attributes: + label: 框架文档是否提及了该问题【必答】 + description: 文档会提供最常见的问题解答,可以先看看是否有自己想要的 + multiple: false + options: + - "未选择" + - "是" + - "否" + validations: + required: true + - type: dropdown + id: input_id_3 + attributes: + label: 是否已经查阅框架文档但还未能解决的【必答】 + description: 如果查阅了文档但还是没有解决的话,可以选择是 + multiple: false + options: + - "未选择" + - "是" + - "否" + validations: + required: true + - type: dropdown + id: input_id_4 + attributes: + label: issue 列表中是否有人曾提过类似的问题【必答】 + description: 可以在 issue 列表在搜索问题关键字,参考一下别人的解决方案 + multiple: false + options: + - "未选择" + - "是" + - "否" + validations: + required: true + - type: dropdown + id: input_id_5 + attributes: + label: 是否已经搜索过了 issue 列表但还未能解决的【必答】 + description: 如果搜索过了 issue 列表但是问题没有解决的话,可以选择是 + multiple: false + options: + - "未选择" + - "是" + - "否" + validations: + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/issue_zh_template_suggest.yml b/.github/ISSUE_TEMPLATE/issue_zh_template_suggest.yml new file mode 100644 index 00000000..e7ef5198 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_zh_template_suggest.yml @@ -0,0 +1,60 @@ +name: 提交建议 +description: 请告诉我框架的不足之处,让我做得更好! +title: "[建议]:" +labels: ["help wanted"] + +body: + - type: markdown + attributes: + value: | + ## [【警告:请务必按照 issue 模板填写,不要抱有侥幸心理,一旦发现 issue 没有按照模板认真填写,一律直接关闭】](https://github.com/getActivity/IssueTemplateGuide) + - type: textarea + id: input_id_1 + attributes: + label: 你觉得框架有什么不足之处?【必答】 + description: 你可以描述框架有什么令你不满意的地方 + validations: + required: true + - type: dropdown + id: input_id_2 + attributes: + label: issue 是否有人曾提过类似的建议?【必答】 + description: 一旦出现重复提问我将不会再次解答 + multiple: false + options: + - "未选择" + - "是" + - "否" + validations: + required: true + - type: dropdown + id: input_id_3 + attributes: + label: 框架文档是否提及了该问题【必答】 + description: 文档会提供最常见的问题解答,可以先看看是否有自己想要的 + multiple: false + options: + - "未选择" + - "是" + - "否" + validations: + required: true + - type: dropdown + id: input_id_4 + attributes: + label: 是否已经查阅框架文档但还未能解决的【必答】 + description: 如果查阅了文档但还是没有解决的话,可以选择是 + multiple: false + options: + - "未选择" + - "是" + - "否" + validations: + required: true + - type: textarea + id: input_id_5 + attributes: + label: 你觉得该怎么去完善会比较好?【非必答】 + description: 你可以提供一下自己的想法或者做法供作者参考 + validations: + required: false \ No newline at end of file diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 00000000..635d6950 --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,15 @@ +name: Android CI + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 diff --git a/.gitignore b/.gitignore index d342ae5d..371c80dd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .gradle .idea .cxx +.kotlin .externalNativeBuild build captures diff --git a/AndroidProject.apk b/AndroidProject.apk deleted file mode 100644 index 5f163a78..00000000 Binary files a/AndroidProject.apk and /dev/null differ diff --git a/HelpDoc.md b/HelpDoc.md index f6d9b40d..5c7c432e 100644 --- a/HelpDoc.md +++ b/HelpDoc.md @@ -26,9 +26,9 @@ * [为什么不拆成多个框架来做这件事](#为什么不拆成多个框架来做这件事) -* [为什么最低兼容到 Android 5](#为什么最低兼容到-android-5) +* [为什么最低兼容到 Android 5.0](#为什么最低兼容到-android-50) -* [为什么不加入扫描二维码功能](#为什么不加入扫描二维码功能) +* [为什么不加入 XXX 功能](#为什么不加入-xxx-功能) * [为什么不加入 EventBus](#为什么不加入-eventbus) @@ -50,15 +50,17 @@ * [为什么不用谷歌 ActivityResultContracts](#为什么不用谷歌-activityresultcontracts) +* [为什么新版移除了权限申请的 AOP 注解](#为什么新版移除了权限申请的-aop-注解) + * [轮子哥你怎么看待层出不穷的新技术](#轮子哥你怎么看待层出不穷的新技术) #### 为什么没有用 MVP -![](picture/help/mvp1.jpg) +![](picture/help/mvp_issue_1.jpg) -![](picture/help/mvp2.jpg) +![](picture/help/mvp_issue_2.jpg) -![](picture/help/mvp3.jpg) +![](picture/help/mvvm_issue.jpg) * AndroidProject 舍弃 MVP 的最大一个原因,需要写各种类,各种回调,如果这个页面比较简单的话,使用 MVP 会让原本简单的代码变复杂,导致后续开发和维护成本是非常高,前期付出的代价和后期的维护不成正比关系,当然这种说法只适用于各种中小型项目,大型的项目我还没有经历过,不过我觉得,无论是 MVC、MVP、MVVM,它们出现的目的是为了解决代码多并且乱的问题,作用就是给代码做分类,但是可以跟大家分享我的心得,我并不看好 MVP,因为它让我开发和维护都很痛苦,所以我就直接将它从 AndroidProject 移除,目的也很简单,不推荐大家使用,因为 MVP 不适合大多数项目的开发和维护。我更推荐大家直接将代码写在 Activity,但是有一个前提条件需要大家遵守,大家要做好代码封装和重复代码的抽取,尽量让 Activity 成为只有业务代码的类,这样一个项目里面的大多数 Activity 代码量都能很好控制在 1000 行代码以内。但是这种看似简单的操作,但是实际要做到是一件不容易的事情,这里面不仅要解决代码带来的问题,还要解决带来的各种人性矛盾,困难重重,这种想法经过很长一段时间的思考,虽然写法在开发和维护中效率是非常高的,但是不被大多数人认可,大家更愿意相信 MVC、MVP、MVVM,而很少有人理解这三种模式的本质是什么,就是为了给代码做分类,但这三种模式都不够灵活,很生硬,像是一套套规则,而这样的代码分类,只会让大多数人的开发越来越头疼。 @@ -100,7 +102,7 @@ binding.tv_data_name.setText("字符串"); * DataBinding 最大的优势在于,因为它可以在 xml 直接给 View 赋值,但它的优点正是它最致命的缺点,当业务逻辑简单时,会显得格外美好,但是一旦判断条件复杂起来,由于 xml 属性不能换行的特性,会导致无法在 xml 直接赋值又或者很长的一段代码堆在布局中,间接导致 CodeReview 时异常艰难,更别说在原有的基础上继续更新迭代,这对每一个开发者来讲无疑是一个巨大的灾难。 -* 还有一个是 DataBinding 的学习成本比较高,其次成本也挺高,使用前需要做很多封装,另外每次使用时都需要添加 `layout` 和 `data` 节点,然后在 Java 代码中初始化 DataBinding 对象,无法在基类中封装处理,每次都要写 `binding.xxx` 才能操作控件,和 ViewBinding 的问题差不多。 +* 还有一个是 DataBinding 的学习成本比较高,其次使用成本也挺高,使用前需要做很多封装,另外每次使用时都需要添加 `layout` 和 `data` 节点,然后在 Java 代码中初始化 DataBinding 对象,无法在基类中封装处理,每次都要写 `binding.xxx` 才能操作控件,和 ViewBinding 的问题差不多。 ```java ActivityXxxxBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_xxxx); @@ -118,7 +120,7 @@ ActivityXxxxBinding binding = DataBindingUtil.setContentView(this, R.layout.acti * AndroidProject 其实有加入过这个功能,但是在 [v9.0 版本](https://github.com/getActivity/AndroidProject/releases/tag/9.0) 就移除了,原因是第三方侧滑框架 [BGASwipeBackLayout](https://github.com/bingoogolapple/BGASwipeBackLayout-Android) 在 Android 9.0 上面会[闪屏](https://github.com/bingoogolapple/BGASwipeBackLayout-Android/issues/173),并且还是 **100% 必现**,**用户体验极差**,我也跟作者反馈过这个问题,但结果不了了之,所以不得不移除。但是到了 [v10.0 版本](https://github.com/getActivity/AndroidProject/releases/tag/10.0),我又加上界面侧滑功能了,不过这次我换成了 [SmartSwipe](https://github.com/luckybilly/SmartSwipe) 来做,但是我又再一次失望了,这个框架在 Android 11 上面,如果 Activity 上有 WindowManager 正在显示,然后使用界面侧滑,那么会出现闪屏的情况,具体效果如下图: -![](picture/help/Swipe.jpg) +![](picture/help/swipe_issue.jpg) * 就这个情况我也联系过作者,并详细阐述了产生的原因和具体的复现步骤,但是我等了三天连个回复都没有,实属有些让我心寒,在等待的期间我看到 Github 的 issue 已经基本没有回复了,并且最后一次提交是在 13 个月前了,种种迹象都已经表明,所以经过慎重考虑,最终决定在 [v12.1 版本](https://github.com/getActivity/AndroidProject/releases/tag/12.1) 移除界面侧滑功能。 @@ -148,7 +150,7 @@ xxxhdpi:1dp=4px * 另外谈谈我的经历,我自己之前的公司主要是做平板上面的应用,所以也用过 [AutoSize 框架](https://github.com/JessYanCoding/AndroidAutoSize),一年多的使用体验下来,发现这个框架 Bug 还算是比较多的,例如框架会偶尔出现机型适配失效,重写了 **getResources** 方法情况之后出现的情况少了一些,但是仍然还有一些奇奇怪怪的问题,这里就不一一举例了,最后总结下来就是框架还不够成熟,但是框架的思想还是很不错的。我后面换了一家公司,也是做平板应用,项目用的是用[通配符的适配方案](https://github.com/wildma/ScreenAdaptation),跟 AutoSize 相对比,没有了那些奇奇怪怪的问题,但是代码的侵入性比较高。这两种方案各有优缺点,大家看着选择。 -![](picture/help/vote2.jpg) +![](picture/help/vote_2.jpg) * 在这块我也发起过群投票,相比谷歌的适配方案,大多数人更认同那种百分比适配方案,秉承着少数服从多数的理念,我在 AndroidProject [v13.0 版本](https://github.com/getActivity/AndroidProject/releases/tag/13.0) 加入了通配符的适配方案。虽然有一部分人不认同,但是我想跟这些人说的是:我的每一个决定都是十分谨慎的,因为这其中涉及到许多人的利益,AndroidProject 虽然是我创造的,但是它早就不是我一个人的了,而是大家的,每个重要的决定我都会考虑再三才会去做,在做决定的时候我会把大众的利益放在第一位,把自己的利益放在最后一位,所以大家唯一能做的是,相信我的选择。或许你可能觉得这样不太对,也随时欢迎你提出不同的意见给到我,我不认为自己做的决定一定都是对的,但是我会一直朝着对的方向前进。 @@ -212,7 +214,7 @@ xxxhdpi:1dp=4px #### 为什么不拆成多个框架来做这件事 -* AndroidProject 其实一直有这样做,把很多组件都拆成了独立的框架,例如:权限请求框架 [XXPermissions](https://github.com/getActivity/XXPermissions),网络请求框架 [EasyHttp](https://github.com/getActivity/EasyHttp)、吐司框架 [ToastUtils](https://github.com/getActivity/ToastUtils) 等等,我都是将它抽离在 AndroidProject 之外,作为一个单独的开源项目进行开发和维护,至于说为什么还有一些代码没有抽取出来,主要原因有几点: +* AndroidProject 其实一直有这样做,把很多组件都拆成了独立的框架,例如:权限请求框架 [XXPermissions](https://github.com/getActivity/XXPermissions),网络请求框架 [EasyHttp](https://github.com/getActivity/EasyHttp)、吐司框架 [Toaster](https://github.com/getActivity/Toaster) 等等,我都是将它抽离在 AndroidProject 之外,作为一个单独的开源项目进行开发和维护,至于说为什么还有一些代码没有抽取出来,主要原因有几点: 1. 和业务的耦合性高,例如 Dialog 组件引用了很多项目的基类,例如 **BaseDialog**、**BaseAdapter** 等 @@ -220,11 +222,11 @@ xxxhdpi:1dp=4px * 基于以上几点,我并不认为所有的东西都适合抽取成框架给大家用,有些东西还是跟随 **AndroidProject** 一起更新比较好。当然像权限请求这种东西,我个人觉得抽成框架是比较合适的,因为它和业务的关联性不大,更重要的是,如果某一天你觉得 **XXPermissions** 做得不够好,你随时可以在 **AndroidProject** 替换掉它,并且整个过程不需要太大的改动。 -#### 为什么最低兼容到 Android 5 +#### 为什么最低兼容到 Android 5.0 * AndroidProject 从 [v11.0 版本](https://github.com/getActivity/AndroidProject/releases/tag/11.0),已经将 minSdkVersion 从 19 升级到 21,原因也很简单,我不推荐大家兼容 Android 4.4 版本,因为这个版本兼容性的问题太多,对 **dex 分包**、**矢量图**的支持不是特别好,这个我们开发者处理不了,除此之外还有很多 API 要做高低版本兼容,这个我们开发者能做,但是我觉得没什么必要性,因为这个版本的机型会越来越少,会逐步退出历史舞台,而 AndroidProject 一旦投入到项目中使用,minSdkVersion 基本不会有变动,所以我的想法是,不如在一开始就不兼容这个版本,免得后面给大家带来一些不必要的麻烦,Android 4.4 有些问题是**真硬伤**,这是一个非常**令人头疼**的问题。 -#### 为什么不加入扫描二维码功能 +#### 为什么不加入 XXX 功能 * AndroidProject 的定位是做一个技术架构,不是什么都做的 Demo 工程,如果只是解决大家的需求问题,那样在我看来意义其实并不大,当然实现需求固然很重要,但并不是所有的技术点在不同项目都会用到,AndroidProject 只是在做架构的同时顺道把模板做了,如果说架构是理论,那么模板就是实践,代码写得再好,如果不实践,那么也只是纸上谈兵,又或者中看不中用。 @@ -262,7 +264,7 @@ xxxhdpi:1dp=4px * 常用的图片加载框架无非就两种,最常用的是 Glide,其次是 Fresco。我曾做过一个技术调研: -![](picture/help/vote1.jpg) +![](picture/help/vote_1.jpg) * 无疑 Glide 已成大家最喜爱的图片加载框架,当然也有人使用 Fresco,但是占比极少。 @@ -292,7 +294,7 @@ xxxhdpi:1dp=4px #### 假设 AndroidProject 更新了该怎么升级它 -* 原因和解释:首先纠正一点,AndroidProject 严格意义上来说,不是框架一种,而属于架构一种,架构升级本身就是一件大事,并且存在很多未知的风险点,我不推荐已使用 AndroidProject 开发的项目去做升级,因为开发和测试的成本极其高,间接能为业务带来价值其实很低,很多时候我知道大家很喜欢 AndroidProject 的代码,想用到公司项目中去,但是我仍然不推荐你那么做,假设这是你的个人项目可以那么做,但是公司项目最好不要,因为公司和你都是要靠这个项目赚钱,谁也不希望项目出现问题,如果是公司要开发人员重构公司项目,也可以考虑那么做,毕竟这个时候的风险公司已经承担了大部分了,接下来的话只需要服从公司安排即可。 +* 原因和解释:首先纠正一点,AndroidProject 严格意义上来说,不是框架一种,而属于架构一种,架构升级本身就是一件大事,并且存在很多未知的风险点,我不推荐已使用 AndroidProject 开发的项目去做升级,因为开发和测试的成本极其高,间接能为业务带来价值其实很低,很多时候我知道大家很喜欢 AndroidProject 的代码,想用最新的代码到公司项目中去,但是我仍然不推荐你那么做,假设这是你的个人项目可以那么做,但是公司项目最好不要,因为公司和你都是要靠这个项目赚钱,谁也不希望项目出现问题,如果是公司要开发人员重构公司项目,也可以考虑那么做,毕竟这个时候的风险公司已经承担了大部分了,接下来的话只需要服从公司安排即可。 * 更新的方式:由于 AndroidProject 不是一个单独的框架那么简单,无法通过更新远程依赖的方式进行升级,所以只能通过替换代码的形式进行更新,需要注意的是,代码覆盖完需要经过严格的自测及测试,测试是做这件事情的关键流程,需要重视起来,对每一处功能进行详细测试,一定要详细,特别涉及到主流程的功能。 @@ -389,6 +391,84 @@ startActivityForResult(HomeActivity.class, new OnActivityCallback() { * 所以并不是我不想用,而是谷歌封装得还不够好,至少在我看来还不够好,抛去 AndroidProject 封装的时间早不说,谷歌封装出来的效果也是强差人意,我感觉谷歌工程师的封装得越来越敷衍了,看起来像是在完成任务,而不是在做好一件事。 +#### 为什么新版移除了权限申请的 AOP 注解 + +* 具体原因可以看这个 [`wurensen/gradle_plugin_android_aspectjx/issues/60`](https://github.com/wurensen/gradle_plugin_android_aspectjx/issues/60),这里就不展开讲了,我的解决方案是移除权限申请的 AOP 注解,避免后面的人踩同样的坑,如果你已经知晓问题的原因,但是就是想用怎么办?我可以把删除的代码贴出来,到底要不要加进去大家自行斟酌。 + +```java +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface Permissions { + + /** + * 需要申请权限的集合 + */ + String[] value(); +} +``` + +```java +@SuppressWarnings("unused") +@Aspect +public class PermissionsAspect { + + /** + * 方法切入点 + */ + @Pointcut("execution(@com.hjq.demo.aop.Permissions * *(..))") + public void method() {} + + /** + * 在连接点进行方法替换 + */ + @Around("method() && @annotation(permissions)") + public void aroundJoinPoint(ProceedingJoinPoint joinPoint, Permissions permissions) { + Activity activity = null; + + // 方法参数值集合 + Object[] parameterValues = joinPoint.getArgs(); + for (Object arg : parameterValues) { + if (!(arg instanceof Activity)) { + continue; + } + activity = (Activity) arg; + break; + } + + if (activity == null || activity.isFinishing() || activity.isDestroyed()) { + activity = ActivityManager.getInstance().getTopActivity(); + } + + if (activity == null || activity.isFinishing() || activity.isDestroyed()) { + Timber.e("The activity has been destroyed and permission requests cannot be made"); + return; + } + + requestPermissions(joinPoint, activity, permissions.value()); + } + + private void requestPermissions(ProceedingJoinPoint joinPoint, Activity activity, String[] requestPermissions) { + XXPermissions.with(activity) + .permission(requestPermissions) + .interceptor(new PermissionInterceptor()) + .description(new PermissionDescription()) + .request((grantedList, deniedList) -> { + boolean allGranted = deniedList.isEmpty(); + if (!allGranted) { + return; + } + try { + // 获得权限,执行原方法 + joinPoint.proceed(); + } catch (Throwable e) { + e.printStackTrace(); + CrashReport.postCatchedException(e); + } + }); + } +} +``` + #### 轮子哥你怎么看待层出不穷的新技术 * 新东西的出现总能引起别人的好奇和尝试,但是我建议有兴趣的人可以学一下,但是如果要应用到项目中,我个人建议还是要慎重,因为纵观历史,我们不难发现,技术创新虽然很受欢迎,但是大多数都经不住时间的考验,最终一个个气尽倒下,这是因为很多新技术,表面看起来很美好,但实际上一入坑深似海。当然也有一些优秀的技术创新活了下来,但是毕竟占的是少数。 diff --git a/README.md b/README.md index 96e1ed0e..5da3fd1c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 安卓技术中台 -* 项目地址:[Github](https://github.com/getActivity/AndroidProject)、[码云](https://gitee.com/getActivity/AndroidProject) +* 项目地址:[Github](https://github.com/getActivity/AndroidProject) * Kotlin 版本:[AndroidProject-Kotlin](https://github.com/getActivity/AndroidProject-Kotlin) @@ -8,9 +8,9 @@ * 当我们日复一日年复一年的搬砖的时候,你是否曾想过提升一下开发效率,如果一个通用的架构摆在你的面前,你还会选择自己搭架构么,但是搭建出一个好的架构并非易事,有多少人愿意选择去做,还有多少人选择努力去做好,可能寥寥无几,但是你今天看到的,正是你所想要的,一个真正能解决你开发新项目时最大痛点的架构工程,你不需要再麻木 Copy 原有旧项目的代码,只需改动少量代码就能得到想要的效果,你会发现开发新项目其实是一件很快乐的事。 -* AndroidProject 已维护三年多的时间,几乎耗尽我所有的业余时间,里面的代码改了再改,改了又改,不断 Review、不断创新、不断改进、不断测试、不断优化,每天都在重复这些枯燥的步骤,但是只有这样才能把这件事做好,因为我相信把同样一件事重复做,迟早有一天可以做好。 +* AndroidProject 已维护七年多的时间,几乎耗尽我所有的业余时间,里面的代码改了再改,改了又改,不断 Review、不断创新、不断改进、不断测试、不断优化,每天都在重复这些枯燥的步骤,但是只有这样才能把这件事做好,因为我相信把同样一件事重复做,迟早有一天可以做好。 -* 已经正式投入到多个公司项目实践中,暂时没有发现任何问题或者 Bug,[点击下载 Apk 体验](AndroidProject.apk),又或者扫码下载 +* 已经正式投入到多个公司项目实践中,暂时没有发现任何问题或者 Bug,[点击下载 Apk 体验](https://github.com/getActivity/AndroidProject/releases/download/13.1/AndroidProject.apk),又或者扫码下载 ![](picture/demo_code.png) @@ -96,40 +96,74 @@ #### [代码规范文档请点击这里查看](https://github.com/getActivity/AndroidCodeStandard) +#### [版本适配文档请点击这里查看](https://github.com/getActivity/AndroidVersionAdapter) + #### [常见问题解答请点击这里查看](HelpDoc.md) +#### 使用案例 + +![](picture/douyin/douyin_logo.png) + +[![](picture/douyin/douyin_open_source_agreement.jpg)](https://aweme.snssdk.com/draft/douyin_agreement/open_source.html?hide_nav_bar=1&disable_auto_expose=1&font_scale=1.0&is_init_login=1&contact_permission=0&push_permission=1&bounce_disable=1&is_tab=0&launch_method=enter_launch&show_loading=1) + #### 作者的其他开源项目 -* 网络框架:[EasyHttp](https://github.com/getActivity/EasyHttp) (已集成) +* 安卓技术中台 Kt 版:[AndroidProject-Kotlin](https://github.com/getActivity/AndroidProject-Kotlin) ![](https://img.shields.io/github/stars/getActivity/AndroidProject-Kotlin.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidProject-Kotlin.svg) + +* 权限框架:[XXPermissions](https://github.com/getActivity/XXPermissions) ![](https://img.shields.io/github/stars/getActivity/XXPermissions.svg) ![](https://img.shields.io/github/forks/getActivity/XXPermissions.svg) + +* 吐司框架:[Toaster](https://github.com/getActivity/Toaster) ![](https://img.shields.io/github/stars/getActivity/Toaster.svg) ![](https://img.shields.io/github/forks/getActivity/Toaster.svg) + +* 网络框架:[EasyHttp](https://github.com/getActivity/EasyHttp) ![](https://img.shields.io/github/stars/getActivity/EasyHttp.svg) ![](https://img.shields.io/github/forks/getActivity/EasyHttp.svg) + +* 标题栏框架:[TitleBar](https://github.com/getActivity/TitleBar) ![](https://img.shields.io/github/stars/getActivity/TitleBar.svg) ![](https://img.shields.io/github/forks/getActivity/TitleBar.svg) + +* 悬浮窗框架:[EasyWindow](https://github.com/getActivity/EasyWindow) ![](https://img.shields.io/github/stars/getActivity/EasyWindow.svg) ![](https://img.shields.io/github/forks/getActivity/EasyWindow.svg) + +* 设备兼容框架:[DeviceCompat](https://github.com/getActivity/DeviceCompat) ![](https://img.shields.io/github/stars/getActivity/DeviceCompat.svg) ![](https://img.shields.io/github/forks/getActivity/DeviceCompat.svg) -* 权限框架:[XXPermissions](https://github.com/getActivity/XXPermissions) (已集成) +* ShapeView 框架:[ShapeView](https://github.com/getActivity/ShapeView) ![](https://img.shields.io/github/stars/getActivity/ShapeView.svg) ![](https://img.shields.io/github/forks/getActivity/ShapeView.svg) -* 吐司框架:[ToastUtils](https://github.com/getActivity/ToastUtils) (已集成) +* ShapeDrawable 框架:[ShapeDrawable](https://github.com/getActivity/ShapeDrawable) ![](https://img.shields.io/github/stars/getActivity/ShapeDrawable.svg) ![](https://img.shields.io/github/forks/getActivity/ShapeDrawable.svg) -* 标题栏框架:[TitleBar](https://github.com/getActivity/TitleBar) (已集成) +* 语种切换框架:[MultiLanguages](https://github.com/getActivity/MultiLanguages) ![](https://img.shields.io/github/stars/getActivity/MultiLanguages.svg) ![](https://img.shields.io/github/forks/getActivity/MultiLanguages.svg) -* Gson 解析容错:[GsonFactory](https://github.com/getActivity/GsonFactory) (已集成) +* Gson 解析容错:[GsonFactory](https://github.com/getActivity/GsonFactory) ![](https://img.shields.io/github/stars/getActivity/GsonFactory.svg) ![](https://img.shields.io/github/forks/getActivity/GsonFactory.svg) -* Shape 框架:[ShapeView](https://github.com/getActivity/ShapeView) (已集成) +* 日志查看框架:[Logcat](https://github.com/getActivity/Logcat) ![](https://img.shields.io/github/stars/getActivity/Logcat.svg) ![](https://img.shields.io/github/forks/getActivity/Logcat.svg) -* 悬浮窗框架:[XToast](https://github.com/getActivity/XToast) (未集成) +* 嵌套滚动布局框架:[NestedScrollLayout](https://github.com/getActivity/NestedScrollLayout) ![](https://img.shields.io/github/stars/getActivity/NestedScrollLayout.svg) ![](https://img.shields.io/github/forks/getActivity/NestedScrollLayout.svg) -* 国际化框架:[MultiLanguages](https://github.com/getActivity/MultiLanguages) (未集成) +* Android 命令行工具集:[AndroidCmdTools](https://github.com/getActivity/AndroidCmdTools) ![](https://img.shields.io/github/stars/getActivity/AndroidCmdTools.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidCmdTools.svg) -* 日志查看框架:[Logcat](https://github.com/getActivity/Logcat) (未集成) +* Android 版本适配:[AndroidVersionAdapter](https://github.com/getActivity/AndroidVersionAdapter) ![](https://img.shields.io/github/stars/getActivity/AndroidVersionAdapter.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidVersionAdapter.svg) + +* Android 代码规范:[AndroidCodeStandard](https://github.com/getActivity/AndroidCodeStandard) ![](https://img.shields.io/github/stars/getActivity/AndroidCodeStandard.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidCodeStandard.svg) + +* Android 资源大汇总:[AndroidIndex](https://github.com/getActivity/AndroidIndex) ![](https://img.shields.io/github/stars/getActivity/AndroidIndex.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidIndex.svg) + +* Android 开源排行榜:[AndroidGithubBoss](https://github.com/getActivity/AndroidGithubBoss) ![](https://img.shields.io/github/stars/getActivity/AndroidGithubBoss.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidGithubBoss.svg) + +* Studio 精品插件:[StudioPlugins](https://github.com/getActivity/StudioPlugins) ![](https://img.shields.io/github/stars/getActivity/StudioPlugins.svg) ![](https://img.shields.io/github/forks/getActivity/StudioPlugins.svg) + +* 表情包大集合:[EmojiPackage](https://github.com/getActivity/EmojiPackage) ![](https://img.shields.io/github/stars/getActivity/EmojiPackage.svg) ![](https://img.shields.io/github/forks/getActivity/EmojiPackage.svg) + +* AI 资源大汇总:[AiIndex](https://github.com/getActivity/AiIndex) ![](https://img.shields.io/github/stars/getActivity/AiIndex.svg) ![](https://img.shields.io/github/forks/getActivity/AiIndex.svg) + +* 省市区 Json 数据:[ProvinceJson](https://github.com/getActivity/ProvinceJson) ![](https://img.shields.io/github/stars/getActivity/ProvinceJson.svg) ![](https://img.shields.io/github/forks/getActivity/ProvinceJson.svg) + +* Markdown 语法文档:[MarkdownDoc](https://github.com/getActivity/MarkdownDoc) ![](https://img.shields.io/github/stars/getActivity/MarkdownDoc.svg) ![](https://img.shields.io/github/forks/getActivity/MarkdownDoc.svg) #### 微信公众号:Android轮子哥 ![](https://raw.githubusercontent.com/getActivity/Donate/master/picture/official_ccount.png) -#### Android 技术分享 QQ 群:78797078 +#### Android 技术 Q 群:10047167 -#### 如果您觉得我的开源库帮你节省了大量的开发时间,请扫描下方的二维码随意打赏,要是能打赏个 10.24 :monkey_face:就太:thumbsup:了。您的支持将鼓励我继续创作:octocat: +#### 如果您觉得我的开源库帮你节省了大量的开发时间,请扫描下方的二维码随意打赏,要是能打赏个 10.24 :monkey_face:就太:thumbsup:了。您的支持将鼓励我继续创作:octocat:([点击查看捐赠列表](https://github.com/getActivity/Donate)) ![](https://raw.githubusercontent.com/getActivity/Donate/master/picture/pay_ali.png) ![](https://raw.githubusercontent.com/getActivity/Donate/master/picture/pay_wechat.png) -#### [点击查看捐赠列表](https://github.com/getActivity/Donate) - ## License ```text diff --git a/app/build.gradle b/app/build.gradle index 3f6193b0..4e5d5fdf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,9 +1,20 @@ -apply plugin : 'com.android.application' -apply plugin : 'android-aspectjx' +plugins { + alias(libs.plugins.application) + alias(libs.plugins.kotlin) + alias(libs.plugins.aop) + alias(libs.plugins.easyLauncher) +} + apply from : '../common.gradle' // Android 代码规范文档:https://github.com/getActivity/AndroidCodeStandard android { + namespace 'com.hjq.demo' + + buildFeatures { + // 是否生成 BuildConfig 类 + buildConfig true + } // 资源目录存放指引:https://developer.android.google.cn/guide/topics/resources/providing-resources defaultConfig { @@ -20,21 +31,38 @@ android { // 混淆配置 proguardFiles 'proguard-sdk.pro', 'proguard-app.pro' - // 日志打印开关 + // 日志开关 buildConfigField('boolean', 'LOG_ENABLE', '' + LOG_ENABLE + '') - // 测试包下的 BuglyId - buildConfigField('String', 'BUGLY_ID', '"' + BUGLY_ID + '"') - // 测试服务器的主机地址 + // 主机地址 buildConfigField('String', 'HOST_URL', '"' + HOST_URL + '"') + // BuglyId + buildConfigField('String', 'BUGLY_ID', '"' + BUGLY_ID + '"') + // BuglyKey + buildConfigField('String', 'BUGLY_KEY', '"' + BUGLY_KEY + '"') + + // 仅保留 arm64-v8a 架构(需要注意的是 mmkv 库在 2.0 及之后的版本已经不支持在 32 位的机器上面运行) + ndk { + abiFilters 'arm64-v8a' + } + } + + sourceSets { + main { + // res 资源目录配置 + res.srcDirs( + 'src/main/res', + 'src/main/res-common' + ) + } } // Apk 签名的那些事:https://www.jianshu.com/p/a1f8e5896aa2 signingConfigs { config { - storeFile file(StoreFile) - storePassword StorePassword - keyAlias KeyAlias - keyPassword KeyPassword + storeFile file(STORE_FILE) + storePassword STORE_PASSWORD + keyAlias KEY_ALIAS + keyPassword KEY_PASSWORD } } @@ -47,8 +75,6 @@ android { // 调试模式开关 debuggable true jniDebuggable true - // 压缩对齐开关 - zipAlignEnabled false // 移除无用的资源 shrinkResources false // 代码混淆开关 @@ -57,12 +83,8 @@ android { signingConfig signingConfigs.config // 添加清单占位符 addManifestPlaceholders([ - 'app_name' : '安卓技术中台 Debug 版' + 'app_name' : '@string/app_name_debug' ]) - // 调试模式下只保留一种架构的 so 库,提升打包速度 - ndk { - abiFilters 'armeabi-v7a' - } } preview.initWith(debug) @@ -70,7 +92,7 @@ android { applicationIdSuffix '' // 添加清单占位符 addManifestPlaceholders([ - 'app_name' : '安卓技术中台 Preview 版' + 'app_name' : '@string/app_name_preview' ]) } @@ -78,8 +100,6 @@ android { // 调试模式开关 debuggable false jniDebuggable false - // 压缩对齐开关 - zipAlignEnabled true // 移除无用的资源 shrinkResources true // 代码混淆开关 @@ -90,13 +110,6 @@ android { addManifestPlaceholders([ 'app_name' : '@string/app_name' ]) - // 仅保留两种架构的 so 库,根据 Bugly 统计得出 - ndk { - // armeabi:万金油架构平台(占用率:0%) - // armeabi-v7a:曾经主流的架构平台(占用率:10%) - // arm64-v8a:目前主流架构平台(占用率:95%) - abiFilters 'armeabi-v7a', 'arm64-v8a' - } } } @@ -106,19 +119,16 @@ android { } // AOP 配置(exclude 和 include 二选一) - // 需要进行配置,否则就会引发冲突,具体表现为: - // 第一种:编译不过去,报错:java.util.zip.ZipException:Cause: zip file is empty - // 第二种:编译能过去,但运行时报错:ClassNotFoundException: Didn't find class on path: DexPathList - aspectjx { + androidAopConfig { // 排除一些第三方库的包名(Gson、 LeakCanary 和 AOP 有冲突) // exclude 'androidx', 'com.google', 'com.squareup', 'org.apache', 'com.alipay', 'com.taobao', 'versions.9' // 只对以下包名做 AOP 处理 include android.defaultConfig.applicationId } - applicationVariants.all { variant -> + applicationVariants.configureEach { variant -> // apk 输出文件名配置 - variant.outputs.all { output -> + variant.outputs.configureEach { output -> outputFileName = rootProject.getName() + '_v' + variant.versionName + '_' + variant.buildType.name if (variant.buildType.name == buildTypes.release.getName()) { outputFileName += '_' + new Date().format('MMdd') @@ -128,80 +138,92 @@ android { } } +// 应用启动图标配置 +easylauncher { + buildTypes { + preview { + filters = [ + customRibbon(label: "preview", position: "topRight", + ribbonColor: "#b65656", labelColor: "#FFFFFF") + ] + } + debug { + filters = [ + customRibbon(label: "debug", position: "topRight", + ribbonColor: "#b65656", labelColor: "#FFFFFF") + ] + } + release { + enable false + } + } +} + // 添加构建依赖项:https://developer.android.google.cn/studio/build/dependencies // api 与 implementation 的区别:https://www.jianshu.com/p/8962d6ba936e dependencies { - // 基类封装 + implementation project(':library:core') implementation project(':library:base') - // 控件封装 - implementation project(':library:widget') - // 友盟封装 - implementation project(':library:umeng') + implementation project(':library:smallestWidth') + implementation project(':library:customWidget') + implementation project(':library:umengSdk') + + implementation libs.deviceCompat + implementation libs.xxPermissions - // 权限请求框架:https://github.com/getActivity/XXPermissions - implementation 'com.github.getActivity:XXPermissions:12.3' + implementation libs.titleBar - // 标题栏框架:https://github.com/getActivity/TitleBar - implementation 'com.github.getActivity:TitleBar:9.2' + implementation libs.toaster - // 吐司框架:https://github.com/getActivity/ToastUtils - implementation 'com.github.getActivity:ToastUtils:9.5' + implementation libs.easyHttp + implementation libs.okHttp - // 网络请求框架:https://github.com/getActivity/EasyHttp - implementation 'com.github.getActivity:EasyHttp:10.2' - // OkHttp 框架:https://github.com/square/okhttp - // noinspection GradleDependency - implementation 'com.squareup.okhttp3:okhttp:3.12.13' + implementation(libs.gsonFactory) { + exclude group: 'org.jetbrains.kotlin', module: 'kotlin-reflect' + } + implementation(libs.gson) + implementation libs.kotlinReflect + + implementation libs.shapeView + implementation libs.shapeDrawable - // Json 解析框架:https://github.com/google/gson - implementation 'com.google.code.gson:gson:2.8.8' - // Gson 解析容错:https://github.com/getActivity/GsonFactory - implementation 'com.github.getActivity:GsonFactory:5.2' + implementation libs.nestedScrollLayout - // Shape 框架:https://github.com/getActivity/ShapeView - implementation 'com.github.getActivity:ShapeView:6.0' + implementation libs.glide + annotationProcessor libs.glideCompiler - // AOP 插件库:https://mvnrepository.com/artifact/org.aspectj/aspectjrt - implementation 'org.aspectj:aspectjrt:1.9.6' + implementation libs.immersionBar - // 图片加载框架:https://github.com/bumptech/glide - // 官方使用文档:https://github.com/Muyangmin/glide-docs-cn - implementation 'com.github.bumptech.glide:glide:4.12.0' - annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0' + implementation libs.photoView - // 沉浸式框架:https://github.com/gyf-dev/ImmersionBar - implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + implementation libs.bugly - // 手势 ImageView:https://github.com/Baseflow/PhotoView - implementation 'com.github.Baseflow:PhotoView:2.3.0' + implementation libs.lottie - // Bugly 异常捕捉:https://bugly.qq.com/docs/user-guide/instruction-manual-android/?v=20190418140644 - implementation 'com.tencent.bugly:crashreport:3.4.4' - implementation 'com.tencent.bugly:nativecrashreport:3.9.2' + implementation libs.refreshLayoutKernel + implementation libs.refreshHeaderMaterial - // 动画解析库:https://github.com/airbnb/lottie-android - // 动画资源:https://lottiefiles.com、https://icons8.com/animated-icons - implementation 'com.airbnb.android:lottie:4.1.0' + implementation libs.timber - // 上拉刷新下拉加载框架:https://github.com/scwang90/SmartRefreshLayout - implementation 'com.scwang.smart:refresh-layout-kernel:2.0.3' - implementation 'com.scwang.smart:refresh-header-material:2.0.3' + implementation libs.circleIndicator - // 日志打印框架:https://github.com/JakeWharton/timber - implementation 'com.jakewharton.timber:timber:4.7.1' + implementation(libs.mmkvStatic) { + exclude group: 'androidx.annotation', module: 'annotation' + } - // 指示器框架:https://github.com/ongakuer/CircleIndicator - implementation 'me.relex:circleindicator:2.1.6' + implementation libs.softInputEvent + implementation libs.androidAopCore + annotationProcessor libs.androidAopApt - // 腾讯 MMKV:https://github.com/Tencent/MMKV - implementation 'com.tencent:mmkv-static:1.2.10' + debugImplementation libs.leakCanary + previewImplementation libs.leakCanary - // 内存泄漏监测框架:https://github.com/square/leakcanary - debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7' - previewImplementation 'com.squareup.leakcanary:leakcanary-android:2.7' + debugImplementation libs.chucker + previewImplementation libs.chucker + releaseImplementation libs.chuckerNoOp // 多语种:https://github.com/getActivity/MultiLanguages - // 悬浮窗:https://github.com/getActivity/XToast + // 悬浮窗:https://github.com/getActivity/EasyWindow // 日志输出:https://github.com/getActivity/Logcat // 工具类:https://github.com/Blankj/AndroidUtilCode // 轮播图:https://github.com/bingoogolapple/BGABanner-Android @@ -212,5 +234,5 @@ dependencies { // 多渠道打包:https://github.com/Meituan-Dianping/walle // 设备唯一标识:http://msa-alliance.cn/col.jsp?id=120 // 嵌套滚动容器:https://github.com/donkingliang/ConsecutiveScroller - // 隐私调用监控:https://github.com/huage2580/PermissionMonitor + // 隐私调用监控:https://github.com/allenymt/PrivacySentry } \ No newline at end of file diff --git a/app/gradle.properties b/app/gradle.properties index 97eeed7c..db1dd0a7 100644 --- a/app/gradle.properties +++ b/app/gradle.properties @@ -1,4 +1,4 @@ -StoreFile = AppSignature.jks -StorePassword = AndroidProject -KeyAlias = AndroidProject -KeyPassword = AndroidProject \ No newline at end of file +STORE_FILE = AppSignature.jks +STORE_PASSWORD = AndroidProject +KEY_ALIAS = AndroidProject +KEY_PASSWORD = AndroidProject \ No newline at end of file diff --git a/app/proguard-sdk.pro b/app/proguard-sdk.pro index 5c39db9a..03bec5c4 100644 --- a/app/proguard-sdk.pro +++ b/app/proguard-sdk.pro @@ -1,36 +1,8 @@ -# Glide --keep public class * implements com.bumptech.glide.module.GlideModule --keep class * extends com.bumptech.glide.module.AppGlideModule { - (...); -} --keep public enum com.bumptech.glide.load.ImageHeaderParser$** { - **[] $VALUES; - public *; -} --keep class com.bumptech.glide.load.data.ParcelFileDescriptorRewinder$InternalRewinder { - *** rewind(); -} - -# for DexGuard only -#-keepresourcexmlelements manifest/application/meta-data@value=GlideModule - -# Bugly --dontwarn com.tencent.bugly.** --keep public class com.tencent.bugly.**{*;} - -# AOP --adaptclassstrings --keepattributes InnerClasses, EnclosingMethod, Signature, *Annotation* - --keepnames @org.aspectj.lang.annotation.Aspect class * { - public ; -} - -# OkHttp3 --keepattributes Signature --keepattributes *Annotation* --keep class okhttp3.** { *; } --keep interface okhttp3.** { *; } --dontwarn okhttp3.** --dontwarn okio.** --dontwarn org.conscrypt.** \ No newline at end of file +# EasyHttp +# 不混淆实现 OnHttpListener 接口的类,必须要加上此规则,否则会导致泛型解析失败 +-keep class * implements com.hjq.http.listener.OnHttpListener { + *; +} +-keep class * extends com.hjq.http.model.ResponseClass { + *; +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d4b70b41..4202a811 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,13 +9,20 @@ - - - + + + + + + + + + + @@ -30,11 +37,12 @@ android:allowBackup="false" android:icon="@mipmap/launcher_ic" android:label="${app_name}" + android:largeHeap="true" android:networkSecurityConfig="@xml/network_security_config" android:requestLegacyExternalStorage="true" android:resizeableActivity="true" android:roundIcon="@mipmap/launcher_ic" - android:supportsRtl="false" + android:supportsRtl="true" android:theme="@style/AppTheme" android:usesCleartextTraffic="true" tools:ignore="AllowBackup,LockedOrientationActivity" @@ -61,6 +69,7 @@ @@ -84,34 +93,35 @@ @@ -124,7 +134,7 @@ - - - @@ -230,13 +235,6 @@ android:launchMode="singleTop" android:screenOrientation="portrait" /> - - - diff --git a/app/src/main/java/com/hjq/demo/action/ImmersionAction.java b/app/src/main/java/com/hjq/demo/action/ImmersionAction.java new file mode 100644 index 00000000..dcfb0ff7 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/action/ImmersionAction.java @@ -0,0 +1,30 @@ +package com.hjq.demo.action; + +import android.view.View; +import androidx.annotation.Nullable; +import com.hjq.bar.OnTitleBarListener; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2025/12/13 + * desc : 沉浸式意图 + */ +public interface ImmersionAction extends OnTitleBarListener { + + /** + * 获取需要沉浸的顶部 View 对象 + */ + @Nullable + default View getImmersionTopView() { + return null; + } + + /** + * 获取需要沉浸的底部 View 对象 + */ + @Nullable + default View getImmersionBottomView() { + return null; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/action/StatusAction.java b/app/src/main/java/com/hjq/demo/action/StatusAction.java index da9f09ba..eefdd2d1 100644 --- a/app/src/main/java/com/hjq/demo/action/StatusAction.java +++ b/app/src/main/java/com/hjq/demo/action/StatusAction.java @@ -4,12 +4,10 @@ import android.graphics.drawable.Drawable; import android.net.ConnectivityManager; import android.net.NetworkInfo; - import androidx.annotation.DrawableRes; import androidx.annotation.RawRes; import androidx.annotation.StringRes; import androidx.core.content.ContextCompat; - import com.hjq.demo.R; import com.hjq.demo.widget.StatusLayout; @@ -24,7 +22,7 @@ public interface StatusAction { /** * 获取状态布局 */ - StatusLayout getStatusLayout(); + StatusLayout acquireStatusLayout(); /** * 显示加载中 @@ -34,7 +32,10 @@ default void showLoading() { } default void showLoading(@RawRes int id) { - StatusLayout layout = getStatusLayout(); + StatusLayout layout = acquireStatusLayout(); + if (layout == null) { + return; + } layout.show(); layout.setAnimResource(id); layout.setHint(""); @@ -45,7 +46,7 @@ default void showLoading(@RawRes int id) { * 显示加载完成 */ default void showComplete() { - StatusLayout layout = getStatusLayout(); + StatusLayout layout = acquireStatusLayout(); if (layout == null || !layout.isShow()) { return; } @@ -63,14 +64,17 @@ default void showEmpty() { * 显示错误提示 */ default void showError(StatusLayout.OnRetryListener listener) { - StatusLayout layout = getStatusLayout(); + StatusLayout layout = acquireStatusLayout(); + if (layout == null) { + return; + } Context context = layout.getContext(); ConnectivityManager manager = ContextCompat.getSystemService(context, ConnectivityManager.class); if (manager != null) { NetworkInfo info = manager.getActiveNetworkInfo(); // 判断网络是否连接 if (info == null || !info.isConnected()) { - showLayout(R.drawable.status_nerwork_ic, R.string.status_layout_error_network, listener); + showLayout(R.drawable.status_network_ic, R.string.status_layout_error_network, listener); return; } } @@ -81,13 +85,19 @@ default void showError(StatusLayout.OnRetryListener listener) { * 显示自定义提示 */ default void showLayout(@DrawableRes int drawableId, @StringRes int stringId, StatusLayout.OnRetryListener listener) { - StatusLayout layout = getStatusLayout(); + StatusLayout layout = acquireStatusLayout(); + if (layout == null) { + return; + } Context context = layout.getContext(); showLayout(ContextCompat.getDrawable(context, drawableId), context.getString(stringId), listener); } default void showLayout(Drawable drawable, CharSequence hint, StatusLayout.OnRetryListener listener) { - StatusLayout layout = getStatusLayout(); + StatusLayout layout = acquireStatusLayout(); + if (layout == null) { + return; + } layout.show(); layout.setIcon(drawable); layout.setHint(hint); diff --git a/app/src/main/java/com/hjq/demo/action/TitleBarAction.java b/app/src/main/java/com/hjq/demo/action/TitleBarAction.java index 65c7d732..8c64d8fa 100644 --- a/app/src/main/java/com/hjq/demo/action/TitleBarAction.java +++ b/app/src/main/java/com/hjq/demo/action/TitleBarAction.java @@ -3,10 +3,8 @@ import android.graphics.drawable.Drawable; import android.view.View; import android.view.ViewGroup; - import androidx.annotation.Nullable; import androidx.annotation.StringRes; - import com.hjq.bar.OnTitleBarListener; import com.hjq.bar.TitleBar; @@ -18,158 +16,167 @@ */ public interface TitleBarAction extends OnTitleBarListener { - @Nullable - TitleBar getTitleBar(); - - /** - * 左项被点击 - * - * @param view 被点击的左项View - */ - @Override - default void onLeftClick(View view) {} - - /** - * 标题被点击 - * - * @param view 被点击的标题View - */ - @Override - default void onTitleClick(View view) {} - /** - * 右项被点击 - * - * @param view 被点击的右项View + * 获取标题栏对象 */ - @Override - default void onRightClick(View view) {} + @Nullable + TitleBar acquireTitleBar(); /** * 设置标题栏的标题 */ default void setTitle(@StringRes int id) { - if (getTitleBar() != null) { - setTitle(getTitleBar().getResources().getString(id)); + TitleBar titleBar = acquireTitleBar(); + if (titleBar == null) { + return; } + titleBar.setTitle(id); } /** * 设置标题栏的标题 */ default void setTitle(CharSequence title) { - if (getTitleBar() != null) { - getTitleBar().setTitle(title); + TitleBar titleBar = acquireTitleBar(); + if (titleBar == null) { + return; } + titleBar.setTitle(title); } /** * 设置标题栏的左标题 */ default void setLeftTitle(int id) { - if (getTitleBar() != null) { - getTitleBar().setLeftTitle(id); + TitleBar titleBar = acquireTitleBar(); + if (titleBar == null) { + return; } + titleBar.setLeftTitle(id); } default void setLeftTitle(CharSequence text) { - if (getTitleBar() != null) { - getTitleBar().setLeftTitle(text); + TitleBar titleBar = acquireTitleBar(); + if (titleBar == null) { + return; } + titleBar.setLeftTitle(text); } default CharSequence getLeftTitle() { - if (getTitleBar() != null) { - return getTitleBar().getLeftTitle(); + TitleBar titleBar = acquireTitleBar(); + if (titleBar == null) { + return ""; } - return ""; + return titleBar.getLeftTitle(); } /** * 设置标题栏的右标题 */ default void setRightTitle(int id) { - if (getTitleBar() != null) { - getTitleBar().setRightTitle(id); + TitleBar titleBar = acquireTitleBar(); + if (titleBar == null) { + return; } + titleBar.setRightTitle(id); } default void setRightTitle(CharSequence text) { - if (getTitleBar() != null) { - getTitleBar().setRightTitle(text); + TitleBar titleBar = acquireTitleBar(); + if (titleBar == null) { + return; } + titleBar.setRightTitle(text); } default CharSequence getRightTitle() { - if (getTitleBar() != null) { - return getTitleBar().getRightTitle(); + TitleBar titleBar = acquireTitleBar(); + if (titleBar == null) { + return ""; } - return ""; + return titleBar.getRightTitle(); } /** * 设置标题栏的左图标 */ default void setLeftIcon(int id) { - if (getTitleBar() != null) { - getTitleBar().setLeftIcon(id); + TitleBar titleBar = acquireTitleBar(); + if (titleBar == null) { + return; } + titleBar.setLeftIcon(id); } default void setLeftIcon(Drawable drawable) { - if (getTitleBar() != null) { - getTitleBar().setLeftIcon(drawable); + TitleBar titleBar = acquireTitleBar(); + if (titleBar == null) { + return; } + titleBar.setLeftIcon(drawable); } @Nullable default Drawable getLeftIcon() { - if (getTitleBar() != null) { - return getTitleBar().getLeftIcon(); + TitleBar titleBar = acquireTitleBar(); + if (titleBar == null) { + return null; } - return null; + return titleBar.getLeftIcon(); } /** * 设置标题栏的右图标 */ default void setRightIcon(int id) { - if (getTitleBar() != null) { - getTitleBar().setRightIcon(id); + TitleBar titleBar = acquireTitleBar(); + if (titleBar == null) { + return; } + titleBar.setRightIcon(id); } default void setRightIcon(Drawable drawable) { - if (getTitleBar() != null) { - getTitleBar().setRightIcon(drawable); + TitleBar titleBar = acquireTitleBar(); + if (titleBar == null) { + return; } + titleBar.setRightIcon(drawable); } @Nullable default Drawable getRightIcon() { - if (getTitleBar() != null) { - return getTitleBar().getRightIcon(); + TitleBar titleBar = acquireTitleBar(); + if (titleBar == null) { + return null; } - return null; + return titleBar.getRightIcon(); } /** * 递归获取 ViewGroup 中的 TitleBar 对象 */ - default TitleBar obtainTitleBar(ViewGroup group) { - if (group == null) { + default TitleBar findTitleBar(@Nullable View contentView) { + if (contentView == null) { return null; } - for (int i = 0; i < group.getChildCount(); i++) { - View view = group.getChildAt(i); - if ((view instanceof TitleBar)) { - return (TitleBar) view; - } + if (contentView instanceof TitleBar) { + return (TitleBar) contentView; + } + if (contentView instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) contentView; + for (int i = 0; i < viewGroup.getChildCount(); i++) { + View view = viewGroup.getChildAt(i); + if ((view instanceof TitleBar)) { + return (TitleBar) view; + } - if (view instanceof ViewGroup) { - TitleBar titleBar = obtainTitleBar((ViewGroup) view); - if (titleBar != null) { - return titleBar; + if (view instanceof ViewGroup) { + TitleBar titleBar = findTitleBar(view); + if (titleBar != null) { + return titleBar; + } } } } diff --git a/app/src/main/java/com/hjq/demo/action/ToastAction.java b/app/src/main/java/com/hjq/demo/action/ToastAction.java index 7f1bccc7..49cd2887 100644 --- a/app/src/main/java/com/hjq/demo/action/ToastAction.java +++ b/app/src/main/java/com/hjq/demo/action/ToastAction.java @@ -1,8 +1,7 @@ package com.hjq.demo.action; import androidx.annotation.StringRes; - -import com.hjq.toast.ToastUtils; +import com.hjq.toast.Toaster; /** * author : Android 轮子哥 @@ -13,14 +12,14 @@ public interface ToastAction { default void toast(CharSequence text) { - ToastUtils.show(text); + Toaster.show(text); } default void toast(@StringRes int id) { - ToastUtils.show(id); + Toaster.show(id); } default void toast(Object object) { - ToastUtils.show(object); + Toaster.show(object); } } \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/aop/CheckNet.java b/app/src/main/java/com/hjq/demo/aop/CheckNet.java index d2906522..77b9fd80 100644 --- a/app/src/main/java/com/hjq/demo/aop/CheckNet.java +++ b/app/src/main/java/com/hjq/demo/aop/CheckNet.java @@ -1,5 +1,6 @@ package com.hjq.demo.aop; +import com.flyjingfish.android_aop_annotation.anno.AndroidAopPointCut; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -13,4 +14,5 @@ */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) +@AndroidAopPointCut(CheckNetCut.class) public @interface CheckNet {} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/aop/CheckNetAspect.java b/app/src/main/java/com/hjq/demo/aop/CheckNetAspect.java deleted file mode 100644 index 1740ac6d..00000000 --- a/app/src/main/java/com/hjq/demo/aop/CheckNetAspect.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.hjq.demo.aop; - -import android.app.Application; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; - -import androidx.core.content.ContextCompat; - -import com.hjq.demo.R; -import com.hjq.demo.manager.ActivityManager; -import com.hjq.toast.ToastUtils; - -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.Around; -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.annotation.Pointcut; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2020/01/11 - * desc : 网络检测切面 - */ -@Aspect -public class CheckNetAspect { - - /** - * 方法切入点 - */ - @Pointcut("execution(@com.hjq.demo.aop.CheckNet * *(..))") - public void method() {} - - /** - * 在连接点进行方法替换 - */ - @Around("method() && @annotation(checkNet)") - public void aroundJoinPoint(ProceedingJoinPoint joinPoint, CheckNet checkNet) throws Throwable { - Application application = ActivityManager.getInstance().getApplication(); - if (application != null) { - ConnectivityManager manager = ContextCompat.getSystemService(application, ConnectivityManager.class); - if (manager != null) { - NetworkInfo info = manager.getActiveNetworkInfo(); - // 判断网络是否连接 - if (info == null || !info.isConnected()) { - ToastUtils.show(R.string.common_network_hint); - return; - } - } - } - //执行原方法 - joinPoint.proceed(); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/aop/CheckNetCut.java b/app/src/main/java/com/hjq/demo/aop/CheckNetCut.java new file mode 100644 index 00000000..3dabd48f --- /dev/null +++ b/app/src/main/java/com/hjq/demo/aop/CheckNetCut.java @@ -0,0 +1,36 @@ +package com.hjq.demo.aop; + +import android.app.Application; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import com.flyjingfish.android_aop_annotation.ProceedJoinPoint; +import com.flyjingfish.android_aop_annotation.base.BasePointCut; +import com.hjq.core.manager.ActivityManager; +import com.hjq.demo.R; +import com.hjq.toast.Toaster; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2020/01/11 + * desc : 网络检测切面 + */ +public class CheckNetCut implements BasePointCut { + + @SuppressWarnings("deprecation") + @Override + public Object invoke(@NonNull ProceedJoinPoint joinPoint, @NonNull CheckNet anno) throws Throwable { + Application application = ActivityManager.getInstance().getApplication(); + ConnectivityManager manager = ContextCompat.getSystemService(application, ConnectivityManager.class); + if (manager != null) { + NetworkInfo info = manager.getActiveNetworkInfo(); + if (info == null || !info.isConnected()) { + Toaster.show(R.string.common_network_hint); + return null; + } + } + return joinPoint.proceed(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/aop/Log.java b/app/src/main/java/com/hjq/demo/aop/Log.java index e3ec4ab9..269da8e5 100644 --- a/app/src/main/java/com/hjq/demo/aop/Log.java +++ b/app/src/main/java/com/hjq/demo/aop/Log.java @@ -1,5 +1,6 @@ package com.hjq.demo.aop; +import com.flyjingfish.android_aop_annotation.anno.AndroidAopPointCut; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -12,8 +13,9 @@ * desc : Debug 日志注解 */ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) +@Target(ElementType.METHOD) +@AndroidAopPointCut(LogCut.class) public @interface Log { - String value() default "AppLog"; + String value() default "AOPLog"; } \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/aop/LogAspect.java b/app/src/main/java/com/hjq/demo/aop/LogAspect.java deleted file mode 100644 index 4dca9b38..00000000 --- a/app/src/main/java/com/hjq/demo/aop/LogAspect.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.hjq.demo.aop; - -import android.os.Looper; -import android.os.Trace; - -import androidx.annotation.NonNull; - -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.Signature; -import org.aspectj.lang.annotation.Around; -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.annotation.Pointcut; -import org.aspectj.lang.reflect.CodeSignature; -import org.aspectj.lang.reflect.MethodSignature; - -import java.util.concurrent.TimeUnit; - -import timber.log.Timber; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2019/12/06 - * desc : Debug 日志切面 - */ -@Aspect -public class LogAspect { - - /** - * 构造方法切入点 - */ - @Pointcut("execution(@com.hjq.demo.aop.Log *.new(..))") - public void constructor() {} - - /** - * 方法切入点 - */ - @Pointcut("execution(@com.hjq.demo.aop.Log * *(..))") - public void method() {} - - /** - * 在连接点进行方法替换 - */ - @Around("(method() || constructor()) && @annotation(log)") - public Object aroundJoinPoint(ProceedingJoinPoint joinPoint, Log log) throws Throwable { - enterMethod(joinPoint, log); - - long startNanos = System.nanoTime(); - Object result = joinPoint.proceed(); - long stopNanos = System.nanoTime(); - - exitMethod(joinPoint, log, result, TimeUnit.NANOSECONDS.toMillis(stopNanos - startNanos)); - - return result; - } - - /** - * 方法执行前切入 - */ - private void enterMethod(ProceedingJoinPoint joinPoint, Log log) { - CodeSignature codeSignature = (CodeSignature) joinPoint.getSignature(); - - // 方法所在类 - String className = codeSignature.getDeclaringType().getName(); - // 方法名 - String methodName = codeSignature.getName(); - // 方法参数名集合 - String[] parameterNames = codeSignature.getParameterNames(); - // 方法参数值集合 - Object[] parameterValues = joinPoint.getArgs(); - - //记录并打印方法的信息 - StringBuilder builder = getMethodLogInfo(className, methodName, parameterNames, parameterValues); - - log(log.value(), builder.toString()); - - final String section = builder.substring(2); - Trace.beginSection(section); - } - - /** - * 获取方法的日志信息 - * - * @param className 类名 - * @param methodName 方法名 - * @param parameterNames 方法参数名集合 - * @param parameterValues 方法参数值集合 - */ - @NonNull - private StringBuilder getMethodLogInfo(String className, String methodName, String[] parameterNames, Object[] parameterValues) { - StringBuilder builder = new StringBuilder("\u21E2 "); - builder.append(className) - .append(".") - .append(methodName) - .append('('); - for (int i = 0; i < parameterValues.length; i++) { - if (i > 0) { - builder.append(", "); - } - builder.append(parameterNames[i]).append('='); - builder.append(parameterValues[i].toString()); - } - builder.append(')'); - - if (Looper.myLooper() != Looper.getMainLooper()) { - builder.append(" [Thread:\"").append(Thread.currentThread().getName()).append("\"]"); - } - return builder; - } - - - /** - * 方法执行完毕,切出 - * - * @param result 方法执行后的结果 - * @param lengthMillis 执行方法所需要的时间 - */ - private void exitMethod(ProceedingJoinPoint joinPoint, Log log, Object result, long lengthMillis) { - Trace.endSection(); - - Signature signature = joinPoint.getSignature(); - - String className = signature.getDeclaringType().getName(); - String methodName = signature.getName(); - - StringBuilder builder = new StringBuilder("\u21E0 ") - .append(className) - .append(".") - .append(methodName) - .append(" [") - .append(lengthMillis) - .append("ms]"); - - // 判断方法是否有返回值 - if (signature instanceof MethodSignature && ((MethodSignature) signature).getReturnType() != void.class) { - builder.append(" = "); - builder.append(result.toString()); - } - - log(log.value(), builder.toString()); - } - - private void log(String tag, String msg) { - Timber.tag(tag); - Timber.d(msg); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/aop/LogCut.java b/app/src/main/java/com/hjq/demo/aop/LogCut.java new file mode 100644 index 00000000..34a5d46c --- /dev/null +++ b/app/src/main/java/com/hjq/demo/aop/LogCut.java @@ -0,0 +1,84 @@ +package com.hjq.demo.aop; + +import android.annotation.SuppressLint; +import android.os.Looper; +import android.os.Trace; +import androidx.annotation.NonNull; +import com.flyjingfish.android_aop_annotation.ProceedJoinPoint; +import com.flyjingfish.android_aop_annotation.base.BasePointCut; +import java.util.concurrent.TimeUnit; +import timber.log.Timber; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/06 + * desc : 日志切面 + */ +public class LogCut implements BasePointCut { + + @Override + public Object invoke(@NonNull ProceedJoinPoint joinPoint, @NonNull Log anno) throws Throwable { + enterMethod(joinPoint, anno); + long startNanos = System.nanoTime(); + Object result = joinPoint.proceed(); + long stopNanos = System.nanoTime(); + exitMethod(joinPoint, anno, result, TimeUnit.NANOSECONDS.toMillis(stopNanos - startNanos)); + return result; + } + + @SuppressLint("UnclosedTrace") + private void enterMethod(ProceedJoinPoint joinPoint, Log log) { + String className = joinPoint.getTarget() != null ? joinPoint.getTarget().getClass().getName() : ""; + String methodName = joinPoint.getTargetMethod().getName(); + String[] parameterNames = null; + Object[] parameterValues = joinPoint.getArgs(); + + StringBuilder builder = getMethodLogInfo(className, methodName, parameterNames, parameterValues); + log(log.value(), builder.toString()); + final String section = builder.substring(2); + Trace.beginSection(section); + } + + @NonNull + private StringBuilder getMethodLogInfo(String className, String methodName, String[] parameterNames, Object[] parameterValues) { + StringBuilder builder = new StringBuilder("\u21E2 "); + builder.append(className).append(".").append(methodName).append('('); + if (parameterValues != null && parameterNames != null) { + for (int i = 0; i < parameterValues.length; i++) { + if (i > 0) { + builder.append(", "); + } + builder.append(parameterNames[i]).append('='); + builder.append(parameterValues[i]); + } + } + builder.append(')'); + if (Looper.myLooper() != Looper.getMainLooper()) { + builder.append(" [Thread:\"").append(Thread.currentThread().getName()).append("\"]"); + } + return builder; + } + + private void exitMethod(ProceedJoinPoint joinPoint, Log log, Object result, long lengthMillis) { + Trace.endSection(); + String className = joinPoint.getTarget() != null ? joinPoint.getTarget().getClass().getName() : ""; + String methodName = joinPoint.getTargetMethod().getName(); + StringBuilder builder = new StringBuilder("\u21E0 ") + .append(className) + .append('.') + .append(methodName) + .append(" [") + .append(lengthMillis) + .append("ms]"); + if (result != null) { + builder.append(" = ").append(result); + } + log(log.value(), builder.toString()); + } + + private void log(String tag, String msg) { + Timber.tag(tag); + Timber.d(msg); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/aop/Permissions.java b/app/src/main/java/com/hjq/demo/aop/Permissions.java deleted file mode 100644 index c7b6a5d1..00000000 --- a/app/src/main/java/com/hjq/demo/aop/Permissions.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.hjq.demo.aop; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2019/12/06 - * desc : 权限申请注解 - */ -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD}) -public @interface Permissions { - - /** - * 需要申请权限的集合 - */ - String[] value(); -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/aop/PermissionsAspect.java b/app/src/main/java/com/hjq/demo/aop/PermissionsAspect.java deleted file mode 100644 index 09b7d1cc..00000000 --- a/app/src/main/java/com/hjq/demo/aop/PermissionsAspect.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.hjq.demo.aop; - -import android.app.Activity; - -import com.hjq.demo.manager.ActivityManager; -import com.hjq.demo.other.PermissionCallback; -import com.hjq.permissions.XXPermissions; -import com.tencent.bugly.crashreport.CrashReport; - -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.Around; -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.annotation.Pointcut; - -import java.util.List; - -import timber.log.Timber; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2019/12/06 - * desc : 权限申请切面 - */ -@Aspect -public class PermissionsAspect { - - /** - * 方法切入点 - */ - @Pointcut("execution(@com.hjq.demo.aop.Permissions * *(..))") - public void method() {} - - /** - * 在连接点进行方法替换 - */ - @Around("method() && @annotation(permissions)") - public void aroundJoinPoint(ProceedingJoinPoint joinPoint, Permissions permissions) { - Activity activity = null; - - // 方法参数值集合 - Object[] parameterValues = joinPoint.getArgs(); - for (Object arg : parameterValues) { - if (!(arg instanceof Activity)) { - continue; - } - activity = (Activity) arg; - break; - } - - if (activity == null || activity.isFinishing() || activity.isDestroyed()) { - activity = ActivityManager.getInstance().getTopActivity(); - } - - if (activity == null || activity.isFinishing() || activity.isDestroyed()) { - Timber.e("The activity has been destroyed and permission requests cannot be made"); - return; - } - - requestPermissions(joinPoint, activity, permissions.value()); - } - - private void requestPermissions(ProceedingJoinPoint joinPoint, Activity activity, String[] permissions) { - XXPermissions.with(activity) - .permission(permissions) - .request(new PermissionCallback() { - - @Override - public void onGranted(List permissions, boolean all) { - if (all) { - try { - // 获得权限,执行原方法 - joinPoint.proceed(); - } catch (Throwable e) { - CrashReport.postCatchedException(e); - } - } - } - }); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/aop/SingleClick.java b/app/src/main/java/com/hjq/demo/aop/SingleClick.java index e27372f6..6bc902c2 100644 --- a/app/src/main/java/com/hjq/demo/aop/SingleClick.java +++ b/app/src/main/java/com/hjq/demo/aop/SingleClick.java @@ -1,5 +1,6 @@ package com.hjq.demo.aop; +import com.flyjingfish.android_aop_annotation.anno.AndroidAopPointCut; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -13,6 +14,7 @@ */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) +@AndroidAopPointCut(SingleClickCut.class) public @interface SingleClick { /** diff --git a/app/src/main/java/com/hjq/demo/aop/SingleClickAspect.java b/app/src/main/java/com/hjq/demo/aop/SingleClickAspect.java deleted file mode 100644 index fb12e5a3..00000000 --- a/app/src/main/java/com/hjq/demo/aop/SingleClickAspect.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.hjq.demo.aop; - -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.Around; -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.annotation.Pointcut; -import org.aspectj.lang.reflect.CodeSignature; - -import timber.log.Timber; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2019/12/06 - * desc : 防重复点击切面 - */ -@Aspect -public class SingleClickAspect { - - /** 最近一次点击的时间 */ - private long mLastTime; - - /** 最近一次点击的标记 */ - private String mLastTag; - - /** - * 方法切入点 - */ - @Pointcut("execution(@com.hjq.demo.aop.SingleClick * *(..))") - public void method() {} - - /** - * 在连接点进行方法替换 - */ - @Around("method() && @annotation(singleClick)") - public void aroundJoinPoint(ProceedingJoinPoint joinPoint, SingleClick singleClick) throws Throwable { - CodeSignature codeSignature = (CodeSignature) joinPoint.getSignature(); - // 方法所在类 - String className = codeSignature.getDeclaringType().getName(); - // 方法名 - String methodName = codeSignature.getName(); - // 构建方法 TAG - StringBuilder builder = new StringBuilder(className + "." + methodName); - builder.append("("); - Object[] parameterValues = joinPoint.getArgs(); - for (int i = 0; i < parameterValues.length; i++) { - Object arg = parameterValues[i]; - if (i == 0) { - builder.append(arg); - } else { - builder.append(", ") - .append(arg); - } - } - builder.append(")"); - - String tag = builder.toString(); - long currentTimeMillis = System.currentTimeMillis(); - if (currentTimeMillis - mLastTime < singleClick.value() && tag.equals(mLastTag)) { - Timber.tag("SingleClick"); - Timber.i("%s 毫秒内发生快速点击:%s", singleClick.value(), tag); - return; - } - mLastTime = currentTimeMillis; - mLastTag = tag; - // 执行原方法 - joinPoint.proceed(); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/aop/SingleClickCut.java b/app/src/main/java/com/hjq/demo/aop/SingleClickCut.java new file mode 100644 index 00000000..1ff65982 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/aop/SingleClickCut.java @@ -0,0 +1,49 @@ +package com.hjq.demo.aop; + +import androidx.annotation.NonNull; +import com.flyjingfish.android_aop_annotation.ProceedJoinPoint; +import com.flyjingfish.android_aop_annotation.base.BasePointCut; +import org.jetbrains.annotations.Nullable; +import timber.log.Timber; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2019/12/06 + * desc : 防重复点击切面 + */ +public class SingleClickCut implements BasePointCut { + + private static long lastTime; + + @Nullable + private static String lastTag; + + @Override + public Object invoke(@NonNull ProceedJoinPoint joinPoint, @NonNull SingleClick anno) throws Throwable { + String className = joinPoint.getTarget() != null ? joinPoint.getTarget().getClass().getName() : ""; + String methodName = joinPoint.getTargetMethod().getName(); + Object[] parameterValues = joinPoint.getArgs(); + + StringBuilder builder = new StringBuilder(className).append(".").append(methodName).append("("); + for (int i = 0; i < (parameterValues != null ? parameterValues.length : 0); i++) { + if (i == 0) { + builder.append(parameterValues[i]); + } else { + builder.append(", ").append(parameterValues[i]); + } + } + builder.append(")"); + String tag = builder.toString(); + + long now = System.currentTimeMillis(); + if (now - lastTime < anno.value() && tag.equals(lastTag)) { + Timber.tag("SingleClick"); + Timber.i("%d 毫秒内发生快速点击:%s", anno.value(), tag); + return null; + } + lastTime = now; + lastTag = tag; + return joinPoint.proceed(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/app/AppActivity.java b/app/src/main/java/com/hjq/demo/app/AppActivity.java index 0669b74a..53b7db67 100644 --- a/app/src/main/java/com/hjq/demo/app/AppActivity.java +++ b/app/src/main/java/com/hjq/demo/app/AppActivity.java @@ -1,25 +1,26 @@ package com.hjq.demo.app; import android.content.Intent; -import android.os.Bundle; +import android.graphics.Insets; import android.view.View; - +import android.view.View.OnApplyWindowInsetsListener; +import android.view.WindowInsets; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; - import com.gyf.immersionbar.ImmersionBar; import com.hjq.bar.TitleBar; import com.hjq.base.BaseActivity; -import com.hjq.base.BaseDialog; +import com.hjq.core.tools.AndroidVersion; import com.hjq.demo.R; +import com.hjq.demo.action.ImmersionAction; import com.hjq.demo.action.TitleBarAction; import com.hjq.demo.action.ToastAction; import com.hjq.demo.http.model.HttpData; -import com.hjq.demo.ui.dialog.WaitDialog; +import com.hjq.demo.ui.dialog.common.WaitDialog; +import com.hjq.http.config.IRequestApi; import com.hjq.http.listener.OnHttpListener; - -import okhttp3.Call; +import com.hjq.umeng.sdk.UmengClient; /** * author : Android 轮子哥 @@ -28,7 +29,7 @@ * desc : Activity 业务基类 */ public abstract class AppActivity extends BaseActivity - implements ToastAction, TitleBarAction, OnHttpListener { + implements ToastAction, TitleBarAction, ImmersionAction, OnHttpListener { /** 标题栏对象 */ private TitleBar mTitleBar; @@ -36,7 +37,7 @@ public abstract class AppActivity extends BaseActivity private ImmersionBar mImmersionBar; /** 加载对话框 */ - private BaseDialog mDialog; + private WaitDialog.Builder mDialog; /** 对话框数量 */ private int mDialogCount; @@ -50,7 +51,11 @@ public boolean isShowDialog() { /** * 显示加载对话框 */ - public void showDialog() { + public void showLoadingDialog() { + showLoadingDialog(getString(R.string.common_loading)); + } + + public void showLoadingDialog(String message) { if (isFinishing() || isDestroyed()) { return; } @@ -63,9 +68,9 @@ public void showDialog() { if (mDialog == null) { mDialog = new WaitDialog.Builder(this) - .setCancelable(false) - .create(); + .setCancelable(false); } + mDialog.setMessage(message); if (!mDialog.isShowing()) { mDialog.show(); } @@ -75,7 +80,7 @@ public void showDialog() { /** * 隐藏加载对话框 */ - public void hideDialog() { + public void hideLoadingDialog() { if (isFinishing() || isDestroyed()) { return; } @@ -95,17 +100,46 @@ public void hideDialog() { protected void initLayout() { super.initLayout(); - if (getTitleBar() != null) { - getTitleBar().setOnTitleBarListener(this); + TitleBar titleBar = acquireTitleBar(); + if (titleBar != null) { + titleBar.setOnTitleBarListener(this); } // 初始化沉浸式状态栏 if (isStatusBarEnabled()) { getStatusBarConfig().init(); + } - // 设置标题栏沉浸 - if (getTitleBar() != null) { - ImmersionBar.setTitleBar(this, getTitleBar()); + // 适配 Android 15 EdgeToEdge 特性 + if (AndroidVersion.isAndroid15()) { + getWindow().getDecorView().setOnApplyWindowInsetsListener(new OnApplyWindowInsetsListener() { + + @NonNull + @Override + public WindowInsets onApplyWindowInsets(@NonNull View v, @NonNull WindowInsets insets) { + Insets systemBars = insets.getInsets(WindowInsets.Type.systemBars()); + View immersionTopView = getImmersionTopView(); + View immersionBottomView = getImmersionBottomView(); + if (immersionTopView != null && immersionTopView == immersionBottomView) { + immersionTopView.setPadding(immersionTopView.getPaddingLeft(), systemBars.top, + immersionTopView.getPaddingRight(), systemBars.bottom); + return insets; + } + if (immersionTopView != null) { + immersionTopView.setPadding(immersionTopView.getPaddingLeft(), systemBars.top, + immersionTopView.getPaddingRight(), immersionTopView.getPaddingBottom()); + } + if (immersionBottomView != null) { + immersionBottomView.setPadding(immersionBottomView.getPaddingLeft(), immersionBottomView.getPaddingTop(), + immersionBottomView.getPaddingRight(), systemBars.bottom); + } + return insets; + } + }); + } else { + View immersionTopView = getImmersionTopView(); + if (immersionTopView != null) { + ImmersionBar.setTitleBar(this, immersionTopView); } } } @@ -163,35 +197,33 @@ public void setTitle(@StringRes int id) { @Override public void setTitle(CharSequence title) { super.setTitle(title); - if (getTitleBar() != null) { - getTitleBar().setTitle(title); + TitleBar titleBar = acquireTitleBar(); + if (titleBar != null) { + titleBar.setTitle(title); } } - @Override @Nullable - public TitleBar getTitleBar() { + @Override + public TitleBar acquireTitleBar() { if (mTitleBar == null) { - mTitleBar = obtainTitleBar(getContentView()); + mTitleBar = findTitleBar(getContentView()); } return mTitleBar; } + /** + * 获取需要沉浸的顶部 View 对象 + */ + @Nullable @Override - public void onLeftClick(View view) { - onBackPressed(); - } - - @Override - public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) { - super.startActivityForResult(intent, requestCode, options); - overridePendingTransition(R.anim.right_in_activity, R.anim.right_out_activity); + public View getImmersionTopView() { + return acquireTitleBar(); } @Override - public void finish() { - super.finish(); - overridePendingTransition(R.anim.left_in_activity, R.anim.left_out_activity); + public void onLeftClick(TitleBar titleBar) { + onBackPressed(); } /** @@ -199,33 +231,40 @@ public void finish() { */ @Override - public void onStart(Call call) { - showDialog(); + public void onHttpStart(@NonNull IRequestApi api) { + showLoadingDialog(); } @Override - public void onSucceed(Object result) { + public void onHttpSuccess(@NonNull Object result) { if (result instanceof HttpData) { toast(((HttpData) result).getMessage()); } } @Override - public void onFail(Exception e) { - toast(e.getMessage()); + public void onHttpFail(@NonNull Throwable throwable) { + toast(throwable.getMessage()); } @Override - public void onEnd(Call call) { - hideDialog(); + public void onHttpEnd(@NonNull IRequestApi api) { + hideLoadingDialog(); } @Override protected void onDestroy() { super.onDestroy(); if (isShowDialog()) { - hideDialog(); + hideLoadingDialog(); } mDialog = null; } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + // 友盟回调 + UmengClient.onActivityResult(this, requestCode, resultCode, data); + } } \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/app/AppAdapter.java b/app/src/main/java/com/hjq/demo/app/AppAdapter.java index 7a02a725..6d370f09 100644 --- a/app/src/main/java/com/hjq/demo/app/AppAdapter.java +++ b/app/src/main/java/com/hjq/demo/app/AppAdapter.java @@ -1,15 +1,15 @@ package com.hjq.demo.app; +import android.annotation.SuppressLint; import android.content.Context; import android.view.View; - import androidx.annotation.IntRange; import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; - +import androidx.recyclerview.widget.RecyclerView; import com.hjq.base.BaseAdapter; - +import com.hjq.custom.widget.layout.WrapRecyclerView; import java.util.ArrayList; import java.util.List; @@ -20,14 +20,18 @@ * time : 2018/12/19 * desc : RecyclerView 适配器业务基类 */ -public abstract class AppAdapter extends BaseAdapter.ViewHolder> { +public abstract class AppAdapter extends BaseAdapter.AppViewHolder> { /** 列表数据 */ - private List mDataSet; + @NonNull + private List mDataSet = new ArrayList<>(); + /** 当前列表的页码,默认为第一页,用于分页加载功能 */ private int mPageNumber = 1; + /** 是否是最后一页,默认为false,用于分页加载功能 */ private boolean mLastPage; + /** 标记对象 */ private Object mTag; @@ -44,24 +48,26 @@ public int getItemCount() { * 获取数据总数 */ public int getCount() { - if (mDataSet == null) { - return 0; - } return mDataSet.size(); } /** * 设置新的数据 */ + @SuppressLint("NotifyDataSetChanged") public void setData(@Nullable List data) { - mDataSet = data; + if (data == null) { + mDataSet.clear(); + } else { + mDataSet = data; + } notifyDataSetChanged(); } /** * 获取当前数据 */ - @Nullable + @NonNull public List getData() { return mDataSet; } @@ -70,12 +76,7 @@ public List getData() { * 追加一些数据 */ public void addData(List data) { - if (data == null || data.size() == 0) { - return; - } - - if (mDataSet == null || mDataSet.size() == 0) { - setData(data); + if (data == null || data.isEmpty()) { return; } @@ -86,11 +87,8 @@ public void addData(List data) { /** * 清空当前数据 */ + @SuppressLint("NotifyDataSetChanged") public void clearData() { - if (mDataSet == null || mDataSet.size() == 0) { - return; - } - mDataSet.clear(); notifyDataSetChanged(); } @@ -106,7 +104,7 @@ public boolean containsItem(@IntRange(from = 0) int position) { * 是否包含某个条目数据 */ public boolean containsItem(T item) { - if (mDataSet == null || item == null) { + if (item == null) { return false; } return mDataSet.contains(item); @@ -116,9 +114,6 @@ public boolean containsItem(T item) { * 获取某个位置上的数据 */ public T getItem(@IntRange(from = 0) int position) { - if (mDataSet == null) { - return null; - } return mDataSet.get(position); } @@ -126,9 +121,6 @@ public T getItem(@IntRange(from = 0) int position) { * 更新某个位置上的数据 */ public void setItem(@IntRange(from = 0) int position, @NonNull T item) { - if (mDataSet == null) { - mDataSet = new ArrayList<>(); - } mDataSet.set(position, item); notifyItemChanged(position); } @@ -137,17 +129,10 @@ public void setItem(@IntRange(from = 0) int position, @NonNull T item) { * 添加单条数据 */ public void addItem(@NonNull T item) { - if (mDataSet == null) { - mDataSet = new ArrayList<>(); - } addItem(mDataSet.size(), item); } public void addItem(@IntRange(from = 0) int position, @NonNull T item) { - if (mDataSet == null) { - mDataSet = new ArrayList<>(); - } - if (position < mDataSet.size()) { mDataSet.add(position, item); } else { @@ -196,8 +181,8 @@ public boolean isLastPage() { /** * 设置是否为最后一页 */ - public void setLastPage(boolean last) { - mLastPage = last; + public void setLastPage(boolean lastPage) { + mLastPage = lastPage; } /** @@ -215,17 +200,41 @@ public void setTag(@NonNull Object tag) { mTag = tag; } - public final class SimpleHolder extends ViewHolder { + public abstract class AppViewHolder extends BaseAdapter.BaseViewHolder { + + public AppViewHolder(@LayoutRes int id) { + super(id); + } + + public AppViewHolder(View itemView) { + super(itemView); + } + + @Override + protected int getViewHolderPosition() { + int position = super.getViewHolderPosition(); + RecyclerView recyclerView = getRecyclerView(); + if (recyclerView instanceof WrapRecyclerView) { + // 这里要减去头部的数量 + position -= ((WrapRecyclerView) recyclerView).getHeaderViewsCount(); + } + return position; + } + } + + public final class SimpleViewHolder extends AppViewHolder { - public SimpleHolder(@LayoutRes int id) { + public SimpleViewHolder(@LayoutRes int id) { super(id); } - public SimpleHolder(View itemView) { + public SimpleViewHolder(View itemView) { super(itemView); } @Override - public void onBindView(int position) {} + public void onBindView(int position) { + // default implementation ignored + } } } \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/app/AppApplication.java b/app/src/main/java/com/hjq/demo/app/AppApplication.java index 8662e829..929d62cd 100644 --- a/app/src/main/java/com/hjq/demo/app/AppApplication.java +++ b/app/src/main/java/com/hjq/demo/app/AppApplication.java @@ -1,47 +1,10 @@ package com.hjq.demo.app; -import android.app.Activity; import android.app.Application; -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.Network; -import android.os.Build; - -import androidx.annotation.NonNull; -import androidx.core.content.ContextCompat; -import androidx.lifecycle.Lifecycle; -import androidx.lifecycle.LifecycleOwner; - -import com.hjq.bar.TitleBar; -import com.hjq.demo.R; +import com.hjq.core.manager.ActivityManager; import com.hjq.demo.aop.Log; import com.hjq.demo.http.glide.GlideApp; -import com.hjq.demo.http.model.RequestHandler; -import com.hjq.demo.http.model.RequestServer; -import com.hjq.demo.manager.ActivityManager; -import com.hjq.demo.other.AppConfig; -import com.hjq.demo.other.CrashHandler; -import com.hjq.demo.other.DebugLoggerTree; -import com.hjq.demo.other.MaterialHeader; -import com.hjq.demo.other.SmartBallPulseFooter; -import com.hjq.demo.other.TitleBarStyle; -import com.hjq.demo.other.ToastLogInterceptor; -import com.hjq.demo.other.ToastStyle; -import com.hjq.gson.factory.GsonFactory; -import com.hjq.http.EasyConfig; -import com.hjq.http.config.IRequestApi; -import com.hjq.http.config.IRequestInterceptor; -import com.hjq.http.model.HttpHeaders; -import com.hjq.http.model.HttpParams; -import com.hjq.permissions.XXPermissions; -import com.hjq.toast.ToastUtils; -import com.hjq.umeng.UmengClient; -import com.scwang.smart.refresh.layout.SmartRefreshLayout; -import com.tencent.bugly.crashreport.CrashReport; -import com.tencent.mmkv.MMKV; - -import okhttp3.OkHttpClient; -import timber.log.Timber; +import com.hjq.demo.manager.InitManager; /** * author : Android 轮子哥 @@ -55,12 +18,16 @@ public final class AppApplication extends Application { @Override public void onCreate() { super.onCreate(); - initSdk(this); - } - @Override - protected void attachBaseContext(Context base) { - super.attachBaseContext(base); + // 如果当前的进程不是主进程的话,则不进行第三方框架的初始化 + if (!ActivityManager.isMainProcess(this)) { + return; + } + + InitManager.preInitSdk(this); + if (InitManager.isAgreePrivacy(this)) { + InitManager.initSdk(this); + } } @Override @@ -76,110 +43,4 @@ public void onTrimMemory(int level) { // 根据手机内存剩余情况清理图片内存缓存 GlideApp.get(this).onTrimMemory(level); } - - /** - * 初始化一些第三方框架 - */ - public static void initSdk(Application application) { - // 设置标题栏初始化器 - TitleBar.setDefaultStyle(new TitleBarStyle()); - - // 设置全局的 Header 构建器 - SmartRefreshLayout.setDefaultRefreshHeaderCreator((cx, layout) -> - new MaterialHeader(application).setColorSchemeColors(ContextCompat.getColor(application, R.color.common_accent_color))); - // 设置全局的 Footer 构建器 - SmartRefreshLayout.setDefaultRefreshFooterCreator((cx, layout) -> new SmartBallPulseFooter(application)); - // 设置全局初始化器 - SmartRefreshLayout.setDefaultRefreshInitializer((cx, layout) -> { - // 刷新头部是否跟随内容偏移 - layout.setEnableHeaderTranslationContent(true) - // 刷新尾部是否跟随内容偏移 - .setEnableFooterTranslationContent(true) - // 加载更多是否跟随内容偏移 - .setEnableFooterFollowWhenNoMoreData(true) - // 内容不满一页时是否可以上拉加载更多 - .setEnableLoadMoreWhenContentNotFull(false) - // 仿苹果越界效果开关 - .setEnableOverScrollDrag(false); - }); - - // 初始化吐司 - ToastUtils.init(application, new ToastStyle()); - // 设置调试模式 - ToastUtils.setDebugMode(AppConfig.isDebug()); - // 设置 Toast 拦截器 - ToastUtils.setInterceptor(new ToastLogInterceptor()); - - // 本地异常捕捉 - CrashHandler.register(application); - - // 友盟统计、登录、分享 SDK - UmengClient.init(application, AppConfig.isLogEnable()); - - // Bugly 异常捕捉 - CrashReport.initCrashReport(application, AppConfig.getBuglyId(), AppConfig.isDebug()); - - // Activity 栈管理初始化 - ActivityManager.getInstance().init(application); - - // MMKV 初始化 - MMKV.initialize(application); - - // 网络请求框架初始化 - OkHttpClient okHttpClient = new OkHttpClient.Builder() - .build(); - - EasyConfig.with(okHttpClient) - // 是否打印日志 - .setLogEnabled(AppConfig.isLogEnable()) - // 设置服务器配置 - .setServer(new RequestServer()) - // 设置请求处理策略 - .setHandler(new RequestHandler(application)) - // 设置请求重试次数 - .setRetryCount(1) - .setInterceptor((api, params, headers) -> { - // 添加全局请求头 - headers.put("token", "66666666666"); - headers.put("deviceOaid", UmengClient.getDeviceOaid()); - headers.put("versionName", AppConfig.getVersionName()); - headers.put("versionCode", String.valueOf(AppConfig.getVersionCode())); - // 添加全局请求参数 - // params.put("6666666", "6666666"); - }) - .into(); - - // 设置 Json 解析容错监听 - GsonFactory.setJsonCallback((typeToken, fieldName, jsonToken) -> { - // 上报到 Bugly 错误列表 - CrashReport.postCatchedException(new IllegalArgumentException( - "类型解析异常:" + typeToken + "#" + fieldName + ",后台返回的类型为:" + jsonToken)); - }); - - // 初始化日志打印 - if (AppConfig.isLogEnable()) { - Timber.plant(new DebugLoggerTree()); - } - - // 注册网络状态变化监听 - ConnectivityManager connectivityManager = ContextCompat.getSystemService(application, ConnectivityManager.class); - if (connectivityManager != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - connectivityManager.registerDefaultNetworkCallback(new ConnectivityManager.NetworkCallback() { - @Override - public void onLost(@NonNull Network network) { - Activity topActivity = ActivityManager.getInstance().getTopActivity(); - if (!(topActivity instanceof LifecycleOwner)) { - return; - } - - LifecycleOwner lifecycleOwner = ((LifecycleOwner) topActivity); - if (lifecycleOwner.getLifecycle().getCurrentState() != Lifecycle.State.RESUMED) { - return; - } - - ToastUtils.show(R.string.common_network_error); - } - }); - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/app/AppFragment.java b/app/src/main/java/com/hjq/demo/app/AppFragment.java index d2ceeea4..2157d89e 100644 --- a/app/src/main/java/com/hjq/demo/app/AppFragment.java +++ b/app/src/main/java/com/hjq/demo/app/AppFragment.java @@ -1,12 +1,12 @@ package com.hjq.demo.app; +import androidx.annotation.NonNull; import com.hjq.base.BaseFragment; import com.hjq.demo.action.ToastAction; import com.hjq.demo.http.model.HttpData; +import com.hjq.http.config.IRequestApi; import com.hjq.http.listener.OnHttpListener; -import okhttp3.Call; - /** * author : Android 轮子哥 * github : https://github.com/getActivity/AndroidProject @@ -30,23 +30,23 @@ public boolean isShowDialog() { /** * 显示加载对话框 */ - public void showDialog() { + public void showLoadingDialog() { A activity = getAttachActivity(); if (activity == null) { return; } - activity.showDialog(); + activity.showLoadingDialog(); } /** * 隐藏加载对话框 */ - public void hideDialog() { + public void hideLoadingDialog() { A activity = getAttachActivity(); if (activity == null) { return; } - activity.hideDialog(); + activity.hideLoadingDialog(); } /** @@ -54,12 +54,12 @@ public void hideDialog() { */ @Override - public void onStart(Call call) { - showDialog(); + public void onHttpStart(@NonNull IRequestApi api) { + showLoadingDialog(); } @Override - public void onSucceed(Object result) { + public void onHttpSuccess(@NonNull Object result) { if (!(result instanceof HttpData)) { return; } @@ -67,12 +67,12 @@ public void onSucceed(Object result) { } @Override - public void onFail(Exception e) { - toast(e.getMessage()); + public void onHttpFail(@NonNull Throwable throwable) { + toast(throwable.getMessage()); } @Override - public void onEnd(Call call) { - hideDialog(); + public void onHttpEnd(@NonNull IRequestApi api) { + hideLoadingDialog(); } } \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/app/TitleBarFragment.java b/app/src/main/java/com/hjq/demo/app/TitleBarFragment.java index bb393bba..7f6212ff 100644 --- a/app/src/main/java/com/hjq/demo/app/TitleBarFragment.java +++ b/app/src/main/java/com/hjq/demo/app/TitleBarFragment.java @@ -1,15 +1,17 @@ package com.hjq.demo.app; +import android.graphics.Insets; import android.os.Bundle; import android.view.View; -import android.view.ViewGroup; - +import android.view.View.OnApplyWindowInsetsListener; +import android.view.WindowInsets; import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import com.gyf.immersionbar.ImmersionBar; import com.hjq.bar.TitleBar; +import com.hjq.core.tools.AndroidVersion; import com.hjq.demo.R; +import com.hjq.demo.action.ImmersionAction; import com.hjq.demo.action.TitleBarAction; /** @@ -18,8 +20,8 @@ * time : 2020/10/31 * desc : 带标题栏的 Fragment 业务基类 */ -public abstract class TitleBarFragment extends AppFragment - implements TitleBarAction { +public abstract class TitleBarFragment + extends AppFragment implements TitleBarAction, ImmersionAction { /** 标题栏对象 */ private TitleBar mTitleBar; @@ -31,17 +33,46 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat super.onViewCreated(view, savedInstanceState); // 设置标题栏点击监听 - if (getTitleBar() != null) { - getTitleBar().setOnTitleBarListener(this); + TitleBar titleBar = acquireTitleBar(); + if (titleBar != null) { + titleBar.setOnTitleBarListener(this); } if (isStatusBarEnabled()) { // 初始化沉浸式状态栏 getStatusBarConfig().init(); + } - if (getTitleBar() != null) { - // 设置标题栏沉浸 - ImmersionBar.setTitleBar(this, getTitleBar()); + // 适配 Android 15 EdgeToEdge 特性 + if (AndroidVersion.isAndroid15()) { + view.setOnApplyWindowInsetsListener(new OnApplyWindowInsetsListener() { + + @NonNull + @Override + public WindowInsets onApplyWindowInsets(@NonNull View v, @NonNull WindowInsets insets) { + Insets systemBars = insets.getInsets(WindowInsets.Type.systemBars()); + View immersionTopView = getImmersionTopView(); + View immersionBottomView = getImmersionBottomView(); + if (immersionTopView != null && immersionTopView == immersionBottomView) { + immersionTopView.setPadding(immersionTopView.getPaddingLeft(), systemBars.top, + immersionTopView.getPaddingRight(), systemBars.bottom); + return insets; + } + if (immersionTopView != null) { + immersionTopView.setPadding(immersionTopView.getPaddingLeft(), systemBars.top, + immersionTopView.getPaddingRight(), immersionTopView.getPaddingBottom()); + } + if (immersionBottomView != null) { + immersionBottomView.setPadding(immersionBottomView.getPaddingLeft(), immersionBottomView.getPaddingTop(), + immersionBottomView.getPaddingRight(), systemBars.bottom); + } + return insets; + } + }); + } else { + View immersionTopView = getImmersionTopView(); + if (immersionTopView != null) { + ImmersionBar.setTitleBar(this, immersionTopView); } } } @@ -91,16 +122,26 @@ protected ImmersionBar createStatusBarConfig() { * 获取状态栏字体颜色 */ protected boolean isStatusBarDarkFont() { + A activity = getAttachActivity(); + if (activity == null) { + return false; + } // 返回真表示黑色字体 - return getAttachActivity().isStatusBarDarkFont(); + return activity.isStatusBarDarkFont(); } @Override @Nullable - public TitleBar getTitleBar() { + public TitleBar acquireTitleBar() { if (mTitleBar == null || !isLoading()) { - mTitleBar = obtainTitleBar((ViewGroup) getView()); + mTitleBar = findTitleBar(getView()); } return mTitleBar; } + + @Nullable + @Override + public View getImmersionTopView() { + return acquireTitleBar(); + } } \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/api/CopyApi.java b/app/src/main/java/com/hjq/demo/http/api/CopyApi.java index 81aad20b..e11b1f4a 100644 --- a/app/src/main/java/com/hjq/demo/http/api/CopyApi.java +++ b/app/src/main/java/com/hjq/demo/http/api/CopyApi.java @@ -1,5 +1,6 @@ package com.hjq.demo.http.api; +import androidx.annotation.NonNull; import com.hjq.http.config.IRequestApi; /** @@ -10,6 +11,7 @@ */ public final class CopyApi implements IRequestApi { + @NonNull @Override public String getApi() { return ""; diff --git a/app/src/main/java/com/hjq/demo/http/api/GetCodeApi.java b/app/src/main/java/com/hjq/demo/http/api/GetCodeApi.java index 01ab4825..1eee3606 100644 --- a/app/src/main/java/com/hjq/demo/http/api/GetCodeApi.java +++ b/app/src/main/java/com/hjq/demo/http/api/GetCodeApi.java @@ -1,5 +1,6 @@ package com.hjq.demo.http.api; +import androidx.annotation.NonNull; import com.hjq.http.config.IRequestApi; /** @@ -10,6 +11,7 @@ */ public final class GetCodeApi implements IRequestApi { + @NonNull @Override public String getApi() { return "code/get"; diff --git a/app/src/main/java/com/hjq/demo/http/api/LoginApi.java b/app/src/main/java/com/hjq/demo/http/api/LoginApi.java index 78b6c893..b0abf283 100644 --- a/app/src/main/java/com/hjq/demo/http/api/LoginApi.java +++ b/app/src/main/java/com/hjq/demo/http/api/LoginApi.java @@ -1,5 +1,6 @@ package com.hjq.demo.http.api; +import androidx.annotation.NonNull; import com.hjq.http.config.IRequestApi; /** @@ -10,6 +11,7 @@ */ public final class LoginApi implements IRequestApi { + @NonNull @Override public String getApi() { return "user/login"; diff --git a/app/src/main/java/com/hjq/demo/http/api/LogoutApi.java b/app/src/main/java/com/hjq/demo/http/api/LogoutApi.java index 1d62a70e..f025893c 100644 --- a/app/src/main/java/com/hjq/demo/http/api/LogoutApi.java +++ b/app/src/main/java/com/hjq/demo/http/api/LogoutApi.java @@ -1,5 +1,6 @@ package com.hjq.demo.http.api; +import androidx.annotation.NonNull; import com.hjq.http.config.IRequestApi; /** @@ -10,6 +11,7 @@ */ public final class LogoutApi implements IRequestApi { + @NonNull @Override public String getApi() { return "user/logout"; diff --git a/app/src/main/java/com/hjq/demo/http/api/PasswordApi.java b/app/src/main/java/com/hjq/demo/http/api/PasswordApi.java index 2593bd05..59b75b45 100644 --- a/app/src/main/java/com/hjq/demo/http/api/PasswordApi.java +++ b/app/src/main/java/com/hjq/demo/http/api/PasswordApi.java @@ -1,5 +1,6 @@ package com.hjq.demo.http.api; +import androidx.annotation.NonNull; import com.hjq.http.config.IRequestApi; /** @@ -10,6 +11,7 @@ */ public final class PasswordApi implements IRequestApi { + @NonNull @Override public String getApi() { return "user/password"; diff --git a/app/src/main/java/com/hjq/demo/http/api/PhoneApi.java b/app/src/main/java/com/hjq/demo/http/api/PhoneApi.java index 17f7ae1f..e881b6e0 100644 --- a/app/src/main/java/com/hjq/demo/http/api/PhoneApi.java +++ b/app/src/main/java/com/hjq/demo/http/api/PhoneApi.java @@ -1,5 +1,6 @@ package com.hjq.demo.http.api; +import androidx.annotation.NonNull; import com.hjq.http.config.IRequestApi; /** @@ -10,6 +11,7 @@ */ public final class PhoneApi implements IRequestApi { + @NonNull @Override public String getApi() { return "user/phone"; diff --git a/app/src/main/java/com/hjq/demo/http/api/RegisterApi.java b/app/src/main/java/com/hjq/demo/http/api/RegisterApi.java index 3c14fc40..f7e562ad 100644 --- a/app/src/main/java/com/hjq/demo/http/api/RegisterApi.java +++ b/app/src/main/java/com/hjq/demo/http/api/RegisterApi.java @@ -1,5 +1,6 @@ package com.hjq.demo.http.api; +import androidx.annotation.NonNull; import com.hjq.http.config.IRequestApi; /** @@ -10,6 +11,7 @@ */ public final class RegisterApi implements IRequestApi { + @NonNull @Override public String getApi() { return "user/register"; diff --git a/app/src/main/java/com/hjq/demo/http/api/UpdateImageApi.java b/app/src/main/java/com/hjq/demo/http/api/UpdateImageApi.java index e54c33bf..a27da66f 100644 --- a/app/src/main/java/com/hjq/demo/http/api/UpdateImageApi.java +++ b/app/src/main/java/com/hjq/demo/http/api/UpdateImageApi.java @@ -1,7 +1,7 @@ package com.hjq.demo.http.api; +import androidx.annotation.NonNull; import com.hjq.http.config.IRequestApi; - import java.io.File; /** @@ -12,6 +12,7 @@ */ public final class UpdateImageApi implements IRequestApi { + @NonNull @Override public String getApi() { return "update/image"; diff --git a/app/src/main/java/com/hjq/demo/http/api/UserInfoApi.java b/app/src/main/java/com/hjq/demo/http/api/UserInfoApi.java index 8aa37cb6..e7c33d0d 100644 --- a/app/src/main/java/com/hjq/demo/http/api/UserInfoApi.java +++ b/app/src/main/java/com/hjq/demo/http/api/UserInfoApi.java @@ -15,7 +15,7 @@ public String getApi() { return "user/info"; } - public final class Bean { + public final static class Bean { } } \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/api/VerifyCodeApi.java b/app/src/main/java/com/hjq/demo/http/api/VerifyCodeApi.java index 935cd599..4cff60b2 100644 --- a/app/src/main/java/com/hjq/demo/http/api/VerifyCodeApi.java +++ b/app/src/main/java/com/hjq/demo/http/api/VerifyCodeApi.java @@ -1,5 +1,6 @@ package com.hjq.demo.http.api; +import androidx.annotation.NonNull; import com.hjq.http.config.IRequestApi; /** @@ -10,6 +11,7 @@ */ public final class VerifyCodeApi implements IRequestApi { + @NonNull @Override public String getApi() { return "code/checkout"; diff --git a/app/src/main/java/com/hjq/demo/http/exception/ResultException.java b/app/src/main/java/com/hjq/demo/http/exception/ResultException.java new file mode 100644 index 00000000..24289522 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/exception/ResultException.java @@ -0,0 +1,32 @@ +package com.hjq.demo.http.exception; + +import androidx.annotation.NonNull; +import com.hjq.demo.http.model.HttpData; +import com.hjq.http.exception.HttpException; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2021/12/19 + * desc : 返回结果异常 + */ +public final class ResultException extends HttpException { + + @NonNull + private final HttpData mData; + + public ResultException(@NonNull String message, @NonNull HttpData data) { + super(message); + mData = data; + } + + public ResultException(@NonNull String message, @NonNull Throwable cause, @NonNull HttpData data) { + super(message, cause); + mData = data; + } + + @NonNull + public HttpData getHttpData() { + return mData; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/exception/TokenException.java b/app/src/main/java/com/hjq/demo/http/exception/TokenException.java new file mode 100644 index 00000000..da6b51dc --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/exception/TokenException.java @@ -0,0 +1,21 @@ +package com.hjq.demo.http.exception; + +import androidx.annotation.NonNull; +import com.hjq.http.exception.HttpException; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2021/12/19 + * desc : Token 失效异常 + */ +public final class TokenException extends HttpException { + + public TokenException(@NonNull String message) { + super(message); + } + + public TokenException(@NonNull String message, @NonNull Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/glide/GlideConfig.java b/app/src/main/java/com/hjq/demo/http/glide/GlideConfig.java index 2d554a77..672ade3e 100644 --- a/app/src/main/java/com/hjq/demo/http/glide/GlideConfig.java +++ b/app/src/main/java/com/hjq/demo/http/glide/GlideConfig.java @@ -1,9 +1,7 @@ package com.hjq.demo.http.glide; import android.content.Context; - import androidx.annotation.NonNull; - import com.bumptech.glide.Glide; import com.bumptech.glide.GlideBuilder; import com.bumptech.glide.Registry; @@ -17,7 +15,6 @@ import com.bumptech.glide.request.RequestOptions; import com.hjq.demo.R; import com.hjq.http.EasyConfig; - import java.io.File; import java.io.InputStream; @@ -41,12 +38,16 @@ public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder // 如果这个路径是一个文件 if (diskCacheFile.exists() && diskCacheFile.isFile()) { // 执行删除操作 + // noinspection ResultOfMethodCallIgnored diskCacheFile.delete(); } // 如果这个路径不存在 if (!diskCacheFile.exists()) { // 创建多级目录 - diskCacheFile.mkdirs(); + if (!diskCacheFile.mkdirs()) { + // 如果创建失败,并且文件夹不存在 + return; + } } builder.setDiskCache(() -> DiskLruCacheWrapper.create(diskCacheFile, IMAGE_DISK_CACHE_MAX_SIZE)); @@ -54,8 +55,8 @@ public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder int defaultMemoryCacheSize = calculator.getMemoryCacheSize(); int defaultBitmapPoolSize = calculator.getBitmapPoolSize(); - int customMemoryCacheSize = (int) (1.2 * defaultMemoryCacheSize); - int customBitmapPoolSize = (int) (1.2 * defaultBitmapPoolSize); + long customMemoryCacheSize = (long) (1.2 * defaultMemoryCacheSize); + long customBitmapPoolSize = (long) (1.2 * defaultBitmapPoolSize); builder.setMemoryCache(new LruResourceCache(customMemoryCacheSize)); builder.setBitmapPool(new LruBitmapPool(customBitmapPoolSize)); diff --git a/app/src/main/java/com/hjq/demo/http/glide/OkHttpFetcher.java b/app/src/main/java/com/hjq/demo/http/glide/OkHttpFetcher.java index 657f7d34..0287c7a5 100644 --- a/app/src/main/java/com/hjq/demo/http/glide/OkHttpFetcher.java +++ b/app/src/main/java/com/hjq/demo/http/glide/OkHttpFetcher.java @@ -1,7 +1,6 @@ package com.hjq.demo.http.glide; import androidx.annotation.NonNull; - import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.HttpException; @@ -9,11 +8,9 @@ import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.util.ContentLengthInputStream; import com.bumptech.glide.util.Preconditions; - import java.io.IOException; import java.io.InputStream; import java.util.Map; - import okhttp3.Call; import okhttp3.Callback; import okhttp3.Request; diff --git a/app/src/main/java/com/hjq/demo/http/glide/OkHttpLoader.java b/app/src/main/java/com/hjq/demo/http/glide/OkHttpLoader.java index 3afcb5c0..d15e1189 100644 --- a/app/src/main/java/com/hjq/demo/http/glide/OkHttpLoader.java +++ b/app/src/main/java/com/hjq/demo/http/glide/OkHttpLoader.java @@ -1,15 +1,12 @@ package com.hjq.demo.http.glide; import androidx.annotation.NonNull; - import com.bumptech.glide.load.Options; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoaderFactory; import com.bumptech.glide.load.model.MultiModelLoaderFactory; - import java.io.InputStream; - import okhttp3.Call; /** @@ -20,6 +17,7 @@ */ public final class OkHttpLoader implements ModelLoader { + @NonNull private final Call.Factory mFactory; private OkHttpLoader(@NonNull Call.Factory factory) { @@ -38,6 +36,7 @@ public LoadData buildLoadData(@NonNull GlideUrl model, int width, i public static class Factory implements ModelLoaderFactory { + @NonNull private final Call.Factory mFactory; Factory(@NonNull Call.Factory factory) { @@ -51,6 +50,8 @@ public ModelLoader build(@NonNull MultiModelLoaderFactory } @Override - public void teardown() {} + public void teardown() { + // default implementation ignored + } } } \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/model/HttpCacheManager.java b/app/src/main/java/com/hjq/demo/http/model/HttpCacheManager.java new file mode 100644 index 00000000..aaf059d3 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/model/HttpCacheManager.java @@ -0,0 +1,99 @@ +package com.hjq.demo.http.model; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.hjq.gson.factory.GsonFactory; +import com.hjq.http.config.IRequestApi; +import com.hjq.http.request.HttpRequest; +import com.tencent.mmkv.MMKV; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2022/03/22 + * desc : Http 缓存管理器 + */ +public final class HttpCacheManager { + + @NonNull + private static final MMKV HTTP_CACHE_CONTENT = MMKV.mmkvWithID("http_cache_content");; + + @NonNull + private static final MMKV HTTP_CACHE_TIME = MMKV.mmkvWithID("http_cache_time"); + + /** + * 生成缓存的 key + */ + @NonNull + public static String generateCacheKey(@NonNull HttpRequest httpRequest) { + IRequestApi requestApi = httpRequest.getRequestApi(); + return "请替换成当前的用户 id" + "\n" + requestApi.getApi() + "\n" + GsonFactory.getSingletonGson().toJson(requestApi); + } + + /** + * 读取缓存 + */ + @Nullable + public static String readHttpCache(@NonNull String cacheKey) { + String cacheValue = HTTP_CACHE_CONTENT.getString(cacheKey, null); + if (cacheValue == null || cacheValue.isEmpty() || "{}".equals(cacheValue)) { + return null; + } + return cacheValue; + } + + /** + * 写入缓存 + */ + public static boolean writeHttpCache(@NonNull String cacheKey, @NonNull String cacheValue) { + return HTTP_CACHE_CONTENT.putString(cacheKey, cacheValue).commit(); + } + + /** + * 删除缓存 + */ + public static boolean deleteHttpCache(@NonNull String cacheKey) { + return HTTP_CACHE_CONTENT.remove(cacheKey).commit(); + } + + /** + * 清理缓存 + */ + public static void clearCache() { + HTTP_CACHE_CONTENT.clearMemoryCache(); + HTTP_CACHE_CONTENT.clearAll(); + + HTTP_CACHE_TIME.clearMemoryCache(); + HTTP_CACHE_TIME.clearAll(); + } + + /** + * 获取 Http 写入缓存的时间 + */ + public static long getHttpCacheTime(@NonNull String cacheKey) { + return HTTP_CACHE_TIME.getLong(cacheKey, 0); + } + + /** + * 设置 Http 写入缓存的时间 + */ + public static boolean setHttpCacheTime(@NonNull String cacheKey, long cacheTime) { + return HTTP_CACHE_TIME.putLong(cacheKey, cacheTime).commit(); + } + + /** + * 判断缓存是否过期 + */ + public static boolean isCacheInvalidate(@NonNull String cacheKey, long maxCacheTime) { + if (maxCacheTime == Long.MAX_VALUE) { + // 表示缓存长期有效,永远不会过期 + return false; + } + long httpCacheTime = getHttpCacheTime(cacheKey); + if (httpCacheTime == 0) { + // 表示不知道缓存的时间,这里默认当做已经过期了 + return true; + } + return httpCacheTime + maxCacheTime < System.currentTimeMillis(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/model/HttpCacheStrategy.java b/app/src/main/java/com/hjq/demo/http/model/HttpCacheStrategy.java new file mode 100644 index 00000000..091b4205 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/http/model/HttpCacheStrategy.java @@ -0,0 +1,74 @@ +package com.hjq.demo.http.model; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.hjq.gson.factory.GsonFactory; +import com.hjq.http.EasyLog; +import com.hjq.http.config.IHttpCacheStrategy; +import com.hjq.http.request.HttpRequest; +import java.lang.reflect.Type; +import okhttp3.Response; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2025/03/23 + * desc : 请求缓存策略实现类 + */ +public final class HttpCacheStrategy implements IHttpCacheStrategy { + + @Nullable + @Override + public Object readCache(@NonNull HttpRequest httpRequest, @NonNull Type type, long cacheTime) { + String cacheKey = HttpCacheManager.generateCacheKey(httpRequest); + String cacheValue = HttpCacheManager.readHttpCache(cacheKey); + if (cacheValue == null || cacheValue.isEmpty() || "{}".equals(cacheValue)) { + return null; + } + EasyLog.printLog(httpRequest, "----- read cache key -----"); + EasyLog.printJson(httpRequest, cacheKey); + EasyLog.printLog(httpRequest, "----- read cache value -----"); + EasyLog.printJson(httpRequest, cacheValue); + EasyLog.printLog(httpRequest, "cacheTime = " + cacheTime); + boolean cacheInvalidate = HttpCacheManager.isCacheInvalidate(cacheKey, cacheTime); + EasyLog.printLog(httpRequest, "cacheInvalidate = " + cacheInvalidate); + if (cacheInvalidate) { + // 表示缓存已经过期了,直接返回 null 给外层,表示缓存不可用 + return null; + } + return GsonFactory.getSingletonGson().fromJson(cacheValue, type); + } + + @Override + public boolean writeCache(@NonNull HttpRequest httpRequest, @NonNull Response response, @NonNull Object result) { + String cacheKey = HttpCacheManager.generateCacheKey(httpRequest); + String cacheValue = GsonFactory.getSingletonGson().toJson(result); + if (cacheValue == null || cacheValue.isEmpty() || "{}".equals(cacheValue)) { + return false; + } + EasyLog.printLog(httpRequest, "----- write cache key -----"); + EasyLog.printJson(httpRequest, cacheKey); + EasyLog.printLog(httpRequest, "----- write cache value -----"); + EasyLog.printJson(httpRequest, cacheValue); + boolean writeHttpCacheResult = HttpCacheManager.writeHttpCache(cacheKey, cacheValue); + EasyLog.printLog(httpRequest, "writeHttpCacheResult = " + writeHttpCacheResult); + boolean refreshHttpCacheTimeResult = HttpCacheManager.setHttpCacheTime(cacheKey, System.currentTimeMillis()); + EasyLog.printLog(httpRequest, "refreshHttpCacheTimeResult = " + refreshHttpCacheTimeResult); + return writeHttpCacheResult && refreshHttpCacheTimeResult; + } + + @Override + public boolean deleteCache(@NonNull HttpRequest httpRequest) { + String cacheKey = HttpCacheManager.generateCacheKey(httpRequest); + EasyLog.printLog(httpRequest, "----- delete cache key -----"); + EasyLog.printJson(httpRequest, cacheKey); + boolean deleteHttpCacheResult = HttpCacheManager.deleteHttpCache(cacheKey); + EasyLog.printLog(httpRequest, "deleteHttpCacheResult = " + deleteHttpCacheResult); + return deleteHttpCacheResult; + } + + @Override + public void clearCache() { + HttpCacheManager.clearCache(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/model/HttpData.java b/app/src/main/java/com/hjq/demo/http/model/HttpData.java index 802f6f4d..8a1fa7d5 100644 --- a/app/src/main/java/com/hjq/demo/http/model/HttpData.java +++ b/app/src/main/java/com/hjq/demo/http/model/HttpData.java @@ -1,5 +1,9 @@ package com.hjq.demo.http.model; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import java.util.Map; + /** * author : Android 轮子哥 * github : https://github.com/getActivity/AndroidProject @@ -8,21 +12,43 @@ */ public class HttpData { + /** 响应头 */ + @Nullable + private Map responseHeaders; + /** 返回码 */ private int code; + /** 提示语 */ + @Nullable private String msg; + /** 数据 */ + @Nullable private T data; + public void setResponseHeaders(@Nullable Map responseHeaders) { + this.responseHeaders = responseHeaders; + } + + @Nullable + public Map getResponseHeaders() { + return responseHeaders; + } + public int getCode() { return code; } + @NonNull public String getMessage() { + if (msg == null) { + return ""; + } return msg; } + @Nullable public T getData() { return data; } @@ -30,14 +56,14 @@ public T getData() { /** * 是否请求成功 */ - public boolean isRequestSucceed() { + public boolean isRequestSuccess() { return code == 200; } /** * 是否 Token 失效 */ - public boolean isTokenFailure() { + public boolean isTokenInvalidation() { return code == 1001; } } \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/model/HttpListData.java b/app/src/main/java/com/hjq/demo/http/model/HttpListData.java index 134b76ca..472c2430 100644 --- a/app/src/main/java/com/hjq/demo/http/model/HttpListData.java +++ b/app/src/main/java/com/hjq/demo/http/model/HttpListData.java @@ -1,10 +1,11 @@ package com.hjq.demo.http.model; +import androidx.annotation.Nullable; import java.util.List; /** * author : Android 轮子哥 - * github : https://github.com/getActivity/EasyHttp + * github : https://github.com/getActivity/AndroidProject * time : 2020/10/07 * desc : 统一接口列表数据结构 */ @@ -19,12 +20,20 @@ public static class ListBean { /** 总数量 */ private int totalNumber; /** 数据 */ + @Nullable private List items; /** * 判断是否是最后一页 */ public boolean isLastPage() { + if (items == null) { + return true; + } + if (pageSize == 0) { + // 避免出现除零异常 + return true; + } return Math.ceil((float) totalNumber / pageSize) <= pageIndex; } @@ -40,6 +49,7 @@ public int getPageSize() { return pageSize; } + @Nullable public List getItems() { return items; } diff --git a/app/src/main/java/com/hjq/demo/http/model/RequestHandler.java b/app/src/main/java/com/hjq/demo/http/model/RequestHandler.java index 0326dcdd..9ee53b00 100644 --- a/app/src/main/java/com/hjq/demo/http/model/RequestHandler.java +++ b/app/src/main/java/com/hjq/demo/http/model/RequestHandler.java @@ -3,40 +3,36 @@ import android.app.Application; import android.content.Context; import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.net.ConnectivityManager; import android.net.NetworkInfo; - -import androidx.lifecycle.LifecycleOwner; - -import com.google.gson.JsonSyntaxException; +import androidx.annotation.NonNull; +import com.hjq.core.manager.ActivityManager; import com.hjq.demo.R; -import com.hjq.demo.manager.ActivityManager; -import com.hjq.demo.ui.activity.LoginActivity; +import com.hjq.demo.http.exception.ResultException; +import com.hjq.demo.http.exception.TokenException; +import com.hjq.demo.ui.activity.account.LoginActivity; import com.hjq.gson.factory.GsonFactory; import com.hjq.http.EasyLog; -import com.hjq.http.config.IRequestApi; import com.hjq.http.config.IRequestHandler; import com.hjq.http.exception.CancelException; import com.hjq.http.exception.DataException; +import com.hjq.http.exception.FileMd5Exception; import com.hjq.http.exception.HttpException; import com.hjq.http.exception.NetworkException; +import com.hjq.http.exception.NullBodyException; import com.hjq.http.exception.ResponseException; -import com.hjq.http.exception.ResultException; import com.hjq.http.exception.ServerException; import com.hjq.http.exception.TimeoutException; -import com.hjq.http.exception.TokenException; -import com.tencent.mmkv.MMKV; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - +import com.hjq.http.request.HttpRequest; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Type; import java.net.SocketTimeoutException; import java.net.UnknownHostException; - +import java.util.HashMap; +import java.util.Map; import okhttp3.Headers; import okhttp3.Response; import okhttp3.ResponseBody; @@ -50,23 +46,25 @@ public final class RequestHandler implements IRequestHandler { private final Application mApplication; - private final MMKV mMmkv; public RequestHandler(Application application) { mApplication = application; - mMmkv = MMKV.mmkvWithID("http_cache_id"); } + @NonNull @Override - public Object requestSucceed(LifecycleOwner lifecycle, IRequestApi api, Response response, Type type) throws Exception { - + public Object requestSuccess(@NonNull HttpRequest httpRequest, @NonNull Response response, @NonNull Type type) throws Throwable { if (Response.class.equals(type)) { return response; } if (!response.isSuccessful()) { - // 返回响应异常 - throw new ResponseException(mApplication.getString(R.string.http_response_error) + ",responseCode:" + response.code() + ",message:" + response.message(), response); + throw new ResponseException(String.format(mApplication.getString(R.string.http_response_error), + response.code(), response.message()), response); + } + + if (Object.class.equals(type)) { + return ""; } if (Headers.class.equals(type)) { @@ -75,13 +73,17 @@ public Object requestSucceed(LifecycleOwner lifecycle, IRequestApi api, Response ResponseBody body = response.body(); if (body == null) { - return null; + throw new NullBodyException(mApplication.getString(R.string.http_response_null_body)); } if (InputStream.class.equals(type)) { return body.byteStream(); } + if (Bitmap.class.equals(type)) { + return BitmapFactory.decodeStream(body.byteStream()); + } + String text; try { text = body.string(); @@ -91,48 +93,38 @@ public Object requestSucceed(LifecycleOwner lifecycle, IRequestApi api, Response } // 打印这个 Json 或者文本 - EasyLog.json(text); + EasyLog.printJson(httpRequest, text); if (String.class.equals(type)) { return text; } - if (JSONObject.class.equals(type)) { - try { - // 如果这是一个 JSONObject 对象 - return new JSONObject(text); - } catch (JSONException e) { - throw new DataException(mApplication.getString(R.string.http_data_explain_error), e); - } - } - - if (JSONArray.class.equals(type)) { - try { - // 如果这是一个 JSONArray 对象 - return new JSONArray(text); - } catch (JSONException e) { - throw new DataException(mApplication.getString(R.string.http_data_explain_error), e); - } - } - final Object result; try { result = GsonFactory.getSingletonGson().fromJson(text, type); - } catch (JsonSyntaxException e) { + } catch (Exception e) { // 返回结果读取异常 throw new DataException(mApplication.getString(R.string.http_data_explain_error), e); } if (result instanceof HttpData) { HttpData model = (HttpData) result; + Headers headers = response.headers(); + int headersSize = headers.size(); + Map headersMap = new HashMap<>(headersSize); + for (int i = 0; i < headersSize; i++) { + headersMap.put(headers.name(i), headers.value(i)); + } + // Github issue 地址:https://github.com/getActivity/EasyHttp/issues/233 + model.setResponseHeaders(headersMap); - if (model.isRequestSucceed()) { + if (model.isRequestSuccess()) { // 代表执行成功 return result; } - if (model.isTokenFailure()) { + if (model.isTokenInvalidation()) { // 代表登录失效,需要重新登录 throw new TokenException(mApplication.getString(R.string.http_token_error)); } @@ -143,11 +135,11 @@ public Object requestSucceed(LifecycleOwner lifecycle, IRequestApi api, Response return result; } + @NonNull @Override - public Exception requestFail(LifecycleOwner lifecycle, IRequestApi api, Exception e) { - // 判断这个异常是不是自己抛的 - if (e instanceof HttpException) { - if (e instanceof TokenException) { + public Throwable requestFail(@NonNull HttpRequest httpRequest, @NonNull Throwable throwable) { + if (throwable instanceof HttpException) { + if (throwable instanceof TokenException) { // 登录信息失效,跳转到登录页 Application application = ActivityManager.getInstance().getApplication(); Intent intent = new Intent(application, LoginActivity.class); @@ -156,58 +148,53 @@ public Exception requestFail(LifecycleOwner lifecycle, IRequestApi api, Exceptio // 销毁除了登录页之外的 Activity ActivityManager.getInstance().finishAllActivities(LoginActivity.class); } - return e; + return throwable; } - if (e instanceof SocketTimeoutException) { - return new TimeoutException(mApplication.getString(R.string.http_server_out_time), e); + if (throwable instanceof SocketTimeoutException) { + return new TimeoutException(mApplication.getString(R.string.http_server_out_time), throwable); } - if (e instanceof UnknownHostException) { + if (throwable instanceof UnknownHostException) { NetworkInfo info = ((ConnectivityManager) mApplication.getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo(); // 判断网络是否连接 if (info == null || !info.isConnected()) { // 没有连接就是网络异常 - return new NetworkException(mApplication.getString(R.string.http_network_error), e); + return new NetworkException(mApplication.getString(R.string.http_network_error), throwable); } // 有连接就是服务器的问题 - return new ServerException(mApplication.getString(R.string.http_server_error), e); + return new ServerException(mApplication.getString(R.string.http_server_error), throwable); } - if (e instanceof IOException) { - //e = new CancelException(context.getString(R.string.http_request_cancel), e); - return new CancelException("", e); + if (throwable instanceof IOException) { + // 出现该异常的两种情况 + // 1. 调用 EasyHttp.cancel + // 2. 网络请求被中断 + return new CancelException(mApplication.getString(R.string.http_request_cancel), throwable); } - return new HttpException(e.getMessage(), e); - } - - @Override - public Object readCache(LifecycleOwner lifecycle, IRequestApi api, Type type) { - String cacheKey = GsonFactory.getSingletonGson().toJson(api); - String cacheValue = mMmkv.getString(cacheKey, null); - if (cacheValue == null || "".equals(cacheValue) || "{}".equals(cacheValue)) { - return null; - } - EasyLog.print("---------- cacheKey ----------"); - EasyLog.json(cacheKey); - EasyLog.print("---------- cacheValue ----------"); - EasyLog.json(cacheValue); - return GsonFactory.getSingletonGson().fromJson(cacheValue, type); + return new HttpException(throwable.getMessage(), throwable); } + @NonNull @Override - public boolean writeCache(LifecycleOwner lifecycle, IRequestApi api, Response response, Object result) { - String cacheKey = GsonFactory.getSingletonGson().toJson(api); - String cacheValue = GsonFactory.getSingletonGson().toJson(result); - if (cacheValue == null || "".equals(cacheValue) || "{}".equals(cacheValue)) { - return false; - } - EasyLog.print("---------- cacheKey ----------"); - EasyLog.json(cacheKey); - EasyLog.print("---------- cacheValue ----------"); - EasyLog.json(cacheValue); - return mMmkv.putString(cacheKey, cacheValue).commit(); + public Throwable downloadFail(@NonNull HttpRequest httpRequest, @NonNull Throwable throwable) { + if (throwable instanceof ResponseException) { + ResponseException responseException = ((ResponseException) throwable); + Response response = responseException.getResponse(); + responseException.setMessage(String.format(mApplication.getString(R.string.http_response_error), + response.code(), response.message())); + return responseException; + } else if (throwable instanceof NullBodyException) { + NullBodyException nullBodyException = ((NullBodyException) throwable); + nullBodyException.setMessage(mApplication.getString(R.string.http_response_null_body)); + return nullBodyException; + } else if (throwable instanceof FileMd5Exception) { + FileMd5Exception fileMd5Exception = ((FileMd5Exception) throwable); + fileMd5Exception.setMessage(mApplication.getString(R.string.http_response_md5_error)); + return fileMd5Exception; + } + return requestFail(httpRequest, throwable); } } \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/http/model/RequestServer.java b/app/src/main/java/com/hjq/demo/http/model/RequestServer.java index 9e3eca09..9195ab8b 100644 --- a/app/src/main/java/com/hjq/demo/http/model/RequestServer.java +++ b/app/src/main/java/com/hjq/demo/http/model/RequestServer.java @@ -1,8 +1,10 @@ package com.hjq.demo.http.model; +import androidx.annotation.NonNull; import com.hjq.demo.other.AppConfig; +import com.hjq.http.config.IHttpPostBodyStrategy; import com.hjq.http.config.IRequestServer; -import com.hjq.http.model.BodyType; +import com.hjq.http.model.RequestBodyType; /** * author : Android 轮子哥 @@ -12,19 +14,16 @@ */ public class RequestServer implements IRequestServer { + @NonNull @Override public String getHost() { - return AppConfig.getHostUrl(); + return AppConfig.getHostUrl() + "api/"; } + @NonNull @Override - public String getPath() { - return "api/"; - } - - @Override - public BodyType getType() { + public IHttpPostBodyStrategy getBodyType() { // 以表单的形式提交参数 - return BodyType.FORM; + return RequestBodyType.FORM; } } \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/manager/DialogManager.java b/app/src/main/java/com/hjq/demo/manager/DialogManager.java index 6ca7639b..462fc2f0 100644 --- a/app/src/main/java/com/hjq/demo/manager/DialogManager.java +++ b/app/src/main/java/com/hjq/demo/manager/DialogManager.java @@ -1,15 +1,15 @@ package com.hjq.demo.manager; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleEventObserver; import androidx.lifecycle.LifecycleOwner; - import com.hjq.base.BaseDialog; - import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; /** * author : Android 轮子哥 @@ -17,9 +17,11 @@ * time : 2021/01/29 * desc : Dialog 显示管理类 */ +@SuppressWarnings("MapOrSetKeyShouldOverrideHashCodeEquals") public final class DialogManager implements LifecycleEventObserver, BaseDialog.OnDismissListener { - private final static HashMap DIALOG_MANAGER = new HashMap<>(); + @NonNull + private static final Map DIALOG_MANAGER = new HashMap<>(); public static DialogManager getInstance(LifecycleOwner lifecycleOwner) { DialogManager manager = DIALOG_MANAGER.get(lifecycleOwner); @@ -30,21 +32,65 @@ public static DialogManager getInstance(LifecycleOwner lifecycleOwner) { return manager; } - private final List mDialogs = new ArrayList<>(); + @NonNull + private final List mDialogList = new ArrayList<>(); + + @NonNull + private final Map mDialogPriority = new HashMap<>(); private DialogManager(LifecycleOwner lifecycleOwner) { lifecycleOwner.getLifecycle().addObserver(this); } + /** + * 获取所有排队显示的对话框 + */ + @NonNull + public List getDialogList() { + return mDialogList; + } + + public void addDialog(@Nullable BaseDialog dialog) { + addDialog(dialog, 0); + } + + /** + * 添加 Dialog 对象 + * + * @param priority 弹窗优先级 + */ + public void addDialog(@Nullable BaseDialog dialog, int priority) { + if (dialog == null) { + return; + } + + if (mDialogList.contains(dialog)) { + return; + } + + int dialogIndex = mDialogList.size(); + for (int i = 0; i < mDialogList.size(); i++) { + BaseDialog itemDialog = mDialogList.get(i); + Integer itemPriority = mDialogPriority.get(itemDialog); + if (itemPriority == null) { + continue; + } + if (priority > itemPriority && !itemDialog.isShowing()) { + dialogIndex = i; + } + } + mDialogList.add(dialogIndex, dialog); + mDialogPriority.put(dialog, priority); + } + /** * 排队显示 Dialog */ - public void addShow(BaseDialog dialog) { - if (dialog == null || dialog.isShowing()) { - throw new IllegalStateException("are you ok?"); + public void startShow() { + if (mDialogList.isEmpty()) { + return; } - mDialogs.add(dialog); - BaseDialog firstDialog = mDialogs.get(0); + BaseDialog firstDialog = mDialogList.get(0); if (!firstDialog.isShowing()) { firstDialog.addOnDismissListener(this); firstDialog.show(); @@ -55,22 +101,24 @@ public void addShow(BaseDialog dialog) { * 取消所有 Dialog 的显示 */ public void clearShow() { - if (mDialogs.isEmpty()) { + if (mDialogList.isEmpty()) { return; } - BaseDialog firstDialog = mDialogs.get(0); + BaseDialog firstDialog = mDialogList.get(0); if (firstDialog.isShowing()) { firstDialog.removeOnDismissListener(this); firstDialog.dismiss(); } - mDialogs.clear(); + mDialogList.clear(); + mDialogPriority.clear(); } @Override - public void onDismiss(BaseDialog dialog) { + public void onDismiss(@NonNull BaseDialog dialog) { dialog.removeOnDismissListener(this); - mDialogs.remove(dialog); - for (BaseDialog nextDialog : mDialogs) { + mDialogList.remove(dialog); + mDialogPriority.remove(dialog); + for (BaseDialog nextDialog : mDialogList) { if (!nextDialog.isShowing()) { nextDialog.addOnDismissListener(this); nextDialog.show(); @@ -84,12 +132,12 @@ public void onDismiss(BaseDialog dialog) { */ @Override - public void onStateChanged(@NonNull LifecycleOwner lifecycleOwner, @NonNull Lifecycle.Event event) { + public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) { if (event != Lifecycle.Event.ON_DESTROY) { return; } - DIALOG_MANAGER.remove(lifecycleOwner); - lifecycleOwner.getLifecycle().removeObserver(this); + DIALOG_MANAGER.remove(source); + source.getLifecycle().removeObserver(this); clearShow(); } } \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/manager/InitManager.java b/app/src/main/java/com/hjq/demo/manager/InitManager.java new file mode 100644 index 00000000..f77e1951 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/manager/InitManager.java @@ -0,0 +1,226 @@ +package com.hjq.demo.manager; + +import android.app.Activity; +import android.app.Application; +import android.content.Context; +import android.content.SharedPreferences; +import android.net.ConnectivityManager; +import android.net.Network; +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleOwner; +import com.chuckerteam.chucker.api.ChuckerInterceptor; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonToken; +import com.hjq.bar.TitleBar; +import com.hjq.core.manager.ActivityManager; +import com.hjq.core.tools.AndroidVersion; +import com.hjq.demo.R; +import com.hjq.demo.http.model.HttpCacheStrategy; +import com.hjq.demo.http.model.RequestHandler; +import com.hjq.demo.http.model.RequestServer; +import com.hjq.demo.other.AppConfig; +import com.hjq.demo.other.CrashHandler; +import com.hjq.demo.other.DebugLoggerTree; +import com.hjq.demo.other.MaterialHeader; +import com.hjq.demo.other.SmartBallPulseFooter; +import com.hjq.demo.other.TitleBarStyle; +import com.hjq.demo.other.ToastInterceptor; +import com.hjq.demo.other.ToastStyle; +import com.hjq.gson.factory.GsonFactory; +import com.hjq.gson.factory.ParseExceptionCallback; +import com.hjq.http.EasyConfig; +import com.hjq.http.config.IRequestInterceptor; +import com.hjq.http.model.HttpHeaders; +import com.hjq.http.model.HttpParams; +import com.hjq.http.request.HttpRequest; +import com.hjq.toast.Toaster; +import com.hjq.umeng.sdk.UmengClient; +import com.scwang.smart.refresh.layout.SmartRefreshLayout; +import com.tencent.bugly.library.Bugly; +import com.tencent.bugly.library.BuglyBuilder; +import com.tencent.mmkv.MMKV; +import okhttp3.OkHttpClient; +import timber.log.Timber; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2023/06/24 + * desc : 初始化管理器 + */ +public final class InitManager { + + /** 隐私政策配置文件 */ + private static final String AGREE_PRIVACY_NAME = "agree_privacy_config"; + /** 隐私政策同意结果 */ + private static final String KEY_AGREE_PRIVACY_RESULT = "key_agree_privacy_result"; + + /** + * 是否同意了隐私协议 + */ + public static boolean isAgreePrivacy(@NonNull Context context) { + SharedPreferences sharedPreferences = context.getSharedPreferences(AGREE_PRIVACY_NAME, Context.MODE_PRIVATE); + return sharedPreferences.getBoolean(KEY_AGREE_PRIVACY_RESULT, false); + } + + /** + * 设置隐私协议结果 + */ + public static void setAgreePrivacy(@NonNull Context context, boolean result) { + SharedPreferences sharedPreferences = context.getSharedPreferences(AGREE_PRIVACY_NAME, Context.MODE_PRIVATE); + sharedPreferences.edit().putBoolean(KEY_AGREE_PRIVACY_RESULT, result).apply(); + } + + /** + * 预初始化第三方 SDK + */ + public static void preInitSdk(@NonNull Application application) { + // 初始化日志打印 + if (AppConfig.isLogEnable()) { + Timber.plant(new DebugLoggerTree()); + } + + // 设置标题栏全局样式 + TitleBar.setGlobalStyle(new TitleBarStyle()); + + // 设置全局的 Header 构建器 + SmartRefreshLayout.setDefaultRefreshHeaderCreator((context, layout) -> + new MaterialHeader(context).setColorSchemeColors(ContextCompat.getColor(context, R.color.common_accent_color))); + // 设置全局的 Footer 构建器 + SmartRefreshLayout.setDefaultRefreshFooterCreator((context, layout) -> new SmartBallPulseFooter(context)); + // 设置全局初始化器 + SmartRefreshLayout.setDefaultRefreshInitializer((context, layout) -> { + // 刷新头部是否跟随内容偏移 + layout.setEnableHeaderTranslationContent(true) + // 刷新尾部是否跟随内容偏移 + .setEnableFooterTranslationContent(true) + // 加载更多是否跟随内容偏移 + .setEnableFooterFollowWhenNoMoreData(true) + // 内容不满一页时是否可以上拉加载更多 + .setEnableLoadMoreWhenContentNotFull(false) + // 仿苹果越界效果开关 + .setEnableOverScrollDrag(false); + + // 关闭框架预埋的彩蛋 + // https://github.com/scwang90/SmartRefreshLayout/issues/1105 + layout.getLayout().setTag("close egg"); + }); + + // 初始化吐司 + Toaster.init(application, new ToastStyle()); + // 设置调试模式 + Toaster.setDebugMode(AppConfig.isDebug()); + // 设置 Toast 拦截器 + Toaster.setInterceptor(new ToastInterceptor()); + + // 本地异常捕捉 + CrashHandler.register(application); + + // Bugly 异常捕捉 + BuglyBuilder builder = new BuglyBuilder(AppConfig.getBuglyId(), AppConfig.getBuglyKey()); + builder.debugMode = AppConfig.isDebug(); + Bugly.init(application, builder); + + // Activity 栈管理初始化 + ActivityManager.getInstance().init(application); + + // MMKV 初始化 + MMKV.initialize(application); + + // 网络请求框架初始化 + OkHttpClient okHttpClient = new OkHttpClient.Builder() + .addInterceptor(new ChuckerInterceptor(application)) + .build(); + + EasyConfig.with(okHttpClient) + // 是否打印日志 + .setLogEnabled(AppConfig.isLogEnable()) + // 设置服务器配置 + .setServer(new RequestServer()) + // 设置请求处理策略 + .setHandler(new RequestHandler(application)) + // 设置请求缓存实现策略(非必须) + .setCacheStrategy(new HttpCacheStrategy()) + // 设置请求重试次数 + .setRetryCount(1) + .setInterceptor(new IRequestInterceptor() { + @Override + public void interceptArguments(@NonNull HttpRequest httpRequest, + @NonNull HttpParams params, + @NonNull HttpHeaders headers) { + // 添加全局请求头 + headers.put("token", "66666666666"); + headers.put("deviceOaid", UmengClient.getDeviceOaid()); + headers.put("versionName", AppConfig.getVersionName()); + headers.put("versionCode", String.valueOf(AppConfig.getVersionCode())); + // 添加全局请求参数 + // params.put("6666666", "6666666"); + } + }) + .into(); + + // 设置 Json 解析容错监听 + GsonFactory.setParseExceptionCallback(new ParseExceptionCallback() { + + @Override + public void onParseObjectException(TypeToken typeToken, String fieldName, JsonToken jsonToken) { + handlerGsonParseException("解析对象析异常:" + typeToken + "#" + fieldName + ",后台返回的类型为:" + jsonToken); + } + + @Override + public void onParseListItemException(TypeToken typeToken, String fieldName, JsonToken listItemJsonToken) { + handlerGsonParseException("解析 List 异常:" + typeToken + "#" + fieldName + ",后台返回的条目类型为:" + listItemJsonToken); + } + + @Override + public void onParseMapItemException(TypeToken typeToken, String fieldName, String mapItemKey, JsonToken mapItemJsonToken) { + handlerGsonParseException("解析 Map 异常:" + typeToken + "#" + fieldName + ",mapItemKey = " + mapItemKey + ",后台返回的条目类型为:" + mapItemJsonToken); + } + + private void handlerGsonParseException(String message) { + IllegalArgumentException e = new IllegalArgumentException(message); + if (AppConfig.isDebug()) { + throw e; + } else { + // 上报到 Bugly 错误列表中 + Bugly.handleCatchException(Thread.currentThread(), e, e.getMessage(), null, true); + } + } + }); + + // 注册网络状态变化监听 + ConnectivityManager connectivityManager = ContextCompat.getSystemService(application, ConnectivityManager.class); + if (connectivityManager != null && AndroidVersion.isAndroid7()) { + connectivityManager.registerDefaultNetworkCallback(new ConnectivityManager.NetworkCallback() { + + @Override + public void onLost(@NonNull Network network) { + Activity topActivity = ActivityManager.getInstance().getTopActivity(); + if (!(topActivity instanceof LifecycleOwner)) { + return; + } + + LifecycleOwner lifecycleOwner = ((LifecycleOwner) topActivity); + if (lifecycleOwner.getLifecycle().getCurrentState() != Lifecycle.State.RESUMED) { + return; + } + + Toaster.show(R.string.common_network_error); + } + }); + } + + // 预初始化友盟 SDK + UmengClient.preInit(application, AppConfig.isLogEnable()); + } + + /** + * 初始化第三方 SDK + */ + public static void initSdk(@NonNull Application application) { + // 友盟统计、登录、分享 SDK + UmengClient.init(application, AppConfig.isLogEnable()); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/AppConfig.java b/app/src/main/java/com/hjq/demo/other/AppConfig.java index 16a3fc04..b6ba0b4f 100644 --- a/app/src/main/java/com/hjq/demo/other/AppConfig.java +++ b/app/src/main/java/com/hjq/demo/other/AppConfig.java @@ -53,16 +53,23 @@ public static int getVersionCode() { } /** - * 获取 Bugly Id + * 获取服务器主机地址 + */ + public static String getHostUrl() { + return BuildConfig.HOST_URL; + } + + /** + * 获取 BuglyId */ public static String getBuglyId() { return BuildConfig.BUGLY_ID; } /** - * 获取服务器主机地址 + * 获取 BuglyKey */ - public static String getHostUrl() { - return BuildConfig.HOST_URL; + public static String getBuglyKey() { + return BuildConfig.BUGLY_KEY; } } \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/ArrowDrawable.java b/app/src/main/java/com/hjq/demo/other/ArrowDrawable.java index 60bdf66c..4bc2687c 100644 --- a/app/src/main/java/com/hjq/demo/other/ArrowDrawable.java +++ b/app/src/main/java/com/hjq/demo/other/ArrowDrawable.java @@ -14,12 +14,12 @@ import android.graphics.drawable.Drawable; import android.view.Gravity; import android.view.View; - import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; - +import androidx.core.content.ContextCompat; import com.hjq.demo.R; +import com.hjq.smallest.width.SmallestWidthAdaptation; /** * author : 王浩 & Android 轮子哥 @@ -31,20 +31,16 @@ public final class ArrowDrawable extends Drawable { private final Builder mBuilder; - private final Paint mPaint; + private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private Path mPath; private ArrowDrawable(Builder builder) { mBuilder = builder; - mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - mPaint.setAntiAlias(true); + mPaint.setStyle(Paint.Style.FILL); } @Override public void draw(@NonNull Canvas canvas) { - if (mPath == null) { - return; - } if (mBuilder.mShadowSize > 0) { mPaint.setMaskFilter(new BlurMaskFilter(mBuilder.mShadowSize, BlurMaskFilter.Blur.OUTER)); mPaint.setColor(mBuilder.mShadowColor); @@ -72,7 +68,7 @@ public int getOpacity() { @SuppressWarnings("SuspiciousNameCombination") @Override - protected void onBoundsChange(Rect viewRect) { + protected void onBoundsChange(@NonNull Rect viewRect) { if (mPath == null) { mPath = new Path(); } else { @@ -183,6 +179,7 @@ protected void onBoundsChange(Rect viewRect) { public static final class Builder { /** 上下文对象 */ + @NonNull private final Context mContext; /** 箭头高度 */ private int mArrowHeight; @@ -203,12 +200,12 @@ public static final class Builder { /** 阴影颜色 */ private int mShadowColor; - public Builder(Context context) { + public Builder(@NonNull Context context) { mContext = context; - mBackgroundColor = 0xFF000000; - mShadowColor = 0x33000000; - mArrowHeight = (int) context.getResources().getDimension(R.dimen.dp_6); - mRadius = (int) context.getResources().getDimension(R.dimen.dp_4); + mBackgroundColor = ContextCompat.getColor(context, R.color.black); + mShadowColor = ContextCompat.getColor(context, R.color.black20); + mArrowHeight = (int) SmallestWidthAdaptation.dp2px(context, 6); + mRadius = (int) SmallestWidthAdaptation.dp2px(context, 4); mShadowSize = 0; mArrowOffsetX = 0; mArrowOffsetY = 0; @@ -261,7 +258,7 @@ public Builder setArrowOrientation(int orientation) { break; default: // 箭头只能在左上右下这四个位置 - throw new IllegalArgumentException("are you ok?"); + throw new IllegalArgumentException("The arrow can only be in the four positions: left, top, right, and bottom"); } return this; } @@ -289,13 +286,13 @@ public Builder setArrowGravity(int gravity) { case Gravity.LEFT: case Gravity.RIGHT: if (mArrowOrientation == Gravity.LEFT || mArrowOrientation == Gravity.RIGHT) { - throw new IllegalArgumentException("are you ok?"); + throw new IllegalArgumentException("The arrow direction cannot be the same as the arrow gravity"); } break; case Gravity.TOP: case Gravity.BOTTOM: if (mArrowOrientation == Gravity.TOP || mArrowOrientation == Gravity.BOTTOM) { - throw new IllegalArgumentException("are you ok?"); + throw new IllegalArgumentException("The arrow direction cannot be the same as the arrow gravity"); } break; case Gravity.CENTER_VERTICAL: @@ -303,7 +300,7 @@ public Builder setArrowGravity(int gravity) { break; default: // 箭头只能在左上右下这四个位置 - throw new IllegalArgumentException("are you ok?"); + throw new IllegalArgumentException("The arrow can only be in the four positions: left, top, right, and bottom"); } mArrowGravity = gravity; return this; @@ -336,10 +333,10 @@ public Builder setShadowSize(int size) { /** * 构建 Drawable */ - public Drawable build() { + public ArrowDrawable build() { if (mArrowOrientation == Gravity.NO_GRAVITY || mArrowGravity == Gravity.NO_GRAVITY) { // 必须要先设置箭头的方向及重心 - throw new IllegalArgumentException("are you ok?"); + throw new IllegalArgumentException("You must set the direction and gravity of the arrow"); } return new ArrowDrawable(this); } diff --git a/app/src/main/java/com/hjq/demo/other/CrashHandler.java b/app/src/main/java/com/hjq/demo/other/CrashHandler.java index 824568e6..d2c78b13 100644 --- a/app/src/main/java/com/hjq/demo/other/CrashHandler.java +++ b/app/src/main/java/com/hjq/demo/other/CrashHandler.java @@ -4,11 +4,9 @@ import android.app.Application; import android.content.Context; import android.content.SharedPreferences; - import androidx.annotation.NonNull; - -import com.hjq.demo.ui.activity.CrashActivity; -import com.hjq.demo.ui.activity.RestartActivity; +import com.hjq.demo.ui.activity.common.CrashActivity; +import com.hjq.demo.ui.activity.common.RestartActivity; /** * author : Android 轮子哥 @@ -19,7 +17,7 @@ public final class CrashHandler implements Thread.UncaughtExceptionHandler { /** Crash 文件名 */ - private static final String CRASH_FILE_NAME = "crash_file"; + private static final String CRASH_FILE_NAME = "crash_config"; /** Crash 时间记录 */ private static final String KEY_CRASH_TIME = "key_crash_time"; @@ -36,9 +34,9 @@ public static void register(Application application) { private CrashHandler(Application application) { mApplication = application; mNextHandler = Thread.getDefaultUncaughtExceptionHandler(); - if (getClass().getName().equals(mNextHandler.getClass().getName())) { + if (mNextHandler != null && getClass().getName().equals(mNextHandler.getClass().getName())) { // 请不要重复注册 Crash 监听 - throw new IllegalStateException("are you ok?"); + throw new IllegalStateException("CrashHandler has already been registered"); } } @@ -51,13 +49,13 @@ public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwab // 记录当前崩溃的时间,以便下次崩溃时进行比对 sharedPreferences.edit().putLong(KEY_CRASH_TIME, currentCrashTime).commit(); - // 致命异常标记:如果上次崩溃的时间距离当前崩溃小于 5 分钟,那么判定为致命异常 - boolean deadlyCrash = currentCrashTime - lastCrashTime < 1000 * 60 * 5; - if (AppConfig.isDebug()) { - CrashActivity.start(mApplication, throwable); + if (currentCrashTime - lastCrashTime > 1000 * 5) { + CrashActivity.start(mApplication, throwable); + } } else { - if (!deadlyCrash) { + // 致命异常标记:如果上次崩溃的时间距离当前崩溃小于 5 分钟,那么判定为致命异常 + if (currentCrashTime - lastCrashTime > 1000 * 60 * 5) { // 如果不是致命的异常就自动重启应用 RestartActivity.start(mApplication); } diff --git a/app/src/main/java/com/hjq/demo/other/DebugLoggerTree.java b/app/src/main/java/com/hjq/demo/other/DebugLoggerTree.java index b5d77e24..fa244f6c 100644 --- a/app/src/main/java/com/hjq/demo/other/DebugLoggerTree.java +++ b/app/src/main/java/com/hjq/demo/other/DebugLoggerTree.java @@ -1,9 +1,7 @@ package com.hjq.demo.other; -import android.os.Build; - +import com.hjq.core.tools.AndroidVersion; import org.jetbrains.annotations.NotNull; - import timber.log.Timber; /** @@ -22,8 +20,8 @@ public final class DebugLoggerTree extends Timber.DebugTree { @Override protected String createStackElementTag(@NotNull StackTraceElement element) { String tag = "(" + element.getFileName() + ":" + element.getLineNumber() + ")"; - // 日志 TAG 长度限制已经在 Android 7.0 被移除 - if (tag.length() <= MAX_TAG_LENGTH || Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + // 日志 TAG 长度限制已经在 Android 8.0 被移除 + if (tag.length() <= MAX_TAG_LENGTH || AndroidVersion.isAndroid8()) { return tag; } return tag.substring(0, MAX_TAG_LENGTH); diff --git a/app/src/main/java/com/hjq/demo/other/LinkClickableSpan.java b/app/src/main/java/com/hjq/demo/other/LinkClickableSpan.java new file mode 100644 index 00000000..fa6577cc --- /dev/null +++ b/app/src/main/java/com/hjq/demo/other/LinkClickableSpan.java @@ -0,0 +1,26 @@ +package com.hjq.demo.other; + +import android.text.style.ClickableSpan; +import android.view.View; +import androidx.annotation.NonNull; +import com.hjq.demo.ui.activity.common.BrowserActivity; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2023/06/24 + * desc : 点击跳转链接的 ClickableSpan + */ +public class LinkClickableSpan extends ClickableSpan { + + private final String mTargetUrl; + + public LinkClickableSpan(@NonNull String url) { + mTargetUrl = url; + } + + @Override + public void onClick(@NonNull View widget) { + BrowserActivity.start(widget.getContext(), mTargetUrl); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/MaterialHeader.java b/app/src/main/java/com/hjq/demo/other/MaterialHeader.java index ea4657c9..04670e27 100644 --- a/app/src/main/java/com/hjq/demo/other/MaterialHeader.java +++ b/app/src/main/java/com/hjq/demo/other/MaterialHeader.java @@ -1,19 +1,22 @@ package com.hjq.demo.other; +import static android.view.View.MeasureSpec.getSize; + import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.util.AttributeSet; import android.widget.ImageView; - import androidx.annotation.ColorInt; import androidx.annotation.ColorRes; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; - import com.hjq.demo.R; +import com.hjq.smallest.width.SmallestWidthAdaptation; import com.scwang.smart.refresh.header.material.CircleImageView; import com.scwang.smart.refresh.header.material.MaterialProgressDrawable; import com.scwang.smart.refresh.layout.api.RefreshHeader; @@ -23,8 +26,6 @@ import com.scwang.smart.refresh.layout.constant.SpinnerStyle; import com.scwang.smart.refresh.layout.simple.SimpleComponent; -import static android.view.View.MeasureSpec.getSize; - /** * author : 树朾 & Android 轮子哥 * github : https://github.com/scwang90/SmartRefreshLayout/tree/master/refresh-header-material @@ -38,40 +39,45 @@ public final class MaterialHeader extends SimpleComponent implements RefreshHead /** 刷新球默认样式 */ public static final int BALL_STYLE_DEFAULT = 1; - protected static final int CIRCLE_BG_LIGHT = 0xFFFAFAFA; - protected static final float MAX_PROGRESS_ANGLE = 0.8f; + private static final int CIRCLE_BG_LIGHT = Color.parseColor("#FAFAFA"); + private static final float MAX_PROGRESS_ANGLE = 0.8f; - protected boolean mFinished; - protected int mCircleDiameter; - protected ImageView mCircleView; - protected MaterialProgressDrawable mProgressDrawable; + private boolean mFinished; + private int mCircleDiameter; + private final ImageView mCircleView; + private final MaterialProgressDrawable mProgressDrawable; - protected int mWaveHeight; - protected int mHeadHeight; - protected Path mBezierPath; - protected Paint mBezierPaint; - protected RefreshState mRefreshState; - protected boolean mShowBezierWave = false; - protected boolean mScrollableWhenRefreshing = true; + private int mWaveHeight; + private int mHeadHeight; + private final Path mBezierPath; + private final Paint mBezierPaint; + private RefreshState mRefreshState; + private boolean mShowBezierWave = false; + private boolean mScrollableWhenRefreshing = true; - public MaterialHeader(Context context) { + public MaterialHeader(@NonNull Context context) { this(context, null); } - public MaterialHeader(Context context, AttributeSet attrs) { + public MaterialHeader(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs, 0); mSpinnerStyle = SpinnerStyle.MatchLayout; - setMinimumHeight((int) getResources().getDimension(R.dimen.dp_100)); + setMinimumHeight((int) SmallestWidthAdaptation.dp2px(context, 100)); mProgressDrawable = new MaterialProgressDrawable(this); - mProgressDrawable.setColorSchemeColors(0xff0099cc, 0xffff4444, 0xff669900, 0xffaa66cc, 0xffff8800); + mProgressDrawable.setColorSchemeColors( + Color.parseColor("#0099CC"), + Color.parseColor("#FF4444"), + Color.parseColor("#669900"), + Color.parseColor("#AA66CC"), + Color.parseColor("#FF8800")); mCircleView = new CircleImageView(context, CIRCLE_BG_LIGHT); mCircleView.setImageDrawable(mProgressDrawable); mCircleView.setAlpha(0f); addView(mCircleView); - mCircleDiameter = (int) getResources().getDimension(R.dimen.dp_40); + mCircleDiameter = (int) SmallestWidthAdaptation.dp2px(context, 40); mBezierPath = new Path(); mBezierPaint = new Paint(); @@ -81,10 +87,10 @@ public MaterialHeader(Context context, AttributeSet attrs) { TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MaterialHeader); mShowBezierWave = typedArray.getBoolean(R.styleable.MaterialHeader_srlShowBezierWave, mShowBezierWave); mScrollableWhenRefreshing = typedArray.getBoolean(R.styleable.MaterialHeader_srlScrollableWhenRefreshing, mScrollableWhenRefreshing); - mBezierPaint.setColor(typedArray.getColor(R.styleable.MaterialHeader_srlPrimaryColor, 0xff11bbff)); + mBezierPaint.setColor(typedArray.getColor(R.styleable.MaterialHeader_srlPrimaryColor, Color.parseColor("#11BBFF"))); if (typedArray.hasValue(R.styleable.MaterialHeader_srlShadowRadius)) { int radius = typedArray.getDimensionPixelOffset(R.styleable.MaterialHeader_srlShadowRadius, 0); - int color = typedArray.getColor(R.styleable.MaterialHeader_mhShadowColor, 0xff000000); + int color = typedArray.getColor(R.styleable.MaterialHeader_mhShadowColor, Color.parseColor("#000000")); mBezierPaint.setShadowLayer(radius, 0, 0, color); setLayerType(LAYER_TYPE_SOFTWARE, null); } @@ -92,11 +98,11 @@ public MaterialHeader(Context context, AttributeSet attrs) { mShowBezierWave = typedArray.getBoolean(R.styleable.MaterialHeader_mhShowBezierWave, mShowBezierWave); mScrollableWhenRefreshing = typedArray.getBoolean(R.styleable.MaterialHeader_mhScrollableWhenRefreshing, mScrollableWhenRefreshing); if (typedArray.hasValue(R.styleable.MaterialHeader_mhPrimaryColor)) { - mBezierPaint.setColor(typedArray.getColor(R.styleable.MaterialHeader_mhPrimaryColor, 0xff11bbff)); + mBezierPaint.setColor(typedArray.getColor(R.styleable.MaterialHeader_mhPrimaryColor, Color.parseColor("#11BBFF"))); } if (typedArray.hasValue(R.styleable.MaterialHeader_mhShadowRadius)) { int radius = typedArray.getDimensionPixelOffset(R.styleable.MaterialHeader_mhShadowRadius, 0); - int color = typedArray.getColor(R.styleable.MaterialHeader_mhShadowColor, 0xff000000); + int color = typedArray.getColor(R.styleable.MaterialHeader_mhShadowColor, Color.parseColor("#000000")); mBezierPaint.setShadowLayer(radius, 0, 0, color); setLayerType(LAYER_TYPE_SOFTWARE, null); } @@ -136,7 +142,7 @@ protected void onLayout(boolean changed, int left, int top, int right, int botto } @Override - protected void dispatchDraw(Canvas canvas) { + protected void dispatchDraw(@NonNull Canvas canvas) { if (mShowBezierWave) { // 重置画笔 mBezierPath.reset(); @@ -269,9 +275,9 @@ public MaterialHeader setBallStyle(int style) { return this; } if (style == BALL_STYLE_LARGE) { - mCircleDiameter = (int) getResources().getDimension(R.dimen.dp_56); + mCircleDiameter = (int) SmallestWidthAdaptation.dp2px(getContext(), 56); } else { - mCircleDiameter = (int) getResources().getDimension(R.dimen.dp_40); + mCircleDiameter = (int) SmallestWidthAdaptation.dp2px(getContext(), 40); } // force the bounds of the progress circle inside the circle view to // update by setting it to null before updating its size and then diff --git a/app/src/main/java/com/hjq/demo/other/PermissionCallback.java b/app/src/main/java/com/hjq/demo/other/PermissionCallback.java deleted file mode 100644 index 66d3d656..00000000 --- a/app/src/main/java/com/hjq/demo/other/PermissionCallback.java +++ /dev/null @@ -1,221 +0,0 @@ -package com.hjq.demo.other; - -import android.app.Activity; -import android.content.Context; -import android.os.Build; - -import com.hjq.demo.R; -import com.hjq.demo.manager.ActivityManager; -import com.hjq.demo.ui.dialog.MessageDialog; -import com.hjq.permissions.OnPermissionCallback; -import com.hjq.permissions.Permission; -import com.hjq.permissions.XXPermissions; -import com.hjq.toast.ToastUtils; - -import java.util.ArrayList; -import java.util.List; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2020/10/24 - * desc : 权限申请回调封装 - */ -public abstract class PermissionCallback implements OnPermissionCallback { - - @Override - public void onDenied(List permissions, boolean never) { - if (never) { - showPermissionDialog(permissions); - return; - } - - if (permissions.size() == 1 && Permission.ACCESS_BACKGROUND_LOCATION.equals(permissions.get(0))) { - ToastUtils.show(R.string.common_permission_fail_4); - return; - } - - ToastUtils.show(R.string.common_permission_fail_1); - } - - /** - * 显示授权对话框 - */ - protected void showPermissionDialog(List permissions) { - Activity activity = ActivityManager.getInstance().getTopActivity(); - if (activity == null || activity.isFinishing() || activity.isDestroyed()) { - return; - } - new MessageDialog.Builder(activity) - .setTitle(R.string.common_permission_alert) - .setMessage(getPermissionHint(activity, permissions)) - .setConfirm(R.string.common_permission_goto) - .setCancel(null) - .setCancelable(false) - .setListener(dialog -> XXPermissions.startPermissionActivity(activity, permissions)) - .show(); - } - - /** - * 根据权限获取提示 - */ - protected String getPermissionHint(Context context, List permissions) { - if (permissions == null || permissions.isEmpty()) { - return context.getString(R.string.common_permission_fail_2); - } - - List hints = new ArrayList<>(); - for (String permission : permissions) { - switch (permission) { - case Permission.READ_EXTERNAL_STORAGE: - case Permission.WRITE_EXTERNAL_STORAGE: - case Permission.MANAGE_EXTERNAL_STORAGE: { - String hint = context.getString(R.string.common_permission_storage); - if (!hints.contains(hint)) { - hints.add(hint); - } - break; - } - case Permission.CAMERA: { - String hint = context.getString(R.string.common_permission_camera); - if (!hints.contains(hint)) { - hints.add(hint); - } - break; - } - case Permission.RECORD_AUDIO: { - String hint = context.getString(R.string.common_permission_microphone); - if (!hints.contains(hint)) { - hints.add(hint); - } - break; - } - case Permission.ACCESS_FINE_LOCATION: - case Permission.ACCESS_COARSE_LOCATION: - case Permission.ACCESS_BACKGROUND_LOCATION: { - String hint; - if (!permissions.contains(Permission.ACCESS_FINE_LOCATION) && - !permissions.contains(Permission.ACCESS_COARSE_LOCATION)) { - hint = context.getString(R.string.common_permission_location_background); - } else { - hint = context.getString(R.string.common_permission_location); - } - if (!hints.contains(hint)) { - hints.add(hint); - } - break; - } - case Permission.READ_PHONE_STATE: - case Permission.CALL_PHONE: - case Permission.ADD_VOICEMAIL: - case Permission.USE_SIP: - case Permission.READ_PHONE_NUMBERS: - case Permission.ANSWER_PHONE_CALLS: { - String hint = context.getString(R.string.common_permission_phone); - if (!hints.contains(hint)) { - hints.add(hint); - } - break; - } - case Permission.GET_ACCOUNTS: - case Permission.READ_CONTACTS: - case Permission.WRITE_CONTACTS: { - String hint = context.getString(R.string.common_permission_contacts); - if (!hints.contains(hint)) { - hints.add(hint); - } - break; - } - case Permission.READ_CALENDAR: - case Permission.WRITE_CALENDAR: { - String hint = context.getString(R.string.common_permission_calendar); - if (!hints.contains(hint)) { - hints.add(hint); - } - break; - } - case Permission.READ_CALL_LOG: - 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_phone); - if (!hints.contains(hint)) { - hints.add(hint); - } - break; - } - case Permission.BODY_SENSORS: { - String hint = context.getString(R.string.common_permission_sensors); - if (!hints.contains(hint)) { - hints.add(hint); - } - break; - } - case Permission.ACTIVITY_RECOGNITION: { - String hint = context.getString(R.string.common_permission_activity_recognition); - if (!hints.contains(hint)) { - hints.add(hint); - } - break; - } - case Permission.SEND_SMS: - case Permission.RECEIVE_SMS: - case Permission.READ_SMS: - case Permission.RECEIVE_WAP_PUSH: - case Permission.RECEIVE_MMS: { - String hint = context.getString(R.string.common_permission_sms); - if (!hints.contains(hint)) { - hints.add(hint); - } - break; - } - case Permission.REQUEST_INSTALL_PACKAGES: { - String hint = context.getString(R.string.common_permission_install); - if (!hints.contains(hint)) { - hints.add(hint); - } - break; - } - case Permission.NOTIFICATION_SERVICE: { - String hint = context.getString(R.string.common_permission_notification); - if (!hints.contains(hint)) { - hints.add(hint); - } - break; - } - case Permission.SYSTEM_ALERT_WINDOW: { - String hint = context.getString(R.string.common_permission_window); - if (!hints.contains(hint)) { - hints.add(hint); - } - break; - } - case Permission.WRITE_SETTINGS: { - String hint = context.getString(R.string.common_permission_setting); - if (!hints.contains(hint)) { - hints.add(hint); - } - break; - } - default: - break; - } - } - - if (!hints.isEmpty()) { - StringBuilder builder = new StringBuilder(); - for (String text : hints) { - if (builder.length() == 0) { - builder.append(text); - } else { - builder.append("、") - .append(text); - } - } - builder.append(" "); - return context.getString(R.string.common_permission_fail_3, builder.toString()); - } - - return context.getString(R.string.common_permission_fail_2); - } -} diff --git a/app/src/main/java/com/hjq/demo/other/SmartBallPulseFooter.java b/app/src/main/java/com/hjq/demo/other/SmartBallPulseFooter.java index fcff3644..084b0bb8 100644 --- a/app/src/main/java/com/hjq/demo/other/SmartBallPulseFooter.java +++ b/app/src/main/java/com/hjq/demo/other/SmartBallPulseFooter.java @@ -7,13 +7,12 @@ import android.graphics.Paint; import android.util.AttributeSet; import android.view.animation.AccelerateDecelerateInterpolator; - import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.ColorUtils; - import com.hjq.demo.R; +import com.hjq.smallest.width.SmallestWidthAdaptation; import com.scwang.smart.refresh.layout.api.RefreshFooter; import com.scwang.smart.refresh.layout.api.RefreshLayout; import com.scwang.smart.refresh.layout.constant.SpinnerStyle; @@ -36,8 +35,11 @@ public final class SmartBallPulseFooter extends SimpleComponent implements Refre private final Paint mPaint; - private int mNormalColor = 0xFFEEEEEE; - private int[] mAnimatingColor = {0xFF30B399, 0xFFFF4600, 0xFF142DCC}; + private int mNormalColor = Color.parseColor("#EEEEEE"); + private int[] mAnimatingColor = { + Color.parseColor("#30B399"), + Color.parseColor("#FF4600"), + Color.parseColor("#142DCC")}; private final float mCircleSpacing; @@ -46,14 +48,14 @@ public final class SmartBallPulseFooter extends SimpleComponent implements Refre private final float mTextWidth; - public SmartBallPulseFooter(Context context) { + public SmartBallPulseFooter(@NonNull Context context) { this(context, null); } - public SmartBallPulseFooter(Context context, @Nullable AttributeSet attrs) { + public SmartBallPulseFooter(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs, 0); - setMinimumHeight((int) getResources().getDimension(R.dimen.dp_60)); + setMinimumHeight((int) SmallestWidthAdaptation.dp2px(context, 60)); mPaint = new Paint(); mPaint.setColor(Color.WHITE); @@ -62,17 +64,17 @@ public SmartBallPulseFooter(Context context, @Nullable AttributeSet attrs) { mSpinnerStyle = SpinnerStyle.Translate; - mCircleSpacing = getResources().getDimension(R.dimen.dp_2); - mPaint.setTextSize(getResources().getDimension(R.dimen.sp_14)); + mCircleSpacing = SmallestWidthAdaptation.dp2px(context, 2); + mPaint.setTextSize(SmallestWidthAdaptation.sp2px(context, 14)); mTextWidth = mPaint.measureText(getContext().getString(R.string.common_no_more_data)); } @Override - protected void dispatchDraw(Canvas canvas) { + protected void dispatchDraw(@NonNull Canvas canvas) { final int width = getWidth(); final int height = getHeight(); if (mNoMoreData) { - mPaint.setColor(0xFF898989); + mPaint.setColor(Color.parseColor("#898989")); canvas.drawText(getContext().getString(R.string.common_no_more_data),(width - mTextWidth) / 2,(height - mPaint.getTextSize()) / 2, mPaint); } else { float radius = (Math.min(width, height) - mCircleSpacing * 2) / 7; @@ -136,7 +138,7 @@ public void setPrimaryColors(@ColorInt int... colors) { if (colors.length > 1) { setNormalColor(colors[1]); } else if (colors.length > 0) { - setNormalColor(ColorUtils.compositeColors(0x99FFFFFF, colors[0])); + setNormalColor(ColorUtils.compositeColors(Color.parseColor("#99FFFFFF"), colors[0])); } mManualNormalColor = false; } diff --git a/app/src/main/java/com/hjq/demo/other/TitleBarStyle.java b/app/src/main/java/com/hjq/demo/other/TitleBarStyle.java index 5a5954ef..e791a3d3 100644 --- a/app/src/main/java/com/hjq/demo/other/TitleBarStyle.java +++ b/app/src/main/java/com/hjq/demo/other/TitleBarStyle.java @@ -3,15 +3,15 @@ import android.content.Context; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; -import android.view.Gravity; +import android.view.View; import android.widget.TextView; - +import androidx.annotation.NonNull; import androidx.appcompat.widget.AppCompatTextView; import androidx.core.content.ContextCompat; - import com.hjq.bar.style.LightBarStyle; +import com.hjq.custom.widget.view.PressAlphaTextView; import com.hjq.demo.R; -import com.hjq.widget.view.PressAlphaTextView; +import com.hjq.smallest.width.SmallestWidthAdaptation; /** * author : Android 轮子哥 @@ -22,77 +22,90 @@ public final class TitleBarStyle extends LightBarStyle { @Override - public TextView newTitleView(Context context) { + public TextView newTitleView(@NonNull Context context) { return new AppCompatTextView(context); } @Override - public TextView newLeftView(Context context) { + public TextView newLeftView(@NonNull Context context) { return new PressAlphaTextView(context); } @Override - public TextView newRightView(Context context) { + public TextView newRightView(@NonNull Context context) { return new PressAlphaTextView(context); } @Override - public Drawable getTitleBarBackground(Context context) { + public Drawable getTitleBarBackground(@NonNull Context context) { return new ColorDrawable(ContextCompat.getColor(context, R.color.common_primary_color)); } @Override - public Drawable getBackButtonDrawable(Context context) { + public Drawable getBackButtonDrawable(@NonNull Context context) { + if (context.getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + return ContextCompat.getDrawable(context, R.drawable.arrows_right_ic); + } return ContextCompat.getDrawable(context, R.drawable.arrows_left_ic); } @Override - public Drawable getLeftTitleBackground(Context context) { + public Drawable getLeftTitleBackground(@NonNull Context context) { return null; } @Override - public Drawable getRightTitleBackground(Context context) { + public Drawable getRightTitleBackground(@NonNull Context context) { return null; } @Override - public int getChildHorizontalPadding(Context context) { - return (int) context.getResources().getDimension(R.dimen.dp_12); + public int getTitleHorizontalPadding(@NonNull Context context) { + return 0; + } + + @Override + public int getLeftHorizontalPadding(@NonNull Context context) { + return (int) SmallestWidthAdaptation.dp2px(context, 10); + } + + @Override + public int getRightHorizontalPadding(@NonNull Context context) { + return (int) SmallestWidthAdaptation.dp2px(context, 10); } @Override - public int getChildVerticalPadding(Context context) { - return (int) context.getResources().getDimension(R.dimen.dp_14); + public int getChildVerticalPadding(@NonNull Context context) { + return (int) SmallestWidthAdaptation.dp2px(context, 14); } @Override - public float getTitleSize(Context context) { - return context.getResources().getDimension(R.dimen.sp_15); + public float getTitleSize(@NonNull Context context) { + return SmallestWidthAdaptation.sp2px(context, 15); } @Override - public float getLeftTitleSize(Context context) { - return context.getResources().getDimension(R.dimen.sp_13); + public float getLeftTitleSize(@NonNull Context context) { + return SmallestWidthAdaptation.sp2px(context, 13); } @Override - public float getRightTitleSize(Context context) { - return context.getResources().getDimension(R.dimen.sp_13); + public float getRightTitleSize(@NonNull Context context) { + return SmallestWidthAdaptation.sp2px(context, 13); } @Override - public int getTitleIconPadding(Context context) { - return (int) context.getResources().getDimension(R.dimen.dp_2); + public int getTitleIconPadding(@NonNull Context context) { + return (int) SmallestWidthAdaptation.dp2px(context, 2); } @Override - public int getLeftIconPadding(Context context) { - return (int) context.getResources().getDimension(R.dimen.dp_2); + public int getLeftIconPadding(@NonNull Context context) { + return (int) SmallestWidthAdaptation.dp2px(context, 2); } @Override - public int getRightIconPadding(Context context) { - return (int) context.getResources().getDimension(R.dimen.dp_2); + public int getRightIconPadding(@NonNull Context context) { + return (int) SmallestWidthAdaptation.dp2px(context, 2); } } \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/ToastInterceptor.java b/app/src/main/java/com/hjq/demo/other/ToastInterceptor.java new file mode 100644 index 00000000..dc6da580 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/other/ToastInterceptor.java @@ -0,0 +1,24 @@ +package com.hjq.demo.other; + +import com.hjq.toast.ToastLogInterceptor; +import timber.log.Timber; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/AndroidProject + * time : 2020/11/04 + * desc : 自定义 Toast 拦截器(用于追踪 Toast 调用的位置) + */ +public final class ToastInterceptor extends ToastLogInterceptor { + + @Override + protected boolean isLogEnable() { + return AppConfig.isLogEnable(); + } + + @Override + protected void printLog(String msg) { + Timber.tag("Toaster"); + Timber.i(msg); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/ToastLogInterceptor.java b/app/src/main/java/com/hjq/demo/other/ToastLogInterceptor.java deleted file mode 100644 index 46dfcadf..00000000 --- a/app/src/main/java/com/hjq/demo/other/ToastLogInterceptor.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.hjq.demo.other; - - -import com.hjq.demo.action.ToastAction; -import com.hjq.toast.ToastUtils; -import com.hjq.toast.config.IToastInterceptor; - -import timber.log.Timber; - -/** - * author : Android 轮子哥 - * github : https://github.com/getActivity/AndroidProject - * time : 2020/11/04 - * desc : 自定义 Toast 拦截器(用于追踪 Toast 调用的位置) - */ -public final class ToastLogInterceptor implements IToastInterceptor { - - @Override - public boolean intercept(CharSequence text) { - if (AppConfig.isLogEnable()) { - // 获取调用的堆栈信息 - StackTraceElement[] stackTrace = new Throwable().getStackTrace(); - // 跳过最前面两个堆栈 - for (int i = 2; stackTrace.length > 2 && i < stackTrace.length; i++) { - // 获取代码行数 - int lineNumber = stackTrace[i].getLineNumber(); - // 获取类的全路径 - String className = stackTrace[i].getClassName(); - if (lineNumber <= 0 || className.startsWith(ToastUtils.class.getName()) || - className.startsWith(ToastAction.class.getName())) { - continue; - } - - Timber.tag("ToastUtils"); - Timber.i("(" + stackTrace[i].getFileName() + ":" + lineNumber + ") " + text.toString()); - break; - } - } - return false; - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/other/ToastStyle.java b/app/src/main/java/com/hjq/demo/other/ToastStyle.java index 418e903d..a37bfcae 100644 --- a/app/src/main/java/com/hjq/demo/other/ToastStyle.java +++ b/app/src/main/java/com/hjq/demo/other/ToastStyle.java @@ -3,9 +3,9 @@ import android.content.Context; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; -import android.util.TypedValue; - +import androidx.annotation.NonNull; import com.hjq.demo.R; +import com.hjq.smallest.width.SmallestWidthAdaptation; import com.hjq.toast.style.BlackToastStyle; /** @@ -17,27 +17,27 @@ public final class ToastStyle extends BlackToastStyle { @Override - protected Drawable getBackgroundDrawable(Context context) { + protected Drawable getBackgroundDrawable(@NonNull Context context) { GradientDrawable drawable = new GradientDrawable(); // 设置颜色 drawable.setColor(0X88000000); // 设置圆角 - drawable.setCornerRadius(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, (int) context.getResources().getDimension(R.dimen.button_circle_size), context.getResources().getDisplayMetrics())); + drawable.setCornerRadius((int) context.getResources().getDimension(R.dimen.button_circle_size)); return drawable; } @Override - protected float getTextSize(Context context) { - return context.getResources().getDimension(R.dimen.sp_14); + protected float getTextSize(@NonNull Context context) { + return SmallestWidthAdaptation.sp2px(context, 14); } @Override - protected int getHorizontalPadding(Context context) { - return (int) context.getResources().getDimension(R.dimen.sp_24); + protected int getHorizontalPadding(@NonNull Context context) { + return (int) SmallestWidthAdaptation.sp2px(context, 24); } @Override - protected int getVerticalPadding(Context context) { - return (int) context.getResources().getDimension(R.dimen.sp_16); + protected int getVerticalPadding(@NonNull Context context) { + return (int) SmallestWidthAdaptation.sp2px(context, 16); } } \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/permission/PermissionConverter.java b/app/src/main/java/com/hjq/demo/permission/PermissionConverter.java new file mode 100644 index 00000000..41c9a8f6 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/permission/PermissionConverter.java @@ -0,0 +1,329 @@ +package com.hjq.demo.permission; + +import android.content.Context; +import android.text.TextUtils; +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.hjq.core.tools.AndroidVersion; +import com.hjq.demo.R; +import com.hjq.permissions.permission.PermissionGroups; +import com.hjq.permissions.permission.PermissionNames; +import com.hjq.permissions.permission.base.IPermission; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/XXPermissions + * time : 2025/05/30 + * desc : 权限转换器(根据权限获取对应的名称和说明) + */ +public final class PermissionConverter { + + /** 权限名称映射(为了适配多语种,这里存储的是 StringId,而不是 String) */ + private static final Map PERMISSION_NAME_MAP = new HashMap<>(); + + /** 权限描述映射(为了适配多语种,这里存储的是 StringId,而不是 String) */ + private static final Map PERMISSION_DESCRIPTION_MAP = new HashMap<>(); + + static { + PERMISSION_NAME_MAP.put(PermissionGroups.STORAGE, R.string.common_permission_storage); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_storage, R.string.common_permission_storage_description); + + PERMISSION_NAME_MAP.put(PermissionGroups.IMAGE_AND_VIDEO_MEDIA, R.string.common_permission_image_and_video); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_image_and_video, R.string.common_permission_image_and_video_description); + + PERMISSION_NAME_MAP.put(PermissionNames.READ_MEDIA_AUDIO, R.string.common_permission_music_and_audio); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_music_and_audio, R.string.common_permission_music_and_audio_description); + + PERMISSION_NAME_MAP.put(PermissionNames.CAMERA, R.string.common_permission_camera); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_camera, R.string.common_permission_camera_description); + + PERMISSION_NAME_MAP.put(PermissionNames.RECORD_AUDIO, R.string.common_permission_microphone); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_microphone, R.string.common_permission_microphone_description); + + PERMISSION_NAME_MAP.put(PermissionGroups.NEARBY_DEVICES, R.string.common_permission_nearby_devices); + // 注意:在 Android 13 的时候,WIFI 相关的权限已经归到附近设备的权限组了,但是在 Android 13 之前,WIFI 相关的权限归属定位权限组 + if (AndroidVersion.isAndroid13()) { + // 需要填充文案:蓝牙权限描述 + WIFI 权限描述 + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_nearby_devices, R.string.common_permission_nearby_devices_description); + } else { + // 需要填充文案:蓝牙权限描述 + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_nearby_devices, R.string.common_permission_nearby_devices_description); + } + + PERMISSION_NAME_MAP.put(PermissionGroups.LOCATION, R.string.common_permission_location); + // 注意:在 Android 12 的时候,蓝牙相关的权限已经归到附近设备的权限组了,但是在 Android 12 之前,蓝牙相关的权限归属定位权限组 + // 注意:在 Android 13 的时候,WIFI 相关的权限已经归到附近设备的权限组了,但是在 Android 13 之前,WIFI 相关的权限归属定位权限组 + if (AndroidVersion.isAndroid13()) { + // 需要填充文案:前台定位权限描述 + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_location, R.string.common_permission_location_description); + } else if (AndroidVersion.isAndroid12()) { + // 需要填充文案:前台定位权限描述 + WIFI 权限描述 + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_location, R.string.common_permission_location_description); + } else { + // 需要填充文案:前台定位权限描述 + 蓝牙权限描述 + WIFI 权限描述 + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_location, R.string.common_permission_location_description); + } + + // 后台定位权限虽然属于定位权限组,但是只要是属于后台权限,都有独属于自己的一套规则 + PERMISSION_NAME_MAP.put(PermissionNames.ACCESS_BACKGROUND_LOCATION, R.string.common_permission_location_background); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_location_background, R.string.common_permission_location_background_description); + + int sensorsPermissionNameStringId; + if (AndroidVersion.isAndroid16()) { + sensorsPermissionNameStringId = R.string.common_permission_health_data; + } else { + sensorsPermissionNameStringId = R.string.common_permission_body_sensors; + } + PERMISSION_NAME_MAP.put(PermissionGroups.SENSORS, sensorsPermissionNameStringId); + PERMISSION_DESCRIPTION_MAP.put(sensorsPermissionNameStringId, R.string.common_permission_body_sensors_description); + + // 后台传感器权限虽然属于传感器权限组,但是只要是属于后台权限,都有独属于自己的一套规则 + int bodySensorsBackgroundPermissionNameStringId; + if (AndroidVersion.isAndroid16()) { + bodySensorsBackgroundPermissionNameStringId = R.string.common_permission_health_data_background; + } else { + bodySensorsBackgroundPermissionNameStringId = R.string.common_permission_body_sensors_background; + } + PERMISSION_NAME_MAP.put(PermissionNames.BODY_SENSORS_BACKGROUND, bodySensorsBackgroundPermissionNameStringId); + PERMISSION_DESCRIPTION_MAP.put(bodySensorsBackgroundPermissionNameStringId, R.string.common_permission_body_sensors_background_description); + + // Android 16 这个版本开始,传感器权限被进行了精细化拆分,拆分成了无数个健康权限 + PERMISSION_NAME_MAP.put(PermissionGroups.HEALTH, R.string.common_permission_health_data); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_health_data, R.string.common_permission_health_data_description); + + PERMISSION_NAME_MAP.put(PermissionNames.READ_HEALTH_DATA_IN_BACKGROUND, R.string.common_permission_health_data_background); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_health_data_background, R.string.common_permission_health_data_background_description); + + PERMISSION_NAME_MAP.put(PermissionNames.READ_HEALTH_DATA_HISTORY, R.string.common_permission_health_data_past); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_health_data_past, R.string.common_permission_health_data_past_description); + + PERMISSION_NAME_MAP.put(PermissionGroups.CALL_LOG, R.string.common_permission_call_logs); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_call_logs, R.string.common_permission_call_logs_description); + + PERMISSION_NAME_MAP.put(PermissionGroups.PHONE, R.string.common_permission_phone); + // 注意:在 Android 9.0 的时候,读写通话记录权限已经归到一个单独的权限组了,但是在 Android 9.0 之前,读写通话记录权限归属电话权限组 + if (AndroidVersion.isAndroid9()) { + // 需要填充文案:电话权限描述 + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_phone, R.string.common_permission_phone_description); + } else { + // 需要填充文案:电话权限描述 + 通话记录权限描述 + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_phone, R.string.common_permission_phone_description); + } + + PERMISSION_NAME_MAP.put(PermissionGroups.CONTACTS, R.string.common_permission_contacts); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_contacts, R.string.common_permission_contacts_description); + + PERMISSION_NAME_MAP.put(PermissionGroups.CALENDAR, R.string.common_permission_calendar); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_calendar, R.string.common_permission_calendar_description); + + // 注意:在 Android 10 的版本,这个权限的名称为《健身运动权限》,但是到了 Android 11 的时候,这个权限的名称被修改成了《身体活动权限》 + // 没错就改了一下权限的叫法,其他的一切没有变,Google 产品经理真的是闲的蛋疼,但是吐槽归吐槽,框架也要灵活应对一下,避免小白用户跳转到设置页找不到对应的选项 + int activityRecognitionPermissionNameStringId = AndroidVersion.isAndroid11() ? R.string.common_permission_activity_recognition_api30 : R.string.common_permission_activity_recognition_api29; + PERMISSION_NAME_MAP.put(PermissionNames.ACTIVITY_RECOGNITION, activityRecognitionPermissionNameStringId); + PERMISSION_DESCRIPTION_MAP.put(activityRecognitionPermissionNameStringId, R.string.common_permission_activity_recognition_description); + + PERMISSION_NAME_MAP.put(PermissionNames.ACCESS_MEDIA_LOCATION, R.string.common_permission_access_media_location_information); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_access_media_location_information, R.string.common_permission_access_media_location_information_description); + + PERMISSION_NAME_MAP.put(PermissionGroups.SMS, R.string.common_permission_sms); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_sms, R.string.common_permission_sms_description); + + PERMISSION_NAME_MAP.put(PermissionNames.GET_INSTALLED_APPS, R.string.common_permission_get_installed_apps); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_get_installed_apps, R.string.common_permission_get_installed_apps_description); + + PERMISSION_NAME_MAP.put(PermissionNames.MANAGE_EXTERNAL_STORAGE, R.string.common_permission_all_file_access); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_all_file_access, R.string.common_permission_all_file_access_description); + + PERMISSION_NAME_MAP.put(PermissionNames.REQUEST_INSTALL_PACKAGES, R.string.common_permission_install_unknown_apps); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_install_unknown_apps, R.string.common_permission_install_unknown_apps_description); + + PERMISSION_NAME_MAP.put(PermissionNames.SYSTEM_ALERT_WINDOW, R.string.common_permission_display_over_other_apps); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_display_over_other_apps, R.string.common_permission_display_over_other_apps_description); + + PERMISSION_NAME_MAP.put(PermissionNames.WRITE_SETTINGS, R.string.common_permission_modify_system_settings); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_modify_system_settings, R.string.common_permission_modify_system_settings_description); + + PERMISSION_NAME_MAP.put(PermissionNames.NOTIFICATION_SERVICE, R.string.common_permission_allow_notifications); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_allow_notifications, R.string.common_permission_allow_notifications_description); + + PERMISSION_NAME_MAP.put(PermissionNames.POST_NOTIFICATIONS, R.string.common_permission_post_notifications); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_post_notifications, R.string.common_permission_post_notifications_description); + + PERMISSION_NAME_MAP.put(PermissionNames.BIND_NOTIFICATION_LISTENER_SERVICE, R.string.common_permission_allow_notifications_access); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_allow_notifications_access, R.string.common_permission_allow_notifications_access_description); + + PERMISSION_NAME_MAP.put(PermissionNames.PACKAGE_USAGE_STATS, R.string.common_permission_apps_with_usage_access); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_apps_with_usage_access, R.string.common_permission_apps_with_usage_access_description); + + PERMISSION_NAME_MAP.put(PermissionNames.SCHEDULE_EXACT_ALARM, R.string.common_permission_alarms_reminders); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_alarms_reminders, R.string.common_permission_alarms_reminders_description); + + PERMISSION_NAME_MAP.put(PermissionNames.ACCESS_NOTIFICATION_POLICY, R.string.common_permission_do_not_disturb_access); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_do_not_disturb_access, R.string.common_permission_do_not_disturb_access_description); + + PERMISSION_NAME_MAP.put(PermissionNames.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, R.string.common_permission_ignore_battery_optimize); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_ignore_battery_optimize, R.string.common_permission_ignore_battery_optimize_description); + + PERMISSION_NAME_MAP.put(PermissionNames.BIND_VPN_SERVICE, R.string.common_permission_vpn); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_vpn, R.string.common_permission_vpn_description); + + PERMISSION_NAME_MAP.put(PermissionNames.PICTURE_IN_PICTURE, R.string.common_permission_picture_in_picture); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_picture_in_picture, R.string.common_permission_picture_in_picture_description); + + PERMISSION_NAME_MAP.put(PermissionNames.USE_FULL_SCREEN_INTENT, R.string.common_permission_full_screen_notifications); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_full_screen_notifications, R.string.common_permission_full_screen_notifications_description); + + PERMISSION_NAME_MAP.put(PermissionNames.BIND_DEVICE_ADMIN, R.string.common_permission_device_admin); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_device_admin, R.string.common_permission_device_admin_description); + + PERMISSION_NAME_MAP.put(PermissionNames.BIND_ACCESSIBILITY_SERVICE, R.string.common_permission_accessibility_service); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_accessibility_service, R.string.common_permission_accessibility_service_description); + + PERMISSION_NAME_MAP.put(PermissionNames.MANAGE_MEDIA, R.string.common_permission_manage_media); + PERMISSION_DESCRIPTION_MAP.put(R.string.common_permission_manage_media, R.string.common_permission_manage_media_description); + } + + /** + * 通过权限获得名称 + */ + @NonNull + public static String getNickNamesByPermissions(@NonNull Context context, @NonNull List permissions) { + List permissionNameList = getNickNameListByPermissions(context, permissions, true); + + StringBuilder builder = new StringBuilder(); + for (String permissionName : permissionNameList) { + if (TextUtils.isEmpty(permissionName)) { + continue; + } + if (builder.length() == 0) { + builder.append(permissionName); + } else { + builder.append(context.getString(R.string.common_permission_comma)) + .append(permissionName); + } + } + if (builder.length() == 0) { + // 如果没有获得到任何信息,则返回一个默认的文本 + return context.getString(R.string.common_permission_unknown); + } + return builder.toString(); + } + + @NonNull + public static List getNickNameListByPermissions(@NonNull Context context, @NonNull List permissions, boolean filterHighVersionPermissions) { + List permissionNickNameList = new ArrayList<>(); + for (IPermission permission : permissions) { + // 如果当前设置了过滤高版本权限,并且这个权限是高版本系统才出现的权限,则不继续往下执行 + // 避免出现在低版本上面执行拒绝权限后,连带高版本的名称也一起显示出来,但是在低版本上面是没有这个权限的 + if (filterHighVersionPermissions && permission.getFromAndroidVersion(context) > AndroidVersion.getSdkVersion()) { + continue; + } + String permissionName = getNickNameByPermission(context, permission); + if (TextUtils.isEmpty(permissionName)) { + continue; + } + if (permissionNickNameList.contains(permissionName)) { + continue; + } + permissionNickNameList.add(permissionName); + } + return permissionNickNameList; + } + + public static String getNickNameByPermission(@NonNull Context context, @NonNull IPermission permission) { + Integer permissionNameStringId = getPermissionNickNameStringId(context, permission); + if (permissionNameStringId == null || permissionNameStringId == 0) { + return ""; + } + return context.getString(permissionNameStringId); + } + + /** + * 通过权限获得描述 + */ + @NonNull + public static String getDescriptionsByPermissions(@NonNull Context context, @NonNull List permissions) { + List descriptionList = getDescriptionListByPermissions(context, permissions); + + StringBuilder builder = new StringBuilder(); + for (String description : descriptionList) { + if (TextUtils.isEmpty(description)) { + continue; + } + if (builder.length() == 0) { + builder.append(description); + } else { + builder.append("\n") + .append(description); + } + } + return builder.toString(); + } + + @NonNull + public static List getDescriptionListByPermissions(@NonNull Context context, @NonNull List permissions) { + List descriptionList = new ArrayList<>(); + for (IPermission permission : permissions) { + String permissionDescription = getDescriptionByPermission(context, permission); + if (TextUtils.isEmpty(permissionDescription)) { + continue; + } + if (descriptionList.contains(permissionDescription)) { + continue; + } + descriptionList.add(permissionDescription); + } + return descriptionList; + } + + /** + * 通过权限获得描述 + */ + @NonNull + public static String getDescriptionByPermission(@NonNull Context context, @NonNull IPermission permission) { + Integer permissionNameStringId = getPermissionNickNameStringId(context, permission); + if (permissionNameStringId == null || permissionNameStringId == 0) { + return ""; + } + String permissionNickName = context.getString(permissionNameStringId); + Integer permissionDescriptionStringId = getPermissionDescriptionStringId(permissionNameStringId); + String permissionDescription; + if (permissionDescriptionStringId == null || permissionDescriptionStringId == 0) { + permissionDescription = ""; + } else { + permissionDescription = context.getString(permissionDescriptionStringId); + } + return permissionNickName + context.getString(R.string.common_permission_colon) + permissionDescription; + } + + /** + * 获取这个权限对应的别名 StringId + */ + @Nullable + public static Integer getPermissionNickNameStringId(@NonNull Context context, @NonNull IPermission permission) { + String permissionName = permission.getPermissionName(); + String permissionGroup = permission.getPermissionGroup(context); + Integer permissionNameStringId = PERMISSION_NAME_MAP.get(permissionName); + if (permissionNameStringId != null && permissionNameStringId > 0) { + return permissionNameStringId; + } + Integer permissionGroupStringId = PERMISSION_NAME_MAP.get(permissionGroup); + if (permissionGroupStringId != null && permissionGroupStringId > 0) { + return permissionGroupStringId; + } + return permissionNameStringId; + } + + /** + * 获取这个权限对应的描述 StringId + */ + @Nullable + public static Integer getPermissionDescriptionStringId(@IdRes int permissionNickNameStringId) { + return PERMISSION_DESCRIPTION_MAP.get(permissionNickNameStringId); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/permission/PermissionDescription.java b/app/src/main/java/com/hjq/demo/permission/PermissionDescription.java new file mode 100644 index 00000000..abaf5681 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/permission/PermissionDescription.java @@ -0,0 +1,233 @@ +package com.hjq.demo.permission; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Point; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.util.DisplayMetrics; +import android.view.Display; +import android.view.Gravity; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.PopupWindow; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.hjq.demo.R; +import com.hjq.demo.ui.dialog.common.MessageDialog; +import com.hjq.demo.ui.popup.PermissionDescriptionPopup; +import com.hjq.permissions.OnPermissionDescription; +import com.hjq.permissions.permission.PermissionPageType; +import com.hjq.permissions.permission.base.IPermission; +import java.util.List; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/XXPermissions + * time : 2025/05/30 + * desc : 权限请求描述实现 + */ +public final class PermissionDescription implements OnPermissionDescription { + + /** 消息处理 Handler 对象 */ + public static final Handler HANDLER = new Handler(Looper.getMainLooper()); + + /** 权限请求描述弹窗显示类型:Dialog */ + private static final int DESCRIPTION_WINDOW_TYPE_DIALOG = 0; + /** 权限请求描述弹窗显示类型:PopupWindow */ + private static final int DESCRIPTION_WINDOW_TYPE_POPUP = 1; + + /** 权限请求描述弹窗显示类型 */ + private int mDescriptionWindowType = DESCRIPTION_WINDOW_TYPE_DIALOG; + + /** 消息 Token */ + @NonNull + private final Object mHandlerToken = new Object(); + + /** 权限申请说明弹窗 */ + @Nullable + private PopupWindow mPermissionPopupWindow; + + /** 权限申请说明对话框 */ + @Nullable + private Dialog mPermissionDialog; + + @Override + public void askWhetherRequestPermission(@NonNull Activity activity, + @NonNull List requestList, + @NonNull Runnable continueRequestRunnable, + @NonNull Runnable breakRequestRunnable) { + // 以下情况使用 Dialog 来展示权限说明弹窗,否则使用 PopupWindow 来展示权限说明弹窗 + // 1. 如果请求的权限显示的系统界面是不透明的 Activity + // 2. 如果当前 Activity 的屏幕是横屏状态的话,要求物理尺寸要够大,否则显示的顶部弹窗会被遮挡住, + // 设备的物理屏幕尺寸还小于 8.5 寸(目前大多数小屏平板大多数集中在 8、8.7、8.8、10 寸), + // 实测 8 寸的平板获取到的物理尺寸到只有 7.958788793906728,所以这里的代码判断基本上是针对 8.5 寸及以上的平板做优化。 + if (isActivityLandscape(activity) && getPhysicalScreenSize(activity) < 8.5) { + mDescriptionWindowType = DESCRIPTION_WINDOW_TYPE_DIALOG; + } else { + mDescriptionWindowType = DESCRIPTION_WINDOW_TYPE_POPUP; + for (IPermission permission : requestList) { + if (permission.getPermissionPageType(activity) == PermissionPageType.OPAQUE_ACTIVITY) { + mDescriptionWindowType = DESCRIPTION_WINDOW_TYPE_DIALOG; + } + } + } + + if (mDescriptionWindowType == DESCRIPTION_WINDOW_TYPE_POPUP) { + continueRequestRunnable.run(); + return; + } + + showDialog(activity, activity.getString(R.string.common_permission_description_title), + generatePermissionDescription(activity, requestList), + activity.getString(R.string.common_permission_confirm), dialog -> { + dialog.dismiss(); + continueRequestRunnable.run(); + }); + } + + @Override + public void onRequestPermissionStart(@NonNull Activity activity, @NonNull List requestList) { + if (mDescriptionWindowType != DESCRIPTION_WINDOW_TYPE_POPUP) { + return; + } + + Runnable showPopupRunnable = () -> showPopupWindow(activity, generatePermissionDescription(activity, requestList)); + // 这里解释一下为什么要延迟一段时间再显示 PopupWindow,这是因为系统没有开放任何 API 给外层直接获取权限是否永久拒绝 + // 目前只有申请过了权限才能通过 shouldShowRequestPermissionRationale 判断是不是永久拒绝,如果此前没有申请过权限,则无法判断 + // 针对这个问题能想到最佳的解决方案是:先申请权限,如果极短的时间内,权限申请没有结束,则证明权限之前没有被用户勾选了《不再询问》 + // 此时系统的权限弹窗正在显示给用户,这个时候再去显示应用的 PopupWindow 权限说明弹窗给用户看,所以这个 PopupWindow 是在发起权限申请后才显示的 + // 这样做是为了避免 PopupWindow 显示了又马上消失,这样就不会出现 PopupWindow 一闪而过的效果,提升用户的视觉体验 + // 最后补充一点:350 毫秒只是一个经验值,经过测试可覆盖大部分机型,具体可根据实际情况进行调整,这里不做强制要求 + // 相关 Github issue 地址:https://github.com/getActivity/XXPermissions/issues/366 + HANDLER.postAtTime(showPopupRunnable, mHandlerToken, SystemClock.uptimeMillis() + 350); + } + + @Override + public void onRequestPermissionEnd(@NonNull Activity activity, @NonNull List requestList) { + // 移除跟这个 Token 有关但是没有还没有执行的消息 + HANDLER.removeCallbacksAndMessages(mHandlerToken); + // 销毁当前正在显示的弹窗 + dismissPopupWindow(); + dismissDialog(); + } + + /** + * 生成权限描述文案 + */ + private String generatePermissionDescription(@NonNull Activity activity, @NonNull List requestList) { + return PermissionConverter.getDescriptionsByPermissions(activity, requestList); + } + + /** + * 显示 Dialog + * + * @param dialogTitle 对话框标题 + * @param dialogMessage 对话框消息 + * @param confirmButtonText 对话框确认按钮文本 + * @param listener 对话框监听事件 + */ + private void showDialog(@NonNull Activity activity, @Nullable String dialogTitle, @Nullable String dialogMessage, + @Nullable String confirmButtonText, @Nullable MessageDialog.OnListener listener) { + if (mPermissionDialog != null) { + dismissDialog(); + } + if (activity.isFinishing() || activity.isDestroyed()) { + return; + } + // 另外这里需要判断 Activity 的类型来申请权限,这是因为只有 AppCompatActivity 才能调用 AndroidX 库的 AlertDialog 来显示,否则会出现报错 + // java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity + // 为什么不直接用系统包 AlertDialog 来显示,而是两套规则?因为系统包 AlertDialog 是系统自带的类,不同 Android 版本展现的样式可能不太一样 + // 如果这个 Android 版本比较低,那么这个对话框的样式就会变得很丑,准确来讲也不能说丑,而是当时系统的 UI 设计就是那样,它只是跟随系统的样式而已 + mPermissionDialog = new MessageDialog.Builder(activity) + .setTitle(dialogTitle) + .setMessage(dialogMessage) + .setConfirm(confirmButtonText) + .setCancelable(false) + .setListener(listener) + .create(); + mPermissionDialog.show(); + } + + /** + * 销毁 Dialog + */ + private void dismissDialog() { + if (mPermissionDialog == null) { + return; + } + if (!mPermissionDialog.isShowing()) { + return; + } + mPermissionDialog.dismiss(); + mPermissionDialog = null; + } + + /** + * 显示 PopupWindow + * + * @param content 弹窗显示的内容 + */ + private void showPopupWindow(@NonNull Activity activity, @NonNull String content) { + if (mPermissionPopupWindow != null) { + dismissPopupWindow(); + } + if (activity.isFinishing() || activity.isDestroyed()) { + return; + } + ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView(); + mPermissionPopupWindow = new PermissionDescriptionPopup.Builder(activity) + .setDescription(content) + .create(); + mPermissionPopupWindow.showAtLocation(decorView, Gravity.TOP, 0, 0); + } + + /** + * 销毁 PopupWindow + */ + private void dismissPopupWindow() { + if (mPermissionPopupWindow == null) { + return; + } + if (!mPermissionPopupWindow.isShowing()) { + return; + } + mPermissionPopupWindow.dismiss(); + mPermissionPopupWindow = null; + } + + /** + * 判断当前 Activity 是否是横盘显示 + */ + public static boolean isActivityLandscape(@NonNull Activity activity) { + return activity.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; + } + + /** + * 获取当前设备的物理屏幕尺寸 + */ + @SuppressWarnings("deprecation") + public static double getPhysicalScreenSize(@NonNull Context context) { + WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + Display defaultDisplay = windowManager.getDefaultDisplay(); + if (defaultDisplay == null) { + return 0; + } + + DisplayMetrics metrics = new DisplayMetrics(); + defaultDisplay.getMetrics(metrics); + + float screenWidthInInches; + float screenHeightInInches; + Point point = new Point(); + defaultDisplay.getRealSize(point); + screenWidthInInches = point.x / metrics.xdpi; + screenHeightInInches = point.y / metrics.ydpi; + + // 勾股定理:直角三角形的两条直角边的平方和等于斜边的平方 + return Math.sqrt(Math.pow(screenWidthInInches, 2) + Math.pow(screenHeightInInches, 2)); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/permission/PermissionInterceptor.java b/app/src/main/java/com/hjq/demo/permission/PermissionInterceptor.java new file mode 100644 index 00000000..00c97f21 --- /dev/null +++ b/app/src/main/java/com/hjq/demo/permission/PermissionInterceptor.java @@ -0,0 +1,235 @@ +package com.hjq.demo.permission; + +import android.app.Activity; +import android.content.Context; +import android.content.pm.PackageManager; +import android.text.TextUtils; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.hjq.core.tools.AndroidVersion; +import com.hjq.demo.R; +import com.hjq.demo.ui.dialog.common.MessageDialog; +import com.hjq.permissions.OnPermissionCallback; +import com.hjq.permissions.OnPermissionInterceptor; +import com.hjq.permissions.XXPermissions; +import com.hjq.permissions.permission.PermissionGroups; +import com.hjq.permissions.permission.PermissionNames; +import com.hjq.permissions.permission.base.IPermission; +import com.hjq.toast.Toaster; +import java.util.List; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/XXPermissions + * time : 2021/01/04 + * desc : 权限申请拦截器 + */ +public final class PermissionInterceptor implements OnPermissionInterceptor { + + @Override + public void onRequestPermissionEnd(@NonNull Activity activity, boolean skipRequest, + @NonNull List requestList, + @NonNull List grantedList, + @NonNull List deniedList, + @Nullable OnPermissionCallback callback) { + if (callback != null) { + callback.onResult(grantedList, deniedList); + } + + if (deniedList.isEmpty()) { + return; + } + boolean doNotAskAgain = XXPermissions.isDoNotAskAgainPermissions(activity, deniedList); + String permissionHint = generatePermissionHint(activity, deniedList, doNotAskAgain); + if (!doNotAskAgain) { + // 如果没有勾选不再询问选项,就弹 Toast 提示给用户 + Toaster.show(permissionHint); + return; + } + + // 如果勾选了不再询问选项,就弹 Dialog 引导用户去授权 + showPermissionSettingDialog(activity, requestList, deniedList, callback, permissionHint); + } + + private void showPermissionSettingDialog(@NonNull Activity activity, + @NonNull List requestList, + @NonNull List deniedList, + @Nullable OnPermissionCallback callback, + @NonNull String permissionHint) { + if (activity.isFinishing() || activity.isDestroyed()) { + return; + } + + new MessageDialog.Builder(activity) + .setTitle(R.string.common_permission_alert) + .setMessage(permissionHint) + .setConfirm(R.string.common_permission_go_to_authorization) + .setListener(dialog -> { + dialog.dismiss(); + XXPermissions.startPermissionActivity(activity, deniedList, (grantedList, deniedList1) -> { + List latestDeniedList = XXPermissions.getDeniedPermissions(activity, requestList); + boolean allGranted = latestDeniedList.isEmpty(); + if (!allGranted) { + // 递归显示对话框,让提示用户授权,只不过对话框是可取消的,用户不想授权了,随时可以点击返回键或者对话框蒙层来取消显示 + showPermissionSettingDialog(activity, requestList, latestDeniedList, callback, + generatePermissionHint(activity, latestDeniedList, true)); + return; + } + + if (callback == null) { + return; + } + // 用户全部授权了,回调成功给外层监听器,免得用户还要再发起权限申请 + callback.onResult(requestList, latestDeniedList); + }); + }) + .show(); + } + + /** + * 生成权限提示文案 + */ + @NonNull + private String generatePermissionHint(@NonNull Activity activity, @NonNull List deniedList, boolean doNotAskAgain) { + int deniedPermissionCount = deniedList.size(); + int deniedLocationPermissionCount = 0; + int deniedSensorsPermissionCount = 0; + int deniedHealthPermissionCount = 0; + for (IPermission deniedPermission : deniedList) { + String permissionGroup = deniedPermission.getPermissionGroup(activity); + if (TextUtils.isEmpty(permissionGroup)) { + continue; + } + if (PermissionGroups.LOCATION.equals(permissionGroup)) { + deniedLocationPermissionCount++; + } else if (PermissionGroups.SENSORS.equals(permissionGroup)) { + deniedSensorsPermissionCount++; + } else if (XXPermissions.isHealthPermission(deniedPermission)) { + deniedHealthPermissionCount++; + } + } + + if (deniedLocationPermissionCount == deniedPermissionCount && AndroidVersion.isAndroid10()) { + if (deniedLocationPermissionCount == 1) { + if (XXPermissions.equalsPermission(deniedList.get(0), PermissionNames.ACCESS_BACKGROUND_LOCATION)) { + return activity.getString(R.string.common_permission_fail_hint_1, + activity.getString(R.string.common_permission_location_background), + getBackgroundPermissionOptionLabel(activity)); + } else if (AndroidVersion.isAndroid12() && + XXPermissions.equalsPermission(deniedList.get(0), PermissionNames.ACCESS_FINE_LOCATION)) { + // 如果请求的定位权限中,既包含了精确定位权限,又包含了模糊定位权限或者后台定位权限, + // 但是用户只同意了模糊定位权限的情况或者后台定位权限,并没有同意精确定位权限的情况,就提示用户开启确切位置选项 + // 需要注意的是 Android 12 才将模糊定位权限和精确定位权限的授权选项进行分拆,之前的版本没有区分得那么仔细 + return activity.getString(R.string.common_permission_fail_hint_3, + activity.getString(R.string.common_permission_location_fine), + activity.getString(R.string.common_permission_location_fine_option)); + } + } else { + if (XXPermissions.containsPermission(deniedList, PermissionNames.ACCESS_BACKGROUND_LOCATION)) { + if (AndroidVersion.isAndroid12() && + XXPermissions.containsPermission(deniedList, PermissionNames.ACCESS_FINE_LOCATION)) { + return activity.getString(R.string.common_permission_fail_hint_2, + activity.getString(R.string.common_permission_location), + getBackgroundPermissionOptionLabel(activity), + activity.getString(R.string.common_permission_location_fine_option)); + } else { + return activity.getString(R.string.common_permission_fail_hint_1, + activity.getString(R.string.common_permission_location), + getBackgroundPermissionOptionLabel(activity)); + } + } + } + } else if (deniedSensorsPermissionCount == deniedPermissionCount && AndroidVersion.isAndroid13()) { + if (deniedPermissionCount == 1) { + if (XXPermissions.equalsPermission(deniedList.get(0), PermissionNames.BODY_SENSORS_BACKGROUND)) { + if (AndroidVersion.isAndroid16()) { + return activity.getString(R.string.common_permission_fail_hint_1, + activity.getString(R.string.common_permission_health_data_background), + activity.getString(R.string.common_permission_health_data_background_option)); + } else { + return activity.getString(R.string.common_permission_fail_hint_1, + activity.getString(R.string.common_permission_body_sensors_background), + getBackgroundPermissionOptionLabel(activity)); + } + } + } else { + if (doNotAskAgain) { + if (AndroidVersion.isAndroid16()) { + return activity.getString(R.string.common_permission_fail_hint_1, + activity.getString(R.string.common_permission_health_data), + activity.getString(R.string.common_permission_allow_all_option)); + } else { + return activity.getString(R.string.common_permission_fail_hint_1, + activity.getString(R.string.common_permission_body_sensors), + getBackgroundPermissionOptionLabel(activity)); + } + } + } + } else if (deniedHealthPermissionCount == deniedPermissionCount && AndroidVersion.isAndroid16()) { + + switch (deniedPermissionCount) { + case 1: + if (XXPermissions.equalsPermission(deniedList.get(0), PermissionNames.READ_HEALTH_DATA_IN_BACKGROUND)) { + return activity.getString(R.string.common_permission_fail_hint_3, + activity.getString(R.string.common_permission_health_data_background), + activity.getString(R.string.common_permission_health_data_background_option)); + } else if (XXPermissions.equalsPermission(deniedList.get(0), PermissionNames.READ_HEALTH_DATA_HISTORY)) { + return activity.getString(R.string.common_permission_fail_hint_3, + activity.getString(R.string.common_permission_health_data_past), + activity.getString(R.string.common_permission_health_data_past_option)); + } + break; + case 2: + if (XXPermissions.containsPermission(deniedList, PermissionNames.READ_HEALTH_DATA_HISTORY) && + XXPermissions.containsPermission(deniedList, PermissionNames.READ_HEALTH_DATA_IN_BACKGROUND)) { + return activity.getString(R.string.common_permission_fail_hint_3, + activity.getString(R.string.common_permission_health_data_past) + activity.getString(R.string.common_permission_and) + activity.getString(R.string.common_permission_health_data_background), + activity.getString(R.string.common_permission_health_data_past_option) + activity.getString(R.string.common_permission_and) + activity.getString(R.string.common_permission_health_data_background_option)); + } else if (XXPermissions.containsPermission(deniedList, PermissionNames.READ_HEALTH_DATA_HISTORY)) { + return activity.getString(R.string.common_permission_fail_hint_2, + activity.getString(R.string.common_permission_health_data) + activity.getString(R.string.common_permission_and) + activity.getString(R.string.common_permission_health_data_past), + activity.getString(R.string.common_permission_allow_all_option), + activity.getString(R.string.common_permission_health_data_background_option)); + } else if (XXPermissions.containsPermission(deniedList, PermissionNames.READ_HEALTH_DATA_IN_BACKGROUND)) { + return activity.getString(R.string.common_permission_fail_hint_2, + activity.getString(R.string.common_permission_health_data) + activity.getString(R.string.common_permission_and) + activity.getString(R.string.common_permission_health_data_background), + activity.getString(R.string.common_permission_allow_all_option), + activity.getString(R.string.common_permission_health_data_background_option)); + } + break; + default: + if (XXPermissions.containsPermission(deniedList, PermissionNames.READ_HEALTH_DATA_HISTORY) && + XXPermissions.containsPermission(deniedList, PermissionNames.READ_HEALTH_DATA_IN_BACKGROUND)) { + return activity.getString(R.string.common_permission_fail_hint_2, + activity.getString(R.string.common_permission_health_data) + activity.getString(R.string.common_permission_and) + activity.getString(R.string.common_permission_health_data_past) + activity.getString(R.string.common_permission_and) + activity.getString(R.string.common_permission_health_data_background), + activity.getString(R.string.common_permission_allow_all_option), + activity.getString(R.string.common_permission_health_data_past_option) + activity.getString(R.string.common_permission_and) + activity.getString(R.string.common_permission_health_data_background_option)); + } + break; + } + return activity.getString(R.string.common_permission_fail_hint_1, + activity.getString(R.string.common_permission_health_data), + activity.getString(R.string.common_permission_allow_all_option)); + } + + return activity.getString(doNotAskAgain ? R.string.common_permission_fail_assign_hint_1 : + R.string.common_permission_fail_assign_hint_2, + PermissionConverter.getNickNamesByPermissions(activity, deniedList)); + } + + /** + * 获取后台权限的《始终允许》选项的文案 + */ + @NonNull + private String getBackgroundPermissionOptionLabel(@NonNull Context context) { + PackageManager packageManager = context.getPackageManager(); + if (packageManager != null && AndroidVersion.isAndroid11()) { + CharSequence backgroundPermissionOptionLabel = packageManager.getBackgroundPermissionOptionLabel(); + if (!TextUtils.isEmpty(backgroundPermissionOptionLabel)) { + return backgroundPermissionOptionLabel.toString(); + } + } + + return context.getString(R.string.common_permission_allow_all_the_time_option); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/ui/activity/AboutActivity.java b/app/src/main/java/com/hjq/demo/ui/activity/AboutActivity.java index 476ee77f..9e80dc34 100644 --- a/app/src/main/java/com/hjq/demo/ui/activity/AboutActivity.java +++ b/app/src/main/java/com/hjq/demo/ui/activity/AboutActivity.java @@ -1,5 +1,7 @@ package com.hjq.demo.ui.activity; +import android.view.View; +import androidx.annotation.Nullable; import com.hjq.demo.R; import com.hjq.demo.app.AppActivity; @@ -17,8 +19,18 @@ protected int getLayoutId() { } @Override - protected void initView() {} + protected void initView() { + + } @Override - protected void initData() {} + protected void initData() { + + } + + @Nullable + @Override + public View getImmersionBottomView() { + return findViewById(R.id.tv_about_copyright); + } } \ No newline at end of file diff --git a/app/src/main/java/com/hjq/demo/ui/activity/DialogActivity.java b/app/src/main/java/com/hjq/demo/ui/activity/DialogActivity.java index 860bcb0a..0b62c2fb 100644 --- a/app/src/main/java/com/hjq/demo/ui/activity/DialogActivity.java +++ b/app/src/main/java/com/hjq/demo/ui/activity/DialogActivity.java @@ -1,41 +1,39 @@ package com.hjq.demo.ui.activity; -import android.content.Intent; import android.view.Gravity; import android.view.View; import android.widget.Button; - +import androidx.annotation.NonNull; import androidx.annotation.Nullable; - +import com.hjq.bar.TitleBar; import com.hjq.base.BaseDialog; +import com.hjq.base.BasePopupWindow; import com.hjq.demo.R; import com.hjq.demo.aop.SingleClick; import com.hjq.demo.app.AppActivity; import com.hjq.demo.manager.DialogManager; -import com.hjq.demo.ui.dialog.AddressDialog; -import com.hjq.demo.ui.dialog.DateDialog; -import com.hjq.demo.ui.dialog.InputDialog; -import com.hjq.demo.ui.dialog.MenuDialog; -import com.hjq.demo.ui.dialog.MessageDialog; import com.hjq.demo.ui.dialog.PayPasswordDialog; import com.hjq.demo.ui.dialog.SafeDialog; -import com.hjq.demo.ui.dialog.SelectDialog; -import com.hjq.demo.ui.dialog.ShareDialog; -import com.hjq.demo.ui.dialog.TimeDialog; -import com.hjq.demo.ui.dialog.TipsDialog; import com.hjq.demo.ui.dialog.UpdateDialog; -import com.hjq.demo.ui.dialog.WaitDialog; +import com.hjq.demo.ui.dialog.common.AddressDialog; +import com.hjq.demo.ui.dialog.common.DateDialog; +import com.hjq.demo.ui.dialog.common.InputDialog; +import com.hjq.demo.ui.dialog.common.MenuDialog; +import com.hjq.demo.ui.dialog.common.MessageDialog; +import com.hjq.demo.ui.dialog.common.SelectDialog; +import com.hjq.demo.ui.dialog.common.ShareDialog; +import com.hjq.demo.ui.dialog.common.TimeDialog; +import com.hjq.demo.ui.dialog.common.TipsDialog; +import com.hjq.demo.ui.dialog.common.WaitDialog; import com.hjq.demo.ui.popup.ListPopup; -import com.hjq.umeng.Platform; -import com.hjq.umeng.UmengClient; -import com.hjq.umeng.UmengShare; +import com.hjq.umeng.sdk.Platform; +import com.hjq.umeng.sdk.UmengShare; import com.umeng.socialize.media.UMImage; import com.umeng.socialize.media.UMWeb; - import java.util.ArrayList; import java.util.Calendar; -import java.util.HashMap; import java.util.List; +import java.util.Map; /** * author : Android 轮子哥 @@ -48,6 +46,9 @@ public final class DialogActivity extends AppActivity { /** 等待对话框 */ private BaseDialog mWaitDialog; + /** 菜单弹窗 */ + private BasePopupWindow mListPopup; + @Override protected int getLayoutId() { return R.layout.dialog_activity; @@ -72,14 +73,20 @@ protected void initData() { } + @Nullable + @Override + public View getImmersionBottomView() { + return findViewById(R.id.ll_dialog_content); + } + @SingleClick @Override - public void onClick(View view) { + public void onClick(@NonNull View view) { int viewId = view.getId(); if (viewId == R.id.btn_dialog_message) { // 消息对话框 - new MessageDialog.Builder(getActivity()) + new MessageDialog.Builder(this) // 标题可以不用填写 .setTitle("我是标题") // 内容必须要填写 @@ -93,12 +100,12 @@ public void onClick(View view) { .setListener(new MessageDialog.OnListener() { @Override - public void onConfirm(BaseDialog dialog) { + public void onConfirm(@NonNull BaseDialog dialog) { toast("确定了"); } @Override - public void onCancel(BaseDialog dialog) { + public void onCancel(@NonNull BaseDialog dialog) { toast("取消了"); } }) @@ -123,12 +130,12 @@ public void onCancel(BaseDialog dialog) { .setListener(new InputDialog.OnListener() { @Override - public void onConfirm(BaseDialog dialog, String content) { + public void onConfirm(@NonNull BaseDialog dialog, String content) { toast("确定了:" + content); } @Override - public void onCancel(BaseDialog dialog) { + public void onCancel(@NonNull BaseDialog dialog) { toast("取消了"); } }) @@ -137,7 +144,7 @@ public void onCancel(BaseDialog dialog) { } else if (viewId == R.id.btn_dialog_bottom_menu) { List data = new ArrayList<>(); - for (int i = 0; i < 10; i++) { + for (int i = 0; i < 20; i++) { data.add("我是数据" + (i + 1)); } // 底部选择框 @@ -150,12 +157,12 @@ public void onCancel(BaseDialog dialog) { .setListener(new MenuDialog.OnListener() { @Override - public void onSelected(BaseDialog dialog, int position, String string) { - toast("位置:" + position + ",文本:" + string); + public void onSelected(@NonNull BaseDialog dialog, int position, String data) { + toast("位置:" + position + ",文本:" + data); } @Override - public void onCancel(BaseDialog dialog) { + public void onCancel(@NonNull BaseDialog dialog) { toast("取消了"); } }) @@ -164,7 +171,7 @@ public void onCancel(BaseDialog dialog) { } else if (viewId == R.id.btn_dialog_center_menu) { List data = new ArrayList<>(); - for (int i = 0; i < 10; i++) { + for (int i = 0; i < 20; i++) { data.add("我是数据" + (i + 1)); } // 居中选择框 @@ -178,12 +185,12 @@ public void onCancel(BaseDialog dialog) { .setListener(new MenuDialog.OnListener() { @Override - public void onSelected(BaseDialog dialog, int position, String string) { - toast("位置:" + position + ",文本:" + string); + public void onSelected(@NonNull BaseDialog dialog, int position, String data) { + toast("位置:" + position + ",文本:" + data); } @Override - public void onCancel(BaseDialog dialog) { + public void onCancel(@NonNull BaseDialog dialog) { toast("取消了"); } }) @@ -199,15 +206,15 @@ public void onCancel(BaseDialog dialog) { .setSingleSelect() // 设置默认选中 .setSelect(0) - .setListener(new SelectDialog.OnListener() { + .setSingleListener(new SelectDialog.OnSingleListener() { @Override - public void onSelected(BaseDialog dialog, HashMap data) { - toast("确定了:" + data.toString()); + public void onSelected(@NonNull BaseDialog dialog, int position, String data) { + toast("位置:" + position + ",数据:" + data); } @Override - public void onCancel(BaseDialog dialog) { + public void onCancel(@NonNull BaseDialog dialog) { toast("取消了"); } }) @@ -223,15 +230,15 @@ public void onCancel(BaseDialog dialog) { .setMaxSelect(3) // 设置默认选中 .setSelect(2, 3, 4) - .setListener(new SelectDialog.OnListener() { + .setMultiListener(new SelectDialog.OnMultiListener() { @Override - public void onSelected(BaseDialog dialog, HashMap data) { + public void onSelected(@NonNull BaseDialog dialog, Map data) { toast("确定了:" + data.toString()); } @Override - public void onCancel(BaseDialog dialog) { + public void onCancel(@NonNull BaseDialog dialog) { toast("取消了"); } }) @@ -285,12 +292,12 @@ public void onCancel(BaseDialog dialog) { .setListener(new PayPasswordDialog.OnListener() { @Override - public void onCompleted(BaseDialog dialog, String password) { + public void onCompleted(@NonNull BaseDialog dialog, String password) { toast(password); } @Override - public void onCancel(BaseDialog dialog) { + public void onCancel(@NonNull BaseDialog dialog) { toast("取消了"); } }) @@ -310,12 +317,12 @@ public void onCancel(BaseDialog dialog) { .setListener(new AddressDialog.OnListener() { @Override - public void onSelected(BaseDialog dialog, String province, String city, String area) { + public void onSelected(@NonNull BaseDialog dialog, @NonNull String province, @NonNull String city, @NonNull String area) { toast(province + city + area); } @Override - public void onCancel(BaseDialog dialog) { + public void onCancel(@NonNull BaseDialog dialog) { toast("取消了"); } }) @@ -344,7 +351,7 @@ public void onCancel(BaseDialog dialog) { //.setIgnoreDay() .setListener(new DateDialog.OnListener() { @Override - public void onSelected(BaseDialog dialog, int year, int month, int day) { + public void onSelected(@NonNull BaseDialog dialog, int year, int month, int day) { toast(year + getString(R.string.common_year) + month + getString(R.string.common_month) + day + getString(R.string.common_day)); // 如果不指定时分秒则默认为现在的时间 @@ -358,7 +365,7 @@ public void onSelected(BaseDialog dialog, int year, int month, int day) { } @Override - public void onCancel(BaseDialog dialog) { + public void onCancel(@NonNull BaseDialog dialog) { toast("取消了"); } }) @@ -387,7 +394,7 @@ public void onCancel(BaseDialog dialog) { .setListener(new TimeDialog.OnListener() { @Override - public void onSelected(BaseDialog dialog, int hour, int minute, int second) { + public void onSelected(@NonNull BaseDialog dialog, int hour, int minute, int second) { toast(hour + getString(R.string.common_hour) + minute + getString(R.string.common_minute) + second + getString(R.string.common_second)); // 如果不指定年月日则默认为今天的日期 @@ -400,7 +407,7 @@ public void onSelected(BaseDialog dialog, int hour, int minute, int second) { } @Override - public void onCancel(BaseDialog dialog) { + public void onCancel(@NonNull BaseDialog dialog) { toast("取消了"); } }) @@ -410,28 +417,30 @@ public void onCancel(BaseDialog dialog) { toast("记得改好第三方 AppID 和 Secret,否则会调不起来哦"); - UMWeb content = new UMWeb("https://github.com/getActivity/AndroidProject"); - content.setTitle("Github"); - content.setThumb(new UMImage(this, R.mipmap.launcher_ic)); - content.setDescription(getString(R.string.app_name)); + UMWeb umWeb = new UMWeb("https://github.com/getActivity/AndroidProject"); + umWeb.setTitle("Github"); + umWeb.setThumb(new UMImage(this, R.mipmap.launcher_ic)); + umWeb.setDescription(getString(R.string.app_name)); + + /* UMImage umImage = new UMImage(this, R.mipmap.launcher_ic); */ // 分享对话框 new ShareDialog.Builder(this) - .setShareLink(content) + .setShareLink(umWeb) .setListener(new UmengShare.OnShareListener() { @Override - public void onSucceed(Platform platform) { + public void onShareSuccess(@NonNull Platform platform) { toast("分享成功"); } @Override - public void onError(Platform platform, Throwable t) { - toast(t.getMessage()); + public void onShareFail(@NonNull Platform platform, @NonNull Throwable throwable) { + toast(throwable.getMessage()); } @Override - public void onCancel(Platform platform) { + public void onShareCancel(@NonNull Platform platform) { toast("分享取消"); } }) @@ -448,9 +457,9 @@ public void onCancel(Platform platform) { // 更新日志 .setUpdateLog("到底更新了啥\n到底更新了啥\n到底更新了啥\n到底更新了啥\n到底更新了啥\n到底更新了啥") // 下载 URL - .setDownloadUrl("https://dldir1.qq.com/weixin/android/weixin807android1920_arm64.apk") + .setDownloadUrl("https://dldir1.qq.com/weixin/android/weixin8015android2020_arm64.apk") // 文件 MD5 - .setFileMd5("df2f045dfa854d8461d9cefe08b813c8") + .setFileMd5("b05b25d4738ea31091dd9f80f4416469") .show(); } else if (viewId == R.id.btn_dialog_safe) { @@ -460,12 +469,12 @@ public void onCancel(Platform platform) { .setListener(new SafeDialog.OnListener() { @Override - public void onConfirm(BaseDialog dialog, String phone, String code) { + public void onConfirm(@NonNull BaseDialog dialog, @NonNull String phone, @NonNull String code) { toast("手机号:" + phone + "\n验证码:" + code); } @Override - public void onCancel(BaseDialog dialog) { + public void onCancel(@NonNull BaseDialog dialog) { toast("取消了"); } }) @@ -477,8 +486,6 @@ public void onCancel(BaseDialog dialog) { new BaseDialog.Builder<>(this) .setContentView(R.layout.custom_dialog) .setAnimStyle(BaseDialog.ANIM_SCALE) - //.setText(id, "我是预设置的文本") - .setOnClickListener(R.id.btn_dialog_custom_ok, (BaseDialog.OnClickListener