Compare commits
259 Commits
74443950c4
...
winboll
| Author | SHA1 | Date | |
|---|---|---|---|
| a5fdffba29 | |||
| fb336412fb | |||
| 937d35d35a | |||
| 4c2002a7ce | |||
| bbb0e22198 | |||
| ee75f95c79 | |||
| c40e301c90 | |||
| 5f2170a7a1 | |||
| 6308df8f36 | |||
| 5f24c2d351 | |||
| 882016454f | |||
| c6c290dcd2 | |||
| e64f3a3e97 | |||
| 998cb3d193 | |||
| 9ba3374b16 | |||
| 4ded8537e8 | |||
| 59e51da991 | |||
| 070e2fb4f0 | |||
| 632ecc51d8 | |||
| 58b2cace16 | |||
| 3648fa3361 | |||
| 122122ef80 | |||
| 4caeb83e5e | |||
| f36ee0d9b1 | |||
| fe0b2f97ea | |||
| 43da2bc03a | |||
| d26ef7bf7f | |||
| 6baca2779d | |||
| 83da84371c | |||
| f0fbdaf121 | |||
| a20f29728e | |||
| 9669fe2a8d | |||
| ff8cb2fb42 | |||
| e42219ac5a | |||
| 42112eb677 | |||
| f3114a8121 | |||
| b5b29b8a77 | |||
| 01b0a7736d | |||
| ed5ab24bd3 | |||
| b5c989b5f9 | |||
| 725b336501 | |||
| d20923eaee | |||
| 9db3b3b703 | |||
| 744fb23291 | |||
| bd01220892 | |||
| 4b2b5acc99 | |||
| 57e4f8770b | |||
| 3b313e2362 | |||
| 1e96cd02bc | |||
| 1274bc7c05 | |||
| f67c57108a | |||
| 6d694992b7 | |||
| 6f5aa807c0 | |||
| f897f6e9ab | |||
| e2c73fdec0 | |||
| 4fcc5f9689 | |||
| 4208cda32f | |||
| 7c83b903f3 | |||
| 54b040285c | |||
| e8667bb26f | |||
| 05259e5ca9 | |||
| a5a5b37121 | |||
| 29726828b0 | |||
| 436e92702f | |||
| 3669a96768 | |||
| 37c3d1563c | |||
| 6741c41c83 | |||
| 4708dd4426 | |||
| 36e2ed0b48 | |||
| 3cfee1c4a8 | |||
| 24af31d51d | |||
| 54f77a8d87 | |||
| 69b18343c9 | |||
| e1bd959842 | |||
| 8f869e19cc | |||
| 270e21ed23 | |||
| 6412554096 | |||
| 286f8513d4 | |||
| bad38e37ae | |||
| 08eb360dbd | |||
| 819018b149 | |||
| 6e6b262e86 | |||
| 9665856b1b | |||
| 052bbce839 | |||
| 90102f4eea | |||
| 759a08cec9 | |||
| 42cc7a2822 | |||
| c9c95d6ab0 | |||
| bc9bd47daa | |||
| 4bec8c3e9e | |||
| e726c9d435 | |||
| 5277913606 | |||
| c1bd31df2f | |||
| c6591e83a5 | |||
| 7119b3b7a5 | |||
| 48d36c6d96 | |||
| 2850d3ca3b | |||
| 5cd9c88cdc | |||
| ed96d0ed07 | |||
| e69ffa4e66 | |||
| 68ddb22e83 | |||
| 4baee6f0e1 | |||
| 8f6b615949 | |||
| d02d57d4dd | |||
| e337bb7a04 | |||
| 9ae848e4c2 | |||
| 9c66f61891 | |||
| bfaf3543b9 | |||
| b44fe3aaf3 | |||
| d518ac50a9 | |||
| d532eae971 | |||
| f661acbbbc | |||
| ecced75a4d | |||
| 5e5d34c90c | |||
| 85a0d39498 | |||
| c542d8dca7 | |||
| ccbdb4010e | |||
| 2e7b9173f2 | |||
| 4f12a5de4f | |||
| 7ab399e520 | |||
| dd2d9f3e55 | |||
| 098516d4d7 | |||
| b1fab5ce46 | |||
| 0ef6141c9d | |||
| fb79b83705 | |||
| de89a326c0 | |||
| dd95e1ea6c | |||
| 0237e72c62 | |||
| 9771f5d8e0 | |||
| 4d0049f66f | |||
| e68098aa10 | |||
| d673ba46a1 | |||
| 88431a6688 | |||
| 2b32bb91a4 | |||
| e9c999dd73 | |||
| 96c828f27b | |||
| b48217cf91 | |||
| 177a9f66d4 | |||
| 8a0b98cd4f | |||
| 0eedbd75bb | |||
| 32c25f1b0a | |||
| 5a1342156f | |||
| 4e1784d99f | |||
| 069e5a66ad | |||
| e9a1dca8ca | |||
| 7e3a3d1446 | |||
| 7414cd0f33 | |||
| b2b3f949b7 | |||
| 83c1b888b6 | |||
| 6afc81939d | |||
| 1cf4c67b4f | |||
| 89697f8c49 | |||
| 5419fad1cf | |||
| d027c8affc | |||
| 7bf6007117 | |||
| 870bf6f8d2 | |||
| 8d8922f3f1 | |||
| ff03270429 | |||
| c9ec7e9a63 | |||
| d747ab2ea8 | |||
| eda55b23c3 | |||
| df717290ec | |||
| 9dae7a01c4 | |||
| fa34ef3b75 | |||
| 25b395b864 | |||
| ef837e9b32 | |||
| 52677bf9cf | |||
| 209a73943e | |||
| 490d3c9da6 | |||
| 01109f9ae9 | |||
| 35a077d01f | |||
| 6003bec1a1 | |||
| 71f4663198 | |||
| 33e3a7c24a | |||
| ed8391ad2c | |||
| 159d2199ae | |||
| 1365a1e499 | |||
| b3a5407442 | |||
| 7a48358cbb | |||
| 04429ab30b | |||
| d85b04f02d | |||
| e641c66b72 | |||
| 69a9eb09af | |||
| d3351b699b | |||
| 9971e94347 | |||
| 6487672697 | |||
| 86401f958e | |||
| 5a38f7b37e | |||
| b6d767f0ba | |||
| 84e9bb6830 | |||
| 168155f0db | |||
| bbb9c7704c | |||
| e0ac8f1f60 | |||
| e44a3bc66b | |||
| 90a0eaad74 | |||
| 63354f21df | |||
| 828ae091e0 | |||
| 6cc0d1de00 | |||
| a7aa90fb26 | |||
| 850a754d6c | |||
| 74c319f01a | |||
| 528935fab7 | |||
| 0d2394c842 | |||
| a18057e095 | |||
| ede5ebf50f | |||
| 9da73f41f0 | |||
| 7c8e5a26b6 | |||
| afb0525d0b | |||
| d2afb716be | |||
| 41d6d453b2 | |||
| fd9014ecb5 | |||
| 368d70f175 | |||
| db4f18d077 | |||
| bdf428a9fa | |||
| 890ec9587c | |||
| 94bfb3e878 | |||
| 37173c7c3a | |||
| bc913fd7f0 | |||
| 17d1c2f321 | |||
| 447b7fa5a8 | |||
| dae39b43d6 | |||
| 530316b976 | |||
| 3f924b004c | |||
| 1db94b52e6 | |||
| 55c653af09 | |||
| 9d97d6ed94 | |||
| e21bb9058d | |||
| ad6175f977 | |||
| 8b659f4b24 | |||
| 13b841f923 | |||
| e9ad701db4 | |||
| 0aaf71f285 | |||
| 4ea2b5fad0 | |||
| 760fe4613f | |||
| a656dfcc62 | |||
| e9605fa991 | |||
| 8546b6c8ad | |||
| f5ddefa895 | |||
| 35527374da | |||
| 2751ce4a39 | |||
| 730022a9f0 | |||
| a3bc90d9b8 | |||
| 32ee7c8845 | |||
| 6e34ee73e9 | |||
| 7eed7357f0 | |||
| d20192cb36 | |||
| 5846784940 | |||
| ef64d6a317 | |||
| 8b2a8328eb | |||
| 88a20d9a85 | |||
| aeaea253cb | |||
| 4890ca42cc | |||
| 2896b6401b | |||
| 1aa270482e | |||
| 3f96da4987 | |||
| 1b265d50b2 | |||
| 89b8f975bb | |||
| 28d5f8823c | |||
| 735f842661 |
8
.gitignore
vendored
@@ -97,9 +97,5 @@ lint-results.html
|
|||||||
## WinBoLL 基础应用(避免上传敏感配置)
|
## WinBoLL 基础应用(避免上传敏感配置)
|
||||||
/winboll.properties
|
/winboll.properties
|
||||||
/local.properties
|
/local.properties
|
||||||
|
/settings.gradle
|
||||||
## WinBoLL 衍生应用,
|
/gradle.properties
|
||||||
## 外派类型类库应用需要注释掉以下部分,以便部署通用类库编译配置。
|
|
||||||
## APPBase,AES需要上传以下两种配置。
|
|
||||||
settings.gradle
|
|
||||||
gradle.properties
|
|
||||||
|
|||||||
97
LICENSE-Private-Demo
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
# WinBoLL 源码 LICENSE-Private-Demo 规范说明书
|
||||||
|
|
||||||
|
# LICENSE-Private-Demo
|
||||||
|
|
||||||
|
# WinBoLL 源码公共转私有继承开发规范守则
|
||||||
|
|
||||||
|
## 核心声明
|
||||||
|
|
||||||
|
本文档**唯一核心设计目的**:通过文件标识、分支隔离、操作规范、责任界定四重约束,**从根源规避私有开发分支代码被人为合并、推送、提交至公共开源主流分支的风险**,明确人为操作失误、违规合并的全部责任归属,同时保证私有分支可正常同步、拉取公共主流分支的上游更新。
|
||||||
|
|
||||||
|
## 一、文件宗旨与风险防控说明
|
||||||
|
|
||||||
|
本文件为 WinBoLL 项目公共开源分支转为私有独立分支开发的**强制标准化操作手册与责任界定文件**,核心风控目标:
|
||||||
|
|
||||||
|
1. 严格隔离公共开源分支与私有开发分支,通过授权文件标记实现分支属性一眼可辨,杜绝人为操作混淆
|
||||||
|
|
||||||
|
2. **重点防控人为操作导致的私有分支代码违规合并、回合、推送至公共 ****`winboll`**** 主流分支**,从流程上封堵合并风险
|
||||||
|
|
||||||
|
3. 明确所有开发提交者的操作责任,违规合并公共分支的行为由操作人承担全部代码泄露、合规风险
|
||||||
|
|
||||||
|
4. 规范私有分支初始化全流程,保证私有分支仅可单向同步公共分支更新,禁止任何反向代码流入公共分支
|
||||||
|
|
||||||
|
## 二、公私分支授权标识文件定义(风控核心依据)
|
||||||
|
|
||||||
|
### 1. 公共开源分支唯一标识
|
||||||
|
|
||||||
|
**文件名:LICENSE**
|
||||||
|
|
||||||
|
- 仅允许存在于公共主流分支 `winboll` 及官方公共衍生分支
|
||||||
|
|
||||||
|
- 标识当前分支为**开源公开可贡献分支**,遵循原开源授权协议
|
||||||
|
|
||||||
|
- **严禁私有分支内保留、恢复此文件**,出现即判定分支属性异常
|
||||||
|
|
||||||
|
### 2. 私有开发分支唯一标识
|
||||||
|
|
||||||
|
**文件名:LICENSE-Private**
|
||||||
|
|
||||||
|
- 仅允许存在于私有开发分支,**绝对禁止出现在公共 ****`winboll`**** 分支**
|
||||||
|
|
||||||
|
- 标识当前分支为**私有闭源分支**,代码仅限内部使用,禁止公开、禁止对外贡献
|
||||||
|
|
||||||
|
- 为本分支私有属性的法定判定依据,也是禁止合并至公共分支的核心标记
|
||||||
|
|
||||||
|
## 三、分支管理与合并风控规则(强制遵守)
|
||||||
|
|
||||||
|
1. **公共主流分支**:固定为 `winboll`,为项目唯一开源主线,仅保留 `LICENSE` 文件,**禁止接收任何私有分支的合并、提交、推送请求**。
|
||||||
|
|
||||||
|
2. **私有开发分支**:统一从 `winboll` 分支检出,命名固定格式为 `private-demo-*`,与公共分支物理隔离。
|
||||||
|
|
||||||
|
3. **核心合并风控铁则**
|
||||||
|
|
||||||
|
- 私有分支 → 公共分支:**永久禁止任何形式的合并、推送、PR 提交、代码回合,人为操作也绝不允许**
|
||||||
|
|
||||||
|
- 公共分支 → 私有分支:允许正常拉取、同步上游更新,不影响私有开发迭代
|
||||||
|
|
||||||
|
4. 所有仓库提交者、合并操作者,均视为已阅读并完全认可本规则,**人为执行私有分支向公共分支的合并操作,由操作人承担全部代码泄露、合规违约、项目安全风险**。
|
||||||
|
|
||||||
|
## 四、公共转私有标准化操作步骤(锁死合并风险)
|
||||||
|
|
||||||
|
请严格按顺序执行,每一步均为风控必要环节,不可跳过、不可修改顺序。
|
||||||
|
|
||||||
|
1. 基于公共主流分支 `winboll`,新建私有开发分支,严格使用 `private-demo-*` 命名,从名称上明确分支私有属性,避免人为混淆。
|
||||||
|
|
||||||
|
2. 本地仓库切换至新建私有分支,确认当前分支名称、检出来源无误。
|
||||||
|
|
||||||
|
3. **永久删除项目根目录公共授权文件 ****`LICENSE`**,彻底移除公共分支标识,断绝误合并的标识漏洞。
|
||||||
|
|
||||||
|
4. 将本规范文件 `LICENSE-Private-Demo` 复制并重命名为 `LICENSE-Private`,作为私有分支生效授权文件。
|
||||||
|
|
||||||
|
5. 将以上所有变更执行一次性 Git 提交,**提交信息必须固定使用以下内容,不可修改**:
|
||||||
|
|
||||||
|
> 初始化私有开发分支,已切换私有授权文件,本分支禁止任何人为合并、推送至 winboll 公共分支
|
||||||
|
>
|
||||||
|
>
|
||||||
|
|
||||||
|
6. 提交完成后,本分支正式转为私有开发状态,后续所有代码提交、分支合并、版本迭代,均严禁指向公共 `winboll` 分支。
|
||||||
|
|
||||||
|
## 五、人为操作责任界定(核心补充条款)
|
||||||
|
|
||||||
|
1. 本分支所有开发者、代码提交者、分支合并操作者,均视为**完全知晓本分支的私有属性与合并禁令**,自愿遵守本规范全部约束。
|
||||||
|
|
||||||
|
2. **无论故意或过失,凡是人为执行私有分支向公共 ****`winboll`**** 分支的合并、推送、PR 提交、代码回合操作,全部责任由执行操作的本人独立承担**,项目方不承担任何因人为违规操作导致的代码泄露、开源合规、版本污染风险。
|
||||||
|
|
||||||
|
3. 仓库管理员需严格校验合并请求的分支标识与授权文件,发现带有 `LICENSE-Private` 标记的分支申请合并至公共分支,一律直接拒绝,并记录操作人信息。
|
||||||
|
|
||||||
|
4. 分支属性校验以根目录授权文件为唯一标准:只要分支内存在 `LICENSE-Private` 文件,就绝对禁止向公共分支发起任何合并操作。
|
||||||
|
|
||||||
|
## 六、分支状态校验与异常处理
|
||||||
|
|
||||||
|
- 合规公共分支:仅存在 `LICENSE`,无 `LICENSE-Private`
|
||||||
|
|
||||||
|
- 合规私有分支:仅存在 `LICENSE-Private`,无 `LICENSE`
|
||||||
|
|
||||||
|
- 异常状态:两个文件同时存在 / 均不存在 → 立即停止开发与提交,按本规范重置分支状态,严禁执行任何合并操作
|
||||||
|
|
||||||
|
> (注:文档部分内容可能由 AI 生成)
|
||||||
97
LICENSE-Private-Demo_docs/LICENSE-Private-Demo
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
# WinBoLL 源码 LICENSE-Private-Demo 规范说明书
|
||||||
|
|
||||||
|
# LICENSE-Private-Demo
|
||||||
|
|
||||||
|
# WinBoLL 源码公共转私有继承开发规范守则
|
||||||
|
|
||||||
|
## 核心声明
|
||||||
|
|
||||||
|
本文档**唯一核心设计目的**:通过文件标识、分支隔离、操作规范、责任界定四重约束,**从根源规避私有开发分支代码被人为合并、推送、提交至公共开源主流分支的风险**,明确人为操作失误、违规合并的全部责任归属,同时保证私有分支可正常同步、拉取公共主流分支的上游更新。
|
||||||
|
|
||||||
|
## 一、文件宗旨与风险防控说明
|
||||||
|
|
||||||
|
本文件为 WinBoLL 项目公共开源分支转为私有独立分支开发的**强制标准化操作手册与责任界定文件**,核心风控目标:
|
||||||
|
|
||||||
|
1. 严格隔离公共开源分支与私有开发分支,通过授权文件标记实现分支属性一眼可辨,杜绝人为操作混淆
|
||||||
|
|
||||||
|
2. **重点防控人为操作导致的私有分支代码违规合并、回合、推送至公共 ****`winboll`**** 主流分支**,从流程上封堵合并风险
|
||||||
|
|
||||||
|
3. 明确所有开发提交者的操作责任,违规合并公共分支的行为由操作人承担全部代码泄露、合规风险
|
||||||
|
|
||||||
|
4. 规范私有分支初始化全流程,保证私有分支仅可单向同步公共分支更新,禁止任何反向代码流入公共分支
|
||||||
|
|
||||||
|
## 二、公私分支授权标识文件定义(风控核心依据)
|
||||||
|
|
||||||
|
### 1. 公共开源分支唯一标识
|
||||||
|
|
||||||
|
**文件名:LICENSE**
|
||||||
|
|
||||||
|
- 仅允许存在于公共主流分支 `winboll` 及官方公共衍生分支
|
||||||
|
|
||||||
|
- 标识当前分支为**开源公开可贡献分支**,遵循原开源授权协议
|
||||||
|
|
||||||
|
- **严禁私有分支内保留、恢复此文件**,出现即判定分支属性异常
|
||||||
|
|
||||||
|
### 2. 私有开发分支唯一标识
|
||||||
|
|
||||||
|
**文件名:LICENSE-Private**
|
||||||
|
|
||||||
|
- 仅允许存在于私有开发分支,**绝对禁止出现在公共 ****`winboll`**** 分支**
|
||||||
|
|
||||||
|
- 标识当前分支为**私有闭源分支**,代码仅限内部使用,禁止公开、禁止对外贡献
|
||||||
|
|
||||||
|
- 为本分支私有属性的法定判定依据,也是禁止合并至公共分支的核心标记
|
||||||
|
|
||||||
|
## 三、分支管理与合并风控规则(强制遵守)
|
||||||
|
|
||||||
|
1. **公共主流分支**:固定为 `winboll`,为项目唯一开源主线,仅保留 `LICENSE` 文件,**禁止接收任何私有分支的合并、提交、推送请求**。
|
||||||
|
|
||||||
|
2. **私有开发分支**:统一从 `winboll` 分支检出,命名固定格式为 `private-demo-*`,与公共分支物理隔离。
|
||||||
|
|
||||||
|
3. **核心合并风控铁则**
|
||||||
|
|
||||||
|
- 私有分支 → 公共分支:**永久禁止任何形式的合并、推送、PR 提交、代码回合,人为操作也绝不允许**
|
||||||
|
|
||||||
|
- 公共分支 → 私有分支:允许正常拉取、同步上游更新,不影响私有开发迭代
|
||||||
|
|
||||||
|
4. 所有仓库提交者、合并操作者,均视为已阅读并完全认可本规则,**人为执行私有分支向公共分支的合并操作,由操作人承担全部代码泄露、合规违约、项目安全风险**。
|
||||||
|
|
||||||
|
## 四、公共转私有标准化操作步骤(锁死合并风险)
|
||||||
|
|
||||||
|
请严格按顺序执行,每一步均为风控必要环节,不可跳过、不可修改顺序。
|
||||||
|
|
||||||
|
1. 基于公共主流分支 `winboll`,新建私有开发分支,严格使用 `private-demo-*` 命名,从名称上明确分支私有属性,避免人为混淆。
|
||||||
|
|
||||||
|
2. 本地仓库切换至新建私有分支,确认当前分支名称、检出来源无误。
|
||||||
|
|
||||||
|
3. **永久删除项目根目录公共授权文件 ****`LICENSE`**,彻底移除公共分支标识,断绝误合并的标识漏洞。
|
||||||
|
|
||||||
|
4. 将本规范文件 `LICENSE-Private-Demo` 复制并重命名为 `LICENSE-Private`,作为私有分支生效授权文件。
|
||||||
|
|
||||||
|
5. 将以上所有变更执行一次性 Git 提交,**提交信息必须固定使用以下内容,不可修改**:
|
||||||
|
|
||||||
|
> 初始化私有开发分支,已切换私有授权文件,本分支禁止任何人为合并、推送至 winboll 公共分支
|
||||||
|
>
|
||||||
|
>
|
||||||
|
|
||||||
|
6. 提交完成后,本分支正式转为私有开发状态,后续所有代码提交、分支合并、版本迭代,均严禁指向公共 `winboll` 分支。
|
||||||
|
|
||||||
|
## 五、人为操作责任界定(核心补充条款)
|
||||||
|
|
||||||
|
1. 本分支所有开发者、代码提交者、分支合并操作者,均视为**完全知晓本分支的私有属性与合并禁令**,自愿遵守本规范全部约束。
|
||||||
|
|
||||||
|
2. **无论故意或过失,凡是人为执行私有分支向公共 ****`winboll`**** 分支的合并、推送、PR 提交、代码回合操作,全部责任由执行操作的本人独立承担**,项目方不承担任何因人为违规操作导致的代码泄露、开源合规、版本污染风险。
|
||||||
|
|
||||||
|
3. 仓库管理员需严格校验合并请求的分支标识与授权文件,发现带有 `LICENSE-Private` 标记的分支申请合并至公共分支,一律直接拒绝,并记录操作人信息。
|
||||||
|
|
||||||
|
4. 分支属性校验以根目录授权文件为唯一标准:只要分支内存在 `LICENSE-Private` 文件,就绝对禁止向公共分支发起任何合并操作。
|
||||||
|
|
||||||
|
## 六、分支状态校验与异常处理
|
||||||
|
|
||||||
|
- 合规公共分支:仅存在 `LICENSE`,无 `LICENSE-Private`
|
||||||
|
|
||||||
|
- 合规私有分支:仅存在 `LICENSE-Private`,无 `LICENSE`
|
||||||
|
|
||||||
|
- 异常状态:两个文件同时存在 / 均不存在 → 立即停止开发与提交,按本规范重置分支状态,严禁执行任何合并操作
|
||||||
|
|
||||||
|
> (注:文档部分内容可能由 AI 生成)
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# WinBoLL 源码 LICENSE\-Private\-Demo 规范说明书
|
||||||
|
|
||||||
|
# LICENSE\-Private\-Demo
|
||||||
|
|
||||||
|
# WinBoLL 源码公共转私有继承开发规范守则
|
||||||
|
|
||||||
|
## 核心声明
|
||||||
|
|
||||||
|
本文档**唯一核心设计目的**:通过文件标识、分支隔离、操作规范、责任界定四重约束,**从根源规避私有开发分支代码被人为合并、推送、提交至公共开源主流分支的风险**,明确人为操作失误、违规合并的全部责任归属,同时保证私有分支可正常同步、拉取公共主流分支的上游更新。
|
||||||
|
|
||||||
|
## 一、文件宗旨与风险防控说明
|
||||||
|
|
||||||
|
本文件为 WinBoLL 项目公共开源分支转为私有独立分支开发的**强制标准化操作手册与责任界定文件**,核心风控目标:
|
||||||
|
|
||||||
|
1. 严格隔离公共开源分支与私有开发分支,通过授权文件标记实现分支属性一眼可辨,杜绝人为操作混淆
|
||||||
|
|
||||||
|
2. **重点防控人为操作导致的私有分支代码违规合并、回合、推送至公共 ****`winboll`**** 主流分支**,从流程上封堵合并风险
|
||||||
|
|
||||||
|
3. 明确所有开发提交者的操作责任,违规合并公共分支的行为由操作人承担全部代码泄露、合规风险
|
||||||
|
|
||||||
|
4. 规范私有分支初始化全流程,保证私有分支仅可单向同步公共分支更新,禁止任何反向代码流入公共分支
|
||||||
|
|
||||||
|
## 二、公私分支授权标识文件定义(风控核心依据)
|
||||||
|
|
||||||
|
### 1\. 公共开源分支唯一标识
|
||||||
|
|
||||||
|
**文件名:LICENSE**
|
||||||
|
|
||||||
|
- 仅允许存在于公共主流分支 `winboll` 及官方公共衍生分支
|
||||||
|
|
||||||
|
- 标识当前分支为**开源公开可贡献分支**,遵循原开源授权协议
|
||||||
|
|
||||||
|
- **严禁私有分支内保留、恢复此文件**,出现即判定分支属性异常
|
||||||
|
|
||||||
|
### 2\. 私有开发分支唯一标识
|
||||||
|
|
||||||
|
**文件名:LICENSE\-Private**
|
||||||
|
|
||||||
|
- 仅允许存在于私有开发分支,**绝对禁止出现在公共 ****`winboll`**** 分支**
|
||||||
|
|
||||||
|
- 标识当前分支为**私有闭源分支**,代码仅限内部使用,禁止公开、禁止对外贡献
|
||||||
|
|
||||||
|
- 为本分支私有属性的法定判定依据,也是禁止合并至公共分支的核心标记
|
||||||
|
|
||||||
|
## 三、分支管理与合并风控规则(强制遵守)
|
||||||
|
|
||||||
|
1. **公共主流分支**:固定为 `winboll`,为项目唯一开源主线,仅保留 `LICENSE` 文件,**禁止接收任何私有分支的合并、提交、推送请求**。
|
||||||
|
|
||||||
|
2. **私有开发分支**:统一从 `winboll` 分支检出,命名固定格式为 `private\-demo\-\*`,与公共分支物理隔离。
|
||||||
|
|
||||||
|
3. **核心合并风控铁则**
|
||||||
|
|
||||||
|
- 私有分支 → 公共分支:**永久禁止任何形式的合并、推送、PR 提交、代码回合,人为操作也绝不允许**
|
||||||
|
|
||||||
|
- 公共分支 → 私有分支:允许正常拉取、同步上游更新,不影响私有开发迭代
|
||||||
|
|
||||||
|
4. 所有仓库提交者、合并操作者,均视为已阅读并完全认可本规则,**人为执行私有分支向公共分支的合并操作,由操作人承担全部代码泄露、合规违约、项目安全风险**。
|
||||||
|
|
||||||
|
## 四、公共转私有标准化操作步骤(锁死合并风险)
|
||||||
|
|
||||||
|
请严格按顺序执行,每一步均为风控必要环节,不可跳过、不可修改顺序。
|
||||||
|
|
||||||
|
1. 基于公共主流分支 `winboll`,新建私有开发分支,严格使用 `private\-demo\-\*` 命名,从名称上明确分支私有属性,避免人为混淆。
|
||||||
|
|
||||||
|
2. 本地仓库切换至新建私有分支,确认当前分支名称、检出来源无误。
|
||||||
|
|
||||||
|
3. **永久删除项目根目录公共授权文件 ****`LICENSE`**,彻底移除公共分支标识,断绝误合并的标识漏洞。
|
||||||
|
|
||||||
|
4. 将本规范文件 `LICENSE\-Private\-Demo` 复制并重命名为 `LICENSE\-Private`,作为私有分支生效授权文件。
|
||||||
|
|
||||||
|
5. 将以上所有变更执行一次性 Git 提交,**提交信息必须固定使用以下内容,不可修改**:
|
||||||
|
|
||||||
|
> 初始化私有开发分支,已切换私有授权文件,本分支禁止任何人为合并、推送至 winboll 公共分支
|
||||||
|
>
|
||||||
|
>
|
||||||
|
|
||||||
|
6. 提交完成后,本分支正式转为私有开发状态,后续所有代码提交、分支合并、版本迭代,均严禁指向公共 `winboll` 分支。
|
||||||
|
|
||||||
|
## 五、人为操作责任界定(核心补充条款)
|
||||||
|
|
||||||
|
1. 本分支所有开发者、代码提交者、分支合并操作者,均视为**完全知晓本分支的私有属性与合并禁令**,自愿遵守本规范全部约束。
|
||||||
|
|
||||||
|
2. **无论故意或过失,凡是人为执行私有分支向公共 ****`winboll`**** 分支的合并、推送、PR 提交、代码回合操作,全部责任由执行操作的本人独立承担**,项目方不承担任何因人为违规操作导致的代码泄露、开源合规、版本污染风险。
|
||||||
|
|
||||||
|
3. 仓库管理员需严格校验合并请求的分支标识与授权文件,发现带有 `LICENSE\-Private` 标记的分支申请合并至公共分支,一律直接拒绝,并记录操作人信息。
|
||||||
|
|
||||||
|
4. 分支属性校验以根目录授权文件为唯一标准:只要分支内存在 `LICENSE\-Private` 文件,就绝对禁止向公共分支发起任何合并操作。
|
||||||
|
|
||||||
|
## 六、分支状态校验与异常处理
|
||||||
|
|
||||||
|
- 合规公共分支:仅存在 `LICENSE`,无 `LICENSE\-Private`
|
||||||
|
|
||||||
|
- 合规私有分支:仅存在 `LICENSE\-Private`,无 `LICENSE`
|
||||||
|
|
||||||
|
- 异常状态:两个文件同时存在 / 均不存在 → 立即停止开发与提交,按本规范重置分支状态,严禁执行任何合并操作
|
||||||
|
|
||||||
|
> (注:文档部分内容可能由 AI 生成)
|
||||||
|
After Width: | Height: | Size: 710 KiB |
@@ -24,13 +24,13 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "cc.winboll.studio.aes"
|
applicationId "cc.winboll.studio.aes"
|
||||||
minSdkVersion 21
|
minSdkVersion 26
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
versionCode 1
|
versionCode 1
|
||||||
// versionName 更新后需要手动设置
|
// versionName 更新后需要手动设置
|
||||||
// 项目模块目录的 build.gradle 文件的 stageCount=0
|
// 项目模块目录的 build.gradle 文件的 stageCount=0
|
||||||
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
|
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
|
||||||
versionName "15.15"
|
versionName "15.20"
|
||||||
if(true) {
|
if(true) {
|
||||||
versionName = genVersionName("${versionName}")
|
versionName = genVersionName("${versionName}")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#Created by .winboll/winboll_app_build.gradle
|
#Created by .winboll/winboll_app_build.gradle
|
||||||
#Sat Apr 25 04:16:42 HKT 2026
|
#Tue May 12 13:11:28 HKT 2026
|
||||||
stageCount=10
|
stageCount=4
|
||||||
libraryProject=libaes
|
libraryProject=libaes
|
||||||
baseVersion=15.15
|
baseVersion=15.20
|
||||||
publishVersion=15.15.9
|
publishVersion=15.20.3
|
||||||
buildCount=0
|
buildCount=0
|
||||||
baseBetaVersion=15.15.10
|
baseBetaVersion=15.20.4
|
||||||
|
|||||||
16
aes/src/main/res/values-night/styles.xml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<style name="MyAESTheme" parent="AESTheme">
|
||||||
|
<item name="themeDebug">@style/MyDebugActivityTheme</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="MyDebugActivityTheme" parent="Theme.AppCompat.NoActionBar">
|
||||||
|
<item name="android:statusBarColor">@color/toolbarBackgroundColor</item>
|
||||||
|
<item name="colorTittle">@color/mainWindowTextColor</item>
|
||||||
|
<item name="colorTittleBackgound">@color/toolbarBackgroundColor</item>
|
||||||
|
<item name="colorText">@color/debugTextColor</item>
|
||||||
|
<item name="colorTextBackgound">@color/mainWindowBackgroundColor</item>
|
||||||
|
<item name="debugTextColor">@color/debugTextColor</item>
|
||||||
|
<item name="toolbarTextColor">@color/toolbarTextColor</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -1,5 +1,16 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<style name="MyAESTheme" parent="AESTheme">
|
<style name="MyAESTheme" parent="AESTheme">
|
||||||
|
<item name="themeDebug">@style/MyDebugActivityTheme</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
|
||||||
|
<style name="MyDebugActivityTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||||
|
<item name="android:statusBarColor">@color/toolbarBackgroundColor</item>
|
||||||
|
<item name="colorTittle">@color/mainWindowTextColor</item>
|
||||||
|
<item name="colorTittleBackgound">@color/toolbarBackgroundColor</item>
|
||||||
|
<item name="colorText">@color/debugTextColor</item>
|
||||||
|
<item name="colorTextBackgound">@color/mainWindowBackgroundColor</item>
|
||||||
|
<item name="debugTextColor">@color/debugTextColor</item>
|
||||||
|
<item name="toolbarTextColor">@color/toolbarTextColor</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
1
appbase/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
36
appbase/README.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# APPBase
|
||||||
|
[](https://jitpack.io/#ZhanGSKen/APPBase)
|
||||||
|
|
||||||
|
#### 介绍
|
||||||
|
WinBoLL 安卓手机端安卓应用开发基础类库。
|
||||||
|
|
||||||
|
#### 软件架构
|
||||||
|
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
|
||||||
|
也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
|
||||||
|
|
||||||
|
|
||||||
|
#### Gradle 编译说明
|
||||||
|
调试版编译命令 :gradle assembleBetaDebug
|
||||||
|
阶段版编译命令 :bash .winboll/bashPublishAPKAddTag.sh appbase
|
||||||
|
阶段版类库发布命令 :git pull &&bash .winboll/bashPublishLIBAddTag.sh libappbase
|
||||||
|
|
||||||
|
#### 使用说明
|
||||||
|
|
||||||
|
#### 参与贡献
|
||||||
|
|
||||||
|
1. Fork 本仓库
|
||||||
|
2. 新建 Feat_xxx 分支
|
||||||
|
3. 提交代码 : ZhanGSKen(ZhanGSKen<zhangsken@188.com>)
|
||||||
|
4. 新建 Pull Request
|
||||||
|
|
||||||
|
|
||||||
|
#### 特技
|
||||||
|
|
||||||
|
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
|
||||||
|
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
|
||||||
|
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
|
||||||
|
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
|
||||||
|
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
|
||||||
|
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
|
||||||
|
|
||||||
|
#### 参考文档
|
||||||
1
appbase/app_update_description.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
50
appbase/build.gradle
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply from: '../.winboll/winboll_app_build.gradle'
|
||||||
|
apply from: '../.winboll/winboll_lint_build.gradle'
|
||||||
|
|
||||||
|
def genVersionName(def versionName){
|
||||||
|
// 检查编译标志位配置
|
||||||
|
assert (winbollBuildProps['stageCount'] != null)
|
||||||
|
assert (winbollBuildProps['baseVersion'] != null)
|
||||||
|
// 保存基础版本号
|
||||||
|
winbollBuildProps.setProperty("baseVersion", "${versionName}");
|
||||||
|
//保存编译标志配置
|
||||||
|
FileOutputStream fos = new FileOutputStream(winbollBuildPropsFile)
|
||||||
|
winbollBuildProps.store(fos, "${winbollBuildPropsDesc}");
|
||||||
|
fos.close();
|
||||||
|
|
||||||
|
// 返回编译版本号
|
||||||
|
return "${versionName}." + winbollBuildProps['stageCount']
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
// 适配MIUI12
|
||||||
|
compileSdkVersion 30
|
||||||
|
buildToolsVersion "30.0.3"
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "cc.winboll.studio.appbase"
|
||||||
|
minSdkVersion 26
|
||||||
|
targetSdkVersion 30
|
||||||
|
versionCode 1
|
||||||
|
// versionName 更新后需要手动设置
|
||||||
|
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
|
||||||
|
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
|
||||||
|
versionName "15.20"
|
||||||
|
if(true) {
|
||||||
|
versionName = genVersionName("${versionName}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保 Java 7 兼容性(已适配项目技术栈)
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_7
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api project(':libappbase')
|
||||||
|
|
||||||
|
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
|
}
|
||||||
8
appbase/build.properties
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#Created by .winboll/winboll_app_build.gradle
|
||||||
|
#Tue May 12 09:17:15 HKT 2026
|
||||||
|
stageCount=10
|
||||||
|
libraryProject=libappbase
|
||||||
|
baseVersion=15.20
|
||||||
|
publishVersion=15.20.9
|
||||||
|
buildCount=0
|
||||||
|
baseBetaVersion=15.20.10
|
||||||
126
appbase/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# By default, the flags in this file are appended to flags specified
|
||||||
|
# in C:\tools\adt-bundle-windows-x86_64-20131030\sdk/tools/proguard/proguard-android.txt
|
||||||
|
# You can edit the include path and order by changing the proguardFiles
|
||||||
|
# directive in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# Add any project specific keep options here:
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# ============================== 基础通用规则 ==============================
|
||||||
|
# 保留系统组件
|
||||||
|
-keep public class * extends android.app.Activity
|
||||||
|
-keep public class * extends android.app.Service
|
||||||
|
-keep public class * extends android.content.BroadcastReceiver
|
||||||
|
-keep public class * extends android.content.ContentProvider
|
||||||
|
-keep public class * extends android.app.backup.BackupAgentHelper
|
||||||
|
-keep public class * extends android.preference.Preference
|
||||||
|
|
||||||
|
# 保留 WinBoLL 核心包及子类(适配你的两个包名)
|
||||||
|
#-keep public class * extends com.winboll.WinBoLLActivity
|
||||||
|
#-keep public class * extends com.winboll.WinBoLLFragment
|
||||||
|
# 主包名
|
||||||
|
-keep class cc.winboll.studio.*.** { *; }
|
||||||
|
# beta包名
|
||||||
|
-keep class cc.winboll.studio.*.beta.** { *; }
|
||||||
|
-keepclassmembers class cc.winboll.studio.*.** { *; }
|
||||||
|
-keepclassmembers class cc.winboll.studio.*.beta.** { *; }
|
||||||
|
|
||||||
|
# 保留所有类中的 public static final String TAG 字段
|
||||||
|
-keepclassmembers class * {
|
||||||
|
public static final java.lang.String TAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 保留序列化类
|
||||||
|
-keep class * implements android.os.Parcelable {
|
||||||
|
public static final android.os.Parcelable$Creator *;
|
||||||
|
}
|
||||||
|
-keepclassmembers class * implements java.io.Serializable {
|
||||||
|
static final long serialVersionUID;
|
||||||
|
private static final java.io.ObjectStreamField[] serialPersistentFields;
|
||||||
|
private void writeObject(java.io.ObjectOutputStream);
|
||||||
|
private void readObject(java.io.ObjectInputStream);
|
||||||
|
java.lang.Object writeReplace();
|
||||||
|
java.lang.Object readResolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
# 保留 R 文件
|
||||||
|
-keepclassmembers class **.R$* {
|
||||||
|
public static <fields>;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 保留 native 方法
|
||||||
|
-keepclasseswithmembernames class * {
|
||||||
|
native <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 保留注解和泛型
|
||||||
|
-keepattributes *Annotation*
|
||||||
|
-keepattributes Signature
|
||||||
|
|
||||||
|
# 屏蔽 Java 8+ 警告(适配 Java 7)
|
||||||
|
-dontwarn java.lang.invoke.*
|
||||||
|
-dontwarn android.support.v8.renderscript.*
|
||||||
|
-dontwarn java.util.function.**
|
||||||
|
|
||||||
|
# ============================== 第三方框架规则 ==============================
|
||||||
|
# Retrofit + OkHttp
|
||||||
|
-keep class retrofit2.** { *; }
|
||||||
|
-keep interface retrofit2.** { *; }
|
||||||
|
-keep class okhttp3.** { *; }
|
||||||
|
-keep interface okhttp3.** { *; }
|
||||||
|
-keep class okio.** { *; }
|
||||||
|
-keepclasseswithmembers class * {
|
||||||
|
@retrofit2.http.* <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Glide 4.x
|
||||||
|
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||||
|
-keep public class * extends com.bumptech.glide.module.AppGlideModule
|
||||||
|
-keep public enum com.bumptech.glide.load.ImageHeaderParser$ImageType {
|
||||||
|
**[] $VALUES;
|
||||||
|
public *;
|
||||||
|
}
|
||||||
|
-dontwarn com.bumptech.glide.load.resource.bitmap.VideoDecoder
|
||||||
|
|
||||||
|
# GreenDAO 3.x
|
||||||
|
-keepclassmembers class * extends org.greenrobot.greendao.AbstractDao {
|
||||||
|
public static java.lang.String TABLENAME;
|
||||||
|
}
|
||||||
|
-keep class **$Properties
|
||||||
|
# 实体类包名(按实际调整)
|
||||||
|
#-keep class cc.winboll.studio.appbase.model.** { *; }
|
||||||
|
|
||||||
|
# ButterKnife 8.x
|
||||||
|
-keep class butterknife.** { *; }
|
||||||
|
-dontwarn butterknife.internal.**
|
||||||
|
-keep class **$$ViewBinder { *; }
|
||||||
|
-keepclasseswithmembernames class * {
|
||||||
|
@butterknife.BindView <fields>;
|
||||||
|
@butterknife.OnClick <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
# EventBus 3.x
|
||||||
|
-keepclassmembers class ** {
|
||||||
|
@org.greenrobot.eventbus.Subscribe <methods>;
|
||||||
|
}
|
||||||
|
-keep enum org.greenrobot.eventbus.ThreadMode { *; }
|
||||||
|
|
||||||
|
# ============================== 优化与调试 ==============================
|
||||||
|
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
|
||||||
|
-optimizationpasses 5
|
||||||
|
-verbose
|
||||||
|
-dontpreverify
|
||||||
|
-dontusemixedcaseclassnames
|
||||||
|
# 保留行号(便于崩溃定位)
|
||||||
|
-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
14
appbase/src/beta/AndroidManifest.xml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools" >
|
||||||
|
|
||||||
|
<application
|
||||||
|
tools:replace="android:icon"
|
||||||
|
android:icon="@drawable/ic_winboll_beta">
|
||||||
|
|
||||||
|
<!-- Put flavor specific code here -->
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
|
|
||||||
6
appbase/src/beta/res/values/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<string name="app_name">APPBase+</string>
|
||||||
|
|
||||||
|
</resources>
|
||||||
57
appbase/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<manifest
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="cc.winboll.studio.appbase">
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name=".App"
|
||||||
|
android:icon="@drawable/ic_winboll"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/MyAPPBaseTheme"
|
||||||
|
android:resizeableActivity="true"
|
||||||
|
android:process=":App"
|
||||||
|
android:sharedUserId="@string/shared_user_id"
|
||||||
|
android:sharedUserLabel="@string/shared_user_label">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:exported="true"
|
||||||
|
android:resizeableActivity="true"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".Main2Activity"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:exported="true"
|
||||||
|
android:resizeableActivity="true"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:taskAffinity="cc.winboll.studio.appbase.Main2Activity"
|
||||||
|
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation">
|
||||||
|
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity android:name=".GlobalApplication$CrashActivity"/>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.max_aspect"
|
||||||
|
android:value="4.0"/>
|
||||||
|
|
||||||
|
<activity android:name="cc.winboll.studio.libappbase.activities.CrashCopyReceiverActivity"/>
|
||||||
|
|
||||||
|
<activity android:name=".AboutActivity"/>
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package cc.winboll.studio.appbase;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Toolbar;
|
||||||
|
import cc.winboll.studio.appbase.R;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
import cc.winboll.studio.libappbase.models.APPInfo;
|
||||||
|
import cc.winboll.studio.libappbase.views.AboutView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||||
|
* @Date 2026/01/11 12:55
|
||||||
|
* @Describe AboutActivity
|
||||||
|
*/
|
||||||
|
public class AboutActivity extends Activity {
|
||||||
|
|
||||||
|
public static final String TAG = "AboutActivity";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_about);
|
||||||
|
|
||||||
|
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||||
|
setActionBar(toolbar);
|
||||||
|
|
||||||
|
AboutView aboutView = findViewById(R.id.aboutview);
|
||||||
|
aboutView.setAPPInfo(genDefaultAppInfo());
|
||||||
|
}
|
||||||
|
|
||||||
|
private APPInfo genDefaultAppInfo() {
|
||||||
|
LogUtils.d(TAG, "genDefaultAppInfo() 调用");
|
||||||
|
String branchName = "appbase";
|
||||||
|
APPInfo appInfo = new APPInfo();
|
||||||
|
appInfo.setAppName("APPBase");
|
||||||
|
appInfo.setAppIcon(R.drawable.ic_winboll);
|
||||||
|
appInfo.setAppDescription(getString(R.string.app_description));
|
||||||
|
appInfo.setAppGitName("WinBoLL");
|
||||||
|
appInfo.setAppGitOwner("Studio");
|
||||||
|
appInfo.setAppGitAPPBranch(branchName);
|
||||||
|
appInfo.setAppGitAPPSubProjectFolder(branchName);
|
||||||
|
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=APPBase");
|
||||||
|
appInfo.setAppAPKName("APPBase");
|
||||||
|
appInfo.setAppAPKFolderName("APPBase");
|
||||||
|
LogUtils.d(TAG, "genDefaultAppInfo: 应用信息已生成");
|
||||||
|
return appInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
48
appbase/src/main/java/cc/winboll/studio/appbase/App.java
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package cc.winboll.studio.appbase;
|
||||||
|
|
||||||
|
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||||
|
import cc.winboll.studio.libappbase.ToastUtils;
|
||||||
|
import cc.winboll.studio.libappbase.BuildConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||||
|
* @Date 2025/01/05 09:54:42
|
||||||
|
* @Describe 应用全局入口类(继承基础库 GlobalApplication)
|
||||||
|
* 负责应用初始化、全局资源管理与生命周期回调处理,是整个应用的核心入口
|
||||||
|
*/
|
||||||
|
public class App extends GlobalApplication {
|
||||||
|
|
||||||
|
/** 当前应用类的日志 TAG(用于调试输出,标识日志来源) */
|
||||||
|
public static final String TAG = "App";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用创建时回调(全局初始化入口)
|
||||||
|
* 在应用进程启动时执行,仅调用一次,用于初始化全局工具类、第三方库等
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
// 如果应用不在调试状态,就根据编译类型设置调试状态
|
||||||
|
if (isDebugging() != true) {
|
||||||
|
setIsDebugging(BuildConfig.DEBUG);
|
||||||
|
}
|
||||||
|
// release 版调试码
|
||||||
|
//setIsDebugging(!BuildConfig.DEBUG);
|
||||||
|
|
||||||
|
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
|
||||||
|
ToastUtils.init(getApplicationContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用终止时回调(资源释放入口)
|
||||||
|
* 仅在模拟环境(如 Android Studio 模拟器)中可靠触发,真机上可能因系统回收进程不执行
|
||||||
|
* 用于释放全局资源,避免内存泄漏
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onTerminate() {
|
||||||
|
super.onTerminate(); // 调用父类终止逻辑(如基础库资源释放)
|
||||||
|
// 释放 Toast 工具类资源(销毁全局 Toast 实例,避免内存泄漏)
|
||||||
|
ToastUtils.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package cc.winboll.studio.appbase;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
import cc.winboll.studio.libappbase.ToastUtils;
|
||||||
|
|
||||||
|
public class CrashTestActivity extends Activity {
|
||||||
|
|
||||||
|
public static final String TAG = "CrashTestActivity";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_crash_test);
|
||||||
|
LogUtils.d(TAG, "CrashTestActivity onCreate()");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onBack(View view) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onTestCrash(View view) {
|
||||||
|
LogUtils.d(TAG, "onTestCrash()");
|
||||||
|
ToastUtils.show("测试布局崩溃...");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package cc.winboll.studio.appbase;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.widget.Toolbar;
|
||||||
|
import cc.winboll.studio.appbase.R;
|
||||||
|
|
||||||
|
public class Main2Activity extends MainActivity {
|
||||||
|
|
||||||
|
public static final String TAG = "Main2Activity";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_main2);
|
||||||
|
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||||
|
if (toolbar != null) {
|
||||||
|
setActionBar(toolbar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
package cc.winboll.studio.appbase;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Toolbar;
|
||||||
|
import cc.winboll.studio.appbase.R;
|
||||||
|
import cc.winboll.studio.appbase.model.TestBean;
|
||||||
|
import cc.winboll.studio.libappbase.LogActivity;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
import cc.winboll.studio.libappbase.ToastUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||||
|
* @Date 未标注(建议补充创建日期)
|
||||||
|
* @Describe 应用主界面 Activity(入口界面)
|
||||||
|
* 包含功能测试按钮(崩溃测试、日志查看、Toast测试)、顶部工具栏(菜单功能),是应用交互的核心入口
|
||||||
|
*/
|
||||||
|
public class MainActivity extends Activity {
|
||||||
|
|
||||||
|
/** 当前 Activity 的日志 TAG(用于调试输出,标识日志来源) */
|
||||||
|
public static final String TAG = "MainActivity";
|
||||||
|
|
||||||
|
/** 顶部工具栏(用于展示标题、菜单,绑定布局中的 Toolbar 控件) */
|
||||||
|
private Toolbar mToolbar;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activity 创建时回调(初始化界面)
|
||||||
|
* 在 Activity 首次创建时执行,用于加载布局、初始化控件、设置事件监听
|
||||||
|
* @param savedInstanceState 保存 Activity 状态的 Bundle(如屏幕旋转时的数据恢复)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
//ToastUtils.show("onCreate"); // 显示 Activity 创建提示(调试用)
|
||||||
|
setContentView(R.layout.activity_main); // 加载主界面布局
|
||||||
|
|
||||||
|
// 初始化 Toolbar 并设置为 ActionBar
|
||||||
|
mToolbar = findViewById(R.id.toolbar);
|
||||||
|
setActionBar(mToolbar); // 将 Toolbar 替代系统默认 ActionBar
|
||||||
|
|
||||||
|
initTestData();
|
||||||
|
}
|
||||||
|
|
||||||
|
void initTestData() {
|
||||||
|
TestBean bean1 = new TestBean();
|
||||||
|
bean1.setTestNum1(456);
|
||||||
|
TestBean.saveBeanToFile(getFilesDir().getAbsolutePath() + getTestBeanRelativePath(), bean1);
|
||||||
|
TestBean bean2 = new TestBean();
|
||||||
|
bean2.setTestNum1(789);
|
||||||
|
TestBean.saveBeanToFile(getExternalFilesDir(null).getAbsolutePath() + getTestBeanRelativePath(), bean2);
|
||||||
|
}
|
||||||
|
|
||||||
|
String getTestBeanRelativePath() {
|
||||||
|
return "/BaseBaen/"+TestBean.class.getName()+".json";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建菜单时回调(加载工具栏菜单)
|
||||||
|
* 初始化 ActionBar 菜单,加载自定义菜单布局
|
||||||
|
* @param menu 菜单对象(用于承载菜单项)
|
||||||
|
* @return true:显示菜单;false:不显示菜单
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
|
// 加载菜单布局(R.menu.toolbar_main 为自定义菜单文件)
|
||||||
|
getMenuInflater().inflate(R.menu.toolbar_main, menu);
|
||||||
|
return super.onCreateOptionsMenu(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 菜单 item 点击时回调(处理菜单事件)
|
||||||
|
* 响应 Toolbar 菜单项的点击事件,执行对应业务逻辑
|
||||||
|
* @param item 被点击的菜单项
|
||||||
|
* @return true:消费点击事件;false:不消费(传递给父类)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
|
switch (item.getItemId()) {
|
||||||
|
case R.id.item_home:
|
||||||
|
// 点击 "首页/官网" 菜单项,唤起浏览器打开指定网站
|
||||||
|
openWebsiteInBrowser(this);
|
||||||
|
break;
|
||||||
|
// 可扩展其他菜单项(如设置、关于等)的处理逻辑
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 崩溃测试按钮点击事件(触发应用崩溃,用于调试异常捕获)
|
||||||
|
* 故意执行非法操作(循环获取不存在的字符串资源),强制应用崩溃
|
||||||
|
* @param view 触发事件的 View(对应布局中的崩溃测试按钮)
|
||||||
|
*/
|
||||||
|
public void onCrashTest(View view) {
|
||||||
|
// 循环从 Integer.MIN_VALUE 到 Integer.MAX_VALUE,获取不存在的字符串资源 ID,触发崩溃
|
||||||
|
for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
|
||||||
|
getString(i); // i 超出资源 ID 范围,抛出 Resources.NotFoundException 导致崩溃
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onLogTestNewTask(View view) {
|
||||||
|
LogActivity.startLogActivity(this, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志测试按钮点击事件(打开日志查看界面)
|
||||||
|
* 启动 LogActivity,用于查看应用运行日志
|
||||||
|
* @param view 触发事件的 View(对应布局中的日志测试按钮)
|
||||||
|
*/
|
||||||
|
public void onLogTest(View view) {
|
||||||
|
LogActivity.startLogActivity(this, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toast 工具测试按钮点击事件(测试全局 Toast 功能)
|
||||||
|
* 测试主线程、子线程中 Toast 的显示效果,验证 ToastUtils 的可用性
|
||||||
|
* @param view 触发事件的 View(对应布局中的 Toast 测试按钮)
|
||||||
|
*/
|
||||||
|
public void onToastUtilsTest(View view) {
|
||||||
|
LogUtils.d(TAG, "onToastUtilsTest"); // 打印调试日志,标识进入 Toast 测试
|
||||||
|
ToastUtils.show("Hello, WinBoLL!"); // 主线程显示 Toast
|
||||||
|
|
||||||
|
// 开启子线程,延迟 2 秒后显示 Toast(测试子线程 Toast 兼容性)
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
Thread.sleep(2000); // 线程休眠 2 秒
|
||||||
|
// 若 ToastUtils 已处理主线程切换,此处可直接调用;否则需通过 Handler 切换到主线程
|
||||||
|
ToastUtils.show("Thread.sleep(2000);ToastUtils.show...");
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// 捕获线程中断异常(如线程被销毁时),不做处理(测试场景)
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 唤起系统默认浏览器打开指定网站(跳转至应用官网)
|
||||||
|
* 通过 Intent.ACTION_VIEW 隐式意图,触发浏览器打开目标 URL
|
||||||
|
* @param context 上下文对象(如 Activity、Application,此处为 MainActivity)
|
||||||
|
*/
|
||||||
|
public void openWebsiteInBrowser(Context context) {
|
||||||
|
String url = "https://www.winboll.cc"; // 目标网站 URL(应用官网)
|
||||||
|
// 构建隐式意图:ACTION_VIEW 表示查看指定数据(Uri 为网站地址)
|
||||||
|
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||||
|
// 设置标志:在新的任务栈中启动 Activity(避免与当前应用任务栈混淆)
|
||||||
|
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
// 启动意图(唤起浏览器)
|
||||||
|
context.startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onAboutActivity(View view) {
|
||||||
|
LogUtils.d(TAG, "onAboutActivity() 调用");
|
||||||
|
Intent aboutIntent = new Intent(getApplicationContext(), AboutActivity.class);
|
||||||
|
startActivity(aboutIntent);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void onMultiInstance(View view) {
|
||||||
|
LogUtils.d(TAG, "onMultiInstance() 多开窗口按钮已点击");
|
||||||
|
ToastUtils.show("多开窗口:已启动新窗口");
|
||||||
|
android.content.Intent intent = new android.content.Intent(this, Main2Activity.class);
|
||||||
|
intent.setFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
LogUtils.d(TAG, "onMultiInstance() 准备启动Main2Activity");
|
||||||
|
startActivity(intent);
|
||||||
|
LogUtils.d(TAG, "onMultiInstance() Main2Activity已启动");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
package cc.winboll.studio.appbase.model;
|
||||||
|
|
||||||
|
import android.util.JsonReader;
|
||||||
|
import android.util.JsonWriter;
|
||||||
|
import cc.winboll.studio.libappbase.BaseBean;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试实体类
|
||||||
|
* 继承BaseBean实现JSON序列化/反序列化能力,提供基础int类型属性的封装与数据持久化支持
|
||||||
|
* 适配Java7语法,遵循BaseBean统一的反射识别、JSON读写规范
|
||||||
|
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||||
|
* @Date 2026/01/31 19:16:00
|
||||||
|
* @LastEditTime 2026/02/01 10:46:00
|
||||||
|
*/
|
||||||
|
public class TestBean extends BaseBean {
|
||||||
|
|
||||||
|
// ====================================== 常量定义 ======================================
|
||||||
|
/** 当前类的日志 TAG(用于调试输出) */
|
||||||
|
public static final String TAG = "TestBean";
|
||||||
|
|
||||||
|
// ====================================== 成员属性 ======================================
|
||||||
|
/**
|
||||||
|
* 测试数字属性(默认值:123)
|
||||||
|
* 基础int类型属性,用于测试BaseBean的JSON序列化/反序列化能力
|
||||||
|
*/
|
||||||
|
private int testNum1;
|
||||||
|
|
||||||
|
// ====================================== 构造方法 ======================================
|
||||||
|
/**
|
||||||
|
* 无参构造器(默认初始化)
|
||||||
|
* 给testNum1赋值默认值123,满足反射实例化、JSON解析的无参构造要求
|
||||||
|
*/
|
||||||
|
public TestBean() {
|
||||||
|
this.testNum1 = 123;
|
||||||
|
LogUtils.d(TAG, "TestBean无参构造器调用,testNum1默认初始化值:" + this.testNum1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 有参构造器(自定义初始化)
|
||||||
|
* @param testNum1 测试数字初始值
|
||||||
|
*/
|
||||||
|
public TestBean(int testNum1) {
|
||||||
|
this.testNum1 = testNum1;
|
||||||
|
LogUtils.d(TAG, "TestBean有参构造器调用,传入testNum1:" + testNum1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================== Get/Set 方法 ======================================
|
||||||
|
/**
|
||||||
|
* 设置测试数字属性值
|
||||||
|
* @param testNum1 待设置的int类型值
|
||||||
|
*/
|
||||||
|
public void setTestNum1(int testNum1) {
|
||||||
|
LogUtils.d(TAG, "setTestNum1调用,传入参数:" + testNum1);
|
||||||
|
this.testNum1 = testNum1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取测试数字属性值
|
||||||
|
* @return 当前testNum1的int类型值
|
||||||
|
*/
|
||||||
|
public int getTestNum1() {
|
||||||
|
LogUtils.d(TAG, "getTestNum1调用,返回值:" + this.testNum1);
|
||||||
|
return testNum1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================== 重写父类BaseBean方法 ======================================
|
||||||
|
/**
|
||||||
|
* 重写父类方法:获取当前类的全限定名
|
||||||
|
* 用于BaseBean反射识别、类名匹配等统一逻辑
|
||||||
|
* @return 类全限定名(cc.winboll.studio.appbase.model.TestBean)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
LogUtils.d(TAG, "getName方法调用,返回类全限定名:" + TestBean.class.getName());
|
||||||
|
return TestBean.class.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重写父类方法:将当前对象序列化为JSON(持久化存储专用)
|
||||||
|
* 遵循BaseBean规范,先执行父类序列化逻辑,再处理子类专属字段
|
||||||
|
* @param jsonWriter JSON写入器(外部传入的JSON流操作实例)
|
||||||
|
* @throws IOException JSON写入异常(流关闭、格式错误等)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||||
|
LogUtils.d(TAG, "writeThisToJsonWriter调用,传入参数JsonWriter:" + jsonWriter);
|
||||||
|
// 执行父类公共字段的序列化逻辑
|
||||||
|
super.writeThisToJsonWriter(jsonWriter);
|
||||||
|
// 序列化子类专属字段testNum1
|
||||||
|
jsonWriter.name("testNum1").value(this.getTestNum1());
|
||||||
|
LogUtils.d(TAG, "writeThisToJsonWriter执行完成,已序列化testNum1:" + this.getTestNum1());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重写父类方法:从JSON字段初始化当前对象属性(解析JSON专用)
|
||||||
|
* 先让父类处理公共字段,再匹配子类专属字段,不匹配则返回false跳过
|
||||||
|
* @param jsonReader JSON读取器(外部传入的JSON流操作实例)
|
||||||
|
* @param name 当前解析的JSON字段名
|
||||||
|
* @return true-字段解析成功;false-字段不匹配,需跳过/父类处理
|
||||||
|
* @throws IOException JSON读取异常(字段类型不匹配、流中断等)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
||||||
|
LogUtils.d(TAG, "initObjectsFromJsonReader调用,传入参数:name=" + name + ",JsonReader=" + jsonReader);
|
||||||
|
// 父类优先处理公共字段,处理成功则直接返回
|
||||||
|
if (super.initObjectsFromJsonReader(jsonReader, name)) {
|
||||||
|
LogUtils.d(TAG, "initObjectsFromJsonReader:字段" + name + "由父类BaseBean处理成功");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 解析子类专属字段
|
||||||
|
if ("testNum1".equals(name)) {
|
||||||
|
this.setTestNum1(jsonReader.nextInt());
|
||||||
|
LogUtils.d(TAG, "initObjectsFromJsonReader:解析testNum1成功,值为:" + this.getTestNum1());
|
||||||
|
} else {
|
||||||
|
LogUtils.w(TAG, "initObjectsFromJsonReader:字段" + name + "不匹配,返回false跳过解析");
|
||||||
|
// 字段不匹配,返回false表示跳过
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重写父类方法:从JSON读取器完整解析并初始化当前对象(JSON解析入口)
|
||||||
|
* 负责JSON对象的开始/结束标识处理,遍历所有字段并调用字段解析方法
|
||||||
|
* 严格遵循writeThisToJsonWriter的序列化结构,保证解析一致性
|
||||||
|
* @param jsonReader JSON读取器(外部传入的JSON流操作实例)
|
||||||
|
* @return 解析后的当前TestBean实例(支持链式调用)
|
||||||
|
* @throws IOException JSON解析异常(格式错误、字段缺失、流异常等)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||||
|
LogUtils.d(TAG, "readBeanFromJsonReader调用,传入参数JsonReader:" + jsonReader);
|
||||||
|
// 开始解析JSON对象,与序列化结构保持一致
|
||||||
|
jsonReader.beginObject();
|
||||||
|
// 遍历所有JSON字段
|
||||||
|
while (jsonReader.hasNext()) {
|
||||||
|
String fieldName = jsonReader.nextName();
|
||||||
|
LogUtils.d(TAG, "readBeanFromJsonReader:开始解析字段,fieldName=" + fieldName);
|
||||||
|
// 解析字段,不匹配则跳过该值
|
||||||
|
if (!this.initObjectsFromJsonReader(jsonReader, fieldName)) {
|
||||||
|
jsonReader.skipValue();
|
||||||
|
LogUtils.w(TAG, "readBeanFromJsonReader:字段" + fieldName + "解析失败,已跳过该值");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 结束JSON对象解析,必须调用避免流异常
|
||||||
|
jsonReader.endObject();
|
||||||
|
LogUtils.d(TAG, "readBeanFromJsonReader执行完成,JSON解析结束,当前TestBean实例testNum1:" + this.getTestNum1());
|
||||||
|
// 返回当前实例,支持链式调用
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
7
appbase/src/main/res/drawable/btn_light_blue.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#81C7F5"/> <!-- 浅蓝色填充 -->
|
||||||
|
<corners android:radius="8dp"/> <!-- 8dp 圆角 -->
|
||||||
|
</shape>
|
||||||
|
|
||||||
22
appbase/src/main/res/layout-night/activity_about.xml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?attr/activityBackgroundColor">
|
||||||
|
|
||||||
|
<android.widget.Toolbar
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/toolbarBackgroundColor"
|
||||||
|
android:id="@+id/toolbar"/>
|
||||||
|
|
||||||
|
<cc.winboll.studio.libappbase.views.AboutView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1.0"
|
||||||
|
android:id="@+id/aboutview"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
106
appbase/src/main/res/layout-night/activity_main.xml
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:padding="0dp"
|
||||||
|
android:background="?attr/activityBackgroundColor">
|
||||||
|
|
||||||
|
<android.widget.Toolbar
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/toolbarBackgroundColor"
|
||||||
|
android:id="@+id/toolbar"/>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1.0">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:spacing="12dp">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="关于应用"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="?attr/activityTextColor"
|
||||||
|
android:background="?attr/toolbarBackgroundColor"
|
||||||
|
android:paddingVertical="12dp"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:onClick="onAboutActivity"
|
||||||
|
android:layout_margin="10dp"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="应用崩溃测试"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="?attr/activityTextColor"
|
||||||
|
android:background="?attr/toolbarBackgroundColor"
|
||||||
|
android:paddingVertical="12dp"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:onClick="onCrashTest"
|
||||||
|
android:layout_margin="10dp"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="应用日志测试"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="?attr/activityTextColor"
|
||||||
|
android:background="?attr/toolbarBackgroundColor"
|
||||||
|
android:paddingVertical="12dp"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:onClick="onLogTest"
|
||||||
|
android:layout_margin="10dp"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="应用日志测试(新窗口)"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="?attr/activityTextColor"
|
||||||
|
android:background="?attr/toolbarBackgroundColor"
|
||||||
|
android:paddingVertical="12dp"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:onClick="onLogTestNewTask"
|
||||||
|
android:layout_margin="10dp"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="应用吐司测试"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="?attr/activityTextColor"
|
||||||
|
android:background="?attr/toolbarBackgroundColor"
|
||||||
|
android:paddingVertical="12dp"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:onClick="onToastUtilsTest"
|
||||||
|
android:layout_margin="10dp"/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="多开窗口"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="?attr/activityTextColor"
|
||||||
|
android:background="?attr/toolbarBackgroundColor"
|
||||||
|
android:paddingVertical="12dp"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:onClick="onMultiInstance"
|
||||||
|
android:layout_margin="10dp"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
17
appbase/src/main/res/layout-night/activity_main2.xml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:background="@android:color/white">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Main2Activity"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textColor="@color/gray_900"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
23
appbase/src/main/res/layout/activity_about.xml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?attr/activityBackgroundColor">
|
||||||
|
|
||||||
|
<android.widget.Toolbar
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/toolbarBackgroundColor"
|
||||||
|
android:id="@+id/toolbar"/>
|
||||||
|
|
||||||
|
<cc.winboll.studio.libappbase.views.AboutView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1.0"
|
||||||
|
android:id="@+id/aboutview"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
60
appbase/src/main/res/layout/activity_crash_test.xml
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:padding="0dp"
|
||||||
|
android:background="?attr/activityBackgroundColor">
|
||||||
|
|
||||||
|
<android.widget.Toolbar
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/toolbarBackgroundColor"
|
||||||
|
android:id="@+id/toolbar"/>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1.0">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<cc.winboll.studio.appbase.UndefinedCustomView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="10dp"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="返回"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="?attr/activityTextColor"
|
||||||
|
android:background="?attr/toolbarBackgroundColor"
|
||||||
|
android:paddingVertical="12dp"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:onClick="onBack"
|
||||||
|
android:layout_margin="10dp"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="测试崩溃"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="?attr/activityTextColor"
|
||||||
|
android:background="?attr/toolbarBackgroundColor"
|
||||||
|
android:paddingVertical="12dp"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:onClick="onTestCrash"
|
||||||
|
android:layout_margin="10dp"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
106
appbase/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:padding="0dp"
|
||||||
|
android:background="?attr/activityBackgroundColor">
|
||||||
|
|
||||||
|
<android.widget.Toolbar
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/toolbarBackgroundColor"
|
||||||
|
android:id="@+id/toolbar"/>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1.0">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:spacing="12dp">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="关于应用"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="?attr/activityTextColor"
|
||||||
|
android:background="?attr/toolbarBackgroundColor"
|
||||||
|
android:paddingVertical="12dp"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:onClick="onAboutActivity"
|
||||||
|
android:layout_margin="10dp"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="应用崩溃测试"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="?attr/activityTextColor"
|
||||||
|
android:background="?attr/toolbarBackgroundColor"
|
||||||
|
android:paddingVertical="12dp"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:onClick="onCrashTest"
|
||||||
|
android:layout_margin="10dp"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="应用日志测试"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="?attr/activityTextColor"
|
||||||
|
android:background="?attr/toolbarBackgroundColor"
|
||||||
|
android:paddingVertical="12dp"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:onClick="onLogTest"
|
||||||
|
android:layout_margin="10dp"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="应用日志测试(新窗口)"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="?attr/activityTextColor"
|
||||||
|
android:background="?attr/toolbarBackgroundColor"
|
||||||
|
android:paddingVertical="12dp"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:onClick="onLogTestNewTask"
|
||||||
|
android:layout_margin="10dp"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="应用吐司测试"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="?attr/activityTextColor"
|
||||||
|
android:background="?attr/toolbarBackgroundColor"
|
||||||
|
android:paddingVertical="12dp"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:onClick="onToastUtilsTest"
|
||||||
|
android:layout_margin="10dp"/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="多开窗口"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="?attr/activityTextColor"
|
||||||
|
android:background="?attr/toolbarBackgroundColor"
|
||||||
|
android:paddingVertical="12dp"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:onClick="onMultiInstance"
|
||||||
|
android:layout_margin="10dp"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
17
appbase/src/main/res/layout/activity_main2.xml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:background="?attr/activityBackgroundColor">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Main2Activity"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textColor="?attr/activityTextColor"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
9
appbase/src/main/res/layout/activity_nfcrsaoperate.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
9
appbase/src/main/res/menu/toolbar_main.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
<item
|
||||||
|
android:id="@+id/item_home"
|
||||||
|
android:title="Home"
|
||||||
|
android:icon="@drawable/ic_winboll"
|
||||||
|
android:showAsAction="always"/>
|
||||||
|
</menu>
|
||||||
7
appbase/src/main/res/values-night/colors.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="colorPrimary">#FF1B8B29</color>
|
||||||
|
<color name="colorPrimaryDark">#FF0A5520</color>
|
||||||
|
<color name="colorAccent">#FF6EE87C</color>
|
||||||
|
<color name="colorText">#FFB8FF7D</color>
|
||||||
|
</resources>
|
||||||
14
appbase/src/main/res/values-night/styles.xml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<style name="MyAPPBaseTheme" parent="APPBaseTheme">
|
||||||
|
<item name="themeDebug">@style/MyDebugActivityTheme</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="MyDebugActivityTheme" parent="DebugActivityTheme">
|
||||||
|
<item name="colorTittle">?attr/mainWindowDarkTextColor</item>
|
||||||
|
<item name="colorTittleBackgound">?attr/toolbarBackgroundColor</item>
|
||||||
|
<item name="colorText">?attr/debugTextColor</item>
|
||||||
|
<item name="colorTextBackgound">?attr/mainWindowDarkBackgroundColor</item>
|
||||||
|
<item name="toolbarTextColor">@color/toolbarTextColor</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
7
appbase/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="colorPrimary">#FF00B322</color>
|
||||||
|
<color name="colorPrimaryDark">#FF005C12</color>
|
||||||
|
<color name="colorAccent">#FF8DFFA2</color>
|
||||||
|
<color name="colorText">#FFFFFB8D</color>
|
||||||
|
</resources>
|
||||||
9
appbase/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">APPBase</string>
|
||||||
|
<string name="app_description">WinBoLL 安卓手机端安卓应用开发基础类库。</string>
|
||||||
|
<string name="app_normal">Click here is switch to Normal APP</string>
|
||||||
|
<string name="app_debug">Click here is switch to APP DEBUG</string>
|
||||||
|
<string name="gitea_home">GITEA HOME</string>
|
||||||
|
<string name="app_update">APP UPDATE</string>
|
||||||
|
</resources>
|
||||||
14
appbase/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<style name="MyAPPBaseTheme" parent="APPBaseTheme">
|
||||||
|
<item name="themeDebug">@style/MyDebugActivityTheme</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="MyDebugActivityTheme" parent="DebugActivityTheme">
|
||||||
|
<item name="colorTittle">?attr/mainWindowTextColor</item>
|
||||||
|
<item name="colorTittleBackgound">?attr/toolbarBackgroundColor</item>
|
||||||
|
<item name="colorText">?attr/debugTextColor</item>
|
||||||
|
<item name="colorTextBackgound">?attr/mainWindowBackgroundColor</item>
|
||||||
|
<item name="toolbarTextColor">@color/toolbarTextColor</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
12
appbase/src/stage/AndroidManifest.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools" >
|
||||||
|
|
||||||
|
<application>
|
||||||
|
|
||||||
|
<!-- Put flavor specific code here -->
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
|
|
||||||
6
appbase/src/stage/res/values/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- Put flavor specific strings here -->
|
||||||
|
|
||||||
|
</resources>
|
||||||
1
autonfc/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
34
autonfc/README.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# AutoNFC
|
||||||
|
|
||||||
|
#### 介绍
|
||||||
|
NFC 卡应用,主要管理 NFC 卡接触手机的动作响应,NFC 接触状态用于作为其他应用激活活动动作的启动令牌。
|
||||||
|
|
||||||
|
#### 软件架构
|
||||||
|
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
|
||||||
|
也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
|
||||||
|
|
||||||
|
|
||||||
|
#### Gradle 编译说明
|
||||||
|
调试版编译命令 :gradle assembleBetaDebug
|
||||||
|
阶段版编译命令 :bash .winboll/bashPublishAPKAddTag.sh autonfc
|
||||||
|
|
||||||
|
#### 使用说明
|
||||||
|
|
||||||
|
#### 参与贡献
|
||||||
|
|
||||||
|
1. Fork 本仓库
|
||||||
|
2. 新建 Feat_xxx 分支
|
||||||
|
3. 提交代码 : ZhanGSKen(ZhanGSKen<zhangsken@188.com>)
|
||||||
|
4. 新建 Pull Request
|
||||||
|
|
||||||
|
|
||||||
|
#### 特技
|
||||||
|
|
||||||
|
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
|
||||||
|
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
|
||||||
|
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
|
||||||
|
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
|
||||||
|
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
|
||||||
|
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
|
||||||
|
|
||||||
|
#### 参考文档
|
||||||
1
autonfc/app_update_description.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
119
autonfc/build.gradle
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply from: '../.winboll/winboll_app_build.gradle'
|
||||||
|
apply from: '../.winboll/winboll_lint_build.gradle'
|
||||||
|
|
||||||
|
def genVersionName(def versionName){
|
||||||
|
// 检查编译标志位配置
|
||||||
|
assert (winbollBuildProps['stageCount'] != null)
|
||||||
|
assert (winbollBuildProps['baseVersion'] != null)
|
||||||
|
// 保存基础版本号
|
||||||
|
winbollBuildProps.setProperty("baseVersion", "${versionName}");
|
||||||
|
//保存编译标志配置
|
||||||
|
FileOutputStream fos = new FileOutputStream(winbollBuildPropsFile)
|
||||||
|
winbollBuildProps.store(fos, "${winbollBuildPropsDesc}");
|
||||||
|
fos.close();
|
||||||
|
|
||||||
|
// 返回编译版本号
|
||||||
|
return "${versionName}." + winbollBuildProps['stageCount']
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
// 适配MIUI12
|
||||||
|
compileSdkVersion 30
|
||||||
|
buildToolsVersion "30.0.3"
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "cc.winboll.studio.autonfc"
|
||||||
|
minSdkVersion 23
|
||||||
|
// 适配MIUI12
|
||||||
|
targetSdkVersion 30
|
||||||
|
versionCode 1
|
||||||
|
// versionName 更新后需要手动设置
|
||||||
|
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
|
||||||
|
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
|
||||||
|
versionName "15.11"
|
||||||
|
if(true) {
|
||||||
|
versionName = genVersionName("${versionName}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 米盟 SDK
|
||||||
|
packagingOptions {
|
||||||
|
doNotStrip "*/*/libmimo_1011.so"
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
jniLibs.srcDirs = ['libs'] // 若SO库放在libs目录下
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
api 'com.google.code.gson:gson:2.10.1'
|
||||||
|
|
||||||
|
// 下拉控件
|
||||||
|
api 'com.baoyz.pullrefreshlayout:library:1.2.0'
|
||||||
|
|
||||||
|
// SSH
|
||||||
|
api 'com.jcraft:jsch:0.1.55'
|
||||||
|
// Html 解析
|
||||||
|
api 'org.jsoup:jsoup:1.13.1'
|
||||||
|
// 二维码类库
|
||||||
|
api 'com.google.zxing:core:3.4.1'
|
||||||
|
api 'com.journeyapps:zxing-android-embedded:3.6.0'
|
||||||
|
// 应用介绍页类库
|
||||||
|
api 'io.github.medyo:android-about-page:2.0.0'
|
||||||
|
// 网络连接类库
|
||||||
|
api 'com.squareup.okhttp3:okhttp:4.4.1'
|
||||||
|
// OkHttp网络请求
|
||||||
|
implementation 'com.squareup.okhttp3:okhttp:3.14.9'
|
||||||
|
// FastJSON解析
|
||||||
|
implementation 'com.alibaba:fastjson:1.2.76'
|
||||||
|
|
||||||
|
// AndroidX 类库
|
||||||
|
/*api 'androidx.appcompat:appcompat:1.1.0'
|
||||||
|
//api 'com.google.android.material:material:1.4.0'
|
||||||
|
//api 'androidx.viewpager:viewpager:1.0.0'
|
||||||
|
//api 'androidx.vectordrawable:vectordrawable:1.1.0'
|
||||||
|
//api 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
|
||||||
|
//api 'androidx.fragment:fragment:1.1.0'*/
|
||||||
|
|
||||||
|
|
||||||
|
// 米盟
|
||||||
|
api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk
|
||||||
|
//注意:以下5个库必须要引入
|
||||||
|
//implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||||
|
api 'androidx.recyclerview:recyclerview:1.0.0'
|
||||||
|
api 'com.google.code.gson:gson:2.8.5'
|
||||||
|
api 'com.github.bumptech.glide:glide:4.9.0'
|
||||||
|
//annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
|
||||||
|
|
||||||
|
implementation "androidx.annotation:annotation:1.3.0"
|
||||||
|
implementation "androidx.core:core:1.6.0"
|
||||||
|
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
|
||||||
|
implementation "androidx.preference:preference:1.1.1"
|
||||||
|
implementation "androidx.viewpager:viewpager:1.0.0"
|
||||||
|
implementation "com.google.android.material:material:1.4.0"
|
||||||
|
implementation "com.google.guava:guava:24.1-jre"
|
||||||
|
/*
|
||||||
|
implementation "io.noties.markwon:core:$markwonVersion"
|
||||||
|
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
|
||||||
|
implementation "io.noties.markwon:linkify:$markwonVersion"
|
||||||
|
implementation "io.noties.markwon:recycler:$markwonVersion"
|
||||||
|
*/
|
||||||
|
implementation 'com.termux:terminal-emulator:0.118.0'
|
||||||
|
implementation 'com.termux:terminal-view:0.118.0'
|
||||||
|
implementation 'com.termux:termux-shared:0.118.0'
|
||||||
|
|
||||||
|
// WinBoLL库 nexus.winboll.cc 地址
|
||||||
|
api 'cc.winboll.studio:libaes:15.15.2'
|
||||||
|
api 'cc.winboll.studio:libappbase:15.15.11'
|
||||||
|
|
||||||
|
// WinBoLL备用库 jitpack.io 地址
|
||||||
|
//api 'com.github.ZhanGSKen:AES:aes-v15.15.7'
|
||||||
|
//api 'com.github.ZhanGSKen:APPBase:appbase-v15.15.4'
|
||||||
|
|
||||||
|
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
|
}
|
||||||
8
autonfc/build.properties
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#Created by .winboll/winboll_app_build.gradle
|
||||||
|
#Mon Mar 16 18:30:19 GMT 2026
|
||||||
|
stageCount=0
|
||||||
|
libraryProject=
|
||||||
|
baseVersion=15.11
|
||||||
|
publishVersion=15.0.0
|
||||||
|
buildCount=54
|
||||||
|
baseBetaVersion=15.0.1
|
||||||
21
autonfc/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
||||||
12
autonfc/src/beta/AndroidManifest.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools" >
|
||||||
|
|
||||||
|
<application>
|
||||||
|
|
||||||
|
<!-- Put flavor specific code here -->
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
|
|
||||||
6
autonfc/src/beta/res/values/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<string name="app_name">AutoNFC✌</string>
|
||||||
|
|
||||||
|
</resources>
|
||||||
51
autonfc/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<manifest
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="cc.winboll.studio.autonfc">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.NFC"/>
|
||||||
|
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.nfc"
|
||||||
|
android:required="true"/>
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/MyAppTheme"
|
||||||
|
android:resizeableActivity="true"
|
||||||
|
android:name=".App">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:label="@string/app_name">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".nfc.NFCInterfaceActivity"
|
||||||
|
android:launchMode="singleTop">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
<data android:mimeType="*/*"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<!-- NFC 绑定服务 -->
|
||||||
|
<service
|
||||||
|
android:name=".nfc.AutoNFCService"
|
||||||
|
android:exported="false"/>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.max_aspect"
|
||||||
|
android:value="4.0"/>
|
||||||
|
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
|
|
||||||
344
autonfc/src/main/java/cc/winboll/studio/autonfc/App.java
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
package cc.winboll.studio.autonfc;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.ClipboardManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageInfo;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.HorizontalScrollView;
|
||||||
|
import android.widget.ScrollView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||||
|
import cc.winboll.studio.libappbase.ToastUtils;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.lang.Thread.UncaughtExceptionHandler;
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
public class App extends GlobalApplication {
|
||||||
|
|
||||||
|
private static Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
|
||||||
|
// 初始化 Toast 框架
|
||||||
|
// ToastUtils.init(this);
|
||||||
|
// // 设置 Toast 布局样式
|
||||||
|
// //ToastUtils.setView(R.layout.view_toast);
|
||||||
|
// ToastUtils.setStyle(new WhiteToastStyle());
|
||||||
|
// ToastUtils.setGravity(Gravity.BOTTOM, 0, 200);
|
||||||
|
//
|
||||||
|
//CrashHandler.getInstance().registerGlobal(this);
|
||||||
|
//CrashHandler.getInstance().registerPart(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void write(InputStream input, OutputStream output) throws IOException {
|
||||||
|
byte[] buf = new byte[1024 * 8];
|
||||||
|
int len;
|
||||||
|
while ((len = input.read(buf)) != -1) {
|
||||||
|
output.write(buf, 0, len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void write(File file, byte[] data) throws IOException {
|
||||||
|
File parent = file.getParentFile();
|
||||||
|
if (parent != null && !parent.exists()) parent.mkdirs();
|
||||||
|
|
||||||
|
ByteArrayInputStream input = new ByteArrayInputStream(data);
|
||||||
|
FileOutputStream output = new FileOutputStream(file);
|
||||||
|
try {
|
||||||
|
write(input, output);
|
||||||
|
} finally {
|
||||||
|
closeIO(input, output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String toString(InputStream input) throws IOException {
|
||||||
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||||
|
write(input, output);
|
||||||
|
try {
|
||||||
|
return output.toString("UTF-8");
|
||||||
|
} finally {
|
||||||
|
closeIO(input, output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void closeIO(Closeable... closeables) {
|
||||||
|
for (Closeable closeable : closeables) {
|
||||||
|
try {
|
||||||
|
if (closeable != null) closeable.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CrashHandler {
|
||||||
|
|
||||||
|
public static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER = Thread.getDefaultUncaughtExceptionHandler();
|
||||||
|
|
||||||
|
private static CrashHandler sInstance;
|
||||||
|
|
||||||
|
private PartCrashHandler mPartCrashHandler;
|
||||||
|
|
||||||
|
public static CrashHandler getInstance() {
|
||||||
|
if (sInstance == null) {
|
||||||
|
sInstance = new CrashHandler();
|
||||||
|
}
|
||||||
|
return sInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerGlobal(Context context) {
|
||||||
|
registerGlobal(context, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerGlobal(Context context, String crashDir) {
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandlerImpl(context.getApplicationContext(), crashDir));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unregister() {
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler(DEFAULT_UNCAUGHT_EXCEPTION_HANDLER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerPart(Context context) {
|
||||||
|
unregisterPart(context);
|
||||||
|
mPartCrashHandler = new PartCrashHandler(context.getApplicationContext());
|
||||||
|
MAIN_HANDLER.postAtFrontOfQueue(mPartCrashHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unregisterPart(Context context) {
|
||||||
|
if (mPartCrashHandler != null) {
|
||||||
|
mPartCrashHandler.isRunning.set(false);
|
||||||
|
mPartCrashHandler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class PartCrashHandler implements Runnable {
|
||||||
|
|
||||||
|
private final Context mContext;
|
||||||
|
|
||||||
|
public AtomicBoolean isRunning = new AtomicBoolean(true);
|
||||||
|
|
||||||
|
public PartCrashHandler(Context context) {
|
||||||
|
this.mContext = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
while (isRunning.get()) {
|
||||||
|
try {
|
||||||
|
Looper.loop();
|
||||||
|
} catch (final Throwable e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
if (isRunning.get()) {
|
||||||
|
MAIN_HANDLER.post(new Runnable(){
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(mContext, e.toString(), Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (e instanceof RuntimeException) {
|
||||||
|
throw (RuntimeException)e;
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class UncaughtExceptionHandlerImpl implements UncaughtExceptionHandler {
|
||||||
|
|
||||||
|
private static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss");
|
||||||
|
|
||||||
|
private final Context mContext;
|
||||||
|
|
||||||
|
private final File mCrashDir;
|
||||||
|
|
||||||
|
public UncaughtExceptionHandlerImpl(Context context, String crashDir) {
|
||||||
|
this.mContext = context;
|
||||||
|
this.mCrashDir = TextUtils.isEmpty(crashDir) ? new File(mContext.getExternalCacheDir(), "crash") : new File(crashDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void uncaughtException(Thread thread, Throwable throwable) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
String log = buildLog(throwable);
|
||||||
|
writeLog(log);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Intent intent = new Intent(mContext, CrashActivity.class);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
intent.putExtra(Intent.EXTRA_TEXT, log);
|
||||||
|
mContext.startActivity(intent);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
writeLog(e.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
throwable.printStackTrace();
|
||||||
|
android.os.Process.killProcess(android.os.Process.myPid());
|
||||||
|
System.exit(0);
|
||||||
|
|
||||||
|
} catch (Throwable e) {
|
||||||
|
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildLog(Throwable throwable) {
|
||||||
|
String time = DATE_FORMAT.format(new Date());
|
||||||
|
|
||||||
|
String versionName = "unknown";
|
||||||
|
long versionCode = 0;
|
||||||
|
try {
|
||||||
|
PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
|
||||||
|
versionName = packageInfo.versionName;
|
||||||
|
versionCode = Build.VERSION.SDK_INT >= 28 ? packageInfo.getLongVersionCode() : packageInfo.versionCode;
|
||||||
|
} catch (Throwable ignored) {}
|
||||||
|
|
||||||
|
LinkedHashMap<String, String> head = new LinkedHashMap<String, String>();
|
||||||
|
head.put("Time Of Crash", time);
|
||||||
|
head.put("Device", String.format("%s, %s", Build.MANUFACTURER, Build.MODEL));
|
||||||
|
head.put("Android Version", String.format("%s (%d)", Build.VERSION.RELEASE, Build.VERSION.SDK_INT));
|
||||||
|
head.put("App Version", String.format("%s (%d)", versionName, versionCode));
|
||||||
|
head.put("Kernel", getKernel());
|
||||||
|
head.put("Support Abis", Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_ABIS != null ? Arrays.toString(Build.SUPPORTED_ABIS): "unknown");
|
||||||
|
head.put("Fingerprint", Build.FINGERPRINT);
|
||||||
|
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
|
||||||
|
for (String key : head.keySet()) {
|
||||||
|
if (builder.length() != 0) builder.append("\n");
|
||||||
|
builder.append(key);
|
||||||
|
builder.append(" : ");
|
||||||
|
builder.append(head.get(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.append("\n\n");
|
||||||
|
builder.append(Log.getStackTraceString(throwable));
|
||||||
|
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeLog(String log) {
|
||||||
|
String time = DATE_FORMAT.format(new Date());
|
||||||
|
File file = new File(mCrashDir, "crash_" + time + ".txt");
|
||||||
|
try {
|
||||||
|
write(file, log.getBytes("UTF-8"));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getKernel() {
|
||||||
|
try {
|
||||||
|
return App.toString(new FileInputStream("/proc/version")).trim();
|
||||||
|
} catch (Throwable e) {
|
||||||
|
return e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class CrashActivity extends Activity {
|
||||||
|
|
||||||
|
private String mLog;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
setTheme(android.R.style.Theme_DeviceDefault);
|
||||||
|
setTitle("App Crash");
|
||||||
|
|
||||||
|
mLog = getIntent().getStringExtra(Intent.EXTRA_TEXT);
|
||||||
|
|
||||||
|
ScrollView contentView = new ScrollView(this);
|
||||||
|
contentView.setFillViewport(true);
|
||||||
|
|
||||||
|
HorizontalScrollView horizontalScrollView = new HorizontalScrollView(this);
|
||||||
|
|
||||||
|
TextView textView = new TextView(this);
|
||||||
|
int padding = dp2px(16);
|
||||||
|
textView.setPadding(padding, padding, padding, padding);
|
||||||
|
textView.setText(mLog);
|
||||||
|
textView.setTextIsSelectable(true);
|
||||||
|
textView.setTypeface(Typeface.DEFAULT);
|
||||||
|
textView.setLinksClickable(true);
|
||||||
|
|
||||||
|
horizontalScrollView.addView(textView);
|
||||||
|
contentView.addView(horizontalScrollView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
|
||||||
|
|
||||||
|
setContentView(contentView);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void restart() {
|
||||||
|
Intent intent = getPackageManager().getLaunchIntentForPackage(getPackageName());
|
||||||
|
if (intent != null) {
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
startActivity(intent);
|
||||||
|
}
|
||||||
|
finish();
|
||||||
|
android.os.Process.killProcess(android.os.Process.myPid());
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int dp2px(float dpValue) {
|
||||||
|
final float scale = Resources.getSystem().getDisplayMetrics().density;
|
||||||
|
return (int) (dpValue * scale + 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
|
menu.add(0, android.R.id.copy, 0, android.R.string.copy)
|
||||||
|
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
||||||
|
return super.onCreateOptionsMenu(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
|
switch (item.getItemId()) {
|
||||||
|
case android.R.id.copy:
|
||||||
|
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
|
cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
package cc.winboll.studio.autonfc;
|
||||||
|
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
import android.nfc.NfcAdapter;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import cc.winboll.studio.autonfc.nfc.ActionDialog;
|
||||||
|
import cc.winboll.studio.autonfc.nfc.AutoNFCService;
|
||||||
|
import cc.winboll.studio.autonfc.nfc.NFCInterfaceActivity;
|
||||||
|
import cc.winboll.studio.libappbase.LogActivity;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
|
||||||
|
public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
public static final String TAG = "MainActivity";
|
||||||
|
|
||||||
|
private NfcAdapter mNfcAdapter;
|
||||||
|
private PendingIntent mPendingIntent;
|
||||||
|
private AutoNFCService mService;
|
||||||
|
private boolean mBound = false;
|
||||||
|
|
||||||
|
// 服务连接
|
||||||
|
private ServiceConnection mConnection = new ServiceConnection() {
|
||||||
|
@Override
|
||||||
|
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||||
|
AutoNFCService.LocalBinder binder = (AutoNFCService.LocalBinder) service;
|
||||||
|
mService = binder.getService();
|
||||||
|
mBound = true;
|
||||||
|
LogUtils.d(TAG, "onServiceConnected: 服务已绑定");
|
||||||
|
|
||||||
|
// 关键:把 Activity 传给 Service,用于回调
|
||||||
|
mService.attachActivity(MainActivity.this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceDisconnected(ComponentName name) {
|
||||||
|
mBound = false;
|
||||||
|
mService = null;
|
||||||
|
LogUtils.d(TAG, "onServiceDisconnected: 服务已断开");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_main);
|
||||||
|
|
||||||
|
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||||
|
setSupportActionBar(toolbar);
|
||||||
|
|
||||||
|
// 初始化 NFC
|
||||||
|
mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
|
||||||
|
Intent nfcIntent = new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||||
|
mPendingIntent = PendingIntent.getActivity(this, 0, nfcIntent, 0);
|
||||||
|
|
||||||
|
LogUtils.d(TAG, "onCreate() -> NFC 监听已绑定到 MainActivity");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
// 绑定服务
|
||||||
|
Intent intent = new Intent(this, AutoNFCService.class);
|
||||||
|
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
|
||||||
|
LogUtils.d(TAG, "onStart: 绑定服务");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
// 解绑服务
|
||||||
|
if (mBound) {
|
||||||
|
unbindService(mConnection);
|
||||||
|
mBound = false;
|
||||||
|
LogUtils.d(TAG, "onStop: 解绑服务");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
LogUtils.d(TAG, "onResume() -> 开启 NFC 前台分发");
|
||||||
|
if (mNfcAdapter != null) {
|
||||||
|
mNfcAdapter.enableForegroundDispatch(this, mPendingIntent, null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
LogUtils.d(TAG, "onPause() -> 关闭 NFC 前台分发");
|
||||||
|
if (mNfcAdapter != null) {
|
||||||
|
mNfcAdapter.disableForegroundDispatch(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NFC 卡片靠近唯一入口
|
||||||
|
@Override
|
||||||
|
protected void onNewIntent(Intent intent) {
|
||||||
|
super.onNewIntent(intent);
|
||||||
|
LogUtils.d(TAG, "onNewIntent() -> 检测到 NFC 卡片");
|
||||||
|
|
||||||
|
// 把 NFC 事件交给 Service 处理
|
||||||
|
if (mBound && mService != null) {
|
||||||
|
mService.handleNfcIntent(intent);
|
||||||
|
} else {
|
||||||
|
LogUtils.e(TAG, "服务未绑定,无法处理 NFC");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
|
getMenuInflater().inflate(R.menu.main_menu, menu);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
|
int id = item.getItemId();
|
||||||
|
|
||||||
|
if (id == R.id.menu_log) {
|
||||||
|
LogActivity.startLogActivity(this);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onNFCInterfaceActivity(View view) {
|
||||||
|
startActivity(new Intent(this, NFCInterfaceActivity.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================= 【新增】关键方法:由 Service 回调来弹出对话框 =========================
|
||||||
|
/**
|
||||||
|
* Service 解析完 NFC 数据后,回调此方法在 Activity 中弹出对话框
|
||||||
|
*/
|
||||||
|
public void showNfcActionDialog(final String nfcData) {
|
||||||
|
LogUtils.d(TAG, "showNfcActionDialog() -> Activity 存活,安全弹出对话框");
|
||||||
|
|
||||||
|
// Activity 正在运行,直接弹框,绝对不会报 BadTokenException
|
||||||
|
final ActionDialog dialog = new ActionDialog(this);
|
||||||
|
dialog.setNfcData(nfcData);
|
||||||
|
dialog.setButtonClickListener(new ActionDialog.OnButtonClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onBuildClick() {
|
||||||
|
LogUtils.d(TAG, "点击 Build");
|
||||||
|
if (mService != null) {
|
||||||
|
mService.executeTermuxCommand(AutoNFCService.ACTION_BUILD, nfcData);
|
||||||
|
}
|
||||||
|
dialog.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewClick() {
|
||||||
|
LogUtils.d(TAG, "点击 View");
|
||||||
|
if (mService != null) {
|
||||||
|
mService.executeTermuxCommand(AutoNFCService.ACTION_BUILD_VIEW, nfcData);
|
||||||
|
}
|
||||||
|
dialog.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCancelClick() {
|
||||||
|
dialog.dismiss();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package cc.winboll.studio.autonfc.models;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||||
|
* @Date 2026/03/16 09:38
|
||||||
|
*/
|
||||||
|
public class NfcTermuxCmd {
|
||||||
|
|
||||||
|
private String script; // 要执行的预制脚本名(如 auth.sh)
|
||||||
|
private String[] args; // 脚本参数
|
||||||
|
private String workDir; // 工作目录
|
||||||
|
private boolean background; // 是否后台执行
|
||||||
|
private String resultDir; // 结果输出目录(可为 null)
|
||||||
|
|
||||||
|
public NfcTermuxCmd() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public NfcTermuxCmd(String script, String[] args, String workDir, boolean background, String resultDir) {
|
||||||
|
this.script = script;
|
||||||
|
this.args = args;
|
||||||
|
this.workDir = workDir;
|
||||||
|
this.background = background;
|
||||||
|
this.resultDir = resultDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getScript() {
|
||||||
|
return script;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setScript(String script) {
|
||||||
|
this.script = script;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] getArgs() {
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setArgs(String[] args) {
|
||||||
|
this.args = args;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getWorkDir() {
|
||||||
|
return workDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWorkDir(String workDir) {
|
||||||
|
this.workDir = workDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBackground() {
|
||||||
|
return background;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBackground(boolean background) {
|
||||||
|
this.background = background;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getResultDir() {
|
||||||
|
return resultDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setResultDir(String resultDir) {
|
||||||
|
this.resultDir = resultDir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package cc.winboll.studio.autonfc.nfc;
|
||||||
|
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
|
import cc.winboll.studio.autonfc.R;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义对话框类,用于与用户交互,展示 NFC 相关操作选项
|
||||||
|
* 兼容 Java 7 语法
|
||||||
|
*
|
||||||
|
* @author 豆包&ZhanGSKen
|
||||||
|
* @create 2025-08-15
|
||||||
|
* @lastModify 2026-03-17
|
||||||
|
*/
|
||||||
|
public class ActionDialog extends Dialog {
|
||||||
|
|
||||||
|
private static final String TAG = "ActionDialog";
|
||||||
|
private String mNfcData;
|
||||||
|
private OnButtonClickListener mClickListener;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*/
|
||||||
|
public ActionDialog(Context context) {
|
||||||
|
super(context);
|
||||||
|
initDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 NFC 数据
|
||||||
|
*/
|
||||||
|
public void setNfcData(String nfcData) {
|
||||||
|
this.mNfcData = nfcData;
|
||||||
|
LogUtils.d(TAG, "setNfcData() -> " + nfcData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置点击监听
|
||||||
|
*/
|
||||||
|
public void setButtonClickListener(OnButtonClickListener listener) {
|
||||||
|
this.mClickListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化布局
|
||||||
|
*/
|
||||||
|
private void initDialog() {
|
||||||
|
setTitle("请选择操作");
|
||||||
|
|
||||||
|
LinearLayout layout = new LinearLayout(getContext());
|
||||||
|
layout.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
layout.setPadding(20, 20, 20, 20);
|
||||||
|
|
||||||
|
addButtons(layout);
|
||||||
|
|
||||||
|
setContentView(layout);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加按钮
|
||||||
|
*/
|
||||||
|
private void addButtons(LinearLayout layout) {
|
||||||
|
// Build 按钮
|
||||||
|
Button btnBuild = createButton("Build", new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
LogUtils.d(TAG, "点击 Build");
|
||||||
|
if (mClickListener != null) {
|
||||||
|
mClickListener.onBuildClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
layout.addView(btnBuild);
|
||||||
|
|
||||||
|
// View 按钮
|
||||||
|
Button btnView = createButton("View", new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
LogUtils.d(TAG, "点击 View");
|
||||||
|
if (mClickListener != null) {
|
||||||
|
mClickListener.onViewClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
layout.addView(btnView);
|
||||||
|
|
||||||
|
// 取消按钮
|
||||||
|
Button btnCancel = createButton("Cancel", new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
LogUtils.d(TAG, "点击 Cancel");
|
||||||
|
dismiss();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
layout.addView(btnCancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建按钮
|
||||||
|
*/
|
||||||
|
private Button createButton(String text, View.OnClickListener listener) {
|
||||||
|
Button button = new Button(getContext());
|
||||||
|
button.setText(text);
|
||||||
|
button.setPadding(10, 10, 10, 10);
|
||||||
|
button.setOnClickListener(listener);
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回调接口
|
||||||
|
*/
|
||||||
|
public interface OnButtonClickListener {
|
||||||
|
void onBuildClick();
|
||||||
|
void onViewClick();
|
||||||
|
void onCancelClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
package cc.winboll.studio.autonfc.nfc;
|
||||||
|
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.nfc.NdefMessage;
|
||||||
|
import android.nfc.NdefRecord;
|
||||||
|
import android.nfc.NfcAdapter;
|
||||||
|
import android.nfc.Tag;
|
||||||
|
import android.nfc.tech.Ndef;
|
||||||
|
import android.os.Binder;
|
||||||
|
import android.os.IBinder;
|
||||||
|
|
||||||
|
import cc.winboll.studio.autonfc.MainActivity;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
import cc.winboll.studio.libappbase.ToastUtils;
|
||||||
|
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class AutoNFCService extends Service {
|
||||||
|
|
||||||
|
public static final String TAG = "AutoNFCService";
|
||||||
|
|
||||||
|
// ================= 已修改:更新为 Beta 包名 =================
|
||||||
|
public static final String ACTION_BUILD = "cc.winboll.studio.winboll.termux.NfcTermuxBridgeActivity.ACTION_BUILD";
|
||||||
|
public static final String ACTION_BUILD_VIEW = "cc.winboll.studio.winboll.termux.NfcTermuxBridgeActivity.ACTION_BUILD_VIEW";
|
||||||
|
|
||||||
|
private final IBinder mBinder = new LocalBinder();
|
||||||
|
private String mNfcData;
|
||||||
|
private MainActivity mActivity; // 持有 Activity 引用,用于回调
|
||||||
|
|
||||||
|
// ========================= 生命周期 =========================
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
LogUtils.d(TAG, "onCreate() -> 服务创建");
|
||||||
|
// 移除:startForeground(NOTIFICATION_ID, buildNotification());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
LogUtils.d(TAG, "onDestroy() -> 服务已停止");
|
||||||
|
mActivity = null; // 释放引用
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================= 服务绑定 =========================
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
LogUtils.d(TAG, "onBind() -> 服务被绑定");
|
||||||
|
return mBinder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onUnbind(Intent intent) {
|
||||||
|
LogUtils.d(TAG, "onUnbind() -> 服务解绑");
|
||||||
|
// 移除:stopForeground(true);
|
||||||
|
stopSelf();
|
||||||
|
return super.onUnbind(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================= 对外暴露方法 =========================
|
||||||
|
/**
|
||||||
|
* 绑定 Activity,用于回调显示对话框
|
||||||
|
*/
|
||||||
|
public void attachActivity(MainActivity activity) {
|
||||||
|
this.mActivity = activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 NFC 意图
|
||||||
|
*/
|
||||||
|
public void handleNfcIntent(Intent intent) {
|
||||||
|
LogUtils.d(TAG, "handleNfcIntent() -> 开始处理");
|
||||||
|
|
||||||
|
if (intent == null) {
|
||||||
|
LogUtils.e(TAG, "handleNfcIntent() -> 参数 intent 为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String action = intent.getAction();
|
||||||
|
LogUtils.d(TAG, "handleNfcIntent() -> Action = " + action);
|
||||||
|
|
||||||
|
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)
|
||||||
|
|| NfcAdapter.ACTION_TECH_DISCOVERED.equals(action)
|
||||||
|
|| NfcAdapter.ACTION_TAG_DISCOVERED.equals(action)) {
|
||||||
|
|
||||||
|
LogUtils.d(TAG, "handleNfcIntent() -> 匹配 NFC 动作");
|
||||||
|
|
||||||
|
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
|
||||||
|
if (tag == null) {
|
||||||
|
LogUtils.e(TAG, "handleNfcIntent() -> Tag 为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogUtils.d(TAG, "handleNfcIntent() -> Tag ID = " + bytesToHexString(tag.getId()));
|
||||||
|
LogUtils.d(TAG, "handleNfcIntent() -> Tech List = " + Arrays.toString(tag.getTechList()));
|
||||||
|
|
||||||
|
parseNdefData(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================= 内部业务 =========================
|
||||||
|
private void parseNdefData(Tag tag) {
|
||||||
|
LogUtils.d(TAG, "parseNdefData() -> 开始解析");
|
||||||
|
|
||||||
|
if (tag == null) return;
|
||||||
|
|
||||||
|
Ndef ndef = Ndef.get(tag);
|
||||||
|
if (ndef == null) {
|
||||||
|
LogUtils.e(TAG, "parseNdefData() -> 不支持 NDEF 格式");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ndef.connect();
|
||||||
|
NdefMessage msg = ndef.getNdefMessage();
|
||||||
|
|
||||||
|
if (msg == null || msg.getRecords() == null || msg.getRecords().length == 0) {
|
||||||
|
LogUtils.w(TAG, "parseNdefData() -> 卡片无数据");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NdefRecord record = msg.getRecords()[0];
|
||||||
|
byte[] payload = record.getPayload();
|
||||||
|
|
||||||
|
int langLen = payload[0] & 0x3F;
|
||||||
|
int start = 1 + langLen;
|
||||||
|
|
||||||
|
if (start < payload.length) {
|
||||||
|
mNfcData = new String(payload, start, payload.length - start, Charset.forName("UTF-8"));
|
||||||
|
LogUtils.d(TAG, "parseNdefData() -> 读卡成功: " + mNfcData);
|
||||||
|
|
||||||
|
// 关键:回调给 Activity 弹框,此时 Activity 一定是存活状态
|
||||||
|
if (mActivity != null) {
|
||||||
|
mActivity.showNfcActionDialog(mNfcData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.e(TAG, "parseNdefData() -> 读取失败", e);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
ndef.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 忽略关闭异常
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行 Termux 命令
|
||||||
|
*/
|
||||||
|
public void executeTermuxCommand(String action, String nfcData) {
|
||||||
|
LogUtils.d(TAG, "executeTermuxCommand() -> 开始执行");
|
||||||
|
|
||||||
|
if (nfcData == null || nfcData.isEmpty()) {
|
||||||
|
ToastUtils.show("数据错误");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
LogUtils.d(TAG, "executeTermuxCommand() -> 发送指令: " + nfcData);
|
||||||
|
|
||||||
|
Intent bridgeIntent = new Intent(action);
|
||||||
|
|
||||||
|
// ================= 已修改:使用 Beta 包名 =================
|
||||||
|
bridgeIntent.setClassName(
|
||||||
|
"cc.winboll.studio.winboll.beta",
|
||||||
|
"cc.winboll.studio.winboll.termux.NfcTermuxBridgeActivity"
|
||||||
|
);
|
||||||
|
|
||||||
|
bridgeIntent.putExtra(Intent.EXTRA_TEXT, nfcData);
|
||||||
|
bridgeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
|
||||||
|
startActivity(bridgeIntent);
|
||||||
|
ToastUtils.show("指令已发送");
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.e(TAG, "executeTermuxCommand() -> 发送失败", e);
|
||||||
|
ToastUtils.show("发送失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================= 工具方法 =========================
|
||||||
|
private String bytesToHexString(byte[] bytes) {
|
||||||
|
if (bytes == null || bytes.length == 0) return "";
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (byte b : bytes) {
|
||||||
|
sb.append(String.format("%02X", b));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================= Binder =========================
|
||||||
|
public class LocalBinder extends Binder {
|
||||||
|
public AutoNFCService getService() {
|
||||||
|
return AutoNFCService.this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
package cc.winboll.studio.autonfc.nfc;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.nfc.NfcAdapter;
|
||||||
|
import android.nfc.Tag;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import cc.winboll.studio.autonfc.R;
|
||||||
|
import cc.winboll.studio.autonfc.models.NfcTermuxCmd;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public class NFCInterfaceActivity extends Activity {
|
||||||
|
|
||||||
|
public static final String TAG = "NFCInterfaceActivity";
|
||||||
|
|
||||||
|
private EditText et_script;
|
||||||
|
private EditText et_args;
|
||||||
|
private EditText et_workDir;
|
||||||
|
private EditText et_background;
|
||||||
|
private EditText et_resultDir;
|
||||||
|
|
||||||
|
private TextView tvResult;
|
||||||
|
private TextView tvStatus;
|
||||||
|
|
||||||
|
private NfcAdapter mNfcAdapter;
|
||||||
|
private PendingIntent mNfcPendingIntent;
|
||||||
|
private Tag mCurrentTag;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_nfc_interface);
|
||||||
|
initView();
|
||||||
|
initNfc();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initView() {
|
||||||
|
et_script = findViewById(R.id.et_script);
|
||||||
|
et_args = findViewById(R.id.et_args);
|
||||||
|
et_workDir = findViewById(R.id.et_workDir);
|
||||||
|
et_background = findViewById(R.id.et_background);
|
||||||
|
et_resultDir = findViewById(R.id.et_resultDir);
|
||||||
|
|
||||||
|
tvResult = findViewById(R.id.tv_result);
|
||||||
|
tvStatus = findViewById(R.id.tv_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initNfc() {
|
||||||
|
mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
|
||||||
|
|
||||||
|
if (mNfcAdapter == null) {
|
||||||
|
tvStatus.setText("设备不支持NFC");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!mNfcAdapter.isEnabled()) {
|
||||||
|
tvStatus.setText("请开启NFC");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Intent nfcIntent = new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||||
|
mNfcPendingIntent = PendingIntent.getActivity(this, 0, nfcIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
|
||||||
|
tvStatus.setText("NFC已启动,等待卡片靠近");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
if (mNfcAdapter != null && mNfcAdapter.isEnabled()) {
|
||||||
|
mNfcAdapter.enableForegroundDispatch(this, mNfcPendingIntent, null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
if (mNfcAdapter != null) {
|
||||||
|
mNfcAdapter.disableForegroundDispatch(this);
|
||||||
|
}
|
||||||
|
mCurrentTag = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onNewIntent(Intent intent) {
|
||||||
|
super.onNewIntent(intent);
|
||||||
|
mCurrentTag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
|
||||||
|
if (mCurrentTag == null) return;
|
||||||
|
|
||||||
|
tvStatus.setText("卡片已连接,解析中...");
|
||||||
|
readNfc();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// 读取 NFC(完全委托给工具类)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
private void readNfc() {
|
||||||
|
try {
|
||||||
|
NfcTermuxCmd cmd = NfcUtils.readTag(mCurrentTag);
|
||||||
|
if (cmd == null) {
|
||||||
|
tvStatus.setText("读取成功:标签为空");
|
||||||
|
tvResult.setText("");
|
||||||
|
// 清空窗体
|
||||||
|
clearUiFields();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 核心改动:读取成功后,同时更新详情显示 和 窗体输入框
|
||||||
|
updateUiWithCmd(cmd);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.e(TAG, "readNfc 失败", e);
|
||||||
|
tvStatus.setText("读取失败:" + e.getMessage());
|
||||||
|
// 出错时清空窗体
|
||||||
|
clearUiFields();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// 新增:根据读取到的 Cmd 填充 UI(详情 + 窗体)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
private void updateUiWithCmd(NfcTermuxCmd cmd) {
|
||||||
|
if (cmd == null) return;
|
||||||
|
|
||||||
|
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA).format(new Date());
|
||||||
|
String show = "【读取时间】 " + time + "\n\n"
|
||||||
|
+ "【解析结果】\n"
|
||||||
|
+ "script: " + cmd.getScript() + "\n"
|
||||||
|
+ "args: " + (cmd.getArgs() != null ? String.join(", ", cmd.getArgs()) : "[]") + "\n"
|
||||||
|
+ "workDir: " + cmd.getWorkDir() + "\n"
|
||||||
|
+ "background: " + cmd.isBackground() + "\n"
|
||||||
|
+ "resultDir: " + cmd.getResultDir();
|
||||||
|
|
||||||
|
tvResult.setText(show);
|
||||||
|
tvStatus.setText("读取成功!");
|
||||||
|
|
||||||
|
// 👇 关键逻辑:自动填入窗体(每次读取后都会覆盖输入框)
|
||||||
|
et_script.setText(cmd.getScript() != null ? cmd.getScript() : "");
|
||||||
|
et_args.setText(cmd.getArgs() != null ? String.join(",", cmd.getArgs()) : "");
|
||||||
|
et_workDir.setText(cmd.getWorkDir() != null ? cmd.getWorkDir() : "");
|
||||||
|
et_background.setText(String.valueOf(cmd.isBackground()));
|
||||||
|
et_resultDir.setText(cmd.getResultDir() != null ? cmd.getResultDir() : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// 辅助:清空所有输入框
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
private void clearUiFields() {
|
||||||
|
et_script.setText("");
|
||||||
|
et_args.setText("");
|
||||||
|
et_workDir.setText("");
|
||||||
|
et_background.setText("");
|
||||||
|
et_resultDir.setText("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// 写入按钮(委托给工具类)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
public void onWriteClick(View view) {
|
||||||
|
if (mCurrentTag == null) {
|
||||||
|
showToast("请先靠近卡片");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
NfcTermuxCmd cmd = buildCmdFromUI();
|
||||||
|
NfcUtils.writeTag(mCurrentTag, cmd);
|
||||||
|
|
||||||
|
tvStatus.setText("写入成功!");
|
||||||
|
showToast("写入成功");
|
||||||
|
readNfc(); // 写入后重读,此时会自动填入窗体
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.e(TAG, "写入失败", e);
|
||||||
|
tvStatus.setText("写入失败:" + e.getMessage());
|
||||||
|
showToast("写入失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// 填充调试数据
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
public void onFillTestDataClick(View view) {
|
||||||
|
String testJson = "{\"script\":\"BuildWinBoLLProject.sh\",\"args\":[\"DebugTemp\"],\"workDir\":null,\"background\":true,\"resultDir\":null}";
|
||||||
|
try {
|
||||||
|
NfcTermuxCmd cmd = NfcUtils.jsonToCmd(testJson);
|
||||||
|
et_script.setText(cmd.getScript());
|
||||||
|
et_args.setText(cmd.getArgs() != null ? String.join(",", cmd.getArgs()) : "");
|
||||||
|
et_workDir.setText(cmd.getWorkDir() != null ? cmd.getWorkDir() : "");
|
||||||
|
et_background.setText(String.valueOf(cmd.isBackground()));
|
||||||
|
et_resultDir.setText(cmd.getResultDir() != null ? cmd.getResultDir() : "");
|
||||||
|
|
||||||
|
showToast("调试数据已填入");
|
||||||
|
} catch (Exception e) {
|
||||||
|
showToast("解析失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// 从 UI 构建 NfcTermuxCmd
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
private NfcTermuxCmd buildCmdFromUI() {
|
||||||
|
String script = et_script.getText().toString().trim();
|
||||||
|
String argsStr = et_args.getText().toString().trim();
|
||||||
|
String workDir = et_workDir.getText().toString().trim();
|
||||||
|
String bgStr = et_background.getText().toString().trim();
|
||||||
|
String resultDir = et_resultDir.getText().toString().trim();
|
||||||
|
|
||||||
|
NfcTermuxCmd cmd = new NfcTermuxCmd();
|
||||||
|
cmd.setScript(script);
|
||||||
|
cmd.setArgs(argsStr.isEmpty() ? new String[0] : argsStr.split(","));
|
||||||
|
cmd.setWorkDir(workDir.isEmpty() ? null : workDir);
|
||||||
|
cmd.setBackground("true".equalsIgnoreCase(bgStr));
|
||||||
|
cmd.setResultDir(resultDir.isEmpty() ? null : resultDir);
|
||||||
|
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showToast(String msg) {
|
||||||
|
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package cc.winboll.studio.autonfc.nfc;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class NfcStateMonitor {
|
||||||
|
private static Map<String, OnNfcStateListener> sListenerMap = new HashMap<>();
|
||||||
|
private static boolean sIsRunning = false;
|
||||||
|
|
||||||
|
public static void startMonitor() {
|
||||||
|
if (sIsRunning) return;
|
||||||
|
sListenerMap = new HashMap<>();
|
||||||
|
sIsRunning = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void stopMonitor() {
|
||||||
|
if (!sIsRunning) return;
|
||||||
|
sIsRunning = false;
|
||||||
|
if (sListenerMap != null) {
|
||||||
|
sListenerMap.clear();
|
||||||
|
sListenerMap = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 你原来的方法名:registerListener
|
||||||
|
public static void registerListener(String key, OnNfcStateListener listener) {
|
||||||
|
if (!sIsRunning || listener == null) return;
|
||||||
|
sListenerMap.put(key, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void unregisterListener(String key) {
|
||||||
|
if (!sIsRunning || key == null) return;
|
||||||
|
sListenerMap.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void notifyNfcConnected() {
|
||||||
|
if (!sIsRunning) return;
|
||||||
|
for (OnNfcStateListener l : sListenerMap.values()) {
|
||||||
|
l.onNfcConnected();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void notifyNfcDisconnected() {
|
||||||
|
if (!sIsRunning) return;
|
||||||
|
for (OnNfcStateListener l : sListenerMap.values()) {
|
||||||
|
l.onNfcDisconnected();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void notifyReadSuccess(String data) {
|
||||||
|
if (!sIsRunning) return;
|
||||||
|
for (OnNfcStateListener l : sListenerMap.values()) {
|
||||||
|
l.onNfcReadSuccess(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void notifyReadFail(String error) {
|
||||||
|
if (!sIsRunning) return;
|
||||||
|
for (OnNfcStateListener l : sListenerMap.values()) {
|
||||||
|
l.onNfcReadFail(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void notifyWriteSuccess() {
|
||||||
|
if (!sIsRunning) return;
|
||||||
|
for (OnNfcStateListener l : sListenerMap.values()) {
|
||||||
|
l.onNfcWriteSuccess();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void notifyWriteFail(String error) {
|
||||||
|
if (!sIsRunning) return;
|
||||||
|
for (OnNfcStateListener l : sListenerMap.values()) {
|
||||||
|
l.onNfcWriteFail(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package cc.winboll.studio.autonfc.nfc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||||
|
* @Date 2026/03/16 14:26
|
||||||
|
*/
|
||||||
|
import android.nfc.NdefMessage;
|
||||||
|
import android.nfc.NdefRecord;
|
||||||
|
import android.nfc.Tag;
|
||||||
|
import android.nfc.tech.Ndef;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonSyntaxException;
|
||||||
|
import cc.winboll.studio.autonfc.models.NfcTermuxCmd;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public class NfcUtils {
|
||||||
|
|
||||||
|
public static final String TAG = "NfcUtils";
|
||||||
|
private static Gson sGson = new Gson();
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// 读取 NFC 标签并解析为 NfcTermuxCmd
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
public static NfcTermuxCmd readTag(Tag tag) throws Exception {
|
||||||
|
if (tag == null) {
|
||||||
|
LogUtils.e(TAG, "readTag: tag is null");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ndef ndef = Ndef.get(tag);
|
||||||
|
if (ndef == null) {
|
||||||
|
LogUtils.e(TAG, "readTag: 不支持 NDEF");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ndef.connect();
|
||||||
|
NdefMessage msg = ndef.getNdefMessage();
|
||||||
|
if (msg == null) return null;
|
||||||
|
|
||||||
|
NdefRecord[] records = msg.getRecords();
|
||||||
|
if (records == null || records.length == 0) return null;
|
||||||
|
|
||||||
|
byte[] payload = records[0].getPayload();
|
||||||
|
int status = payload[0] & 0xFF;
|
||||||
|
int langLen = status & 0x3F;
|
||||||
|
int start = 1 + langLen;
|
||||||
|
|
||||||
|
if (start >= payload.length) return null;
|
||||||
|
|
||||||
|
String json = new String(payload, start, payload.length - start, Charset.forName("UTF-8"));
|
||||||
|
LogUtils.d(TAG, "readTag: 提取 JSON -> " + json);
|
||||||
|
|
||||||
|
return sGson.fromJson(json, NfcTermuxCmd.class);
|
||||||
|
} finally {
|
||||||
|
if (ndef != null && ndef.isConnected()) {
|
||||||
|
ndef.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// 写入 NfcTermuxCmd 到 NFC 标签
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
public static void writeTag(Tag tag, NfcTermuxCmd cmd) throws Exception {
|
||||||
|
if (tag == null) throw new Exception("tag is null");
|
||||||
|
|
||||||
|
String json = sGson.toJson(cmd);
|
||||||
|
writeJson(tag, json);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// 写入原始 JSON 字符串到 NFC
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
public static void writeJson(Tag tag, String json) throws Exception {
|
||||||
|
if (tag == null) throw new Exception("tag is null");
|
||||||
|
|
||||||
|
Ndef ndef = Ndef.get(tag);
|
||||||
|
if (ndef == null) throw new Exception("标签不支持 NDEF");
|
||||||
|
|
||||||
|
try {
|
||||||
|
ndef.connect();
|
||||||
|
int maxSize = ndef.getMaxSize();
|
||||||
|
int realSize = json.getBytes(Charset.forName("UTF-8")).length;
|
||||||
|
|
||||||
|
if (realSize > maxSize) {
|
||||||
|
throw new Exception("数据过大 (" + realSize + ") > 容量 (" + maxSize + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
NdefRecord record = createTextRecord(json, true);
|
||||||
|
NdefMessage msg = new NdefMessage(new NdefRecord[]{record});
|
||||||
|
|
||||||
|
ndef.writeNdefMessage(msg);
|
||||||
|
LogUtils.d(TAG, "writeJson: 写入成功");
|
||||||
|
} finally {
|
||||||
|
if (ndef != null && ndef.isConnected()) {
|
||||||
|
ndef.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// 创建 NFC 文本记录
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
public static NdefRecord createTextRecord(String text, boolean isUtf8) {
|
||||||
|
byte[] langBytes = "en".getBytes(Charset.forName("US-ASCII"));
|
||||||
|
byte[] textBytes = text.getBytes(Charset.forName(isUtf8 ? "UTF-8" : "UTF-16"));
|
||||||
|
|
||||||
|
int status = isUtf8 ? 0 : 0x80;
|
||||||
|
status |= langBytes.length & 0x3F;
|
||||||
|
|
||||||
|
byte[] data = new byte[1 + langBytes.length + textBytes.length];
|
||||||
|
data[0] = (byte) status;
|
||||||
|
System.arraycopy(langBytes, 0, data, 1, langBytes.length);
|
||||||
|
System.arraycopy(textBytes, 0, data, 1 + langBytes.length, textBytes.length);
|
||||||
|
|
||||||
|
return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_TEXT, new byte[0], data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// 辅助:JSON -> NfcTermuxCmd
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
public static NfcTermuxCmd jsonToCmd(String json) throws JsonSyntaxException {
|
||||||
|
return sGson.fromJson(json, NfcTermuxCmd.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// 辅助:NfcTermuxCmd -> JSON
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
public static String cmdToJson(NfcTermuxCmd cmd) {
|
||||||
|
return sGson.toJson(cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package cc.winboll.studio.autonfc.nfc;
|
||||||
|
|
||||||
|
public interface OnNfcStateListener {
|
||||||
|
void onNfcConnected(); // 无参数!
|
||||||
|
void onNfcDisconnected();
|
||||||
|
void onNfcReadSuccess(String data);
|
||||||
|
void onNfcReadFail(String error);
|
||||||
|
void onNfcWriteSuccess();
|
||||||
|
void onNfcWriteFail(String error);
|
||||||
|
}
|
||||||
|
|
||||||
34
autonfc/src/main/res/drawable-v24/ic_launcher_foreground.xml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:viewportWidth="108">
|
||||||
|
<path
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeWidth="1">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:endX="78.5885"
|
||||||
|
android:endY="90.9159"
|
||||||
|
android:startX="48.7653"
|
||||||
|
android:startY="61.0927"
|
||||||
|
android:type="linear">
|
||||||
|
<item
|
||||||
|
android:color="#44000000"
|
||||||
|
android:offset="0.0" />
|
||||||
|
<item
|
||||||
|
android:color="#00000000"
|
||||||
|
android:offset="1.0" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeWidth="1" />
|
||||||
|
</vector>
|
||||||
170
autonfc/src/main/res/drawable/ic_launcher_background.xml
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:viewportWidth="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="#26A69A"
|
||||||
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M9,0L9,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,0L19,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,0L29,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,0L39,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,0L49,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,0L59,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,0L69,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,0L79,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M89,0L89,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M99,0L99,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,9L108,9"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,19L108,19"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,29L108,29"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,39L108,39"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,49L108,49"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,59L108,59"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,69L108,69"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,79L108,79"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,89L108,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,99L108,99"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,29L89,29"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,39L89,39"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,49L89,49"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,59L89,59"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,69L89,69"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,79L89,79"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,19L29,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,19L39,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,19L49,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,19L59,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,19L69,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,19L79,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
</vector>
|
||||||
38
autonfc/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1.0"
|
||||||
|
android:gravity="center_vertical|center_horizontal">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="NFC Interface Activity"
|
||||||
|
android:onClick="onNFCInterfaceActivity"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
104
autonfc/src/main/res/layout/activity_nfc_interface.xml
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="脚本名称 script:"/>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/et_script"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="如 auth.sh"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="参数 args (逗号分隔):"/>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/et_args"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="user1,pass123"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="工作目录 workDir:"/>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/et_workDir"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="/data/data/com.termux/files/home"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="后台执行 background (true/false):"/>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/et_background"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="true"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="结果目录 resultDir:"/>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/et_resultDir"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="/data/data/com.termux/files/log"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:onClick="onFillTestDataClick"
|
||||||
|
android:text="填入调试数据 (BuildWinBoLLProject.sh)"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:onClick="onWriteClick"
|
||||||
|
android:text="写入 NFC (NfcTermuxCmd JSON)"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_status"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="状态"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_result"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
android:textSize="14sp"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
6
autonfc/src/main/res/menu/main_menu.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_log"
|
||||||
|
android:title="启动日志"/>
|
||||||
|
</menu>
|
||||||
5
autonfc/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
BIN
autonfc/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
autonfc/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
autonfc/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
autonfc/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
autonfc/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
autonfc/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
autonfc/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
autonfc/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
autonfc/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
autonfc/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
6
autonfc/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="colorPrimary">#009688</color>
|
||||||
|
<color name="colorPrimaryDark">#00796B</color>
|
||||||
|
<color name="colorAccent">#FF9800</color>
|
||||||
|
</resources>
|
||||||
4
autonfc/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">AutoNFC</string>
|
||||||
|
|
||||||
|
</resources>
|
||||||
11
autonfc/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="MyAppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</resources>
|
||||||
12
autonfc/src/stage/AndroidManifest.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools" >
|
||||||
|
|
||||||
|
<application>
|
||||||
|
|
||||||
|
<!-- Put flavor specific code here -->
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
|
|
||||||
6
autonfc/src/stage/res/values/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- Put flavor specific strings here -->
|
||||||
|
|
||||||
|
</resources>
|
||||||
40
contacts/README.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Contacts
|
||||||
|
源码参考自:
|
||||||
|
https://github.com/aJIEw/PhoneCallApp.git
|
||||||
|
|
||||||
|
#### 介绍
|
||||||
|
这是可以根据正则表达式匹配拦截骚扰电话的手机拨号应用。
|
||||||
|
|
||||||
|
#### 软件架构
|
||||||
|
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
|
||||||
|
也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
|
||||||
|
|
||||||
|
|
||||||
|
#### Gradle 编译说明
|
||||||
|
调试版编译命令 :gradle assembleBetaDebug
|
||||||
|
阶段版编译命令 :gradle assembleStageRelease
|
||||||
|
|
||||||
|
#### 使用说明
|
||||||
|
|
||||||
|
在安卓系统中需要设置两个权限允许。
|
||||||
|
1.自启动权限允许。
|
||||||
|
2.省电策略-无限制权限允许。
|
||||||
|
|
||||||
|
#### 参与贡献
|
||||||
|
|
||||||
|
1. Fork 本仓库
|
||||||
|
2. 新建 Feat_xxx 分支
|
||||||
|
3. 提交代码 : ZhanGSKen(ZhanGSKen<zhangsken@188.com>)
|
||||||
|
4. 新建 Pull Request
|
||||||
|
|
||||||
|
|
||||||
|
#### 特技
|
||||||
|
|
||||||
|
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
|
||||||
|
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
|
||||||
|
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
|
||||||
|
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
|
||||||
|
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
|
||||||
|
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
|
||||||
|
|
||||||
|
#### 参考文档
|
||||||
1
contacts/app_update_description.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
100
contacts/build.gradle
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply from: '../.winboll/winboll_app_build.gradle'
|
||||||
|
apply from: '../.winboll/winboll_lint_build.gradle'
|
||||||
|
|
||||||
|
def genVersionName(def versionName){
|
||||||
|
// 检查编译标志位配置
|
||||||
|
assert (winbollBuildProps['stageCount'] != null)
|
||||||
|
assert (winbollBuildProps['baseVersion'] != null)
|
||||||
|
// 保存基础版本号
|
||||||
|
winbollBuildProps.setProperty("baseVersion", "${versionName}");
|
||||||
|
//保存编译标志配置
|
||||||
|
FileOutputStream fos = new FileOutputStream(winbollBuildPropsFile)
|
||||||
|
winbollBuildProps.store(fos, "${winbollBuildPropsDesc}");
|
||||||
|
fos.close();
|
||||||
|
|
||||||
|
// 返回编译版本号
|
||||||
|
return "${versionName}." + winbollBuildProps['stageCount']
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
// 适配MIUI12
|
||||||
|
compileSdkVersion 30
|
||||||
|
buildToolsVersion "30.0.3"
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "cc.winboll.studio.contacts"
|
||||||
|
minSdkVersion 23
|
||||||
|
// 适配MIUI12
|
||||||
|
targetSdkVersion 30
|
||||||
|
versionCode 2
|
||||||
|
// versionName 更新后需要手动设置
|
||||||
|
// 项目模块目录的 build.gradle 文件的 stageCount=0
|
||||||
|
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
|
||||||
|
versionName "15.14"
|
||||||
|
if(true) {
|
||||||
|
versionName = genVersionName("${versionName}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 米盟 SDK
|
||||||
|
packagingOptions {
|
||||||
|
doNotStrip "*/*/libmimo_1011.so"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
// 米盟
|
||||||
|
api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk
|
||||||
|
//注意:以下5个库必须要引入
|
||||||
|
//api 'androidx.appcompat:appcompat:1.4.1'
|
||||||
|
api 'androidx.recyclerview:recyclerview:1.0.0'
|
||||||
|
api 'com.google.code.gson:gson:2.8.5'
|
||||||
|
api 'com.github.bumptech.glide:glide:4.9.0'
|
||||||
|
//annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
|
||||||
|
|
||||||
|
// 权限请求框架:https://github.com/getActivity/XXPermissions
|
||||||
|
//api 'com.github.getActivity:XXPermissions:18.63'
|
||||||
|
// 下拉控件
|
||||||
|
api 'com.baoyz.pullrefreshlayout:library:1.2.0'
|
||||||
|
// 拼音搜索
|
||||||
|
// https://mvnrepository.com/artifact/com.github.open-android/pinyin4j
|
||||||
|
api 'com.github.open-android:pinyin4j:2.5.0'
|
||||||
|
// SSH
|
||||||
|
api 'com.jcraft:jsch:0.1.55'
|
||||||
|
// Html 解析
|
||||||
|
api 'org.jsoup:jsoup:1.13.1'
|
||||||
|
// 二维码类库
|
||||||
|
api 'com.google.zxing:core:3.4.1'
|
||||||
|
api 'com.journeyapps:zxing-android-embedded:3.6.0'
|
||||||
|
// 应用介绍页类库
|
||||||
|
api 'io.github.medyo:android-about-page:2.0.0'
|
||||||
|
// 网络连接类库
|
||||||
|
api 'com.squareup.okhttp3:okhttp:4.4.1'
|
||||||
|
|
||||||
|
// AndroidX 类库
|
||||||
|
/*implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||||
|
implementation 'androidx.viewpager:viewpager:1.0.0'
|
||||||
|
implementation 'androidx.vectordrawable:vectordrawable:1.1.0'
|
||||||
|
implementation 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
|
||||||
|
implementation 'androidx.fragment:fragment:1.1.0'
|
||||||
|
implementation 'com.google.android.material:material:1.4.0'
|
||||||
|
*/
|
||||||
|
api 'androidx.appcompat:appcompat:1.1.0'
|
||||||
|
api 'com.google.android.material:material:1.4.0'
|
||||||
|
//api 'androidx.viewpager:viewpager:1.0.0'
|
||||||
|
//api 'androidx.vectordrawable:vectordrawable:1.1.0'
|
||||||
|
//api 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
|
||||||
|
//api 'androidx.fragment:fragment:1.1.0'
|
||||||
|
|
||||||
|
// WinBoLL库 nexus.winboll.cc 地址
|
||||||
|
api 'cc.winboll.studio:libaes:15.12.13'
|
||||||
|
api 'cc.winboll.studio:libappbase:15.14.2'
|
||||||
|
|
||||||
|
// WinBoLL备用库 jitpack.io 地址
|
||||||
|
//api 'com.github.ZhanGSKen:AES:aes-v15.12.9'
|
||||||
|
//api 'com.github.ZhanGSKen:APPBase:appbase-v15.14.1'
|
||||||
|
|
||||||
|
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
|
}
|
||||||
8
contacts/build.properties
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#Created by .winboll/winboll_app_build.gradle
|
||||||
|
#Sat Apr 18 21:14:59 HKT 2026
|
||||||
|
stageCount=13
|
||||||
|
libraryProject=
|
||||||
|
baseVersion=15.14
|
||||||
|
publishVersion=15.14.12
|
||||||
|
buildCount=0
|
||||||
|
baseBetaVersion=15.14.13
|
||||||
143
contacts/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# By default, the flags in this file are appended to flags specified
|
||||||
|
# in C:\tools\adt-bundle-windows-x86_64-20131030\sdk/tools/proguard/proguard-android.txt
|
||||||
|
# You can edit the include path and order by changing the proguardFiles
|
||||||
|
# directive in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# Add any project specific keep options here:
|
||||||
|
|
||||||
|
# ============================== 基础通用规则 ==============================
|
||||||
|
# 保留系统组件
|
||||||
|
-keep public class * extends android.app.Activity
|
||||||
|
-keep public class * extends android.app.Service
|
||||||
|
-keep public class * extends android.content.BroadcastReceiver
|
||||||
|
-keep public class * extends android.content.ContentProvider
|
||||||
|
-keep public class * extends android.app.backup.BackupAgentHelper
|
||||||
|
-keep public class * extends android.preference.Preference
|
||||||
|
|
||||||
|
# 保留 WinBoLL 核心包及子类(合并简化规则)
|
||||||
|
-keep class cc.winboll.studio.** { *; }
|
||||||
|
-keepclassmembers class cc.winboll.studio.** { *; }
|
||||||
|
|
||||||
|
# 保留所有类中的 public static final String TAG 字段(便于日志定位)
|
||||||
|
-keepclassmembers class * {
|
||||||
|
public static final java.lang.String TAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 保留序列化类(避免Parcelable/Gson解析异常)
|
||||||
|
-keep class * implements android.os.Parcelable {
|
||||||
|
public static final android.os.Parcelable$Creator *;
|
||||||
|
}
|
||||||
|
-keepclassmembers class * implements java.io.Serializable {
|
||||||
|
static final long serialVersionUID;
|
||||||
|
private static final java.io.ObjectStreamField[] serialPersistentFields;
|
||||||
|
private void writeObject(java.io.ObjectOutputStream);
|
||||||
|
private void readObject(java.io.ObjectInputStream);
|
||||||
|
java.lang.Object writeReplace();
|
||||||
|
java.lang.Object readResolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
# 保留 R 文件(避免资源ID混淆)
|
||||||
|
-keepclassmembers class **.R$* {
|
||||||
|
public static <fields>;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 保留 native 方法(避免JNI调用失败)
|
||||||
|
-keepclasseswithmembernames class * {
|
||||||
|
native <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 保留注解和泛型(避免反射/序列化异常)
|
||||||
|
-keepattributes *Annotation*
|
||||||
|
-keepattributes Signature
|
||||||
|
|
||||||
|
# 屏蔽 Java 8+ 警告(适配 Java 7 语法)
|
||||||
|
-dontwarn java.lang.invoke.*
|
||||||
|
-dontwarn android.support.v8.renderscript.*
|
||||||
|
-dontwarn java.util.function.**
|
||||||
|
|
||||||
|
# ============================== 第三方框架专项规则 ==============================
|
||||||
|
# OkHttp 4.4.1(米盟广告请求依赖,完善Lambda兼容)
|
||||||
|
-keep class okhttp3.** { *; }
|
||||||
|
-keep interface okhttp3.** { *; }
|
||||||
|
-keep class okhttp3.internal.** { *; }
|
||||||
|
-keep class okio.** { *; }
|
||||||
|
-dontwarn okhttp3.internal.platform.**
|
||||||
|
-dontwarn okio.**
|
||||||
|
# ============================== 必要补充规则 ==============================
|
||||||
|
# OkHttp 4.4.1 补充规则(Java 7 兼容)
|
||||||
|
-keep class okhttp3.internal.concurrent.** { *; }
|
||||||
|
-keep class okhttp3.internal.connection.** { *; }
|
||||||
|
-dontwarn okhttp3.internal.concurrent.TaskRunner
|
||||||
|
-dontwarn okhttp3.internal.connection.RealCall
|
||||||
|
|
||||||
|
# Glide 4.9.0(米盟广告图片加载依赖)
|
||||||
|
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||||
|
-keep public class * extends com.bumptech.glide.module.AppGlideModule
|
||||||
|
-keep public enum com.bumptech.glide.load.ImageHeaderParser$ImageType {
|
||||||
|
**[] $VALUES;
|
||||||
|
public *;
|
||||||
|
}
|
||||||
|
-keepclassmembers class * implements com.bumptech.glide.module.AppGlideModule {
|
||||||
|
<init>();
|
||||||
|
}
|
||||||
|
-dontwarn com.bumptech.glide.**
|
||||||
|
|
||||||
|
# Gson 2.8.5(米盟广告数据序列化依赖)
|
||||||
|
-keep class com.google.gson.** { *; }
|
||||||
|
-keep interface com.google.gson.** { *; }
|
||||||
|
-keepclassmembers class * {
|
||||||
|
@com.google.gson.annotations.SerializedName <fields>;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 米盟 SDK(核心广告组件,完整保留避免加载失败)
|
||||||
|
-keep class com.miui.zeus.** { *; }
|
||||||
|
-keep interface com.miui.zeus.** { *; }
|
||||||
|
# 保留米盟日志字段(便于广告加载失败排查)
|
||||||
|
-keepclassmembers class com.miui.zeus.mimo.sdk.** {
|
||||||
|
public static final java.lang.String TAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
# RecyclerView 1.0.0(米盟广告布局渲染依赖)
|
||||||
|
-keep class androidx.recyclerview.** { *; }
|
||||||
|
-keep interface androidx.recyclerview.** { *; }
|
||||||
|
-keepclassmembers class androidx.recyclerview.widget.RecyclerView$Adapter {
|
||||||
|
public *;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 其他第三方框架(按引入依赖保留,无则可删除)
|
||||||
|
# XXPermissions 18.63
|
||||||
|
-keep class com.hjq.permissions.** { *; }
|
||||||
|
-keep interface com.hjq.permissions.** { *; }
|
||||||
|
|
||||||
|
# ZXing 二维码(核心解析组件)
|
||||||
|
-keep class com.google.zxing.** { *; }
|
||||||
|
-keep class com.journeyapps.zxing.** { *; }
|
||||||
|
|
||||||
|
# Jsoup HTML解析
|
||||||
|
-keep class org.jsoup.** { *; }
|
||||||
|
|
||||||
|
# Pinyin4j 拼音搜索
|
||||||
|
-keep class net.sourceforge.pinyin4j.** { *; }
|
||||||
|
|
||||||
|
# JSch SSH组件
|
||||||
|
-keep class com.jcraft.jsch.** { *; }
|
||||||
|
|
||||||
|
# AndroidX 基础组件
|
||||||
|
-keep class androidx.appcompat.** { *; }
|
||||||
|
-keep interface androidx.appcompat.** { *; }
|
||||||
|
|
||||||
|
# ============================== 优化与调试配置 ==============================
|
||||||
|
# 优化级别(平衡混淆效果与性能)
|
||||||
|
-optimizationpasses 5
|
||||||
|
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
|
||||||
|
|
||||||
|
# 调试辅助(保留行号便于崩溃定位)
|
||||||
|
-verbose
|
||||||
|
-dontpreverify
|
||||||
|
-dontusemixedcaseclassnames
|
||||||
|
-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
13
contacts/src/beta/AndroidManifest.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools" >
|
||||||
|
|
||||||
|
<application
|
||||||
|
tools:replace="android:icon"
|
||||||
|
android:icon="@drawable/ic_winbollbeta">
|
||||||
|
|
||||||
|
<!-- Put flavor specific code here -->
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
|
|
||||||
6
contacts/src/beta/res/values/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<string name="app_name">Contacts+</string>
|
||||||
|
|
||||||
|
</resources>
|
||||||
258
contacts/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<manifest
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="cc.winboll.studio.contacts">
|
||||||
|
|
||||||
|
<!-- BIND_AUTOFILL_SERVICE -->
|
||||||
|
<uses-permission android:name="android.permission.BIND_AUTOFILL_SERVICE"/>
|
||||||
|
|
||||||
|
<!-- 拨打电话 -->
|
||||||
|
<uses-permission android:name="android.permission.CALL_PHONE"/>
|
||||||
|
|
||||||
|
<!-- 读取手机状态和身份 -->
|
||||||
|
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
|
||||||
|
|
||||||
|
<!-- 读取电话号码 -->
|
||||||
|
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS"/>
|
||||||
|
|
||||||
|
<!-- 修改系统设置 -->
|
||||||
|
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
|
||||||
|
|
||||||
|
<!-- 读取联系人 -->
|
||||||
|
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||||
|
|
||||||
|
<!-- 修改您的通讯录 -->
|
||||||
|
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||||
|
|
||||||
|
<!-- GET_CONTACTS -->
|
||||||
|
<uses-permission android:name="android.permission.GET_CONTACTS"/>
|
||||||
|
|
||||||
|
<!-- 此应用可显示在其他应用上方 -->
|
||||||
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||||
|
|
||||||
|
<!-- 更改您的音频设置 -->
|
||||||
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
||||||
|
|
||||||
|
<!-- 读取通话记录 -->
|
||||||
|
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
|
||||||
|
|
||||||
|
<!-- 新建/修改/删除通话记录 -->
|
||||||
|
<uses-permission android:name="android.permission.WRITE_CALL_LOG"/>
|
||||||
|
|
||||||
|
<!-- GET_CALL_LOG -->
|
||||||
|
<uses-permission android:name="android.permission.GET_CALL_LOG"/>
|
||||||
|
|
||||||
|
<!-- 录音 -->
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||||
|
|
||||||
|
<!-- 运行前台服务 -->
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||||
|
|
||||||
|
<!-- 运行“dataSync”类型的前台服务 -->
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||||
|
|
||||||
|
<!-- 运行“phoneCall”类型的前台服务 -->
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL"/>
|
||||||
|
|
||||||
|
<!-- 运行“microphone”类型的前台服务 -->
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
|
||||||
|
|
||||||
|
<!-- BIND_CALL_SCREENING_SERVICE -->
|
||||||
|
<uses-permission android:name="android.permission.BIND_CALL_SCREENING_SERVICE"/>
|
||||||
|
|
||||||
|
<!-- 读取您共享存储空间中的内容 -->
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
|
|
||||||
|
<!-- 修改或删除您共享存储空间中的内容 -->
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
|
|
||||||
|
<!-- MANAGE_EXTERNAL_STORAGE -->
|
||||||
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name=".App"
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@drawable/ic_winboll"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/MyAppTheme"
|
||||||
|
android:requestLegacyExternalStorage="true"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:exported="true">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".activities.CallActivity"
|
||||||
|
android:label="CallActivity"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:exported="true">
|
||||||
|
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".phonecallui.PhoneCallActivity"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:exported="true">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
|
||||||
|
<action android:name="android.intent.action.DIAL"/>
|
||||||
|
|
||||||
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
|
|
||||||
|
<data android:scheme="tel"/>
|
||||||
|
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
|
||||||
|
<action android:name="android.intent.action.DIAL"/>
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity android:name="cc.winboll.studio.contacts.activities.SettingsActivity"/>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".services.MainService"
|
||||||
|
android:foregroundServiceType="dataSync"
|
||||||
|
android:exported="false"
|
||||||
|
android:stopWithTask="false"/>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".services.AssistantService"
|
||||||
|
android:exported="false"
|
||||||
|
android:stopWithTask="false"/>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".phonecallui.PhoneCallService"
|
||||||
|
android:permission="android.permission.BIND_INCALL_SERVICE"
|
||||||
|
android:exported="false"
|
||||||
|
android:stopWithTask="false">
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.telecom.IN_CALL_SERVICE_UI"
|
||||||
|
android:value="true"/>
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
|
||||||
|
<action android:name="android.telecom.InCallService"/>
|
||||||
|
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".listenphonecall.CallListenerService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="false"
|
||||||
|
android:stopWithTask="false">
|
||||||
|
|
||||||
|
<intent-filter android:priority="1000">
|
||||||
|
|
||||||
|
<action android:name=".service.CallShowService"/>
|
||||||
|
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".services.MyCallScreeningService"
|
||||||
|
android:permission="android.permission.BIND_CALL_SCREENING_SERVICE"
|
||||||
|
android:exported="true"
|
||||||
|
android:stopWithTask="false">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
|
||||||
|
<action android:name="android.telecom.CallScreeningService"/>
|
||||||
|
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".receivers.MainReceiver"
|
||||||
|
android:stopWithTask="false">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
|
||||||
|
<action android:name="cc.winboll.studio.contacts.receivers.MainReceiver"/>
|
||||||
|
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".widgets.APPStatusWidget"
|
||||||
|
android:exported="true"
|
||||||
|
android:stopWithTask="false">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||||
|
|
||||||
|
<action android:name="cc.winboll.studio.contacts.widgets.APPStatusWidget.ACTION_STATUS_ACTIVE"/>
|
||||||
|
|
||||||
|
<action android:name="cc.winboll.studio.contacts.widgets.APPStatusWidget.ACTION_STATUS_NOACTIVE"/>
|
||||||
|
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/appwidget_provider_info"/>
|
||||||
|
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".widgets.APPStatusWidgetClickListener"
|
||||||
|
android:stopWithTask="false">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
|
||||||
|
<action android:name="cc.winboll.studio.contacts.widgets.APPStatusWidgetClickListener.ACTION_APPICON_CLICK"/>
|
||||||
|
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_provider"/>
|
||||||
|
|
||||||
|
</provider>
|
||||||
|
|
||||||
|
<activity android:name="cc.winboll.studio.contacts.activities.UnitTestActivity"/>
|
||||||
|
|
||||||
|
<activity android:name="cc.winboll.studio.contacts.activities.AboutActivity"/>
|
||||||
|
|
||||||
|
<service android:name="cc.winboll.studio.contacts.services.LimitedTimeSpecialChannelService"/>
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
package cc.winboll.studio.contacts;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||||
|
* @Date 2025/02/13 06:58:04
|
||||||
|
* @Describe Activity 栈管理工具,统一管理应用内 Activity 生命周期
|
||||||
|
* 适配:Java7 + Android API29-30 + 小米机型,优化并发安全与通话场景稳定性
|
||||||
|
*/
|
||||||
|
public class ActivityStack {
|
||||||
|
// 常量定义(核心标识+版本兼容常量)
|
||||||
|
public static final String TAG = "ActivityStack";
|
||||||
|
private static final int API_VERSION_O = 26; // Android 8.0 API26(isDestroyed适配用)
|
||||||
|
|
||||||
|
// 单例与核心成员变量(按优先级排序)
|
||||||
|
private static final ActivityStack INSTANCE = new ActivityStack();
|
||||||
|
// 替换为ArrayList+同步锁:解决CopyOnWriteArrayList迭代器不能删除的崩溃,兼顾并发安全
|
||||||
|
private final List<Activity> mActivityList = new ArrayList<Activity>();
|
||||||
|
private final Handler mMainHandler = new Handler(Looper.getMainLooper()); // 复用主线程Handler,避免内存泄漏
|
||||||
|
|
||||||
|
// 单例对外暴露方法
|
||||||
|
public static ActivityStack getInstance() {
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 私有构造,禁止外部实例化
|
||||||
|
private ActivityStack() {
|
||||||
|
LogUtils.d(TAG, "ActivityStack 初始化完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 栈基础操作(添加/移除) ======================
|
||||||
|
/**
|
||||||
|
* 添加Activity到栈中,避免重复入栈
|
||||||
|
* @param activity 待添加的Activity
|
||||||
|
*/
|
||||||
|
public void addActivity(Activity activity) {
|
||||||
|
if (activity == null) {
|
||||||
|
LogUtils.w(TAG, "addActivity: activity is null, skip");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 同步锁:解决多线程并发添加冲突(小米机型多线程场景适配)
|
||||||
|
synchronized (mActivityList) {
|
||||||
|
if (!mActivityList.contains(activity)) {
|
||||||
|
mActivityList.add(activity);
|
||||||
|
LogUtils.d(TAG, "addActivity: " + activity.getClass().getSimpleName() + ", stack size: " + mActivityList.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除Activity(不销毁,用于正常退出场景)
|
||||||
|
* @param activity 待移除的Activity
|
||||||
|
*/
|
||||||
|
public void removeActivity(Activity activity) {
|
||||||
|
if (activity == null) {
|
||||||
|
LogUtils.w(TAG, "removeActivity: activity is null, skip");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
synchronized (mActivityList) {
|
||||||
|
if (mActivityList.remove(activity)) {
|
||||||
|
LogUtils.d(TAG, "removeActivity: " + activity.getClass().getSimpleName() + ", stack size: " + mActivityList.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== Activity状态查询(获取/判断存活) ======================
|
||||||
|
/**
|
||||||
|
* 获取栈顶有效Activity(迭代遍历替代递归,避免栈溢出,适配小米多页面场景)
|
||||||
|
* @return 栈顶有效Activity,无则返回null
|
||||||
|
*/
|
||||||
|
public Activity getTopActivity() {
|
||||||
|
synchronized (mActivityList) {
|
||||||
|
if (mActivityList.isEmpty()) {
|
||||||
|
LogUtils.w(TAG, "getTopActivity: stack is empty, return null");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Activity validTopActivity = null;
|
||||||
|
// 倒序遍历,优先取最顶层有效Activity,同时清理无效残留
|
||||||
|
for (int i = mActivityList.size() - 1; i >= 0; i--) {
|
||||||
|
Activity activity = mActivityList.get(i);
|
||||||
|
// 版本兼容校验:API26+才支持isDestroyed
|
||||||
|
if (activity != null && !activity.isFinishing() && (getSdkVersion() < API_VERSION_O || !activity.isDestroyed())) {
|
||||||
|
validTopActivity = activity;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
mActivityList.remove(i);
|
||||||
|
String className = (activity != null) ? activity.getClass().getSimpleName() : "null";
|
||||||
|
LogUtils.w(TAG, "getTopActivity: remove invalid activity: " + className);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validTopActivity != null) {
|
||||||
|
LogUtils.d(TAG, "getTopActivity: top activity: " + validTopActivity.getClass().getSimpleName());
|
||||||
|
}
|
||||||
|
return validTopActivity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定类的有效Activity实例(通话场景核心方法,判断页面是否存活)
|
||||||
|
* @param activityClass 目标Activity类
|
||||||
|
* @return 有效实例,无则返回null
|
||||||
|
*/
|
||||||
|
public Activity getActivity(Class<?> activityClass) {
|
||||||
|
if (activityClass == null) {
|
||||||
|
LogUtils.w(TAG, "getActivity: activityClass is null, return null");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
synchronized (mActivityList) {
|
||||||
|
if (mActivityList.isEmpty()) {
|
||||||
|
LogUtils.w(TAG, "getActivity: stack empty, return null");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Activity activity : mActivityList) {
|
||||||
|
if (activity != null && activity.getClass().equals(activityClass) && !activity.isFinishing() && (getSdkVersion() < API_VERSION_O || !activity.isDestroyed())) {
|
||||||
|
LogUtils.d(TAG, "getActivity: find valid activity: " + activityClass.getSimpleName());
|
||||||
|
return activity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LogUtils.w(TAG, "getActivity: no valid activity: " + activityClass.getSimpleName());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断指定Activity是否存活(简化通话场景调用,避免重复判空)
|
||||||
|
* @param activityClass 目标Activity类
|
||||||
|
* @return true:存活,false:未存活
|
||||||
|
*/
|
||||||
|
public boolean isActivityAlive(Class<?> activityClass) {
|
||||||
|
boolean isAlive = getActivity(activityClass) != null;
|
||||||
|
LogUtils.d(TAG, "isActivityAlive: " + activityClass.getSimpleName() + ", result: " + isAlive);
|
||||||
|
return isAlive;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== Activity销毁操作(单/批量/全部) ======================
|
||||||
|
/**
|
||||||
|
* 销毁栈顶Activity(主线程执行,适配小米机型线程限制)
|
||||||
|
*/
|
||||||
|
public void finishTopActivity() {
|
||||||
|
runOnMainThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
synchronized (mActivityList) {
|
||||||
|
if (mActivityList.isEmpty()) {
|
||||||
|
LogUtils.w(TAG, "finishTopActivity: stack is empty, skip");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先移除再校验,避免并发冲突(小米多线程场景适配)
|
||||||
|
Activity topActivity = mActivityList.remove(mActivityList.size() - 1);
|
||||||
|
if (topActivity == null) {
|
||||||
|
LogUtils.w(TAG, "finishTopActivity: top activity is null, skip");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!topActivity.isFinishing() && (getSdkVersion() < API_VERSION_O || !topActivity.isDestroyed())) {
|
||||||
|
topActivity.finish();
|
||||||
|
LogUtils.d(TAG, "finishTopActivity: destroy top activity: " + topActivity.getClass().getSimpleName() + ", stack size: " + mActivityList.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁指定Activity(主线程执行,避免跨线程异常)
|
||||||
|
* @param activity 待销毁的Activity
|
||||||
|
*/
|
||||||
|
public void finishActivity(final Activity activity) {
|
||||||
|
runOnMainThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (activity == null) {
|
||||||
|
LogUtils.w(TAG, "finishActivity: activity is null, skip");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
synchronized (mActivityList) {
|
||||||
|
if (mActivityList.contains(activity) && !activity.isFinishing() && (getSdkVersion() < API_VERSION_O || !activity.isDestroyed())) {
|
||||||
|
mActivityList.remove(activity);
|
||||||
|
activity.finish();
|
||||||
|
LogUtils.d(TAG, "finishActivity: destroy activity: " + activity.getClass().getSimpleName() + ", stack size: " + mActivityList.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁指定类的所有Activity(核心修复:迭代器删除崩溃,通话场景核心)
|
||||||
|
* @param activityClass 目标Activity类
|
||||||
|
*/
|
||||||
|
public void finishActivity(final Class<?> activityClass) {
|
||||||
|
runOnMainThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (activityClass == null) {
|
||||||
|
LogUtils.w(TAG, "finishActivity: activityClass is null, skip");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
synchronized (mActivityList) {
|
||||||
|
if (mActivityList.isEmpty()) {
|
||||||
|
LogUtils.w(TAG, "finishActivity: stack empty, skip");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 核心修复:用索引遍历+倒序删除,替代迭代器删除(避免UnsupportedOperationException)
|
||||||
|
for (int i = mActivityList.size() - 1; i >= 0; i--) {
|
||||||
|
Activity activity = mActivityList.get(i);
|
||||||
|
if (activity != null && activity.getClass().equals(activityClass)) {
|
||||||
|
if (!activity.isFinishing() && (getSdkVersion() < API_VERSION_O || !activity.isDestroyed())) {
|
||||||
|
mActivityList.remove(i); // 索引删除,支持ArrayList
|
||||||
|
activity.finish();
|
||||||
|
LogUtils.d(TAG, "finishActivity: destroy class activity: " + activityClass.getSimpleName() + ", stack size: " + mActivityList.size());
|
||||||
|
} else {
|
||||||
|
mActivityList.remove(i); // 清理无效残留
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁栈中所有Activity(退出应用/清空栈场景用)
|
||||||
|
*/
|
||||||
|
public void finishAllActivity() {
|
||||||
|
runOnMainThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
synchronized (mActivityList) {
|
||||||
|
if (mActivityList.isEmpty()) {
|
||||||
|
LogUtils.w(TAG, "finishAllActivity: stack is empty, skip");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 遍历销毁所有有效Activity,逐个状态校验(小米机型稳定性适配)
|
||||||
|
for (Activity activity : mActivityList) {
|
||||||
|
if (activity != null && !activity.isFinishing() && (getSdkVersion() < API_VERSION_O || !activity.isDestroyed())) {
|
||||||
|
activity.finish();
|
||||||
|
LogUtils.d(TAG, "finishAllActivity: destroy activity: " + activity.getClass().getSimpleName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mActivityList.clear();
|
||||||
|
LogUtils.d(TAG, "finishAllActivity: all activity destroyed, stack cleared");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 栈优化与工具方法 ======================
|
||||||
|
/**
|
||||||
|
* 清理栈中所有无效Activity(null/已销毁/已结束),优化小米机型内存占用
|
||||||
|
*/
|
||||||
|
public void clearInvalidActivities() {
|
||||||
|
runOnMainThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
synchronized (mActivityList) {
|
||||||
|
if (mActivityList.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 倒序索引删除,避免遍历过程中索引错乱
|
||||||
|
for (int i = mActivityList.size() - 1; i >= 0; i--) {
|
||||||
|
Activity activity = mActivityList.get(i);
|
||||||
|
if (activity == null || activity.isFinishing() || (getSdkVersion() >= API_VERSION_O && activity.isDestroyed())) {
|
||||||
|
mActivityList.remove(i);
|
||||||
|
String className = (activity != null) ? activity.getClass().getSimpleName() : "null";
|
||||||
|
LogUtils.d(TAG, "clearInvalidActivities: remove invalid activity: " + className);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LogUtils.d(TAG, "clearInvalidActivities: done, stack size: " + mActivityList.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保任务在主线程执行(Activity操作必须主线程,小米机型严格限制)
|
||||||
|
* @param runnable 待执行任务
|
||||||
|
*/
|
||||||
|
private void runOnMainThread(Runnable runnable) {
|
||||||
|
if (runnable == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 避免不必要的线程切换,优化性能(小米机型流畅度适配)
|
||||||
|
if (Looper.getMainLooper() == Looper.myLooper()) {
|
||||||
|
runnable.run();
|
||||||
|
} else {
|
||||||
|
mMainHandler.post(runnable);
|
||||||
|
LogUtils.d(TAG, "runOnMainThread: post task to main thread");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 辅助方法:获取当前系统SDK版本(简化版本判断逻辑,统一调用)
|
||||||
|
* @return SDK版本号
|
||||||
|
*/
|
||||||
|
private int getSdkVersion() {
|
||||||
|
return android.os.Build.VERSION.SDK_INT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
33
contacts/src/main/java/cc/winboll/studio/contacts/App.java
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package cc.winboll.studio.contacts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||||
|
* @Date 2024/12/08 15:10:51
|
||||||
|
* @Describe 全局应用类
|
||||||
|
*/
|
||||||
|
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||||
|
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||||
|
import cc.winboll.studio.libappbase.ToastUtils;
|
||||||
|
|
||||||
|
public class App extends GlobalApplication {
|
||||||
|
|
||||||
|
public static final String TAG = "App";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
// 设置应用调试标志
|
||||||
|
setIsDebugging(BuildConfig.DEBUG);
|
||||||
|
|
||||||
|
// 初始化窗口管理类
|
||||||
|
WinBoLLActivityManager.init(this);
|
||||||
|
// 初始化 Toast 框架
|
||||||
|
ToastUtils.init(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTerminate() {
|
||||||
|
super.onTerminate();
|
||||||
|
ToastUtils.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,529 @@
|
|||||||
|
package cc.winboll.studio.contacts;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.ActivityManager;
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.telecom.TelecomManager;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.CheckBox;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
import androidx.fragment.app.FragmentPagerAdapter;
|
||||||
|
import androidx.viewpager.widget.ViewPager;
|
||||||
|
import cc.winboll.studio.contacts.activities.SettingsActivity;
|
||||||
|
import cc.winboll.studio.contacts.activities.WinBollActivity;
|
||||||
|
import cc.winboll.studio.contacts.dun.Rules;
|
||||||
|
import cc.winboll.studio.contacts.fragments.CallLogFragment;
|
||||||
|
import cc.winboll.studio.contacts.fragments.ContactsFragment;
|
||||||
|
import cc.winboll.studio.contacts.fragments.LogFragment;
|
||||||
|
import cc.winboll.studio.contacts.model.MainServiceBean;
|
||||||
|
import cc.winboll.studio.contacts.services.MainService;
|
||||||
|
import cc.winboll.studio.contacts.utils.PermissionUtils;
|
||||||
|
import cc.winboll.studio.contacts.views.DunTemperatureView;
|
||||||
|
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||||
|
import cc.winboll.studio.libaes.views.ADsBannerView;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
import cc.winboll.studio.libappbase.LogView;
|
||||||
|
import com.google.android.material.tabs.TabLayout;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||||
|
* @Date 2025/08/30 14:32
|
||||||
|
* @Describe Contacts 主窗口(完全适配 API 30 + Java 7 语法)
|
||||||
|
* 核心优化:1. 移除电话状态监听 2. 移除通话筛选服务 3. 移除 MainService 所有相关逻辑 4. ViewPager 实现 Fragment 懒加载(仅首屏初始化)
|
||||||
|
* 问题修复:解决首屏 Fragment 空白问题(删除 setPrimaryItem 冲突逻辑+延迟首屏初始化)
|
||||||
|
*/
|
||||||
|
public final class MainActivity extends WinBollActivity implements IWinBoLLActivity, ViewPager.OnPageChangeListener, View.OnClickListener {
|
||||||
|
|
||||||
|
// ====================== 1. 常量定义区(硬编码API版本,避免高版本依赖) ======================
|
||||||
|
public static final String TAG = "MainActivity";
|
||||||
|
public static final int REQUEST_HOME_ACTIVITY = 0;
|
||||||
|
public static final int REQUEST_ABOUT_ACTIVITY = 1;
|
||||||
|
public static final int REQUEST_APP_SETTINGS = 2;
|
||||||
|
public static final String ACTION_SOS = "cc.winboll.studio.libappbase.WinBoLL.ACTION_SOS";
|
||||||
|
private static final int DIALER_REQUEST_CODE = 1;
|
||||||
|
private static final int REQUEST_REQUIRED_PERMISSIONS = 1002;
|
||||||
|
private static final int REQUEST_OVERLAY_PERMISSION = 1003;
|
||||||
|
|
||||||
|
// API版本硬编码常量(Java 7兼容,杜绝Build.VERSION_CODES高版本引用)
|
||||||
|
private static final int ANDROID_6_API = 23;
|
||||||
|
private static final int ANDROID_8_API = 26;
|
||||||
|
private static final int ANDROID_10_API = 29;
|
||||||
|
private static final int ANDROID_14_API = 34;
|
||||||
|
|
||||||
|
// ====================== 2. 静态成员区 ======================
|
||||||
|
static MainActivity _MainActivity;
|
||||||
|
|
||||||
|
// ====================== 3. 权限常量区 ======================
|
||||||
|
private final String[] REQUIRED_PERMISSIONS = PermissionUtils.BASE_PERMISSIONS;
|
||||||
|
|
||||||
|
// ====================== 4. UI控件成员区 ======================
|
||||||
|
private ADsBannerView mADsBannerView;
|
||||||
|
private LogView mLogView;
|
||||||
|
private Toolbar mToolbar;
|
||||||
|
private CheckBox cbMainService;
|
||||||
|
private TabLayout tabLayout;
|
||||||
|
private ViewPager viewPager;
|
||||||
|
private List<View> views;
|
||||||
|
private ImageView[] imageViews;
|
||||||
|
private LinearLayout linearLayout;
|
||||||
|
|
||||||
|
// ====================== 5. 业务逻辑成员区 ======================
|
||||||
|
private int currentPoint = 0;
|
||||||
|
private List<Fragment> fragmentList;
|
||||||
|
private List<String> tabTitleList;
|
||||||
|
// 记录已初始化的Fragment位置(避免重复初始化)
|
||||||
|
private boolean[] isFragmentInit;
|
||||||
|
|
||||||
|
// ====================== 6. 接口实现区 ======================
|
||||||
|
@Override
|
||||||
|
public Activity getActivity() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTag() {
|
||||||
|
return TAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 7. 生命周期函数区 ======================
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
LogUtils.d(TAG, "===== onCreate: 主Activity开始创建 =====");
|
||||||
|
_MainActivity = this;
|
||||||
|
|
||||||
|
// 直接初始化UI(原权限检查逻辑注释保留,按需启用)
|
||||||
|
initUIAndLogic(savedInstanceState);
|
||||||
|
|
||||||
|
MainServiceBean mainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||||
|
if (mainServiceBean != null && mainServiceBean.isEnable()) {
|
||||||
|
Intent intent = new Intent(this, MainService.class);
|
||||||
|
// 根据应用前后台状态选择启动方式(Android 12+ 后台用 startForegroundService)
|
||||||
|
if (Build.VERSION.SDK_INT >= 31) {
|
||||||
|
startForegroundService(intent);
|
||||||
|
} else {
|
||||||
|
startService(intent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LogUtils.d(TAG, "===== onCreate: 主Activity创建流程结束 =====");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostCreate(Bundle savedInstanceState) {
|
||||||
|
super.onPostCreate(savedInstanceState);
|
||||||
|
LogUtils.d(TAG, "onPostCreate: 主Activity创建完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
if (mADsBannerView != null) {
|
||||||
|
mADsBannerView.resumeADs(MainActivity.this);
|
||||||
|
LogUtils.d(TAG, "onResume: 广告栏资源已恢复");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
LogUtils.d(TAG, "===== onDestroy: 主Activity开始销毁 =====");
|
||||||
|
// 释放广告资源
|
||||||
|
if (mADsBannerView != null) {
|
||||||
|
mADsBannerView.releaseAdResources();
|
||||||
|
LogUtils.d(TAG, "onDestroy: 广告栏资源已释放");
|
||||||
|
}
|
||||||
|
// 清空Fragment相关引用,避免内存泄漏
|
||||||
|
if (fragmentList != null) {
|
||||||
|
fragmentList.clear();
|
||||||
|
fragmentList = null;
|
||||||
|
}
|
||||||
|
if (tabTitleList != null) {
|
||||||
|
tabTitleList.clear();
|
||||||
|
tabTitleList = null;
|
||||||
|
}
|
||||||
|
isFragmentInit = null;
|
||||||
|
LogUtils.d(TAG, "===== onDestroy: 主Activity销毁完成 =====");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 8. 权限相关回调函数区 ======================
|
||||||
|
@Override
|
||||||
|
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
|
LogUtils.d(TAG, "onRequestPermissionsResult: 权限请求回调,requestCode=" + requestCode);
|
||||||
|
|
||||||
|
if (requestCode == REQUEST_REQUIRED_PERMISSIONS) {
|
||||||
|
String deniedPerms = PermissionUtils.getDeniedPermissions(this, permissions);
|
||||||
|
if (deniedPerms.length() == 0) {
|
||||||
|
LogUtils.d(TAG, "onRequestPermissionsResult: 所有危险权限授予成功");
|
||||||
|
checkAndRequestRemainingPermissions();
|
||||||
|
} else {
|
||||||
|
LogUtils.e(TAG, "onRequestPermissionsResult: 被拒权限:" + deniedPerms);
|
||||||
|
showPermissionDeniedDialogAndExit("应用需要「" + deniedPerms + "」权限才能正常运行,请授予权限后重新打开应用。");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
|
LogUtils.d(TAG, "onActivityResult: 页面回调触发,requestCode=" + requestCode + ",resultCode=" + resultCode);
|
||||||
|
|
||||||
|
switch (requestCode) {
|
||||||
|
case DIALER_REQUEST_CODE:
|
||||||
|
if (resultCode == Activity.RESULT_OK) {
|
||||||
|
LogUtils.d(TAG, "onActivityResult: 设为默认拨号应用成功");
|
||||||
|
Toast.makeText(MainActivity.this, getString(R.string.app_name) + " 已成为默认电话应用", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case REQUEST_APP_SETTINGS:
|
||||||
|
LogUtils.d(TAG, "onActivityResult: 从设置页返回,重建Activity");
|
||||||
|
recreate();
|
||||||
|
break;
|
||||||
|
case REQUEST_OVERLAY_PERMISSION:
|
||||||
|
handleOverlayPermissionResult();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LogUtils.w(TAG, "onActivityResult: 未知requestCode=" + requestCode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理悬浮窗权限申请结果
|
||||||
|
*/
|
||||||
|
private void handleOverlayPermissionResult() {
|
||||||
|
if (PermissionUtils.isOverlayPermissionGranted(this)) {
|
||||||
|
LogUtils.d(TAG, "handleOverlayPermissionResult: 悬浮窗权限申请成功");
|
||||||
|
LogUtils.d(TAG, "handleOverlayPermissionResult: 所有权限已授予");
|
||||||
|
initUIAndLogic(null);
|
||||||
|
} else {
|
||||||
|
LogUtils.e(TAG, "handleOverlayPermissionResult: 悬浮窗权限申请失败");
|
||||||
|
showPermissionDeniedDialogAndExit("应用需要悬浮窗权限才能展示来电弹窗,请授予后重新打开应用。");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查并申请剩余权限(仅保留悬浮窗)
|
||||||
|
*/
|
||||||
|
private void checkAndRequestRemainingPermissions() {
|
||||||
|
if (!PermissionUtils.isOverlayPermissionGranted(this)) {
|
||||||
|
LogUtils.d(TAG, "checkAndRequestRemainingPermissions: 悬浮窗权限未授予,跳转设置页");
|
||||||
|
PermissionUtils.requestOverlayPermission(this, REQUEST_OVERLAY_PERMISSION);
|
||||||
|
} else {
|
||||||
|
LogUtils.d(TAG, "checkAndRequestRemainingPermissions: 所有权限已授予");
|
||||||
|
initUIAndLogic(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权限拒绝提示对话框(Java 7 匿名内部类实现,禁止Lambda)
|
||||||
|
*/
|
||||||
|
private void showPermissionDeniedDialogAndExit(String tip) {
|
||||||
|
LogUtils.d(TAG, "showPermissionDeniedDialogAndExit: 弹出权限不足提示框");
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||||
|
builder.setTitle("权限不足,无法使用");
|
||||||
|
builder.setMessage(tip);
|
||||||
|
builder.setCancelable(false);
|
||||||
|
|
||||||
|
builder.setNegativeButton("去设置", new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
dialog.dismiss();
|
||||||
|
LogUtils.d(TAG, "showPermissionDeniedDialogAndExit: 用户选择去设置权限");
|
||||||
|
PermissionUtils.goAppDetailsSettings(MainActivity.this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.setPositiveButton("确定退出", new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
dialog.dismiss();
|
||||||
|
LogUtils.d(TAG, "showPermissionDeniedDialogAndExit: 用户选择退出应用");
|
||||||
|
finishAndRemoveTask();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 9. UI与业务逻辑初始化区 ======================
|
||||||
|
private void initUIAndLogic(Bundle savedInstanceState) {
|
||||||
|
if (mToolbar != null) {
|
||||||
|
LogUtils.d(TAG, "initUIAndLogic: UI已初始化,无需重复执行");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogUtils.d(TAG, "===== initUIAndLogic: 开始初始化UI与业务逻辑 =====");
|
||||||
|
setContentView(R.layout.activity_main);
|
||||||
|
|
||||||
|
// 1. 工具栏初始化
|
||||||
|
mToolbar = (Toolbar) findViewById(R.id.activitymainToolbar1);
|
||||||
|
setSupportActionBar(mToolbar);
|
||||||
|
getSupportActionBar().setSubtitle(TAG);
|
||||||
|
LogUtils.d(TAG, "initUIAndLogic: 工具栏初始化完成");
|
||||||
|
|
||||||
|
// 2. TabLayout与ViewPager初始化
|
||||||
|
tabLayout = (TabLayout) findViewById(R.id.tabLayout);
|
||||||
|
viewPager = (ViewPager) findViewById(R.id.viewPager);
|
||||||
|
initViewPagerAndTabs();
|
||||||
|
tabLayout.setupWithViewPager(viewPager);
|
||||||
|
LogUtils.d(TAG, "initUIAndLogic: ViewPager与TabLayout初始化完成");
|
||||||
|
|
||||||
|
// 3. 广告栏初始化
|
||||||
|
mADsBannerView = (ADsBannerView) findViewById(R.id.adsbanner);
|
||||||
|
LogUtils.d(TAG, "initUIAndLogic: 广告栏控件初始化完成");
|
||||||
|
|
||||||
|
// 左边盾值视图初始化(Java7分步写法,禁止链式调用)
|
||||||
|
DunTemperatureView tempViewLeft = (DunTemperatureView) findViewById(R.id.dun_temp_view_left);
|
||||||
|
tempViewLeft.setMaxValue(Rules.getInstance(this).getSettingsModel().getDunTotalCount());
|
||||||
|
tempViewLeft.setCurrentValue(Rules.getInstance(this).getSettingsModel().getDunCurrentCount());
|
||||||
|
|
||||||
|
int[] customColors = new int[2];
|
||||||
|
customColors[0] = Color.parseColor("#FF3366FF");
|
||||||
|
customColors[1] = Color.parseColor("#FF9900CC");
|
||||||
|
float[] positions = new float[2];
|
||||||
|
positions[0] = 0.0f;
|
||||||
|
positions[1] = 1.0f;
|
||||||
|
tempViewLeft.setGradientColors(customColors, positions);
|
||||||
|
// 文本放在温度条右侧(默认,可省略)
|
||||||
|
tempViewLeft.setTextPosition(true);
|
||||||
|
// 右边盾值视图初始化(Java7分步写法,禁止链式调用)
|
||||||
|
DunTemperatureView tempViewRight = (DunTemperatureView) findViewById(R.id.dun_temp_view_right);
|
||||||
|
tempViewRight.setMaxValue(Rules.getInstance(this).getSettingsModel().getDunTotalCount());
|
||||||
|
tempViewRight.setCurrentValue(Rules.getInstance(this).getSettingsModel().getDunCurrentCount());
|
||||||
|
|
||||||
|
tempViewRight.setGradientColors(customColors, positions);
|
||||||
|
// 文本放在温度条左侧
|
||||||
|
tempViewRight.setTextPosition(false);
|
||||||
|
LogUtils.d(TAG, "initUIAndLogic: 盾值视图初始化完成");
|
||||||
|
LogUtils.d(TAG, "===== initUIAndLogic: 初始化流程全部结束 =====");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化ViewPager与Tab数据(Java7规范,泛型完整声明),添加懒加载标记
|
||||||
|
* 关键修改:延迟50ms初始化首屏,确保Fragment控件就绪;删除setPrimaryItem冲突逻辑
|
||||||
|
*/
|
||||||
|
private void initViewPagerAndTabs() {
|
||||||
|
LogUtils.d(TAG, "initViewPagerAndTabs: 开始初始化ViewPager数据");
|
||||||
|
fragmentList = new ArrayList<Fragment>();
|
||||||
|
tabTitleList = new ArrayList<String>();
|
||||||
|
|
||||||
|
// 添加Fragment实例(仅创建对象,不初始化业务逻辑)
|
||||||
|
fragmentList.add(CallLogFragment.newInstance(0));
|
||||||
|
fragmentList.add(ContactsFragment.newInstance(1));
|
||||||
|
fragmentList.add(LogFragment.newInstance(2));
|
||||||
|
tabTitleList.add("通话记录");
|
||||||
|
tabTitleList.add("联系人");
|
||||||
|
tabTitleList.add("应用日志");
|
||||||
|
|
||||||
|
// 初始化懒加载标记数组(默认均未初始化)
|
||||||
|
int fragmentCount = fragmentList.size();
|
||||||
|
isFragmentInit = new boolean[fragmentCount];
|
||||||
|
for (int i = 0; i < fragmentCount; i++) {
|
||||||
|
isFragmentInit[i] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置自定义适配器(已删除setPrimaryItem,避免初始化冲突)
|
||||||
|
LazyLoadPagerAdapter adapter = new LazyLoadPagerAdapter(getSupportFragmentManager(), fragmentList, tabTitleList);
|
||||||
|
viewPager.setAdapter(adapter);
|
||||||
|
// 关闭预加载(设为0仅加载当前页,关键)
|
||||||
|
viewPager.setOffscreenPageLimit(0);
|
||||||
|
viewPager.addOnPageChangeListener(this);
|
||||||
|
|
||||||
|
// 关键优化:延迟50ms初始化首屏(确保Fragment已完成onCreateView,控件绑定就绪)
|
||||||
|
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
initFragmentByPosition(0);
|
||||||
|
LogUtils.d(TAG, "initViewPagerAndTabs: 延迟初始化首屏Fragment,位置=0");
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
LogUtils.d(TAG, "initViewPagerAndTabs: ViewPager初始化完成,等待延迟初始化首屏");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据位置初始化Fragment(调用Fragment的初始化逻辑,避免重复执行)
|
||||||
|
* 优化:添加isAdded判断,确保Fragment已附加到Activity,防止上下文空指针
|
||||||
|
*/
|
||||||
|
private void initFragmentByPosition(int position) {
|
||||||
|
// 校验位置合法性 + 避免重复初始化 + 确保Fragment已附加到Activity
|
||||||
|
if (position < 0 || position >= fragmentList.size() || isFragmentInit[position]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Fragment targetFragment = fragmentList.get(position);
|
||||||
|
if (targetFragment != null && targetFragment.isAdded()) {
|
||||||
|
// 触发Fragment初始化(调用各Fragment的initData方法)
|
||||||
|
if (targetFragment instanceof CallLogFragment) {
|
||||||
|
((CallLogFragment) targetFragment).initData();
|
||||||
|
} else if (targetFragment instanceof ContactsFragment) {
|
||||||
|
((ContactsFragment) targetFragment).initData();
|
||||||
|
} else if (targetFragment instanceof LogFragment) {
|
||||||
|
((LogFragment) targetFragment).initData();
|
||||||
|
}
|
||||||
|
// 标记为已初始化
|
||||||
|
isFragmentInit[position] = true;
|
||||||
|
LogUtils.d(TAG, "initFragmentByPosition: 初始化Fragment,位置=" + position + ",标题=" + tabTitleList.get(position));
|
||||||
|
} else {
|
||||||
|
LogUtils.w(TAG, "initFragmentByPosition: Fragment未附加到Activity/实例为空,位置=" + position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 10. 菜单相关函数区 ======================
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
|
getMenuInflater().inflate(R.menu.toolbar_main, menu);
|
||||||
|
LogUtils.d(TAG, "onCreateOptionsMenu: 菜单加载完成");
|
||||||
|
return super.onCreateOptionsMenu(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
|
if (item.getItemId() == R.id.item_settings) {
|
||||||
|
LogUtils.d(TAG, "onOptionsItemSelected: 用户点击设置菜单");
|
||||||
|
startActivity(new Intent(this, SettingsActivity.class));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 11. ViewPager页面回调区(切换时初始化对应Fragment) ======================
|
||||||
|
@Override
|
||||||
|
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPageSelected(int position) {
|
||||||
|
currentPoint = position;
|
||||||
|
LogUtils.d(TAG, "onPageSelected: 页面切换至[" + position + "],标题=" + tabTitleList.get(position));
|
||||||
|
// 切换页面时,初始化当前页Fragment(未初始化过才执行)
|
||||||
|
initFragmentByPosition(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPageScrollStateChanged(int state) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {}
|
||||||
|
|
||||||
|
// ====================== 12. 工具函数区 ======================
|
||||||
|
/**
|
||||||
|
* 拨号工具方法(添加空指针防护)
|
||||||
|
*/
|
||||||
|
public static void dialPhoneNumber(String phoneNumber) {
|
||||||
|
if (_MainActivity == null) {
|
||||||
|
LogUtils.e(TAG, "dialPhoneNumber: MainActivity实例为空,无法拨号");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (phoneNumber == null || phoneNumber.trim().isEmpty()) {
|
||||||
|
LogUtils.e(TAG, "dialPhoneNumber: 拨号号码为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (PermissionUtils.checkPermission(_MainActivity, Manifest.permission.CALL_PHONE)) {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_DIAL);
|
||||||
|
intent.setData(Uri.parse("tel:" + phoneNumber));
|
||||||
|
LogUtils.d(TAG, "dialPhoneNumber: 发起拨号,号码=" + phoneNumber);
|
||||||
|
_MainActivity.startActivity(intent);
|
||||||
|
} else {
|
||||||
|
LogUtils.e(TAG, "dialPhoneNumber: 拨号权限不足,无法发起拨号");
|
||||||
|
Toast.makeText(_MainActivity, "拨号权限不足", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为默认拨号应用(适配API30,硬编码版本判断)
|
||||||
|
*/
|
||||||
|
public boolean isDefaultPhoneCallApp() {
|
||||||
|
if (Build.VERSION.SDK_INT >= ANDROID_6_API) {
|
||||||
|
TelecomManager manager = (TelecomManager) getSystemService(Context.TELECOM_SERVICE);
|
||||||
|
if (manager != null && manager.getDefaultDialerPackage() != null) {
|
||||||
|
boolean isDefault = manager.getDefaultDialerPackage().equals(getPackageName());
|
||||||
|
LogUtils.d(TAG, "isDefaultPhoneCallApp: 是否为默认拨号应用=" + isDefault);
|
||||||
|
return isDefault;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LogUtils.d(TAG, "isDefaultPhoneCallApp: 系统版本低于Android 6,无法判断");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查服务是否正在运行(通用工具方法,添加空指针防护)
|
||||||
|
*/
|
||||||
|
public boolean isServiceRunning(Class<?> serviceClass) {
|
||||||
|
if (serviceClass == null) {
|
||||||
|
LogUtils.e(TAG, "isServiceRunning: 服务类参数为null");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
|
||||||
|
if (manager == null) {
|
||||||
|
LogUtils.w(TAG, "isServiceRunning: ActivityManager获取失败");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
|
||||||
|
if (serviceClass.getName().equals(service.service.getClassName())) {
|
||||||
|
LogUtils.d(TAG, "isServiceRunning: 服务[" + serviceClass.getSimpleName() + "]正在运行");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LogUtils.d(TAG, "isServiceRunning: 服务[" + serviceClass.getSimpleName() + "]未运行");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 13. 内部类定义区(Java 7 规范,禁止Lambda) ======================
|
||||||
|
/**
|
||||||
|
* 自定义懒加载ViewPager适配器(删除setPrimaryItem方法,解决首屏初始化冲突)
|
||||||
|
*/
|
||||||
|
private class LazyLoadPagerAdapter extends FragmentPagerAdapter {
|
||||||
|
private final List<Fragment> fragmentList;
|
||||||
|
private final List<String> tabTitleList;
|
||||||
|
|
||||||
|
public LazyLoadPagerAdapter(FragmentManager fm, List<Fragment> fragmentList, List<String> tabTitleList) {
|
||||||
|
super(fm);
|
||||||
|
this.fragmentList = fragmentList;
|
||||||
|
this.tabTitleList = tabTitleList;
|
||||||
|
LogUtils.d(MainActivity.TAG, "LazyLoadPagerAdapter: 初始化完成,Fragment数量=" + fragmentList.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Fragment getItem(int position) {
|
||||||
|
return fragmentList.get(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCount() {
|
||||||
|
return fragmentList.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CharSequence getPageTitle(int position) {
|
||||||
|
return tabTitleList.get(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 【已删除】移除setPrimaryItem方法,避免与手动初始化+onPageSelected回调冲突
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package cc.winboll.studio.contacts.activities;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
|
||||||
|
import cc.winboll.studio.contacts.R;
|
||||||
|
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||||
|
import cc.winboll.studio.libaes.models.APPInfo;
|
||||||
|
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||||
|
import cc.winboll.studio.libaes.views.AboutView;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||||
|
* @Date 2025/03/31 15:15:54
|
||||||
|
* @Describe 应用介绍窗口
|
||||||
|
*/
|
||||||
|
public class AboutActivity extends WinBollActivity implements IWinBoLLActivity {
|
||||||
|
|
||||||
|
// ====================== 常量定义区 ======================
|
||||||
|
public static final String TAG = "AboutActivity";
|
||||||
|
private static final String BRANCH_NAME = "contacts";
|
||||||
|
|
||||||
|
// ====================== 成员变量区 ======================
|
||||||
|
private Context mContext;
|
||||||
|
private Toolbar mToolbar;
|
||||||
|
|
||||||
|
// ====================== 接口实现区 ======================
|
||||||
|
@Override
|
||||||
|
public Activity getActivity() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTag() {
|
||||||
|
return TAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 生命周期函数区 ======================
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
LogUtils.d(TAG, "onCreate: 关于页面开始创建");
|
||||||
|
|
||||||
|
mContext = this;
|
||||||
|
setContentView(R.layout.activity_about);
|
||||||
|
|
||||||
|
// 初始化工具栏
|
||||||
|
initToolbar();
|
||||||
|
// 初始化关于页面视图
|
||||||
|
initAboutView();
|
||||||
|
// 注册Activity管理
|
||||||
|
WinBoLLActivityManager.getInstance().add(this);
|
||||||
|
|
||||||
|
LogUtils.d(TAG, "onCreate: 关于页面初始化完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
LogUtils.d(TAG, "onDestroy: 关于页面开始销毁");
|
||||||
|
WinBoLLActivityManager.getInstance().registeRemove(this);
|
||||||
|
LogUtils.d(TAG, "onDestroy: 关于页面销毁完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 控件初始化函数区 ======================
|
||||||
|
private void initToolbar() {
|
||||||
|
LogUtils.d(TAG, "initToolbar: 初始化工具栏");
|
||||||
|
// Java7 适配:添加强制类型转换
|
||||||
|
mToolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||||
|
setSupportActionBar(mToolbar);
|
||||||
|
mToolbar.setSubtitle(TAG);
|
||||||
|
// 非空判断,避免空指针异常
|
||||||
|
if (getSupportActionBar() != null) {
|
||||||
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initAboutView() {
|
||||||
|
LogUtils.d(TAG, "initAboutView: 初始化关于页面内容视图");
|
||||||
|
AboutView aboutView = createAboutView();
|
||||||
|
LinearLayout layout = (LinearLayout) findViewById(R.id.aboutviewroot_ll);
|
||||||
|
|
||||||
|
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
);
|
||||||
|
layout.addView(aboutView, params);
|
||||||
|
LogUtils.d(TAG, "initAboutView: AboutView已添加到布局");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 业务逻辑函数区 ======================
|
||||||
|
private AboutView createAboutView() {
|
||||||
|
LogUtils.d(TAG, "createAboutView: 构建APP信息并创建AboutView");
|
||||||
|
APPInfo appInfo = new APPInfo();
|
||||||
|
appInfo.setAppName("Contacts");
|
||||||
|
appInfo.setAppIcon(cc.winboll.studio.libaes.R.drawable.ic_winboll);
|
||||||
|
appInfo.setAppDescription("这是可以根据正则表达式匹配拦截骚扰电话的手机拨号应用。");
|
||||||
|
appInfo.setAppGitName("WinBoLL");
|
||||||
|
appInfo.setAppGitOwner("Studio");
|
||||||
|
appInfo.setAppGitAPPBranch(BRANCH_NAME);
|
||||||
|
appInfo.setAppGitAPPSubProjectFolder(BRANCH_NAME);
|
||||||
|
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=Contacts");
|
||||||
|
appInfo.setAppAPKName("Contacts");
|
||||||
|
appInfo.setAppAPKFolderName("Contacts");
|
||||||
|
|
||||||
|
return new AboutView(mContext, appInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
package cc.winboll.studio.contacts.activities;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.telephony.PhoneStateListener;
|
||||||
|
import android.telephony.TelephonyManager;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.core.app.ActivityCompat;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
import cc.winboll.studio.contacts.R;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||||
|
* @Date 2025/02/20 17:15:46
|
||||||
|
* @Describe 拨号窗口
|
||||||
|
*/
|
||||||
|
public class CallActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
// ====================== 常量定义区 ======================
|
||||||
|
public static final String TAG = "CallActivity";
|
||||||
|
private static final int REQUEST_CALL_PHONE = 1;
|
||||||
|
|
||||||
|
// ====================== UI控件区 ======================
|
||||||
|
private EditText phoneNumberEditText;
|
||||||
|
private TextView callStatusTextView;
|
||||||
|
private Button dialButton;
|
||||||
|
|
||||||
|
// ====================== 业务成员区 ======================
|
||||||
|
private TelephonyManager telephonyManager;
|
||||||
|
private MyPhoneStateListener phoneStateListener;
|
||||||
|
|
||||||
|
// ====================== 生命周期函数区 ======================
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
LogUtils.d(TAG, "onCreate: 拨号页面开始创建");
|
||||||
|
setContentView(R.layout.activity_call);
|
||||||
|
|
||||||
|
// 初始化控件
|
||||||
|
initViews();
|
||||||
|
// 初始化电话状态监听
|
||||||
|
initPhoneStateListener();
|
||||||
|
LogUtils.d(TAG, "onCreate: 拨号页面初始化完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
LogUtils.d(TAG, "onDestroy: 拨号页面开始销毁");
|
||||||
|
// 取消电话状态监听,避免内存泄漏
|
||||||
|
if (telephonyManager != null && phoneStateListener != null) {
|
||||||
|
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE);
|
||||||
|
LogUtils.d(TAG, "onDestroy: 电话状态监听已取消");
|
||||||
|
}
|
||||||
|
LogUtils.d(TAG, "onDestroy: 拨号页面销毁完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 权限回调函数区 ======================
|
||||||
|
@Override
|
||||||
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
|
LogUtils.d(TAG, "onRequestPermissionsResult: 权限请求回调,requestCode=" + requestCode);
|
||||||
|
if (requestCode == REQUEST_CALL_PHONE) {
|
||||||
|
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
LogUtils.d(TAG, "onRequestPermissionsResult: 拨打电话权限授予成功");
|
||||||
|
String phoneNumber = phoneNumberEditText.getText().toString().trim();
|
||||||
|
dialPhoneNumber(phoneNumber);
|
||||||
|
} else {
|
||||||
|
LogUtils.w(TAG, "onRequestPermissionsResult: 拨打电话权限被拒绝");
|
||||||
|
Toast.makeText(this, "未授予拨打电话权限", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 控件初始化函数区 ======================
|
||||||
|
private void initViews() {
|
||||||
|
LogUtils.d(TAG, "initViews: 初始化UI控件");
|
||||||
|
// Java7 适配:添加强制类型转换
|
||||||
|
phoneNumberEditText = (EditText) findViewById(R.id.phone_number);
|
||||||
|
dialButton = (Button) findViewById(R.id.dial_button);
|
||||||
|
callStatusTextView = (TextView) findViewById(R.id.call_status);
|
||||||
|
|
||||||
|
// 设置拨号按钮点击事件
|
||||||
|
dialButton.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
String phoneNumber = phoneNumberEditText.getText().toString().trim();
|
||||||
|
LogUtils.d(TAG, "initViews: 拨号按钮点击,号码=" + phoneNumber);
|
||||||
|
if (phoneNumber.isEmpty()) {
|
||||||
|
Toast.makeText(CallActivity.this, "请输入电话号码", Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 权限检查
|
||||||
|
if (ContextCompat.checkSelfPermission(CallActivity.this, Manifest.permission.CALL_PHONE)
|
||||||
|
!= PackageManager.PERMISSION_GRANTED) {
|
||||||
|
LogUtils.w(TAG, "initViews: 拨打电话权限未授予,发起权限申请");
|
||||||
|
ActivityCompat.requestPermissions(CallActivity.this,
|
||||||
|
new String[]{Manifest.permission.CALL_PHONE},
|
||||||
|
REQUEST_CALL_PHONE);
|
||||||
|
} else {
|
||||||
|
dialPhoneNumber(phoneNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 电话状态监听初始化函数区 ======================
|
||||||
|
private void initPhoneStateListener() {
|
||||||
|
LogUtils.d(TAG, "initPhoneStateListener: 初始化电话状态监听");
|
||||||
|
telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
|
||||||
|
phoneStateListener = new MyPhoneStateListener();
|
||||||
|
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 核心业务函数区 ======================
|
||||||
|
private void dialPhoneNumber(String phoneNumber) {
|
||||||
|
LogUtils.d(TAG, "dialPhoneNumber: 发起拨号,号码=" + phoneNumber);
|
||||||
|
Intent intent = new Intent(Intent.ACTION_CALL);
|
||||||
|
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
|
||||||
|
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
LogUtils.e(TAG, "dialPhoneNumber: 拨打电话权限缺失,拨号失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 内部电话状态监听类 ======================
|
||||||
|
private class MyPhoneStateListener extends PhoneStateListener {
|
||||||
|
@Override
|
||||||
|
public void onCallStateChanged(int state, String incomingNumber) {
|
||||||
|
super.onCallStateChanged(state, incomingNumber);
|
||||||
|
switch (state) {
|
||||||
|
case TelephonyManager.CALL_STATE_IDLE:
|
||||||
|
callStatusTextView.setText("电话已挂断");
|
||||||
|
LogUtils.d(TAG, "MyPhoneStateListener: 通话状态-挂断");
|
||||||
|
break;
|
||||||
|
case TelephonyManager.CALL_STATE_OFFHOOK:
|
||||||
|
callStatusTextView.setText("正在通话中");
|
||||||
|
LogUtils.d(TAG, "MyPhoneStateListener: 通话状态-通话中");
|
||||||
|
break;
|
||||||
|
case TelephonyManager.CALL_STATE_RINGING:
|
||||||
|
callStatusTextView.setText("来电: " + incomingNumber);
|
||||||
|
LogUtils.d(TAG, "MyPhoneStateListener: 通话状态-来电,号码=" + incomingNumber);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package cc.winboll.studio.contacts.activities;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import cc.winboll.studio.contacts.R;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||||
|
* @Date 2025/02/20 20:18:26
|
||||||
|
* @Describe 拨号盘窗口(跳转到系统拨号界面)
|
||||||
|
*/
|
||||||
|
public class DialerActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
// ====================== 常量定义区 ======================
|
||||||
|
public static final String TAG = "DialerActivity";
|
||||||
|
|
||||||
|
// ====================== UI控件区 ======================
|
||||||
|
private EditText phoneNumberEditText;
|
||||||
|
private Button dialButton;
|
||||||
|
|
||||||
|
// ====================== 生命周期函数区 ======================
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
LogUtils.d(TAG, "onCreate: 拨号盘页面开始创建");
|
||||||
|
setContentView(R.layout.activity_dialer);
|
||||||
|
|
||||||
|
// 初始化UI控件与点击事件
|
||||||
|
initViews();
|
||||||
|
LogUtils.d(TAG, "onCreate: 拨号盘页面初始化完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
LogUtils.d(TAG, "onDestroy: 拨号盘页面已销毁");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 控件初始化函数区 ======================
|
||||||
|
private void initViews() {
|
||||||
|
LogUtils.d(TAG, "initViews: 初始化UI控件");
|
||||||
|
// Java7 适配:添加强制类型转换
|
||||||
|
phoneNumberEditText = (EditText) findViewById(R.id.phone_number_edit_text);
|
||||||
|
dialButton = (Button) findViewById(R.id.dial_button);
|
||||||
|
|
||||||
|
// 设置拨号按钮点击事件
|
||||||
|
dialButton.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
String phoneNumber = phoneNumberEditText.getText().toString().trim();
|
||||||
|
LogUtils.d(TAG, "initViews: 拨号按钮点击,输入号码=" + phoneNumber);
|
||||||
|
|
||||||
|
// 空号码校验
|
||||||
|
if (phoneNumber.isEmpty()) {
|
||||||
|
LogUtils.w(TAG, "initViews: 拨号失败,号码为空");
|
||||||
|
Toast.makeText(DialerActivity.this, "请输入有效电话号码", Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转到系统拨号界面
|
||||||
|
Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + phoneNumber));
|
||||||
|
if (intent.resolveActivity(getPackageManager()) != null) {
|
||||||
|
startActivity(intent);
|
||||||
|
LogUtils.d(TAG, "initViews: 成功跳转到系统拨号界面");
|
||||||
|
} else {
|
||||||
|
LogUtils.e(TAG, "initViews: 跳转失败,无可用拨号应用");
|
||||||
|
Toast.makeText(DialerActivity.this, "未找到可用拨号应用", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,613 @@
|
|||||||
|
package cc.winboll.studio.contacts.activities;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.media.AudioManager;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.provider.Settings;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.SeekBar;
|
||||||
|
import android.widget.Switch;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import cc.winboll.studio.contacts.R;
|
||||||
|
import cc.winboll.studio.contacts.adapters.PhoneConnectRuleAdapter;
|
||||||
|
import cc.winboll.studio.contacts.bobulltoon.TomCat;
|
||||||
|
import cc.winboll.studio.contacts.dun.Rules;
|
||||||
|
import cc.winboll.studio.contacts.model.MainServiceBean;
|
||||||
|
import cc.winboll.studio.contacts.model.PhoneConnectRuleBean;
|
||||||
|
import cc.winboll.studio.contacts.model.RingTongBean;
|
||||||
|
import cc.winboll.studio.contacts.model.SettingsBean;
|
||||||
|
import cc.winboll.studio.contacts.services.MainService;
|
||||||
|
import cc.winboll.studio.contacts.views.DuInfoTextView;
|
||||||
|
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||||
|
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
import cc.winboll.studio.libappbase.ToastUtils;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||||
|
* @Date 2025/02/21 05:37:42
|
||||||
|
* @Describe Contacts 设置页面(完全适配 API 30 + Java 7 语法)
|
||||||
|
* 核心优化:1. 移除高版本API依赖 2. Java7规范写法 3. 强化内存泄漏防护 4. 版本判断硬编码 5. LogUtils统一日志管理
|
||||||
|
*/
|
||||||
|
public class SettingsActivity extends WinBollActivity implements IWinBoLLActivity {
|
||||||
|
|
||||||
|
// ====================== 常量定义区(置顶,统一管理) ======================
|
||||||
|
public static final String TAG = "SettingsActivity";
|
||||||
|
// API版本硬编码(替代Build.VERSION_CODES,适配Java7)
|
||||||
|
private static final int ANDROID_6_API = 23;
|
||||||
|
|
||||||
|
// ====================== 静态成员属性区 ======================
|
||||||
|
private static DuInfoTextView sDuInfoTextView; // 规范命名:静态属性加s前缀
|
||||||
|
|
||||||
|
// ====================== 数据业务属性区 ======================
|
||||||
|
private int mStreamMaxVolume; // 铃音最大音量
|
||||||
|
private int mStreamVolume; // 当前铃音音量
|
||||||
|
private List<PhoneConnectRuleBean> mRuleList; // 通话规则列表
|
||||||
|
private PhoneConnectRuleAdapter mRuleAdapter; // 规则列表适配器
|
||||||
|
|
||||||
|
// ====================== UI控件属性区(统一归类,规范命名) ======================
|
||||||
|
private Toolbar mToolbar; // 顶部工具栏
|
||||||
|
private Switch mSwMainService; // 主服务开关
|
||||||
|
private SeekBar mSbVolume; // 音量调节条
|
||||||
|
private TextView mTvVolume; // 音量显示文本
|
||||||
|
private Switch mSwEnableDun; // 云盾功能开关
|
||||||
|
private EditText mEtDunTotalCount; // 云盾总次数输入框
|
||||||
|
private EditText mEtDunResumeSecondCount; // 云盾恢复秒数输入框
|
||||||
|
private EditText mEtDunResumeCount; // 云盾恢复次数输入框
|
||||||
|
private RecyclerView mRvRuleList; // 规则列表RecyclerView
|
||||||
|
private EditText mEtBoBullToonUrl; // BoBullToon地址输入框
|
||||||
|
private EditText mEtSearchPhone; // 号码查询输入框
|
||||||
|
|
||||||
|
// ====================== 接口实现区(IWinBoLLActivity规范实现) ======================
|
||||||
|
@Override
|
||||||
|
public AppCompatActivity getActivity() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTag() {
|
||||||
|
return TAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 生命周期函数区(按执行顺序排列) ======================
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
LogUtils.d(TAG, "onCreate: 设置页面启动");
|
||||||
|
setContentView(R.layout.activity_settings);
|
||||||
|
|
||||||
|
// 初始化核心流程(按优先级执行)
|
||||||
|
initToolbar(); // 工具栏初始化(优先)
|
||||||
|
initMainServiceSwitch();// 主服务开关初始化
|
||||||
|
initVolumeControl(); // 音量控制初始化
|
||||||
|
initRuleRecyclerView(); // 规则列表初始化
|
||||||
|
initDunSettings(); // 云盾设置初始化
|
||||||
|
initBoBullToonViews(); // BoBullToon功能初始化
|
||||||
|
|
||||||
|
LogUtils.d(TAG, "onCreate: 设置页面初始化完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
LogUtils.d(TAG, "onDestroy: 设置页面销毁");
|
||||||
|
// 内存泄漏防护:清空所有引用(静态+成员+UI)
|
||||||
|
sDuInfoTextView = null;
|
||||||
|
mRuleList = null;
|
||||||
|
mRuleAdapter = null;
|
||||||
|
mToolbar = null;
|
||||||
|
mSwMainService = null;
|
||||||
|
mSbVolume = null;
|
||||||
|
mTvVolume = null;
|
||||||
|
mSwEnableDun = null;
|
||||||
|
mEtDunTotalCount = null;
|
||||||
|
mEtDunResumeSecondCount = null;
|
||||||
|
mEtDunResumeCount = null;
|
||||||
|
mRvRuleList = null;
|
||||||
|
mEtBoBullToonUrl = null;
|
||||||
|
mEtSearchPhone = null;
|
||||||
|
LogUtils.d(TAG, "onDestroy: 设置页面资源清理完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 初始化函数区(按功能模块归类) ======================
|
||||||
|
/**
|
||||||
|
* 初始化顶部工具栏(后退按钮+标题)
|
||||||
|
*/
|
||||||
|
private void initToolbar() {
|
||||||
|
LogUtils.d(TAG, "initToolbar: 初始化工具栏");
|
||||||
|
mToolbar = (Toolbar) findViewById(R.id.activitymainToolbar1);
|
||||||
|
setSupportActionBar(mToolbar);
|
||||||
|
|
||||||
|
// 显示后退按钮(空指针防护)
|
||||||
|
if (getSupportActionBar() != null) {
|
||||||
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
|
getSupportActionBar().setSubtitle(TAG);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后退按钮点击事件(Java7匿名内部类)
|
||||||
|
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
LogUtils.d(TAG, "initToolbar: 点击后退按钮,关闭页面");
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化主服务开关(联动MainService启停)
|
||||||
|
*/
|
||||||
|
private void initMainServiceSwitch() {
|
||||||
|
LogUtils.d(TAG, "initMainServiceSwitch: 初始化主服务开关");
|
||||||
|
mSwMainService = (Switch) findViewById(R.id.sw_mainservice);
|
||||||
|
MainServiceBean serviceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
||||||
|
|
||||||
|
// 加载开关状态(空指针防护)
|
||||||
|
boolean isServiceEnable = serviceBean != null && serviceBean.isEnable();
|
||||||
|
mSwMainService.setChecked(isServiceEnable);
|
||||||
|
LogUtils.d(TAG, "initMainServiceSwitch: 主服务当前状态:" + (isServiceEnable ? "启用" : "禁用"));
|
||||||
|
|
||||||
|
// 开关点击事件
|
||||||
|
mSwMainService.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
boolean isChecked = mSwMainService.isChecked();
|
||||||
|
LogUtils.d(TAG, "initMainServiceSwitch: 主服务开关切换:" + (isChecked ? "启用" : "禁用"));
|
||||||
|
if (isChecked) {
|
||||||
|
MainService.startMainServiceAndSaveStatus(SettingsActivity.this);
|
||||||
|
} else {
|
||||||
|
MainService.stopMainServiceAndSaveStatus(SettingsActivity.this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化音量控制(SeekBar+音量显示+配置保存)
|
||||||
|
*/
|
||||||
|
private void initVolumeControl() {
|
||||||
|
LogUtils.d(TAG, "initVolumeControl: 初始化音量控制");
|
||||||
|
mSbVolume = (SeekBar) findViewById(R.id.bellvolume);
|
||||||
|
mTvVolume = (TextView) findViewById(R.id.tv_volume);
|
||||||
|
final AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
|
||||||
|
|
||||||
|
// 空指针防护:AudioManager获取失败直接返回
|
||||||
|
if (audioManager == null) {
|
||||||
|
LogUtils.e(TAG, "initVolumeControl: AudioManager获取失败,音量控制初始化失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化音量参数
|
||||||
|
mStreamMaxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_RING);
|
||||||
|
mStreamVolume = audioManager.getStreamVolume(AudioManager.STREAM_RING);
|
||||||
|
mSbVolume.setMax(mStreamMaxVolume);
|
||||||
|
mSbVolume.setProgress(mStreamVolume);
|
||||||
|
updateVolumeDisplay(); // 更新音量文本显示
|
||||||
|
|
||||||
|
// 音量调节监听
|
||||||
|
mSbVolume.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||||
|
@Override
|
||||||
|
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||||
|
if (fromUser) {
|
||||||
|
LogUtils.d(TAG, "initVolumeControl: 音量调节至:" + progress + "/" + mStreamMaxVolume);
|
||||||
|
// 实时更新系统音量+保存配置
|
||||||
|
audioManager.setStreamVolume(AudioManager.STREAM_RING, progress, 0);
|
||||||
|
RingTongBean ringBean = RingTongBean.loadBean(SettingsActivity.this, RingTongBean.class);
|
||||||
|
if (ringBean == null) {
|
||||||
|
ringBean = new RingTongBean();
|
||||||
|
}
|
||||||
|
ringBean.setStreamVolume(progress);
|
||||||
|
RingTongBean.saveBean(SettingsActivity.this, ringBean);
|
||||||
|
mStreamVolume = progress;
|
||||||
|
updateVolumeDisplay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStartTrackingTouch(SeekBar seekBar) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStopTrackingTouch(SeekBar seekBar) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化通话规则列表(加载黑白名单规则)
|
||||||
|
*/
|
||||||
|
private void initRuleRecyclerView() {
|
||||||
|
LogUtils.d(TAG, "initRuleRecyclerView: 初始化规则列表");
|
||||||
|
mRvRuleList = (RecyclerView) findViewById(R.id.recycler_view);
|
||||||
|
mRvRuleList.setLayoutManager(new LinearLayoutManager(this));
|
||||||
|
|
||||||
|
// 加载规则数据
|
||||||
|
Rules rules = Rules.getInstance(this);
|
||||||
|
if (rules == null) {
|
||||||
|
LogUtils.e(TAG, "initRuleRecyclerView: Rules实例获取失败,列表初始化失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mRuleList = rules.getPhoneBlacRuleBeanList();
|
||||||
|
mRuleAdapter = new PhoneConnectRuleAdapter(this, mRuleList);
|
||||||
|
mRvRuleList.setAdapter(mRuleAdapter);
|
||||||
|
LogUtils.d(TAG, "initRuleRecyclerView: 规则列表加载完成,共" + mRuleList.size() + "条规则");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化云盾设置(参数加载+开关联动)
|
||||||
|
*/
|
||||||
|
private void initDunSettings() {
|
||||||
|
LogUtils.d(TAG, "initDunSettings: 初始化云盾设置");
|
||||||
|
sDuInfoTextView = (DuInfoTextView) findViewById(R.id.tv_DunInfo);
|
||||||
|
mSwEnableDun = (Switch) findViewById(R.id.sw_IsEnableDun);
|
||||||
|
mEtDunTotalCount = (EditText) findViewById(R.id.et_DunTotalCount);
|
||||||
|
mEtDunResumeSecondCount = (EditText) findViewById(R.id.et_DunResumeSecondCount);
|
||||||
|
mEtDunResumeCount = (EditText) findViewById(R.id.et_DunResumeCount);
|
||||||
|
|
||||||
|
// 加载云盾配置
|
||||||
|
Rules rules = Rules.getInstance(this);
|
||||||
|
if (rules == null) {
|
||||||
|
LogUtils.e(TAG, "initDunSettings: Rules实例获取失败,云盾初始化失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SettingsBean dunSettings = rules.getSettingsModel();
|
||||||
|
if (dunSettings == null) {
|
||||||
|
LogUtils.e(TAG, "initDunSettings: 云盾配置获取失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填充配置参数
|
||||||
|
mEtDunTotalCount.setText(String.valueOf(dunSettings.getDunTotalCount()));
|
||||||
|
mEtDunResumeSecondCount.setText(String.valueOf(dunSettings.getDunResumeSecondCount()));
|
||||||
|
mEtDunResumeCount.setText(String.valueOf(dunSettings.getDunResumeCount()));
|
||||||
|
mSwEnableDun.setChecked(dunSettings.isEnableDun());
|
||||||
|
|
||||||
|
// 开关联动:启用云盾时禁用参数编辑
|
||||||
|
boolean isDunEnable = dunSettings.isEnableDun();
|
||||||
|
mEtDunTotalCount.setEnabled(!isDunEnable);
|
||||||
|
mEtDunResumeSecondCount.setEnabled(!isDunEnable);
|
||||||
|
mEtDunResumeCount.setEnabled(!isDunEnable);
|
||||||
|
LogUtils.d(TAG, "initDunSettings: 云盾当前状态:" + (isDunEnable ? "启用" : "禁用"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化BoBullToon功能(地址配置+号码查询)
|
||||||
|
*/
|
||||||
|
private void initBoBullToonViews() {
|
||||||
|
LogUtils.d(TAG, "initBoBullToonViews: 初始化BoBullToon功能");
|
||||||
|
mEtBoBullToonUrl = (EditText) findViewById(R.id.bobulltoonurl_et);
|
||||||
|
mEtSearchPhone = (EditText) findViewById(R.id.activitysettingsEditText1);
|
||||||
|
|
||||||
|
// 加载保存的地址
|
||||||
|
Rules rules = Rules.getInstance(this);
|
||||||
|
if (rules != null) {
|
||||||
|
mEtBoBullToonUrl.setText(rules.getBoBullToonURL());
|
||||||
|
LogUtils.d(TAG, "initBoBullToonViews: 加载BoBullToon地址完成");
|
||||||
|
} else {
|
||||||
|
LogUtils.e(TAG, "initBoBullToonViews: Rules实例获取失败,地址加载失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 点击事件回调区(按功能模块归类) ======================
|
||||||
|
/**
|
||||||
|
* 云盾开关点击事件(联动参数编辑权限+配置保存)
|
||||||
|
*/
|
||||||
|
public void onSW_IsEnableDun(View view) {
|
||||||
|
boolean isChecked = mSwEnableDun.isChecked();
|
||||||
|
LogUtils.d(TAG, "onSW_IsEnableDun: 云盾开关切换:" + (isChecked ? "启用" : "禁用"));
|
||||||
|
|
||||||
|
// 联动参数编辑权限
|
||||||
|
mEtDunTotalCount.setEnabled(!isChecked);
|
||||||
|
mEtDunResumeSecondCount.setEnabled(!isChecked);
|
||||||
|
mEtDunResumeCount.setEnabled(!isChecked);
|
||||||
|
|
||||||
|
// 保存配置
|
||||||
|
Rules rules = Rules.getInstance(this);
|
||||||
|
if (rules == null) {
|
||||||
|
LogUtils.e(TAG, "onSW_IsEnableDun: Rules实例获取失败,配置保存失败");
|
||||||
|
mSwEnableDun.setChecked(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SettingsBean dunSettings = rules.getSettingsModel();
|
||||||
|
if (dunSettings == null) {
|
||||||
|
LogUtils.e(TAG, "onSW_IsEnableDun: 云盾配置获取失败,保存失败");
|
||||||
|
mSwEnableDun.setChecked(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启用云盾时校验参数合法性
|
||||||
|
if (isChecked) {
|
||||||
|
try {
|
||||||
|
String totalCountStr = mEtDunTotalCount.getText().toString().trim();
|
||||||
|
String resumeSecStr = mEtDunResumeSecondCount.getText().toString().trim();
|
||||||
|
String resumeCountStr = mEtDunResumeCount.getText().toString().trim();
|
||||||
|
|
||||||
|
// 空参数校验
|
||||||
|
if (totalCountStr.isEmpty() || resumeSecStr.isEmpty() || resumeCountStr.isEmpty()) {
|
||||||
|
throw new NumberFormatException("参数不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换参数并保存
|
||||||
|
int totalCount = Integer.parseInt(totalCountStr);
|
||||||
|
int resumeSec = Integer.parseInt(resumeSecStr);
|
||||||
|
int resumeCount = Integer.parseInt(resumeCountStr);
|
||||||
|
dunSettings.setDunTotalCount(totalCount);
|
||||||
|
dunSettings.setDunResumeSecondCount(resumeSec);
|
||||||
|
dunSettings.setDunResumeCount(resumeCount);
|
||||||
|
LogUtils.d(TAG, "onSW_IsEnableDun: 云盾参数保存完成,总次数:" + totalCount + ",恢复秒数:" + resumeSec);
|
||||||
|
|
||||||
|
// 提示信息
|
||||||
|
String toastMsg = totalCount == 1 ? "电话骚扰防御力几乎为0" : "连拨" + totalCount + "次后接通电话";
|
||||||
|
ToastUtils.show(toastMsg);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
LogUtils.e(TAG, "onSW_IsEnableDun: 云盾参数格式错误", e);
|
||||||
|
ToastUtils.show("参数格式错误,请输入整数");
|
||||||
|
mSwEnableDun.setChecked(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存开关状态并刷新配置
|
||||||
|
dunSettings.setIsEnableDun(isChecked);
|
||||||
|
rules.saveDun();
|
||||||
|
rules.reload();
|
||||||
|
LogUtils.d(TAG, "onSW_IsEnableDun: 云盾配置保存完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加新通话规则(黑白名单)
|
||||||
|
*/
|
||||||
|
public void onAddNewConnectionRule(View view) {
|
||||||
|
LogUtils.d(TAG, "onAddNewConnectionRule: 添加新通话规则");
|
||||||
|
Rules rules = Rules.getInstance(this);
|
||||||
|
if (rules == null) {
|
||||||
|
LogUtils.e(TAG, "onAddNewConnectionRule: Rules实例获取失败,添加失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mRuleList.add(new PhoneConnectRuleBean());
|
||||||
|
rules.saveRules();
|
||||||
|
mRuleAdapter.notifyDataSetChanged();
|
||||||
|
LogUtils.d(TAG, "onAddNewConnectionRule: 规则添加完成,当前共" + mRuleList.size() + "条规则");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转默认电话应用设置
|
||||||
|
*/
|
||||||
|
public void onDefaultPhone(View view) {
|
||||||
|
LogUtils.d(TAG, "onDefaultPhone: 跳转默认电话应用设置");
|
||||||
|
startActivity(new Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 悬浮窗权限检查与请求
|
||||||
|
*/
|
||||||
|
public void onCanDrawOverlays(View view) {
|
||||||
|
LogUtils.d(TAG, "onCanDrawOverlays: 检查悬浮窗权限");
|
||||||
|
// API6.0+校验权限
|
||||||
|
if (Build.VERSION.SDK_INT >= ANDROID_6_API && !Settings.canDrawOverlays(this)) {
|
||||||
|
LogUtils.d(TAG, "onCanDrawOverlays: 未开启悬浮窗权限,发起请求");
|
||||||
|
showDrawOverlayRequestDialog();
|
||||||
|
} else {
|
||||||
|
ToastUtils.show("悬浮窗权限已开启");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理BoBullToon本地数据
|
||||||
|
*/
|
||||||
|
public void onCleanBoBullToonData(View view) {
|
||||||
|
LogUtils.d(TAG, "onCleanBoBullToonData: 清理BoBullToon数据");
|
||||||
|
TomCat tomCat = TomCat.getInstance(this);
|
||||||
|
if (tomCat != null) {
|
||||||
|
tomCat.cleanBoBullToon();
|
||||||
|
ToastUtils.show("BoBullToon数据已清理");
|
||||||
|
LogUtils.d(TAG, "onCleanBoBullToonData: 数据清理完成");
|
||||||
|
} else {
|
||||||
|
LogUtils.e(TAG, "onCleanBoBullToonData: TomCat实例获取失败,清理失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置BoBullToon默认地址
|
||||||
|
*/
|
||||||
|
public void onResetBoBullToonURL(View view) {
|
||||||
|
LogUtils.d(TAG, "onResetBoBullToonURL: 重置BoBullToon地址");
|
||||||
|
Rules rules = Rules.getInstance(this);
|
||||||
|
if (rules == null) {
|
||||||
|
LogUtils.e(TAG, "onResetBoBullToonURL: Rules实例获取失败,重置失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rules.resetDefaultBoBullToonURL();
|
||||||
|
mEtBoBullToonUrl.setText(rules.getBoBullToonURL());
|
||||||
|
ToastUtils.show("BoBullToon地址已重置为默认");
|
||||||
|
LogUtils.d(TAG, "onResetBoBullToonURL: 地址重置完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载BoBullToon数据(子线程执行,避免阻塞UI)
|
||||||
|
*/
|
||||||
|
public void onDownloadBoBullToon(View view) {
|
||||||
|
LogUtils.d(TAG, "onDownloadBoBullToon: 开始下载BoBullToon数据");
|
||||||
|
Rules rules = Rules.getInstance(this);
|
||||||
|
if (rules == null) {
|
||||||
|
LogUtils.e(TAG, "onDownloadBoBullToon: Rules实例获取失败,下载失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验并更新地址
|
||||||
|
String inputUrl = mEtBoBullToonUrl.getText().toString().trim();
|
||||||
|
String savedUrl = rules.getBoBullToonURL();
|
||||||
|
if (!inputUrl.equals(savedUrl)) {
|
||||||
|
rules.setBoBullToonURL(inputUrl);
|
||||||
|
LogUtils.d(TAG, "onDownloadBoBullToon: BoBullToon地址更新为:" + inputUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 子线程下载(Java7匿名内部类)
|
||||||
|
final TomCat tomCat = TomCat.getInstance(this);
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
boolean downloadSuccess = tomCat != null && tomCat.downloadBoBullToon();
|
||||||
|
if (downloadSuccess) {
|
||||||
|
LogUtils.d(TAG, "onDownloadBoBullToon: 数据下载成功");
|
||||||
|
// 主线程更新UI
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
ToastUtils.show("BoBullToon下载成功");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 重启主服务+刷新配置
|
||||||
|
MainService.restartMainService(SettingsActivity.this);
|
||||||
|
Rules.getInstance(SettingsActivity.this).reload();
|
||||||
|
} else {
|
||||||
|
LogUtils.e(TAG, "onDownloadBoBullToon: 数据下载失败");
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
ToastUtils.show("BoBullToon下载失败");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询号码是否为BoBullToon号码
|
||||||
|
*/
|
||||||
|
public void onSearchBoBullToonPhone(View view) {
|
||||||
|
LogUtils.d(TAG, "onSearchBoBullToonPhone: 执行号码查询");
|
||||||
|
String phone = mEtSearchPhone.getText().toString().trim();
|
||||||
|
// 空号码校验
|
||||||
|
if (phone.isEmpty()) {
|
||||||
|
LogUtils.w(TAG, "onSearchBoBullToonPhone: 查询号码为空,取消查询");
|
||||||
|
ToastUtils.show("请输入查询号码");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行查询
|
||||||
|
TomCat tomCat = TomCat.getInstance(this);
|
||||||
|
if (tomCat == null || !tomCat.loadPhoneBoBullToon()) {
|
||||||
|
LogUtils.w(TAG, "onSearchBoBullToonPhone: BoBullToon数据未加载,查询失败");
|
||||||
|
ToastUtils.show("请先下载BoBullToon数据");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isBoBullToon = tomCat.isPhoneBoBullToon(phone);
|
||||||
|
String resultMsg = isBoBullToon ? "是BoBullToon号码" : "非BoBullToon号码";
|
||||||
|
ToastUtils.show(resultMsg);
|
||||||
|
LogUtils.d(TAG, "onSearchBoBullToonPhone: 号码" + phone + "查询结果:" + resultMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转单元测试页面
|
||||||
|
*/
|
||||||
|
public void onUnitTest(View view) {
|
||||||
|
LogUtils.d(TAG, "onUnitTest: 跳转单元测试页面");
|
||||||
|
startActivity(new Intent(this, UnitTestActivity.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转关于页面
|
||||||
|
*/
|
||||||
|
public void onAbout(View view) {
|
||||||
|
LogUtils.d(TAG, "onAbout: 跳转关于页面");
|
||||||
|
WinBoLLActivityManager.getInstance().startWinBoLLActivity(this, AboutActivity.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转日志查看页面
|
||||||
|
*/
|
||||||
|
public void onLogView(View view) {
|
||||||
|
LogUtils.d(TAG, "onLogView: 跳转日志页面");
|
||||||
|
WinBoLLActivityManager.getInstance().startLogActivity(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 工具方法区(通用功能+权限相关) ======================
|
||||||
|
/**
|
||||||
|
* 更新音量显示文本(当前音量/最大音量)
|
||||||
|
*/
|
||||||
|
private void updateVolumeDisplay() {
|
||||||
|
mTvVolume.setText(mStreamVolume + "/" + mStreamMaxVolume);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示悬浮窗权限请求对话框
|
||||||
|
*/
|
||||||
|
private void showDrawOverlayRequestDialog() {
|
||||||
|
AlertDialog dialog = new AlertDialog.Builder(this)
|
||||||
|
.setTitle("权限请求")
|
||||||
|
.setMessage("为保证通话监听功能正常,需开启悬浮窗权限")
|
||||||
|
.setPositiveButton("去设置", new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
dialog.dismiss();
|
||||||
|
jumpToDrawOverlaySettings();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.setNegativeButton("稍后", new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
dialog.dismiss();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.create();
|
||||||
|
|
||||||
|
// 解决对话框焦点问题
|
||||||
|
if (dialog.getWindow() != null) {
|
||||||
|
dialog.getWindow().setFlags(
|
||||||
|
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||||
|
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
|
||||||
|
}
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转悬浮窗权限设置页面(反射适配低版本)
|
||||||
|
*/
|
||||||
|
private void jumpToDrawOverlaySettings() {
|
||||||
|
LogUtils.d(TAG, "jumpToDrawOverlaySettings: 跳转悬浮窗权限设置");
|
||||||
|
try {
|
||||||
|
// 反射获取设置页面Action(避免高版本API依赖)
|
||||||
|
Class<?> settingsClazz = Settings.class;
|
||||||
|
Field actionField = settingsClazz.getDeclaredField("ACTION_MANAGE_OVERLAY_PERMISSION");
|
||||||
|
String action = (String) actionField.get(null);
|
||||||
|
|
||||||
|
// 跳转当前应用权限设置页
|
||||||
|
Intent intent = new Intent(action);
|
||||||
|
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
intent.setData(Uri.parse("package:" + getPackageName()));
|
||||||
|
startActivity(intent);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.e(TAG, "jumpToDrawOverlaySettings: 跳转权限设置失败", e);
|
||||||
|
Toast.makeText(this, "请手动在设置中开启悬浮窗权限", Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 静态通知方法区(云盾信息更新) ======================
|
||||||
|
/**
|
||||||
|
* 通知云盾信息刷新(外部调用)
|
||||||
|
*/
|
||||||
|
public static void notifyDunInfoUpdate() {
|
||||||
|
if (sDuInfoTextView != null) {
|
||||||
|
LogUtils.d(TAG, "notifyDunInfoUpdate: 刷新云盾信息显示");
|
||||||
|
sDuInfoTextView.notifyInfoUpdate();
|
||||||
|
} else {
|
||||||
|
LogUtils.w(TAG, "notifyDunInfoUpdate: 云盾信息控件未初始化,刷新失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
package cc.winboll.studio.contacts.activities;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import cc.winboll.studio.contacts.R;
|
||||||
|
import cc.winboll.studio.contacts.activities.UnitTestActivity;
|
||||||
|
import cc.winboll.studio.contacts.dun.Rules;
|
||||||
|
import cc.winboll.studio.contacts.services.LimitedTimeSpecialChannelService;
|
||||||
|
import cc.winboll.studio.contacts.utils.IntUtils;
|
||||||
|
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
import cc.winboll.studio.libappbase.LogView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||||
|
* @Date 2025/03/02 16:07:04
|
||||||
|
* @Describe 规则单元测试页面
|
||||||
|
*/
|
||||||
|
public class UnitTestActivity extends WinBollActivity implements IWinBoLLActivity {
|
||||||
|
|
||||||
|
// ====================== 常量定义区 ======================
|
||||||
|
public static final String TAG = "UnitTestActivity";
|
||||||
|
|
||||||
|
// ====================== UI控件区 ======================
|
||||||
|
private LogView logView;
|
||||||
|
private EditText etPhone;
|
||||||
|
|
||||||
|
// ====================== 接口实现区 ======================
|
||||||
|
@Override
|
||||||
|
public AppCompatActivity getActivity() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTag() {
|
||||||
|
return TAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 生命周期函数区 ======================
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
LogUtils.d(TAG, "onCreate: 单元测试页面开始创建");
|
||||||
|
setContentView(R.layout.activity_unittest);
|
||||||
|
|
||||||
|
// 初始化控件
|
||||||
|
initViews();
|
||||||
|
LogUtils.d(TAG, "onCreate: 单元测试页面初始化完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
LogUtils.d(TAG, "onDestroy: 单元测试页面开始销毁");
|
||||||
|
if (logView != null) {
|
||||||
|
// 若LogView有停止方法,建议调用避免资源泄漏
|
||||||
|
// logView.stop();
|
||||||
|
LogUtils.d(TAG, "onDestroy: LogView资源已处理");
|
||||||
|
}
|
||||||
|
LogUtils.d(TAG, "onDestroy: 单元测试页面销毁完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 控件初始化函数区 ======================
|
||||||
|
private void initViews() {
|
||||||
|
LogUtils.d(TAG, "initViews: 初始化UI控件");
|
||||||
|
// Java7 适配:添加强制类型转换
|
||||||
|
logView = (LogView) findViewById(R.id.logview);
|
||||||
|
etPhone = (EditText) findViewById(R.id.phone_et);
|
||||||
|
|
||||||
|
// 启动日志视图
|
||||||
|
logView.start();
|
||||||
|
LogUtils.d(TAG, "initViews: LogView已启动");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 点击事件测试函数区 ======================
|
||||||
|
/**
|
||||||
|
* 测试单个号码匹配规则
|
||||||
|
*/
|
||||||
|
public void onTestPhone(View view) {
|
||||||
|
LogUtils.d(TAG, "onTestPhone: 开始测试单个号码规则匹配");
|
||||||
|
String phone = etPhone.getText().toString().trim();
|
||||||
|
if (phone.isEmpty()) {
|
||||||
|
LogUtils.w(TAG, "onTestPhone: 测试号码为空,跳过匹配");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rules rules = Rules.getInstance(this);
|
||||||
|
boolean isAllowed = rules.isAllowed(phone);
|
||||||
|
LogUtils.d(TAG, String.format("onTestPhone: 测试号码: %s | 匹配结果: %s", phone, isAllowed));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量测试预设号码规则匹配
|
||||||
|
*/
|
||||||
|
public void onTestMain(View view) {
|
||||||
|
LogUtils.d(TAG, "onTestMain: 开始批量测试号码规则匹配");
|
||||||
|
// 测试IntUtils工具类方法
|
||||||
|
LogUtils.d(TAG, "onTestMain: 执行 IntUtils.unittest_getIntInRange() 测试");
|
||||||
|
IntUtils.unittest_getIntInRange();
|
||||||
|
|
||||||
|
// 初始化规则实例
|
||||||
|
Rules rules = Rules.getInstance(this);
|
||||||
|
// 无规则时添加测试规则集
|
||||||
|
initTestRulesIfEmpty(rules);
|
||||||
|
|
||||||
|
// 预设测试号码列表
|
||||||
|
String[] testPhones = {
|
||||||
|
"16769764848", "16856582777", "17519703124",
|
||||||
|
"0205658955", "0108965253", "+8616769764848",
|
||||||
|
"4005816769764848", "95566"
|
||||||
|
};
|
||||||
|
|
||||||
|
// 遍历测试号码并输出结果
|
||||||
|
for (String phone : testPhones) {
|
||||||
|
boolean isAllowed = rules.isAllowed(phone);
|
||||||
|
LogUtils.d(TAG, String.format("onTestMain: 测试号码: %s | 匹配结果: %s", phone, isAllowed));
|
||||||
|
}
|
||||||
|
LogUtils.d(TAG, "onTestMain: 批量号码规则测试完成");
|
||||||
|
|
||||||
|
new Thread(new Runnable(){
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
LimitedTimeSpecialChannelService.unitTest(UnitTestActivity.this);
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 私有工具函数区 ======================
|
||||||
|
/**
|
||||||
|
* 规则集为空时初始化测试规则
|
||||||
|
*/
|
||||||
|
private void initTestRulesIfEmpty(Rules rules) {
|
||||||
|
if (rules.getPhoneBlacRuleBeanList().size() == 0) {
|
||||||
|
LogUtils.d(TAG, "initTestRulesIfEmpty: 当前无规则,添加测试规则集");
|
||||||
|
// 规则1:中国手机号允许
|
||||||
|
rules.add("^1[3-9]\\d{9}$", true, true);
|
||||||
|
// 规则2:0660区号号码允许
|
||||||
|
rules.add("^0660\\d+$", true, true);
|
||||||
|
// 规则3:020区号号码允许
|
||||||
|
rules.add("^020\\d+$", true, true);
|
||||||
|
// 规则4:默认拒接所有号码
|
||||||
|
rules.add(".*", false, true);
|
||||||
|
|
||||||
|
// 保存规则到本地
|
||||||
|
rules.saveRules();
|
||||||
|
LogUtils.d(TAG, "initTestRulesIfEmpty: 测试规则集已保存");
|
||||||
|
} else {
|
||||||
|
LogUtils.d(TAG, "initTestRulesIfEmpty: 当前已有规则,跳过初始化");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||