Compare commits
8 Commits
gpsrelayse
...
mymessagem
| Author | SHA1 | Date | |
|---|---|---|---|
| 99de6c05ba | |||
| 4c856367f5 | |||
| 63580b111c | |||
| 3b60a3b713 | |||
| 3231cd557a | |||
| 65f0515139 | |||
| 5316ac1815 | |||
| 6c2581276e |
@@ -1,97 +0,0 @@
|
|||||||
# 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 生成)
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
# 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 生成)
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
# 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 生成)
|
|
||||||
|
Before Width: | Height: | Size: 710 KiB |
@@ -24,13 +24,13 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "cc.winboll.studio.aes"
|
applicationId "cc.winboll.studio.aes"
|
||||||
minSdkVersion 26
|
minSdkVersion 21
|
||||||
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.20"
|
versionName "15.15"
|
||||||
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
|
||||||
#Tue May 12 13:11:28 HKT 2026
|
#Sat Apr 25 04:16:42 HKT 2026
|
||||||
stageCount=4
|
stageCount=10
|
||||||
libraryProject=libaes
|
libraryProject=libaes
|
||||||
baseVersion=15.20
|
baseVersion=15.15
|
||||||
publishVersion=15.20.3
|
publishVersion=15.15.9
|
||||||
buildCount=0
|
buildCount=0
|
||||||
baseBetaVersion=15.20.4
|
baseBetaVersion=15.15.10
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
<?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,16 +1,5 @@
|
|||||||
<?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>
|
|
||||||
|
|||||||
@@ -24,13 +24,13 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "cc.winboll.studio.appbase"
|
applicationId "cc.winboll.studio.appbase"
|
||||||
minSdkVersion 26
|
minSdkVersion 21
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
versionCode 1
|
versionCode 1
|
||||||
// versionName 更新后需要手动设置
|
// versionName 更新后需要手动设置
|
||||||
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
|
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
|
||||||
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
|
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
|
||||||
versionName "15.20"
|
versionName "15.15"
|
||||||
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
|
||||||
#Tue May 12 09:17:15 HKT 2026
|
#Tue Apr 28 17:08:30 HKT 2026
|
||||||
stageCount=10
|
stageCount=22
|
||||||
libraryProject=libappbase
|
libraryProject=libappbase
|
||||||
baseVersion=15.20
|
baseVersion=15.15
|
||||||
publishVersion=15.20.9
|
publishVersion=15.15.21
|
||||||
buildCount=0
|
buildCount=0
|
||||||
baseBetaVersion=15.20.10
|
baseBetaVersion=15.15.22
|
||||||
|
|||||||
@@ -9,9 +9,7 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/MyAPPBaseTheme"
|
android:theme="@style/MyAPPBaseTheme"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:process=":App"
|
android:process=":App">
|
||||||
android:sharedUserId="@string/shared_user_id"
|
|
||||||
android:sharedUserLabel="@string/shared_user_label">
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
@@ -21,16 +19,28 @@
|
|||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation">
|
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation">
|
||||||
|
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivityAlias"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:exported="true"
|
||||||
|
android:resizeableActivity="true"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation">
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".Main2Activity"
|
android:name=".Main2Activity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
|||||||
@@ -26,8 +26,6 @@ public class App extends GlobalApplication {
|
|||||||
if (isDebugging() != true) {
|
if (isDebugging() != true) {
|
||||||
setIsDebugging(BuildConfig.DEBUG);
|
setIsDebugging(BuildConfig.DEBUG);
|
||||||
}
|
}
|
||||||
// release 版调试码
|
|
||||||
//setIsDebugging(!BuildConfig.DEBUG);
|
|
||||||
|
|
||||||
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
|
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
|
||||||
ToastUtils.init(getApplicationContext());
|
ToastUtils.init(getApplicationContext());
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
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("测试布局崩溃...");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -162,7 +162,25 @@ public class MainActivity extends Activity {
|
|||||||
startActivity(aboutIntent);
|
startActivity(aboutIntent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onSplitScreenMode(View view) {
|
||||||
|
LogUtils.d(TAG, "onSplitScreenMode() 分屏测试按钮已点击");
|
||||||
|
ToastUtils.show("分屏测试:已启动新窗口");
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
|
||||||
|
android.graphics.Rect bounds = new android.graphics.Rect();
|
||||||
|
getWindow().getDecorView().getDisplay().getRectSize(bounds);
|
||||||
|
int height = bounds.height();
|
||||||
|
int width = bounds.width();
|
||||||
|
bounds.set(0, 0, width, height / 2);
|
||||||
|
LogUtils.d(TAG, "onSplitScreenMode() 分屏窗口范围: " + bounds);
|
||||||
|
android.content.Intent intent = new android.content.Intent(this, MainActivityAlias.class);
|
||||||
|
intent.setFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
LogUtils.d(TAG, "onSplitScreenMode() 准备启动MainActivityAlias");
|
||||||
|
android.app.ActivityOptions options = android.app.ActivityOptions.makeBasic();
|
||||||
|
options.setLaunchBounds(bounds);
|
||||||
|
startActivity(intent, options.toBundle());
|
||||||
|
LogUtils.d(TAG, "onSplitScreenMode() MainActivityAlias已启动");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void onMultiInstance(View view) {
|
public void onMultiInstance(View view) {
|
||||||
LogUtils.d(TAG, "onMultiInstance() 多开窗口按钮已点击");
|
LogUtils.d(TAG, "onMultiInstance() 多开窗口按钮已点击");
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package cc.winboll.studio.appbase;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Toolbar;
|
||||||
|
import cc.winboll.studio.appbase.R;
|
||||||
|
|
||||||
|
public class MainActivityAlias extends MainActivity {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_main);
|
||||||
|
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||||
|
setActionBar(toolbar);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@@ -4,13 +4,11 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent">
|
||||||
android:background="?attr/activityBackgroundColor">
|
|
||||||
|
|
||||||
<android.widget.Toolbar
|
<android.widget.Toolbar
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="?attr/toolbarBackgroundColor"
|
|
||||||
android:id="@+id/toolbar"/>
|
android:id="@+id/toolbar"/>
|
||||||
|
|
||||||
<cc.winboll.studio.libappbase.views.AboutView
|
<cc.winboll.studio.libappbase.views.AboutView
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@@ -4,13 +4,11 @@
|
|||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:padding="0dp"
|
android:padding="16dp">
|
||||||
android:background="?attr/activityBackgroundColor">
|
|
||||||
|
|
||||||
<android.widget.Toolbar
|
<android.widget.Toolbar
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="?attr/toolbarBackgroundColor"
|
|
||||||
android:id="@+id/toolbar"/>
|
android:id="@+id/toolbar"/>
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
@@ -30,8 +28,8 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="关于应用"
|
android:text="关于应用"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:textColor="?attr/activityTextColor"
|
android:textColor="@android:color/white"
|
||||||
android:background="?attr/toolbarBackgroundColor"
|
android:background="#81C7F5"
|
||||||
android:paddingVertical="12dp"
|
android:paddingVertical="12dp"
|
||||||
android:layout_marginHorizontal="24dp"
|
android:layout_marginHorizontal="24dp"
|
||||||
android:onClick="onAboutActivity"
|
android:onClick="onAboutActivity"
|
||||||
@@ -42,8 +40,8 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="应用崩溃测试"
|
android:text="应用崩溃测试"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:textColor="?attr/activityTextColor"
|
android:textColor="@android:color/white"
|
||||||
android:background="?attr/toolbarBackgroundColor"
|
android:background="#81C7F5"
|
||||||
android:paddingVertical="12dp"
|
android:paddingVertical="12dp"
|
||||||
android:layout_marginHorizontal="24dp"
|
android:layout_marginHorizontal="24dp"
|
||||||
android:onClick="onCrashTest"
|
android:onClick="onCrashTest"
|
||||||
@@ -54,8 +52,8 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="应用日志测试"
|
android:text="应用日志测试"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:textColor="?attr/activityTextColor"
|
android:textColor="@android:color/white"
|
||||||
android:background="?attr/toolbarBackgroundColor"
|
android:background="#81C7F5"
|
||||||
android:paddingVertical="12dp"
|
android:paddingVertical="12dp"
|
||||||
android:layout_marginHorizontal="24dp"
|
android:layout_marginHorizontal="24dp"
|
||||||
android:onClick="onLogTest"
|
android:onClick="onLogTest"
|
||||||
@@ -66,8 +64,8 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="应用日志测试(新窗口)"
|
android:text="应用日志测试(新窗口)"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:textColor="?attr/activityTextColor"
|
android:textColor="@android:color/white"
|
||||||
android:background="?attr/toolbarBackgroundColor"
|
android:background="#81C7F5"
|
||||||
android:paddingVertical="12dp"
|
android:paddingVertical="12dp"
|
||||||
android:layout_marginHorizontal="24dp"
|
android:layout_marginHorizontal="24dp"
|
||||||
android:onClick="onLogTestNewTask"
|
android:onClick="onLogTestNewTask"
|
||||||
@@ -78,22 +76,32 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="应用吐司测试"
|
android:text="应用吐司测试"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:textColor="?attr/activityTextColor"
|
android:textColor="@android:color/white"
|
||||||
android:background="?attr/toolbarBackgroundColor"
|
android:background="#81C7F5"
|
||||||
android:paddingVertical="12dp"
|
android:paddingVertical="12dp"
|
||||||
android:layout_marginHorizontal="24dp"
|
android:layout_marginHorizontal="24dp"
|
||||||
android:onClick="onToastUtilsTest"
|
android:onClick="onToastUtilsTest"
|
||||||
android:layout_margin="10dp"/>
|
android:layout_margin="10dp"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="分屏测试"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:background="#81C7F5"
|
||||||
|
android:paddingVertical="12dp"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:onClick="onSplitScreenMode"
|
||||||
|
android:layout_margin="10dp"/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="多开窗口"
|
android:text="多开窗口"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:textColor="?attr/activityTextColor"
|
android:textColor="@android:color/white"
|
||||||
android:background="?attr/toolbarBackgroundColor"
|
android:background="#81C7F5"
|
||||||
android:paddingVertical="12dp"
|
android:paddingVertical="12dp"
|
||||||
android:layout_marginHorizontal="24dp"
|
android:layout_marginHorizontal="24dp"
|
||||||
android:onClick="onMultiInstance"
|
android:onClick="onMultiInstance"
|
||||||
|
|||||||
@@ -5,13 +5,13 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:background="?attr/activityBackgroundColor">
|
android:background="@android:color/white">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Main2Activity"
|
android:text="Main2Activity"
|
||||||
android:textSize="24sp"
|
android:textSize="24sp"
|
||||||
android:textColor="?attr/activityTextColor"/>
|
android:textColor="@color/gray_900"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<?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>
|
|
||||||
15
appbase/src/main/res/values/attrs.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<declare-styleable name="AboutView">
|
||||||
|
<attr name="app_name" format="string" />
|
||||||
|
<attr name="app_apkfoldername" format="string" />
|
||||||
|
<attr name="app_apkname" format="string" />
|
||||||
|
<attr name="app_gitname" format="string" />
|
||||||
|
<attr name="app_gitowner" format="string" />
|
||||||
|
<attr name="app_gitappbranch" format="string" />
|
||||||
|
<attr name="app_gitappsubprojectfolder" format="string" />
|
||||||
|
<attr name="appdescription" format="string" />
|
||||||
|
<attr name="appicon" format="reference" />
|
||||||
|
<attr name="is_adddebugtools" format="boolean" />
|
||||||
|
</declare-styleable>
|
||||||
|
</resources>
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<style name="MyAPPBaseTheme" parent="APPBaseTheme">
|
<style name="MyAPPBaseTheme" parent="APPBaseTheme">
|
||||||
<item name="themeDebug">@style/MyDebugActivityTheme</item>
|
<item name="themeGlobalCrashActivity">@style/MyGlobalCrashActivityTheme</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="MyDebugActivityTheme" parent="DebugActivityTheme">
|
<style name="MyGlobalCrashActivityTheme" parent="GlobalCrashActivityTheme">
|
||||||
<item name="colorTittle">?attr/mainWindowTextColor</item>
|
<item name="colorTittle">#FFFFFFFF</item>
|
||||||
<item name="colorTittleBackgound">?attr/toolbarBackgroundColor</item>
|
<item name="colorTittleBackgound">#FF00A4B3</item>
|
||||||
<item name="colorText">?attr/debugTextColor</item>
|
<item name="colorText">#FFFFFFFF</item>
|
||||||
<item name="colorTextBackgound">?attr/mainWindowBackgroundColor</item>
|
<item name="colorTextBackgound">#FF000000</item>
|
||||||
<item name="toolbarTextColor">@color/toolbarTextColor</item>
|
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
1
autonfc/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
/build
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
# 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,119 +0,0 @@
|
|||||||
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'])
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#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
@@ -1,21 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<?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>
|
|
||||||
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<?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>
|
|
||||||
|
|
||||||
@@ -1,344 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,230 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<?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>
|
|
||||||
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
<?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>
|
|
||||||
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?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>
|
|
||||||
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="colorPrimary">#009688</color>
|
|
||||||
<color name="colorPrimaryDark">#00796B</color>
|
|
||||||
<color name="colorAccent">#FF9800</color>
|
|
||||||
</resources>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<resources>
|
|
||||||
<string name="app_name">AutoNFC</string>
|
|
||||||
|
|
||||||
</resources>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<?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>
|
|
||||||
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
# 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 +0,0 @@
|
|||||||
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#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
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<?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>
|
|
||||||
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<string name="app_name">Contacts+</string>
|
|
||||||
|
|
||||||
</resources>
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@@ -1,313 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,529 +0,0 @@
|
|||||||
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回调冲突
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,613 +0,0 @@
|
|||||||
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: 云盾信息控件未初始化,刷新失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
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: 当前已有规则,跳过初始化");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
package cc.winboll.studio.contacts.activities;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
|
||||||
import cc.winboll.studio.libaes.models.AESThemeBean;
|
|
||||||
import cc.winboll.studio.libaes.utils.AESThemeUtil;
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
|
||||||
* @Date 2025/03/31 15:16:45
|
|
||||||
* @Describe 应用窗口基类,统一处理主题设置与导航返回
|
|
||||||
*/
|
|
||||||
public class WinBollActivity extends AppCompatActivity implements IWinBoLLActivity {
|
|
||||||
|
|
||||||
// ====================== 常量定义区 ======================
|
|
||||||
public static final String TAG = "WinBollActivity";
|
|
||||||
|
|
||||||
// ====================== 成员变量区 ======================
|
|
||||||
protected volatile AESThemeBean.ThemeType mThemeType;
|
|
||||||
|
|
||||||
// ====================== 接口实现区 ======================
|
|
||||||
@Override
|
|
||||||
public Activity getActivity() {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getTag() {
|
|
||||||
return TAG;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 生命周期函数区 ======================
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
//LogUtils.d(TAG, "onCreate: 基类页面开始创建");
|
|
||||||
// 优先设置主题,再执行父类初始化
|
|
||||||
// mThemeType = getThemeType();
|
|
||||||
// setThemeStyle();
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
//LogUtils.d(TAG, "onCreate: 基类主题设置完成,当前主题类型=" + mThemeType);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 主题相关函数区 ======================
|
|
||||||
/**
|
|
||||||
* 获取当前应用主题类型
|
|
||||||
*/
|
|
||||||
AESThemeBean.ThemeType getThemeType() {
|
|
||||||
LogUtils.d(TAG, "getThemeType: 获取应用主题类型");
|
|
||||||
// 注释的SharedPreferences逻辑保留,便于后续扩展
|
|
||||||
/*SharedPreferences sharedPreferences = getSharedPreferences(
|
|
||||||
SHAREDPREFERENCES_NAME, MODE_PRIVATE);
|
|
||||||
return AESThemeBean.ThemeType.values()[((sharedPreferences.getInt(DRAWER_THEME_TYPE, AESThemeBean.ThemeType.DEFAULT.ordinal())))];
|
|
||||||
*/
|
|
||||||
return AESThemeBean.getThemeStyleType(AESThemeUtil.getThemeTypeID(getApplicationContext()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用当前主题样式
|
|
||||||
*/
|
|
||||||
void setThemeStyle() {
|
|
||||||
LogUtils.d(TAG, "setThemeStyle: 开始设置应用主题");
|
|
||||||
// 替换原注释逻辑,使用AESThemeUtil获取的主题ID
|
|
||||||
setTheme(AESThemeUtil.getThemeTypeID(getApplicationContext()));
|
|
||||||
LogUtils.d(TAG, "setThemeStyle: 主题设置完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 菜单与导航函数区 ======================
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
LogUtils.d(TAG, "onOptionsItemSelected: 菜单选项点击,itemId=" + item.getItemId());
|
|
||||||
// 处理导航栏返回按钮点击事件
|
|
||||||
// if (item.getItemId() == android.R.id.home) {
|
|
||||||
// LogUtils.d(TAG, "onOptionsItemSelected: 点击导航返回按钮,关闭当前页面");
|
|
||||||
// finish();
|
|
||||||
// return true;
|
|
||||||
// }
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
package cc.winboll.studio.contacts.adapters;
|
|
||||||
|
|
||||||
import android.content.ClipData;
|
|
||||||
import android.content.ClipboardManager;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.PopupMenu;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import cc.winboll.studio.contacts.R;
|
|
||||||
import cc.winboll.studio.contacts.model.CallLogModel;
|
|
||||||
import cc.winboll.studio.contacts.utils.ContactUtils;
|
|
||||||
import cc.winboll.studio.libaes.views.AOHPCTCSeekBar;
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
|
||||||
import cc.winboll.studio.libappbase.ToastUtils;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import cc.winboll.studio.contacts.dun.Rules;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
|
||||||
* @Date 2025/02/26 13:09:32
|
|
||||||
* @Describe 通话记录列表适配器
|
|
||||||
*/
|
|
||||||
public class CallLogAdapter extends RecyclerView.Adapter<CallLogAdapter.CallLogViewHolder> {
|
|
||||||
|
|
||||||
// ====================== 常量定义区 ======================
|
|
||||||
public static final String TAG = "CallLogAdapter";
|
|
||||||
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
|
|
||||||
|
|
||||||
// ====================== 成员变量区 ======================
|
|
||||||
private Context mContext;
|
|
||||||
private List<CallLogModel> callLogList;
|
|
||||||
private ContactUtils mContactUtils;
|
|
||||||
|
|
||||||
// ====================== 构造函数区 ======================
|
|
||||||
public CallLogAdapter(Context context, List<CallLogModel> callLogList) {
|
|
||||||
LogUtils.d(TAG, "CallLogAdapter: 初始化适配器,数据量=" + callLogList.size());
|
|
||||||
this.mContext = context;
|
|
||||||
this.callLogList = callLogList;
|
|
||||||
this.mContactUtils = ContactUtils.getInstance(mContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 公共方法区 ======================
|
|
||||||
/**
|
|
||||||
* 重新加载联系人数据
|
|
||||||
*/
|
|
||||||
public void relaodContacts() {
|
|
||||||
LogUtils.d(TAG, "relaodContacts: 开始重新加载联系人数据");
|
|
||||||
this.mContactUtils.reloadContacts();
|
|
||||||
notifyDataSetChanged();
|
|
||||||
LogUtils.d(TAG, "relaodContacts: 联系人数据加载完成,列表已刷新");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== RecyclerView 重写方法区 ======================
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public CallLogViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
||||||
LogUtils.d(TAG, "onCreateViewHolder: 创建列表项ViewHolder");
|
|
||||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_call_log, parent, false);
|
|
||||||
return new CallLogViewHolder(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull CallLogViewHolder holder, int position) {
|
|
||||||
LogUtils.d(TAG, "onBindViewHolder: 绑定列表项数据,position=" + position);
|
|
||||||
final CallLogModel callLog = callLogList.get(position);
|
|
||||||
|
|
||||||
// 绑定通话号码与联系人名称
|
|
||||||
String contactName = mContactUtils.getContactName(callLog.getPhoneNumber());
|
|
||||||
String phoneText = callLog.getPhoneNumber() + "☎" + (contactName == null ? "" : contactName);
|
|
||||||
holder.phoneNumber.setText(phoneText);
|
|
||||||
|
|
||||||
// 号码长按弹出菜单事件
|
|
||||||
holder.phoneNumber.setOnLongClickListener(new View.OnLongClickListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onLongClick(View p1) {
|
|
||||||
showPhonePopupMenu(holder.phoneNumber, callLog);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 绑定通话状态与时间
|
|
||||||
holder.callStatus.setText(callLog.getCallStatus());
|
|
||||||
holder.callDate.setText(DATE_FORMAT.format(callLog.getCallDate()));
|
|
||||||
|
|
||||||
// 初始化滑动拨号SeekBar
|
|
||||||
initDialSeekBar(holder.dialAOHPCTCSeekBar, callLog);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return callLogList == null ? 0 : callLogList.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 私有工具方法区 ======================
|
|
||||||
/**
|
|
||||||
* 显示号码操作弹窗菜单
|
|
||||||
*/
|
|
||||||
private void showPhonePopupMenu(View anchorView, final CallLogModel callLog) {
|
|
||||||
LogUtils.d(TAG, "showPhonePopupMenu: 弹出号码操作菜单");
|
|
||||||
PopupMenu menu = new PopupMenu(mContext, anchorView);
|
|
||||||
menu.getMenuInflater().inflate(R.menu.toolbar_calllog_phonenumber, menu.getMenu());
|
|
||||||
|
|
||||||
menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onMenuItemClick(MenuItem menuItem) {
|
|
||||||
int itemId = menuItem.getItemId();
|
|
||||||
if (itemId == R.id.item_calllog_phonenumber_copy) {
|
|
||||||
// 复制号码到剪贴板
|
|
||||||
ClipboardManager clipboard = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
|
|
||||||
ClipData clip = ClipData.newPlainText("call_log_phone", callLog.getPhoneNumber());
|
|
||||||
clipboard.setPrimaryClip(clip);
|
|
||||||
Toast.makeText(mContext, "Copy to clipboard.", Toast.LENGTH_SHORT).show();
|
|
||||||
LogUtils.d(TAG, "showPhonePopupMenu: 号码" + callLog.getPhoneNumber() + "已复制到剪贴板");
|
|
||||||
} else if (itemId == R.id.item_calllog_phonenumber_yundun_test) {
|
|
||||||
// 跳转到添加联系人页面
|
|
||||||
//if (Rules.getInstance(mContext).isAllowed(callLog.getPhoneNumber(), false)) {
|
|
||||||
if (Rules.getInstance(mContext).isAllowed(callLog.getPhoneNumber(), true)) {
|
|
||||||
ToastUtils.show("(✔)" + callLog.getPhoneNumber() + " Is Allowed By YunDun.");
|
|
||||||
} else {
|
|
||||||
ToastUtils.show("(✘)YunDun Defense The Phone " + callLog.getPhoneNumber() + "");
|
|
||||||
}
|
|
||||||
} else if (itemId == R.id.item_calllog_phonenumber_add_contact) {
|
|
||||||
// 跳转到添加联系人页面
|
|
||||||
ContactUtils.jumpToAddContact(mContext, callLog.getPhoneNumber());
|
|
||||||
LogUtils.d(TAG, "showPhonePopupMenu: 跳转添加联系人页面,号码=" + callLog.getPhoneNumber());
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
menu.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化滑动拨号SeekBar
|
|
||||||
*/
|
|
||||||
private void initDialSeekBar(AOHPCTCSeekBar seekBar, final CallLogModel callLog) {
|
|
||||||
LogUtils.d(TAG, "initDialSeekBar: 初始化滑动拨号控件");
|
|
||||||
seekBar.setThumb(seekBar.getContext().getDrawable(R.drawable.ic_call));
|
|
||||||
seekBar.setBlurRightDP(80);
|
|
||||||
seekBar.setThumbOffset(0);
|
|
||||||
|
|
||||||
seekBar.setOnOHPCListener(new AOHPCTCSeekBar.OnOHPCListener() {
|
|
||||||
@Override
|
|
||||||
public void onOHPCommit() {
|
|
||||||
String phoneNumber = callLog.getPhoneNumber().replaceAll("\\s", "");
|
|
||||||
LogUtils.d(TAG, "initDialSeekBar: 滑动拨号触发,号码=" + phoneNumber);
|
|
||||||
ToastUtils.show(phoneNumber);
|
|
||||||
|
|
||||||
Intent intent = new Intent(Intent.ACTION_CALL);
|
|
||||||
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
mContext.startActivity(intent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== ViewHolder 内部类 ======================
|
|
||||||
public class CallLogViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
TextView phoneNumber;
|
|
||||||
TextView callStatus;
|
|
||||||
TextView callDate;
|
|
||||||
AOHPCTCSeekBar dialAOHPCTCSeekBar;
|
|
||||||
|
|
||||||
public CallLogViewHolder(@NonNull View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
// Java7 适配:添加强制类型转换
|
|
||||||
phoneNumber = (TextView) itemView.findViewById(R.id.phone_number);
|
|
||||||
callStatus = (TextView) itemView.findViewById(R.id.call_status);
|
|
||||||
callDate = (TextView) itemView.findViewById(R.id.call_date);
|
|
||||||
dialAOHPCTCSeekBar = (AOHPCTCSeekBar) itemView.findViewById(R.id.aohpctcseekbar_dial);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
package cc.winboll.studio.contacts.adapters;
|
|
||||||
|
|
||||||
import android.content.ClipData;
|
|
||||||
import android.content.ClipboardManager;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.PopupMenu;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import cc.winboll.studio.contacts.R;
|
|
||||||
import cc.winboll.studio.contacts.model.ContactModel;
|
|
||||||
import cc.winboll.studio.contacts.utils.ContactUtils;
|
|
||||||
import cc.winboll.studio.libaes.views.AOHPCTCSeekBar;
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
|
||||||
import cc.winboll.studio.libappbase.ToastUtils;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
|
||||||
* @Date 2025/02/26 13:35:44
|
|
||||||
* @Describe 联系人列表适配器
|
|
||||||
*/
|
|
||||||
public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ContactViewHolder> {
|
|
||||||
|
|
||||||
// ====================== 常量定义区 ======================
|
|
||||||
public static final String TAG = "ContactAdapter";
|
|
||||||
// 移除未使用的 REQUEST_CALL_PHONE 常量,精简冗余代码
|
|
||||||
|
|
||||||
// ====================== 成员变量区 ======================
|
|
||||||
private Context mContext;
|
|
||||||
private List<ContactModel> contactList;
|
|
||||||
|
|
||||||
// ====================== 构造函数区 ======================
|
|
||||||
public ContactAdapter(Context context, List<ContactModel> contactList) {
|
|
||||||
LogUtils.d(TAG, "ContactAdapter: 初始化适配器,联系人数量=" + contactList.size());
|
|
||||||
this.mContext = context;
|
|
||||||
this.contactList = contactList;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== RecyclerView 重写方法区 ======================
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public ContactViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
||||||
LogUtils.d(TAG, "onCreateViewHolder: 创建联系人列表项ViewHolder");
|
|
||||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_contact, parent, false);
|
|
||||||
return new ContactViewHolder(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull ContactViewHolder holder, int position) {
|
|
||||||
LogUtils.d(TAG, "onBindViewHolder: 绑定联系人列表项数据,position=" + position);
|
|
||||||
final ContactModel contact = contactList.get(position);
|
|
||||||
|
|
||||||
// 绑定联系人名称与号码
|
|
||||||
holder.contactName.setText(contact.getName());
|
|
||||||
holder.contactNumber.setText(contact.getNumber());
|
|
||||||
|
|
||||||
// 长按联系人条目弹出操作菜单
|
|
||||||
holder.llPhoneNumberMain.setOnLongClickListener(new View.OnLongClickListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onLongClick(View v) {
|
|
||||||
showContactPopupMenu(holder.llPhoneNumberMain, contact);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 初始化滑动拨号SeekBar
|
|
||||||
initDialSeekBar(holder.dialAOHPCTCSeekBar, contact);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
// 增加空指针判断,避免空列表崩溃
|
|
||||||
return contactList == null ? 0 : contactList.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 私有工具方法区 ======================
|
|
||||||
/**
|
|
||||||
* 显示联系人操作弹窗菜单
|
|
||||||
*/
|
|
||||||
private void showContactPopupMenu(View anchorView, final ContactModel contact) {
|
|
||||||
LogUtils.d(TAG, "showContactPopupMenu: 弹出联系人操作菜单");
|
|
||||||
PopupMenu menu = new PopupMenu(mContext, anchorView);
|
|
||||||
menu.getMenuInflater().inflate(R.menu.toolbar_contact_phonenumber, menu.getMenu());
|
|
||||||
|
|
||||||
menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onMenuItemClick(MenuItem menuItem) {
|
|
||||||
int itemId = menuItem.getItemId();
|
|
||||||
if (itemId == R.id.item_contact_phonenumber_copy) {
|
|
||||||
// 复制联系人号码到剪贴板
|
|
||||||
ClipboardManager clipboard = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE);
|
|
||||||
ClipData clip = ClipData.newPlainText("contact_phone", contact.getNumber());
|
|
||||||
clipboard.setPrimaryClip(clip);
|
|
||||||
Toast.makeText(mContext, "Copy to clipboard.", Toast.LENGTH_SHORT).show();
|
|
||||||
LogUtils.d(TAG, "showContactPopupMenu: 联系人号码" + contact.getNumber() + "已复制到剪贴板");
|
|
||||||
} else if (itemId == R.id.item_calllog_phonenumber_edit_contact) {
|
|
||||||
// 跳转到编辑联系人页面
|
|
||||||
Long contactId = ContactUtils.getContactIdByPhone(mContext, contact.getNumber());
|
|
||||||
ContactUtils.jumpToEditContact(mContext, contact.getNumber(), contactId);
|
|
||||||
LogUtils.d(TAG, "showContactPopupMenu: 跳转编辑联系人页面,号码=" + contact.getNumber() + ",ID=" + contactId);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
menu.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化滑动拨号SeekBar
|
|
||||||
*/
|
|
||||||
private void initDialSeekBar(AOHPCTCSeekBar seekBar, final ContactModel contact) {
|
|
||||||
LogUtils.d(TAG, "initDialSeekBar: 初始化滑动拨号控件");
|
|
||||||
seekBar.setThumb(seekBar.getContext().getDrawable(R.drawable.ic_call));
|
|
||||||
seekBar.setBlurRightDP(80);
|
|
||||||
seekBar.setThumbOffset(0);
|
|
||||||
|
|
||||||
seekBar.setOnOHPCListener(new AOHPCTCSeekBar.OnOHPCListener() {
|
|
||||||
@Override
|
|
||||||
public void onOHPCommit() {
|
|
||||||
String phoneNumber = contact.getNumber().replaceAll("\\s", "");
|
|
||||||
LogUtils.d(TAG, "initDialSeekBar: 滑动拨号触发,号码=" + phoneNumber);
|
|
||||||
ToastUtils.show(phoneNumber);
|
|
||||||
|
|
||||||
Intent intent = new Intent(Intent.ACTION_CALL);
|
|
||||||
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
mContext.startActivity(intent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== ViewHolder 内部类 ======================
|
|
||||||
public class ContactViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
LinearLayout llPhoneNumberMain;
|
|
||||||
TextView contactName;
|
|
||||||
TextView contactNumber;
|
|
||||||
AOHPCTCSeekBar dialAOHPCTCSeekBar;
|
|
||||||
|
|
||||||
public ContactViewHolder(@NonNull View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
// Java7 适配:添加强制类型转换
|
|
||||||
llPhoneNumberMain = (LinearLayout) itemView.findViewById(R.id.itemcontactLinearLayout1);
|
|
||||||
contactName = (TextView) itemView.findViewById(R.id.contact_name);
|
|
||||||
contactNumber = (TextView) itemView.findViewById(R.id.contact_number);
|
|
||||||
dialAOHPCTCSeekBar = (AOHPCTCSeekBar) itemView.findViewById(R.id.aohpctcseekbar_dial);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
package cc.winboll.studio.contacts.adapters;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.CheckBox;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import cc.winboll.studio.contacts.R;
|
|
||||||
import cc.winboll.studio.contacts.model.PhoneConnectRuleBean;
|
|
||||||
import cc.winboll.studio.contacts.dun.Rules;
|
|
||||||
import cc.winboll.studio.contacts.views.LeftScrollView;
|
|
||||||
import cc.winboll.studio.libaes.dialogs.YesNoAlertDialog;
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
|
||||||
import cc.winboll.studio.libappbase.ToastUtils;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
|
||||||
* @Date 2025/03/02 17:27:41
|
|
||||||
* @Describe 通话规则列表适配器,支持简单查看/编辑两种视图切换
|
|
||||||
*/
|
|
||||||
public class PhoneConnectRuleAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
|
||||||
|
|
||||||
// ====================== 常量定义区 ======================
|
|
||||||
public static final String TAG = "PhoneConnectRuleAdapter";
|
|
||||||
private static final int VIEW_TYPE_SIMPLE = 0;
|
|
||||||
private static final int VIEW_TYPE_EDIT = 1;
|
|
||||||
private static final String NULL_RULE_TEXT = "[NULL]";
|
|
||||||
|
|
||||||
// ====================== 成员变量区 ======================
|
|
||||||
private Context mContext;
|
|
||||||
private List<PhoneConnectRuleBean> mRuleList;
|
|
||||||
|
|
||||||
// ====================== 构造函数区 ======================
|
|
||||||
public PhoneConnectRuleAdapter(Context context, List<PhoneConnectRuleBean> ruleList) {
|
|
||||||
LogUtils.d(TAG, "PhoneConnectRuleAdapter: 初始化适配器,规则数量=" + ruleList.size());
|
|
||||||
this.mContext = context;
|
|
||||||
this.mRuleList = ruleList;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== RecyclerView 重写方法区 ======================
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
||||||
LayoutInflater inflater = LayoutInflater.from(mContext);
|
|
||||||
if (viewType == VIEW_TYPE_SIMPLE) {
|
|
||||||
LogUtils.d(TAG, "onCreateViewHolder: 创建简单视图ViewHolder");
|
|
||||||
View view = inflater.inflate(R.layout.view_phone_connect_rule_simple, parent, false);
|
|
||||||
return new SimpleViewHolder(parent, view);
|
|
||||||
} else {
|
|
||||||
LogUtils.d(TAG, "onCreateViewHolder: 创建编辑视图ViewHolder");
|
|
||||||
View view = inflater.inflate(R.layout.view_phone_connect_rule, parent, false);
|
|
||||||
return new EditViewHolder(view);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, final int position) {
|
|
||||||
final PhoneConnectRuleBean model = mRuleList.get(position);
|
|
||||||
LogUtils.d(TAG, "onBindViewHolder: 绑定规则数据,position=" + position + ",视图类型=" + getItemViewType(position));
|
|
||||||
|
|
||||||
if (holder instanceof SimpleViewHolder) {
|
|
||||||
bindSimpleViewHolder((SimpleViewHolder) holder, model, position);
|
|
||||||
} else if (holder instanceof EditViewHolder) {
|
|
||||||
bindEditViewHolder((EditViewHolder) holder, model, position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return mRuleList == null ? 0 : mRuleList.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemViewType(int position) {
|
|
||||||
return mRuleList.get(position).isSimpleView() ? VIEW_TYPE_SIMPLE : VIEW_TYPE_EDIT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 私有视图绑定方法区 ======================
|
|
||||||
/**
|
|
||||||
* 绑定简单视图数据
|
|
||||||
*/
|
|
||||||
private void bindSimpleViewHolder(final SimpleViewHolder holder, final PhoneConnectRuleBean model, final int position) {
|
|
||||||
// 绑定规则文本,空值显示[NULL]
|
|
||||||
String ruleText = model.getRuleText().trim().isEmpty() ? NULL_RULE_TEXT : model.getRuleText().trim();
|
|
||||||
holder.tvRuleText.setText(ruleText);
|
|
||||||
// 设置复选框状态并禁用编辑
|
|
||||||
holder.checkBoxAllow.setChecked(model.isAllowConnection());
|
|
||||||
holder.checkBoxAllow.setEnabled(false);
|
|
||||||
holder.checkBoxEnable.setChecked(model.isEnable());
|
|
||||||
holder.checkBoxEnable.setEnabled(false);
|
|
||||||
|
|
||||||
// 设置左滑操作监听
|
|
||||||
holder.scrollView.setOnActionListener(new LeftScrollView.OnActionListener() {
|
|
||||||
@Override
|
|
||||||
public void onUp() {
|
|
||||||
LogUtils.d(TAG, "onUp: 规则上移,position=" + position);
|
|
||||||
moveRuleUp(position);
|
|
||||||
holder.scrollView.smoothScrollTo(0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDown() {
|
|
||||||
LogUtils.d(TAG, "onDown: 规则下移,position=" + position);
|
|
||||||
moveRuleDown(position);
|
|
||||||
holder.scrollView.smoothScrollTo(0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onEdit() {
|
|
||||||
LogUtils.d(TAG, "onEdit: 切换到编辑视图,position=" + position);
|
|
||||||
model.setIsSimpleView(false);
|
|
||||||
notifyItemChanged(position);
|
|
||||||
holder.scrollView.smoothScrollTo(0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDelete() {
|
|
||||||
LogUtils.d(TAG, "onDelete: 触发规则删除确认,position=" + position);
|
|
||||||
showDeleteConfirmDialog(holder.scrollView.getContext(), model, position);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绑定编辑视图数据
|
|
||||||
*/
|
|
||||||
private void bindEditViewHolder(final EditViewHolder holder, final PhoneConnectRuleBean model, final int position) {
|
|
||||||
// 绑定规则文本到输入框
|
|
||||||
holder.editText.setText(model.getRuleText());
|
|
||||||
// 绑定复选框状态
|
|
||||||
holder.checkBoxAllow.setChecked(model.isAllowConnection());
|
|
||||||
holder.checkBoxEnable.setChecked(model.isEnable());
|
|
||||||
|
|
||||||
// 确认按钮点击事件
|
|
||||||
holder.buttonConfirm.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
String newRuleText = holder.editText.getText().toString().trim();
|
|
||||||
model.setRuleText(newRuleText);
|
|
||||||
model.setIsAllowConnection(holder.checkBoxAllow.isChecked());
|
|
||||||
model.setIsEnable(holder.checkBoxEnable.isChecked());
|
|
||||||
model.setIsSimpleView(true);
|
|
||||||
|
|
||||||
// 保存规则并刷新视图
|
|
||||||
Rules.getInstance(mContext).saveRules();
|
|
||||||
notifyItemChanged(position);
|
|
||||||
Toast.makeText(mContext, "保存成功", Toast.LENGTH_SHORT).show();
|
|
||||||
LogUtils.d(TAG, "bindEditViewHolder: 规则保存成功,position=" + position + ",规则内容=" + newRuleText);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 私有业务工具方法区 ======================
|
|
||||||
/**
|
|
||||||
* 规则上移
|
|
||||||
*/
|
|
||||||
private void moveRuleUp(int position) {
|
|
||||||
if (position <= 0) {
|
|
||||||
ToastUtils.show("已到顶部,无法上移");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ArrayList<PhoneConnectRuleBean> ruleList = Rules.getInstance(mContext).getPhoneBlacRuleBeanList();
|
|
||||||
swapRulePosition(ruleList, position, position - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 规则下移
|
|
||||||
*/
|
|
||||||
private void moveRuleDown(int position) {
|
|
||||||
ArrayList<PhoneConnectRuleBean> ruleList = Rules.getInstance(mContext).getPhoneBlacRuleBeanList();
|
|
||||||
if (position >= ruleList.size() - 1) {
|
|
||||||
ToastUtils.show("已到底部,无法下移");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
swapRulePosition(ruleList, position, position + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 交换规则位置
|
|
||||||
*/
|
|
||||||
private void swapRulePosition(ArrayList<PhoneConnectRuleBean> list, int fromPos, int toPos) {
|
|
||||||
PhoneConnectRuleBean temp = list.get(fromPos);
|
|
||||||
list.set(fromPos, list.get(toPos));
|
|
||||||
list.set(toPos, temp);
|
|
||||||
Rules.getInstance(mContext).saveRules();
|
|
||||||
notifyDataSetChanged();
|
|
||||||
LogUtils.d(TAG, "swapRulePosition: 规则位置交换完成,from=" + fromPos + ",to=" + toPos);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示删除确认弹窗
|
|
||||||
*/
|
|
||||||
private void showDeleteConfirmDialog(Context dialogContext, final PhoneConnectRuleBean model, final int position) {
|
|
||||||
YesNoAlertDialog.show(dialogContext, "删除确认", "是否删除该通话规则?", new YesNoAlertDialog.OnDialogResultListener() {
|
|
||||||
@Override
|
|
||||||
public void onYes() {
|
|
||||||
ArrayList<PhoneConnectRuleBean> ruleList = Rules.getInstance(mContext).getPhoneBlacRuleBeanList();
|
|
||||||
ruleList.remove(position);
|
|
||||||
Rules.getInstance(mContext).saveRules();
|
|
||||||
notifyDataSetChanged();
|
|
||||||
LogUtils.d(TAG, "showDeleteConfirmDialog: 规则删除成功,position=" + position);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNo() {
|
|
||||||
LogUtils.d(TAG, "showDeleteConfirmDialog: 用户取消删除规则,position=" + position);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== ViewHolder 内部类区 ======================
|
|
||||||
static class SimpleViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
LeftScrollView scrollView;
|
|
||||||
TextView tvRuleText;
|
|
||||||
CheckBox checkBoxAllow;
|
|
||||||
CheckBox checkBoxEnable;
|
|
||||||
|
|
||||||
public SimpleViewHolder(@NonNull ViewGroup parent, @NonNull View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
scrollView = (LeftScrollView) itemView.findViewById(R.id.scrollView);
|
|
||||||
// 初始化简单视图内容布局
|
|
||||||
LayoutInflater inflater = LayoutInflater.from(itemView.getContext());
|
|
||||||
View viewContent = inflater.inflate(R.layout.view_phone_connect_rule_simple_content, parent, false);
|
|
||||||
tvRuleText = (TextView) viewContent.findViewById(R.id.ruletext_tv);
|
|
||||||
checkBoxAllow = (CheckBox) viewContent.findViewById(R.id.checkbox_allow);
|
|
||||||
checkBoxEnable = (CheckBox) viewContent.findViewById(R.id.checkbox_enable);
|
|
||||||
// 设置内容宽度并添加到滚动视图
|
|
||||||
scrollView.setContentWidth(parent.getWidth());
|
|
||||||
scrollView.addContentLayout(viewContent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class EditViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
EditText editText;
|
|
||||||
CheckBox checkBoxAllow;
|
|
||||||
CheckBox checkBoxEnable;
|
|
||||||
Button buttonConfirm;
|
|
||||||
|
|
||||||
public EditViewHolder(@NonNull View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
// Java7 适配:添加强制类型转换
|
|
||||||
editText = (EditText) itemView.findViewById(R.id.edit_text);
|
|
||||||
checkBoxAllow = (CheckBox) itemView.findViewById(R.id.checkbox_allow);
|
|
||||||
checkBoxEnable = (CheckBox) itemView.findViewById(R.id.checkbox_enable);
|
|
||||||
buttonConfirm = (Button) itemView.findViewById(R.id.button_confirm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,260 +0,0 @@
|
|||||||
package cc.winboll.studio.contacts.bobulltoon;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
|
||||||
* @Date 2025/03/02 13:47:48
|
|
||||||
* @Describe 汤姆猫管家 :使用 BoBullToon 项目,对通讯地址进行筛选判断的好朋友。
|
|
||||||
*/
|
|
||||||
import android.content.Context;
|
|
||||||
import cc.winboll.studio.contacts.R;
|
|
||||||
import cc.winboll.studio.contacts.dun.Rules;
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
|
||||||
import cc.winboll.studio.libappbase.ToastUtils;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileFilter;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.zip.ZipEntry;
|
|
||||||
import java.util.zip.ZipInputStream;
|
|
||||||
import okhttp3.OkHttpClient;
|
|
||||||
import okhttp3.Request;
|
|
||||||
import okhttp3.Response;
|
|
||||||
|
|
||||||
public class TomCat {
|
|
||||||
|
|
||||||
public static final String TAG = "TomCat";
|
|
||||||
|
|
||||||
List<String> listPhoneBoBullToon = new ArrayList<String>();
|
|
||||||
String mszBoBullToon_URL;
|
|
||||||
|
|
||||||
static volatile TomCat _TomCat;
|
|
||||||
Context mContext;
|
|
||||||
TomCat(Context context) {
|
|
||||||
mContext = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static synchronized TomCat getInstance(Context context) {
|
|
||||||
if (_TomCat == null) {
|
|
||||||
_TomCat = new TomCat(context);
|
|
||||||
}
|
|
||||||
return _TomCat;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDefaultBobulltoonUrl() {
|
|
||||||
return mContext.getString(R.string.default_bobulltoon_url);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean downloadAndExtractZip(String zipUrl, String destinationFolder) throws IOException {
|
|
||||||
OkHttpClient client = new OkHttpClient();
|
|
||||||
Request request = new Request.Builder()
|
|
||||||
.url(zipUrl)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
try {
|
|
||||||
Response response = client.newCall(request).execute();
|
|
||||||
if (!response.isSuccessful()) {
|
|
||||||
throw new IOException("Unexpected code " + response);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 下载 ZIP 文件到临时位置
|
|
||||||
File tempZipFile = File.createTempFile("temp", ".zip");
|
|
||||||
try {
|
|
||||||
InputStream inputStream = response.body().byteStream();
|
|
||||||
FileOutputStream outputStream = new FileOutputStream(tempZipFile);
|
|
||||||
byte[] buffer = new byte[1024];
|
|
||||||
int length;
|
|
||||||
while ((length = inputStream.read(buffer)) > 0) {
|
|
||||||
outputStream.write(buffer, 0, length);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解压 ZIP 文件到指定文件夹
|
|
||||||
try {
|
|
||||||
ZipInputStream zipInputStream = new ZipInputStream(Files.newInputStream(tempZipFile.toPath()));
|
|
||||||
ZipEntry zipEntry;
|
|
||||||
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
|
|
||||||
Path targetFilePath = Paths.get(destinationFolder, zipEntry.getName());
|
|
||||||
if (zipEntry.isDirectory()) {
|
|
||||||
Files.createDirectories(targetFilePath);
|
|
||||||
} else {
|
|
||||||
Files.createDirectories(targetFilePath.getParent());
|
|
||||||
try (FileOutputStream fos = new FileOutputStream(targetFilePath.toFile())) {
|
|
||||||
byte[] buffer = new byte[1024];
|
|
||||||
int len;
|
|
||||||
while ((len = zipInputStream.read(buffer)) > 0) {
|
|
||||||
fos.write(buffer, 0, len);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
zipInputStream.closeEntry();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除临时 ZIP 文件
|
|
||||||
tempZipFile.delete();
|
|
||||||
LogUtils.d(TAG, "已更新 BoBullToon 数据");
|
|
||||||
return true;
|
|
||||||
} catch (Exception e) {
|
|
||||||
ToastUtils.show(e.getMessage());
|
|
||||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean downloadBoBullToon() {
|
|
||||||
String zipUrl = Rules.getInstance(mContext).getBoBullToonURL(); // 替换为实际的 ZIP 文件 URL
|
|
||||||
String destinationFolder = getWorkingFolder().getPath(); // 替换为实际的目标文件夹路径
|
|
||||||
try {
|
|
||||||
// 删除旧文件
|
|
||||||
File fOldFolder = new File(destinationFolder);
|
|
||||||
if (fOldFolder.exists()) {
|
|
||||||
deleteFolderRecursive(fOldFolder);
|
|
||||||
fOldFolder.mkdirs();
|
|
||||||
LogUtils.d(TAG, "已清空 BoBullToon 数据");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新新文件
|
|
||||||
if (downloadAndExtractZip(zipUrl, destinationFolder)) {
|
|
||||||
LogUtils.d(TAG, "ZIP 文件下载并解压成功。");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} catch (IOException e) {
|
|
||||||
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 递归删除文件夹及其内容的方法
|
|
||||||
public static void deleteFolderRecursive(File file) {
|
|
||||||
// 判断是否为文件夹
|
|
||||||
if (file.isDirectory()) {
|
|
||||||
// 列出文件夹中的所有文件和子文件夹
|
|
||||||
File[] files = file.listFiles();
|
|
||||||
if (files != null) {
|
|
||||||
// 遍历并递归删除每个文件和子文件夹
|
|
||||||
for (File f : files) {
|
|
||||||
deleteFolderRecursive(f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 删除文件或空文件夹
|
|
||||||
file.delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
File getWorkingFolder() {
|
|
||||||
return mContext.getExternalFilesDir(TAG);
|
|
||||||
}
|
|
||||||
|
|
||||||
public File getBoBullToonDataFolder() {
|
|
||||||
File fCheckRoot = getWorkingFolder();
|
|
||||||
if (fCheckRoot == null || !fCheckRoot.exists()) {
|
|
||||||
return fCheckRoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 递归查找符合条件的文件夹
|
|
||||||
File targetFolder = findTargetFolder(fCheckRoot);
|
|
||||||
return targetFolder != null ? targetFolder : fCheckRoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 递归查找同时包含LICENSE和README.md文件的文件夹
|
|
||||||
*/
|
|
||||||
private File findTargetFolder(File currentFolder) {
|
|
||||||
// 检查当前文件夹是否符合条件
|
|
||||||
if (hasRequiredFiles(currentFolder)) {
|
|
||||||
return currentFolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查找子文件夹(Java 7不支持方法引用,用匿名内部类过滤)
|
|
||||||
File[] subFolders = currentFolder.listFiles(new FileFilter() {
|
|
||||||
@Override
|
|
||||||
public boolean accept(File file) {
|
|
||||||
return file.isDirectory(); // 仅保留子文件夹
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (subFolders != null) {
|
|
||||||
for (File subFolder : subFolders) {
|
|
||||||
File result = findTargetFolder(subFolder);
|
|
||||||
if (result != null) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查文件夹中是否同时存在LICENSE和README.md文件
|
|
||||||
*/
|
|
||||||
private boolean hasRequiredFiles(File folder) {
|
|
||||||
if (folder == null || !folder.isDirectory()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查两个文件是否同时存在且均为文件(非文件夹)
|
|
||||||
File licenseFile = new File(folder, "LICENSE");
|
|
||||||
File readmeFile = new File(folder, "README.md");
|
|
||||||
|
|
||||||
return licenseFile.exists() && licenseFile.isFile()
|
|
||||||
&& readmeFile.exists() && readmeFile.isFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void cleanBoBullToon() {
|
|
||||||
String destinationFolder = getWorkingFolder().getPath(); // 替换为实际的目标文件夹路径
|
|
||||||
// 删除旧文件
|
|
||||||
File fOldFolder = new File(destinationFolder);
|
|
||||||
if (fOldFolder.exists()) {
|
|
||||||
deleteFolderRecursive(fOldFolder);
|
|
||||||
fOldFolder.mkdirs();
|
|
||||||
}
|
|
||||||
|
|
||||||
ToastUtils.show("已清空 BoBullToon 数据!");
|
|
||||||
LogUtils.d(TAG, "已清空 BoBullToon 数据");
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean loadPhoneBoBullToon() {
|
|
||||||
listPhoneBoBullToon.clear();
|
|
||||||
File fBoBullToon = getBoBullToonDataFolder();
|
|
||||||
if (fBoBullToon.exists()) {
|
|
||||||
LogUtils.d(TAG, String.format("getBoBullToonDataFolder() %s", getWorkingFolder()));
|
|
||||||
for (File userFolder : fBoBullToon.listFiles()) {
|
|
||||||
if (userFolder.isDirectory()) {
|
|
||||||
for (File recordFile : userFolder.listFiles()) {
|
|
||||||
listPhoneBoBullToon.add(recordFile.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < listPhoneBoBullToon.size(); i++) {
|
|
||||||
LogUtils.d(TAG, String.format("listPhoneBoBullToon add : %s", listPhoneBoBullToon.get(i)));
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
LogUtils.d(TAG, "fBoBullToon not exists。");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isPhoneBoBullToon(String phone) {
|
|
||||||
for (int i = 0; i < listPhoneBoBullToon.size(); i++) {
|
|
||||||
LogUtils.d(TAG, String.format("isPhoneBoBullToon(...) get(i) phone : %s", listPhoneBoBullToon.get(i)));
|
|
||||||
if (listPhoneBoBullToon.get(i).equals(phone)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
package cc.winboll.studio.contacts.dun;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import cc.winboll.studio.contacts.activities.SettingsActivity;
|
|
||||||
import cc.winboll.studio.contacts.bobulltoon.TomCat;
|
|
||||||
import cc.winboll.studio.contacts.model.PhoneConnectRuleBean;
|
|
||||||
import cc.winboll.studio.contacts.model.SettingsBean;
|
|
||||||
import cc.winboll.studio.contacts.services.LimitedTimeSpecialChannelService;
|
|
||||||
import cc.winboll.studio.contacts.services.MainService;
|
|
||||||
import cc.winboll.studio.contacts.utils.ContactUtils;
|
|
||||||
import cc.winboll.studio.contacts.utils.IntUtils;
|
|
||||||
import cc.winboll.studio.contacts.utils.RegexPPiUtils;
|
|
||||||
import cc.winboll.studio.contacts.views.DunTemperatureView;
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Timer;
|
|
||||||
import java.util.TimerTask;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
|
||||||
* @Date 2025/02/21 06:15:10
|
|
||||||
* @Describe 云盾防御规则(双重校验锁单例模式)
|
|
||||||
*/
|
|
||||||
public class Rules {
|
|
||||||
|
|
||||||
public static final String TAG = "Rules";
|
|
||||||
|
|
||||||
// 单例核心:volatile 保证多线程可见性,禁止指令重排
|
|
||||||
private static volatile Rules sInstance;
|
|
||||||
// 上下文需使用 ApplicationContext 避免内存泄漏
|
|
||||||
private static Context sApplicationContext;
|
|
||||||
|
|
||||||
ArrayList<PhoneConnectRuleBean> _PhoneConnectRuleModelList;
|
|
||||||
Context mContext;
|
|
||||||
SettingsBean mSettingsModel;
|
|
||||||
Timer mDunResumeTimer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 私有化构造方法,禁止外部 new 实例
|
|
||||||
*/
|
|
||||||
private Rules(Context context) {
|
|
||||||
mContext = context.getApplicationContext();
|
|
||||||
_PhoneConnectRuleModelList = new ArrayList<PhoneConnectRuleBean>();
|
|
||||||
reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取单例实例(双重校验锁,线程安全)
|
|
||||||
* @param context 上下文,建议传入 ApplicationContext
|
|
||||||
* @return Rules 唯一实例
|
|
||||||
*/
|
|
||||||
public static Rules getInstance(Context context) {
|
|
||||||
// 第一次校验:无锁,提高性能
|
|
||||||
if (sInstance == null) {
|
|
||||||
// 加锁:保证多线程下仅初始化一次
|
|
||||||
synchronized (Rules.class) {
|
|
||||||
// 第二次校验:防止多线程并发时重复创建
|
|
||||||
if (sInstance == null) {
|
|
||||||
sInstance = new Rules(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sInstance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void reload() {
|
|
||||||
LogUtils.d(TAG, "reload()");
|
|
||||||
loadRules();
|
|
||||||
loadDun();
|
|
||||||
setDunResumTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDunResumTimer() {
|
|
||||||
if (mDunResumeTimer != null) {
|
|
||||||
mDunResumeTimer.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 盾牌恢复定时器
|
|
||||||
mDunResumeTimer = new Timer();
|
|
||||||
int ss = IntUtils.getIntInRange(mSettingsModel.getDunResumeSecondCount() * 1000, SettingsBean.MIN_INTRANGE, SettingsBean.MAX_INTRANGE);
|
|
||||||
mDunResumeTimer.schedule(new TimerTask() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
if (mSettingsModel.getDunCurrentCount() != mSettingsModel.getDunTotalCount()) {
|
|
||||||
LogUtils.d(TAG, String.format("当前防御值为%d,最大防御值为%d", mSettingsModel.getDunCurrentCount(), mSettingsModel.getDunTotalCount()));
|
|
||||||
int newDunCount = mSettingsModel.getDunCurrentCount() + mSettingsModel.getDunResumeCount();
|
|
||||||
// 设置盾值在[0,DunTotalCount]之内其他值一律重置为 DunTotalCount。
|
|
||||||
newDunCount = (newDunCount > mSettingsModel.getDunTotalCount()) ?mSettingsModel.getDunTotalCount(): newDunCount;
|
|
||||||
mSettingsModel.setDunCurrentCount(newDunCount);
|
|
||||||
LogUtils.d(TAG, String.format("设置防御值为%d", newDunCount));
|
|
||||||
saveDun();
|
|
||||||
// 一键更新所有 DunTemperatureView 实例的盾值
|
|
||||||
DunTemperatureView.updateDunValue(mSettingsModel.getDunTotalCount(), mSettingsModel.getDunCurrentCount());
|
|
||||||
|
|
||||||
SettingsActivity.notifyDunInfoUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 1000, ss);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void loadRules() {
|
|
||||||
_PhoneConnectRuleModelList.clear();
|
|
||||||
PhoneConnectRuleBean.loadBeanList(mContext, _PhoneConnectRuleModelList, PhoneConnectRuleBean.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void saveRules() {
|
|
||||||
LogUtils.d(TAG, String.format("saveRules()"));
|
|
||||||
PhoneConnectRuleBean.saveBeanList(mContext, _PhoneConnectRuleModelList, PhoneConnectRuleBean.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void resetDefaultBoBullToonURL() {
|
|
||||||
mSettingsModel.setBoBullToon_URL(TomCat.getInstance(mContext).getDefaultBobulltoonUrl());
|
|
||||||
saveDun();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setBoBullToonURL(String szUrl) {
|
|
||||||
mSettingsModel.setBoBullToon_URL(szUrl);
|
|
||||||
saveDun();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getBoBullToonURL() {
|
|
||||||
return mSettingsModel.getBoBullToon_URL();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void loadDun() {
|
|
||||||
mSettingsModel = SettingsBean.loadBean(mContext, SettingsBean.class);
|
|
||||||
if (mSettingsModel == null) {
|
|
||||||
mSettingsModel = new SettingsBean();
|
|
||||||
SettingsBean.saveBean(mContext, mSettingsModel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void saveDun() {
|
|
||||||
LogUtils.d(TAG, String.format("saveDun()"));
|
|
||||||
SettingsBean.saveBean(mContext, mSettingsModel);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isAllowed(String phoneNumber) {
|
|
||||||
return isAllowed(phoneNumber, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isAllowed(String phoneNumber, boolean isTest) {
|
|
||||||
// 没有启用云盾,默认允许接通任何电话
|
|
||||||
if (!mSettingsModel.isEnableDun()) {
|
|
||||||
LogUtils.d(TAG, String.format("没有启用云盾,默认允许接通任何电话。isAllowed(...) return true"));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 云盾防御体系
|
|
||||||
boolean isDefend = false; // 盾牌是否生效
|
|
||||||
boolean isConnect = true; // 防御结果是否连接
|
|
||||||
|
|
||||||
// 进行盾牌层数预计缩减计算
|
|
||||||
int nDunCurrentCount = mSettingsModel.getDunCurrentCount() - 1;
|
|
||||||
LogUtils.d(TAG, String.format("nDunCurrentCount : %d", nDunCurrentCount));
|
|
||||||
|
|
||||||
// 如果盾值小于1,则解除防御
|
|
||||||
if (!isDefend && nDunCurrentCount < 1) {
|
|
||||||
// 盾层为1以下,防御解除
|
|
||||||
LogUtils.d(TAG, "盾层为1以下,防御解除");
|
|
||||||
isDefend = true;
|
|
||||||
isConnect = true;
|
|
||||||
LogUtils.d(TAG, String.format("isDefend == %s\nisConnect == %s", isDefend, isConnect));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 正则运算预防针
|
|
||||||
if (!isDefend && !RegexPPiUtils.isPPiOK(phoneNumber)) {
|
|
||||||
LogUtils.d(TAG, "正则运算预防针生效。");
|
|
||||||
isDefend = true;
|
|
||||||
isConnect = false;
|
|
||||||
LogUtils.d(TAG, String.format("isDefend == %s\nisConnect == %s", isDefend, isConnect));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 限时特殊通道打开时返回连接
|
|
||||||
if (!isDefend && LimitedTimeSpecialChannelService.isServiceRunning()) {
|
|
||||||
LogUtils.d(TAG, String.format("PhoneNumber %s\n and Limited Time Special Channel Service Is Running.", phoneNumber));
|
|
||||||
isDefend = true;
|
|
||||||
isConnect = true;
|
|
||||||
LogUtils.d(TAG, String.format("isDefend == %s\nisConnect == %s", isDefend, isConnect));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检验拨不通号码群
|
|
||||||
if (!isDefend && MainService.isPhoneInBoBullToon(phoneNumber)) {
|
|
||||||
LogUtils.d(TAG, String.format("PhoneNumber %s\n Is In BoBullToon", phoneNumber));
|
|
||||||
isDefend = true;
|
|
||||||
isConnect = false;
|
|
||||||
LogUtils.d(TAG, String.format("isDefend == %s\nisConnect == %s", isDefend, isConnect));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查询通讯录是否有该联系人
|
|
||||||
boolean isPhoneInContacts = ContactUtils.getInstance(mContext).isPhoneInContacts(mContext, phoneNumber);
|
|
||||||
if (!isDefend) {
|
|
||||||
if (isPhoneInContacts) {
|
|
||||||
LogUtils.d(TAG, String.format("Phone %s is in contacts.", phoneNumber));
|
|
||||||
isDefend = true;
|
|
||||||
isConnect = true;
|
|
||||||
LogUtils.d(TAG, String.format("isDefend == %s\nisConnect == %s", isDefend, isConnect));
|
|
||||||
} else {
|
|
||||||
LogUtils.d(TAG, String.format("Phone %s is not in contacts.", phoneNumber));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 正则匹配规则名单校验
|
|
||||||
if (!isDefend) {
|
|
||||||
for (int i = 0; i < _PhoneConnectRuleModelList.size(); i++) {
|
|
||||||
if (_PhoneConnectRuleModelList.get(i).isEnable()) {
|
|
||||||
String regex = _PhoneConnectRuleModelList.get(i).getRuleText();
|
|
||||||
if (Pattern.matches(regex, phoneNumber)) {
|
|
||||||
LogUtils.d(TAG, String.format("Phone Number [%s] is matched by rule : %s", phoneNumber, _PhoneConnectRuleModelList.get(i)));
|
|
||||||
isDefend = true;
|
|
||||||
isConnect = _PhoneConnectRuleModelList.get(i).isAllowConnection();
|
|
||||||
LogUtils.d(TAG, String.format("isDefend == %s\nisConnect == %s", isDefend, isConnect));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果不是规则测试时,就执行云盾防御机能。
|
|
||||||
if (isTest == false) {
|
|
||||||
if (isConnect) {
|
|
||||||
// 如果防御结果为连接,则恢复防御盾牌最大值层数
|
|
||||||
mSettingsModel.setDunCurrentCount(mSettingsModel.getDunTotalCount());
|
|
||||||
LogUtils.d(TAG, String.format("防御结果为连接,恢复防御盾牌最大值层数 %d", mSettingsModel.getDunTotalCount()));
|
|
||||||
saveDun();
|
|
||||||
SettingsActivity.notifyDunInfoUpdate();
|
|
||||||
} else if (isDefend) {
|
|
||||||
// 如果触发了以上某个防御模块,减少防御盾牌层数
|
|
||||||
int newDunCount = nDunCurrentCount;
|
|
||||||
LogUtils.d(TAG, String.format("新的防御层数预计为 %d", newDunCount));
|
|
||||||
|
|
||||||
// 保证盾值在[1,DunTotalCount]之内其他值一律重置为 DunTotalCount。
|
|
||||||
if (newDunCount > 0 && newDunCount < mSettingsModel.getDunTotalCount()) {
|
|
||||||
mSettingsModel.setDunCurrentCount(newDunCount);
|
|
||||||
LogUtils.d(TAG, String.format("设置防御层数为 %d", newDunCount));
|
|
||||||
} else {
|
|
||||||
mSettingsModel.setDunCurrentCount(mSettingsModel.getDunTotalCount());
|
|
||||||
LogUtils.d(TAG, String.format("盾值不在[0,%d]区间,恢复防御最大值%d", mSettingsModel.getDunTotalCount(), mSettingsModel.getDunTotalCount()));
|
|
||||||
}
|
|
||||||
|
|
||||||
saveDun();
|
|
||||||
SettingsActivity.notifyDunInfoUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 一键更新所有 DunTemperatureView 实例的盾值
|
|
||||||
DunTemperatureView.updateDunValue(mSettingsModel.getDunTotalCount(), mSettingsModel.getDunCurrentCount());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回校验结果
|
|
||||||
LogUtils.d(TAG, String.format("返回校验结果 isConnect == %s", isConnect));
|
|
||||||
return isConnect;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void add(String szPhoneConnectRule, boolean isAllowConnection, boolean isEnable) {
|
|
||||||
_PhoneConnectRuleModelList.add(new PhoneConnectRuleBean(szPhoneConnectRule, isAllowConnection, isEnable));
|
|
||||||
}
|
|
||||||
|
|
||||||
public ArrayList<PhoneConnectRuleBean> getPhoneBlacRuleBeanList() {
|
|
||||||
return _PhoneConnectRuleModelList;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SettingsBean getSettingsModel() {
|
|
||||||
return mSettingsModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 可选:释放单例资源(如退出应用时调用)
|
|
||||||
*/
|
|
||||||
public static void releaseInstance() {
|
|
||||||
if (sInstance != null) {
|
|
||||||
sInstance.mDunResumeTimer.cancel();
|
|
||||||
sInstance._PhoneConnectRuleModelList.clear();
|
|
||||||
sInstance.mSettingsModel = null;
|
|
||||||
sInstance.mContext = null;
|
|
||||||
sInstance = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
package cc.winboll.studio.contacts.fragments;
|
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.os.Message;
|
|
||||||
import android.provider.CallLog;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.core.app.ActivityCompat;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import cc.winboll.studio.contacts.R;
|
|
||||||
import cc.winboll.studio.contacts.adapters.CallLogAdapter;
|
|
||||||
import cc.winboll.studio.contacts.model.CallLogModel;
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
|
||||||
* @Date 2025/02/20 12:57:00
|
|
||||||
* @Describe 通话记录区域视图(支持懒加载,仅切换到当前页才加载数据)
|
|
||||||
*/
|
|
||||||
public class CallLogFragment extends Fragment {
|
|
||||||
|
|
||||||
// ====================== 常量定义区 ======================
|
|
||||||
public static final String TAG = "CallLogFragment";
|
|
||||||
public static final int MSG_UPDATE = 1;
|
|
||||||
private static final String ARG_PAGE = "ARG_PAGE";
|
|
||||||
private static final int REQUEST_READ_CALL_LOG = 1;
|
|
||||||
|
|
||||||
// ====================== 静态成员区 ======================
|
|
||||||
static volatile CallLogFragment _CallLogFragment;
|
|
||||||
|
|
||||||
// ====================== 页面参数区 ======================
|
|
||||||
private int mPage;
|
|
||||||
|
|
||||||
// ====================== UI控件与适配器区 ======================
|
|
||||||
private RecyclerView recyclerView;
|
|
||||||
private CallLogAdapter callLogAdapter;
|
|
||||||
private List<CallLogModel> callLogList = new ArrayList<CallLogModel>();
|
|
||||||
|
|
||||||
// ====================== 业务逻辑成员区 ======================
|
|
||||||
private Handler mHandler;
|
|
||||||
// 懒加载标记:记录当前Fragment是否已初始化数据(避免重复加载)
|
|
||||||
private boolean isDataInited = false;
|
|
||||||
|
|
||||||
// ====================== 单例与实例化函数区 ======================
|
|
||||||
CallLogFragment() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static CallLogFragment newInstance(int page) {
|
|
||||||
LogUtils.d(TAG, "newInstance: 创建通话记录Fragment实例,页码=" + page);
|
|
||||||
Bundle args = new Bundle();
|
|
||||||
args.putInt(ARG_PAGE, page);
|
|
||||||
CallLogFragment fragment = new CallLogFragment();
|
|
||||||
fragment.setArguments(args);
|
|
||||||
_CallLogFragment = fragment;
|
|
||||||
return fragment;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 生命周期函数区 ======================
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
LogUtils.d(TAG, "onCreate: Fragment创建开始");
|
|
||||||
if (getArguments() != null) {
|
|
||||||
mPage = getArguments().getInt(ARG_PAGE);
|
|
||||||
LogUtils.d(TAG, "onCreate: 读取页面参数,mPage=" + mPage);
|
|
||||||
}
|
|
||||||
// Java7 兼容:移除Lambda,使用匿名内部类初始化Handler
|
|
||||||
mHandler = new Handler(Looper.getMainLooper()) {
|
|
||||||
@Override
|
|
||||||
public void handleMessage(Message msg) {
|
|
||||||
if (msg.what == MSG_UPDATE) {
|
|
||||||
LogUtils.d(TAG, "handleMessage: 收到更新消息,开始读取通话记录");
|
|
||||||
readCallLog();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
LogUtils.d(TAG, "onCreate: Fragment创建完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
|
||||||
LogUtils.d(TAG, "onCreateView: 加载Fragment布局");
|
|
||||||
return inflater.inflate(R.layout.fragment_call_log, container, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
|
||||||
super.onViewCreated(view, savedInstanceState);
|
|
||||||
LogUtils.d(TAG, "onViewCreated: 视图创建完成,仅初始化控件(不加载数据)");
|
|
||||||
// 初始化RecyclerView(仅绑定控件、设置布局管理器,不设置数据/发起请求)
|
|
||||||
recyclerView = (RecyclerView) view.findViewById(R.id.recyclerView);
|
|
||||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
|
||||||
// 初始化适配器(传入空列表,后续懒加载时更新数据)
|
|
||||||
callLogAdapter = new CallLogAdapter(getContext(), callLogList);
|
|
||||||
recyclerView.setAdapter(callLogAdapter);
|
|
||||||
LogUtils.d(TAG, "onViewCreated: RecyclerView控件初始化完成(未加载数据)");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
LogUtils.d(TAG, "onResume: Fragment进入前台");
|
|
||||||
// 已初始化过数据 → 仅刷新(避免重复初始化,优化性能)
|
|
||||||
if (isDataInited && callLogAdapter != null) {
|
|
||||||
LogUtils.d(TAG, "onResume: 数据已初始化,仅刷新列表");
|
|
||||||
callLogAdapter.relaodContacts();
|
|
||||||
readCallLog(); // 刷新最新通话记录
|
|
||||||
LogUtils.d(TAG, "onResume: 通话记录数据刷新完成");
|
|
||||||
}
|
|
||||||
// 未初始化 → 不操作(等待MainActivity调用initData触发初始化)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
LogUtils.d(TAG, "onDestroy: Fragment开始销毁");
|
|
||||||
if (mHandler != null) {
|
|
||||||
mHandler.removeCallbacksAndMessages(null);
|
|
||||||
LogUtils.d(TAG, "onDestroy: Handler消息已清空");
|
|
||||||
}
|
|
||||||
// 释放资源,避免内存泄漏
|
|
||||||
if (callLogList != null) {
|
|
||||||
callLogList.clear();
|
|
||||||
callLogList = null;
|
|
||||||
}
|
|
||||||
callLogAdapter = null;
|
|
||||||
recyclerView = null;
|
|
||||||
_CallLogFragment = null;
|
|
||||||
isDataInited = false;
|
|
||||||
LogUtils.d(TAG, "onDestroy: Fragment销毁完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 权限回调函数区 ======================
|
|
||||||
@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_READ_CALL_LOG) {
|
|
||||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
LogUtils.d(TAG, "onRequestPermissionsResult: 通话记录权限授予成功,开始加载数据");
|
|
||||||
mHandler.sendEmptyMessage(MSG_UPDATE);
|
|
||||||
} else {
|
|
||||||
LogUtils.e(TAG, "onRequestPermissionsResult: 通话记录权限被拒绝,无法加载数据");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 懒加载核心方法(供MainActivity调用) ======================
|
|
||||||
public void initData() {
|
|
||||||
// 避免重复初始化(双重防护:标记+判断)
|
|
||||||
if (isDataInited || getContext() == null) {
|
|
||||||
LogUtils.d(TAG, "initData: 数据已初始化/上下文为空,跳过");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
LogUtils.d(TAG, "initData: 开始懒加载初始化通话记录数据");
|
|
||||||
// 权限检查与数据加载(原onViewCreated中的核心逻辑迁移至此)
|
|
||||||
if (ActivityCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_CALL_LOG) != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
LogUtils.w(TAG, "initData: 读取通话记录权限未授予,发起权限申请");
|
|
||||||
ActivityCompat.requestPermissions(requireActivity(), new String[]{Manifest.permission.READ_CALL_LOG}, REQUEST_READ_CALL_LOG);
|
|
||||||
} else {
|
|
||||||
LogUtils.d(TAG, "initData: 权限已授予,发送更新消息加载数据");
|
|
||||||
mHandler.sendEmptyMessage(MSG_UPDATE);
|
|
||||||
}
|
|
||||||
// 标记为已初始化(后续仅刷新,不重复初始化)
|
|
||||||
isDataInited = true;
|
|
||||||
LogUtils.d(TAG, "initData: 懒加载初始化流程完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 业务核心函数区 ======================
|
|
||||||
private void readCallLog() {
|
|
||||||
LogUtils.d(TAG, "readCallLog: 开始读取系统通话记录");
|
|
||||||
// 避免空指针(懒加载场景下,控件可能未初始化完成)
|
|
||||||
if (callLogList == null || callLogAdapter == null || getContext() == null) {
|
|
||||||
LogUtils.w(TAG, "readCallLog: 控件/列表为空,跳过读取");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
callLogList.clear();
|
|
||||||
Cursor cursor = null;
|
|
||||||
try {
|
|
||||||
cursor = requireContext().getContentResolver().query(
|
|
||||||
CallLog.Calls.CONTENT_URI,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
CallLog.Calls.DATE + " DESC"
|
|
||||||
);
|
|
||||||
if (cursor != null) {
|
|
||||||
LogUtils.d(TAG, "readCallLog: 成功获取通话记录游标,数据条数=" + cursor.getCount());
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
String phoneNumber = cursor.getString(cursor.getColumnIndex(CallLog.Calls.NUMBER));
|
|
||||||
int callType = cursor.getInt(cursor.getColumnIndex(CallLog.Calls.TYPE));
|
|
||||||
long callDateLong = cursor.getLong(cursor.getColumnIndex(CallLog.Calls.DATE));
|
|
||||||
Date callDate = new Date(callDateLong);
|
|
||||||
String callStatus = getCallStatus(callType);
|
|
||||||
|
|
||||||
callLogList.add(new CallLogModel(phoneNumber, callStatus, callDate));
|
|
||||||
}
|
|
||||||
callLogAdapter.notifyDataSetChanged();
|
|
||||||
LogUtils.d(TAG, "readCallLog: 通话记录数据解析完成,共" + callLogList.size() + "条");
|
|
||||||
} else {
|
|
||||||
LogUtils.w(TAG, "readCallLog: 通话记录游标为空");
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
LogUtils.e(TAG, "readCallLog: 读取通话记录异常", e);
|
|
||||||
} finally {
|
|
||||||
if (cursor != null) {
|
|
||||||
cursor.close();
|
|
||||||
LogUtils.d(TAG, "readCallLog: 游标已关闭");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getCallStatus(int callType) {
|
|
||||||
switch (callType) {
|
|
||||||
case CallLog.Calls.OUTGOING_TYPE:
|
|
||||||
return "Outgoing";
|
|
||||||
case CallLog.Calls.INCOMING_TYPE:
|
|
||||||
return "Incoming";
|
|
||||||
case CallLog.Calls.MISSED_TYPE:
|
|
||||||
return "Missed";
|
|
||||||
default:
|
|
||||||
return "Unknown";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 外部调用函数区 ======================
|
|
||||||
public void triggerUpdate() {
|
|
||||||
LogUtils.d(TAG, "triggerUpdate: 外部触发通话记录更新");
|
|
||||||
if (isDataInited) { // 已初始化才触发更新(避免未加载时调用)
|
|
||||||
mHandler.sendEmptyMessage(MSG_UPDATE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void updateCallLogFragment() {
|
|
||||||
if (_CallLogFragment != null) {
|
|
||||||
LogUtils.d(TAG, "updateCallLogFragment: 静态方法触发Fragment更新");
|
|
||||||
_CallLogFragment.triggerUpdate();
|
|
||||||
} else {
|
|
||||||
LogUtils.w(TAG, "updateCallLogFragment: Fragment实例为空,无法更新");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,401 +0,0 @@
|
|||||||
package cc.winboll.studio.contacts.fragments;
|
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.provider.ContactsContract;
|
|
||||||
import android.text.Editable;
|
|
||||||
import android.text.TextWatcher;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.core.app.ActivityCompat;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import cc.winboll.studio.contacts.R;
|
|
||||||
import cc.winboll.studio.contacts.adapters.ContactAdapter;
|
|
||||||
import cc.winboll.studio.contacts.model.ContactModel;
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
|
||||||
import cc.winboll.studio.libappbase.ToastUtils;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
|
||||||
* @Date 2025/08/30 14:32
|
|
||||||
* @Describe 联系人区域视图(支持懒加载,仅切换到当前页才加载数据)
|
|
||||||
*/
|
|
||||||
public class ContactsFragment extends Fragment {
|
|
||||||
|
|
||||||
// ====================== 常量定义区 ======================
|
|
||||||
public static final String TAG = "ContactsFragment";
|
|
||||||
private static final String ARG_PAGE = "ARG_PAGE";
|
|
||||||
private static final int REQUEST_READ_CONTACTS = 1;
|
|
||||||
private static final long DEBOUNCE_DELAY = 300; // 搜索防抖延迟
|
|
||||||
|
|
||||||
// ====================== 静态缓存区 ======================
|
|
||||||
// 全局复用联系人数据,减少重复查询
|
|
||||||
private static List<ContactModel> sCachedOriginalList = new ArrayList<ContactModel>();
|
|
||||||
private static List<ContactModel> sCachedFilteredList = new ArrayList<ContactModel>();
|
|
||||||
|
|
||||||
// ====================== 页面参数区 ======================
|
|
||||||
private int mPage;
|
|
||||||
private boolean isViewInitialized = false; // 视图初始化标记(控件绑定完成)
|
|
||||||
private boolean isDataLoaded = false; // 数据加载标记(数据+功能初始化完成)
|
|
||||||
private boolean isLazyInitCompleted = false; // 懒加载总标记(供MainActivity判断)
|
|
||||||
|
|
||||||
// ====================== UI控件区 ======================
|
|
||||||
private RecyclerView recyclerView;
|
|
||||||
private ContactAdapter contactAdapter;
|
|
||||||
private EditText searchEditText;
|
|
||||||
private Button btnDial;
|
|
||||||
|
|
||||||
// ====================== 数据容器区 ======================
|
|
||||||
private List<ContactModel> contactList = new ArrayList<ContactModel>();
|
|
||||||
private List<ContactModel> originalContactList = new ArrayList<ContactModel>();
|
|
||||||
|
|
||||||
// ====================== 异步工具区 ======================
|
|
||||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
|
||||||
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
|
||||||
|
|
||||||
// ====================== 实例化函数区 ======================
|
|
||||||
public static ContactsFragment newInstance(int page) {
|
|
||||||
LogUtils.d(TAG, "newInstance: 创建联系人Fragment实例,页码=" + page);
|
|
||||||
Bundle args = new Bundle();
|
|
||||||
args.putInt(ARG_PAGE, page);
|
|
||||||
ContactsFragment fragment = new ContactsFragment();
|
|
||||||
fragment.setArguments(args);
|
|
||||||
return fragment;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 生命周期函数区 ======================
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
LogUtils.d(TAG, "onCreate: Fragment创建开始");
|
|
||||||
if (getArguments() != null) {
|
|
||||||
mPage = getArguments().getInt(ARG_PAGE);
|
|
||||||
LogUtils.d(TAG, "onCreate: 读取页面参数,mPage=" + mPage);
|
|
||||||
}
|
|
||||||
LogUtils.d(TAG, "onCreate: Fragment创建完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
|
||||||
LogUtils.d(TAG, "onCreateView: 加载Fragment布局");
|
|
||||||
return inflater.inflate(R.layout.fragment_contacts, container, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
|
||||||
super.onViewCreated(view, savedInstanceState);
|
|
||||||
LogUtils.d(TAG, "onViewCreated: 开始初始化UI控件(仅绑定,不加载数据/功能)");
|
|
||||||
// 初始化RecyclerView(仅绑定控件、设适配器,隐藏列表)
|
|
||||||
recyclerView = (RecyclerView) view.findViewById(R.id.contacts_recycler_view);
|
|
||||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
|
||||||
contactAdapter = new ContactAdapter(getActivity(), contactList);
|
|
||||||
recyclerView.setAdapter(contactAdapter);
|
|
||||||
recyclerView.setVisibility(View.GONE);
|
|
||||||
|
|
||||||
// 绑定搜索框和拨号按钮(仅赋值,不显示、不绑定事件)
|
|
||||||
searchEditText = (EditText) view.findViewById(R.id.search_edit_text);
|
|
||||||
btnDial = (Button) view.findViewById(R.id.btn_dial);
|
|
||||||
searchEditText.setVisibility(View.GONE);
|
|
||||||
btnDial.setVisibility(View.GONE);
|
|
||||||
|
|
||||||
// 标记视图控件绑定完成
|
|
||||||
isViewInitialized = true;
|
|
||||||
LogUtils.d(TAG, "onViewCreated: UI控件初始化完成(未加载数据/功能)");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
LogUtils.d(TAG, "onResume: Fragment进入前台");
|
|
||||||
// 已完成懒加载 → 仅恢复缓存数据(切回页面时刷新)
|
|
||||||
if (isLazyInitCompleted && isDataLoaded) {
|
|
||||||
LogUtils.d(TAG, "onResume: 懒加载已完成,恢复缓存数据");
|
|
||||||
contactList.clear();
|
|
||||||
contactList.addAll(sCachedFilteredList);
|
|
||||||
contactAdapter.notifyDataSetChanged();
|
|
||||||
recyclerView.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
// 未完成懒加载 → 不操作(等待MainActivity调用initData触发)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
LogUtils.d(TAG, "onDestroy: Fragment开始销毁");
|
|
||||||
executor.shutdown(); // 关闭线程池
|
|
||||||
mainHandler.removeCallbacksAndMessages(null); // 清空Handler任务
|
|
||||||
// 释放本地数据引用(保留静态缓存,全局复用)
|
|
||||||
if (contactList != null) {
|
|
||||||
contactList.clear();
|
|
||||||
contactList = null;
|
|
||||||
}
|
|
||||||
if (originalContactList != null) {
|
|
||||||
originalContactList.clear();
|
|
||||||
originalContactList = null;
|
|
||||||
}
|
|
||||||
// 重置标记
|
|
||||||
isViewInitialized = false;
|
|
||||||
isDataLoaded = false;
|
|
||||||
isLazyInitCompleted = false;
|
|
||||||
LogUtils.d(TAG, "onDestroy: 异步工具+本地资源已释放");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onHiddenChanged(boolean hidden) {
|
|
||||||
super.onHiddenChanged(hidden);
|
|
||||||
LogUtils.d(TAG, "onHiddenChanged: Fragment隐藏状态变更,hidden=" + hidden);
|
|
||||||
// 已完成懒加载+显示状态 → 恢复缓存数据(兼容Tab切换场景)
|
|
||||||
if (!hidden && isLazyInitCompleted && isDataLoaded) {
|
|
||||||
contactList.clear();
|
|
||||||
contactList.addAll(sCachedFilteredList);
|
|
||||||
contactAdapter.notifyDataSetChanged();
|
|
||||||
recyclerView.setVisibility(View.VISIBLE);
|
|
||||||
LogUtils.d(TAG, "onHiddenChanged: 恢复缓存数据,列表已显示");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 权限相关函数区 ======================
|
|
||||||
private void checkContactPermission() {
|
|
||||||
LogUtils.d(TAG, "checkContactPermission: 检查联系人读取权限");
|
|
||||||
if (ActivityCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
LogUtils.w(TAG, "checkContactPermission: 权限未授予,发起申请");
|
|
||||||
ActivityCompat.requestPermissions(requireActivity(), new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_READ_CONTACTS);
|
|
||||||
} else {
|
|
||||||
LogUtils.d(TAG, "checkContactPermission: 权限已授予,开始加载数据");
|
|
||||||
loadContacts();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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_READ_CONTACTS) {
|
|
||||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
LogUtils.d(TAG, "onRequestPermissionsResult: 联系人权限授予成功");
|
|
||||||
loadContacts();
|
|
||||||
} else {
|
|
||||||
LogUtils.e(TAG, "onRequestPermissionsResult: 联系人权限被拒绝");
|
|
||||||
ToastUtils.show("请授予联系人权限以查看联系人列表");
|
|
||||||
recyclerView.setVisibility(View.VISIBLE);
|
|
||||||
// 权限拒绝也标记懒加载完成(避免重复触发)
|
|
||||||
isLazyInitCompleted = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 懒加载核心方法(供MainActivity调用) ======================
|
|
||||||
public void initData() {
|
|
||||||
// 双重防护:避免重复初始化(标记+视图就绪判断)
|
|
||||||
if (isLazyInitCompleted || !isViewInitialized || getContext() == null) {
|
|
||||||
LogUtils.d(TAG, "initData: 懒加载已完成/视图未就绪,跳过");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
LogUtils.d(TAG, "initData: 开始懒加载初始化(功能+数据)");
|
|
||||||
// 1. 初始化搜索、拨号功能(原onResume首次进入逻辑迁移至此)
|
|
||||||
initSearchAndDial();
|
|
||||||
// 2. 检查权限+加载数据(原onResume首次进入逻辑迁移至此)
|
|
||||||
checkContactPermission();
|
|
||||||
// 标记懒加载总流程完成(无论权限是否授予,仅执行一次)
|
|
||||||
isLazyInitCompleted = true;
|
|
||||||
LogUtils.d(TAG, "initData: 懒加载初始化流程启动完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== UI功能初始化区 ======================
|
|
||||||
private void initSearchAndDial() {
|
|
||||||
LogUtils.d(TAG, "initSearchAndDial: 初始化搜索和拨号功能");
|
|
||||||
// 显示控件
|
|
||||||
searchEditText.setVisibility(View.VISIBLE);
|
|
||||||
btnDial.setVisibility(View.VISIBLE);
|
|
||||||
|
|
||||||
// 搜索防抖监听
|
|
||||||
searchEditText.addTextChangedListener(new DebounceTextWatcher(DEBOUNCE_DELAY) {
|
|
||||||
@Override
|
|
||||||
public void onDebounceTextChanged(String query) {
|
|
||||||
filterContacts(query);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 拨号按钮点击事件
|
|
||||||
btnDial.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
String phoneNumber = searchEditText.getText().toString().replaceAll("\\s", "");
|
|
||||||
if (phoneNumber.isEmpty()) {
|
|
||||||
ToastUtils.show("请输入号码");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
LogUtils.d(TAG, "initSearchAndDial: 发起拨号,号码=" + phoneNumber);
|
|
||||||
Intent intent = new Intent(Intent.ACTION_CALL);
|
|
||||||
intent.setData(android.net.Uri.parse("tel:" + phoneNumber));
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
LogUtils.d(TAG, "initSearchAndDial: 功能初始化完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 数据加载与处理区 ======================
|
|
||||||
private void loadContacts() {
|
|
||||||
// 优先使用缓存数据(保留原有缓存逻辑,提升性能)
|
|
||||||
if (!sCachedOriginalList.isEmpty() && !sCachedFilteredList.isEmpty()) {
|
|
||||||
LogUtils.d(TAG, "loadContacts: 存在缓存数据,直接复用");
|
|
||||||
originalContactList.clear();
|
|
||||||
originalContactList.addAll(sCachedOriginalList);
|
|
||||||
contactList.clear();
|
|
||||||
contactList.addAll(sCachedFilteredList);
|
|
||||||
contactAdapter.notifyDataSetChanged();
|
|
||||||
recyclerView.setVisibility(View.VISIBLE);
|
|
||||||
isDataLoaded = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 无缓存时异步加载(保留原有异步逻辑,避免主线程阻塞)
|
|
||||||
if (!isDataLoaded) {
|
|
||||||
LogUtils.d(TAG, "loadContacts: 无缓存,异步读取联系人数据");
|
|
||||||
recyclerView.setVisibility(View.GONE);
|
|
||||||
executor.execute(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
final List<ContactModel> tempList = readContactsInBackground();
|
|
||||||
// 主线程更新UI和缓存
|
|
||||||
mainHandler.post(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
sCachedOriginalList.clear();
|
|
||||||
sCachedOriginalList.addAll(tempList);
|
|
||||||
sCachedFilteredList.clear();
|
|
||||||
sCachedFilteredList.addAll(tempList);
|
|
||||||
|
|
||||||
originalContactList.clear();
|
|
||||||
originalContactList.addAll(sCachedOriginalList);
|
|
||||||
contactList.clear();
|
|
||||||
contactList.addAll(sCachedFilteredList);
|
|
||||||
|
|
||||||
contactAdapter.notifyDataSetChanged();
|
|
||||||
recyclerView.setVisibility(View.VISIBLE);
|
|
||||||
isDataLoaded = true;
|
|
||||||
|
|
||||||
LogUtils.d(TAG, "loadContacts: 联系人数据加载完成,共" + contactList.size() + "条");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<ContactModel> readContactsInBackground() {
|
|
||||||
LogUtils.d(TAG, "readContactsInBackground: 子线程读取联系人");
|
|
||||||
List<ContactModel> tempList = new ArrayList<ContactModel>();
|
|
||||||
Cursor cursor = null;
|
|
||||||
try {
|
|
||||||
cursor = requireContext().getContentResolver().query(
|
|
||||||
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
|
|
||||||
new String[]{
|
|
||||||
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
|
|
||||||
ContactsContract.CommonDataKinds.Phone.NUMBER
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME + " ASC"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (cursor != null && cursor.moveToFirst()) {
|
|
||||||
int nameIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME);
|
|
||||||
int numberIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
|
|
||||||
do {
|
|
||||||
String name = cursor.getString(nameIndex);
|
|
||||||
String number = cursor.getString(numberIndex).replaceAll("\\s", "");
|
|
||||||
tempList.add(new ContactModel(name, number));
|
|
||||||
} while (cursor.moveToNext());
|
|
||||||
LogUtils.d(TAG, "readContactsInBackground: 成功读取" + tempList.size() + "条联系人数据");
|
|
||||||
} else {
|
|
||||||
LogUtils.w(TAG, "readContactsInBackground: 未读取到联系人数据");
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
LogUtils.e(TAG, "readContactsInBackground: 读取联系人异常", e);
|
|
||||||
} finally {
|
|
||||||
if (cursor != null) {
|
|
||||||
cursor.close();
|
|
||||||
LogUtils.d(TAG, "readContactsInBackground: 游标已关闭");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tempList;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void filterContacts(String query) {
|
|
||||||
LogUtils.d(TAG, "filterContacts: 搜索过滤,关键词=" + query);
|
|
||||||
contactList.clear();
|
|
||||||
sCachedFilteredList.clear();
|
|
||||||
if (query.isEmpty()) {
|
|
||||||
contactList.addAll(originalContactList);
|
|
||||||
sCachedFilteredList.addAll(originalContactList);
|
|
||||||
} else {
|
|
||||||
String lowerQuery = query.toLowerCase();
|
|
||||||
for (ContactModel contact : originalContactList) {
|
|
||||||
boolean matchName = contact.getName().toLowerCase().contains(lowerQuery);
|
|
||||||
boolean matchPinyin = contact.getPinyin().toLowerCase().contains(lowerQuery);
|
|
||||||
boolean matchFirstLetter = contact.getPinyinFirstLetter().toLowerCase().contains(lowerQuery);
|
|
||||||
boolean matchNumber = contact.getNumber().contains(lowerQuery);
|
|
||||||
if (matchName || matchPinyin || matchFirstLetter || matchNumber) {
|
|
||||||
contactList.add(contact);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sCachedFilteredList.addAll(contactList);
|
|
||||||
}
|
|
||||||
contactAdapter.notifyDataSetChanged();
|
|
||||||
recyclerView.setVisibility(View.VISIBLE);
|
|
||||||
LogUtils.d(TAG, "filterContacts: 过滤完成,显示" + contactList.size() + "条数据");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 内部防抖监听类 ======================
|
|
||||||
public abstract static class DebounceTextWatcher implements TextWatcher {
|
|
||||||
private final long debounceDelay;
|
|
||||||
private Handler handler = new Handler(Looper.getMainLooper());
|
|
||||||
private Runnable pendingRunnable;
|
|
||||||
|
|
||||||
public DebounceTextWatcher(long debounceDelay) {
|
|
||||||
this.debounceDelay = debounceDelay;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTextChanged(final CharSequence s, int start, int before, int count) {
|
|
||||||
if (pendingRunnable != null) {
|
|
||||||
handler.removeCallbacks(pendingRunnable);
|
|
||||||
}
|
|
||||||
pendingRunnable = new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
onDebounceTextChanged(s.toString());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
handler.postDelayed(pendingRunnable, debounceDelay);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void afterTextChanged(Editable s) {}
|
|
||||||
|
|
||||||
public abstract void onDebounceTextChanged(String query);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
package cc.winboll.studio.contacts.fragments;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import cc.winboll.studio.contacts.R;
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
|
||||||
import cc.winboll.studio.libappbase.LogView;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
|
||||||
* @Date 2025/02/20 12:58:15
|
|
||||||
* @Describe 应用日志区域视图(支持懒加载,仅切换到当前页才启动日志)
|
|
||||||
*/
|
|
||||||
public class LogFragment extends Fragment {
|
|
||||||
|
|
||||||
// ====================== 常量定义区 ======================
|
|
||||||
public static final String TAG = "LogFragment";
|
|
||||||
private static final String ARG_PAGE = "ARG_PAGE";
|
|
||||||
|
|
||||||
// ====================== 页面参数区 ======================
|
|
||||||
private int mPage;
|
|
||||||
|
|
||||||
// ====================== UI控件区 ======================
|
|
||||||
private LogView mLogView;
|
|
||||||
|
|
||||||
// ====================== 懒加载标记区 ======================
|
|
||||||
private boolean isViewInitialized = false; // 视图控件绑定完成标记
|
|
||||||
private boolean isLazyInitCompleted = false; // 懒加载总流程完成标记
|
|
||||||
private boolean isLogViewStarted = false; // LogView启动状态标记
|
|
||||||
|
|
||||||
// ====================== 实例化函数区 ======================
|
|
||||||
public static LogFragment newInstance(int page) {
|
|
||||||
LogUtils.d(TAG, "newInstance: 创建日志Fragment实例,页码=" + page);
|
|
||||||
Bundle args = new Bundle();
|
|
||||||
args.putInt(ARG_PAGE, page);
|
|
||||||
LogFragment fragment = new LogFragment();
|
|
||||||
fragment.setArguments(args);
|
|
||||||
return fragment;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 生命周期函数区 ======================
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
LogUtils.d(TAG, "onCreate: Fragment创建开始");
|
|
||||||
if (getArguments() != null) {
|
|
||||||
mPage = getArguments().getInt(ARG_PAGE);
|
|
||||||
LogUtils.d(TAG, "onCreate: 读取页面参数,mPage=" + mPage);
|
|
||||||
}
|
|
||||||
LogUtils.d(TAG, "onCreate: Fragment创建完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
|
||||||
@Nullable Bundle savedInstanceState) {
|
|
||||||
LogUtils.d(TAG, "onCreateView: 加载Fragment布局");
|
|
||||||
View view = inflater.inflate(R.layout.fragment_log, container, false);
|
|
||||||
// Java7 适配:添加强制类型转换,仅初始化LogView控件(不启动)
|
|
||||||
mLogView = (LogView) view.findViewById(R.id.logview);
|
|
||||||
LogUtils.d(TAG, "onCreateView: LogView控件初始化完成(未启动)");
|
|
||||||
// 标记视图控件绑定完成
|
|
||||||
isViewInitialized = true;
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
LogUtils.d(TAG, "onResume: Fragment进入前台");
|
|
||||||
// 已完成懒加载 → 仅重启LogView(切回页面时恢复日志显示)
|
|
||||||
if (isLazyInitCompleted && mLogView != null && !isLogViewStarted) {
|
|
||||||
mLogView.start();
|
|
||||||
isLogViewStarted = true;
|
|
||||||
LogUtils.d(TAG, "onResume: LogView已重启,恢复日志显示");
|
|
||||||
}
|
|
||||||
// 未完成懒加载 → 不操作(等待MainActivity调用initData触发)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
LogUtils.d(TAG, "onDestroy: Fragment开始销毁");
|
|
||||||
if (mLogView != null) {
|
|
||||||
// 若LogView有停止方法,必须调用(避免后台持续占用资源,根据实际API调整)
|
|
||||||
// mLogView.stop(); // 关键:释放LogView资源,防止内存泄漏
|
|
||||||
LogUtils.d(TAG, "onDestroy: LogView资源已释放");
|
|
||||||
}
|
|
||||||
// 重置所有标记,避免重建时状态异常
|
|
||||||
mLogView = null;
|
|
||||||
isViewInitialized = false;
|
|
||||||
isLazyInitCompleted = false;
|
|
||||||
isLogViewStarted = false;
|
|
||||||
LogUtils.d(TAG, "onDestroy: Fragment销毁完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 懒加载核心方法(供MainActivity调用) ======================
|
|
||||||
public void initData() {
|
|
||||||
// 双重防护:避免重复初始化(标记+视图就绪+控件非空)
|
|
||||||
if (isLazyInitCompleted || !isViewInitialized || mLogView == null || getContext() == null) {
|
|
||||||
LogUtils.d(TAG, "initData: 懒加载已完成/视图未就绪,跳过");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
LogUtils.d(TAG, "initData: 开始懒加载初始化,启动LogView");
|
|
||||||
// 核心:启动LogView(原onCreateView中的start逻辑迁移至此)
|
|
||||||
mLogView.start();
|
|
||||||
isLogViewStarted = true;
|
|
||||||
// 标记懒加载总流程完成(仅执行一次)
|
|
||||||
isLazyInitCompleted = true;
|
|
||||||
LogUtils.d(TAG, "initData: 懒加载初始化完成,LogView正常启动");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package cc.winboll.studio.contacts.handlers;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
|
||||||
* @Date 2025/02/14 03:51:40
|
|
||||||
*/
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Message;
|
|
||||||
import cc.winboll.studio.contacts.services.MainService;
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
|
|
||||||
public class MainServiceHandler extends Handler {
|
|
||||||
public static final String TAG = "MainServiceHandler";
|
|
||||||
|
|
||||||
public static final int MSG_REMINDTHREAD = 0;
|
|
||||||
|
|
||||||
WeakReference<MainService> serviceWeakReference;
|
|
||||||
public MainServiceHandler(MainService service) {
|
|
||||||
serviceWeakReference = new WeakReference<MainService>(service);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleMessage(Message msg) {
|
|
||||||
switch (msg.what) {
|
|
||||||
case MSG_REMINDTHREAD: // 处理下载完成消息,更新UI
|
|
||||||
{
|
|
||||||
// 显示提醒消息
|
|
||||||
//
|
|
||||||
//LogUtils.d(TAG, "显示提醒消息");
|
|
||||||
MainService mainService = serviceWeakReference.get();
|
|
||||||
if (mainService != null) {
|
|
||||||
mainService.appenMessage((String)msg.obj);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,392 +0,0 @@
|
|||||||
package cc.winboll.studio.contacts.listenphonecall;
|
|
||||||
|
|
||||||
import android.app.Service;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.ActivityInfo;
|
|
||||||
import android.graphics.PixelFormat;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.telephony.PhoneStateListener;
|
|
||||||
import android.telephony.TelephonyManager;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.view.Gravity;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.View.OnClickListener;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import cc.winboll.studio.contacts.R;
|
|
||||||
import cc.winboll.studio.contacts.model.MainServiceBean;
|
|
||||||
import cc.winboll.studio.contacts.phonecallui.PhoneCallActivity;
|
|
||||||
import cc.winboll.studio.contacts.phonecallui.PhoneCallService;
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
|
||||||
* @Describe 通话监听服务(无前台服务),负责监听通话状态、显示通话悬浮窗、跳转通话界面
|
|
||||||
* 严格适配 Java7 语法 + Android API29-30 | 轻量稳定 | 避免内存泄漏
|
|
||||||
*/
|
|
||||||
public class CallListenerService extends Service {
|
|
||||||
|
|
||||||
// ====================== 常量定义区(精准适配API29-30,无冗余) ======================
|
|
||||||
public static final String TAG = "CallListenerService";
|
|
||||||
|
|
||||||
// Android版本常量(仅保留适配必需版本,精简无用定义)
|
|
||||||
private static final int ANDROID_8_API = 26; // 悬浮窗类型适配(API26+必需)
|
|
||||||
private static final int ANDROID_10_API = 29; // API29+ 悬浮窗权限/参数适配
|
|
||||||
private static final int ANDROID_19_API = 19; // 透明状态栏/导航栏适配
|
|
||||||
|
|
||||||
// 延迟初始化参数(让出主线程,避免启动阻塞)
|
|
||||||
private static final long DELAY_INIT_MS = 100L;
|
|
||||||
|
|
||||||
// ====================== 成员属性区(按功能归类,命名规范) ======================
|
|
||||||
// 延迟初始化核心
|
|
||||||
private Handler mDelayHandler; // 延迟处理器(避免onCreate阻塞)
|
|
||||||
|
|
||||||
// 通话监听核心
|
|
||||||
private TelephonyManager mTelephonyManager; // 电话管理器(监听通话状态)
|
|
||||||
private PhoneStateListener mPhoneStateListener;// 通话状态监听回调
|
|
||||||
private String mCallNumber; // 当前通话号码
|
|
||||||
private boolean mIsCallingIn; // 是否为来电(true=来电,false=去电)
|
|
||||||
|
|
||||||
// 悬浮窗核心
|
|
||||||
private WindowManager mWindowManager; // 窗口管理器(添加/移除悬浮窗)
|
|
||||||
private WindowManager.LayoutParams mWindowParams;// 悬浮窗参数配置
|
|
||||||
private View mPhoneCallView; // 通话悬浮窗根视图
|
|
||||||
private TextView mTvCallNumber; // 悬浮窗号码显示控件
|
|
||||||
private Button mBtnOpenApp; // 悬浮窗跳转APP按钮
|
|
||||||
private boolean mHasShown; // 悬浮窗显示状态标记(避免重复操作)
|
|
||||||
|
|
||||||
// ====================== Service生命周期方法区(按执行顺序排列) ======================
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
LogUtils.d(TAG, "===== onCreate: 通话监听服务启动 =====");
|
|
||||||
|
|
||||||
// 延迟初始化所有逻辑(让出主线程,避免启动阻塞,提升启动速度)
|
|
||||||
initDelayHandlerAndLogic();
|
|
||||||
|
|
||||||
LogUtils.d(TAG, "===== onCreate: 通话监听服务启动完成 =====");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
|
||||||
LogUtils.d(TAG, "onStartCommand: 服务被启动,startId=" + startId);
|
|
||||||
|
|
||||||
// 加载服务配置,决定重启策略(启用则自动重启,禁用则默认)
|
|
||||||
MainServiceBean serviceConfig = MainServiceBean.loadBean(this, MainServiceBean.class);
|
|
||||||
int startMode = (serviceConfig != null && serviceConfig.isEnable()) ? START_STICKY : super.onStartCommand(intent, flags, startId);
|
|
||||||
LogUtils.d(TAG, "onStartCommand: 服务启动模式:" + (startMode == START_STICKY ? "START_STICKY(自动重启)" : "默认模式"));
|
|
||||||
|
|
||||||
return startMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public IBinder onBind(Intent intent) {
|
|
||||||
LogUtils.d(TAG, "onBind: 服务无需绑定,返回null");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
LogUtils.d(TAG, "===== onDestroy: 通话监听服务开始销毁 =====");
|
|
||||||
|
|
||||||
// 全量清理资源,彻底避免内存泄漏
|
|
||||||
dismissFloatWindow(); // 移除悬浮窗
|
|
||||||
unregisterPhoneStateListener();// 注销通话监听
|
|
||||||
clearDelayHandler(); // 清空延迟任务
|
|
||||||
resetAllReferences(); // 置空所有成员属性
|
|
||||||
|
|
||||||
LogUtils.d(TAG, "===== onDestroy: 通话监听服务销毁完成 =====");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 延迟初始化方法区(非阻塞启动,提升稳定性) ======================
|
|
||||||
/**
|
|
||||||
* 初始化延迟处理器,执行核心逻辑(通话监听+悬浮窗)
|
|
||||||
*/
|
|
||||||
private void initDelayHandlerAndLogic() {
|
|
||||||
mDelayHandler = new Handler(Looper.getMainLooper());
|
|
||||||
mDelayHandler.postDelayed(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
LogUtils.d(TAG, "initDelayHandlerAndLogic: 开始延迟初始化核心逻辑");
|
|
||||||
initPhoneStateListener(); // 初始化通话状态监听
|
|
||||||
initFloatWindow(); // 初始化通话悬浮窗
|
|
||||||
LogUtils.d(TAG, "initDelayHandlerAndLogic: 延迟初始化完成,服务就绪");
|
|
||||||
}
|
|
||||||
}, DELAY_INIT_MS);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化通话状态监听(注册TelephonyManager,响应通话状态变化)
|
|
||||||
*/
|
|
||||||
private void initPhoneStateListener() {
|
|
||||||
// 1. 创建通话状态监听回调
|
|
||||||
mPhoneStateListener = new PhoneStateListener() {
|
|
||||||
@Override
|
|
||||||
public void onCallStateChanged(int callState, String incomingNumber) {
|
|
||||||
super.onCallStateChanged(callState, incomingNumber);
|
|
||||||
mCallNumber = incomingNumber;
|
|
||||||
LogUtils.d(TAG, "onCallStateChanged: 通话状态变化,状态=" + getCallStateDesc(callState) + ",号码=" + incomingNumber);
|
|
||||||
|
|
||||||
// 响应不同通话状态
|
|
||||||
switch (callState) {
|
|
||||||
case TelephonyManager.CALL_STATE_IDLE:
|
|
||||||
// 通话空闲(挂断/未通话):隐藏悬浮窗
|
|
||||||
dismissFloatWindow();
|
|
||||||
break;
|
|
||||||
case TelephonyManager.CALL_STATE_RINGING:
|
|
||||||
// 来电响铃:标记来电状态,更新UI并显示悬浮窗
|
|
||||||
mIsCallingIn = true;
|
|
||||||
updateFloatWindowUI();
|
|
||||||
showFloatWindow();
|
|
||||||
break;
|
|
||||||
case TelephonyManager.CALL_STATE_OFFHOOK:
|
|
||||||
// 通话中(接听/拨号):更新UI并显示悬浮窗
|
|
||||||
updateFloatWindowUI();
|
|
||||||
showFloatWindow();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 2. 注册通话监听(非空校验,避免崩溃)
|
|
||||||
mTelephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
|
|
||||||
if (mTelephonyManager != null) {
|
|
||||||
mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
|
|
||||||
LogUtils.d(TAG, "initPhoneStateListener: 通话状态监听注册成功");
|
|
||||||
} else {
|
|
||||||
LogUtils.e(TAG, "initPhoneStateListener: TelephonyManager获取失败,监听注册失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化通话悬浮窗(配置参数+加载布局+绑定事件,适配API29-30)
|
|
||||||
*/
|
|
||||||
private void initFloatWindow() {
|
|
||||||
// 1. 获取窗口管理器(非空校验,避免后续崩溃)
|
|
||||||
mWindowManager = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
|
|
||||||
if (mWindowManager == null) {
|
|
||||||
LogUtils.e(TAG, "initFloatWindow: WindowManager获取失败,悬浮窗初始化失败");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 配置悬浮窗参数(精准适配API29+,兼容悬浮窗权限)
|
|
||||||
initFloatWindowParams();
|
|
||||||
|
|
||||||
// 3. 加载悬浮窗布局(添加返回键拦截,避免误关闭)
|
|
||||||
FrameLayout keyInterceptorLayout = new FrameLayout(this) {
|
|
||||||
@Override
|
|
||||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
|
||||||
// 拦截返回键,保障通话时悬浮窗正常显示
|
|
||||||
if (event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
|
||||||
LogUtils.d(TAG, "dispatchKeyEvent: 拦截悬浮窗返回键事件");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.dispatchKeyEvent(event);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
mPhoneCallView = LayoutInflater.from(this).inflate(R.layout.view_phone_call, keyInterceptorLayout);
|
|
||||||
|
|
||||||
// 4. 绑定悬浮窗控件,设置跳转按钮事件
|
|
||||||
bindFloatWindowViews();
|
|
||||||
|
|
||||||
LogUtils.d(TAG, "initFloatWindow: 悬浮窗初始化完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 配置悬浮窗参数(适配API29+窗口类型,确保正常显示)
|
|
||||||
*/
|
|
||||||
private void initFloatWindowParams() {
|
|
||||||
mWindowParams = new WindowManager.LayoutParams();
|
|
||||||
// 窗口位置:顶部居中
|
|
||||||
mWindowParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
|
|
||||||
// 窗口大小:宽度全屏,高度自适应
|
|
||||||
mWindowParams.width = WindowManager.LayoutParams.MATCH_PARENT;
|
|
||||||
mWindowParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
|
|
||||||
// 固定竖屏显示
|
|
||||||
mWindowParams.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
|
|
||||||
// 窗口格式:半透明
|
|
||||||
mWindowParams.format = PixelFormat.TRANSLUCENT;
|
|
||||||
|
|
||||||
// 窗口类型(API29+ 强制用 TYPE_APPLICATION_OVERLAY,需悬浮窗权限)
|
|
||||||
if (Build.VERSION.SDK_INT >= ANDROID_10_API) {
|
|
||||||
mWindowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
|
|
||||||
LogUtils.d(TAG, "initFloatWindowParams: API29+ 悬浮窗类型=TYPE_APPLICATION_OVERLAY(需开启悬浮窗权限)");
|
|
||||||
} else if (Build.VERSION.SDK_INT >= ANDROID_8_API) {
|
|
||||||
mWindowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
|
|
||||||
} else {
|
|
||||||
mWindowParams.type = WindowManager.LayoutParams.TYPE_PHONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 窗口标志:无焦点(不抢占输入)、全屏布局、兼容透明状态栏/导航栏
|
|
||||||
mWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
|
||||||
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
|
|
||||||
if (Build.VERSION.SDK_INT >= ANDROID_19_API) {
|
|
||||||
mWindowParams.flags |= WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
|
|
||||||
| WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绑定悬浮窗控件,设置跳转通话详情页事件
|
|
||||||
*/
|
|
||||||
private void bindFloatWindowViews() {
|
|
||||||
mTvCallNumber = (TextView) mPhoneCallView.findViewById(R.id.tv_call_number);
|
|
||||||
mBtnOpenApp = (Button) mPhoneCallView.findViewById(R.id.btn_open_app);
|
|
||||||
|
|
||||||
// 跳转按钮点击事件
|
|
||||||
mBtnOpenApp.setOnClickListener(new OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
if (TextUtils.isEmpty(mCallNumber)) {
|
|
||||||
LogUtils.w(TAG, "bindFloatWindowViews: 通话号码为空,跳过跳转");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
LogUtils.d(TAG, "bindFloatWindowViews: 点击跳转通话详情页,号码=" + mCallNumber);
|
|
||||||
PhoneCallService.CallType callType = mIsCallingIn ? PhoneCallService.CallType.CALL_IN : PhoneCallService.CallType.CALL_OUT;
|
|
||||||
PhoneCallActivity.actionStart(CallListenerService.this, mCallNumber, callType);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 悬浮窗功能逻辑区(显示/隐藏/更新UI) ======================
|
|
||||||
/**
|
|
||||||
* 显示通话悬浮窗(避免重复添加,防止窗口泄露)
|
|
||||||
*/
|
|
||||||
private void showFloatWindow() {
|
|
||||||
if (!mHasShown && mPhoneCallView != null && mWindowManager != null) {
|
|
||||||
try {
|
|
||||||
mWindowManager.addView(mPhoneCallView, mWindowParams);
|
|
||||||
mHasShown = true;
|
|
||||||
LogUtils.d(TAG, "showFloatWindow: 悬浮窗显示成功");
|
|
||||||
} catch (SecurityException e) {
|
|
||||||
LogUtils.e(TAG, "showFloatWindow: 悬浮窗显示失败(无悬浮窗权限,需引导用户开启)", e);
|
|
||||||
} catch (Exception e) {
|
|
||||||
LogUtils.e(TAG, "showFloatWindow: 悬浮窗显示异常", e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LogUtils.d(TAG, "showFloatWindow: 悬浮窗已显示/组件未初始化,跳过显示");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 隐藏通话悬浮窗(避免重复移除,防止崩溃)
|
|
||||||
*/
|
|
||||||
private void dismissFloatWindow() {
|
|
||||||
if (mHasShown && mPhoneCallView != null && mWindowManager != null) {
|
|
||||||
try {
|
|
||||||
mWindowManager.removeView(mPhoneCallView);
|
|
||||||
LogUtils.d(TAG, "dismissFloatWindow: 悬浮窗隐藏成功");
|
|
||||||
} catch (Exception e) {
|
|
||||||
LogUtils.e(TAG, "dismissFloatWindow: 悬浮窗隐藏异常", e);
|
|
||||||
} finally {
|
|
||||||
mHasShown = false;
|
|
||||||
mIsCallingIn = false; // 重置来电状态标记
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LogUtils.d(TAG, "dismissFloatWindow: 悬浮窗已隐藏/组件未初始化,跳过隐藏");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新悬浮窗UI(显示格式化号码+通话类型图标)
|
|
||||||
*/
|
|
||||||
private void updateFloatWindowUI() {
|
|
||||||
if (mTvCallNumber == null || TextUtils.isEmpty(mCallNumber)) {
|
|
||||||
LogUtils.w(TAG, "updateFloatWindowUI: 控件未初始化/号码为空,更新失败");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化11位手机号(3-4-4分隔,提升可读性)
|
|
||||||
String formattedNumber = formatPhoneNumber(mCallNumber);
|
|
||||||
mTvCallNumber.setText(formattedNumber);
|
|
||||||
|
|
||||||
// 设置通话类型图标(来电/去电区分)
|
|
||||||
int iconResId = mIsCallingIn ? R.drawable.ic_phone_call_in : R.drawable.ic_phone_call_out;
|
|
||||||
mTvCallNumber.setCompoundDrawablesWithIntrinsicBounds(
|
|
||||||
null, null, getResources().getDrawable(iconResId), null
|
|
||||||
);
|
|
||||||
LogUtils.d(TAG, "updateFloatWindowUI: 悬浮窗UI更新完成,号码=" + formattedNumber + ",类型=" + (mIsCallingIn ? "来电" : "去电"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 资源清理方法区(服务销毁时全量释放) ======================
|
|
||||||
/**
|
|
||||||
* 注销通话状态监听(释放TelephonyManager资源)
|
|
||||||
*/
|
|
||||||
private void unregisterPhoneStateListener() {
|
|
||||||
if (mTelephonyManager != null && mPhoneStateListener != null) {
|
|
||||||
mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
|
|
||||||
LogUtils.d(TAG, "unregisterPhoneStateListener: 通话监听已注销");
|
|
||||||
}
|
|
||||||
mTelephonyManager = null;
|
|
||||||
mPhoneStateListener = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清空延迟处理器(移除未执行任务,避免内存泄漏)
|
|
||||||
*/
|
|
||||||
private void clearDelayHandler() {
|
|
||||||
if (mDelayHandler != null) {
|
|
||||||
mDelayHandler.removeCallbacksAndMessages(null);
|
|
||||||
mDelayHandler = null;
|
|
||||||
LogUtils.d(TAG, "clearDelayHandler: 延迟处理器已清空");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 置空所有成员属性(彻底释放引用,避免内存泄漏)
|
|
||||||
*/
|
|
||||||
private void resetAllReferences() {
|
|
||||||
mCallNumber = null;
|
|
||||||
mPhoneCallView = null;
|
|
||||||
mWindowParams = null;
|
|
||||||
mWindowManager = null;
|
|
||||||
mTvCallNumber = null;
|
|
||||||
mBtnOpenApp = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 工具方法区(通用辅助功能,独立归类) ======================
|
|
||||||
/**
|
|
||||||
* 格式化手机号(11位手机号:3-4-4分隔,非11位保持原格式)
|
|
||||||
* @param phoneNum 待格式化的手机号
|
|
||||||
* @return 格式化后的号码
|
|
||||||
*/
|
|
||||||
public static String formatPhoneNumber(String phoneNum) {
|
|
||||||
if (!TextUtils.isEmpty(phoneNum) && phoneNum.length() == 11) {
|
|
||||||
String formatted = phoneNum.substring(0, 3) + "-"
|
|
||||||
+ phoneNum.substring(3, 7) + "-"
|
|
||||||
+ phoneNum.substring(7);
|
|
||||||
LogUtils.d(TAG, "formatPhoneNumber: 号码格式化,原=" + phoneNum + ",新=" + formatted);
|
|
||||||
return formatted;
|
|
||||||
}
|
|
||||||
LogUtils.d(TAG, "formatPhoneNumber: 非11位号码,无需格式化,号码=" + phoneNum);
|
|
||||||
return phoneNum;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 转换通话状态为文字描述(便于日志查看,快速定位问题)
|
|
||||||
* @param callState 通话状态(TelephonyManager常量)
|
|
||||||
* @return 状态描述文字
|
|
||||||
*/
|
|
||||||
private String getCallStateDesc(int callState) {
|
|
||||||
switch (callState) {
|
|
||||||
case TelephonyManager.CALL_STATE_IDLE:
|
|
||||||
return "空闲(挂断/未通话)";
|
|
||||||
case TelephonyManager.CALL_STATE_RINGING:
|
|
||||||
return "响铃(来电)";
|
|
||||||
case TelephonyManager.CALL_STATE_OFFHOOK:
|
|
||||||
return "通话中(接听/拨号)";
|
|
||||||
default:
|
|
||||||
return "未知状态";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
package cc.winboll.studio.contacts.model;
|
|
||||||
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
|
||||||
* @Date 2025/02/26 13:10:57
|
|
||||||
* @Describe 通话记录数据模型
|
|
||||||
*/
|
|
||||||
public class CallLogModel {
|
|
||||||
// ====================== 常量定义区 ======================
|
|
||||||
public static final String TAG = "CallLogModel";
|
|
||||||
|
|
||||||
// ====================== 成员变量区 ======================
|
|
||||||
private String phoneNumber;
|
|
||||||
private String callStatus;
|
|
||||||
private Date callDate;
|
|
||||||
|
|
||||||
// ====================== 构造函数区 ======================
|
|
||||||
public CallLogModel(String phoneNumber, String callStatus, Date callDate) {
|
|
||||||
// 去除号码中的空格并初始化
|
|
||||||
this.phoneNumber = phoneNumber.replaceAll("\\s", "");
|
|
||||||
this.callStatus = callStatus;
|
|
||||||
this.callDate = callDate;
|
|
||||||
|
|
||||||
LogUtils.d(TAG, "CallLogModel: 初始化通话记录模型 | 号码=" + this.phoneNumber
|
|
||||||
+ " | 状态=" + this.callStatus + " | 时间=" + this.callDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== Getter 方法区 ======================
|
|
||||||
public String getPhoneNumber() {
|
|
||||||
return phoneNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCallStatus() {
|
|
||||||
return callStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Date getCallDate() {
|
|
||||||
return callDate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
package cc.winboll.studio.contacts.model;
|
|
||||||
|
|
||||||
import net.sourceforge.pinyin4j.PinyinHelper;
|
|
||||||
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType;
|
|
||||||
import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat;
|
|
||||||
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType;
|
|
||||||
import net.sourceforge.pinyin4j.format.exception.BadHanyuPinyinOutputFormatCombination;
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
|
||||||
* @Date 2025/08/30 14:32
|
|
||||||
* @Describe 联系人信息数据模型,支持姓名转全拼和拼音首字母
|
|
||||||
*/
|
|
||||||
public class ContactModel {
|
|
||||||
// ====================== 常量定义区 ======================
|
|
||||||
public static final String TAG = "ContactModel";
|
|
||||||
// 汉字匹配正则常量,避免重复创建
|
|
||||||
private static final String CHINESE_CHAR_REGEX = "[\\u4e00-\\u9fa5]";
|
|
||||||
|
|
||||||
// ====================== 成员变量区 ======================
|
|
||||||
private String name;
|
|
||||||
private String number;
|
|
||||||
private String pinyin;
|
|
||||||
private String pinyinFirstLetter;
|
|
||||||
|
|
||||||
// ====================== 构造函数区 ======================
|
|
||||||
public ContactModel(String name, String number) {
|
|
||||||
LogUtils.d(TAG, "ContactModel: 开始初始化联系人模型");
|
|
||||||
this.name = name == null ? "" : name;
|
|
||||||
// 去除号码空格,空值处理为""
|
|
||||||
this.number = number == null ? "" : number.replaceAll("\\s", "");
|
|
||||||
// 初始化拼音和拼音首字母
|
|
||||||
this.pinyin = convertToPinyin(this.name);
|
|
||||||
this.pinyinFirstLetter = convertToPinyinFirstLetter(this.name);
|
|
||||||
|
|
||||||
LogUtils.d(TAG, "ContactModel: 联系人初始化完成 | 姓名=" + this.name
|
|
||||||
+ " | 号码=" + this.number + " | 全拼=" + this.pinyin
|
|
||||||
+ " | 拼音首字母=" + this.pinyinFirstLetter);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 拼音转换工具方法区 ======================
|
|
||||||
/**
|
|
||||||
* 姓名转为全拼(多音字默认取首个拼音)
|
|
||||||
*/
|
|
||||||
private String convertToPinyin(String chinese) {
|
|
||||||
LogUtils.d(TAG, "convertToPinyin: 开始转换姓名为全拼,姓名=" + chinese);
|
|
||||||
HanyuPinyinOutputFormat format = getPinyinOutputFormat();
|
|
||||||
StringBuilder pinyinSb = new StringBuilder();
|
|
||||||
|
|
||||||
for (int i = 0; i < chinese.length(); i++) {
|
|
||||||
char ch = chinese.charAt(i);
|
|
||||||
// 仅处理汉字
|
|
||||||
if (Character.toString(ch).matches(CHINESE_CHAR_REGEX)) {
|
|
||||||
try {
|
|
||||||
String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(ch, format);
|
|
||||||
if (pinyinArray != null && pinyinArray.length > 0) {
|
|
||||||
pinyinSb.append(pinyinArray[0]);
|
|
||||||
LogUtils.v(TAG, "convertToPinyin: 字符[" + ch + "]转为拼音[" + pinyinArray[0] + "]");
|
|
||||||
}
|
|
||||||
} catch (BadHanyuPinyinOutputFormatCombination e) {
|
|
||||||
LogUtils.e(TAG, "convertToPinyin: 拼音转换异常,字符=" + ch, e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pinyinSb.append(ch);
|
|
||||||
LogUtils.v(TAG, "convertToPinyin: 非汉字字符直接拼接,字符=" + ch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String result = pinyinSb.toString();
|
|
||||||
LogUtils.d(TAG, "convertToPinyin: 全拼转换完成,结果=" + result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 姓名转为拼音首字母(多音字默认取首个拼音首字母)
|
|
||||||
*/
|
|
||||||
private String convertToPinyinFirstLetter(String chinese) {
|
|
||||||
LogUtils.d(TAG, "convertToPinyinFirstLetter: 开始转换姓名为拼音首字母,姓名=" + chinese);
|
|
||||||
HanyuPinyinOutputFormat format = getPinyinOutputFormat();
|
|
||||||
StringBuilder firstLetterSb = new StringBuilder();
|
|
||||||
|
|
||||||
for (int i = 0; i < chinese.length(); i++) {
|
|
||||||
char ch = chinese.charAt(i);
|
|
||||||
if (Character.toString(ch).matches(CHINESE_CHAR_REGEX)) {
|
|
||||||
try {
|
|
||||||
String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(ch, format);
|
|
||||||
if (pinyinArray != null && pinyinArray.length > 0) {
|
|
||||||
char firstChar = pinyinArray[0].charAt(0);
|
|
||||||
firstLetterSb.append(firstChar);
|
|
||||||
LogUtils.v(TAG, "convertToPinyinFirstLetter: 字符[" + ch + "]转为首字母[" + firstChar + "]");
|
|
||||||
}
|
|
||||||
} catch (BadHanyuPinyinOutputFormatCombination e) {
|
|
||||||
LogUtils.e(TAG, "convertToPinyinFirstLetter: 拼音首字母转换异常,字符=" + ch, e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
firstLetterSb.append(ch);
|
|
||||||
LogUtils.v(TAG, "convertToPinyinFirstLetter: 非汉字字符直接拼接,字符=" + ch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String result = firstLetterSb.toString();
|
|
||||||
LogUtils.d(TAG, "convertToPinyinFirstLetter: 拼音首字母转换完成,结果=" + result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取统一的拼音输出格式(小写、无音调)
|
|
||||||
* 抽离为公共方法,避免重复创建对象
|
|
||||||
*/
|
|
||||||
private HanyuPinyinOutputFormat getPinyinOutputFormat() {
|
|
||||||
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
|
|
||||||
format.setCaseType(HanyuPinyinCaseType.LOWERCASE);
|
|
||||||
format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
|
|
||||||
return format;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== Getter 方法区 ======================
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getNumber() {
|
|
||||||
return number;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPinyin() {
|
|
||||||
return pinyin;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPinyinFirstLetter() {
|
|
||||||
return pinyinFirstLetter;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
package cc.winboll.studio.contacts.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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
|
||||||
* @Date 2025/02/13 07:06:13
|
|
||||||
* @Describe 主服务配置实体类,支持JSON序列化与反序列化
|
|
||||||
*/
|
|
||||||
public class MainServiceBean extends BaseBean {
|
|
||||||
|
|
||||||
// ====================== 常量定义区 ======================
|
|
||||||
public static final String TAG = "MainServiceBean";
|
|
||||||
private static final String JSON_KEY_IS_ENABLE = "isEnable";
|
|
||||||
|
|
||||||
// ====================== 成员变量区 ======================
|
|
||||||
private boolean isEnable;
|
|
||||||
|
|
||||||
// ====================== 构造函数区 ======================
|
|
||||||
public MainServiceBean() {
|
|
||||||
this.isEnable = false;
|
|
||||||
LogUtils.d(TAG, "MainServiceBean: 初始化实体类,默认状态为禁用");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== Getter & Setter 方法区 ======================
|
|
||||||
public void setIsEnable(boolean isEnable) {
|
|
||||||
LogUtils.d(TAG, "setIsEnable: 服务状态设置为" + isEnable);
|
|
||||||
this.isEnable = isEnable;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isEnable() {
|
|
||||||
return isEnable;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 重写 BaseBean 抽象方法区 ======================
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
String className = MainServiceBean.class.getName();
|
|
||||||
LogUtils.v(TAG, "getName: 获取类名=" + className);
|
|
||||||
return className;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
|
||||||
LogUtils.d(TAG, "writeThisToJsonWriter: 开始将实体类写入JSON");
|
|
||||||
super.writeThisToJsonWriter(jsonWriter);
|
|
||||||
// 写入服务启用状态字段
|
|
||||||
jsonWriter.name(JSON_KEY_IS_ENABLE).value(this.isEnable);
|
|
||||||
LogUtils.d(TAG, "writeThisToJsonWriter: JSON写入完成,isEnable=" + this.isEnable);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
|
||||||
// 优先调用父类方法处理通用字段
|
|
||||||
if (super.initObjectsFromJsonReader(jsonReader, name)) {
|
|
||||||
LogUtils.v(TAG, "initObjectsFromJsonReader: 父类已处理字段=" + name);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理当前类专属字段
|
|
||||||
if (JSON_KEY_IS_ENABLE.equals(name)) {
|
|
||||||
this.isEnable = jsonReader.nextBoolean();
|
|
||||||
LogUtils.d(TAG, "initObjectsFromJsonReader: 读取字段[" + name + "]值=" + this.isEnable);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
LogUtils.w(TAG, "initObjectsFromJsonReader: 未识别字段=" + name);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
|
||||||
LogUtils.d(TAG, "readBeanFromJsonReader: 开始从JSON读取实体类数据");
|
|
||||||
jsonReader.beginObject();
|
|
||||||
while (jsonReader.hasNext()) {
|
|
||||||
String name = jsonReader.nextName();
|
|
||||||
if (!initObjectsFromJsonReader(jsonReader, name)) {
|
|
||||||
LogUtils.w(TAG, "readBeanFromJsonReader: 跳过未识别字段=" + name);
|
|
||||||
jsonReader.skipValue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
jsonReader.endObject();
|
|
||||||
LogUtils.d(TAG, "readBeanFromJsonReader: JSON读取完成,当前实体状态=" + this.isEnable);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
package cc.winboll.studio.contacts.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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
|
||||||
* @Date 2025/02/21 09:52:10
|
|
||||||
* @Describe 电话黑名单规则实体类,支持JSON序列化与反序列化
|
|
||||||
*/
|
|
||||||
public class PhoneConnectRuleBean extends BaseBean {
|
|
||||||
// ====================== 常量定义区 ======================
|
|
||||||
public static final String TAG = "PhoneConnectRuleModel";
|
|
||||||
// JSON字段名常量,避免硬编码错误
|
|
||||||
private static final String JSON_KEY_RULE_TEXT = "ruleText";
|
|
||||||
private static final String JSON_KEY_ALLOW_CONNECTION = "isAllowConnection";
|
|
||||||
private static final String JSON_KEY_IS_ENABLE = "isEnable";
|
|
||||||
|
|
||||||
// ====================== 成员变量区 ======================
|
|
||||||
private String ruleText;
|
|
||||||
private boolean isAllowConnection;
|
|
||||||
private boolean isEnable;
|
|
||||||
private boolean isSimpleView;
|
|
||||||
|
|
||||||
// ====================== 构造函数区 ======================
|
|
||||||
/**
|
|
||||||
* 默认构造,初始化默认值
|
|
||||||
*/
|
|
||||||
public PhoneConnectRuleBean() {
|
|
||||||
this.ruleText = "";
|
|
||||||
this.isAllowConnection = false;
|
|
||||||
this.isEnable = false;
|
|
||||||
this.isSimpleView = true;
|
|
||||||
LogUtils.d(TAG, "PhoneConnectRuleModel: 默认构造初始化完成 | 规则文本空串,默认禁用状态");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 带参构造,初始化核心规则参数
|
|
||||||
*/
|
|
||||||
public PhoneConnectRuleBean(String ruleText, boolean isAllowConnection, boolean isEnable) {
|
|
||||||
this.ruleText = ruleText == null ? "" : ruleText;
|
|
||||||
this.isAllowConnection = isAllowConnection;
|
|
||||||
this.isEnable = isEnable;
|
|
||||||
this.isSimpleView = true;
|
|
||||||
LogUtils.d(TAG, "PhoneConnectRuleModel: 带参构造初始化完成 | 规则文本=" + this.ruleText
|
|
||||||
+ " | 允许连接=" + this.isAllowConnection + " | 规则启用=" + this.isEnable);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== Getter & Setter 方法区 ======================
|
|
||||||
public String getRuleText() {
|
|
||||||
return ruleText;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRuleText(String ruleText) {
|
|
||||||
String oldValue = this.ruleText;
|
|
||||||
this.ruleText = ruleText == null ? "" : ruleText;
|
|
||||||
LogUtils.d(TAG, "setRuleText: 规则文本更新 | 旧值=" + oldValue + " | 新值=" + this.ruleText);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isAllowConnection() {
|
|
||||||
return isAllowConnection;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setIsAllowConnection(boolean isAllowConnection) {
|
|
||||||
LogUtils.d(TAG, "setIsAllowConnection: 允许连接状态更新为" + isAllowConnection);
|
|
||||||
this.isAllowConnection = isAllowConnection;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isEnable() {
|
|
||||||
return isEnable;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setIsEnable(boolean isEnable) {
|
|
||||||
LogUtils.d(TAG, "setIsEnable: 规则启用状态更新为" + isEnable);
|
|
||||||
this.isEnable = isEnable;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isSimpleView() {
|
|
||||||
return isSimpleView;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setIsSimpleView(boolean isSimpleView) {
|
|
||||||
LogUtils.d(TAG, "setIsSimpleView: 视图模式更新 | 简洁模式=" + isSimpleView);
|
|
||||||
this.isSimpleView = isSimpleView;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 重写 BaseBean 抽象方法区 ======================
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
String className = PhoneConnectRuleBean.class.getName();
|
|
||||||
LogUtils.v(TAG, "getName: 获取当前类名=" + className);
|
|
||||||
return className;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
|
||||||
LogUtils.d(TAG, "writeThisToJsonWriter: 开始JSON序列化规则数据");
|
|
||||||
super.writeThisToJsonWriter(jsonWriter);
|
|
||||||
// 序列化核心字段
|
|
||||||
jsonWriter.name(JSON_KEY_RULE_TEXT).value(getRuleText());
|
|
||||||
jsonWriter.name(JSON_KEY_ALLOW_CONNECTION).value(isAllowConnection());
|
|
||||||
jsonWriter.name(JSON_KEY_IS_ENABLE).value(isEnable());
|
|
||||||
LogUtils.d(TAG, "writeThisToJsonWriter: JSON序列化完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
|
||||||
// 优先让父类处理通用字段
|
|
||||||
if (super.initObjectsFromJsonReader(jsonReader, name)) {
|
|
||||||
LogUtils.v(TAG, "initObjectsFromJsonReader: 父类已处理字段=" + name);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理当前类专属字段
|
|
||||||
if (JSON_KEY_RULE_TEXT.equals(name)) {
|
|
||||||
setRuleText(jsonReader.nextString());
|
|
||||||
} else if (JSON_KEY_ALLOW_CONNECTION.equals(name)) {
|
|
||||||
setIsAllowConnection(jsonReader.nextBoolean());
|
|
||||||
} else if (JSON_KEY_IS_ENABLE.equals(name)) {
|
|
||||||
setIsEnable(jsonReader.nextBoolean());
|
|
||||||
} else {
|
|
||||||
LogUtils.w(TAG, "initObjectsFromJsonReader: 未识别的JSON字段=" + name);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
LogUtils.v(TAG, "initObjectsFromJsonReader: 成功解析字段=" + name);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
|
||||||
LogUtils.d(TAG, "readBeanFromJsonReader: 开始从JSON解析规则数据");
|
|
||||||
jsonReader.beginObject();
|
|
||||||
while (jsonReader.hasNext()) {
|
|
||||||
String fieldName = jsonReader.nextName();
|
|
||||||
if (!initObjectsFromJsonReader(jsonReader, fieldName)) {
|
|
||||||
LogUtils.w(TAG, "readBeanFromJsonReader: 跳过未识别字段=" + fieldName);
|
|
||||||
jsonReader.skipValue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
jsonReader.endObject();
|
|
||||||
LogUtils.d(TAG, "readBeanFromJsonReader: JSON解析完成 | 解析后规则=" + getRuleText());
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
package cc.winboll.studio.contacts.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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
|
||||||
* @Date 2025/02/24 18:47:11
|
|
||||||
* @Describe 手机铃声设置参数类,支持JSON序列化与反序列化
|
|
||||||
*/
|
|
||||||
public class RingTongBean extends BaseBean {
|
|
||||||
|
|
||||||
// ====================== 常量定义区 ======================
|
|
||||||
public static final String TAG = "AudioRingTongBean";
|
|
||||||
private static final String JSON_KEY_STREAM_VOLUME = "streamVolume";
|
|
||||||
// 铃声音量范围常量(参考AudioManager标准)
|
|
||||||
private static final int VOLUME_MIN = 0;
|
|
||||||
private static final int VOLUME_MAX = 100;
|
|
||||||
|
|
||||||
// ====================== 成员变量区 ======================
|
|
||||||
private int streamVolume;
|
|
||||||
|
|
||||||
// ====================== 构造函数区 ======================
|
|
||||||
/**
|
|
||||||
* 默认构造,铃声音量初始化为最大值
|
|
||||||
*/
|
|
||||||
public RingTongBean() {
|
|
||||||
this.streamVolume = VOLUME_MAX;
|
|
||||||
LogUtils.d(TAG, "RingTongBean: 默认构造初始化 | 铃声音量=" + this.streamVolume);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 带参构造,初始化指定铃声音量
|
|
||||||
*/
|
|
||||||
public RingTongBean(int streamVolume) {
|
|
||||||
// 音量值范围校验,避免非法值
|
|
||||||
this.streamVolume = Math.max(VOLUME_MIN, Math.min(VOLUME_MAX, streamVolume));
|
|
||||||
LogUtils.d(TAG, "RingTongBean: 带参构造初始化 | 原始音量=" + streamVolume + " | 校正后=" + this.streamVolume);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== Getter & Setter 方法区 ======================
|
|
||||||
public int getStreamVolume() {
|
|
||||||
return streamVolume;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStreamVolume(int streamVolume) {
|
|
||||||
int oldVolume = this.streamVolume;
|
|
||||||
// 音量值范围校验
|
|
||||||
this.streamVolume = Math.max(VOLUME_MIN, Math.min(VOLUME_MAX, streamVolume));
|
|
||||||
LogUtils.d(TAG, "setStreamVolume: 铃声音量更新 | 旧值=" + oldVolume + " | 新值=" + this.streamVolume);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 重写 BaseBean 抽象方法区 ======================
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
String className = RingTongBean.class.getName();
|
|
||||||
LogUtils.v(TAG, "getName: 获取当前类名=" + className);
|
|
||||||
return className;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
|
||||||
LogUtils.d(TAG, "writeThisToJsonWriter: 开始JSON序列化铃声音量参数");
|
|
||||||
super.writeThisToJsonWriter(jsonWriter);
|
|
||||||
jsonWriter.name(JSON_KEY_STREAM_VOLUME).value(getStreamVolume());
|
|
||||||
LogUtils.d(TAG, "writeThisToJsonWriter: JSON序列化完成 | 音量值=" + getStreamVolume());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
|
||||||
// 优先调用父类处理通用字段
|
|
||||||
if (super.initObjectsFromJsonReader(jsonReader, name)) {
|
|
||||||
LogUtils.v(TAG, "initObjectsFromJsonReader: 父类已处理字段=" + name);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理当前类专属字段
|
|
||||||
if (JSON_KEY_STREAM_VOLUME.equals(name)) {
|
|
||||||
setStreamVolume(jsonReader.nextInt());
|
|
||||||
LogUtils.v(TAG, "initObjectsFromJsonReader: 解析字段[" + name + "]值=" + this.streamVolume);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
LogUtils.w(TAG, "initObjectsFromJsonReader: 未识别的JSON字段=" + name);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
|
||||||
LogUtils.d(TAG, "readBeanFromJsonReader: 开始从JSON解析铃声音量参数");
|
|
||||||
jsonReader.beginObject();
|
|
||||||
while (jsonReader.hasNext()) {
|
|
||||||
String fieldName = jsonReader.nextName();
|
|
||||||
if (!initObjectsFromJsonReader(jsonReader, fieldName)) {
|
|
||||||
LogUtils.w(TAG, "readBeanFromJsonReader: 跳过未识别字段=" + fieldName);
|
|
||||||
jsonReader.skipValue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
jsonReader.endObject();
|
|
||||||
LogUtils.d(TAG, "readBeanFromJsonReader: JSON解析完成 | 最终音量值=" + this.streamVolume);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
package cc.winboll.studio.contacts.model;
|
|
||||||
|
|
||||||
import android.util.JsonReader;
|
|
||||||
import android.util.JsonWriter;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import cc.winboll.studio.contacts.utils.IntUtils;
|
|
||||||
import cc.winboll.studio.libappbase.BaseBean;
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
|
||||||
* @Date 2025/03/02 19:51:40
|
|
||||||
* @Describe 应用设置数据模型,支持云盾防御配置与JSON序列化
|
|
||||||
*/
|
|
||||||
public class SettingsBean extends BaseBean {
|
|
||||||
|
|
||||||
// ====================== 常量定义区 ======================
|
|
||||||
public static final String TAG = "SettingsModel";
|
|
||||||
// 数值范围常量
|
|
||||||
public static final int MAX_INTRANGE = 666666;
|
|
||||||
public static final int MIN_INTRANGE = 1;
|
|
||||||
// JSON字段名常量,消除硬编码
|
|
||||||
private static final String JSON_KEY_DUN_TOTAL = "dunTotalCount";
|
|
||||||
private static final String JSON_KEY_DUN_CURRENT = "dunCurrentCount";
|
|
||||||
private static final String JSON_KEY_DUN_RESUME_SECOND = "dunResumeSecondCount";
|
|
||||||
private static final String JSON_KEY_DUN_RESUME_COUNT = "dunResumeCount";
|
|
||||||
private static final String JSON_KEY_DUN_ENABLE = "isEnableDun";
|
|
||||||
private static final String JSON_KEY_URL = "szBoBullToon_URL";
|
|
||||||
|
|
||||||
// ====================== 成员变量区 ======================
|
|
||||||
// 云盾防御层数量
|
|
||||||
private int dunTotalCount;
|
|
||||||
// 当前云盾防御层
|
|
||||||
private int dunCurrentCount;
|
|
||||||
// 防御层恢复时间间隔(秒钟)
|
|
||||||
private int dunResumeSecondCount;
|
|
||||||
// 每次恢复防御层数
|
|
||||||
private int dunResumeCount;
|
|
||||||
// 是否启用云盾
|
|
||||||
private boolean isEnableDun;
|
|
||||||
// BoBullToon 应用模块数据请求地址
|
|
||||||
private String szBoBullToon_URL;
|
|
||||||
|
|
||||||
// ====================== 构造函数区 ======================
|
|
||||||
/**
|
|
||||||
* 默认构造,初始化默认配置
|
|
||||||
*/
|
|
||||||
public SettingsBean() {
|
|
||||||
this.dunTotalCount = 6;
|
|
||||||
this.dunCurrentCount = 6;
|
|
||||||
this.dunResumeSecondCount = 60;
|
|
||||||
this.dunResumeCount = 1;
|
|
||||||
this.isEnableDun = false;
|
|
||||||
this.szBoBullToon_URL = "";
|
|
||||||
LogUtils.d(TAG, "SettingsModel: 默认构造初始化完成 | 云盾默认配置加载完毕");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 带参构造,初始化自定义配置并校验数值范围
|
|
||||||
*/
|
|
||||||
public SettingsBean(int dunTotalCount, int dunCurrentCount, int dunResumeSecondCount,
|
|
||||||
int dunResumeCount, boolean isEnableDun, String szBoBullToon_URL) {
|
|
||||||
this.dunTotalCount = getSettingsModelRangeInt(dunTotalCount);
|
|
||||||
this.dunCurrentCount = getSettingsModelRangeInt(dunCurrentCount);
|
|
||||||
this.dunResumeSecondCount = getSettingsModelRangeInt(dunResumeSecondCount);
|
|
||||||
this.dunResumeCount = getSettingsModelRangeInt(dunResumeCount);
|
|
||||||
this.isEnableDun = isEnableDun;
|
|
||||||
this.szBoBullToon_URL = szBoBullToon_URL == null ? "" : szBoBullToon_URL;
|
|
||||||
|
|
||||||
LogUtils.d(TAG, "SettingsModel: 带参构造初始化完成 | 总层数=" + this.dunTotalCount
|
|
||||||
+ " | 当前层数=" + this.dunCurrentCount + " | 恢复间隔=" + this.dunResumeSecondCount
|
|
||||||
+ " | 恢复层数=" + this.dunResumeCount + " | 云盾启用=" + this.isEnableDun);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 私有工具方法区 ======================
|
|
||||||
/**
|
|
||||||
* 数值范围校验,确保参数在 MIN~MAX 区间内
|
|
||||||
*/
|
|
||||||
private int getSettingsModelRangeInt(int origin) {
|
|
||||||
int result = IntUtils.getIntInRange(origin, MIN_INTRANGE, MAX_INTRANGE);
|
|
||||||
if (result != origin) {
|
|
||||||
LogUtils.w(TAG, "getSettingsModelRangeInt: 数值校正 | 原始值=" + origin + " | 校正后=" + result);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== Getter & Setter 方法区 ======================
|
|
||||||
public int getDunTotalCount() {
|
|
||||||
return dunTotalCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDunTotalCount(int dunTotalCount) {
|
|
||||||
int oldValue = this.dunTotalCount;
|
|
||||||
this.dunTotalCount = getSettingsModelRangeInt(dunTotalCount);
|
|
||||||
LogUtils.d(TAG, "setDunTotalCount: 总防御层数更新 | 旧值=" + oldValue + " | 新值=" + this.dunTotalCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getDunCurrentCount() {
|
|
||||||
return dunCurrentCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDunCurrentCount(int dunCurrentCount) {
|
|
||||||
int oldValue = this.dunCurrentCount;
|
|
||||||
this.dunCurrentCount = getSettingsModelRangeInt(dunCurrentCount);
|
|
||||||
LogUtils.d(TAG, "setDunCurrentCount: 当前防御层数更新 | 旧值=" + oldValue + " | 新值=" + this.dunCurrentCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getDunResumeSecondCount() {
|
|
||||||
return dunResumeSecondCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDunResumeSecondCount(int dunResumeSecondCount) {
|
|
||||||
int oldValue = this.dunResumeSecondCount;
|
|
||||||
this.dunResumeSecondCount = getSettingsModelRangeInt(dunResumeSecondCount);
|
|
||||||
LogUtils.d(TAG, "setDunResumeSecondCount: 恢复间隔更新 | 旧值=" + oldValue + " | 新值=" + this.dunResumeSecondCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getDunResumeCount() {
|
|
||||||
return dunResumeCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDunResumeCount(int dunResumeCount) {
|
|
||||||
int oldValue = this.dunResumeCount;
|
|
||||||
this.dunResumeCount = getSettingsModelRangeInt(dunResumeCount);
|
|
||||||
LogUtils.d(TAG, "setDunResumeCount: 恢复层数更新 | 旧值=" + oldValue + " | 新值=" + this.dunResumeCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isEnableDun() {
|
|
||||||
return isEnableDun;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setIsEnableDun(boolean isEnableDun) {
|
|
||||||
LogUtils.d(TAG, "setIsEnableDun: 云盾启用状态更新为" + isEnableDun);
|
|
||||||
this.isEnableDun = isEnableDun;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getBoBullToon_URL() {
|
|
||||||
return szBoBullToon_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setBoBullToon_URL(String boBullToon_URL) {
|
|
||||||
String oldValue = this.szBoBullToon_URL;
|
|
||||||
this.szBoBullToon_URL = boBullToon_URL == null ? "" : boBullToon_URL;
|
|
||||||
LogUtils.d(TAG, "setBoBullToon_URL: 请求地址更新 | 旧值=" + oldValue + " | 新值=" + this.szBoBullToon_URL);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 重写 BaseBean 抽象方法区 ======================
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
String className = SettingsBean.class.getName();
|
|
||||||
LogUtils.v(TAG, "getName: 获取当前类名=" + className);
|
|
||||||
return className;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
|
||||||
LogUtils.d(TAG, "writeThisToJsonWriter: 开始JSON序列化设置数据");
|
|
||||||
super.writeThisToJsonWriter(jsonWriter);
|
|
||||||
// 写入所有配置字段
|
|
||||||
jsonWriter.name(JSON_KEY_DUN_TOTAL).value(getDunTotalCount());
|
|
||||||
jsonWriter.name(JSON_KEY_DUN_CURRENT).value(getDunCurrentCount());
|
|
||||||
jsonWriter.name(JSON_KEY_DUN_RESUME_SECOND).value(getDunResumeSecondCount());
|
|
||||||
jsonWriter.name(JSON_KEY_DUN_RESUME_COUNT).value(getDunResumeCount());
|
|
||||||
jsonWriter.name(JSON_KEY_DUN_ENABLE).value(isEnableDun());
|
|
||||||
jsonWriter.name(JSON_KEY_URL).value(getBoBullToon_URL());
|
|
||||||
LogUtils.d(TAG, "writeThisToJsonWriter: JSON序列化完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
|
||||||
// 优先调用父类处理通用字段
|
|
||||||
if (super.initObjectsFromJsonReader(jsonReader, name)) {
|
|
||||||
LogUtils.v(TAG, "initObjectsFromJsonReader: 父类已处理字段=" + name);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理当前类专属配置字段
|
|
||||||
if (JSON_KEY_DUN_TOTAL.equals(name)) {
|
|
||||||
setDunTotalCount(getSettingsModelRangeInt(jsonReader.nextInt()));
|
|
||||||
} else if (JSON_KEY_DUN_CURRENT.equals(name)) {
|
|
||||||
setDunCurrentCount(getSettingsModelRangeInt(jsonReader.nextInt()));
|
|
||||||
} else if (JSON_KEY_DUN_RESUME_SECOND.equals(name)) {
|
|
||||||
setDunResumeSecondCount(getSettingsModelRangeInt(jsonReader.nextInt()));
|
|
||||||
} else if (JSON_KEY_DUN_RESUME_COUNT.equals(name)) {
|
|
||||||
setDunResumeCount(getSettingsModelRangeInt(jsonReader.nextInt()));
|
|
||||||
} else if (JSON_KEY_DUN_ENABLE.equals(name)) {
|
|
||||||
setIsEnableDun(jsonReader.nextBoolean());
|
|
||||||
} else if (JSON_KEY_URL.equals(name)) {
|
|
||||||
setBoBullToon_URL(jsonReader.nextString());
|
|
||||||
} else {
|
|
||||||
LogUtils.w(TAG, "initObjectsFromJsonReader: 未识别的JSON字段=" + name);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
LogUtils.v(TAG, "initObjectsFromJsonReader: 成功解析字段=" + name);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
|
||||||
LogUtils.d(TAG, "readBeanFromJsonReader: 开始从JSON解析设置数据");
|
|
||||||
jsonReader.beginObject();
|
|
||||||
while (jsonReader.hasNext()) {
|
|
||||||
String fieldName = jsonReader.nextName();
|
|
||||||
if (!initObjectsFromJsonReader(jsonReader, fieldName)) {
|
|
||||||
LogUtils.w(TAG, "readBeanFromJsonReader: 跳过未识别字段=" + fieldName);
|
|
||||||
jsonReader.skipValue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
jsonReader.endObject();
|
|
||||||
LogUtils.d(TAG, "readBeanFromJsonReader: JSON解析完成 | 云盾配置加载完毕");
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,362 +0,0 @@
|
|||||||
package cc.winboll.studio.contacts.phonecallui;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.os.Message;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.Window;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import cc.winboll.studio.contacts.ActivityStack;
|
|
||||||
import cc.winboll.studio.contacts.R;
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
|
||||||
import java.util.Timer;
|
|
||||||
import java.util.TimerTask;
|
|
||||||
|
|
||||||
import static cc.winboll.studio.contacts.listenphonecall.CallListenerService.formatPhoneNumber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author aJIEw, ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
|
||||||
* @Date 2025/12/14 21:01
|
|
||||||
* @Describe 接打电话界面(单例模式 + 适配API29 - 30 + 小米机型兼容性优化)
|
|
||||||
* 功能:单例通话窗口、来电/去电显示、通话计时、免提控制、锁屏显示
|
|
||||||
*/
|
|
||||||
public class PhoneCallActivity extends Activity implements View.OnClickListener {
|
|
||||||
// 常量定义区(核心常量+小米适配标识)
|
|
||||||
public static final String TAG = "PhoneCallActivity";
|
|
||||||
private static final int MSG_CLOSE_ACTIVITY = 0x001;
|
|
||||||
private static final String MI_ADAPT_TAG = "MiAdapt";
|
|
||||||
private static final String TOAST_CALLING = "通话进行中,无法重复创建通话窗口";
|
|
||||||
private static final long CLOSE_DELAY_MS = 100; // 小米机型关闭延迟时间
|
|
||||||
|
|
||||||
// 静态属性区(单例核心+全局工具对象)
|
|
||||||
private static volatile boolean sIsActivityAlive = false;
|
|
||||||
private static Handler sCloseHandler;
|
|
||||||
|
|
||||||
// 控件属性区(按界面布局顺序排列)
|
|
||||||
private TextView mTvCallNumberLabel;
|
|
||||||
private TextView mTvCallNumber;
|
|
||||||
private TextView mTvPickUp;
|
|
||||||
private TextView mTvCallingTime;
|
|
||||||
private TextView mTvHangUp;
|
|
||||||
|
|
||||||
// 业务属性区(按依赖优先级排列)
|
|
||||||
private PhoneCallManager mPhoneCallManager;
|
|
||||||
private PhoneCallService.CallType mCallType;
|
|
||||||
private String mPhoneNumber;
|
|
||||||
private Timer mOnGoingCallTimer;
|
|
||||||
private int mCallingTime;
|
|
||||||
private boolean isClosing = false; // 新增:避免重复关闭页面
|
|
||||||
|
|
||||||
// 对外静态接口(单例启动+外部关闭)
|
|
||||||
public static void actionStart(Context context, String phoneNumber, PhoneCallService.CallType callType) {
|
|
||||||
if (context == null || phoneNumber == null || callType == null) {
|
|
||||||
LogUtils.e(TAG, "actionStart: 入参为空,启动失败");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sIsActivityAlive) {
|
|
||||||
LogUtils.w(TAG, MI_ADAPT_TAG + " 已有活跃通话窗口,拒绝重复启动");
|
|
||||||
Toast.makeText(context, TOAST_CALLING, Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 启动通话界面,号码=" + phoneNumber + ",类型=" + callType.name());
|
|
||||||
Intent intent = new Intent(context, PhoneCallActivity.class);
|
|
||||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
|
|
||||||
intent.putExtra("call_type", callType);
|
|
||||||
intent.putExtra(Intent.EXTRA_PHONE_NUMBER, phoneNumber);
|
|
||||||
context.startActivity(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void closePhoneCallActivity() {
|
|
||||||
LogUtils.d(TAG, "closePhoneCallActivity: 收到外部关闭指令");
|
|
||||||
if (sIsActivityAlive && sCloseHandler != null) {
|
|
||||||
sCloseHandler.sendEmptyMessage(MSG_CLOSE_ACTIVITY);
|
|
||||||
LogUtils.d(TAG, "closePhoneCallActivity: 关闭消息已发送");
|
|
||||||
} else {
|
|
||||||
LogUtils.w(TAG, "closePhoneCallActivity: 页面已销毁或Handler未初始化,关闭跳过");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生命周期方法区(按执行流程排序)
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话界面开始创建,SDK版本=" + Build.VERSION.SDK_INT);
|
|
||||||
|
|
||||||
// 单例双重校验,防止异常场景多实例
|
|
||||||
if (sIsActivityAlive) {
|
|
||||||
Toast.makeText(this, TOAST_CALLING, Toast.LENGTH_SHORT).show();
|
|
||||||
LogUtils.w(TAG, MI_ADAPT_TAG + " 拦截重复创建,即将关闭当前实例");
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sIsActivityAlive = false;
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_phone_call);
|
|
||||||
ActivityStack.getInstance().addActivity(this);
|
|
||||||
adaptLockScreenAndXiaomi();
|
|
||||||
initHandler();
|
|
||||||
initData();
|
|
||||||
initView();
|
|
||||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话界面创建完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话界面开始销毁");
|
|
||||||
|
|
||||||
sIsActivityAlive = false;
|
|
||||||
isClosing = false;
|
|
||||||
stopTimer();
|
|
||||||
// 销毁通话管理器
|
|
||||||
if (mPhoneCallManager != null) {
|
|
||||||
mPhoneCallManager.destroy();
|
|
||||||
mPhoneCallManager = null;
|
|
||||||
LogUtils.d(TAG, "销毁通话管理器资源");
|
|
||||||
}
|
|
||||||
// 销毁Handler避免内存泄漏
|
|
||||||
if (sCloseHandler != null) {
|
|
||||||
sCloseHandler.removeCallbacksAndMessages(null);
|
|
||||||
sCloseHandler = null;
|
|
||||||
LogUtils.d(TAG, "销毁关闭Handler");
|
|
||||||
}
|
|
||||||
ActivityStack.getInstance().removeActivity(this);
|
|
||||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话界面销毁完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStop() {
|
|
||||||
super.onStop();
|
|
||||||
if (isFinishing()) {
|
|
||||||
sIsActivityAlive = false;
|
|
||||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 页面即将关闭,重置单例标记");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 点击事件回调
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
if (v == null) {
|
|
||||||
LogUtils.w(TAG, "onClick: 点击控件为空,忽略操作");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
switch (v.getId()) {
|
|
||||||
case R.id.tv_phone_pick_up:
|
|
||||||
LogUtils.d(TAG, "onClick: 触发接听操作");
|
|
||||||
answerCall();
|
|
||||||
break;
|
|
||||||
case R.id.tv_phone_hang_up:
|
|
||||||
LogUtils.d(TAG, "onClick: 触发挂断操作,当前通话时长=" + mCallingTime + "秒");
|
|
||||||
hangUpCall();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
LogUtils.w(TAG, "onClick: 未知点击事件,控件ID=" + v.getId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化方法区(按初始化顺序排列)
|
|
||||||
private void initHandler() {
|
|
||||||
sCloseHandler = new Handler(Looper.getMainLooper()) {
|
|
||||||
@Override
|
|
||||||
public void handleMessage(Message msg) {
|
|
||||||
super.handleMessage(msg);
|
|
||||||
if (msg.what == MSG_CLOSE_ACTIVITY) {
|
|
||||||
LogUtils.d(TAG, "handleMessage: 收到关闭消息,执行挂断逻辑");
|
|
||||||
hangUpCall();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
LogUtils.d(TAG, "initHandler: 关闭Handler初始化完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initData() {
|
|
||||||
LogUtils.d(TAG, "initData: 开始初始化业务数据");
|
|
||||||
mPhoneCallManager = PhoneCallManager.getInstance(this);
|
|
||||||
Intent intent = getIntent();
|
|
||||||
|
|
||||||
if (intent == null) {
|
|
||||||
LogUtils.e(TAG, "initData: 启动Intent为空,终止初始化");
|
|
||||||
removeFromRecentsAndFinish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mPhoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
|
|
||||||
mCallType = (PhoneCallService.CallType) intent.getSerializableExtra("call_type");
|
|
||||||
if (mPhoneNumber == null || mCallType == null) {
|
|
||||||
LogUtils.e(TAG, "initData: 通话号码或类型解析失败");
|
|
||||||
removeFromRecentsAndFinish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mOnGoingCallTimer = new Timer();
|
|
||||||
mCallingTime = 0;
|
|
||||||
LogUtils.d(TAG, "initData: 业务数据初始化完成,号码=" + mPhoneNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initView() {
|
|
||||||
LogUtils.d(TAG, "initView: 开始初始化界面控件");
|
|
||||||
// 修复沉浸式导航栏语法,适配小米全面屏
|
|
||||||
int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
|
||||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
|
||||||
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
|
||||||
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
|
|
||||||
getWindow().getDecorView().setSystemUiVisibility(uiOptions);
|
|
||||||
|
|
||||||
// 绑定控件
|
|
||||||
mTvCallNumberLabel = findViewById(R.id.tv_call_number_label);
|
|
||||||
mTvCallNumber = findViewById(R.id.tv_call_number);
|
|
||||||
mTvPickUp = findViewById(R.id.tv_phone_pick_up);
|
|
||||||
mTvCallingTime = findViewById(R.id.tv_phone_calling_time);
|
|
||||||
mTvHangUp = findViewById(R.id.tv_phone_hang_up);
|
|
||||||
|
|
||||||
// 设置控件属性
|
|
||||||
mTvCallNumber.setText(formatPhoneNumber(mPhoneNumber));
|
|
||||||
mTvPickUp.setOnClickListener(this);
|
|
||||||
mTvHangUp.setOnClickListener(this);
|
|
||||||
|
|
||||||
// 区分来电/去电UI样式
|
|
||||||
if (PhoneCallService.CallType.CALL_IN == mCallType) {
|
|
||||||
mTvCallNumberLabel.setText("来电号码");
|
|
||||||
mTvPickUp.setVisibility(View.VISIBLE);
|
|
||||||
mTvCallingTime.setVisibility(View.GONE);
|
|
||||||
} else if (PhoneCallService.CallType.CALL_OUT == mCallType) {
|
|
||||||
mTvCallNumberLabel.setText("呼叫号码");
|
|
||||||
mTvPickUp.setVisibility(View.GONE);
|
|
||||||
mTvCallingTime.setVisibility(View.VISIBLE);
|
|
||||||
mTvCallingTime.setText("通话中:00:00");
|
|
||||||
if (mPhoneCallManager != null) {
|
|
||||||
mPhoneCallManager.openSpeaker();
|
|
||||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 去电模式自动开启免提");
|
|
||||||
}
|
|
||||||
startCallTimer();
|
|
||||||
}
|
|
||||||
LogUtils.d(TAG, "initView: 界面控件初始化完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 小米机型专属适配方法
|
|
||||||
private void adaptLockScreenAndXiaomi() {
|
|
||||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 执行锁屏适配逻辑");
|
|
||||||
Window window = getWindow();
|
|
||||||
if (window == null) {
|
|
||||||
LogUtils.e(TAG, MI_ADAPT_TAG + " Window对象为空,适配失败");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int flags = WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
|
|
||||||
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
|
||||||
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
|
|
||||||
|
|
||||||
// 小米机型额外添加解锁屏标志,解决MIUI锁屏拦截问题
|
|
||||||
if (Build.MANUFACTURER.equalsIgnoreCase("Xiaomi")) {
|
|
||||||
flags |= WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD;
|
|
||||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 已添加小米机型专属锁屏适配标志");
|
|
||||||
}
|
|
||||||
window.addFlags(flags);
|
|
||||||
|
|
||||||
// 适配API29+锁屏新接口
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
setShowWhenLocked(true);
|
|
||||||
setTurnScreenOn(true);
|
|
||||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 适配API29+锁屏接口完成");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通话核心业务方法
|
|
||||||
private void answerCall() {
|
|
||||||
LogUtils.d(TAG, "answerCall: 执行接听操作");
|
|
||||||
if (mPhoneCallManager == null) {
|
|
||||||
LogUtils.e(TAG, "answerCall: 通话管理器为空,接听失败");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mPhoneCallManager.answer();
|
|
||||||
mTvPickUp.setVisibility(View.GONE);
|
|
||||||
mTvCallingTime.setVisibility(View.VISIBLE);
|
|
||||||
mTvCallingTime.setText("通话中:00:00");
|
|
||||||
startCallTimer();
|
|
||||||
LogUtils.d(TAG, "answerCall: 接听操作完成,启动通话计时");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void hangUpCall() {
|
|
||||||
if (isClosing) {
|
|
||||||
LogUtils.w(TAG, "hangUpCall: 挂断操作已执行,无需重复调用");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
LogUtils.d(TAG, "hangUpCall: 执行挂断操作,当前时长=" + mCallingTime + "秒");
|
|
||||||
isClosing = true;
|
|
||||||
stopTimer();
|
|
||||||
if (mPhoneCallManager != null) {
|
|
||||||
mPhoneCallManager.disconnect();
|
|
||||||
LogUtils.d(TAG, "hangUpCall: 通话连接已断开");
|
|
||||||
}
|
|
||||||
// 延迟关闭页面,适配小米机型通话时序
|
|
||||||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
removeFromRecentsAndFinish();
|
|
||||||
}
|
|
||||||
}, CLOSE_DELAY_MS);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 任务栈清理方法
|
|
||||||
private void removeFromRecentsAndFinish() {
|
|
||||||
if (isFinishing()) {
|
|
||||||
LogUtils.d(TAG, "removeFromRecentsAndFinish: 页面已在关闭中,无需重复操作");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
finishAndRemoveTask();
|
|
||||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 移除任务栈并关闭页面");
|
|
||||||
} else {
|
|
||||||
finish();
|
|
||||||
LogUtils.d(TAG, "兼容低版本,关闭页面");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计时工具方法
|
|
||||||
private void startCallTimer() {
|
|
||||||
LogUtils.d(TAG, "startCallTimer: 启动通话计时器");
|
|
||||||
if (mOnGoingCallTimer == null) {
|
|
||||||
mOnGoingCallTimer = new Timer();
|
|
||||||
}
|
|
||||||
mOnGoingCallTimer.schedule(new TimerTask() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
mCallingTime++;
|
|
||||||
mTvCallingTime.setText("通话中:" + formatCallingTime(mCallingTime));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 0, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void stopTimer() {
|
|
||||||
LogUtils.d(TAG, "stopTimer: 停止通话计时器");
|
|
||||||
if (mOnGoingCallTimer != null) {
|
|
||||||
mOnGoingCallTimer.cancel();
|
|
||||||
mOnGoingCallTimer = null;
|
|
||||||
}
|
|
||||||
mCallingTime = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 辅助工具方法:格式化通话时长
|
|
||||||
private String formatCallingTime(int seconds) {
|
|
||||||
int minute = seconds / 60;
|
|
||||||
int second = seconds % 60;
|
|
||||||
String minuteStr = minute < 10 ? "0" + minute : String.valueOf(minute);
|
|
||||||
String secondStr = second < 10 ? "0" + second : String.valueOf(second);
|
|
||||||
return minuteStr + ":" + secondStr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
package cc.winboll.studio.contacts.phonecallui;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.media.AudioManager;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.telecom.Call;
|
|
||||||
import android.telecom.VideoProfile;
|
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
|
||||||
* @Date 2025/12/15 20:11
|
|
||||||
* @Describe 通话核心管理类
|
|
||||||
* 功能:接听/挂断通话、免提控制、资源释放,适配API29-30及小米机型
|
|
||||||
*/
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.Q) // 匹配目标适配区间API29
|
|
||||||
public class PhoneCallManager {
|
|
||||||
// 常量定义区
|
|
||||||
public static final String TAG = "PhoneCallManager";
|
|
||||||
private static final String MI_ADAPT_TAG = "MiDeviceAdapt"; // 小米适配标识
|
|
||||||
private static final int VIDEO_PROFILE_AUDIO_ONLY = VideoProfile.STATE_AUDIO_ONLY;
|
|
||||||
private static final int AUDIO_MODE_BACKUP = -1; // 音频模式备份默认值
|
|
||||||
|
|
||||||
// 成员属性区(按依赖优先级排序,移除静态call避免跨组件冲突)
|
|
||||||
private Context mContext;
|
|
||||||
private AudioManager mAudioManager;
|
|
||||||
private int mAudioModeBackup; // 备份原始音频模式,避免影响其他应用
|
|
||||||
private boolean mIsSpeakerOpened; // 免提状态标记,防止重复切换
|
|
||||||
|
|
||||||
// 构造方法(单例化改造,避免多实例冲突)
|
|
||||||
private static volatile PhoneCallManager sInstance;
|
|
||||||
public static PhoneCallManager getInstance(Context context) {
|
|
||||||
if (context == null) {
|
|
||||||
LogUtils.e(TAG, "getInstance: 上下文为空,初始化失败");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (sInstance == null) {
|
|
||||||
synchronized (PhoneCallManager.class) {
|
|
||||||
if (sInstance == null) {
|
|
||||||
sInstance = new PhoneCallManager(context.getApplicationContext()); // 用应用上下文,避免内存泄漏
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sInstance;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 私有构造,禁止外部实例化
|
|
||||||
private PhoneCallManager(Context context) {
|
|
||||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 初始化通话管理类");
|
|
||||||
this.mContext = context;
|
|
||||||
this.mAudioModeBackup = AUDIO_MODE_BACKUP;
|
|
||||||
this.mIsSpeakerOpened = false;
|
|
||||||
initAudioManager();
|
|
||||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话管理类初始化完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化辅助方法
|
|
||||||
private void initAudioManager() {
|
|
||||||
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
|
||||||
if (mAudioManager != null) {
|
|
||||||
// 备份原始音频模式(小米机型切换后需恢复,避免外放异常)
|
|
||||||
mAudioModeBackup = mAudioManager.getMode();
|
|
||||||
LogUtils.d(TAG, "音频管理器初始化成功,原始模式备份:" + mAudioModeBackup);
|
|
||||||
} else {
|
|
||||||
LogUtils.e(TAG, "音频管理器初始化失败,将影响通话音频控制");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 核心业务方法(按使用场景排序,强化小米适配+容错)
|
|
||||||
/**
|
|
||||||
* 接听电话,默认音频通话模式
|
|
||||||
*/
|
|
||||||
public void answer() {
|
|
||||||
LogUtils.d(TAG, "执行接听通话操作");
|
|
||||||
// 从PhoneCallService的静态管理器获取通话对象,统一数据源
|
|
||||||
Call currentCall = PhoneCallService.PhoneCallManager.call;
|
|
||||||
if (currentCall == null) {
|
|
||||||
LogUtils.e(TAG, "接听失败:通话对象为空");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 校验通话状态,避免重复接听(小米机型状态变更延迟)
|
|
||||||
if (currentCall.getState() != Call.STATE_RINGING) {
|
|
||||||
LogUtils.w(TAG, MI_ADAPT_TAG + " 非响铃状态,无需接听,当前状态:" + currentCall.getState());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
currentCall.answer(VIDEO_PROFILE_AUDIO_ONLY);
|
|
||||||
openSpeaker(); // 接听后自动开免提
|
|
||||||
LogUtils.d(TAG, "通话接听成功,自动开启免提");
|
|
||||||
} catch (SecurityException e) {
|
|
||||||
LogUtils.e(TAG, MI_ADAPT_TAG + " 接听权限不足(需android.permission.ANSWER_PHONE_CALLS)", e);
|
|
||||||
} catch (IllegalStateException e) {
|
|
||||||
LogUtils.e(TAG, MI_ADAPT_TAG + " 通话状态异常,无法接听", e);
|
|
||||||
} catch (Exception e) {
|
|
||||||
LogUtils.e(TAG, "接听通话异常", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 断开通话(支持来电拒接、通话中挂断)
|
|
||||||
*/
|
|
||||||
public void disconnect() {
|
|
||||||
LogUtils.d(TAG, "执行断开通话操作");
|
|
||||||
Call currentCall = PhoneCallService.PhoneCallManager.call;
|
|
||||||
if (currentCall == null) {
|
|
||||||
LogUtils.e(TAG, "挂断失败:通话对象为空");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 校验通话状态,避免重复挂断
|
|
||||||
if (currentCall.getState() == Call.STATE_DISCONNECTED) {
|
|
||||||
LogUtils.w(TAG, MI_ADAPT_TAG + " 通话已断开,无需重复操作");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
currentCall.disconnect();
|
|
||||||
closeSpeaker(); // 挂断后关闭免提+恢复音频模式
|
|
||||||
LogUtils.d(TAG, "通话断开成功");
|
|
||||||
} catch (SecurityException e) {
|
|
||||||
LogUtils.e(TAG, MI_ADAPT_TAG + " 挂断权限不足(需android.permission.CALL_PHONE)", e);
|
|
||||||
} catch (IllegalStateException e) {
|
|
||||||
LogUtils.e(TAG, MI_ADAPT_TAG + " 通话状态异常,无法挂断", e);
|
|
||||||
} catch (Exception e) {
|
|
||||||
LogUtils.e(TAG, "断开通话异常", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 打开免提,适配小米机型音频通道切换(解决MIUI音频混乱)
|
|
||||||
*/
|
|
||||||
public void openSpeaker() {
|
|
||||||
LogUtils.d(TAG, "执行打开免提操作");
|
|
||||||
if (mAudioManager == null) {
|
|
||||||
LogUtils.e(TAG, "打开免提失败:音频管理器未初始化");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (mIsSpeakerOpened) {
|
|
||||||
LogUtils.w(TAG, "免提已开启,无需重复操作");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 小米机型适配步骤:1. 设置通话模式 2. 关闭静音 3. 开启免提(固定顺序)
|
|
||||||
mAudioManager.setMode(AudioManager.MODE_IN_CALL);
|
|
||||||
mAudioManager.setStreamMute(AudioManager.STREAM_VOICE_CALL, false); // 确保通话音频不静音
|
|
||||||
mAudioManager.setSpeakerphoneOn(true);
|
|
||||||
|
|
||||||
mIsSpeakerOpened = true;
|
|
||||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 免提开启成功,当前模式:" + mAudioManager.getMode());
|
|
||||||
} catch (SecurityException e) {
|
|
||||||
LogUtils.e(TAG, MI_ADAPT_TAG + " 音频控制权限不足", e);
|
|
||||||
} catch (Exception e) {
|
|
||||||
LogUtils.e(TAG, "打开免提异常", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 新增:关闭免提(挂断/切换场景调用,修复小米音频残留)
|
|
||||||
*/
|
|
||||||
public void closeSpeaker() {
|
|
||||||
LogUtils.d(TAG, "执行关闭免提操作");
|
|
||||||
if (mAudioManager == null || !mIsSpeakerOpened) {
|
|
||||||
LogUtils.w(TAG, "免提未开启或音频管理器为空,无需操作");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
mAudioManager.setSpeakerphoneOn(false);
|
|
||||||
// 恢复原始音频模式(关键:小米机型不恢复会导致其他应用外放异常)
|
|
||||||
if (mAudioModeBackup != AUDIO_MODE_BACKUP) {
|
|
||||||
mAudioManager.setMode(mAudioModeBackup);
|
|
||||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 恢复原始音频模式:" + mAudioModeBackup);
|
|
||||||
}
|
|
||||||
mIsSpeakerOpened = false;
|
|
||||||
LogUtils.d(TAG, "免提关闭成功");
|
|
||||||
} catch (Exception e) {
|
|
||||||
LogUtils.e(TAG, MI_ADAPT_TAG + " 关闭免提异常", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 销毁资源,避免内存泄漏+音频残留(适配小米内存管理)
|
|
||||||
*/
|
|
||||||
public void destroy() {
|
|
||||||
LogUtils.d(TAG, "开始销毁通话管理资源");
|
|
||||||
closeSpeaker(); // 销毁前强制关闭免提+恢复音频模式
|
|
||||||
// 释放资源(应用上下文无需主动置空,避免空指针)
|
|
||||||
mAudioManager = null;
|
|
||||||
sInstance = null; // 单例置空,下次重新初始化
|
|
||||||
LogUtils.d(TAG, MI_ADAPT_TAG + " 通话管理资源销毁完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 新增:获取当前免提状态(供UI层同步显示)
|
|
||||||
*/
|
|
||||||
public boolean isSpeakerOpened() {
|
|
||||||
return mIsSpeakerOpened;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
package cc.winboll.studio.contacts.phonecallui;
|
|
||||||
|
|
||||||
import android.media.AudioManager;
|
|
||||||
import android.telecom.Call;
|
|
||||||
import android.telecom.InCallService;
|
|
||||||
import android.telephony.TelephonyManager;
|
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
import cc.winboll.studio.contacts.ActivityStack;
|
|
||||||
import cc.winboll.studio.contacts.dun.Rules;
|
|
||||||
import cc.winboll.studio.contacts.fragments.CallLogFragment;
|
|
||||||
import cc.winboll.studio.contacts.model.RingTongBean;
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 监听电话通信状态的服务,实现该类的同时必须提供电话管理的 UI
|
|
||||||
* @author aJIEw, ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
|
||||||
* @see PhoneCallActivity
|
|
||||||
* @see android.telecom.InCallService
|
|
||||||
* 适配:Java7 语法 + Android API29 - 30 | 移除录音功能 | 强化小米设备稳定性与容错性
|
|
||||||
*/
|
|
||||||
@RequiresApi(api = 29)
|
|
||||||
public class PhoneCallService extends InCallService {
|
|
||||||
// 常量定义区
|
|
||||||
public static final String TAG = "PhoneCallService";
|
|
||||||
// 小米设备适配标识,便于日志区分
|
|
||||||
private static final String MI_DEVICE_TAG = "MiDeviceAdapt";
|
|
||||||
|
|
||||||
// 成员属性区(按依赖顺序排列)
|
|
||||||
private Call.Callback mCallCallback;
|
|
||||||
private AudioManager mAudioManager;
|
|
||||||
|
|
||||||
// 内部枚举类(通话类型定义)
|
|
||||||
public enum CallType {
|
|
||||||
CALL_IN, // 来电
|
|
||||||
CALL_OUT // 去电
|
|
||||||
}
|
|
||||||
|
|
||||||
// Service生命周期方法区(按执行流程排序)
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 通话监听服务启动");
|
|
||||||
initAudioManager();
|
|
||||||
initCallCallback();
|
|
||||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 服务初始化完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCallAdded(Call call) {
|
|
||||||
super.onCallAdded(call);
|
|
||||||
LogUtils.d(TAG, "检测到新通话");
|
|
||||||
if (call == null) {
|
|
||||||
LogUtils.e(TAG, "通话对象为空,跳过处理");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 双重校验回调,避免重复注册
|
|
||||||
if (mCallCallback != null) {
|
|
||||||
call.registerCallback(mCallCallback);
|
|
||||||
}
|
|
||||||
// 绑定通话对象到管理器,供UI层调用
|
|
||||||
PhoneCallManager.call = call;
|
|
||||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 通话回调注册成功,对象绑定完成");
|
|
||||||
|
|
||||||
CallType callType = judgeCallType(call);
|
|
||||||
if (callType != null) {
|
|
||||||
handleValidCall(call, callType);
|
|
||||||
} else {
|
|
||||||
LogUtils.w(TAG, "无法识别通话类型,状态码:" + call.getState());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCallRemoved(Call call) {
|
|
||||||
super.onCallRemoved(call);
|
|
||||||
LogUtils.d(TAG, "通话结束,开始清理资源");
|
|
||||||
if (call != null && mCallCallback != null) {
|
|
||||||
call.unregisterCallback(mCallCallback);
|
|
||||||
LogUtils.d(TAG, "通话回调已注销");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 延迟置空通话对象,避免UI层挂断时对象已被释放(适配小米机型时序)
|
|
||||||
new Thread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
try {
|
|
||||||
// 延迟200ms,确保PhoneCallActivity挂断逻辑执行完成
|
|
||||||
Thread.sleep(200);
|
|
||||||
PhoneCallManager.call = null;
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
LogUtils.e(TAG, MI_DEVICE_TAG + " 延迟置空通话对象异常", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
|
|
||||||
PhoneCallActivity.closePhoneCallActivity();
|
|
||||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 通话资源清理完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
LogUtils.d(TAG, "服务开始销毁");
|
|
||||||
CallLogFragment.updateCallLogFragment();
|
|
||||||
// 释放资源,适配小米设备内存管理,避免内存泄漏
|
|
||||||
mCallCallback = null;
|
|
||||||
mAudioManager = null;
|
|
||||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 服务销毁完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化方法区
|
|
||||||
private void initAudioManager() {
|
|
||||||
mAudioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
|
|
||||||
if (mAudioManager == null) {
|
|
||||||
LogUtils.e(TAG, MI_DEVICE_TAG + " 获取音频管理器失败");
|
|
||||||
} else {
|
|
||||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 音频管理器初始化成功");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initCallCallback() {
|
|
||||||
mCallCallback = new Call.Callback() {
|
|
||||||
@Override
|
|
||||||
public void onStateChanged(Call call, int state) {
|
|
||||||
super.onStateChanged(call, state);
|
|
||||||
if (call == null) {
|
|
||||||
LogUtils.e(TAG, "onStateChanged: 通话对象为空");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String stateDesc = getCallStateDesc(state);
|
|
||||||
LogUtils.d(TAG, "通话状态变更:" + stateDesc + "(状态码:" + state + ")");
|
|
||||||
|
|
||||||
switch (state) {
|
|
||||||
case Call.STATE_DISCONNECTED:
|
|
||||||
// 双重校验,避免重复关闭页面
|
|
||||||
if (ActivityStack.getInstance().getActivity(PhoneCallActivity.class) != null) {
|
|
||||||
ActivityStack.getInstance().finishActivity(PhoneCallActivity.class);
|
|
||||||
LogUtils.d(TAG, "通话界面已关闭");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Call.STATE_ACTIVE:
|
|
||||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 通话进入活跃状态,适配音频通道");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
LogUtils.d(TAG, "通话状态回调初始化完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 核心业务处理方法区
|
|
||||||
private CallType judgeCallType(Call call) {
|
|
||||||
if (call == null) {
|
|
||||||
LogUtils.e(TAG, "judgeCallType: 通话对象为空");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
int callState = call.getState();
|
|
||||||
if (callState == Call.STATE_RINGING) {
|
|
||||||
LogUtils.d(TAG, "识别为来电");
|
|
||||||
return CallType.CALL_IN;
|
|
||||||
} else if (callState == Call.STATE_CONNECTING) {
|
|
||||||
LogUtils.d(TAG, "识别为去电");
|
|
||||||
return CallType.CALL_OUT;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean handleValidCall(Call call, CallType callType) {
|
|
||||||
if (call == null || callType == null) {
|
|
||||||
LogUtils.e(TAG, "handleValidCall: 通话对象或类型为空");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Call.Details callDetails = call.getDetails();
|
|
||||||
if (callDetails == null || callDetails.getHandle() == null) {
|
|
||||||
LogUtils.e(TAG, "通话详情缺失,处理终止");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String phoneNumber = callDetails.getHandle().getSchemeSpecificPart();
|
|
||||||
LogUtils.d(TAG, "处理通话:号码=" + phoneNumber + ",类型=" + callType.name());
|
|
||||||
|
|
||||||
if (mAudioManager == null) {
|
|
||||||
LogUtils.e(TAG, "音频管理器未初始化");
|
|
||||||
PhoneCallActivity.actionStart(this, phoneNumber, callType);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkRulesAndHandleRingerVolumeControl(phoneNumber, call)) {
|
|
||||||
PhoneCallActivity.actionStart(this, phoneNumber, callType);
|
|
||||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 通话界面启动成功");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean checkRulesAndHandleRingerVolumeControl(String phoneNumber, Call call) {
|
|
||||||
if (mAudioManager == null || phoneNumber == null || call == null) {
|
|
||||||
LogUtils.e(TAG, "checkRulesAndHandleRingerVolumeControl: 入参为空");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int currentVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_RING);
|
|
||||||
LogUtils.d(TAG, "当前铃声音量:" + currentVolume);
|
|
||||||
|
|
||||||
RingTongBean ringTongBean = RingTongBean.loadBean(this, RingTongBean.class);
|
|
||||||
if (ringTongBean == null) {
|
|
||||||
ringTongBean = new RingTongBean();
|
|
||||||
RingTongBean.saveBean(this, ringTongBean);
|
|
||||||
LogUtils.d(TAG, "初始化默认铃音配置");
|
|
||||||
}
|
|
||||||
final int configVolume = ringTongBean.getStreamVolume();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 小米机型适配:调整音量时添加权限校验
|
|
||||||
if (currentVolume != configVolume) {
|
|
||||||
mAudioManager.setStreamVolume(AudioManager.STREAM_RING, configVolume, 0);
|
|
||||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 铃声音量调整为配置值:" + configVolume);
|
|
||||||
}
|
|
||||||
} catch (SecurityException e) {
|
|
||||||
LogUtils.e(TAG, "音量调整失败,权限不足", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 校验拦截规则
|
|
||||||
if (!Rules.getInstance(this).isAllowed(phoneNumber)) {
|
|
||||||
LogUtils.d(TAG, "号码" + phoneNumber + "命中拦截规则");
|
|
||||||
try {
|
|
||||||
// 拦截时静音并挂断
|
|
||||||
mAudioManager.setStreamVolume(AudioManager.STREAM_RING, 0, 0);
|
|
||||||
call.disconnect();
|
|
||||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 拦截通话已挂断并静音");
|
|
||||||
|
|
||||||
// 延迟恢复音量,适配小米机型音频通道延迟
|
|
||||||
new Thread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
try {
|
|
||||||
Thread.sleep(500);
|
|
||||||
if (mAudioManager != null) {
|
|
||||||
mAudioManager.setStreamVolume(AudioManager.STREAM_RING, configVolume, 0);
|
|
||||||
LogUtils.d(TAG, MI_DEVICE_TAG + " 延迟恢复铃音配置");
|
|
||||||
}
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
LogUtils.e(TAG, "恢复音量线程中断", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
} catch (SecurityException e) {
|
|
||||||
LogUtils.e(TAG, "拦截静音失败", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 辅助工具方法区:解析通话状态描述
|
|
||||||
private String getCallStateDesc(int state) {
|
|
||||||
switch (state) {
|
|
||||||
case TelephonyManager.CALL_STATE_RINGING:
|
|
||||||
return "响铃中";
|
|
||||||
case TelephonyManager.CALL_STATE_OFFHOOK:
|
|
||||||
return "通话中";
|
|
||||||
case TelephonyManager.CALL_STATE_IDLE:
|
|
||||||
return "空闲";
|
|
||||||
case Call.STATE_ACTIVE:
|
|
||||||
return "通话活跃";
|
|
||||||
case Call.STATE_CONNECTING:
|
|
||||||
return "连接中";
|
|
||||||
case Call.STATE_DISCONNECTED:
|
|
||||||
return "已断开";
|
|
||||||
default:
|
|
||||||
return "未知状态";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 静态内部类:统一管理通话对象,避免跨组件对象混乱
|
|
||||||
public static class PhoneCallManager {
|
|
||||||
public static Call call;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
package cc.winboll.studio.contacts.receivers;
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.IntentFilter;
|
|
||||||
import cc.winboll.studio.contacts.services.MainService;
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
|
||||||
import cc.winboll.studio.libappbase.ToastUtils;
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
|
||||||
* @Date 2025/02/13 06:58:04
|
|
||||||
* @Describe 主要广播接收器,监听系统开机广播并自动启动主服务
|
|
||||||
*/
|
|
||||||
public class MainReceiver extends BroadcastReceiver {
|
|
||||||
// ====================== 常量定义区 ======================
|
|
||||||
public static final String TAG = "MainReceiver";
|
|
||||||
// 监听的系统广播 Action
|
|
||||||
private static final String ACTION_BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED";
|
|
||||||
|
|
||||||
// ====================== 成员变量区 ======================
|
|
||||||
// 使用弱引用关联 MainService,避免内存泄漏
|
|
||||||
private WeakReference<MainService> mMainServiceWeakRef;
|
|
||||||
|
|
||||||
// ====================== 构造函数区 ======================
|
|
||||||
public MainReceiver(MainService service) {
|
|
||||||
this.mMainServiceWeakRef = new WeakReference<>(service);
|
|
||||||
LogUtils.d(TAG, "MainReceiver: 初始化完成,已关联 MainService 实例");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 重写 BroadcastReceiver 核心方法 ======================
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
// 空值校验,避免空指针异常
|
|
||||||
if (context == null) {
|
|
||||||
LogUtils.e(TAG, "onReceive: Context 为 null,无法处理广播");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (intent == null || intent.getAction() == null) {
|
|
||||||
LogUtils.w(TAG, "onReceive: 接收到空 Intent 或空 Action");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String action = intent.getAction();
|
|
||||||
LogUtils.d(TAG, "onReceive: 接收到广播 | Action=" + action);
|
|
||||||
|
|
||||||
// 处理开机完成广播
|
|
||||||
if (ACTION_BOOT_COMPLETED.equals(action)) {
|
|
||||||
LogUtils.i(TAG, "onReceive: 监听到开机完成广播,自动启动 MainService");
|
|
||||||
ToastUtils.show("设备开机,启动拨号主服务");
|
|
||||||
MainService.startMainService(context);
|
|
||||||
} else {
|
|
||||||
LogUtils.i(TAG, "onReceive: 接收到未处理的广播 | Action=" + action);
|
|
||||||
ToastUtils.show("收到广播:" + action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 广播注册/注销方法区 ======================
|
|
||||||
/**
|
|
||||||
* 注册广播接收器,监听指定系统广播
|
|
||||||
* @param context 上下文对象
|
|
||||||
*/
|
|
||||||
public void registerAction(Context context) {
|
|
||||||
if (context == null) {
|
|
||||||
LogUtils.e(TAG, "registerAction: Context 为 null,注册失败");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
IntentFilter intentFilter = new IntentFilter();
|
|
||||||
intentFilter.addAction(ACTION_BOOT_COMPLETED);
|
|
||||||
// 可按需添加其他监听的 Action
|
|
||||||
// intentFilter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
|
|
||||||
|
|
||||||
context.registerReceiver(this, intentFilter);
|
|
||||||
LogUtils.d(TAG, "registerAction: 广播接收器注册成功 | 监听 Action=" + ACTION_BOOT_COMPLETED);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 注销广播接收器,释放资源(解决 mMainReceiver.unregisterAction(this) 调用缺失问题)
|
|
||||||
* @param context 上下文对象
|
|
||||||
*/
|
|
||||||
public void unregisterAction(Context context) {
|
|
||||||
if (context == null) {
|
|
||||||
LogUtils.e(TAG, "unregisterAction: Context 为 null,注销失败");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
context.unregisterReceiver(this);
|
|
||||||
LogUtils.d(TAG, "unregisterAction: 广播接收器注销成功");
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
LogUtils.w(TAG, "unregisterAction: 广播接收器未注册,无需注销", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
package cc.winboll.studio.contacts.services;
|
|
||||||
|
|
||||||
import android.app.Service;
|
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.ServiceConnection;
|
|
||||||
import android.os.Binder;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import cc.winboll.studio.contacts.model.MainServiceBean;
|
|
||||||
import cc.winboll.studio.contacts.utils.NotificationManagerUtils;
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
|
||||||
* @Date 2025/02/14 03:38:31
|
|
||||||
* @Describe 守护进程服务,用于监控并保活主服务 MainService
|
|
||||||
* 适配 Android 12+ 后台服务启动限制,支持前台服务运行
|
|
||||||
* 兼容 Java 7 语法 & 低版本 SDK 编译
|
|
||||||
* 移除无关的 microphone 类型配置,修复前台服务类型不匹配崩溃
|
|
||||||
*/
|
|
||||||
public class AssistantService extends Service {
|
|
||||||
// ====================== 常量定义区 ======================
|
|
||||||
public static final String TAG = "AssistantService";
|
|
||||||
// 前台服务通知配置
|
|
||||||
private static final String FOREGROUND_CHANNEL_ID = "assistant_service_foreground_channel";
|
|
||||||
private static final int FOREGROUND_NOTIFICATION_ID = 1002;
|
|
||||||
// 修复:前台服务类型改为 dataSync(0x00000001),与 Manifest 保持一致,移除 microphone 类型
|
|
||||||
private static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 0x00000001;
|
|
||||||
// Android 版本常量硬编码(Java 7 兼容)
|
|
||||||
private static final int ANDROID_8_API = 26; // 通知渠道最低版本
|
|
||||||
private static final int ANDROID_10_API = 29; // 前台服务类型最低支持版本
|
|
||||||
private static final int ANDROID_12_API = 31; // 后台启动限制最低版本
|
|
||||||
// 重试延迟时间(避免频繁触发后台启动限制)
|
|
||||||
private static final long RETRY_DELAY_MS = 3000L;
|
|
||||||
|
|
||||||
// ====================== 成员变量区 ======================
|
|
||||||
private MainServiceBean mMainServiceBean;
|
|
||||||
private MyServiceConnection mMyServiceConnection;
|
|
||||||
private MainService mMainService;
|
|
||||||
private boolean mIsBound = false;
|
|
||||||
private volatile boolean mIsThreadAlive = false;
|
|
||||||
|
|
||||||
// ====================== Binder 内部类 ======================
|
|
||||||
/**
|
|
||||||
* 对外暴露服务实例的 Binder
|
|
||||||
*/
|
|
||||||
public class MyBinder extends Binder {
|
|
||||||
public AssistantService getService() {
|
|
||||||
LogUtils.d(TAG, "MyBinder.getService: 获取 AssistantService 实例");
|
|
||||||
return AssistantService.this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== ServiceConnection 内部类 ======================
|
|
||||||
/**
|
|
||||||
* 主服务连接状态监听回调
|
|
||||||
*/
|
|
||||||
private class MyServiceConnection implements ServiceConnection {
|
|
||||||
@Override
|
|
||||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
|
||||||
if (service == null) {
|
|
||||||
LogUtils.w(TAG, "MyServiceConnection.onServiceConnected: 绑定的 IBinder 为 null");
|
|
||||||
mIsBound = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
MainService.MyBinder binder = (MainService.MyBinder) service;
|
|
||||||
mMainService = binder.getService();
|
|
||||||
mIsBound = true;
|
|
||||||
LogUtils.d(TAG, "MyServiceConnection.onServiceConnected: 主服务绑定成功 | MainService=" + mMainService);
|
|
||||||
} catch (ClassCastException e) {
|
|
||||||
LogUtils.e(TAG, "MyServiceConnection.onServiceConnected: IBinder 类型转换失败", e);
|
|
||||||
mIsBound = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onServiceDisconnected(ComponentName name) {
|
|
||||||
LogUtils.w(TAG, "MyServiceConnection.onServiceDisconnected: 主服务连接断开");
|
|
||||||
mMainService = null;
|
|
||||||
mIsBound = false;
|
|
||||||
|
|
||||||
// 尝试重新绑定主服务(如果配置为启用)
|
|
||||||
reloadMainServiceConfig();
|
|
||||||
if (mMainServiceBean != null && mMainServiceBean.isEnable()) {
|
|
||||||
LogUtils.d(TAG, "MyServiceConnection.onServiceDisconnected: 延迟重试绑定主服务");
|
|
||||||
wakeupAndBindMain();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 对外方法区 ======================
|
|
||||||
/**
|
|
||||||
* 设置线程存活状态
|
|
||||||
*/
|
|
||||||
public synchronized void setIsThreadAlive(boolean isThreadAlive) {
|
|
||||||
this.mIsThreadAlive = isThreadAlive;
|
|
||||||
LogUtils.d(TAG, "setIsThreadAlive: 线程存活状态变更 | " + isThreadAlive);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取线程存活状态
|
|
||||||
*/
|
|
||||||
public boolean isThreadAlive() {
|
|
||||||
return mIsThreadAlive;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 前台服务辅助方法 ======================
|
|
||||||
/**
|
|
||||||
* 创建前台服务通知(Android 8.0+ 必须配置渠道)
|
|
||||||
*/
|
|
||||||
// private Notification createForegroundNotification() {
|
|
||||||
// // 1. 创建通知渠道(API 26+ 必需)
|
|
||||||
// if (Build.VERSION.SDK_INT >= ANDROID_8_API) {
|
|
||||||
// NotificationChannel channel = new NotificationChannel(
|
|
||||||
// FOREGROUND_CHANNEL_ID,
|
|
||||||
// "守护服务",
|
|
||||||
// NotificationManager.IMPORTANCE_LOW
|
|
||||||
// );
|
|
||||||
// channel.setDescription("守护服务后台运行,保障主服务存活");
|
|
||||||
// // 空指针防护
|
|
||||||
// NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
|
||||||
// if (manager != null) {
|
|
||||||
// manager.createNotificationChannel(channel);
|
|
||||||
// LogUtils.d(TAG, "createForegroundNotification: 通知渠道创建成功");
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // 2. 构建通知(Java 7 分步设置,取消链式调用简化)
|
|
||||||
// Notification.Builder builder;
|
|
||||||
// if (Build.VERSION.SDK_INT >= ANDROID_8_API) {
|
|
||||||
// builder = new Notification.Builder(this, FOREGROUND_CHANNEL_ID);
|
|
||||||
// } else {
|
|
||||||
// builder = new Notification.Builder(this);
|
|
||||||
// }
|
|
||||||
// builder.setSmallIcon(R.drawable.ic_launcher);
|
|
||||||
// builder.setContentTitle("守护服务运行中");
|
|
||||||
// builder.setContentText("正在监控主服务状态");
|
|
||||||
// builder.setPriority(Notification.PRIORITY_LOW);
|
|
||||||
// builder.setOngoing(true); // 不可手动取消
|
|
||||||
//
|
|
||||||
// return builder.build();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// ====================== Service 生命周期方法区 ======================
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
LogUtils.d(TAG, "onCreate: 守护服务创建");
|
|
||||||
|
|
||||||
// 初始化主服务连接回调
|
|
||||||
if (mMyServiceConnection == null) {
|
|
||||||
mMyServiceConnection = new MyServiceConnection();
|
|
||||||
LogUtils.d(TAG, "onCreate: 初始化 MyServiceConnection 完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化运行状态
|
|
||||||
setIsThreadAlive(false);
|
|
||||||
// 启动守护逻辑
|
|
||||||
assistantService();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public IBinder onBind(Intent intent) {
|
|
||||||
LogUtils.d(TAG, "onBind: 服务被绑定 | Intent=" + intent);
|
|
||||||
return new MyBinder();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
|
||||||
LogUtils.d(TAG, "onStartCommand: 服务被启动 | startId=" + startId);
|
|
||||||
// 每次启动都执行守护逻辑,确保主服务存活
|
|
||||||
assistantService();
|
|
||||||
// START_STICKY:服务被杀死后系统尝试重启
|
|
||||||
return START_STICKY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
LogUtils.d(TAG, "onDestroy: 守护服务销毁");
|
|
||||||
|
|
||||||
// 停止线程并解除主服务绑定
|
|
||||||
setIsThreadAlive(false);
|
|
||||||
if (mIsBound && mMyServiceConnection != null) {
|
|
||||||
try {
|
|
||||||
unbindService(mMyServiceConnection);
|
|
||||||
LogUtils.d(TAG, "onDestroy: 解除主服务绑定成功");
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
LogUtils.w(TAG, "onDestroy: 解除绑定失败,服务未绑定", e);
|
|
||||||
}
|
|
||||||
mIsBound = false;
|
|
||||||
}
|
|
||||||
mMainService = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 核心守护逻辑方法区 ======================
|
|
||||||
/**
|
|
||||||
* 守护服务核心逻辑:检查配置并保活主服务
|
|
||||||
*/
|
|
||||||
private void assistantService() {
|
|
||||||
LogUtils.d(TAG, "assistantService: 执行守护逻辑");
|
|
||||||
|
|
||||||
// 加载主服务配置
|
|
||||||
reloadMainServiceConfig();
|
|
||||||
if (mMainServiceBean == null) {
|
|
||||||
LogUtils.e(TAG, "assistantService: 主服务配置加载失败,终止守护逻辑");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LogUtils.d(TAG, "assistantService: 主服务启用状态 | " + mMainServiceBean.isEnable());
|
|
||||||
// 配置启用且线程未存活时,唤醒并绑定主服务
|
|
||||||
if (mMainServiceBean.isEnable() && !isThreadAlive()) {
|
|
||||||
setIsThreadAlive(true);
|
|
||||||
wakeupAndBindMain();
|
|
||||||
} else if (!mMainServiceBean.isEnable()) {
|
|
||||||
setIsThreadAlive(false);
|
|
||||||
LogUtils.d(TAG, "assistantService: 主服务已禁用,停止保活");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 唤醒并绑定主服务 MainService(适配后台启动限制)
|
|
||||||
*/
|
|
||||||
private void wakeupAndBindMain() {
|
|
||||||
if (mMyServiceConnection == null) {
|
|
||||||
LogUtils.e(TAG, "wakeupAndBindMain: MyServiceConnection 未初始化,绑定失败");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Intent intent = new Intent(this, MainService.class);
|
|
||||||
// 根据应用前后台状态选择启动方式(Android 12+ 后台用 startForegroundService)
|
|
||||||
startForegroundService(intent);
|
|
||||||
|
|
||||||
// BIND_IMPORTANT:提高绑定优先级,主服务被杀时会回调断开
|
|
||||||
bindService(intent, mMyServiceConnection, Context.BIND_IMPORTANT);
|
|
||||||
LogUtils.d(TAG, "wakeupAndBindMain: 已启动并绑定主服务 MainService");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== 辅助方法区 ======================
|
|
||||||
/**
|
|
||||||
* 重新加载主服务配置
|
|
||||||
*/
|
|
||||||
private void reloadMainServiceConfig() {
|
|
||||||
mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
|
|
||||||
LogUtils.d(TAG, "reloadMainServiceConfig: 主服务配置重新加载完成 | " + mMainServiceBean);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,327 +0,0 @@
|
|||||||
package cc.winboll.studio.contacts.services;
|
|
||||||
|
|
||||||
import android.app.Service;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.os.Looper;
|
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
|
||||||
* @Date 2026/04/18 15:00:00 (GMT+8)
|
|
||||||
* @LastEditTime 2026/04/22 17:20:00 (GMT+8)
|
|
||||||
* @Describe 限时特殊通道服务
|
|
||||||
* 提供安全的服务启动、令牌校验及循环任务管理
|
|
||||||
* 新增功能:
|
|
||||||
* 1. 计时过程中实时输出剩余秒数
|
|
||||||
* 2. 服务销毁前,发送本地广播通知应用内其他组件,并携带剩余秒数信息
|
|
||||||
* 3. 每秒发送一次倒计时状态的本地广播
|
|
||||||
*/
|
|
||||||
public class LimitedTimeSpecialChannelService extends Service {
|
|
||||||
|
|
||||||
// ========================= 常量定义 =========================
|
|
||||||
public static final String TAG = "LimitedTimeSpecialChannelService";
|
|
||||||
public static final String EXTRA_DELAY_MILLIS = "EXTRA_DELAY_MILLIS";
|
|
||||||
private static final String EXTRA_SECURITY_TOKEN = "EXTRA_SECURITY_TOKEN";
|
|
||||||
|
|
||||||
// 本地广播 Action 常量
|
|
||||||
public static final String ACTION_SERVICE_DESTROYED = "cc.winboll.studio.contacts.services.ACTION_SERVICE_DESTROYED";
|
|
||||||
|
|
||||||
// 倒计时心跳广播 Action
|
|
||||||
public static final String ACTION_COUNTDOWN_TICK = "cc.winboll.studio.contacts.services.ACTION_COUNTDOWN_TICK";
|
|
||||||
// 广播携带的额外参数:剩余秒数
|
|
||||||
public static final String EXTRA_REMAINING_SECONDS = "EXTRA_REMAINING_SECONDS";
|
|
||||||
// 广播携带的额外参数:总定时秒数
|
|
||||||
public static final String EXTRA_TOTAL_SECONDS = "EXTRA_TOTAL_SECONDS";
|
|
||||||
|
|
||||||
// ========================= 静态变量 =========================
|
|
||||||
/**
|
|
||||||
* 公共静态私有字段:安全校验令牌
|
|
||||||
* 确保全局可访问且值不可变更
|
|
||||||
*/
|
|
||||||
public static final String mValidToken = "VALID_TOKEN_" + System.currentTimeMillis();
|
|
||||||
|
|
||||||
private static volatile LimitedTimeSpecialChannelService sInstance = null;
|
|
||||||
private static volatile boolean sIsServiceRunning = false;
|
|
||||||
|
|
||||||
// ========================= 成员变量 =========================
|
|
||||||
private final Handler mHandler = new Handler(Looper.getMainLooper());
|
|
||||||
private long mTotalMillis = 0; // 总定时时长
|
|
||||||
private long mRemainingMillis = 0; // 剩余时长
|
|
||||||
private LocalBroadcastManager mLocalBroadcastManager; // 本地广播管理器实例
|
|
||||||
|
|
||||||
// ========================= 公共静态方法 =========================
|
|
||||||
/**
|
|
||||||
* 公共静态方法:启动服务
|
|
||||||
* @param context 上下文
|
|
||||||
* @param delayMillis 定时时长(毫秒)
|
|
||||||
*/
|
|
||||||
public static void startService(Context context, long delayMillis) {
|
|
||||||
LogUtils.i(TAG, "调用静态入口方法 startService");
|
|
||||||
LogUtils.i(TAG, "入参 - context: " + context + ", delayMillis: " + delayMillis);
|
|
||||||
|
|
||||||
if (context == null) {
|
|
||||||
LogUtils.w(TAG, "启动失败,上下文为null");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isServiceRunning()) {
|
|
||||||
LogUtils.i(TAG, "服务已运行,忽略重复启动请求");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建Intent传递参数
|
|
||||||
Intent intent = new Intent(context, LimitedTimeSpecialChannelService.class);
|
|
||||||
intent.putExtra(EXTRA_SECURITY_TOKEN, mValidToken);
|
|
||||||
intent.putExtra(EXTRA_DELAY_MILLIS, delayMillis);
|
|
||||||
|
|
||||||
context.startService(intent);
|
|
||||||
LogUtils.i(TAG, "服务启动命令已发出");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 公共静态方法:停止服务
|
|
||||||
* @param context 上下文
|
|
||||||
*/
|
|
||||||
public static void stopService(Context context) {
|
|
||||||
LogUtils.i(TAG, "调用静态入口方法 stopService");
|
|
||||||
LogUtils.i(TAG, "入参 - context: " + context);
|
|
||||||
|
|
||||||
if (context == null) {
|
|
||||||
LogUtils.w(TAG, "停止失败,上下文为null");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Intent intent = new Intent(context, LimitedTimeSpecialChannelService.class);
|
|
||||||
context.stopService(intent);
|
|
||||||
LogUtils.i(TAG, "服务停止命令已发出");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 公共静态方法:查询服务运行状态
|
|
||||||
* @return true if running
|
|
||||||
*/
|
|
||||||
public static boolean isServiceRunning() {
|
|
||||||
return sIsServiceRunning;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 【核心单元测试方法】
|
|
||||||
* 执行完整的单元测试流程
|
|
||||||
* @param context 上下文
|
|
||||||
*/
|
|
||||||
public static void unitTest(Context context) {
|
|
||||||
LogUtils.i(TAG, "=== 开始执行单元测试 ===");
|
|
||||||
|
|
||||||
// 测试1: 初始状态应为未运行
|
|
||||||
boolean initialState = isServiceRunning();
|
|
||||||
LogUtils.i(TAG, "测试1 - 初始状态检查: " + (initialState ? "失败(不应运行)" : "成功(未运行)"));
|
|
||||||
|
|
||||||
// 启动服务,设置5秒定时
|
|
||||||
startService(context, 5000L);
|
|
||||||
|
|
||||||
// 核心修复:同步等待服务启动完成
|
|
||||||
try {
|
|
||||||
Thread.sleep(1200);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
LogUtils.e(TAG, "单元测试等待被中断", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试3: 验证服务已启动
|
|
||||||
boolean runningState = isServiceRunning();
|
|
||||||
LogUtils.i(TAG, "测试3 - 运行状态检查: " + (runningState ? "成功(运行中)" : "失败(未运行)"));
|
|
||||||
|
|
||||||
// 测试4: 尝试重复启动
|
|
||||||
LogUtils.i(TAG, "测试4 - 尝试重复启动(预期忽略)");
|
|
||||||
startService(context, 5000L);
|
|
||||||
|
|
||||||
// 测试5: 等待服务自动停止
|
|
||||||
LogUtils.i(TAG, "测试5 - 等待服务自动停止(5秒)...");
|
|
||||||
try {
|
|
||||||
// 等待超过服务的5秒运行时间,确保能观测到销毁状态
|
|
||||||
Thread.sleep(6000);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
LogUtils.e(TAG, "测试等待被中断", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证最终状态
|
|
||||||
boolean finalState = isServiceRunning();
|
|
||||||
LogUtils.i(TAG, "测试5 - 最终状态检查: " + (!finalState ? "成功(已销毁)" : "失败(仍运行)"));
|
|
||||||
LogUtils.i(TAG, "=== 单元测试执行完成 ===");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========================= 生命周期 =========================
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
LogUtils.i(TAG, "服务 onCreate,创建实例");
|
|
||||||
sInstance = this;
|
|
||||||
sIsServiceRunning = true;
|
|
||||||
|
|
||||||
// 初始化本地广播管理器
|
|
||||||
mLocalBroadcastManager = LocalBroadcastManager.getInstance(this);
|
|
||||||
LogUtils.i(TAG, "本地广播管理器初始化完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
|
||||||
LogUtils.i(TAG, "服务 onStartCommand,处理启动请求");
|
|
||||||
LogUtils.i(TAG, "入参 - intent: " + intent + ", flags: " + flags + ", startId: " + startId);
|
|
||||||
|
|
||||||
if (!isValidToken(intent)) {
|
|
||||||
LogUtils.w(TAG, "安全校验失败,拒绝启动服务");
|
|
||||||
stopSelf();
|
|
||||||
return START_NOT_STICKY;
|
|
||||||
}
|
|
||||||
|
|
||||||
long delayMillis = intent.getLongExtra(EXTRA_DELAY_MILLIS, 0);
|
|
||||||
if (delayMillis <= 0) {
|
|
||||||
LogUtils.w(TAG, "无效的定时时长: " + delayMillis + ",服务将退出");
|
|
||||||
stopSelf();
|
|
||||||
return START_NOT_STICKY;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化总时长和剩余时长
|
|
||||||
mTotalMillis = delayMillis;
|
|
||||||
mRemainingMillis = delayMillis;
|
|
||||||
LogUtils.i(TAG, "初始化定时时长: " + (mTotalMillis / 1000) + " 秒");
|
|
||||||
|
|
||||||
startLoopTask();
|
|
||||||
// 设置自动停止定时器
|
|
||||||
mHandler.postDelayed(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
LogUtils.i(TAG, "定时时长结束,准备停止服务");
|
|
||||||
stopSelf();
|
|
||||||
}
|
|
||||||
}, delayMillis);
|
|
||||||
|
|
||||||
return START_STICKY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
LogUtils.i(TAG, "服务 onDestroy,准备销毁");
|
|
||||||
|
|
||||||
// 发送本地广播,通知应用内其他组件服务即将销毁
|
|
||||||
// 此处携带剩余秒数信息,方便接收端知晓具体的结束状态
|
|
||||||
sendServiceDestroyedBroadcast();
|
|
||||||
|
|
||||||
// 重置运行状态
|
|
||||||
sIsServiceRunning = false;
|
|
||||||
sInstance = null;
|
|
||||||
|
|
||||||
// 清理所有回调
|
|
||||||
mHandler.removeCallbacksAndMessages(null);
|
|
||||||
|
|
||||||
// 执行父类销毁
|
|
||||||
super.onDestroy();
|
|
||||||
|
|
||||||
LogUtils.i(TAG, "服务销毁流程完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送服务销毁的本地广播
|
|
||||||
* 确保应用内所有注册了该广播接收器的组件都能收到通知
|
|
||||||
* [新增] 携带当前剩余秒数信息
|
|
||||||
*/
|
|
||||||
private void sendServiceDestroyedBroadcast() {
|
|
||||||
LogUtils.i(TAG, "准备发送服务销毁本地广播");
|
|
||||||
try {
|
|
||||||
Intent intent = new Intent(ACTION_SERVICE_DESTROYED);
|
|
||||||
// 新增:将当前剩余秒数放入广播附加数据中
|
|
||||||
intent.putExtra(EXTRA_REMAINING_SECONDS, mRemainingMillis / 1000);
|
|
||||||
|
|
||||||
mLocalBroadcastManager.sendBroadcast(intent);
|
|
||||||
LogUtils.i(TAG, "服务销毁广播发送成功,Action: " + ACTION_SERVICE_DESTROYED);
|
|
||||||
LogUtils.i(TAG, "广播附加数据 - 剩余秒数: " + (mRemainingMillis / 1000));
|
|
||||||
} catch (Exception e) {
|
|
||||||
LogUtils.e(TAG, "发送服务销毁广播失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送倒计时心跳的本地广播
|
|
||||||
* 每秒执行一次,携带当前剩余秒数和总时长
|
|
||||||
*/
|
|
||||||
private void sendCountdownTickBroadcast() {
|
|
||||||
try {
|
|
||||||
Intent intent = new Intent(ACTION_COUNTDOWN_TICK);
|
|
||||||
intent.putExtra(EXTRA_REMAINING_SECONDS, mRemainingMillis / 1000);
|
|
||||||
intent.putExtra(EXTRA_TOTAL_SECONDS, mTotalMillis / 1000);
|
|
||||||
|
|
||||||
mLocalBroadcastManager.sendBroadcast(intent);
|
|
||||||
// 日志已精简,只在关键节点打印
|
|
||||||
} catch (Exception e) {
|
|
||||||
LogUtils.e(TAG, "发送倒计时广播失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public IBinder onBind(Intent intent) {
|
|
||||||
LogUtils.i(TAG, "服务 onBind,不支持绑定操作");
|
|
||||||
// 不支持绑定
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========================= 私有辅助方法 =========================
|
|
||||||
/**
|
|
||||||
* 校验令牌有效性
|
|
||||||
*/
|
|
||||||
private boolean isValidToken(Intent intent) {
|
|
||||||
LogUtils.i(TAG, "调用 isValidToken 方法进行安全校验");
|
|
||||||
LogUtils.i(TAG, "入参 - intent: " + intent);
|
|
||||||
|
|
||||||
if (intent == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String incomingToken = intent.getStringExtra(EXTRA_SECURITY_TOKEN);
|
|
||||||
LogUtils.i(TAG, "接收到外部传入令牌: " + incomingToken);
|
|
||||||
LogUtils.i(TAG, "本地校验令牌: " + mValidToken);
|
|
||||||
return mValidToken.equals(incomingToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 启动循环任务
|
|
||||||
*/
|
|
||||||
private void startLoopTask() {
|
|
||||||
LogUtils.i(TAG, "启动倒计时循环任务");
|
|
||||||
mHandler.removeCallbacks(mLoopTaskRunnable);
|
|
||||||
mHandler.postDelayed(mLoopTaskRunnable, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 循环任务心跳
|
|
||||||
* 核心逻辑:先递减时长,再发送广播,确保0秒能被正确发送
|
|
||||||
*/
|
|
||||||
private final Runnable mLoopTaskRunnable = new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
if (sIsServiceRunning) {
|
|
||||||
// 核心修复:先递减剩余时长
|
|
||||||
if (mRemainingMillis >= 1000) {
|
|
||||||
mRemainingMillis -= 1000;
|
|
||||||
} else {
|
|
||||||
// 兜底:防止负数,直接置为0
|
|
||||||
mRemainingMillis = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算并发送当前剩余秒数
|
|
||||||
long remainingSeconds = mRemainingMillis / 1000;
|
|
||||||
// 日志已精简,只在非0状态打印,避免日志刷屏
|
|
||||||
if (remainingSeconds > 0) {
|
|
||||||
LogUtils.i(TAG, "循环心跳:剩余 " + remainingSeconds + " 秒");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送倒计时广播
|
|
||||||
sendCountdownTickBroadcast();
|
|
||||||
|
|
||||||
// 递归调用,形成循环
|
|
||||||
startLoopTask();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||