Compare commits
91 Commits
positions-
...
positions-
| Author | SHA1 | Date | |
|---|---|---|---|
| e99d7ebc06 | |||
| 07a286e7e0 | |||
| 7761c80275 | |||
| e1f6c3de05 | |||
| 84c616cbda | |||
| 9d868f216c | |||
| 79b0320fb9 | |||
| 173290333d | |||
| f9ef5ab16e | |||
| 748661b984 | |||
| 2cfc29845e | |||
| 389300433c | |||
| 4a65e16427 | |||
| 21d7da20d3 | |||
| 66ea626fbd | |||
| f3b3036591 | |||
| 28ecc605e1 | |||
| 523a8e49e0 | |||
| 59a9e0ee45 | |||
| cbf1341435 | |||
| dadf573675 | |||
| 7420a5cd48 | |||
| dc6a589db4 | |||
| e3f47043ef | |||
| a825951aad | |||
| 79cb841349 | |||
| d3c40efffa | |||
| 876237fa04 | |||
| 89604946e2 | |||
| c509a0126c | |||
| 07a7409d10 | |||
| f1a7313ac1 | |||
| 41492a2251 | |||
| b5431ccac2 | |||
| ee3e202ecf | |||
| 5e7828cf2b | |||
| 6c8867e15c | |||
| 4c5df10c54 | |||
| c0084cd160 | |||
| f89dbede9c | |||
| c946d7af3a | |||
| b8ddd87e66 | |||
| 5ffc049790 | |||
| 0fcf1c5952 | |||
| 610d3811db | |||
| 2d949eb5a3 | |||
| e6940805d9 | |||
| 1641424276 | |||
| 5d1cdff283 | |||
| da66cea1e5 | |||
| 5eb7441dc7 | |||
| 5f3168e17f | |||
| e3c4bab6c9 | |||
| 6d907e46cb | |||
| 5ab16c2387 | |||
| 2af6427ca8 | |||
| b8c70bef98 | |||
| 7713d6c460 | |||
| 73c69bd665 | |||
| a076fe50cd | |||
| 1512b76c36 | |||
| 850b9af6ec | |||
| 31c1592086 | |||
| b3976a8633 | |||
| ea896228d7 | |||
| d49ecb3943 | |||
| ad3aecf867 | |||
| c417d9732a | |||
| 7bd1357c8c | |||
| 16a2c3c0c8 | |||
| b747d83972 | |||
| f2788dda96 | |||
| ea3a66bebe | |||
| a53a0cbcdc | |||
| 94ac2d9f9c | |||
| 11d7846cd2 | |||
| 21e464bf5e | |||
| 47f27b6bcb | |||
| 013ad7ff1b | |||
| ab6fd0c6b5 | |||
| 3c1afc1c0b | |||
| 395920743b | |||
| 25f4c79860 | |||
| 5440c1ad39 | |||
| 47dd4698b8 | |||
| 861ccb5832 | |||
| 8b1f119e44 | |||
| c6495d1859 | |||
| 15197b123c | |||
| df2e91d390 | |||
| 61ca0d2672 |
236
README.md
236
README.md
@@ -1,175 +1,103 @@
|
||||
## OriginMaster
|
||||
【OriginMaster】WinBoLL 源生态计划。正如话,我需要一个 Point, 去撬动成个地球。
|
||||
WinBoLL系列项目各个项目源码合并推送方向:
|
||||
WinBoLL=>APPBase=>OriginMaster
|
||||
WinBoLL=>AES=>OriginMaster
|
||||
WinBoLL=>PowerBell=>OriginMaster
|
||||
WinBoLL=>Positions=>OriginMaster
|
||||
仓库规划概述:
|
||||
☆ WinBoLL,APPBase,AES,PowerBell与Positions都是开发库。
|
||||
☆ OriginMaster 是分支汇总存档库。
|
||||
☆ OriginMaster 不适宜作为开发库克隆使用,不是应用开发基础库,而是汇总资料库。
|
||||
☆ WinBoLL 库可以作为应用开发的基础继承模板。
|
||||
# WinBoLL 源生态计划项目说明书
|
||||
|
||||
########
|
||||
## ☁ ☁ ☁ WinBoLL APP ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ WinBoLL Studio Android 应用开源项目。☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ WinBoLL 网站地址 https://www.winboll.cc/ ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ WinBoLL 源码地址 <https://gitea.winboll.cc/Studio/WinBoLL.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ GitHub 源码地址 <https://github.com/ZhanGSKen/WinBoLL.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ 码云 源码地址 <https://gitee.com/zhangsken/winboll.git> ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ 在 jitpack.io 托管的 APPBase 类库源码<https://github.com/ZhanGSKen/APPBase.git> ☁ ☁ ☁ ☁
|
||||
# ☁ ☁ ☁ 在 jitpack.io 托管的 AES 类库源码<https://github.com/ZhanGSKen/AES.git> ☁ ☁ ☁ ☁
|
||||
## WinBoLL 提问
|
||||
同样是 /sdcard 目录,在开发 Android 应用时,
|
||||
能否实现手机编译与电脑编译的源码同步。
|
||||
☁因而 WinBoLL 项目组诞生了。
|
||||
## 一、项目概述
|
||||
|
||||
## WinBoLL 项目组研发计划
|
||||
致力于把 WinBoLL-APP 应用在手机端 Android 项目开发。
|
||||
也在探索 https://gitea.winboll.cc/<WinBoLL 项目组>/APP.git 应用于 WinBoLL-APP APK 分发。
|
||||
更想进阶 https://github.com/<WinBoLL 项目组>/APP.git 应用于 WinBoLL-APP Beta APK 分发。
|
||||
### 1. 核心定位
|
||||
WinBoLL 手机源码计划,旨在通过核心项目 WinBoLL 构建手机端与服务器端的 Android 项目的开发源码生态。实现手机与服务器的源码的联合开发。
|
||||
|
||||
## WinBoLL-APP 汗下...
|
||||
#### ☁应用何置如此呢。且观用户云云。
|
||||
### 2. 仓库架构
|
||||
#### **仓库类型:功能说明**
|
||||
☆ 基础项目分支 WinBoLL:手机端安卓应用开发基础模板。
|
||||
☆ 应用项目分支 APPBase、AES、PowerBell、Positions**:安卓应用单一管理系列项目。
|
||||
☆ 源码汇总管理 OriginMaster**:各类分支源码合并存档,不适宜作为开发库使用。
|
||||
|
||||
#### ☁ 正当下 ☁ ###
|
||||
#### ☁ 且容傻家叙说 ☁ WinBoLL-APP 应用场景
|
||||
### ☁ WinBoLL 设备资源概述
|
||||
#### ☁ 1. Raid Disk.
|
||||
概述:这是一个矩阵存储类设备。
|
||||
优点:该设备具有数据容错存储功能,
|
||||
数据存储具有特长持久性。
|
||||
缺点:设备使用能源消耗比较高,
|
||||
设备存取速度一般。
|
||||
|
||||
#### ☁ 2. Data Disk.
|
||||
概述:这是一个普通硬盘存储设备
|
||||
优点:该设备独立于操作系统,
|
||||
数据持久性一般,
|
||||
存取能源消耗小于 Raid Disk。
|
||||
缺点:数据存储速度一般,存储能源消耗一般。
|
||||
### 3. 源码合并管理推送路线图
|
||||
⚠️ **注意**:仅仅展示不同应用模块源码的综合管理路线。分支合并操作时,必须具备 Git 管理经验。
|
||||
|
||||
#### ☁ 3. SSD Disk.
|
||||
概述:这是一个 SSD 硬盘存储设备。
|
||||
优点:存取速度快于 Data Disk 与 Raid Disk,
|
||||
存取能源消耗小于 Data Disk 与 Raid Disk。
|
||||
缺点:数据持久性一般,
|
||||
设备位于操作系统内部文件系统。
|
||||
数据持久性与操作系统挂钩。
|
||||
|
||||
#### ☁ 4. WinBoLL 用户资源概述。
|
||||
1> /home/<用户名> 位于 WinBoLL 操作系统目录下。
|
||||
2> /rdisk/<用户名> 挂载用户 Raid Disk.
|
||||
3> /data/<用户名> 挂载用户 Data Disk.
|
||||
4> /sdcard/<用户名> 挂载用户 SSD Disk.
|
||||
★ WinBoLL → APPBase → OriginMaster
|
||||
★ WinBoLL → AES → OriginMaster
|
||||
★ WinBoLL → PowerBell → OriginMaster
|
||||
★ WinBoLL → Positions → OriginMaster
|
||||
|
||||
#### ☁ 5. WinBoLL-APP 用户资源概述。
|
||||
1> /sdcard 挂载用户手机 SD 存储/storage/emulated/0
|
||||
## 二、WinBoLL 项目核心信息
|
||||
|
||||
### ☁ 稍稍歇 ☁ ###
|
||||
### ☁ 急急停 ☁ WinBoLL 应用前置条件
|
||||
☁ WinBoLL 主机建立 1Panel MySQL 应用。
|
||||
☁ WinBoLL 主机建立 1Panel Gitea 应用。
|
||||
☁ WinBoLL 主机设置 WinBoLL 应用为非登录状态。
|
||||
☁ WinBoLL 主机建立 WinBoLL 账户与 WinBoLL 用户组。
|
||||
☁ WinBoLL 账户 User ID 为: J。
|
||||
☁ WinBoLL 用户组 Group ID 为: Studio。
|
||||
☁ WinBoLL 主机 WinBoLL 1Panel Gitea 建立 WinBoLL 工作组。
|
||||
☁ WinBoLL 主机 WinBoLL 1Panel Gitea 用户项目 APK 编译输出目录为 /sdcard/WinBoLLStudio/<用户名>/APKs/
|
||||
☁ WinBoLL 项目配置文件示例为 "<WinBoLL 项目根目录>/.winboll/winboll.properties-demo"(WinBoLL 项目已设置)
|
||||
☁ WinBoLL 项目配置文件为 "<WinBoLL 项目根目录>/.winboll/winboll.properties"
|
||||
☁ WinBoLL 项目配置文件设定为源码提交时忽略。(WinBoLL 项目已设置)
|
||||
☁ Gradle 项目配置文件示例为 "<WinBoLL 项目根目录>/.winboll/local.properties-demo"(WinBoLL 项目已设置)
|
||||
☁ Gradle 项目配置文件为 "<WinBoLL 项目根目录>/local.properties"(WinBoLL 项目已设置)
|
||||
☁ Gradle 项目配置文件设定为源码提交时忽略。(WinBoLL 项目已设置)
|
||||
### 1. 项目简介
|
||||
☆ WinBoLL 项目是为手机端开发Android 项目的需求而设计的项目。
|
||||
|
||||
### ☁ 登高处 ☁ WinBoLL 应用需求规划
|
||||
☁ WinBoLL 主机建立 WinBoLL 客户端用户数据库为 MySQL winbollclient 数据库。
|
||||
☁ WinBoLL 主机设置 WinBoLL 客户端用户信息存储在 winbollclient 数据库中。
|
||||
☁ MySQL winbollclient 数据库中
|
||||
WinBoLL 客户端用户信息设定为:
|
||||
<用户名, 验证密码, 验证邮箱, 验证手机, 唯一存储令牌Token, 备用验证邮箱>。
|
||||
☁ WinBoLL 项目源码仓库托管在 WinBoLL 1Panel Gitea 目录 /opt/1panel/apps/gitea/gitea/data/git/repositories/studio/app.git中。
|
||||
☁ WinBoLL 主机提供 WinBoLL 1Panel Gitea 应用的 WinBoLL 项目源码仓库存取功能。(Gitea 应用已提供)
|
||||
☁ WinBoLL 主机提供 WinBoLL Gitea 项目仓库存档功能。(Gitea 应用已提供)
|
||||
☁ 提供 WinBoLL 客户端用户登录功能。(Gitea 应用已提供)
|
||||
### 2. 官方资源
|
||||
#### ☆ 官方网站**:https://www.winboll.cc/
|
||||
#### ☆ 源码地址:
|
||||
★ Gitea:https://gitea.winboll.cc/Studio/WinBoLL.git
|
||||
★ GitHub:https://github.com/ZhanGSKen/WinBoLL.git
|
||||
★ 码云:https://gitee.com/zhangsken/winboll.git
|
||||
|
||||
### ☁ 看远方 ☁ ###
|
||||
### ☁ 心忧虑 ☁ WinBoLL-APP 应用前置需求
|
||||
☁ WinBoLL-APP WinBoLL 项目根目录设定为手机的 /sdcard/WinBoLLStudio/Sources 目录。(需要用户手动建立文件夹)
|
||||
☁ WinBoLL-APP 具有手机 /sdcard/WinBoLL 目录的存储权限。(需要手机操作系统授权)
|
||||
☁ WinBoLL-APP WinBoLL 项目仓库源码存储路径为 /sdcard/WinBoLLStudio/Sources/APP.git(需要用户手动建立文件夹)
|
||||
☁ WinBoLL-APP 项目 APK 编译输出目录为 /sdcard/WinBoLLStudio/APKs/
|
||||
☁ WinBoLL-APP 应用签名验证可定制化。(WinBoLL 项目已提供)
|
||||
☁ WinBoLL-APP 与系列衍生 APP 应用共享 cc.winboll.studio 命名空间资源。(WinBoLL 项目已提供)
|
||||
☁ WinBoLL-APP 用户客户端信息存储在命名空间为 WinBoLL APP MySQLLite 应用的 winbollappclient 数据库中。
|
||||
☁ WinBoLL-APP MySQLLite 应用的 winbollappclient 数据库中,
|
||||
WinBoLL 用户客户端信息设定为:
|
||||
<用户名, 唯一存储令牌Token>。
|
||||
## 三、应用编译环境检查问题
|
||||
### 核心判断条件:
|
||||
☆ WinBoLL 项目以文件夹 `"/sdcard/WinBoLLStudio/APKs"` 是否存在为判断环境编译输出条件,因为编译输出的APK文件需要一个可供保存的环境。
|
||||
|
||||
### ☁ 云游四方 ☁ ###
|
||||
### ☁ 呔! ☁ WinBoLL-APP 应用需求规划
|
||||
☁ 如要使用 WinBoLL Android 项目的 Gradle 编译功能,则需要设置以下两个文件夹。
|
||||
☁ 1. 则需要建立数据存储目录 /sdcard/WinBoLLStudio/APKs。
|
||||
WinBoLL 项目源码编译出来的安装包会拷贝一份到 /sdcard/WinBoLLStudio/APKs 目录下。
|
||||
☁ 2. 则需要建立数据存储目录 /sdcard/AppProjects。
|
||||
WinBoLL 项目源码编译出来的安装包会拷贝一份并命名 "app.apk" 的安装文件为到 /sdcard/AppProjects 目录下。
|
||||
☆ 文件夹"/sdcard/WinBoLLStudio/APKs" 目录条件设置方法:
|
||||
***Linux 服务器端方面***:建立 `/sdcard/WinBoLLStudio/APKs` 目录即可。
|
||||
***手机开发端方面***:建立 `"/sdcard/WinBoLLStudio/APKs"` 目录(即 `"/storage/emulated/0/WinBoLLStudio/APKs"` 目录) 即可。
|
||||
|
||||
## 四、前置条件
|
||||
|
||||
### ☁ 吁! ☁ WinBoLL-APP 共享计划前景
|
||||
☁ WinBoLL-APP 将会实现 https://winboll.cc/api 访问功能。
|
||||
☁ WinBoLL-APP 将会实现手机端 Android 应用的开发与管理功能。
|
||||
### 1. WinBoLL APP 开发环境配置介绍
|
||||
#### WinBoLL APK 编译输出内容包括:
|
||||
☆ "/sdcard/WinBoLLStudio/APKs"` 目录内的所有应用分支的 APK 文件。
|
||||
☆ "/sdcard/AppProjects/app.apk"文件。
|
||||
#### WinBoLL APK 源码命名空间规范
|
||||
☆ WinBoLL 项目使用 "cc.winboll.studio" 作为源码命名空间。在此命名空间下进行源码定义。
|
||||
|
||||
## ☁ WinBoLL ☁ WinBoLL 主机忧虑
|
||||
☁ WinBoLL 将会提供 gitea.winboll.cc 域名用户注册登录功能。
|
||||
☁ WinBoLL 将会提供 WinBoLL-APP 及其衍生应用的 Gitea 仓库管理服务。
|
||||
☁ WinBoLL 将会提供 winboll.cc 域名 WinBoLL 项目组注册登录功能。
|
||||
## 五、核心需求规划
|
||||
|
||||
# 本项目要实际运用需要注意以下几个步骤:
|
||||
# 在项目根目录下:
|
||||
## ★. 项目模块编译环境设置(必须),settings.gradle-demo 要复制为 settings.gradle,并取消相应项目模块的注释。
|
||||
## ★. 项目模块编译环境设置(必须) 在根目录拷贝 gradle.properties-androidx-demo 或者 gradle.properties-android-demo 文件为 gradle.properties。
|
||||
## ★. 项目 Android SDK 编译环境设置(可选),local.properties-demo 要复制为 local.properties,并按需要设置 Android SDK 目录。
|
||||
## ★. 应用签名密钥 keystore 设置问题。一般调试编译只需用【Termux】cd 进 GenKeyStore 目录执行 $ bash gen_debug_keystore.sh 命令即可完成设置。
|
||||
## ☆. 应用 WiBoLL 签名密钥配置问题<非必须考虑>。设置时需要 clone 【keystore】模块源码并拷贝模块目录的 appkey.jks 与 appkey.keystore 到项目根目录即可。
|
||||
## ☆. 类库型模块编译环境设置(可选),winboll.properties-demo 要复制为 winboll.properties,并按需要设置 WinBoLL Maven 库登录用户信息, 和 APK 文件额外输出路径。
|
||||
### 1. WinBoLL 应用安全验证需求
|
||||
#### ☆ 支持访问 https://console.winboll.cc/ 服务器以校验应用包签名与版本。
|
||||
|
||||
### 2. 手机端源码开发管理需求
|
||||
#### ☆ 支持切换不同 WinBoLL 分支,以开发不同安卓应用。
|
||||
|
||||
# ☆类库型项目编译方法
|
||||
## 先编译类库对应的模块测试项目
|
||||
### 修改模块测试项目的 build.properties 文件
|
||||
设置属性 libraryProject=<类库项目模块文件夹名称>
|
||||
### 再编译测试项目
|
||||
$ bash .winboll/bashPublishAPKAddTag.sh <应用项目模块文件夹名称>
|
||||
#### 测试项目编译后,编译器会复制一份 APK 到 路径:"/sdcard/WinBoLLStudio/APKs/<项目根目录名称>/tag/" 文件夹。
|
||||
#### 若是 winboll.properties 文件的 [ExtraAPKOutputPath] 属性设置了路径。编译器也会复制一份 APK 到这个路径。
|
||||
### 最后编译类库项目
|
||||
$ bash .winboll/bashPublishLIBAddTag.sh <类库项目模块文件夹名称>
|
||||
#### 类库模块编译命令执行后,编译器会发布到 WinBoLL Nexus Maven 库:Maven 库地址可以参阅根项目目录配置 build.gradle 文件。
|
||||
|
||||
# ☆应用型项目编译方法
|
||||
## 直接调用以下命令编译应用型项目
|
||||
$ bash .winboll/bashPublishAPKAddTag.sh <应用项目模块文件夹名称>
|
||||
#### 应用模块编译命令执行后,编译器会复制一份 APK 到
|
||||
#### 测试项目编译后,编译器会复制一份 APK 到 路径:"/sdcard/WinBoLLStudio/APKs/<项目根目录名称>/tag/" 文件夹。
|
||||
#### 若是 winboll.properties 文件的 [ExtraAPKOutputPath] 属性设置了路径。编译器也会复制一份 APK 到这个路径。
|
||||
## 六、编译与使用指南
|
||||
|
||||
## ☆应用调试编译方法
|
||||
使用以下命令编译调试:
|
||||
### 1. 项目初始化(必须)
|
||||
#### 1. 复制 `settings.gradle-demo` 为 `settings.gradle`。编辑 `settings.gradle` 文件内容,取消对应项目模块注释。
|
||||
#### 2. 复制 `gradle.properties-androidx-demo` (Android X 项目) 或 `gradle.properties-android-demo` (基本 Android 项目) 为 `gradle.properties`。
|
||||
#### 3. 复制(可选)`local.properties-demo` 为 `local.properties`,编辑 `local.properties` 文件内容,配置 Android SDK 目录。
|
||||
#### 4. **签名设置**:
|
||||
☆ **调试编译秘钥制作**:使用 Termux 应用终端,cd 进入 GenKeyStore 目录,运行 `bash gen_debug_keystore.sh` 脚本即可生成应用调试秘钥。
|
||||
☆ **应用秘钥配置方法**:拷贝调试编译秘钥制作生成的 `appkey.jks` 与 `appkey.keystore` 文件到项目根目录即可。
|
||||
|
||||
### Beta调试使用:
|
||||
$ bash gradlew assembleBetaDebug
|
||||
## 七、应用编译命令介绍
|
||||
|
||||
### Stage调试使用:
|
||||
$ bash gradlew assembleStageDebug
|
||||
### (1)类库型模块配置要点
|
||||
#### 1. **优先修改配置文件**:优先修改应用测试项目(目录为 `"<WinBoLl根目录>/<类库测试应用>/"`)内 `build.properties` 文件,设置对应的类库项目名称:`libraryProject=<类库项目模块名>`。
|
||||
#### 2. **编译优先启动步骤**:使用 Termux 应用,进入 `"<WinBoLl根目录>"`,运行 `$ bash .winboll/bashPublishAPKAddTag.sh <类库测试项目模块名>` 命令。运行后可生成测试项目与类库项目的编译参数文件 `build.properties`。生成的 `build.properties` 文件有两份,一份在测试项目模块的文件夹内,一份在类库项目本身的模块文件夹内。
|
||||
#### 3. **最后类库编译发布步骤**:使用 Termux 应用,进入 `"<WinBoLl根目录>"`,运行 `$ bash .winboll/bashPublishLIBAddTag.sh <类库项目模块名>` 命令。运行后可发布至 WinBoLL Nexus Maven 库、本地 maven 目录或者是通用默认的 Gradle Maven 库。
|
||||
|
||||
### 若是 winboll.properties 文件的 [ExtraAPKOutputPath] 属性设置了路径。编译器也会复制一份 APK 到这个路径。
|
||||
### (2)单一应用型模块与类库测试型模块配置要点
|
||||
#### ☆ APK 编译方法:
|
||||
使用 Termux 应用,进入 `"<WinBoLl根目录>"`,运行 `$ bash .winboll/bashPublishAPKAddTag.sh <应用项目模块名>`。
|
||||
#### ☆ 运行后的 APK 输出路径:
|
||||
★ 默认路径 (`$ bash gradlew assembleBetaDebug` 任务):APK 在 `/sdcard/WinBoLLStudio/APKs/<项目根目录名称>/debug/` 目录。
|
||||
★ 默认路径 (`$ bash assembleStageRelease` 任务):APK 在 `/sdcard/WinBoLLStudio/APKs/<项目根目录名称>/tag/` 目录。
|
||||
★ 额外输出路径:(假设 `winboll.properties` 文件已配置 `ExtraAPKOutputPath` 属性) 输出至 `ExtraAPKOutputPath` 属性配置的目录下。
|
||||
|
||||
# 应用版本号命名方式
|
||||
## statge 渠道
|
||||
V<应用开发环境编号><应用功能变更号><应用调试阶段号>
|
||||
如:APPBase_15.7.0
|
||||
## beta 渠道
|
||||
V<应用开发环境编号><应用功能变更号><应用调试阶段号>-beta<调试编译计数>_<调试编译时间(分钟+秒钟)>
|
||||
如:APPBase_15.9.6-beta8_5413
|
||||
### (3)手机端应用调试命令介绍
|
||||
#### ☆ Beta 渠道调试命令
|
||||
$bash gradlew assembleBetaDebug
|
||||
|
||||
#### ☆ Stage 渠道调试命令
|
||||
$bash gradlew assembleStageDebug
|
||||
|
||||
### (4)服务器端开发命令介绍
|
||||
##### ☆ Stage 渠道应用发布命令为:
|
||||
("<WinBoLl根目录>/settings.gradle"文件需要配置编译模块开启参数,拷贝 settings.gradle-demo 为 settings.gradle 文件取消对应的分支配置部分即可。)
|
||||
$bash .winboll/bashPublishAPKAddTag.sh <应用项目模块名>
|
||||
或者是
|
||||
$bash gradlew assembleStageRelease
|
||||
|
||||
|
||||
## 八、WinBoLL 应用 APK 版本号命名规则
|
||||
### ☆ Stage 渠道:
|
||||
#### V<应用开发环境编号><应用功能变更号><应用调试阶段号> (示例: APPBase_15.7.0 )
|
||||
### ☆ Beta 渠道:
|
||||
#### V<应用开发环境编号><应用功能变更号><应用调试阶段号>-beta<调试编译计数>_<调试编译时间(分钟+秒钟)> (示例: APPBase_15.9.6-beta8_5413 )
|
||||
|
||||
44
build.gradle
44
build.gradle
@@ -1,6 +1,15 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
repositories {
|
||||
maven { url 'https://maven.aliyun.com/repository/public/' }
|
||||
maven { url 'https://maven.aliyun.com/repository/google/' }
|
||||
maven { url 'https://maven.aliyun.com/repository/gradle-plugin/' }
|
||||
maven { url 'https://dl.bintray.com/ppartisan/maven/' }
|
||||
maven { url "https://clojars.org/repo/" }
|
||||
maven { url "https://jitpack.io" }
|
||||
mavenCentral()
|
||||
google()
|
||||
|
||||
mavenLocal {
|
||||
// 设置本地Maven仓库路径
|
||||
url 'file:///sdcard/.m2/repository/'
|
||||
@@ -11,19 +20,6 @@ buildscript {
|
||||
maven { url "https://nexus.winboll.cc/repository/maven-public/" }
|
||||
// "WinBoLL Snapshot"
|
||||
maven { url "https://nexus.winboll.cc/repository/maven-snapshots/" }
|
||||
|
||||
maven { url 'https://maven.aliyun.com/repository/public/' }
|
||||
maven { url 'https://maven.aliyun.com/repository/google/' }
|
||||
maven { url 'https://maven.aliyun.com/repository/gradle-plugin/' }
|
||||
maven { url 'https://dl.bintray.com/ppartisan/maven/' }
|
||||
maven { url "https://clojars.org/repo/" }
|
||||
maven { url "https://jitpack.io" }
|
||||
mavenCentral()
|
||||
google()
|
||||
//println "mavenLocal : ==========="
|
||||
//println mavenLocal().url
|
||||
//println "mavenLocal : ==========="
|
||||
//mavenLocal()
|
||||
}
|
||||
dependencies {
|
||||
// 适配MIUI12
|
||||
@@ -35,6 +31,15 @@ buildscript {
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
maven { url 'https://maven.aliyun.com/repository/public/' }
|
||||
maven { url 'https://maven.aliyun.com/repository/google/' }
|
||||
maven { url 'https://maven.aliyun.com/repository/gradle-plugin/' }
|
||||
maven { url 'https://dl.bintray.com/ppartisan/maven/' }
|
||||
maven { url "https://clojars.org/repo/" }
|
||||
maven { url "https://jitpack.io" }
|
||||
mavenCentral()
|
||||
google()
|
||||
|
||||
mavenLocal {
|
||||
// 设置本地Maven仓库路径
|
||||
url 'file:///sdcard/.m2/repository/'
|
||||
@@ -45,19 +50,6 @@ allprojects {
|
||||
maven { url "https://nexus.winboll.cc/repository/maven-public/" }
|
||||
// "WinBoLL Snapshot"
|
||||
maven { url "https://nexus.winboll.cc/repository/maven-snapshots/" }
|
||||
|
||||
maven { url 'https://maven.aliyun.com/repository/public/' }
|
||||
maven { url 'https://maven.aliyun.com/repository/google/' }
|
||||
maven { url 'https://maven.aliyun.com/repository/gradle-plugin/' }
|
||||
maven { url 'https://dl.bintray.com/ppartisan/maven/' }
|
||||
maven { url "https://clojars.org/repo/" }
|
||||
maven { url "https://jitpack.io" }
|
||||
mavenCentral()
|
||||
google()
|
||||
//println "mavenLocal : ==========="
|
||||
//println mavenLocal().url
|
||||
//println "mavenLocal : ==========="
|
||||
//mavenLocal()
|
||||
}
|
||||
ext {
|
||||
// 定义全局变量,常用于版本管理
|
||||
|
||||
@@ -81,8 +81,8 @@ dependencies {
|
||||
//api 'androidx.fragment:fragment:1.1.0'
|
||||
|
||||
// WinBoLL库 nexus.winboll.cc 地址
|
||||
api 'cc.winboll.studio:libaes:15.15.2'
|
||||
api 'cc.winboll.studio:libappbase:15.15.11'
|
||||
api 'cc.winboll.studio:libaes:15.15.9'
|
||||
api 'cc.winboll.studio:libappbase:15.15.21'
|
||||
|
||||
// WinBoLL备用库 jitpack.io 地址
|
||||
//api 'com.github.ZhanGSKen:AES:aes-v15.12.9'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Sat Jan 24 21:01:09 HKT 2026
|
||||
stageCount=13
|
||||
#Mon May 04 11:31:30 HKT 2026
|
||||
stageCount=22
|
||||
libraryProject=
|
||||
baseVersion=15.12
|
||||
publishVersion=15.12.12
|
||||
publishVersion=15.12.21
|
||||
buildCount=0
|
||||
baseBetaVersion=15.12.13
|
||||
baseBetaVersion=15.12.22
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Typeface;
|
||||
@@ -13,8 +14,6 @@ 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;
|
||||
@@ -22,9 +21,13 @@ import android.widget.HorizontalScrollView;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.positions.services.MainService;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.Closeable;
|
||||
@@ -42,28 +45,121 @@ import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* 全局Application应用入口类
|
||||
* 功能概括:
|
||||
* 1. 全局初始化管理、工具类框架初始化
|
||||
* 2. 应用全局空转状态统一管理
|
||||
* 3. IO读写通用工具封装
|
||||
* 4. 全局崩溃捕获、崩溃日志本地保存、崩溃弹窗页面
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @CreateTime 2026/05/03 12:23:00
|
||||
* @EditTime 2026/05/03 15:02:47
|
||||
*/
|
||||
public class App extends GlobalApplication {
|
||||
|
||||
//===================== 全局静态常量与变量 =====================
|
||||
public static final String TAG = "App";
|
||||
private static Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
|
||||
|
||||
private static App sInstance;
|
||||
|
||||
private static final String SP_NAME = "app_idle_config";
|
||||
private static final String SP_KEY_IDLE_RUNNING = "is_idle_running";
|
||||
|
||||
// 应用全局空转状态标记
|
||||
private static boolean isAppIdleRunning = false;
|
||||
|
||||
//===================== 空转状态对外方法 =====================
|
||||
/**
|
||||
* 获取当前应用空转运行状态
|
||||
* @return 是否处于空转状态
|
||||
*/
|
||||
public static boolean isAppIdleRunning() {
|
||||
if (isAppIdleRunning) {
|
||||
LogUtils.d(TAG, "isAppIdleRunning -> 当前应用处于空转运行状态");
|
||||
}
|
||||
return isAppIdleRunning;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置应用空转运行状态
|
||||
* @param idleRunning 空转开关状态
|
||||
*/
|
||||
public static void setAppIdleRunning(boolean idleRunning) {
|
||||
LogUtils.d(TAG, "setAppIdleRunning -> 传入参数 idleRunning = " + idleRunning);
|
||||
if (isDebugging()) {
|
||||
isAppIdleRunning = idleRunning;
|
||||
saveIdleRunningToSp(idleRunning);
|
||||
LogUtils.i(TAG, "setAppIdleRunning -> 调试模式,空转状态设置生效并持久化");
|
||||
|
||||
// 重启MainService服务以同步新的空转状态(stopService对未运行服务无效,故无需判断状态)
|
||||
if (sInstance != null) {
|
||||
LogUtils.i(TAG, "setAppIdleRunning -> 重启MainService服务以同步空转状态");
|
||||
Intent serviceIntent = new Intent(sInstance, MainService.class);
|
||||
sInstance.stopService(serviceIntent);
|
||||
sInstance.startService(serviceIntent);
|
||||
}
|
||||
} else {
|
||||
LogUtils.i(TAG, "setAppIdleRunning -> 非调试模式,空转设置无效");
|
||||
LogUtils.i(TAG, "Non-debug state, app idle setting is meaningless.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从SharedPreferences加载空转状态
|
||||
*/
|
||||
private static void loadIdleRunningFromSp() {
|
||||
try {
|
||||
if (sInstance != null) {
|
||||
SharedPreferences sp = sInstance.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
|
||||
isAppIdleRunning = sp.getBoolean(SP_KEY_IDLE_RUNNING, false);
|
||||
LogUtils.i(TAG, "loadIdleRunningFromSp -> 从SP加载空转状态:" + isAppIdleRunning);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "loadIdleRunningFromSp -> 加载空转状态失败:" + e.getMessage());
|
||||
isAppIdleRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存空转状态到SharedPreferences
|
||||
* @param idleRunning 空转状态
|
||||
*/
|
||||
private static void saveIdleRunningToSp(boolean idleRunning) {
|
||||
try {
|
||||
if (sInstance != null) {
|
||||
SharedPreferences sp = sInstance.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
|
||||
sp.edit().putBoolean(SP_KEY_IDLE_RUNNING, idleRunning).apply();
|
||||
LogUtils.i(TAG, "saveIdleRunningToSp -> 空转状态已保存:" + idleRunning);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "saveIdleRunningToSp -> 保存空转状态失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
//===================== 应用生命周期 =====================
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
setIsDebugging(BuildConfig.DEBUG);
|
||||
|
||||
WinBoLLActivityManager.init(this);
|
||||
|
||||
// 初始化 Toast 框架
|
||||
sInstance = this;
|
||||
LogUtils.i(TAG, "onCreate -> 全局Application初始化开始");
|
||||
|
||||
setIsDebugging(BuildConfig.DEBUG);
|
||||
WinBoLLActivityManager.init(this);
|
||||
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);
|
||||
|
||||
loadIdleRunningFromSp();
|
||||
|
||||
LogUtils.i(TAG, "onCreate -> 全局组件初始化全部完成");
|
||||
}
|
||||
|
||||
//===================== IO通用工具方法 =====================
|
||||
/**
|
||||
* 流式读写输入输出流
|
||||
* @param input 输入流
|
||||
* @param output 输出流
|
||||
* @throws IOException IO读写异常
|
||||
*/
|
||||
public static void write(InputStream input, OutputStream output) throws IOException {
|
||||
byte[] buf = new byte[1024 * 8];
|
||||
int len;
|
||||
@@ -72,9 +168,17 @@ public class App extends GlobalApplication {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字节数组写入指定文件
|
||||
* @param file 目标文件
|
||||
* @param data 字节数据
|
||||
* @throws IOException IO读写异常
|
||||
*/
|
||||
public static void write(File file, byte[] data) throws IOException {
|
||||
File parent = file.getParentFile();
|
||||
if (parent != null && !parent.exists()) parent.mkdirs();
|
||||
if (parent != null && !parent.exists()) {
|
||||
parent.mkdirs();
|
||||
}
|
||||
|
||||
ByteArrayInputStream input = new ByteArrayInputStream(data);
|
||||
FileOutputStream output = new FileOutputStream(file);
|
||||
@@ -85,6 +189,12 @@ public class App extends GlobalApplication {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输入流转为UTF-8字符串
|
||||
* @param input 输入流
|
||||
* @return 转换后的文本
|
||||
* @throws IOException IO读写异常
|
||||
*/
|
||||
public static String toString(InputStream input) throws IOException {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
write(input, output);
|
||||
@@ -95,20 +205,27 @@ public class App extends GlobalApplication {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量关闭IO流
|
||||
* @param closeables 可关闭对象
|
||||
*/
|
||||
public static void closeIO(Closeable... closeables) {
|
||||
for (Closeable closeable : closeables) {
|
||||
try {
|
||||
if (closeable != null) closeable.close();
|
||||
} catch (IOException ignored) {}
|
||||
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() {
|
||||
@@ -118,35 +235,56 @@ public class App extends GlobalApplication {
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册全局崩溃捕获
|
||||
*/
|
||||
public void registerGlobal(Context context) {
|
||||
registerGlobal(context, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册全局崩溃并自定义崩溃日志保存目录
|
||||
* @param crashDir 崩溃日志路径
|
||||
*/
|
||||
public void registerGlobal(Context context, String crashDir) {
|
||||
LogUtils.d(TAG, "CrashHandler -> 注册全局异常捕获器");
|
||||
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandlerImpl(context.getApplicationContext(), crashDir));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解除全局崩溃捕获
|
||||
*/
|
||||
public void unregister() {
|
||||
LogUtils.d(TAG, "CrashHandler -> 解除全局异常捕获");
|
||||
Thread.setDefaultUncaughtExceptionHandler(DEFAULT_UNCAUGHT_EXCEPTION_HANDLER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册局部前台异常捕获
|
||||
*/
|
||||
public void registerPart(Context context) {
|
||||
unregisterPart(context);
|
||||
mPartCrashHandler = new PartCrashHandler(context.getApplicationContext());
|
||||
MAIN_HANDLER.postAtFrontOfQueue(mPartCrashHandler);
|
||||
LogUtils.d(TAG, "CrashHandler -> 局部异常监听已注册");
|
||||
}
|
||||
|
||||
/**
|
||||
* 解除局部异常捕获
|
||||
*/
|
||||
public void unregisterPart(Context context) {
|
||||
if (mPartCrashHandler != null) {
|
||||
mPartCrashHandler.isRunning.set(false);
|
||||
mPartCrashHandler = null;
|
||||
LogUtils.d(TAG, "CrashHandler -> 局部异常监听已销毁");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 局部前台Looper异常捕获
|
||||
*/
|
||||
private static class PartCrashHandler implements Runnable {
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
public AtomicBoolean isRunning = new AtomicBoolean(true);
|
||||
|
||||
public PartCrashHandler(Context context) {
|
||||
@@ -159,18 +297,16 @@ public class App extends GlobalApplication {
|
||||
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();
|
||||
}
|
||||
});
|
||||
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;
|
||||
throw (RuntimeException) e;
|
||||
} else {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@@ -180,12 +316,12 @@ public class App extends GlobalApplication {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局未捕获异常处理实现
|
||||
*/
|
||||
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) {
|
||||
@@ -196,39 +332,38 @@ public class App extends GlobalApplication {
|
||||
@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());
|
||||
}
|
||||
Intent intent = new Intent(mContext, CrashActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, log);
|
||||
mContext.startActivity(intent);
|
||||
|
||||
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);
|
||||
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) {}
|
||||
} catch (Throwable ignored) {
|
||||
|
||||
}
|
||||
|
||||
LinkedHashMap<String, String> head = new LinkedHashMap<String, String>();
|
||||
head.put("Time Of Crash", time);
|
||||
@@ -236,34 +371,41 @@ public class App extends GlobalApplication {
|
||||
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("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");
|
||||
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));
|
||||
builder.append(throwable.toString());
|
||||
|
||||
return builder.toString();
|
||||
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"));
|
||||
LogUtils.d(TAG, "崩溃日志已保存至本地文件");
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
LogUtils.e(TAG, "崩溃日志写入失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统内核版本信息
|
||||
*/
|
||||
private static String getKernel() {
|
||||
try {
|
||||
return App.toString(new FileInputStream("/proc/version")).trim();
|
||||
@@ -274,6 +416,7 @@ public class App extends GlobalApplication {
|
||||
}
|
||||
}
|
||||
|
||||
//===================== 崩溃展示页面 =====================
|
||||
public static final class CrashActivity extends Activity {
|
||||
|
||||
private String mLog;
|
||||
@@ -281,15 +424,14 @@ public class App extends GlobalApplication {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.i(TAG, "CrashActivity -> 崩溃展示页面已启动");
|
||||
|
||||
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);
|
||||
@@ -302,10 +444,12 @@ public class App extends GlobalApplication {
|
||||
|
||||
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) {
|
||||
@@ -317,6 +461,9 @@ public class App extends GlobalApplication {
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* dp转px通用方法
|
||||
*/
|
||||
private static int dp2px(float dpValue) {
|
||||
final float scale = Resources.getSystem().getDisplayMetrics().density;
|
||||
return (int) (dpValue * scale + 0.5f);
|
||||
@@ -325,17 +472,17 @@ public class App extends GlobalApplication {
|
||||
@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);
|
||||
.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;
|
||||
if (item.getItemId() == android.R.id.copy) {
|
||||
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
|
||||
LogUtils.d(TAG, "CrashActivity -> 崩溃日志已复制到剪贴板");
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
@@ -346,3 +493,4 @@ public class App extends GlobalApplication {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,46 +14,382 @@ import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libaes.utils.AESThemeUtil;
|
||||
import cc.winboll.studio.libaes.utils.DevelopUtils;
|
||||
import cc.winboll.studio.libaes.views.ADsBannerView;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.positions.R;
|
||||
import cc.winboll.studio.positions.activities.AboutActivity;
|
||||
import cc.winboll.studio.positions.activities.LocationActivity;
|
||||
import cc.winboll.studio.positions.activities.SettingsActivity;
|
||||
import cc.winboll.studio.positions.activities.WinBoLLActivity;
|
||||
import cc.winboll.studio.positions.handlers.AppIdleRunningModeHandler;
|
||||
import cc.winboll.studio.positions.utils.AppConfigsUtil;
|
||||
import cc.winboll.studio.positions.utils.ServiceUtil;
|
||||
import cc.winboll.studio.positions.services.IdleGpsService;
|
||||
import cc.winboll.studio.positions.services.MainService;
|
||||
import cc.winboll.studio.positions.R;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Handler;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.content.Intent;
|
||||
import android.os.Handler;
|
||||
|
||||
/**
|
||||
* 主页面:仅负责
|
||||
* 1. 位置服务启动/停止(通过 Switch 开关控制)
|
||||
* 2. 跳转至“位置管理页(LocationActivity)”和“日志页(LogActivity)”
|
||||
* 3. Java 7 语法适配:无 Lambda、显式接口实现、兼容低版本
|
||||
* 主页面控制器
|
||||
* 功能简述:
|
||||
* 1. 位置服务启停开关控制
|
||||
* 2. 页面菜单跳转与主题管理
|
||||
* 3. 应用空转状态接收与日志实时输出展示
|
||||
* 4. 全局权限申请与权限结果回调处理
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @CreateTime 2026/05/03 12:23:00
|
||||
* @EditTime 2026/05/03 15:42:15
|
||||
*/
|
||||
public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
|
||||
// ===================== 常量定义 =====================
|
||||
public static final String TAG = "MainActivity";
|
||||
// 权限请求码(建议定义为类常量,避免魔法值)
|
||||
private static final int REQUEST_LOCATION_PERMISSIONS = 1001;
|
||||
private static final int REQUEST_BACKGROUND_LOCATION_PERMISSION = 1002;
|
||||
private static final int REQUEST_LOCATION_PERMISSIONS = 1001;
|
||||
private static final int REQUEST_BACKGROUND_LOCATION_PERMISSION = 1002;
|
||||
|
||||
// UI 控件:服务控制开关、顶部工具栏
|
||||
private Switch mServiceSwitch;
|
||||
private Button mManagePositionsButton;
|
||||
// ===================== UI控件声明 =====================
|
||||
private Toolbar mToolbar;
|
||||
// 服务相关:服务实例、绑定状态标记
|
||||
//private DistanceRefreshService mDistanceService;
|
||||
private Switch mServiceSwitch;
|
||||
private Button mManagePositionsButton;
|
||||
private ADsBannerView mADsBannerView;
|
||||
private TextView mTvIdleLog;
|
||||
private ScrollView mScrollIdleLog;
|
||||
|
||||
// ===================== 业务标记与回调 =====================
|
||||
private boolean isServiceBound = false;
|
||||
ADsBannerView mADsBannerView;
|
||||
private OnAppIdleRunningListener mIdleRunningListener;
|
||||
|
||||
/**
|
||||
* 应用空转状态回调内部接口
|
||||
*/
|
||||
public interface OnAppIdleRunningListener {
|
||||
void onIdleStatusChange(boolean isRunning);
|
||||
void onIdleLogReceive(String log);
|
||||
}
|
||||
|
||||
// ===================== 回调绑定方法 =====================
|
||||
public void setOnAppIdleRunningListener(OnAppIdleRunningListener listener) {
|
||||
this.mIdleRunningListener = listener;
|
||||
LogUtils.i(TAG, "setOnAppIdleRunningListener -> 空转监听绑定完成");
|
||||
}
|
||||
|
||||
public OnAppIdleRunningListener getOnAppIdleRunningListener() {
|
||||
return mIdleRunningListener;
|
||||
}
|
||||
|
||||
// ===================== 生命周期重写 =====================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
LogUtils.i(TAG, "onCreate -> MainActivity页面创建完成");
|
||||
|
||||
initToolbar();
|
||||
initViews();
|
||||
setLLMainBackgroundColor();
|
||||
|
||||
if (!checkLocationPermissions()) {
|
||||
LogUtils.d(TAG, "onCreate -> 定位权限未授予,发起权限申请");
|
||||
requestLocationPermissions();
|
||||
}
|
||||
|
||||
mADsBannerView = findViewById(R.id.adsbanner);
|
||||
initAppIdleHandler();
|
||||
refreshIdleLogLayout();
|
||||
|
||||
// 根据调试模式控制日志区域的显示
|
||||
if (App.isDebugging()) {
|
||||
mScrollIdleLog.setVisibility(View.VISIBLE);
|
||||
LogUtils.d(TAG, "onCreate -> 调试模式,显示日志区域");
|
||||
} else {
|
||||
mScrollIdleLog.setVisibility(View.GONE);
|
||||
LogUtils.d(TAG, "onCreate -> 非调试模式,隐藏日志区域");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
LogUtils.d(TAG, "onResume -> 页面恢复可见");
|
||||
if (App.isAppIdleRunning()) {
|
||||
IdleGpsService.getInstance().start();
|
||||
}
|
||||
if (mADsBannerView != null) {
|
||||
mADsBannerView.resumeADs(MainActivity.this);
|
||||
}
|
||||
// 重新加载菜单以根据当前调试状态刷新
|
||||
invalidateOptionsMenu();
|
||||
LogUtils.d(TAG, "onResume -> 重新加载菜单完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.i(TAG, "onDestroy -> MainActivity页面销毁释放资源");
|
||||
if (mADsBannerView != null) {
|
||||
mADsBannerView.releaseAdResources();
|
||||
}
|
||||
mIdleRunningListener = null;
|
||||
}
|
||||
|
||||
// ===================== 初始化相关方法 =====================
|
||||
private void initToolbar() {
|
||||
mToolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
setSupportActionBar(mToolbar);
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setTitle(getString(R.string.app_name));
|
||||
}
|
||||
LogUtils.d(TAG, "initToolbar -> 顶部工具栏初始化完毕");
|
||||
}
|
||||
|
||||
private void initViews() {
|
||||
mTvIdleLog = (TextView) findViewById(R.id.tv_idle_log);
|
||||
mScrollIdleLog = (ScrollView) findViewById(R.id.scroll_idle_log);
|
||||
mServiceSwitch = (Switch) findViewById(R.id.switch_service_control);
|
||||
mManagePositionsButton = (Button) findViewById(R.id.btn_manage_positions);
|
||||
|
||||
boolean serviceEnable = AppConfigsUtil.getInstance(this).isEnableMainService(true);
|
||||
mServiceSwitch.setChecked(serviceEnable);
|
||||
mManagePositionsButton.setEnabled(serviceEnable);
|
||||
|
||||
mServiceSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
//切换开关时先校验权限,无权限直接强制关闭开关
|
||||
if(isChecked && !checkLocationPermissions()){
|
||||
mServiceSwitch.setChecked(false);
|
||||
Toast.makeText(MainActivity.this,"未获取定位权限,无法开启GPS服务",Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "onCheckedChanged -> 服务开关状态变更,isChecked = " + isChecked);
|
||||
if (isChecked) {
|
||||
ServiceUtil.startAutoService(MainActivity.this);
|
||||
} else {
|
||||
ServiceUtil.stopAutoService(MainActivity.this);
|
||||
}
|
||||
mManagePositionsButton.setEnabled(isChecked);
|
||||
refreshManageButtonState();
|
||||
}
|
||||
});
|
||||
LogUtils.i(TAG, "initViews -> 全部UI控件初始化绑定完成");
|
||||
}
|
||||
|
||||
private void initAppIdleHandler() {
|
||||
AppIdleRunningModeHandler.init(MainActivity.this);
|
||||
setOnAppIdleRunningListener(new OnAppIdleRunningListener() {
|
||||
@Override
|
||||
public void onIdleStatusChange(boolean isRunning) {
|
||||
refreshIdleLogLayout();
|
||||
appendIdleLog("IdleRunning Status : " + isRunning);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIdleLogReceive(String log) {
|
||||
appendIdleLog(log);
|
||||
}
|
||||
});
|
||||
LogUtils.i(TAG, "initAppIdleHandler -> 空转处理器初始化与监听绑定完成");
|
||||
}
|
||||
|
||||
private void setLLMainBackgroundColor() {
|
||||
TypedArray typedArray = getTheme().obtainStyledAttributes(new int[]{android.R.attr.colorAccent});
|
||||
int colorAccent = typedArray.getColor(0, Color.GRAY);
|
||||
typedArray.recycle();
|
||||
LinearLayout llmain = findViewById(R.id.llmain);
|
||||
llmain.setBackgroundColor(colorAccent);
|
||||
}
|
||||
|
||||
// ===================== 空转日志输出工具 =====================
|
||||
private void appendIdleLog(final String logText) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
String allLog = mTvIdleLog.getText().toString();
|
||||
mTvIdleLog.setText(allLog + logText + "\n");
|
||||
mScrollIdleLog.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mScrollIdleLog.fullScroll(ScrollView.FOCUS_DOWN);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void refreshIdleLogLayout() {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (App.isAppIdleRunning()) {
|
||||
mScrollIdleLog.setBackgroundResource(R.drawable.shape_log_border);
|
||||
} else {
|
||||
//关闭空转时:检测权限,无权限强制关闭GPS开关
|
||||
if(!checkLocationPermissions()){
|
||||
mServiceSwitch.setChecked(false);
|
||||
ServiceUtil.stopAutoService(MainActivity.this);
|
||||
}
|
||||
mTvIdleLog.setText("");
|
||||
mScrollIdleLog.setBackgroundColor(Color.TRANSPARENT);
|
||||
LogUtils.d(TAG, "refreshIdleLogLayout -> 非空转状态:日志清空、边框已移除");
|
||||
}
|
||||
refreshToolbarSubTitle();
|
||||
refreshManageButtonState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void refreshToolbarSubTitle() {
|
||||
if(getSupportActionBar() == null){
|
||||
return;
|
||||
}
|
||||
if(App.isAppIdleRunning()){
|
||||
getSupportActionBar().setSubtitle("当前处于空转运行状态");
|
||||
}else{
|
||||
getSupportActionBar().setSubtitle("");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 空转状态专属:按钮强制可点击 + 文字追加提示
|
||||
*/
|
||||
private void refreshManageButtonState() {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (App.isAppIdleRunning()) {
|
||||
mManagePositionsButton.setText("位置与任务管理【当前空转中】");
|
||||
mManagePositionsButton.setEnabled(true);
|
||||
} else {
|
||||
mManagePositionsButton.setText("位置与任务管理");
|
||||
boolean serviceEnable = AppConfigsUtil.getInstance(MainActivity.this).isEnableMainService(true);
|
||||
mManagePositionsButton.setEnabled(serviceEnable);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ===================== 权限处理相关 =====================
|
||||
private boolean checkLocationPermissions() {
|
||||
int foregroundPerm = checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
boolean hasForegroundPerm = (foregroundPerm == PackageManager.PERMISSION_GRANTED);
|
||||
boolean hasBackgroundPerm = true;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
int backgroundPerm = checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION);
|
||||
hasBackgroundPerm = (backgroundPerm == PackageManager.PERMISSION_GRANTED);
|
||||
}
|
||||
return hasForegroundPerm && hasBackgroundPerm;
|
||||
}
|
||||
|
||||
private void requestLocationPermissions() {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
String[] foregroundPermissions = new String[]{Manifest.permission.ACCESS_FINE_LOCATION};
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
requestPermissions(foregroundPermissions, REQUEST_LOCATION_PERMISSIONS);
|
||||
}
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
requestPermissions(new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION},
|
||||
REQUEST_BACKGROUND_LOCATION_PERMISSION);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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_LOCATION_PERMISSIONS) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
requestLocationPermissions();
|
||||
} else {
|
||||
Toast.makeText(this, "需要前台定位权限才能使用该功能", Toast.LENGTH_SHORT).show();
|
||||
//权限被拒绝,强制关闭开关
|
||||
mServiceSwitch.setChecked(false);
|
||||
ServiceUtil.stopAutoService(MainActivity.this);
|
||||
mManagePositionsButton.setEnabled(false);
|
||||
}
|
||||
} else if (requestCode == REQUEST_BACKGROUND_LOCATION_PERMISSION) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
Toast.makeText(this, "已获得后台定位权限", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
Toast.makeText(this, "拒绝后台权限将无法在后台持续定位", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 菜单与页面跳转 =====================
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
AESThemeUtil.inflateMenu(this, menu);
|
||||
if (App.isDebugging()) {
|
||||
DevelopUtils.inflateMenu(this, menu);
|
||||
getMenuInflater().inflate(R.menu.toolbar_main_idle, menu);
|
||||
}
|
||||
getMenuInflater().inflate(R.menu.toolbar_main, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int menuItemId = item.getItemId();
|
||||
LogUtils.d(TAG, "onOptionsItemSelected -> 点击菜单ID = " + menuItemId);
|
||||
|
||||
if (AESThemeUtil.onAppThemeItemSelected(this, item)) {
|
||||
recreate();
|
||||
} else if (DevelopUtils.onDevelopItemSelected(this, item)) {
|
||||
LogUtils.d(TAG, "onOptionsItemSelected -> 进入开发工具菜单");
|
||||
} else if (menuItemId == R.id.item_idle_switch) {
|
||||
boolean idleNow = App.isAppIdleRunning();
|
||||
boolean idleNew = !idleNow;
|
||||
App.setAppIdleRunning(idleNew);
|
||||
AppIdleRunningModeHandler.sendIdleSwitch(idleNew);
|
||||
AppIdleRunningModeHandler.sendIdleLog("菜单手动切换空转状态:" + idleNew);
|
||||
LogUtils.d(TAG, "onOptionsItemSelected -> 空转状态已切换,当前:" + idleNew);
|
||||
refreshIdleLogLayout();
|
||||
} else if (menuItemId == R.id.item_settings) {
|
||||
Intent intent = new Intent();
|
||||
intent.setClass(this, SettingsActivity.class);
|
||||
startActivity(intent);
|
||||
} else if (menuItemId == R.id.item_about) {
|
||||
Intent intent = new Intent();
|
||||
intent.setClass(this, AboutActivity.class);
|
||||
startActivity(intent);
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void onPositions(View view) {
|
||||
LogUtils.d(TAG, "onPositions -> 跳转位置任务管理页面");
|
||||
startActivity(new Intent(MainActivity.this, LocationActivity.class));
|
||||
}
|
||||
|
||||
// ===================== 接口实现 =====================
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
@@ -63,268 +399,5 @@ public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ---------------------- 服务连接回调(仅用于获取服务状态,不依赖服务执行核心逻辑) ----------------------
|
||||
// private final ServiceConnection mServiceConn = new ServiceConnection() {
|
||||
// /**
|
||||
// * 服务绑定成功:获取服务实例,同步开关状态(以服务实际状态为准)
|
||||
// */
|
||||
// @Override
|
||||
// public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
// // Java 7 显式强转 Binder 实例(确保类型匹配,避免ClassCastException)
|
||||
// DistanceRefreshService.DistanceBinder binder = (DistanceRefreshService.DistanceBinder) service;
|
||||
// mDistanceService = binder.getService();
|
||||
// isServiceBound = true;
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 服务意外断开(如服务崩溃):重置服务实例和绑定状态
|
||||
// */
|
||||
// @Override
|
||||
// public void onServiceDisconnected(ComponentName name) {
|
||||
// mDistanceService = null;
|
||||
// isServiceBound = false;
|
||||
// }
|
||||
// };
|
||||
|
||||
// ---------------------- Activity 生命周期(核心:初始化UI、申请权限、绑定服务、释放资源) ----------------------
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main); // 关联主页面布局
|
||||
|
||||
// 1. 初始化顶部 Toolbar(保留原逻辑,设置页面标题)
|
||||
initToolbar();
|
||||
// 2. 初始化其他控件
|
||||
initViews();
|
||||
// 3. 检查并申请位置权限(含后台GPS权限,确保服务启动前权限就绪)
|
||||
if (!checkLocationPermissions()) {
|
||||
requestLocationPermissions();
|
||||
}
|
||||
// 4. 绑定服务(仅用于获取服务实时状态,不影响服务独立运行)
|
||||
//bindDistanceService();
|
||||
|
||||
mADsBannerView = findViewById(R.id.adsbanner);
|
||||
|
||||
setLLMainBackgroundColor();
|
||||
}
|
||||
|
||||
// 在 Activity 的 onCreate() 或需要获取颜色的方法中调用
|
||||
private void setLLMainBackgroundColor() {
|
||||
// 1. 定义要解析的主题属性(这里是 colorAccent)
|
||||
TypedArray a = getTheme().obtainStyledAttributes(new int[]{android.R.attr.colorAccent});
|
||||
// 2. 获取对应的颜色值(默认值可设为你需要的 fallback 颜色,如 Color.GRAY)
|
||||
int colorAccent = a.getColor(0, Color.GRAY);
|
||||
// 3. 必须回收,避免内存泄漏
|
||||
a.recycle();
|
||||
|
||||
LinearLayout llmain = findViewById(R.id.llmain);
|
||||
llmain.setBackgroundColor(colorAccent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (mADsBannerView != null) {
|
||||
mADsBannerView.releaseAdResources();
|
||||
}
|
||||
|
||||
// 页面销毁时解绑服务,避免Activity与服务相互引用导致内存泄漏
|
||||
// if (isServiceBound) {
|
||||
// unbindService(mServiceConn);
|
||||
// isServiceBound = false;
|
||||
// mDistanceService = null;
|
||||
// }
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (mADsBannerView != null) {
|
||||
mADsBannerView.resumeADs(MainActivity.this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ---------------------- 核心功能1:初始化UI组件(Toolbar + 服务开关) ----------------------
|
||||
/**
|
||||
* 初始化顶部 Toolbar,设置页面标题
|
||||
*/
|
||||
private void initToolbar() {
|
||||
mToolbar = (Toolbar) findViewById(R.id.toolbar); // Java 7 显式 findViewById + 强转
|
||||
setSupportActionBar(mToolbar);
|
||||
// 给ActionBar设置标题(先判断非空,避免空指针异常)
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setTitle(getString(R.string.app_name));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化服务控制开关:读取SP状态、绑定点击事件(含权限检查)
|
||||
*/
|
||||
private void initViews() {
|
||||
mServiceSwitch = (Switch) findViewById(R.id.switch_service_control); // 显式强转
|
||||
mServiceSwitch.setChecked(AppConfigsUtil.getInstance(this).isEnableMainService(true));
|
||||
|
||||
mManagePositionsButton = (Button) findViewById(R.id.btn_manage_positions);
|
||||
mManagePositionsButton.setEnabled(mServiceSwitch.isChecked());
|
||||
|
||||
// Java 7 用匿名内部类实现 CompoundButton.OnCheckedChangeListener
|
||||
mServiceSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
// 开关打开前先检查权限:无权限则终止操作、重置开关、引导申请
|
||||
if (isChecked && !checkLocationPermissions()) {
|
||||
requestLocationPermissions();
|
||||
return;
|
||||
}
|
||||
|
||||
// 权限就绪:执行服务启停逻辑
|
||||
if (isChecked) {
|
||||
LogUtils.d(TAG, "设置启动服务");
|
||||
ServiceUtil.startAutoService(MainActivity.this);
|
||||
} else {
|
||||
LogUtils.d(TAG, "设置关闭服务");
|
||||
|
||||
ServiceUtil.stopAutoService(MainActivity.this);
|
||||
}
|
||||
|
||||
mManagePositionsButton.setEnabled(isChecked);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
// 主题菜单
|
||||
AESThemeUtil.inflateMenu(this, menu);
|
||||
// 调试工具菜单
|
||||
if (App.isDebugging()) {
|
||||
DevelopUtils.inflateMenu(this, menu);
|
||||
}
|
||||
// 应用其他菜单
|
||||
getMenuInflater().inflate(R.menu.toolbar_main, menu);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int menuItemId = item.getItemId();
|
||||
if (AESThemeUtil.onAppThemeItemSelected(this, item)) {
|
||||
recreate();
|
||||
} if (DevelopUtils.onDevelopItemSelected(this, item)) {
|
||||
LogUtils.d(TAG, String.format("onOptionsItemSelected item.getItemId() %d ", item.getItemId()));
|
||||
} else if (menuItemId == R.id.item_settings) {
|
||||
Intent intent = new Intent();
|
||||
intent.setClass(this, SettingsActivity.class);
|
||||
startActivity(intent);
|
||||
} else if (menuItemId == R.id.item_about) {
|
||||
Intent intent = new Intent();
|
||||
intent.setClass(this, AboutActivity.class);
|
||||
startActivity(intent);
|
||||
} else {
|
||||
// 在switch语句中处理每个ID,并在处理完后返回true,未处理的情况返回false。
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 绑定服务(仅用于获取服务状态,不启动服务)
|
||||
*/
|
||||
// private void bindDistanceService() {
|
||||
// Intent serviceIntent = new Intent(this, MainService.class);
|
||||
// // BIND_AUTO_CREATE:服务未启动则创建(仅为获取状态,启停由开关控制)
|
||||
// bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
|
||||
// }
|
||||
|
||||
// ---------------------- 核心功能3:页面跳转(位置管理页+日志页) ----------------------
|
||||
/**
|
||||
* 跳转至“位置管理页(LocationActivity)”(按钮点击触发,需在布局中设置 android:onClick="onPositions")
|
||||
* 服务未启动时提示,不允许跳转(避免LocationActivity无数据)
|
||||
*/
|
||||
public void onPositions(View view) {
|
||||
//ToastUtils.show("onPositions");
|
||||
// 服务已启动:跳转到位置管理页
|
||||
startActivity(new Intent(MainActivity.this, LocationActivity.class));
|
||||
}
|
||||
|
||||
// ---------------------- 新增:位置权限处理(适配Java7 + 后台GPS权限) ----------------------
|
||||
/**
|
||||
* 检查是否拥有「前台+后台」位置权限(适配Android版本差异)
|
||||
* Java7 特性:显式类型判断、无Lambda、兼容低版本API
|
||||
*/
|
||||
private boolean checkLocationPermissions() {
|
||||
// 1. 检查前台精确定位权限(Android 6.0+ 必需,显式强转权限常量)
|
||||
int foregroundPermResult = checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
boolean hasForegroundPerm = (foregroundPermResult == PackageManager.PERMISSION_GRANTED);
|
||||
|
||||
// 2. 检查后台定位权限(仅Android 10+ 需要,Java7 显式用Build.VERSION判断版本)
|
||||
boolean hasBackgroundPerm = true;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
int backgroundPermResult = checkSelfPermission(android.Manifest.permission.ACCESS_BACKGROUND_LOCATION);
|
||||
hasBackgroundPerm = (backgroundPermResult == PackageManager.PERMISSION_GRANTED);
|
||||
}
|
||||
|
||||
// 前台+后台权限均满足,才返回true
|
||||
return hasForegroundPerm && hasBackgroundPerm;
|
||||
}
|
||||
|
||||
private void requestLocationPermissions() {
|
||||
// 1. 先判断前台定位权限(ACCESS_FINE_LOCATION)是否已授予
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
// 1.1 未授予前台权限:先申请前台权限(API 30+ 后台权限依赖前台权限)
|
||||
String[] foregroundPermissions = new String[]{Manifest.permission.ACCESS_FINE_LOCATION};
|
||||
// 对API 23+(Android 6.0)动态申请,低版本会直接授予(清单已声明前提下)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
requestPermissions(foregroundPermissions, REQUEST_LOCATION_PERMISSIONS);
|
||||
}
|
||||
} else {
|
||||
// 2. 已授予前台权限:判断是否需要申请后台权限(仅API 29+需要)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// 2.1 检查后台权限是否未授予
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
// 2.2 API 30+ 必须单独申请后台权限(不能和前台权限一起弹框)
|
||||
requestPermissions(
|
||||
new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION},
|
||||
REQUEST_BACKGROUND_LOCATION_PERMISSION
|
||||
);
|
||||
}
|
||||
}
|
||||
// 3. 前台权限已授予(+ 后台权限按需授予):此处可执行定位相关逻辑
|
||||
// doLocationRelatedLogic();
|
||||
}
|
||||
}
|
||||
|
||||
// 【必须补充】权限申请结果回调(处理用户同意/拒绝逻辑)
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
// 处理前台权限申请结果
|
||||
if (requestCode == REQUEST_LOCATION_PERMISSIONS) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
// 前台权限同意:自动尝试申请后台权限(如果是API 29+)
|
||||
requestLocationPermissions();
|
||||
} else {
|
||||
// 前台权限拒绝:提示用户(可选:引导跳转到应用设置页)
|
||||
Toast.makeText(this, "需要前台定位权限才能使用该功能", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else if (requestCode == REQUEST_BACKGROUND_LOCATION_PERMISSION) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
// 后台权限同意:可执行后台定位逻辑
|
||||
Toast.makeText(this, "已获得后台定位权限", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
// 后台权限拒绝:提示用户(可选:说明后台定位的用途,引导手动开启)
|
||||
Toast.makeText(this, "拒绝后台权限将无法在后台持续定位", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package cc.winboll.studio.positions.activities;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/09/29 18:22
|
||||
* @Describe 位置列表页面(适配MainService GPS接口+规范服务交互+完善生命周期)
|
||||
*/
|
||||
import android.app.Activity;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
@@ -16,74 +11,74 @@ import android.view.View;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import cc.winboll.studio.libaes.dialogs.YesNoAlertDialog;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.positions.App;
|
||||
import cc.winboll.studio.positions.MainActivity;
|
||||
import cc.winboll.studio.positions.R;
|
||||
import cc.winboll.studio.positions.adapters.PositionAdapter;
|
||||
import cc.winboll.studio.positions.models.PositionModel;
|
||||
import cc.winboll.studio.positions.services.IdleGpsService;
|
||||
import cc.winboll.studio.positions.services.MainService;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Java 7 语法适配:
|
||||
* 1. 服务绑定用匿名内部类实现 ServiceConnection
|
||||
* 2. Adapter 初始化传入 MainService 实例,确保数据来源唯一
|
||||
* 3. 所有位置/任务操作通过 MainService 接口执行
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @CreateTime 2025/09/29 18:22:00
|
||||
* @EditTime 2026/05/03 16:36:54
|
||||
* @Describe 位置列表页面,适配MainService GPS接口、规范服务交互、完善生命周期资源释放,新增应用空转状态副标题提示
|
||||
*/
|
||||
public class LocationActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
// 常量
|
||||
public static final String TAG = "LocationActivity";
|
||||
|
||||
private Toolbar mToolbar;
|
||||
|
||||
// 成员属性有序排版
|
||||
private Toolbar mToolbar;
|
||||
private RecyclerView mRvPosition;
|
||||
private PositionAdapter mPositionAdapter;
|
||||
|
||||
// MainService 引用+绑定状态(AtomicBoolean 确保多线程状态可见性)
|
||||
private MainService mMainService;
|
||||
private final AtomicBoolean isServiceBound = new AtomicBoolean(false);
|
||||
// 标记 Adapter 是否已初始化(避免重复初始化/销毁后初始化)
|
||||
private final AtomicBoolean isAdapterInited = new AtomicBoolean(false);
|
||||
|
||||
// ---------------------- 新增:GPS监听核心变量 ----------------------
|
||||
private MainService.GpsUpdateListener mGpsUpdateListener; // GPS监听实例
|
||||
private PositionModel mCurrentGpsPos; // 缓存当前GPS位置(供页面使用)
|
||||
// 本地位置缓存(解决服务数据未同步时Adapter空数据问题)
|
||||
private MainService.GpsUpdateListener mGpsUpdateListener;
|
||||
private PositionModel mCurrentGpsPos;
|
||||
private final ArrayList<PositionModel> mLocalPosCache = new ArrayList<PositionModel>();
|
||||
|
||||
|
||||
// 服务连接(Java 7 匿名内部类实现,强化状态同步+数据预加载)
|
||||
private ServiceConnection mServiceConnection = new ServiceConnection() {
|
||||
// 服务连接回调
|
||||
private final ServiceConnection mServiceConnection = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
// 1. 安全获取服务实例(避免强转失败+服务未就绪)
|
||||
public void onServiceConnected(final ComponentName name, final IBinder service) {
|
||||
LogUtils.d(TAG, "onServiceConnected invoke");
|
||||
if (!(service instanceof MainService.LocalBinder)) {
|
||||
LogUtils.e(TAG, "服务绑定失败:Binder类型不匹配(非MainService.LocalBinder)");
|
||||
LogUtils.e(TAG, "服务绑定失败:Binder类型不匹配");
|
||||
isServiceBound.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
MainService.LocalBinder binder = (MainService.LocalBinder) service;
|
||||
final MainService.LocalBinder binder = (MainService.LocalBinder) service;
|
||||
mMainService = binder.getService();
|
||||
// 2. 标记服务绑定成功(原子操作,确保多线程可见)
|
||||
isServiceBound.set(true);
|
||||
LogUtils.d(TAG, "MainService绑定成功,开始同步数据+初始化Adapter");
|
||||
LogUtils.d(TAG, "MainService bind success");
|
||||
|
||||
// 3. 同步服务数据到本地缓存(核心:先同步数据,再初始化Adapter)
|
||||
syncDataFromMainService();
|
||||
// 4. 注册GPS监听(确保监听在Adapter前初始化,数据不丢失)
|
||||
registerGpsListener();
|
||||
// 5. 初始化Adapter(传入本地缓存+服务实例,数据非空)
|
||||
initPositionAdapter();
|
||||
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, "服务绑定后初始化失败:" + e.getMessage());
|
||||
} catch (final Exception e) {
|
||||
LogUtils.e(TAG, "服务绑定异常:" + e.getMessage());
|
||||
isServiceBound.set(false);
|
||||
mMainService = null;
|
||||
showToast("服务初始化失败,无法加载数据");
|
||||
@@ -91,16 +86,15 @@ public class LocationActivity extends WinBoLLActivity implements IWinBoLLActivit
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
LogUtils.w(TAG, "MainService断开连接,清空引用+标记状态");
|
||||
// 1. 清空服务引用+标记绑定状态
|
||||
public void onServiceDisconnected(final ComponentName name) {
|
||||
LogUtils.w(TAG, "MainService 服务断开");
|
||||
mMainService = null;
|
||||
isServiceBound.set(false);
|
||||
// 2. 标记Adapter未初始化(下次绑定需重新初始化)
|
||||
isAdapterInited.set(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 接口方法实现
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
@@ -111,176 +105,143 @@ public class LocationActivity extends WinBoLLActivity implements IWinBoLLActivit
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_location);
|
||||
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(mToolbar);
|
||||
mToolbar.setSubtitle(getTag());
|
||||
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【导航栏】点击返回");
|
||||
startActivity(new Intent(LocationActivity.this, MainActivity.class));
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
// 1. 初始化视图(优先执行,避免Adapter初始化时视图为空)
|
||||
initView();
|
||||
// 2. 初始化GPS监听(提前创建,避免绑定服务后空指针)
|
||||
initGpsUpdateListener();
|
||||
// 3. 绑定MainService(最后执行,确保视图/监听已就绪)
|
||||
bindMainService();
|
||||
/**
|
||||
* 刷新空转状态副标题
|
||||
*/
|
||||
private void refreshIdleStatusTitle() {
|
||||
LogUtils.d(TAG, "refreshIdleStatusTitle invoke");
|
||||
if (mToolbar == null) {
|
||||
LogUtils.w(TAG, "Toolbar为空,跳过刷新");
|
||||
return;
|
||||
}
|
||||
final boolean idleStatus = App.isAppIdleRunning();
|
||||
if (idleStatus) {
|
||||
mToolbar.setSubtitle("当前状态:应用正在空转运行");
|
||||
LogUtils.d(TAG, "检测应用空转状态为 : " + idleStatus);
|
||||
} else {
|
||||
mToolbar.setSubtitle(getTag());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化视图(RecyclerView)- 确保视图先于Adapter初始化
|
||||
* 初始化页面控件
|
||||
*/
|
||||
private void initView() {
|
||||
mRvPosition = (RecyclerView) findViewById(R.id.rv_position_list);
|
||||
// 1. 显式设置布局管理器(避免Adapter设置时无布局管理器崩溃)
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||
LogUtils.d(TAG, "initView invoke");
|
||||
mRvPosition = findViewById(R.id.rv_position_list);
|
||||
final LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
|
||||
mRvPosition.setLayoutManager(layoutManager);
|
||||
// 2. 初始化本地缓存(避免首次加载时缓存为空)
|
||||
mLocalPosCache.clear();
|
||||
LogUtils.d(TAG, "视图初始化完成(布局管理器+本地缓存已就绪)");
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定MainService(Java 7 显式Intent,强化绑定安全性)
|
||||
* 绑定后台服务
|
||||
*/
|
||||
private void bindMainService() {
|
||||
// 1. 避免重复绑定(快速重建Activity时防止多绑定)
|
||||
LogUtils.d(TAG, "bindMainService invoke");
|
||||
if (isServiceBound.get()) {
|
||||
LogUtils.w(TAG, "无需重复绑定:MainService已绑定");
|
||||
LogUtils.w(TAG, "服务已绑定,无需重复执行");
|
||||
return;
|
||||
}
|
||||
|
||||
Intent serviceIntent = new Intent(this, MainService.class);
|
||||
// 2. 绑定服务(BIND_AUTO_CREATE:服务不存在时自动创建,增加绑定成功率)
|
||||
boolean bindSuccess = bindService(serviceIntent, mServiceConnection, BIND_AUTO_CREATE);
|
||||
final Intent serviceIntent = new Intent(this, MainService.class);
|
||||
final boolean bindSuccess = bindService(serviceIntent, mServiceConnection, BIND_AUTO_CREATE);
|
||||
if (!bindSuccess) {
|
||||
LogUtils.e(TAG, "发起MainService绑定请求失败(服务未找到/系统限制)");
|
||||
LogUtils.e(TAG, "发起服务绑定请求失败");
|
||||
showToast("服务绑定失败,无法加载位置数据");
|
||||
} else {
|
||||
LogUtils.d(TAG, "MainService绑定请求已发起");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从MainService同步数据到本地缓存(核心解决Adapter空数据问题)
|
||||
* 作用:1. 服务数据优先同步到本地,Adapter基于本地缓存初始化
|
||||
* 2. 避免服务数据更新时直接操作Adapter,通过缓存中转
|
||||
* 同步服务数据至本地缓存
|
||||
*/
|
||||
private void syncDataFromMainService() {
|
||||
// 1. 安全校验(服务未绑定/服务空,用本地缓存兜底)
|
||||
LogUtils.d(TAG, "syncDataFromMainService invoke");
|
||||
if (!isServiceBound.get() || mMainService == null) {
|
||||
LogUtils.w(TAG, "同步数据:服务未就绪,使用本地缓存(当前缓存量=" + mLocalPosCache.size() + ")");
|
||||
LogUtils.w(TAG, "服务未就绪,使用本地缓存");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. 从服务获取最新位置数据(同步操作,确保数据拿到后再返回)
|
||||
ArrayList<PositionModel> servicePosList = mMainService.getPositionList();
|
||||
// 3. 同步到本地缓存(清空旧数据+添加新数据,避免重复)
|
||||
synchronized (mLocalPosCache) { // 加锁避免多线程操作缓存冲突
|
||||
final ArrayList<PositionModel> servicePosList = mMainService.getPositionList();
|
||||
synchronized (mLocalPosCache) {
|
||||
mLocalPosCache.clear();
|
||||
if (servicePosList != null && !servicePosList.isEmpty()) {
|
||||
mLocalPosCache.addAll(servicePosList);
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "数据同步完成:服务位置数=" + (servicePosList == null ? 0 : servicePosList.size())
|
||||
+ ",本地缓存数=" + mLocalPosCache.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, "同步服务数据失败:" + e.getMessage());
|
||||
// 异常时保留本地缓存,避免Adapter无数据
|
||||
LogUtils.w(TAG, "同步失败,使用本地缓存兜底(缓存量=" + mLocalPosCache.size() + ")");
|
||||
LogUtils.d(TAG, "同步完成,缓存数量 : " + mLocalPosCache.size());
|
||||
} catch (final Exception e) {
|
||||
LogUtils.e(TAG, "数据同步异常 : " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化PositionAdapter(核心优化:基于本地缓存初始化,避免空数据)
|
||||
* 初始化列表适配器
|
||||
*/
|
||||
private void initPositionAdapter() {
|
||||
// 1. 多重安全校验(避免销毁后初始化/重复初始化/依赖未就绪)
|
||||
if (isAdapterInited.get() || !isServiceBound.get() || mMainService == null || mRvPosition == null) {
|
||||
LogUtils.w(TAG, "Adapter初始化跳过:"
|
||||
+ "已初始化=" + isAdapterInited.get()
|
||||
+ ",服务绑定=" + isServiceBound.get()
|
||||
+ ",视图就绪=" + (mRvPosition != null));
|
||||
LogUtils.d(TAG, "initPositionAdapter invoke");
|
||||
if (isAdapterInited.get() || !isServiceBound.get()
|
||||
|| mMainService == null || mRvPosition == null) {
|
||||
LogUtils.w(TAG, "适配器初始化条件不满足,跳过");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. 基于本地缓存初始化Adapter(缓存已同步服务数据,非空)
|
||||
mPositionAdapter = new PositionAdapter(this, mLocalPosCache, mMainService);
|
||||
|
||||
// 3. 设置删除回调(删除时同步服务+本地缓存+Adapter)
|
||||
mPositionAdapter.setOnDeleteClickListener(new PositionAdapter.OnDeleteClickListener() {
|
||||
@Override
|
||||
public void onDeleteClick(int position) {
|
||||
// 安全校验(索引有效+服务绑定+缓存非空)
|
||||
if (position < 0 || position >= mLocalPosCache.size() || !isServiceBound.get() || mMainService == null) {
|
||||
LogUtils.w(TAG, "删除位置失败:索引无效/服务未就绪(索引=" + position + ",缓存量=" + mLocalPosCache.size() + ")");
|
||||
return;
|
||||
}
|
||||
public void onDeleteClick(final int position) {
|
||||
LogUtils.d(TAG, "onDeleteClick position = " + position);
|
||||
YesNoAlertDialog.show(LocationActivity.this, "删除位置提示",
|
||||
"是否删除此项锚点位置?", new YesNoAlertDialog.OnDialogResultListener() {
|
||||
@Override
|
||||
public void onNo() {
|
||||
|
||||
PositionModel deletePos = mLocalPosCache.get(position);
|
||||
if (deletePos != null && !deletePos.getPositionId().isEmpty()) {
|
||||
// 步骤1:调用服务删除(确保服务数据一致性)
|
||||
mMainService.removePosition(deletePos.getPositionId());
|
||||
// 步骤2:删除本地缓存(确保缓存与服务同步)
|
||||
synchronized (mLocalPosCache) {
|
||||
mLocalPosCache.remove(position);
|
||||
}
|
||||
// 步骤3:通知Adapter刷新(基于缓存操作,避免空数据)
|
||||
mPositionAdapter.notifyItemRemoved(position);
|
||||
showToast("删除位置成功:" + deletePos.getMemo());
|
||||
LogUtils.d(TAG, "删除位置完成:ID=" + deletePos.getPositionId() + "(服务+缓存已同步)");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onYes() {
|
||||
if (position < 0 || position >= mLocalPosCache.size()) {
|
||||
LogUtils.w(TAG, "删除索引参数非法");
|
||||
return;
|
||||
}
|
||||
final PositionModel deletePos = mLocalPosCache.get(position);
|
||||
if (deletePos != null && !deletePos.getPositionId().isEmpty()) {
|
||||
mMainService.removePosition(deletePos.getPositionId());
|
||||
synchronized (mLocalPosCache) {
|
||||
mLocalPosCache.remove(position);
|
||||
}
|
||||
mPositionAdapter.notifyItemRemoved(position);
|
||||
showToast("删除位置成功:" + deletePos.getMemo());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 4. 设置保存回调(保存时同步服务+本地缓存+Adapter)
|
||||
mPositionAdapter.setOnSavePositionClickListener(new PositionAdapter.OnSavePositionClickListener() {
|
||||
@Override
|
||||
public void onSavePositionClick(int position, PositionModel updatedPos) {
|
||||
// 安全校验(索引有效+服务绑定+数据非空)
|
||||
if (!isServiceBound.get() || mMainService == null
|
||||
|| position < 0 || position >= mLocalPosCache.size() || updatedPos == null) {
|
||||
LogUtils.w(TAG, "保存位置失败:服务未就绪/索引无效/数据空");
|
||||
public void onSavePositionClick(final int position, final PositionModel updatedPos) {
|
||||
LogUtils.d(TAG, "onSavePositionClick position = " + position);
|
||||
if (position < 0 || position >= mLocalPosCache.size() || updatedPos == null) {
|
||||
LogUtils.w(TAG, "保存参数非法");
|
||||
showToast("服务未就绪,保存失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 步骤1:调用服务更新(确保服务数据一致性)
|
||||
mMainService.updatePosition(updatedPos);
|
||||
// 步骤2:更新本地缓存(确保缓存与服务同步)
|
||||
synchronized (mLocalPosCache) {
|
||||
mLocalPosCache.set(position, updatedPos);
|
||||
}
|
||||
// 步骤3:通知Adapter刷新(基于缓存操作,避免空数据)
|
||||
mPositionAdapter.notifyItemChanged(position);
|
||||
showToast("保存位置成功:" + updatedPos.getMemo());
|
||||
LogUtils.d(TAG, "保存位置完成:ID=" + updatedPos.getPositionId() + "(服务+缓存已同步)");
|
||||
}
|
||||
});
|
||||
|
||||
// 5. 设置Adapter到RecyclerView(最后一步,确保Adapter已配置完成)
|
||||
mRvPosition.setAdapter(mPositionAdapter);
|
||||
// 6. 标记Adapter已初始化(避免重复初始化)
|
||||
isAdapterInited.set(true);
|
||||
LogUtils.d(TAG, "PositionAdapter初始化完成(基于本地缓存,数据量=" + mLocalPosCache.size() + ")");
|
||||
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, "Adapter初始化失败:" + e.getMessage());
|
||||
LogUtils.d(TAG, "适配器初始化完成");
|
||||
} catch (final Exception e) {
|
||||
LogUtils.e(TAG, "适配器初始化异常 : " + e.getMessage());
|
||||
isAdapterInited.set(false);
|
||||
mPositionAdapter = null;
|
||||
showToast("位置列表初始化失败,请重试");
|
||||
@@ -288,89 +249,91 @@ public class LocationActivity extends WinBoLLActivity implements IWinBoLLActivit
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示Toast(Java 7 显式Toast.makeText,避免空Context)
|
||||
* 通用Toast弹窗
|
||||
*/
|
||||
private void showToast(String content) {
|
||||
if (isFinishing() || isDestroyed()) { // 避免Activity销毁后弹Toast崩溃
|
||||
LogUtils.w(TAG, "Activity已销毁,跳过Toast:" + content);
|
||||
private void showToast(final String content) {
|
||||
if (isFinishing() || isDestroyed()) {
|
||||
LogUtils.w(TAG, "页面已销毁,取消Toast");
|
||||
return;
|
||||
}
|
||||
Toast.makeText(this, content, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
// ---------------------- 页面交互(新增位置逻辑保留,适配GPS数据) ----------------------
|
||||
/**
|
||||
* 新增位置(调用服务addPosition(),可选:用当前GPS位置初始化新位置)
|
||||
* 新增位置按钮事件
|
||||
*/
|
||||
public void addNewPosition(View view) {
|
||||
// 1. 隐藏软键盘(避免软键盘遮挡操作)
|
||||
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
public void addNewPosition(final View view) {
|
||||
LogUtils.d(TAG, "addNewPosition invoke");
|
||||
final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
if (imm != null && getCurrentFocus() != null) {
|
||||
imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
|
||||
}
|
||||
|
||||
// 2. 安全校验(服务未绑定,不允许新增)
|
||||
if (!isServiceBound.get() || mMainService == null) {
|
||||
LogUtils.w(TAG, "新增位置失败:MainService未绑定");
|
||||
LogUtils.w(TAG, "服务未绑定,无法新增");
|
||||
showToast("服务未就绪,无法新增位置");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 创建新位置模型(优化:优先用当前GPS位置初始化,无则用默认值)
|
||||
PositionModel newPos = new PositionModel();
|
||||
newPos.setPositionId(PositionModel.genPositionId()); // 生成唯一ID(需PositionModel实现)
|
||||
final PositionModel newPos = new PositionModel();
|
||||
newPos.setPositionId(PositionModel.genPositionId());
|
||||
if (mCurrentGpsPos != null) {
|
||||
newPos.setLongitude(mCurrentGpsPos.getLongitude());
|
||||
newPos.setLatitude(mCurrentGpsPos.getLatitude());
|
||||
newPos.setMemo("当前GPS位置(可编辑)");
|
||||
} else {
|
||||
newPos.setLongitude(116.404267); // 北京经度(默认值)
|
||||
newPos.setLatitude(39.915119); // 北京纬度(默认值)
|
||||
newPos.setLongitude(116.404267);
|
||||
newPos.setLatitude(39.915119);
|
||||
newPos.setMemo("默认位置(可编辑备注)");
|
||||
}
|
||||
newPos.setIsSimpleView(true); // 默认简单视图
|
||||
newPos.setIsEnableRealPositionDistance(true); // 启用距离计算(依赖GPS)
|
||||
newPos.setIsSimpleView(true);
|
||||
newPos.setIsEnableRealPositionDistance(true);
|
||||
|
||||
// 4. 调用服务新增+同步本地缓存(确保缓存与服务一致)
|
||||
mMainService.addPosition(newPos);
|
||||
synchronized (mLocalPosCache) {
|
||||
mLocalPosCache.add(newPos);
|
||||
}
|
||||
LogUtils.d(TAG, "通过服务新增位置:ID=" + newPos.getPositionId() + ",纬度=" + newPos.getLatitude() + "(缓存已同步)");
|
||||
LogUtils.d(TAG, "新增位置成功,ID : " + newPos.getPositionId());
|
||||
|
||||
// 5. 刷新Adapter(基于缓存操作,确保数据立即显示)
|
||||
if (isAdapterInited.get() && mPositionAdapter != null) {
|
||||
mPositionAdapter.notifyItemInserted(mLocalPosCache.size() - 1);
|
||||
}
|
||||
showToast("新增位置成功(已启用GPS距离计算)");
|
||||
}
|
||||
|
||||
// ---------------------- 新增:GPS监听初始化+注册/反注册(核心适配逻辑) ----------------------
|
||||
/**
|
||||
* 初始化GPS监听:实现MainService.GpsUpdateListener,接收实时GPS数据
|
||||
* 初始化GPS监听实例
|
||||
*/
|
||||
private void initGpsUpdateListener() {
|
||||
LogUtils.d(TAG, "initGpsUpdateListener()");
|
||||
LogUtils.d(TAG, "initGpsUpdateListener invoke");
|
||||
mGpsUpdateListener = new MainService.GpsUpdateListener() {
|
||||
@Override
|
||||
public void onGpsPositionUpdated(PositionModel currentGpsPos) {
|
||||
public void onGpsPositionUpdated(final PositionModel currentGpsPos) {
|
||||
if (currentGpsPos == null || isFinishing() || isDestroyed()) {
|
||||
LogUtils.w(TAG, "GPS位置更新:数据为空或Activity已销毁");
|
||||
return;
|
||||
}
|
||||
// 缓存当前GPS位置(供页面其他逻辑使用)
|
||||
mCurrentGpsPos = currentGpsPos;
|
||||
LogUtils.d(TAG, String.format("收到GPS更新:纬度=%.4f,经度=%.4f"
|
||||
, currentGpsPos.getLatitude(), currentGpsPos.getLongitude()));
|
||||
// 安全更新UI(避免Activity销毁后操作视图崩溃)
|
||||
((TextView)findViewById(R.id.tv_latitude)).setText(String.format("当前纬度:%f", currentGpsPos.getLatitude()));
|
||||
((TextView)findViewById(R.id.tv_longitude)).setText(String.format("当前经度:%f", currentGpsPos.getLongitude()));
|
||||
LogUtils.d(TAG, "GPS更新 -> 纬度:" + currentGpsPos.getLatitude() + " 经度:" + currentGpsPos.getLongitude());
|
||||
|
||||
final TextView tvLat = (TextView) findViewById(R.id.tv_latitude);
|
||||
final TextView tvLon = (TextView) findViewById(R.id.tv_longitude);
|
||||
final TextView tvTime = (TextView) findViewById(R.id.tv_timenow);
|
||||
|
||||
tvLat.setText(String.format("当前纬度:%f", currentGpsPos.getLatitude()));
|
||||
tvLon.setText(String.format("当前经度:%f", currentGpsPos.getLongitude()));
|
||||
|
||||
final long currentTime = System.currentTimeMillis();
|
||||
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault());
|
||||
final String timeStr = sdf.format(new Date(currentTime));
|
||||
tvTime.setText("现在时间:" + timeStr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGpsStatusChanged(String status) {
|
||||
if (status == null || isFinishing() || isDestroyed()) return;
|
||||
LogUtils.d(TAG, "GPS状态变化:" + status);
|
||||
public void onGpsStatusChanged(final String status) {
|
||||
if (status == null || isFinishing() || isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "GPS状态变更 : " + status);
|
||||
if (status.contains("未开启") || status.contains("权限") || status.contains("失败")) {
|
||||
ToastUtils.show("GPS提示:" + status);
|
||||
}
|
||||
@@ -378,111 +341,147 @@ public class LocationActivity extends WinBoLLActivity implements IWinBoLLActivit
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册GPS监听:调用MainService的PUBLIC方法,绑定监听
|
||||
/**
|
||||
* 刷新GPS监听源(根据空转状态自动切换)
|
||||
*/
|
||||
private void refreshGpsListener() {
|
||||
LogUtils.d(TAG, "refreshGpsListener invoke");
|
||||
// 统一注销旧监听(防止重复注册)
|
||||
IdleGpsService.getInstance().unregisterGpsUpdateListener(mGpsUpdateListener);
|
||||
if (mMainService != null) {
|
||||
try { mMainService.unregisterGpsUpdateListener(mGpsUpdateListener); } catch (Exception e) {}
|
||||
}
|
||||
// 重新注册正确的监听
|
||||
registerGpsListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册GPS监听
|
||||
*/
|
||||
private void registerGpsListener() {
|
||||
// 安全校验(避免Activity销毁/服务未绑定/监听为空时注册)
|
||||
if (isFinishing() || isDestroyed() || !isServiceBound.get() || mMainService == null || mGpsUpdateListener == null) {
|
||||
LogUtils.w(TAG, "GPS监听注册跳过:Activity状态异常/依赖未就绪");
|
||||
LogUtils.d(TAG, "registerGpsListener invoke");
|
||||
if (isFinishing() || isDestroyed() || mGpsUpdateListener == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
mMainService.registerGpsUpdateListener(mGpsUpdateListener);
|
||||
LogUtils.d(TAG, "GPS监听已注册");
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, "GPS监听注册失败:" + e.getMessage());
|
||||
if (App.isAppIdleRunning()) {
|
||||
IdleGpsService.getInstance().registerGpsUpdateListener(mGpsUpdateListener);
|
||||
LogUtils.d(TAG, "空转GPS监听注册成功");
|
||||
} else {
|
||||
if (mMainService != null) {
|
||||
mMainService.registerGpsUpdateListener(mGpsUpdateListener);
|
||||
LogUtils.d(TAG, "系统GPS监听注册成功");
|
||||
}
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
LogUtils.e(TAG, "GPS注册异常 : " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 反注册GPS监听:调用MainService的PUBLIC方法,解绑监听(核心防内存泄漏+数据异常)
|
||||
* 反注册GPS监听
|
||||
*/
|
||||
private void unregisterGpsListener() {
|
||||
// 避免Activity销毁后调用服务方法(防止空指针/服务已解绑)
|
||||
if (mMainService == null || mGpsUpdateListener == null) {
|
||||
LogUtils.w(TAG, "GPS监听反注册跳过:服务/监听未初始化");
|
||||
LogUtils.d(TAG, "unregisterGpsListener invoke");
|
||||
if (mGpsUpdateListener == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
mMainService.unregisterGpsUpdateListener(mGpsUpdateListener);
|
||||
LogUtils.d(TAG, "GPS监听已反注册");
|
||||
// 尝试从空转服务注销
|
||||
IdleGpsService.getInstance().unregisterGpsUpdateListener(mGpsUpdateListener);
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, "GPS监听反注册失败:" + e.getMessage());
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
// 尝试从主服务注销
|
||||
if (mMainService != null) {
|
||||
mMainService.unregisterGpsUpdateListener(mGpsUpdateListener);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
LogUtils.e(TAG, "GPS反注册异常 : " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面可见时同步数据(解决快速切回时数据未更新问题)
|
||||
* 场景:快速关闭再打开Activity,服务已绑定但数据未重新同步
|
||||
*/
|
||||
// 生命周期方法
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onCreate invoke");
|
||||
setContentView(R.layout.activity_location);
|
||||
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(mToolbar);
|
||||
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final View v) {
|
||||
LogUtils.d(TAG, "点击返回导航按钮");
|
||||
final Intent intent = new Intent(LocationActivity.this, MainActivity.class);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
refreshIdleStatusTitle();
|
||||
initView();
|
||||
initGpsUpdateListener();
|
||||
bindMainService();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
// 1. 服务已绑定但Adapter未初始化:重新同步数据+初始化Adapter
|
||||
LogUtils.d(TAG, "onResume invoke");
|
||||
refreshIdleStatusTitle();
|
||||
refreshGpsListener(); // 根据当前空转状态刷新GPS源
|
||||
|
||||
if (isServiceBound.get() && mMainService != null && !isAdapterInited.get()) {
|
||||
LogUtils.d(TAG, "onResume:服务已绑定但Adapter未初始化,重新同步数据");
|
||||
syncDataFromMainService();
|
||||
initPositionAdapter();
|
||||
} else if (isServiceBound.get() && mMainService != null && isAdapterInited.get() && mPositionAdapter != null) {
|
||||
} else if (isServiceBound.get() && mMainService != null && mPositionAdapter != null) {
|
||||
syncDataFromMainService();
|
||||
mPositionAdapter.notifyDataSetChanged();
|
||||
LogUtils.d(TAG, "onResume:刷新位置数据(与服务同步)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面不可见时暂停操作(避免后台操作导致数据异常)
|
||||
*/
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
// 避免后台时仍执行UI刷新(如GPS更新触发的视图操作)
|
||||
LogUtils.d(TAG, "onPause:页面不可见,暂停UI相关操作");
|
||||
LogUtils.d(TAG, "onPause invoke");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "onDestroy:开始释放资源");
|
||||
LogUtils.d(TAG, "onDestroy invoke,开始释放资源");
|
||||
|
||||
// 1. 反注册GPS监听(优先执行,避免服务持有Activity引用导致内存泄漏)
|
||||
unregisterGpsListener();
|
||||
|
||||
// 2. 释放Adapter资源(反注册可能的监听,避免内存泄漏)
|
||||
if (mPositionAdapter != null) {
|
||||
mPositionAdapter.release();
|
||||
mPositionAdapter = null; // 清空引用,帮助GC回收
|
||||
LogUtils.d(TAG, "Adapter资源已释放");
|
||||
mPositionAdapter = null;
|
||||
LogUtils.d(TAG, "Adapter资源释放完成");
|
||||
}
|
||||
|
||||
// 3. 解绑MainService(最后执行,确保其他资源已释放)
|
||||
if (isServiceBound.get()) {
|
||||
try {
|
||||
unbindService(mServiceConnection);
|
||||
LogUtils.d(TAG, "MainService解绑完成");
|
||||
} catch (IllegalArgumentException e) {
|
||||
// 捕获“服务未绑定”异常(快速开关时可能出现,避免崩溃)
|
||||
LogUtils.d(TAG, "解绑MainService失败:服务未绑定(可能已提前解绑)");
|
||||
LogUtils.d(TAG, "服务解绑完成");
|
||||
} catch (final IllegalArgumentException e) {
|
||||
LogUtils.e(TAG, "解绑异常:服务已提前解绑");
|
||||
}
|
||||
// 重置绑定状态+服务引用
|
||||
isServiceBound.set(false);
|
||||
mMainService = null;
|
||||
}
|
||||
|
||||
// 4. 清空本地缓存+GPS引用(帮助GC回收)
|
||||
synchronized (mLocalPosCache) {
|
||||
mLocalPosCache.clear();
|
||||
}
|
||||
mCurrentGpsPos = null;
|
||||
mGpsUpdateListener = null;
|
||||
isAdapterInited.set(false);
|
||||
LogUtils.d(TAG, "所有资源释放完成(onDestroy执行结束)");
|
||||
LogUtils.d(TAG, "全部资源释放完毕");
|
||||
}
|
||||
|
||||
// ---------------------- 移除重复定义:LocalBinder 统一在 MainService 中定义 ----------------------
|
||||
// 说明:原LocationActivity中的LocalBinder是重复定义(MainService已实现),会导致类型强转失败
|
||||
// 此处删除该类,确保Activity绑定服务时强转的是MainService中的LocalBinder
|
||||
}
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,143 @@
|
||||
package cc.winboll.studio.positions.handlers;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.positions.App;
|
||||
import cc.winboll.studio.positions.MainActivity;
|
||||
|
||||
/**
|
||||
* 应用空转事务处理器
|
||||
* 作用:接收空转开关消息、空转日志消息,回调MainActivity内部接口实现UI联动
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @CreateTime 2026/05/03 12:23:00
|
||||
* @EditTime 2026/05/03 14:42:18
|
||||
*/
|
||||
public class AppIdleRunningModeHandler extends Handler {
|
||||
|
||||
//===================== 常量标识 =====================
|
||||
public static final String TAG = "AppIdleRunningModeHandler";
|
||||
public static final int MSG_IDLE_MODE_SWITCH = 1001;
|
||||
public static final int MSG_IDLE_LOG_PRINT = 1002;
|
||||
|
||||
//===================== 成员变量 =====================
|
||||
private static AppIdleRunningModeHandler mHandler;
|
||||
private MainActivity mMainActivity;
|
||||
|
||||
//===================== 静态初始化 =====================
|
||||
static {
|
||||
mHandler = new AppIdleRunningModeHandler();
|
||||
LogUtils.d(TAG, "静态代码块:默认主线程Looper完成初始化");
|
||||
}
|
||||
|
||||
//===================== 构造方法 =====================
|
||||
/**
|
||||
* 私有无参构造,禁止外部直接实例化
|
||||
*/
|
||||
private AppIdleRunningModeHandler() {
|
||||
super(Looper.getMainLooper());
|
||||
}
|
||||
|
||||
/**
|
||||
* 带MainActivity绑定构造
|
||||
* @param activity 主页面实例
|
||||
*/
|
||||
public AppIdleRunningModeHandler(MainActivity activity) {
|
||||
super(Looper.getMainLooper());
|
||||
this.mMainActivity = activity;
|
||||
LogUtils.d(TAG, "构造方法:完成MainActivity绑定初始化");
|
||||
}
|
||||
|
||||
//===================== 对外初始化与获取 =====================
|
||||
/**
|
||||
* 全局静态初始化、重新绑定MainActivity
|
||||
* @param activity 主页面实例
|
||||
*/
|
||||
public static void init(MainActivity activity) {
|
||||
if (mHandler == null) {
|
||||
mHandler = new AppIdleRunningModeHandler(activity);
|
||||
} else {
|
||||
mHandler.mMainActivity = activity;
|
||||
}
|
||||
LogUtils.i(TAG, "init -> AppIdleRunningModeHandler初始化绑定成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前绑定的MainActivity实例
|
||||
* @return 已绑定的Activity
|
||||
*/
|
||||
public MainActivity getBindMainActivity() {
|
||||
return mMainActivity;
|
||||
}
|
||||
|
||||
//===================== 对外静态发送方法 =====================
|
||||
/**
|
||||
* 发送空转开关控制消息
|
||||
* @param isOpen 是否开启空转状态
|
||||
*/
|
||||
public static void sendIdleSwitch(boolean isOpen) {
|
||||
if (!App.isAppIdleRunning()) {
|
||||
LogUtils.d(TAG, "sendIdleSwitch -> 当前非空转状态,函数执行无效");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "sendIdleSwitch -> 发送空转开关信号,参数isOpen = " + isOpen);
|
||||
|
||||
Message message = Message.obtain();
|
||||
message.what = MSG_IDLE_MODE_SWITCH;
|
||||
message.obj = isOpen;
|
||||
mHandler.sendMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送空转日志打印消息
|
||||
* @param logText 待输出的日志内容
|
||||
*/
|
||||
public static void sendIdleLog(String logText) {
|
||||
if (!App.isAppIdleRunning()) {
|
||||
LogUtils.d(TAG, "sendIdleLog -> 当前非空转状态,函数执行无效");
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "sendIdleLog -> 发送空转日志消息");
|
||||
|
||||
Message message = Message.obtain();
|
||||
message.what = MSG_IDLE_LOG_PRINT;
|
||||
message.obj = logText;
|
||||
mHandler.sendMessage(message);
|
||||
}
|
||||
|
||||
//===================== 消息接收处理 =====================
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
super.handleMessage(msg);
|
||||
|
||||
// 全局状态校验
|
||||
if (!App.isAppIdleRunning()) {
|
||||
return;
|
||||
}
|
||||
// 空指针安全防护
|
||||
if (mMainActivity == null || mMainActivity.getOnAppIdleRunningListener() == null) {
|
||||
LogUtils.d(TAG, "handleMessage -> Activity或监听接口为空,终止回调");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (msg.what) {
|
||||
case MSG_IDLE_MODE_SWITCH:
|
||||
boolean idleState = (boolean) msg.obj;
|
||||
App.setAppIdleRunning(idleState);
|
||||
LogUtils.i(TAG, "handleMessage -> 空转状态已变更:" + idleState);
|
||||
// 回调主页面接口
|
||||
mMainActivity.getOnAppIdleRunningListener().onIdleStatusChange(idleState);
|
||||
break;
|
||||
|
||||
case MSG_IDLE_LOG_PRINT:
|
||||
String logContent = (String) msg.obj;
|
||||
LogUtils.i(TAG, logContent);
|
||||
// 回调主页面日志接收接口
|
||||
mMainActivity.getOnAppIdleRunningListener().onIdleLogReceive(logContent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
package cc.winboll.studio.positions.services;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.positions.handlers.AppIdleRunningModeHandler;
|
||||
import cc.winboll.studio.positions.models.PositionModel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 空转GPS模拟服务
|
||||
* 在应用空转模式下,模拟系统GPS服务向客户端发送坐标数据
|
||||
*/
|
||||
public class IdleGpsService {
|
||||
|
||||
private static final long MOCK_INTERVAL_MS = 5000; // 模拟坐标更新间隔(5秒)
|
||||
private static final long BEARING_INTERVAL_MS = 1000; // 角动量递增间隔(1秒)
|
||||
private static final double EARTH_RADIUS_M = 6371000; // 地球平均半径(米)
|
||||
private static final double ANCHOR_LAT = 39.9042; // 固定锚点纬度(北京)
|
||||
private static final double ANCHOR_LON = 116.4074; // 固定锚点经度(北京)
|
||||
private static final double CIRCLE_RADIUS_M = 50; // 移动轨迹半径(米)
|
||||
|
||||
private static IdleGpsService instance;
|
||||
private final List<MainService.GpsUpdateListener> listeners = new ArrayList<>();
|
||||
private final Handler handler;
|
||||
private final Runnable updateRunnable;
|
||||
private final Runnable bearingRunnable;
|
||||
private final PositionModel mockPosition;
|
||||
private boolean isRunning;
|
||||
private int currentBearing = 1; // 当前角动量度数(1-360)
|
||||
|
||||
private IdleGpsService() {
|
||||
handler = new Handler(Looper.getMainLooper());
|
||||
mockPosition = new PositionModel();
|
||||
mockPosition.setPositionId("mock_idle_pos");
|
||||
mockPosition.setMemo("空转模拟坐标");
|
||||
|
||||
updateRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isRunning) {
|
||||
calculatePosition();
|
||||
notifyListeners(mockPosition);
|
||||
AppIdleRunningModeHandler.sendIdleLog("模拟GPS数据更新 -> 纬度:" + mockPosition.getLatitude() + ", 经度:" + mockPosition.getLongitude() + ", 角度:" + currentBearing + "°");
|
||||
handler.postDelayed(this, MOCK_INTERVAL_MS);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
bearingRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isRunning) {
|
||||
currentBearing++;
|
||||
if (currentBearing > 360) {
|
||||
currentBearing = 1;
|
||||
}
|
||||
handler.postDelayed(this, BEARING_INTERVAL_MS);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void calculatePosition() {
|
||||
double bearingRad = Math.toRadians(currentBearing);
|
||||
double angularDistance = CIRCLE_RADIUS_M / EARTH_RADIUS_M;
|
||||
|
||||
double anchorLatRad = Math.toRadians(ANCHOR_LAT);
|
||||
double anchorLonRad = Math.toRadians(ANCHOR_LON);
|
||||
|
||||
double newLatRad = Math.asin(
|
||||
Math.sin(anchorLatRad) * Math.cos(angularDistance) +
|
||||
Math.cos(anchorLatRad) * Math.sin(angularDistance) * Math.cos(bearingRad)
|
||||
);
|
||||
|
||||
double newLonRad = anchorLonRad + Math.atan2(
|
||||
Math.sin(bearingRad) * Math.sin(angularDistance) * Math.cos(anchorLatRad),
|
||||
Math.cos(angularDistance) - Math.sin(anchorLatRad) * Math.sin(newLatRad)
|
||||
);
|
||||
|
||||
mockPosition.setLatitude(Math.toDegrees(newLatRad));
|
||||
mockPosition.setLongitude(Math.toDegrees(newLonRad));
|
||||
}
|
||||
|
||||
public static IdleGpsService getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new IdleGpsService();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动空转模拟服务
|
||||
*/
|
||||
public void start() {
|
||||
if (isRunning) return;
|
||||
isRunning = true;
|
||||
currentBearing = 1;
|
||||
AppIdleRunningModeHandler.sendIdleLog("空转GPS服务已启动");
|
||||
notifyStatusChange("空转GPS服务已启动");
|
||||
ToastUtils.show("空转GPS服务已启动");
|
||||
handler.post(updateRunnable);
|
||||
handler.post(bearingRunnable);
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止空转模拟服务
|
||||
*/
|
||||
public void stop() {
|
||||
if (!isRunning) return;
|
||||
isRunning = false;
|
||||
handler.removeCallbacks(updateRunnable);
|
||||
handler.removeCallbacks(bearingRunnable);
|
||||
notifyStatusChange("空转GPS服务已停止");
|
||||
AppIdleRunningModeHandler.sendIdleLog("空转GPS服务已停止");
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册GPS监听
|
||||
*/
|
||||
public void registerGpsUpdateListener(MainService.GpsUpdateListener listener) {
|
||||
synchronized (listeners) {
|
||||
if (!listeners.contains(listener)) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
}
|
||||
if (!isRunning) {
|
||||
start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销GPS监听
|
||||
*/
|
||||
public void unregisterGpsUpdateListener(MainService.GpsUpdateListener listener) {
|
||||
synchronized (listeners) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
if (listeners.isEmpty() && isRunning) {
|
||||
stopMockUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private void startMockUpdate() {
|
||||
if (isRunning) return;
|
||||
isRunning = true;
|
||||
currentBearing = 1;
|
||||
notifyStatusChange("空转GPS服务已启动");
|
||||
handler.post(updateRunnable);
|
||||
handler.post(bearingRunnable);
|
||||
}
|
||||
|
||||
private void stopMockUpdate() {
|
||||
isRunning = false;
|
||||
handler.removeCallbacks(updateRunnable);
|
||||
handler.removeCallbacks(bearingRunnable);
|
||||
notifyStatusChange("空转GPS服务已停止");
|
||||
}
|
||||
|
||||
private void notifyListeners(PositionModel pos) {
|
||||
synchronized (listeners) {
|
||||
for (MainService.GpsUpdateListener listener : listeners) {
|
||||
listener.onGpsPositionUpdated(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyStatusChange(String status) {
|
||||
synchronized (listeners) {
|
||||
for (MainService.GpsUpdateListener listener : listeners) {
|
||||
listener.onGpsStatusChanged(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return isRunning;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import android.os.Looper;
|
||||
import android.text.TextUtils;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.positions.App;
|
||||
import cc.winboll.studio.positions.models.PositionModel;
|
||||
import cc.winboll.studio.positions.models.PositionTaskModel;
|
||||
import cc.winboll.studio.positions.utils.AppConfigsUtil;
|
||||
@@ -54,6 +55,35 @@ public class MainService extends Service {
|
||||
void onGpsStatusChanged(String status);
|
||||
}
|
||||
|
||||
// 空转GPS监听实例(用于注册到IdleGpsService)
|
||||
private final GpsUpdateListener mIdleGpsListener = new GpsUpdateListener() {
|
||||
@Override
|
||||
public void onGpsPositionUpdated(PositionModel currentGpsPos) {
|
||||
handleGpsPositionUpdate(currentGpsPos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGpsStatusChanged(String status) {
|
||||
handleGpsStatusChange(status);
|
||||
}
|
||||
};
|
||||
|
||||
// 中央处理:GPS 位置更新
|
||||
private void handleGpsPositionUpdate(PositionModel pos) {
|
||||
if (pos == null) return;
|
||||
syncCurrentGpsPosition(pos);
|
||||
DistanceCalculatorUtil.getInstance(MainService.this).checkAllTaskTriggerCondition(pos);
|
||||
String src = App.isAppIdleRunning() ? " (空转)" : "";
|
||||
LogUtils.d(TAG, "GPS位置更新:纬度=" + pos.getLatitude() + ",经度=" + pos.getLongitude() + src);
|
||||
}
|
||||
|
||||
// 中央处理:GPS 状态变化
|
||||
private void handleGpsStatusChange(String status) {
|
||||
LogUtils.d(TAG, "GPS状态变化:" + status);
|
||||
notifyAllGpsStatusListeners(status);
|
||||
updateNotificationGpsStatus(status);
|
||||
}
|
||||
|
||||
// 任务更新监听接口(Java 7 风格,供Adapter监听任务变化)
|
||||
public interface TaskUpdateListener {
|
||||
void onTaskUpdated();
|
||||
@@ -77,6 +107,7 @@ public class MainService extends Service {
|
||||
private final ArrayList<PositionModel> mPositionList = new ArrayList<PositionModel>(); // 位置数据列表
|
||||
private final ArrayList<PositionTaskModel> mAllTasks = new ArrayList<PositionTaskModel>();// 任务数据列表
|
||||
private static PositionModel _mCurrentGpsPosition; // 当前GPS定位数据
|
||||
private boolean isListeningToIdleGps = false; // 当前是否监听空转GPS
|
||||
|
||||
// 服务相关变量(Java 7 显式声明,保持原逻辑)
|
||||
MyServiceConnection mMyServiceConnection;
|
||||
@@ -230,24 +261,24 @@ public class MainService extends Service {
|
||||
* 删除任务(Adapter调用,通过迭代器安全删除,避免并发异常)
|
||||
* @param taskId 待删除任务的ID
|
||||
*/
|
||||
public void deleteTask(String taskId) {
|
||||
public void deleteTask(final String taskId) {
|
||||
if (TextUtils.isEmpty(taskId) || mAllTasks.isEmpty()) {
|
||||
LogUtils.w(TAG, "deletePositionTask:任务ID为空或列表为空,删除失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 迭代器删除(Java 7 唯一安全删除集合元素的方式)
|
||||
Iterator<PositionTaskModel> taskIter = mAllTasks.iterator();
|
||||
while (taskIter.hasNext()) {
|
||||
PositionTaskModel task = taskIter.next();
|
||||
if (taskId.equals(task.getTaskId())) {
|
||||
taskIter.remove(); // 迭代器安全删除,无ConcurrentModificationException
|
||||
saveAllTasks();
|
||||
notifyTaskUpdated();
|
||||
LogUtils.d(TAG, "deletePositionTask:成功(任务ID=" + taskId + ")");
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 迭代器删除(Java 7 唯一安全删除集合元素的方式)
|
||||
Iterator<PositionTaskModel> taskIter = mAllTasks.iterator();
|
||||
while (taskIter.hasNext()) {
|
||||
PositionTaskModel task = taskIter.next();
|
||||
if (taskId.equals(task.getTaskId())) {
|
||||
taskIter.remove(); // 迭代器安全删除,无ConcurrentModificationException
|
||||
saveAllTasks();
|
||||
notifyTaskUpdated();
|
||||
LogUtils.d(TAG, "deletePositionTask:成功(任务ID=" + taskId + ")");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -366,6 +397,9 @@ public class MainService extends Service {
|
||||
}
|
||||
|
||||
if (mAppConfigsUtil.isEnableMainService(true)) {
|
||||
if (App.isAppIdleRunning()) {
|
||||
IdleGpsService.getInstance().start();
|
||||
}
|
||||
run(); // 启动服务核心逻辑
|
||||
}
|
||||
}
|
||||
@@ -382,6 +416,9 @@ public class MainService extends Service {
|
||||
|
||||
// 启动前台服务(Java 7 显式调用,无方法引用)
|
||||
String initialStatus = "[ Positions ] is in Service.";
|
||||
if (App.isAppIdleRunning()) {
|
||||
initialStatus += " [IDLE RUNNING]";
|
||||
}
|
||||
NotificationUtil.createForegroundServiceNotification(this, initialStatus);
|
||||
startForeground(NotificationUtil.FOREGROUND_SERVICE_NOTIFICATION_ID,
|
||||
NotificationUtil.createForegroundServiceNotification(this, initialStatus));
|
||||
@@ -599,21 +636,28 @@ public class MainService extends Service {
|
||||
if (!_mIsServiceRunning || _mCurrentGpsPosition == null) {
|
||||
return;
|
||||
}
|
||||
// 格式化通知内容(Java 7 String.format,无String.join等Java 8+方法)
|
||||
final String gpsStatus = String.format(
|
||||
"GPS位置:北纬%.4f° 东经%.4f° | 可见位置:%d个",
|
||||
// 根据空转状态决定通知前缀(区分空转GPS与真实GPS)
|
||||
String prefix = App.isAppIdleRunning() ? "空转GPS" : "GPS位置";
|
||||
// 格式化通知内容(Java 7 String.format,使用%.15f显示全部GPS精度)
|
||||
String gpsStatus = String.format(
|
||||
"%s:北纬%.15f° 东经%.15f° | 可见位置:%d个",
|
||||
prefix,
|
||||
_mCurrentGpsPosition.getLatitude(),
|
||||
_mCurrentGpsPosition.getLongitude(),
|
||||
mVisiblePositionIds.size()
|
||||
);
|
||||
if (App.isAppIdleRunning()) {
|
||||
gpsStatus += " [IDLE RUNNING]";
|
||||
}
|
||||
final String finalGpsStatus = gpsStatus;
|
||||
// 主线程判断+切换(Java 7 匿名内部类)
|
||||
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||
NotificationUtil.updateForegroundServiceStatus(this, gpsStatus);
|
||||
NotificationUtil.updateForegroundServiceStatus(this, finalGpsStatus);
|
||||
} else {
|
||||
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
NotificationUtil.updateForegroundServiceStatus(MainService.this, gpsStatus);
|
||||
NotificationUtil.updateForegroundServiceStatus(MainService.this, finalGpsStatus);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -634,6 +678,9 @@ public class MainService extends Service {
|
||||
if (intent != null) {
|
||||
isSettingToEnable = intent.getBooleanExtra(EXTRA_IS_SETTING_TO_ENABLE, false);
|
||||
if (isSettingToEnable) {
|
||||
if (App.isAppIdleRunning()) {
|
||||
IdleGpsService.getInstance().start();
|
||||
}
|
||||
run(); // 重启服务核心逻辑(保证服务启动后进入运行状态)
|
||||
}
|
||||
}
|
||||
@@ -701,10 +748,8 @@ public class MainService extends Service {
|
||||
gpsPos.setPositionId("CURRENT_GPS_POS");
|
||||
gpsPos.setMemo("实时GPS位置");
|
||||
|
||||
// 同步GPS位置+刷新距离+日志(原逻辑保留)
|
||||
syncCurrentGpsPosition(gpsPos);
|
||||
DistanceCalculatorUtil.getInstance(MainService.this).checkAllTaskTriggerCondition(gpsPos);
|
||||
LogUtils.d(TAG, "GPS位置更新:纬度=" + location.getLatitude() + ",经度=" + location.getLongitude());
|
||||
// 调用中央处理方法
|
||||
handleGpsPositionUpdate(gpsPos);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -725,9 +770,7 @@ public class MainService extends Service {
|
||||
statusDesc = "GPS状态:临时不可用(遮挡)";
|
||||
break;
|
||||
}
|
||||
LogUtils.d(TAG, statusDesc);
|
||||
notifyAllGpsStatusListeners(statusDesc);
|
||||
updateNotificationGpsStatus(statusDesc);
|
||||
handleGpsStatusChange(statusDesc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -736,10 +779,7 @@ public class MainService extends Service {
|
||||
// GPS启用时更新状态+通知+重启定位(Java 7 基础逻辑)
|
||||
if (provider.equals(LocationManager.GPS_PROVIDER)) {
|
||||
isGpsEnabled = true;
|
||||
String statusDesc = "GPS已开启(用户手动打开)";
|
||||
LogUtils.d(TAG, statusDesc);
|
||||
notifyAllGpsStatusListeners(statusDesc);
|
||||
updateNotificationGpsStatus("GPS已开启,正在获取位置...");
|
||||
handleGpsStatusChange("GPS已开启(用户手动打开)");
|
||||
startGpsLocation();
|
||||
}
|
||||
}
|
||||
@@ -750,10 +790,7 @@ public class MainService extends Service {
|
||||
if (provider.equals(LocationManager.GPS_PROVIDER)) {
|
||||
isGpsEnabled = false;
|
||||
_mCurrentGpsPosition = null;
|
||||
String statusDesc = "GPS已关闭(用户手动关闭)";
|
||||
LogUtils.w(TAG, statusDesc);
|
||||
notifyAllGpsStatusListeners(statusDesc);
|
||||
updateNotificationGpsStatus("GPS已关闭,请在设置中开启");
|
||||
handleGpsStatusChange("GPS已关闭(用户手动关闭)");
|
||||
ToastUtils.show("GPS已关闭,无法获取位置,请在设置中开启");
|
||||
}
|
||||
}
|
||||
@@ -801,8 +838,27 @@ public class MainService extends Service {
|
||||
|
||||
/**
|
||||
* 启动GPS定位(Java 7 异常处理,无try-with-resources,显式捕获SecurityException)
|
||||
* 【关键修改】根据应用空转状态切换数据源(IdleGpsService 或 系统GPS)
|
||||
*/
|
||||
private void startGpsLocation() {
|
||||
// 检查空转状态:如果处于空转,使用 IdleGpsService
|
||||
if (App.isAppIdleRunning()) {
|
||||
if (isListeningToIdleGps) return; // 已在监听空转GPS,无需重复注册
|
||||
stopGpsLocation(); // 停止系统GPS监听(如果正在运行)
|
||||
IdleGpsService.getInstance().registerGpsUpdateListener(mIdleGpsListener);
|
||||
isListeningToIdleGps = true;
|
||||
LogUtils.d(TAG, "启动GPS定位:使用空转模拟数据");
|
||||
handleGpsStatusChange("空转GPS监听中...");
|
||||
return;
|
||||
}
|
||||
|
||||
// 系统GPS逻辑
|
||||
if (isListeningToIdleGps) {
|
||||
// 之前是空转,现在切换到系统GPS
|
||||
IdleGpsService.getInstance().unregisterGpsUpdateListener(mIdleGpsListener);
|
||||
isListeningToIdleGps = false;
|
||||
}
|
||||
|
||||
if (!checkGpsReady()) {
|
||||
return;
|
||||
}
|
||||
@@ -824,12 +880,11 @@ public class MainService extends Service {
|
||||
lastGpsPos.setLatitude(lastKnownLocation.getLatitude());
|
||||
lastGpsPos.setLongitude(lastKnownLocation.getLongitude());
|
||||
lastGpsPos.setPositionId("CURRENT_GPS_POS");
|
||||
syncCurrentGpsPosition(lastGpsPos);
|
||||
handleGpsPositionUpdate(lastGpsPos);
|
||||
LogUtils.d(TAG, "已获取缓存GPS位置:纬度=" + lastKnownLocation.getLatitude());
|
||||
} else {
|
||||
String tip = "无缓存GPS位置,等待实时定位...";
|
||||
LogUtils.d(TAG, tip);
|
||||
notifyAllGpsStatusListeners(tip);
|
||||
handleGpsStatusChange(tip);
|
||||
updateNotificationGpsStatus("GPS搜索中(请移至开阔地带)");
|
||||
}
|
||||
|
||||
@@ -837,33 +892,40 @@ public class MainService extends Service {
|
||||
// 定位权限异常(Java 7 显式捕获,无Lambda异常处理)
|
||||
String error = "启动GPS失败(权限异常):" + e.getMessage();
|
||||
LogUtils.e(TAG, error);
|
||||
notifyAllGpsStatusListeners(error);
|
||||
handleGpsStatusChange(error);
|
||||
isGpsPermissionGranted = false;
|
||||
updateNotificationGpsStatus("定位权限异常,无法获取GPS");
|
||||
} catch (Exception e) {
|
||||
// 其他异常(如LocationManager为空、系统服务异常等)
|
||||
String error = "启动GPS失败:" + e.getMessage();
|
||||
LogUtils.e(TAG, error);
|
||||
notifyAllGpsStatusListeners(error);
|
||||
handleGpsStatusChange(error);
|
||||
updateNotificationGpsStatus("GPS启动失败,尝试重试...");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止GPS定位(Java 7 异常处理,移除监听器避免内存泄漏)
|
||||
* 【关键修改】根据当前监听源停止对应的服务
|
||||
*/
|
||||
private void stopGpsLocation() {
|
||||
// 校验参数:避免空指针+权限未授予时调用
|
||||
if (mLocationManager != null && mGpsLocationListener != null && isGpsPermissionGranted) {
|
||||
try {
|
||||
mLocationManager.removeUpdates(mGpsLocationListener);
|
||||
String tip = "GPS定位已停止(移除监听器)";
|
||||
LogUtils.d(TAG, tip);
|
||||
notifyAllGpsStatusListeners(tip);
|
||||
} catch (Exception e) {
|
||||
String error = "停止GPS失败:" + e.getMessage();
|
||||
LogUtils.e(TAG, error);
|
||||
notifyAllGpsStatusListeners(error);
|
||||
if (isListeningToIdleGps) {
|
||||
IdleGpsService.getInstance().unregisterGpsUpdateListener(mIdleGpsListener);
|
||||
isListeningToIdleGps = false;
|
||||
LogUtils.d(TAG, "停止GPS定位:已注销空转GPS监听");
|
||||
} else {
|
||||
// 校验参数:避免空指针+权限未授予时调用
|
||||
if (mLocationManager != null && mGpsLocationListener != null && isGpsPermissionGranted) {
|
||||
try {
|
||||
mLocationManager.removeUpdates(mGpsLocationListener);
|
||||
String tip = "GPS定位已停止(移除监听器)";
|
||||
LogUtils.d(TAG, tip);
|
||||
handleGpsStatusChange(tip);
|
||||
} catch (Exception e) {
|
||||
String error = "停止GPS失败:" + e.getMessage();
|
||||
LogUtils.e(TAG, error);
|
||||
handleGpsStatusChange(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -915,14 +977,19 @@ public class MainService extends Service {
|
||||
*/
|
||||
void updateNotificationGpsStatus(final String statusText) {
|
||||
if (_mIsServiceRunning) {
|
||||
String text = statusText;
|
||||
if (App.isAppIdleRunning()) {
|
||||
text += " [IDLE RUNNING]";
|
||||
}
|
||||
final String finalText = text;
|
||||
// 判断当前线程是否为主线程,避免UI操作在子线程
|
||||
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||
NotificationUtil.updateForegroundServiceStatus(this, statusText);
|
||||
NotificationUtil.updateForegroundServiceStatus(this, finalText);
|
||||
} else {
|
||||
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
NotificationUtil.updateForegroundServiceStatus(MainService.this, statusText);
|
||||
NotificationUtil.updateForegroundServiceStatus(MainService.this, finalText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package cc.winboll.studio.positions.views;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/09/30 08:09
|
||||
* @Describe 位置任务列表视图(适配MainService唯一数据源+同步任务状态+支持简单/编辑模式)
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @CreateTime 2025-09-30 08:09:00
|
||||
* @EditTime 2026-03-31 19:00:00
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -21,6 +23,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import cc.winboll.studio.libaes.dialogs.YesNoAlertDialog;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.positions.R;
|
||||
import cc.winboll.studio.positions.models.PositionTaskModel;
|
||||
@@ -35,101 +38,96 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class PositionTaskListView extends LinearLayout {
|
||||
|
||||
// 视图模式常量
|
||||
public static final int VIEW_MODE_SIMPLE = 1;
|
||||
public static final int VIEW_MODE_EDIT = 2;
|
||||
|
||||
// 核心成员变量(新增MainService引用,作为唯一数据源)
|
||||
private static final String TAG = "PositionTaskListView";
|
||||
|
||||
// 核心成员变量
|
||||
private String mBindPositionId;
|
||||
private MainService mMainService; // 持有MainService实例,所有任务数据从服务获取
|
||||
private MainService mMainService;
|
||||
private int mCurrentViewMode;
|
||||
private TaskListAdapter mTaskAdapter;
|
||||
private RecyclerView mRvTasks;
|
||||
|
||||
// 任务修改回调接口(保留,用于通知外部同步UI)
|
||||
// 任务修改回调接口
|
||||
public interface OnTaskUpdatedListener {
|
||||
void onTaskUpdated(String positionId, ArrayList<PositionTaskModel> updatedTasks);
|
||||
}
|
||||
|
||||
private OnTaskUpdatedListener mOnTaskUpdatedListener;
|
||||
|
||||
// ---------------------- 构造函数(不变,新增MainService空校验) ----------------------
|
||||
// ---------------------- 构造函数 ----------------------
|
||||
public PositionTaskListView(Context context) {
|
||||
super(context);
|
||||
LogUtils.d(TAG, "PositionTaskListView 构造函数 1 参数: context=" + context);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
public PositionTaskListView(Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
LogUtils.d(TAG, "PositionTaskListView 构造函数 2 参数: context=" + context + ", attrs=" + attrs);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
public PositionTaskListView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
LogUtils.d(TAG, "PositionTaskListView 构造函数 3 参数: context=" + context + ", attrs=" + attrs + ", defStyleAttr=" + defStyleAttr);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
// 初始化视图(绑定控件+设置布局,Adapter初始化为空数据)
|
||||
// ---------------------- 初始化视图 ----------------------
|
||||
private void initView(Context context) {
|
||||
LogUtils.d(TAG, "initView 开始执行 参数: context=" + context);
|
||||
setOrientation(VERTICAL);
|
||||
LayoutInflater.from(context).inflate(R.layout.view_position_task_list, this, true);
|
||||
|
||||
mRvTasks = (RecyclerView) findViewById(R.id.rv_position_tasks);
|
||||
mRvTasks.setLayoutManager(new LinearLayoutManager(context));
|
||||
|
||||
// 初始化为空列表(数据后续从MainService同步)
|
||||
mTaskAdapter = new TaskListAdapter(new ArrayList<PositionTaskModel>());
|
||||
mRvTasks.setAdapter(mTaskAdapter);
|
||||
|
||||
mCurrentViewMode = VIEW_MODE_SIMPLE;
|
||||
LogUtils.d(TAG, "视图初始化完成(等待绑定MainService和位置ID)");
|
||||
LogUtils.d(TAG, "initView 执行完成,视图初始化完毕");
|
||||
}
|
||||
|
||||
// ---------------------- 对外API(核心调整:绑定MainService+从服务同步数据) ----------------------
|
||||
/**
|
||||
* 初始化:绑定MainService+关联位置ID(必须先调用此方法,否则无数据)
|
||||
* @param mainService MainService实例(从Activity传入,确保唯一数据源)
|
||||
* @param positionId 关联的位置ID(只加载该位置下的任务)
|
||||
*/
|
||||
// ---------------------- 对外API ----------------------
|
||||
public void init(MainService mainService, String positionId) {
|
||||
LogUtils.d(TAG, "init 开始执行 参数: mainService=" + mainService + ", positionId=" + positionId);
|
||||
if (mainService == null) {
|
||||
LogUtils.e(TAG, "init失败:MainService实例为空(需从Activity传入有效服务实例)");
|
||||
LogUtils.e(TAG, "init 失败:MainService 为空");
|
||||
showToast("任务列表初始化失败:服务未就绪");
|
||||
return;
|
||||
}
|
||||
if (positionId == null || positionId.trim().isEmpty()) {
|
||||
LogUtils.e(TAG, "init失败:位置ID为空(需关联有效位置)");
|
||||
LogUtils.e(TAG, "init 失败:positionId 为空");
|
||||
showToast("任务列表初始化失败:未关联位置");
|
||||
return;
|
||||
}
|
||||
|
||||
// 绑定服务实例+位置ID
|
||||
this.mMainService = mainService;
|
||||
this.mBindPositionId = positionId;
|
||||
LogUtils.d(TAG, "已绑定MainService和位置ID:" + positionId);
|
||||
mMainService = mainService;
|
||||
mBindPositionId = positionId;
|
||||
LogUtils.d(TAG, "init 绑定成功:positionId=" + positionId);
|
||||
|
||||
// 从MainService同步当前位置的任务(核心:数据来源改为服务)
|
||||
syncTasksFromMainService();
|
||||
LogUtils.d(TAG, "init 执行完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 从MainService同步当前位置的任务(核心方法,所有数据加载入口)
|
||||
* 作用:1. 清空本地缓存→2. 从服务获取全量任务→3. 筛选当前位置任务→4. 刷新Adapter
|
||||
*/
|
||||
public void syncTasksFromMainService() {
|
||||
// 安全校验(服务未绑定/位置ID为空,不执行同步)
|
||||
LogUtils.d(TAG, "syncTasksFromMainService 开始执行");
|
||||
if (mMainService == null || mBindPositionId == null || mBindPositionId.trim().isEmpty()) {
|
||||
LogUtils.w(TAG, "同步任务失败:MainService未绑定或位置ID无效");
|
||||
LogUtils.w(TAG, "syncTasksFromMainService 失败:服务或位置ID无效");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 从MainService获取全量任务(服务是唯一数据源,避免本地缓存不一致)
|
||||
ArrayList<PositionTaskModel> allServiceTasks = mMainService.getAllTasks();
|
||||
LogUtils.d(TAG, "从MainService获取全量任务数:" + (allServiceTasks == null ? 0 : allServiceTasks.size()));
|
||||
LogUtils.d(TAG, "syncTasksFromMainService 从服务获取任务总数: " + (allServiceTasks == null ? 0 : allServiceTasks.size()));
|
||||
|
||||
// 2. 筛选当前位置关联的任务(只保留与mBindPositionId匹配的任务)
|
||||
ArrayList<PositionTaskModel> currentPosTasks = new ArrayList<PositionTaskModel>();
|
||||
ArrayList<PositionTaskModel> currentPosTasks = new ArrayList<>();
|
||||
if (allServiceTasks != null && !allServiceTasks.isEmpty()) {
|
||||
for (PositionTaskModel task : allServiceTasks) {
|
||||
if (isTaskMatchedWithPosition(task)) {
|
||||
@@ -137,144 +135,148 @@ public class PositionTaskListView extends LinearLayout {
|
||||
}
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "筛选后当前位置任务数:" + currentPosTasks.size());
|
||||
LogUtils.d(TAG, "syncTasksFromMainService 筛选后当前位置任务数: " + currentPosTasks.size());
|
||||
|
||||
// 3. 更新Adapter数据(直接替换数据源,避免本地缓存)
|
||||
mTaskAdapter.updateData(currentPosTasks);
|
||||
LogUtils.d(TAG, "从MainService同步任务完成(Adapter已刷新)");
|
||||
LogUtils.d(TAG, "syncTasksFromMainService 执行完成,Adapter已刷新");
|
||||
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, "同步任务失败(MainService调用异常):" + e.getMessage());
|
||||
LogUtils.e(TAG, "syncTasksFromMainService 异常: " + e.getMessage(), e);
|
||||
showToast("任务同步失败,请重试");
|
||||
}
|
||||
}
|
||||
|
||||
// 视图模式切换(不变,刷新Adapter触发视图类型变更)
|
||||
public void setViewStatus(int viewMode) {
|
||||
LogUtils.d(TAG, "setViewStatus 参数: viewMode=" + viewMode);
|
||||
if (viewMode != VIEW_MODE_SIMPLE && viewMode != VIEW_MODE_EDIT) {
|
||||
LogUtils.w(TAG, "设置视图模式失败:无效模式(仅支持简单/编辑模式)");
|
||||
LogUtils.w(TAG, "setViewStatus 无效模式");
|
||||
return;
|
||||
}
|
||||
mCurrentViewMode = viewMode;
|
||||
mTaskAdapter.notifyDataSetChanged();
|
||||
LogUtils.d(TAG, "已切换视图模式:" + (viewMode == VIEW_MODE_SIMPLE ? "简单模式" : "编辑模式"));
|
||||
LogUtils.d(TAG, "setViewStatus 切换完成: " + (viewMode == VIEW_MODE_SIMPLE ? "简单模式" : "编辑模式"));
|
||||
}
|
||||
|
||||
// 保留回调接口(用于通知外部UI刷新,如Activity更新列表)
|
||||
public void setOnTaskUpdatedListener(OnTaskUpdatedListener listener) {
|
||||
this.mOnTaskUpdatedListener = listener;
|
||||
LogUtils.d(TAG, "已设置任务更新回调监听");
|
||||
LogUtils.d(TAG, "setOnTaskUpdatedListener 参数: listener=" + listener);
|
||||
mOnTaskUpdatedListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前位置的任务(从Adapter数据源获取,而非本地缓存)
|
||||
* @return 当前位置任务列表(新集合,避免外部修改数据源)
|
||||
*/
|
||||
public ArrayList<PositionTaskModel> getCurrentPosTasks() {
|
||||
return new ArrayList<PositionTaskModel>(mTaskAdapter.getAdapterData());
|
||||
LogUtils.d(TAG, "getCurrentPosTasks 被调用");
|
||||
return new ArrayList<>(mTaskAdapter.getAdapterData());
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空数据(解绑服务+清空位置ID+重置Adapter)
|
||||
* 场景:视图销毁/切换位置时调用,避免数据残留
|
||||
*/
|
||||
public void clearData() {
|
||||
// 1. 清空Adapter数据源
|
||||
LogUtils.d(TAG, "clearData 开始执行");
|
||||
mTaskAdapter.updateData(new ArrayList<PositionTaskModel>());
|
||||
// 2. 解绑服务+位置ID(避免下次使用时引用旧数据)
|
||||
this.mMainService = null;
|
||||
this.mBindPositionId = null;
|
||||
// 3. 重置视图模式
|
||||
mMainService = null;
|
||||
mBindPositionId = null;
|
||||
mCurrentViewMode = VIEW_MODE_SIMPLE;
|
||||
LogUtils.d(TAG, "数据已清空(解绑服务+重置视图)");
|
||||
LogUtils.d(TAG, "clearData 执行完成,数据已清空");
|
||||
}
|
||||
|
||||
/**
|
||||
* 主动触发任务同步(强制从服务拉取最新数据,刷新视图)
|
||||
* 场景:外部操作后(如新增任务),调用此方法更新列表
|
||||
*/
|
||||
public void triggerTaskSync() {
|
||||
LogUtils.d(TAG, "主动触发任务同步(从MainService拉取最新数据)");
|
||||
LogUtils.d(TAG, "triggerTaskSync 主动触发同步");
|
||||
syncTasksFromMainService();
|
||||
|
||||
// 通知外部(如Activity)任务已更新(可选,根据业务需求)
|
||||
if (mOnTaskUpdatedListener != null && mBindPositionId != null) {
|
||||
mOnTaskUpdatedListener.onTaskUpdated(mBindPositionId, getCurrentPosTasks());
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------- 内部工具方法(新增服务空校验) ----------------------
|
||||
private static final String TAG = "PositionTaskListView";
|
||||
|
||||
/**
|
||||
* 校验任务是否与当前绑定的位置匹配
|
||||
* @param task 待校验的任务
|
||||
* @return true=匹配(任务位置ID=当前绑定位置ID),false=不匹配
|
||||
*/
|
||||
// ---------------------- 内部工具 ----------------------
|
||||
private boolean isTaskMatchedWithPosition(PositionTaskModel task) {
|
||||
if (task == null || mBindPositionId == null || mBindPositionId.trim().isEmpty()) {
|
||||
if (task == null || mBindPositionId == null) {
|
||||
return false;
|
||||
}
|
||||
// 严格匹配任务的位置ID(确保只加载当前位置的任务)
|
||||
return mBindPositionId.equals(task.getPositionId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示Toast(简化调用,避免重复代码)
|
||||
*/
|
||||
private void showToast(String content) {
|
||||
if (getContext() == null) return;
|
||||
Toast.makeText(getContext(), content, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
// ---------------------- 内部Adapter:适配MainService数据源(核心调整) ----------------------
|
||||
private String genSelectedTimeText(long timeMillis) {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault());
|
||||
return sdf.format(new Date(timeMillis));
|
||||
}
|
||||
|
||||
// ---------------------- 外部新增任务 ----------------------
|
||||
public void addNewTask(PositionTaskModel newTask) {
|
||||
LogUtils.d(TAG, "addNewTask 参数: newTask=" + newTask);
|
||||
if (mMainService == null) {
|
||||
LogUtils.e(TAG, "addNewTask 失败:MainService为空");
|
||||
showToast("新增任务失败:服务未就绪");
|
||||
return;
|
||||
}
|
||||
if (newTask == null) {
|
||||
LogUtils.e(TAG, "addNewTask 失败:任务为空");
|
||||
showToast("新增任务失败:任务数据为空");
|
||||
return;
|
||||
}
|
||||
if (mBindPositionId == null || mBindPositionId.trim().isEmpty()) {
|
||||
LogUtils.e(TAG, "addNewTask 失败:未绑定位置");
|
||||
showToast("新增任务失败:未关联位置");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
newTask.setPositionId(mBindPositionId);
|
||||
mMainService.addTask(newTask);
|
||||
LogUtils.d(TAG, "addNewTask 服务添加成功 taskId=" + newTask.getTaskId());
|
||||
|
||||
syncTasksFromMainService();
|
||||
if (mOnTaskUpdatedListener != null) {
|
||||
mOnTaskUpdatedListener.onTaskUpdated(mBindPositionId, getCurrentPosTasks());
|
||||
}
|
||||
showToast("新增任务成功");
|
||||
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "addNewTask 异常: " + e.getMessage(), e);
|
||||
showToast("新增失败,请重试");
|
||||
syncTasksFromMainService();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------- Adapter ----------------------
|
||||
private class TaskListAdapter extends RecyclerView.Adapter<TaskListAdapter.TaskViewHolder> {
|
||||
// Adapter数据源(仅保留一份,直接从MainService同步,无本地冗余)
|
||||
|
||||
private List<PositionTaskModel> mAdapterData;
|
||||
|
||||
// 初始化Adapter(空数据源)
|
||||
public TaskListAdapter(List<PositionTaskModel> data) {
|
||||
this.mAdapterData = new ArrayList<PositionTaskModel>(data); // 防御性拷贝,避免外部修改
|
||||
LogUtils.d(TAG, "TaskListAdapter 构造 参数 data.size=" + data.size());
|
||||
mAdapterData = new ArrayList<>(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新Adapter数据源(核心:从MainService同步后调用,替换数据源并刷新)
|
||||
* @param newData 从MainService筛选后的当前位置任务列表
|
||||
*/
|
||||
public void updateData(List<PositionTaskModel> newData) {
|
||||
LogUtils.d(TAG, "updateData 参数 newData.size=" + (newData == null ? 0 : newData.size()));
|
||||
if (newData == null) {
|
||||
this.mAdapterData.clear();
|
||||
mAdapterData.clear();
|
||||
} else {
|
||||
this.mAdapterData = new ArrayList<PositionTaskModel>(newData); // 防御性拷贝
|
||||
mAdapterData = new ArrayList<>(newData);
|
||||
}
|
||||
notifyDataSetChanged(); // 刷新列表(数据源已替换,确保显示最新数据)
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Adapter当前数据源(对外提供,避免直接操作mAdapterData)
|
||||
* @return 当前数据源(新集合,避免外部修改)
|
||||
*/
|
||||
public List<PositionTaskModel> getAdapterData() {
|
||||
return new ArrayList<PositionTaskModel>(mAdapterData);
|
||||
return new ArrayList<>(mAdapterData);
|
||||
}
|
||||
|
||||
// getItemCount:空列表显示1个“空提示”项,非空显示任务数
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mAdapterData.isEmpty() ? 1 : mAdapterData.size();
|
||||
}
|
||||
|
||||
// getItemViewType:按“空列表/简单模式/编辑模式”区分视图类型(不变)
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (mAdapterData.isEmpty()) {
|
||||
return 0; // 0=空提示视图
|
||||
return 0;
|
||||
} else {
|
||||
return mCurrentViewMode; // 1=简单模式视图,2=编辑模式视图
|
||||
return mCurrentViewMode;
|
||||
}
|
||||
}
|
||||
|
||||
// onCreateViewHolder:按视图类型加载对应布局(不变,确保布局与模式匹配)
|
||||
@NonNull
|
||||
@Override
|
||||
public TaskViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
@@ -282,298 +284,230 @@ public class PositionTaskListView extends LinearLayout {
|
||||
LayoutInflater inflater = LayoutInflater.from(context);
|
||||
|
||||
if (viewType == 0) {
|
||||
// 空提示布局(无任务时显示)
|
||||
View emptyView = inflater.inflate(R.layout.item_task_empty, parent, false);
|
||||
return new EmptyViewHolder(emptyView);
|
||||
} else if (viewType == VIEW_MODE_SIMPLE) {
|
||||
// 简单模式布局(带isBingo红点,仅展示)
|
||||
View simpleTaskView = inflater.inflate(R.layout.item_position_task_simple, parent, false);
|
||||
return new SimpleTaskViewHolder(simpleTaskView);
|
||||
View simpleView = inflater.inflate(R.layout.item_position_task_simple, parent, false);
|
||||
return new SimpleTaskViewHolder(simpleView);
|
||||
} else {
|
||||
// 编辑模式布局(带编辑/删除按钮+启用开关,支持修改)
|
||||
View editTaskView = inflater.inflate(R.layout.item_task_content, parent, false);
|
||||
return new TaskContentViewHolder(editTaskView);
|
||||
View editView = inflater.inflate(R.layout.item_task_content, parent, false);
|
||||
return new TaskContentViewHolder(editView);
|
||||
}
|
||||
}
|
||||
|
||||
// onBindViewHolder:按视图类型绑定数据(核心调整:操作后同步MainService)
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull TaskViewHolder holder, int position) {
|
||||
// 1. 空提示视图绑定(根据模式显示不同提示文案)
|
||||
if (holder instanceof EmptyViewHolder) {
|
||||
EmptyViewHolder emptyHolder = (EmptyViewHolder) holder;
|
||||
TextView tvEmptyTip = emptyHolder.itemView.findViewById(R.id.tv_task_empty_tip);
|
||||
tvEmptyTip.setText(mCurrentViewMode == VIEW_MODE_EDIT
|
||||
? "暂无任务,点击\"添加新任务\"创建"
|
||||
: "暂无启用的任务");
|
||||
TextView tv = emptyHolder.itemView.findViewById(R.id.tv_task_empty_tip);
|
||||
tv.setText(mCurrentViewMode == VIEW_MODE_EDIT
|
||||
? "暂无任务,点击\"添加新任务\"创建"
|
||||
: "暂无启用的任务");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 任务项有效性校验(避免越界/空数据)
|
||||
if (position >= mAdapterData.size()) {
|
||||
LogUtils.w(TAG, "绑定任务数据失败:位置索引越界(position=" + position + ",数据量=" + mAdapterData.size() + ")");
|
||||
LogUtils.w(TAG, "onBindViewHolder 越界 position=" + position);
|
||||
return;
|
||||
}
|
||||
|
||||
final PositionTaskModel task = mAdapterData.get(position);
|
||||
if (task == null) {
|
||||
LogUtils.w(TAG, "绑定任务数据失败:第" + position + "项任务为空");
|
||||
LogUtils.w(TAG, "onBindViewHolder task为空 position=" + position);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 简单模式绑定(仅展示,无修改操作,不变)
|
||||
if (holder instanceof SimpleTaskViewHolder) {
|
||||
SimpleTaskViewHolder simpleHolder = (SimpleTaskViewHolder) holder;
|
||||
// 任务描述
|
||||
String taskDesc = task.getTaskDescription() == null ? "未设置描述" : task.getTaskDescription();
|
||||
simpleHolder.tvSimpleTaskDesc.setText(String.format("任务:%s", taskDesc));
|
||||
simpleHolder.tvStartTime.setText(genSelectedTimeText(task.getStartTime()));
|
||||
// 距离条件(大于/小于+距离值)
|
||||
String distanceCond = task.isGreaterThan() ? "大于" : "小于";
|
||||
simpleHolder.tvSimpleDistanceCond.setText(String.format("条件:距离 %s %d 米", distanceCond, task.getDiscussDistance()));
|
||||
// 启用状态
|
||||
simpleHolder.tvSimpleIsEnable.setText(task.isEnable() ? "状态:已启用" : "状态:已禁用");
|
||||
// isBingo红点(任务触发时显示,未触发时隐藏)
|
||||
simpleHolder.vBingoDot.setVisibility(task.isBingo() ? View.VISIBLE : View.GONE);
|
||||
SimpleTaskViewHolder h = (SimpleTaskViewHolder) holder;
|
||||
String desc = task.getTaskDescription() == null ? "未设置描述" : task.getTaskDescription();
|
||||
h.tvSimpleTaskDesc.setText(String.format("任务:%s", desc));
|
||||
h.tvStartTime.setText(genSelectedTimeText(task.getStartTime()));
|
||||
String cond = task.isGreaterThan() ? "大于(◎)" : "小于(•)";
|
||||
h.tvSimpleDistanceCond.setText(String.format("条件:距离 %s %d 米", cond, task.getDiscussDistance()));
|
||||
h.tvSimpleIsEnable.setText(task.isEnable() ? "状态:已启用" : "状态:已禁用");
|
||||
h.tvSimpleIsEnable.setTextColor(task.isEnable()
|
||||
? getContext().getResources().getColor(R.color.colorEnableGreen)
|
||||
: getContext().getResources().getColor(R.color.colorGrayText));
|
||||
h.vBingoDot.setVisibility(task.isBingo() ? View.VISIBLE : View.GONE);
|
||||
|
||||
} else if (holder instanceof TaskContentViewHolder) {
|
||||
TaskContentViewHolder contentHolder = (TaskContentViewHolder) holder;
|
||||
bindEditModeTask(contentHolder, task, position);
|
||||
TaskContentViewHolder h = (TaskContentViewHolder) holder;
|
||||
bindEditModeTask(h, task, position);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑模式任务绑定(核心:修改操作→更新MainService→刷新Adapter)
|
||||
*/
|
||||
private void bindEditModeTask(final TaskContentViewHolder holder, final PositionTaskModel task, final int position) {
|
||||
// 4.1 绑定基础数据(描述+距离条件)
|
||||
String taskDesc = task.getTaskDescription() == null ? "未设置描述" : task.getTaskDescription();
|
||||
holder.tvTaskDesc.setText(String.format("任务:%s", taskDesc));
|
||||
String distanceCond = task.isGreaterThan() ? "大于" : "小于";
|
||||
holder.tvTaskDistance.setText(String.format("条件:%s %d 米", distanceCond, task.getDiscussDistance()));
|
||||
holder.tvStartTime.setText(genSelectedTimeText(task.getStartTime()));
|
||||
LogUtils.d(TAG, "bindEditModeTask position=" + position + " taskId=" + task.getTaskId());
|
||||
|
||||
String desc = task.getTaskDescription() == null ? "未设置描述" : task.getTaskDescription();
|
||||
holder.tvTaskDesc.setText(String.format("任务:%s", desc));
|
||||
String cond = task.isGreaterThan() ? "大于(◎)" : "小于(•)";
|
||||
holder.tvTaskDistance.setText(String.format("条件:%s %d 米", cond, task.getDiscussDistance()));
|
||||
holder.tvStartTime.setText(genSelectedTimeText(task.getStartTime()));
|
||||
|
||||
// 4.2 绑定“启用开关”(修复:先解绑监听→设值→再绑定监听,避免设值触发回调)
|
||||
holder.cbTaskEnable.setOnCheckedChangeListener(null);
|
||||
holder.cbTaskEnable.setChecked(task.isEnable());
|
||||
holder.cbTaskEnable.setEnabled(mCurrentViewMode == VIEW_MODE_EDIT); // 编辑模式才允许操作
|
||||
holder.cbTaskEnable.setEnabled(mCurrentViewMode == VIEW_MODE_EDIT);
|
||||
|
||||
// 4.3 编辑模式特有:显示编辑/删除按钮(仅编辑模式可见)
|
||||
holder.btnEditTask.setVisibility(View.VISIBLE);
|
||||
holder.btnDeleteTask.setVisibility(View.VISIBLE);
|
||||
|
||||
// 4.4 删除按钮逻辑(核心:删除→同步MainService→刷新Adapter)
|
||||
holder.btnDeleteTask.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (mMainService == null) {
|
||||
showToast("删除失败:服务未就绪");
|
||||
LogUtils.e(TAG, "删除任务失败:MainService实例为空");
|
||||
return;
|
||||
}
|
||||
YesNoAlertDialog.show(getContext(), "删除任务提示", "是否删除此项位置任务?", new YesNoAlertDialog.OnDialogResultListener(){
|
||||
@Override
|
||||
public void onNo() {
|
||||
}
|
||||
|
||||
try {
|
||||
// 步骤1:调用MainService删除任务(服务是唯一数据源,确保数据一致性)
|
||||
mMainService.deleteTask(task.getTaskId()); // 需在MainService中实现deleteTask()方法(删除服务内全量任务列表中的对应项)
|
||||
LogUtils.d(TAG, "调用MainService删除任务:ID=" + task.getTaskId() + "(位置ID=" + mBindPositionId + ")");
|
||||
@Override
|
||||
public void onYes() {
|
||||
LogUtils.d(TAG, "删除按钮点击 position=" + position + " taskId=" + task.getTaskId());
|
||||
if (mMainService == null) {
|
||||
showToast("删除失败:服务未就绪");
|
||||
LogUtils.e(TAG, "删除失败:MainService为空");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
mMainService.deleteTask(task.getTaskId());
|
||||
LogUtils.d(TAG, "服务删除任务成功 taskId=" + task.getTaskId());
|
||||
notifyItemRemoved(position);
|
||||
notifyItemRangeChanged(position, mAdapterData.size());
|
||||
|
||||
// 步骤2:从Adapter数据源移除任务(避免等待同步,立即反馈UI)
|
||||
//mAdapterData.remove(position);
|
||||
// 步骤3:刷新Adapter(局部刷新+范围通知,避免列表错乱)
|
||||
notifyItemRemoved(position);
|
||||
notifyItemRangeChanged(position, mAdapterData.size());
|
||||
|
||||
LogUtils.d(TAG, "Adapter已移除任务,刷新列表(位置索引=" + position + ")");
|
||||
if (mOnTaskUpdatedListener != null && mBindPositionId != null) {
|
||||
mOnTaskUpdatedListener.onTaskUpdated(mBindPositionId, new ArrayList<>(mAdapterData));
|
||||
}
|
||||
showToast("任务已删除");
|
||||
|
||||
// 步骤4:通知外部(如Activity)任务已更新
|
||||
if (mOnTaskUpdatedListener != null && mBindPositionId != null) {
|
||||
mOnTaskUpdatedListener.onTaskUpdated(mBindPositionId, new ArrayList<PositionTaskModel>(mAdapterData));
|
||||
}
|
||||
showToast("任务已删除(已同步至服务)");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "删除异常: " + e.getMessage(), e);
|
||||
showToast("删除失败,请重试");
|
||||
syncTasksFromMainService();
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, "删除任务失败(服务调用/Adapter刷新异常):" + e.getMessage());
|
||||
showToast("删除失败,请重试");
|
||||
// 异常时重新同步数据(确保Adapter与服务一致)
|
||||
syncTasksFromMainService();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 4.5 编辑按钮逻辑(核心:修改后同步MainService→刷新Adapter)
|
||||
holder.btnEditTask.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "编辑按钮点击 position=" + position + " taskId=" + task.getTaskId());
|
||||
if (mMainService == null) {
|
||||
showToast("编辑失败:服务未就绪");
|
||||
LogUtils.e(TAG, "编辑任务失败:MainService实例为空");
|
||||
LogUtils.e(TAG, "编辑失败:MainService为空");
|
||||
return;
|
||||
}
|
||||
// 弹出编辑弹窗(修改后同步服务)
|
||||
showEditTaskDialog(task, position);
|
||||
}
|
||||
});
|
||||
|
||||
// 4.6 启用开关逻辑(核心:状态变更→同步MainService→刷新Adapter)
|
||||
holder.cbTaskEnable.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, final boolean isChecked) {
|
||||
LogUtils.d(TAG, "开关状态变更 taskId=" + task.getTaskId() + " isChecked=" + isChecked);
|
||||
if (mMainService == null) {
|
||||
showToast("状态修改失败:服务未就绪");
|
||||
LogUtils.e(TAG, "修改任务启用状态失败:MainService实例为空");
|
||||
// 回滚开关状态(避免UI与服务不一致)
|
||||
buttonView.setChecked(!isChecked);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 步骤1:更新任务状态(先改内存数据,确保UI反馈及时)
|
||||
task.setIsEnable(isChecked);
|
||||
LogUtils.d(TAG, "更新任务启用状态:ID=" + task.getTaskId() + ",新状态=" + (isChecked ? "启用" : "禁用"));
|
||||
|
||||
// 步骤2:调用MainService同步状态(服务是唯一数据源,持久化变更)
|
||||
mMainService.updateTaskStatus(task); // 需在MainService中实现updateTaskStatus()方法(更新服务内任务的isEnable字段)
|
||||
LogUtils.d(TAG, "调用MainService同步任务状态(已" + (isChecked ? "启用" : "禁用") + ")");
|
||||
|
||||
// 步骤3:延迟刷新Adapter(避免列表滚动/布局计算时异常)
|
||||
mMainService.updateTaskStatus(task);
|
||||
mRvTasks.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
notifyItemChanged(position);
|
||||
// 通知外部任务已更新
|
||||
if (mOnTaskUpdatedListener != null && mBindPositionId != null) {
|
||||
mOnTaskUpdatedListener.onTaskUpdated(mBindPositionId, new ArrayList<PositionTaskModel>(mAdapterData));
|
||||
mOnTaskUpdatedListener.onTaskUpdated(mBindPositionId, new ArrayList<>(mAdapterData));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, "修改任务启用状态失败:" + e.getMessage());
|
||||
showToast("状态修改失败,请重试");
|
||||
// 回滚状态(UI与服务保持一致)
|
||||
LogUtils.e(TAG, "开关变更异常: " + e.getMessage(), e);
|
||||
buttonView.setChecked(!isChecked);
|
||||
task.setIsEnable(!isChecked);
|
||||
// 重新同步数据(修复可能的不一致)
|
||||
syncTasksFromMainService();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String genSelectedTimeText(long timeMillis) {
|
||||
// 2. 格式化时间字符串(Java 7 用 SimpleDateFormat,需处理 ParseException)
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault());
|
||||
String formattedDateTime = sdf.format(new Date(timeMillis)); // Date 需导入 java.util.Date
|
||||
|
||||
return formattedDateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑任务弹窗(核心:保存修改→同步MainService→刷新Adapter)
|
||||
*/
|
||||
private void showEditTaskDialog(final PositionTaskModel task, final int position) {
|
||||
LogUtils.d(TAG, "showEditTaskDialog position=" + position + " taskId=" + task.getTaskId());
|
||||
final Context context = getContext();
|
||||
if (context == null) {
|
||||
LogUtils.w(TAG, "编辑弹窗无法显示:上下文为空");
|
||||
LogUtils.w(TAG, "showEditTaskDialog 上下文为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载弹窗布局
|
||||
View dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_edit_task, null);
|
||||
final EditText etEditDesc = dialogView.findViewById(R.id.et_edit_task_desc);
|
||||
final RadioGroup rgDistanceCondition = dialogView.findViewById(R.id.rg_distance_condition);
|
||||
final EditText etEditDistance = dialogView.findViewById(R.id.et_edit_distance);
|
||||
Button btnCancel = dialogView.findViewById(R.id.btn_dialog_cancel);
|
||||
Button btnCancel = dialogView.findViewById(R.id.btn_dialog_cancel);
|
||||
Button btnSave = dialogView.findViewById(R.id.btn_dialog_save);
|
||||
final Button btnSelectTime = dialogView.findViewById(R.id.btn_select_time);
|
||||
final TextView tvSelectedTime = dialogView.findViewById(R.id.tv_selected_time);
|
||||
|
||||
tvSelectedTime.setText(genSelectedTimeText(task.getStartTime()));
|
||||
|
||||
// 绑定外层对话框内的控件
|
||||
final Button btnSelectTime = (Button) dialogView.findViewById(R.id.btn_select_time);
|
||||
final TextView tv_SelectedTime = (TextView) dialogView.findViewById(R.id.tv_selected_time);
|
||||
Calendar initCal = Calendar.getInstance();
|
||||
initCal.setTimeInMillis(task.getStartTime());
|
||||
int y = initCal.get(Calendar.YEAR);
|
||||
int M = initCal.get(Calendar.MONTH) + 1;
|
||||
int d = initCal.get(Calendar.DAY_OF_MONTH);
|
||||
int h = initCal.get(Calendar.HOUR_OF_DAY);
|
||||
int m = initCal.get(Calendar.MINUTE);
|
||||
|
||||
tv_SelectedTime.setText(genSelectedTimeText(task.getStartTime()));
|
||||
|
||||
// 核心:从 long 时间戳解析年月日时分,用于初始化弹窗
|
||||
// --------------------------
|
||||
Calendar initCalendar = Calendar.getInstance();
|
||||
initCalendar.setTimeInMillis(task.getStartTime()); // 将 long 时间戳传入 Calendar
|
||||
|
||||
// 从 Calendar 中解析出年月日时分(对应弹窗需要的参数格式)
|
||||
int initYear = initCalendar.get(Calendar.YEAR);
|
||||
int initMonth = initCalendar.get(Calendar.MONTH) + 1; // 关键:Calendar 月份0-11,转成1-12
|
||||
int initDay = initCalendar.get(Calendar.DAY_OF_MONTH);
|
||||
int initHour = initCalendar.get(Calendar.HOUR_OF_DAY); // 24小时制
|
||||
int initMinute = initCalendar.get(Calendar.MINUTE);
|
||||
|
||||
// 初始化弹窗,用解析出的年月日时分设置默认选中时间
|
||||
final DateTimePickerPopup dateTimePopup = new DateTimePickerPopup.Builder(context)
|
||||
final DateTimePickerPopup dateTimePopup = new DateTimePickerPopup.Builder(context)
|
||||
.setDateTimeRange(2020, 2030, 1, 12, 1, 31, 0, 23, 0, 59)
|
||||
// 用 long 时间戳解析出的参数设置初始时间
|
||||
.setDefaultDateTime(initYear, initMonth, initDay, initHour, initMinute)
|
||||
.setDefaultDateTime(y, M, d, h, m)
|
||||
.setOnDateTimeSelectedListener(new DateTimePickerPopup.OnDateTimeSelectedListener() {
|
||||
@Override
|
||||
public void onDateTimeSelected(int year, int month, int day, int hour, int minute) {
|
||||
// 处理选择的日期时间
|
||||
// 1. 创建 Calendar 实例(用于组装日期时间)
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
// 2. 设置 Calendar 的年、月、日、时、分(注意:Calendar 月份从 0 开始,需减 1)
|
||||
calendar.set(Calendar.YEAR, year);
|
||||
calendar.set(Calendar.MONTH, month - 1); // 关键:传入的 month 是 1-12,需转为 0-11
|
||||
calendar.set(Calendar.DAY_OF_MONTH, day);
|
||||
calendar.set(Calendar.HOUR_OF_DAY, hour); // 24小时制,对应参数中的 hour(0-23)
|
||||
calendar.set(Calendar.MINUTE, minute);
|
||||
calendar.set(Calendar.SECOND, 0); // 秒默认设为 0,避免随机值
|
||||
calendar.set(Calendar.MILLISECOND, 0); // 毫秒默认设为 0,确保时间戳精确
|
||||
|
||||
// 3. 转为 long 类型时间戳(单位:毫秒,从 1970-01-01 00:00:00 开始计算)
|
||||
long timeMillis = calendar.getTimeInMillis();
|
||||
|
||||
// 4. 后续使用(示例:打印时间戳或传递给其他逻辑)
|
||||
tv_SelectedTime.setText(genSelectedTimeText(timeMillis));
|
||||
task.setStartTime(timeMillis);
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.set(year, month - 1, day, hour, minute, 0);
|
||||
cal.set(Calendar.MILLISECOND, 0);
|
||||
long time = cal.getTimeInMillis();
|
||||
tvSelectedTime.setText(genSelectedTimeText(time));
|
||||
task.setStartTime(time);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel() {
|
||||
// 处理取消操作
|
||||
}
|
||||
public void onCancel() {}
|
||||
})
|
||||
.build();
|
||||
|
||||
// 3. “选择时间”按钮点击事件(弹出 TimePickerPopup)
|
||||
btnSelectTime.setOnClickListener(new View.OnClickListener() {
|
||||
btnSelectTime.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final View v) {
|
||||
public void onClick(View v) {
|
||||
dateTimePopup.showAsDropDown(btnSelectTime);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 初始化弹窗数据(填充当前任务信息)
|
||||
etEditDesc.setText(task.getTaskDescription() == null ? "" : task.getTaskDescription());
|
||||
etEditDesc.setSelection(etEditDesc.getText().length()); // 光标定位到末尾
|
||||
// 初始化距离条件(大于/小于)
|
||||
etEditDesc.setSelection(etEditDesc.getText().length());
|
||||
|
||||
if (task.isGreaterThan()) {
|
||||
rgDistanceCondition.check(R.id.rb_greater_than);
|
||||
} else {
|
||||
rgDistanceCondition.check(R.id.rb_less_than);
|
||||
}
|
||||
etEditDistance.setText(String.valueOf(task.getDiscussDistance())); // 填充当前距离
|
||||
etEditDistance.setText(String.valueOf(task.getDiscussDistance()));
|
||||
|
||||
// 创建并显示弹窗
|
||||
final android.app.AlertDialog dialog = new android.app.AlertDialog.Builder(context)
|
||||
.setView(dialogView)
|
||||
.setCancelable(false) // 不允许点击外部关闭(避免未保存退出)
|
||||
.setCancelable(false)
|
||||
.create();
|
||||
dialog.show();
|
||||
|
||||
final OnDateSetListener listener = new OnDateSetListener() {
|
||||
@Override
|
||||
public void onDateSet(TimePickerDialog timePickerDialog, long p) {}
|
||||
};
|
||||
|
||||
|
||||
final OnDateSetListener onSateSetListener = new OnDateSetListener(){
|
||||
@Override
|
||||
public void onDateSet(TimePickerDialog timePickerDialog, long p) {
|
||||
}
|
||||
};
|
||||
|
||||
// 取消按钮:关闭弹窗(不做操作)
|
||||
btnCancel.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
@@ -581,24 +515,22 @@ public class PositionTaskListView extends LinearLayout {
|
||||
}
|
||||
});
|
||||
|
||||
// 保存按钮:校验输入→更新任务→同步服务→刷新UI
|
||||
btnSave.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// 1. 输入校验(避免无效数据)
|
||||
String newDesc = etEditDesc.getText().toString().trim();
|
||||
String distanceStr = etEditDistance.getText().toString().trim();
|
||||
String distStr = etEditDistance.getText().toString().trim();
|
||||
|
||||
if (distanceStr.isEmpty()) {
|
||||
showToast("请输入有效距离(1米及以上)");
|
||||
if (distStr.isEmpty()) {
|
||||
showToast("请输入有效距离");
|
||||
etEditDistance.requestFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
int newDistance;
|
||||
int newDist;
|
||||
try {
|
||||
newDistance = Integer.parseInt(distanceStr);
|
||||
if (newDistance < 1) {
|
||||
newDist = Integer.parseInt(distStr);
|
||||
if (newDist < 1) {
|
||||
showToast("距离不能小于1米");
|
||||
etEditDistance.requestFocus();
|
||||
return;
|
||||
@@ -609,144 +541,84 @@ public class PositionTaskListView extends LinearLayout {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 收集新数据(更新任务对象)
|
||||
task.setTaskDescription(newDesc); // 新描述
|
||||
task.setDiscussDistance(newDistance); // 新距离
|
||||
task.setTaskDescription(newDesc);
|
||||
task.setDiscussDistance(newDist);
|
||||
boolean isGreater = rgDistanceCondition.getCheckedRadioButtonId() == R.id.rb_greater_than;
|
||||
task.setIsGreaterThan(isGreater); // 新距离条件(大于/小于)
|
||||
task.setPositionId(mBindPositionId); // 确保位置ID不变(防止错位)
|
||||
task.setIsGreaterThan(isGreater);
|
||||
task.setPositionId(mBindPositionId);
|
||||
|
||||
try {
|
||||
// 3. 调用MainService同步修改(服务是唯一数据源,持久化变更)
|
||||
mMainService.updateTask(task); // 需在MainService中实现updateTask()方法(更新服务内任务的字段)
|
||||
LogUtils.d(TAG, "调用MainService更新任务:ID=" + task.getTaskId() + "(描述=" + newDesc + ",距离=" + newDistance + "米)");
|
||||
|
||||
// 4. 更新Adapter数据源(立即反馈UI)
|
||||
mMainService.updateTask(task);
|
||||
LogUtils.d(TAG, "保存修改成功 taskId=" + task.getTaskId());
|
||||
mAdapterData.set(position, task);
|
||||
// 5. 延迟刷新Adapter(避免弹窗未关闭时布局异常)
|
||||
mRvTasks.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
notifyItemChanged(position);
|
||||
// 通知外部任务已更新
|
||||
if (mOnTaskUpdatedListener != null && mBindPositionId != null) {
|
||||
mOnTaskUpdatedListener.onTaskUpdated(mBindPositionId, new ArrayList<PositionTaskModel>(mAdapterData));
|
||||
mOnTaskUpdatedListener.onTaskUpdated(mBindPositionId, new ArrayList<>(mAdapterData));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dialog.dismiss();
|
||||
showToast("任务已更新(已同步至服务)");
|
||||
|
||||
showToast("任务已更新");
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, "保存任务修改失败:" + e.getMessage());
|
||||
LogUtils.e(TAG, "保存异常: " + e.getMessage(), e);
|
||||
showToast("保存失败,请重试");
|
||||
// 重新同步数据(修复可能的不一致)
|
||||
syncTasksFromMainService();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------- ViewHolder 定义(完全适配布局,无修改) ----------------------
|
||||
// 基础抽象ViewHolder(统一父类,适配多视图类型)
|
||||
// ---------------------- ViewHolder ----------------------
|
||||
public abstract class TaskViewHolder extends RecyclerView.ViewHolder {
|
||||
public TaskViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
}
|
||||
|
||||
// 空提示ViewHolder(对应 item_task_empty.xml)
|
||||
public class EmptyViewHolder extends TaskViewHolder {
|
||||
public EmptyViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
}
|
||||
|
||||
// 简单模式ViewHolder(对应 item_position_task_simple.xml,带isBingo红点)
|
||||
public class SimpleTaskViewHolder extends TaskViewHolder {
|
||||
TextView tvSimpleTaskDesc; // 任务描述
|
||||
TextView tvSimpleDistanceCond;// 距离条件(大于/小于+距离)
|
||||
TextView tvStartTime;
|
||||
TextView tvSimpleIsEnable; // 启用状态(已启用/已禁用)
|
||||
View vBingoDot; // isBingo红点(任务触发时显示)
|
||||
TextView tvSimpleTaskDesc;
|
||||
TextView tvSimpleDistanceCond;
|
||||
TextView tvStartTime;
|
||||
TextView tvSimpleIsEnable;
|
||||
View vBingoDot;
|
||||
|
||||
public SimpleTaskViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
// 绑定简单模式布局控件(与XML控件ID严格对应)
|
||||
tvSimpleTaskDesc = itemView.findViewById(R.id.tv_simple_task_desc);
|
||||
tvSimpleDistanceCond = itemView.findViewById(R.id.tv_simple_distance_cond);
|
||||
tvStartTime = itemView.findViewById(R.id.tv_starttime);
|
||||
tvStartTime = itemView.findViewById(R.id.tv_starttime);
|
||||
tvSimpleIsEnable = itemView.findViewById(R.id.tv_simple_is_enable);
|
||||
vBingoDot = itemView.findViewById(R.id.v_bingo_dot);
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑模式ViewHolder(对应 item_task_content.xml,带编辑/删除/开关)
|
||||
public class TaskContentViewHolder extends TaskViewHolder {
|
||||
TextView tvTaskDesc; // 任务描述
|
||||
TextView tvTaskDistance; // 距离条件
|
||||
TextView tvStartTime;
|
||||
CompoundButton cbTaskEnable; // 启用开关
|
||||
Button btnEditTask; // 编辑按钮
|
||||
Button btnDeleteTask; // 删除按钮
|
||||
TextView tvTaskDesc;
|
||||
TextView tvTaskDistance;
|
||||
TextView tvStartTime;
|
||||
CompoundButton cbTaskEnable;
|
||||
Button btnEditTask;
|
||||
Button btnDeleteTask;
|
||||
|
||||
public TaskContentViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
// 绑定编辑模式布局控件(与XML控件ID严格对应)
|
||||
tvTaskDesc = itemView.findViewById(R.id.tv_task_desc);
|
||||
tvTaskDistance = itemView.findViewById(R.id.tv_task_distance);
|
||||
tvStartTime = itemView.findViewById(R.id.tv_starttime);
|
||||
tvStartTime = itemView.findViewById(R.id.tv_starttime);
|
||||
cbTaskEnable = itemView.findViewById(R.id.cb_task_enable);
|
||||
btnEditTask = itemView.findViewById(R.id.btn_edit_task);
|
||||
btnDeleteTask = itemView.findViewById(R.id.btn_delete_task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------- 新增:外部调用“新增任务”方法(适配MainService) ----------------------
|
||||
/**
|
||||
* 新增任务(对外提供,如Activity调用添加任务)
|
||||
* @param newTask 待新增的任务(需关联当前位置ID)
|
||||
*/
|
||||
public void addNewTask(PositionTaskModel newTask) {
|
||||
if (mMainService == null) {
|
||||
showToast("新增任务失败:服务未就绪");
|
||||
LogUtils.e(TAG, "新增任务失败:MainService实例为空");
|
||||
return;
|
||||
}
|
||||
if (newTask == null) {
|
||||
showToast("新增任务失败:任务数据为空");
|
||||
LogUtils.e(TAG, "新增任务失败:待新增任务对象为空");
|
||||
return;
|
||||
}
|
||||
if (mBindPositionId == null || mBindPositionId.trim().isEmpty()) {
|
||||
showToast("新增任务失败:未关联位置");
|
||||
LogUtils.e(TAG, "新增任务失败:未绑定位置ID(需先调用init()方法)");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 关联任务到当前位置(确保任务属于当前位置)
|
||||
newTask.setPositionId(mBindPositionId);
|
||||
// 2. 调用MainService新增任务(服务是唯一数据源,持久化数据)
|
||||
mMainService.addTask(newTask); // 需在MainService中实现addTask()方法(添加到服务内全量任务列表)
|
||||
LogUtils.d(TAG, "调用MainService新增任务:ID=" + newTask.getTaskId() + "(位置ID=" + mBindPositionId + ")");
|
||||
|
||||
// 3. 重新同步数据(从服务拉取最新列表,避免本地计算错误)
|
||||
syncTasksFromMainService();
|
||||
// 4. 通知外部任务已更新
|
||||
if (mOnTaskUpdatedListener != null) {
|
||||
mOnTaskUpdatedListener.onTaskUpdated(mBindPositionId, getCurrentPosTasks());
|
||||
}
|
||||
showToast("新增任务成功(已同步至服务)");
|
||||
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, "新增任务失败:" + e.getMessage());
|
||||
showToast("新增失败,请重试");
|
||||
// 重新同步数据(修复可能的不一致)
|
||||
syncTasksFromMainService();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
9
positions/src/main/res/drawable/shape_log_border.xml
Normal file
9
positions/src/main/res/drawable/shape_log_border.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#00000000"/>
|
||||
<stroke
|
||||
android:width="5px"
|
||||
android:color="#888888"/>
|
||||
<corners android:radius="6dp"/>
|
||||
</shape>
|
||||
|
||||
@@ -24,14 +24,31 @@
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginTop="30dp"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal">
|
||||
android:gravity="left">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="实时位置信息"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_timenow"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="实时位置信息"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold"/>
|
||||
android:text="现在时间:等待更新..."
|
||||
android:textSize="18sp"
|
||||
android:layout_marginTop="10dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_longitude"
|
||||
@@ -39,7 +56,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:text="当前经度:等待更新..."
|
||||
android:textSize="18sp"
|
||||
android:layout_marginTop="15dp"/>
|
||||
android:layout_marginTop="10dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_latitude"
|
||||
@@ -49,6 +66,7 @@
|
||||
android:textSize="18sp"
|
||||
android:layout_marginTop="10dp"/>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
android:text="GPS服务开关"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="120dp"
|
||||
android:layout_marginTop="100dp"
|
||||
android:layout_marginLeft="50dp"
|
||||
android:layout_marginRight="50dp"
|
||||
android:background="@drawable/shape_2px_border"
|
||||
@@ -47,7 +47,7 @@
|
||||
<Button
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginLeft="50dp"
|
||||
android:layout_marginRight="50dp"
|
||||
android:onClick="onPositions"
|
||||
@@ -57,6 +57,27 @@
|
||||
android:textColor="@drawable/btn_text_selector"
|
||||
android:padding="12dp"/>
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scroll_idle_log"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginLeft="50dp"
|
||||
android:layout_marginRight="50dp"
|
||||
android:layout_weight="1.0"
|
||||
android:background="@drawable/shape_log_border"
|
||||
android:padding="8dp"
|
||||
android:layout_marginBottom="80dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_idle_log"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textIsSelectable="true"
|
||||
android:textColor="#FF000000"/>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<cc.winboll.studio.libaes.views.ADsBannerView
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="输入任务描述"
|
||||
android:maxLines="1"
|
||||
android:textSize="14sp"/>
|
||||
android:textSize="14sp"
|
||||
android:lines="3"
|
||||
android:inputType="textMultiLine"/>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
@@ -33,7 +35,7 @@
|
||||
android:id="@+id/rb_greater_than"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="大于"
|
||||
android:text="大于(◎)"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="14sp"/>
|
||||
|
||||
@@ -41,7 +43,7 @@
|
||||
android:id="@+id/rb_less_than"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="小于"
|
||||
android:text="小于(•)"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="14sp"/>
|
||||
|
||||
@@ -77,7 +79,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:text="开始时间"
|
||||
android:id="@+id/btn_select_time"/>
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_selected_time"
|
||||
android:layout_width="0dp"
|
||||
@@ -85,7 +87,6 @@
|
||||
android:text="Text"
|
||||
android:layout_weight="1.0"/>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
|
||||
<item
|
||||
android:id="@+id/item_settings"
|
||||
android:title="Settings"/>
|
||||
@@ -9,4 +9,6 @@
|
||||
<item
|
||||
android:id="@+id/item_about"
|
||||
android:title="@string/item_about"/>
|
||||
|
||||
</menu>
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
|
||||
<item
|
||||
android:id="@+id/item_termux_env_test"
|
||||
android:title="TermuxEnvTestActivity"/>
|
||||
|
||||
android:id="@+id/item_idle_switch"
|
||||
android:title="空转状态切换"/>
|
||||
|
||||
</menu>
|
||||
@@ -27,7 +27,7 @@
|
||||
<color name="background_light">#F5F5F5</color> <!-- 浅灰背景(备用,提升页面质感) -->
|
||||
|
||||
<!-- 扩展颜色(避免后续新增功能时再次缺失) -->
|
||||
<color name="green">#4CAF50</color> <!-- 绿色(备用:如“启用”状态标识) -->
|
||||
<color name="green">#2E8B57</color> <!-- 绿色(备用:如“启用”状态标识) -->
|
||||
<color name="yellow">#FFC107</color> <!-- 黄色(备用:如“提醒”标识) -->
|
||||
<color name="color_text_primary">#333333</color>
|
||||
<color name="color_gray">#999999</color>
|
||||
|
||||
@@ -13,6 +13,9 @@ WinBoLL 网站浏览器。
|
||||
阶段版编译命令 :bash .winboll/bashPublishAPKAddTag.sh winboll
|
||||
|
||||
#### 使用说明
|
||||
3. Termux应用配置:
|
||||
- 已安装Termux(包名 com.termux );
|
||||
- 执行 echo "allow-external-apps = true" > ~/.termux/termux.properties
|
||||
|
||||
#### 参与贡献
|
||||
|
||||
|
||||
@@ -73,12 +73,13 @@ dependencies {
|
||||
implementation 'com.alibaba:fastjson:1.2.76'
|
||||
|
||||
// AndroidX 类库
|
||||
api 'androidx.appcompat:appcompat:1.1.0'
|
||||
/*api 'androidx.appcompat:appcompat:1.1.0'
|
||||
//api 'com.google.android.material:material:1.4.0'
|
||||
//api 'androidx.viewpager:viewpager:1.0.0'
|
||||
//api 'androidx.vectordrawable:vectordrawable:1.1.0'
|
||||
//api 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
|
||||
//api 'androidx.fragment:fragment:1.1.0'
|
||||
//api 'androidx.fragment:fragment:1.1.0'*/
|
||||
|
||||
|
||||
// 米盟
|
||||
api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk
|
||||
@@ -88,10 +89,27 @@ dependencies {
|
||||
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.7'
|
||||
api 'cc.winboll.studio:libappbase:15.15.4'
|
||||
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'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Mon Jan 19 03:57:58 GMT 2026
|
||||
stageCount=11
|
||||
#Thu Apr 30 14:58:25 CST 2026
|
||||
stageCount=27
|
||||
libraryProject=
|
||||
baseVersion=15.11
|
||||
publishVersion=15.11.10
|
||||
buildCount=12
|
||||
baseBetaVersion=15.11.11
|
||||
publishVersion=15.11.26
|
||||
buildCount=11
|
||||
baseBetaVersion=15.11.27
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="cc.winboll.studio.winboll"
|
||||
android:sharedUserId="com.termux">
|
||||
|
||||
@@ -13,6 +14,15 @@
|
||||
<!-- 对正在运行的应用重新排序 -->
|
||||
<uses-permission android:name="android.permission.REORDER_TASKS"/>
|
||||
|
||||
<!-- 计算应用存储空间 -->
|
||||
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission"/>
|
||||
|
||||
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
@@ -283,8 +293,39 @@
|
||||
|
||||
<activity android:name="cc.winboll.studio.winboll.activities.WXPayActivity"/>
|
||||
|
||||
<activity android:name="cc.winboll.studio.winboll.activities.PatternLockActivity"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="cc.winboll.studio.winboll.activities.PatternLockActivity.ACTION_OPEN_PATTERN"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<activity android:name="cc.winboll.studio.winboll.unittest.TermuxEnvTestActivity"/>
|
||||
|
||||
<activity
|
||||
android:name=".termux.NfcTermuxBridgeActivity"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="cc.winboll.studio.winboll.termux.NfcTermuxBridgeActivity.ACTION_BUILD"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<activity android:name="cc.winboll.studio.winboll.applications.MyTermuxActivity"
|
||||
android:label="@string/my_termux_activity"
|
||||
android:exported="true"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
@@ -15,6 +15,7 @@ import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.winboll.R;
|
||||
import cc.winboll.studio.winboll.activities.AboutActivity;
|
||||
import cc.winboll.studio.winboll.activities.SettingsActivity;
|
||||
import cc.winboll.studio.winboll.applications.MyTermuxActivity;
|
||||
import cc.winboll.studio.winboll.fragments.BrowserFragment;
|
||||
import cc.winboll.studio.winboll.unittest.TermuxEnvTestActivity;
|
||||
import java.util.ArrayList;
|
||||
@@ -23,7 +24,7 @@ public class MainActivity extends DrawerFragmentActivity {
|
||||
|
||||
|
||||
public static final String TAG = "MainActivity";
|
||||
|
||||
|
||||
BrowserFragment mBrowserFragment;
|
||||
|
||||
// ------------------- 新增:Handler 消息定义(接收URL历史更新消息) -------------------
|
||||
@@ -45,12 +46,6 @@ public class MainActivity extends DrawerFragmentActivity {
|
||||
showFragment(mBrowserFragment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
|
||||
public static void sendMessage(Message msg) {
|
||||
_mMainHandler.sendMessage(msg);
|
||||
}
|
||||
@@ -128,7 +123,9 @@ public class MainActivity extends DrawerFragmentActivity {
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.toolbar_main, menu);
|
||||
getMenuInflater().inflate(R.menu.toolbar_termux, menu);
|
||||
if (App.isDebugging()) {
|
||||
getMenuInflater().inflate(R.menu.toolbar_test, menu);
|
||||
}
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@@ -157,8 +154,12 @@ public class MainActivity extends DrawerFragmentActivity {
|
||||
WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), SettingsActivity.class);
|
||||
} else if (nItemId == R.id.item_about) {
|
||||
Intent intent = new Intent(getApplicationContext(), AboutActivity.class);
|
||||
|
||||
|
||||
WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), intent, AboutActivity.class);
|
||||
} else if (nItemId == R.id.item_mytermux) {
|
||||
Intent intent = new Intent(getApplicationContext(), MyTermuxActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
} else if (nItemId == R.id.item_termux_env_test) {
|
||||
Intent intent = new Intent(getApplicationContext(), TermuxEnvTestActivity.class);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import cc.winboll.studio.libappbase.models.APPInfo;
|
||||
import cc.winboll.studio.libappbase.views.AboutView;
|
||||
import cc.winboll.studio.winboll.MainActivity;
|
||||
import cc.winboll.studio.winboll.R;
|
||||
import android.app.Activity;
|
||||
|
||||
/**
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
@@ -18,6 +19,12 @@ import cc.winboll.studio.winboll.R;
|
||||
*/
|
||||
public class AboutActivity extends BaseWinBoLLActivity {
|
||||
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public static final String TAG = "AboutActivity";
|
||||
|
||||
private Toolbar mToolbar;
|
||||
@@ -54,7 +61,6 @@ public class AboutActivity extends BaseWinBoLLActivity {
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "导航栏 点击返回按钮");
|
||||
getActivity().finish();
|
||||
WinBoLLActivityManager.getInstance().startWinBoLLActivity(getActivity(), MainActivity.class);
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "initToolbar() 配置完成");
|
||||
|
||||
@@ -51,9 +51,6 @@ public abstract class BaseWinBoLLActivity extends AppCompatActivity implements I
|
||||
@Override
|
||||
public abstract String getTag();
|
||||
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
public abstract Activity getActivity();
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,16 @@ import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import cc.winboll.studio.winboll.R;
|
||||
import android.app.Activity;
|
||||
|
||||
public class New2Activity extends BaseWinBoLLActivity {
|
||||
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public static final String TAG = "New2Activity";
|
||||
|
||||
Toolbar mToolbar;
|
||||
|
||||
@@ -10,9 +10,16 @@ import android.view.View;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||
import cc.winboll.studio.winboll.R;
|
||||
import android.app.Activity;
|
||||
|
||||
public class NewActivity extends BaseWinBoLLActivity {
|
||||
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public static final String TAG = "NewActivity";
|
||||
|
||||
Toolbar mToolbar;
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
package cc.winboll.studio.winboll.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.MotionEvent;
|
||||
import android.widget.FrameLayout;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import cc.winboll.studio.libaes.utils.AESThemeUtil;
|
||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||
import cc.winboll.studio.winboll.R;
|
||||
|
||||
public class PatternLockActivity extends BaseWinBoLLActivity {
|
||||
|
||||
public static final String TAG = "PatternLockActivity";
|
||||
|
||||
private static final int DOT_RADIUS = 8;
|
||||
private static final int PATTERN_ERROR_DURATION = 1500;
|
||||
|
||||
private static final String PREFS_NAME = "pattern_lock_prefs";
|
||||
static final String KEY_LOCK_PATTERN = "lock_pattern";
|
||||
static final String KEY_ERROR_STATE = "error_state";
|
||||
private static final String KEY_ERROR_REPEAT_PATTERN = "error_repeat_pattern";
|
||||
|
||||
private boolean mIsInErrorState;
|
||||
private boolean mNeedRestart;
|
||||
private Handler mHandler;
|
||||
|
||||
private FrameLayout mContainer;
|
||||
private PatternView mPatternView;
|
||||
|
||||
public PatternLockActivity() {
|
||||
mHandler = new Handler(Looper.getMainLooper());
|
||||
}
|
||||
|
||||
PatternLockActivity(Context context) {
|
||||
mHandler = new Handler(Looper.getMainLooper());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
AESThemeUtil.applyAppTheme(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_pattern_lock);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
getSupportActionBar().setSubtitle(TAG);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
toolbar.setNavigationOnClickListener(v -> finish());
|
||||
|
||||
mContainer = findViewById(R.id.container);
|
||||
mPatternView = new PatternView(this);
|
||||
mContainer.addView(mPatternView);
|
||||
mPatternView.invalidate();
|
||||
|
||||
mNeedRestart = false;
|
||||
boolean isEnoughPoints = savedInstanceIsEnoughPoints();
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
mIsInErrorState = savedInstanceState.getBoolean(KEY_ERROR_STATE, false);
|
||||
mNeedRestart = savedInstanceState.getBoolean(KEY_ERROR_REPEAT_PATTERN, false);
|
||||
}
|
||||
|
||||
if (mIsInErrorState) {
|
||||
mPatternView.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
boolean savedInstanceIsEnoughPoints() {
|
||||
int count = 0;
|
||||
if (mPatternView != null) {
|
||||
for (int i = 0; i < 9; i++) {
|
||||
if (mPatternView.mDotState[i] == 1) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count >= 4 || count == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
if (mIsInErrorState) {
|
||||
outState.putBoolean(KEY_ERROR_STATE, mIsInErrorState);
|
||||
}
|
||||
if (mNeedRestart) {
|
||||
outState.putBoolean(KEY_ERROR_REPEAT_PATTERN, mNeedRestart);
|
||||
}
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
private void showErrorState() {
|
||||
mIsInErrorState = true;
|
||||
invalidatePattern();
|
||||
mHandler.postDelayed(() -> {
|
||||
mIsInErrorState = false;
|
||||
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
|
||||
prefs.edit().putBoolean(KEY_ERROR_STATE, false).apply();
|
||||
invalidatePattern();
|
||||
if (mPatternView != null) mPatternView.invalidate();
|
||||
}, PATTERN_ERROR_DURATION);
|
||||
}
|
||||
|
||||
private void clearErrorState() {
|
||||
mIsInErrorState = false;
|
||||
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
|
||||
prefs.edit().putBoolean(KEY_ERROR_STATE, false).apply();
|
||||
invalidatePattern();
|
||||
if (mPatternView != null) mPatternView.invalidate();
|
||||
}
|
||||
|
||||
private void showErrorToast() {
|
||||
android.widget.Toast.makeText(this, "图案点数不足,请重新绘制",
|
||||
android.widget.Toast.LENGTH_SHORT).show();
|
||||
mNeedRestart = true;
|
||||
}
|
||||
|
||||
private void showSuccessDialog() {
|
||||
android.app.AlertDialog alertDialog = new android.app.AlertDialog.Builder(this)
|
||||
.setTitle("设置成功")
|
||||
.setMessage("图案密码已设置成功")
|
||||
.setPositiveButton("确定", (dialog, which) -> finish())
|
||||
.setCancelable(false)
|
||||
.create();
|
||||
alertDialog.show();
|
||||
}
|
||||
|
||||
void finishWithRestart() {
|
||||
finish();
|
||||
}
|
||||
|
||||
private void invalidatePattern() {
|
||||
if (mPatternView != null) {
|
||||
mPatternView.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
class PatternView extends FrameLayout {
|
||||
int mPatternSize = 0;
|
||||
int MAX_DOT_COUNT = 9;
|
||||
int[] mDotX = new int[MAX_DOT_COUNT];
|
||||
int[] mDotY = new int[MAX_DOT_COUNT];
|
||||
int[] mDotState = new int[MAX_DOT_COUNT];
|
||||
Bitmap mDotBitmap;
|
||||
Paint mPaintConnector;
|
||||
Paint mPaintErrorBackground;
|
||||
int mDotCount = 0;
|
||||
|
||||
PatternView(Context context) {
|
||||
super(context);
|
||||
setBackgroundColor(Color.WHITE);
|
||||
for (int i = 0; i < MAX_DOT_COUNT; i++) {
|
||||
mDotX[i] = -1;
|
||||
mDotY[i] = -1;
|
||||
mDotState[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
if (w == 0 || h == 0) return;
|
||||
mPatternSize = w > h ? h : w;
|
||||
int grid = 3;
|
||||
int cell = mPatternSize / grid;
|
||||
|
||||
for (int i = 0; i < MAX_DOT_COUNT; i++) {
|
||||
mDotX[i] = (i % grid) * cell + cell / 2 - cell / 24;
|
||||
mDotY[i] = (i / grid) * cell + cell / 2 - cell / 24;
|
||||
mDotState[i] = 0;
|
||||
}
|
||||
|
||||
if (mDotBitmap == null) {
|
||||
mDotBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.dot_darkgreen_dark);
|
||||
}
|
||||
if (mPaintConnector == null) {
|
||||
mPaintConnector = new Paint(Paint.FILTER_BITMAP_FLAG);
|
||||
mPaintConnector.setColor(-0xFF006400);
|
||||
}
|
||||
if (mPaintErrorBackground == null) {
|
||||
mPaintErrorBackground = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
mPaintErrorBackground.setColor(Color.RED);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
if (mDotCount > 0) return false;
|
||||
|
||||
switch (event.getActionMasked()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
invalidate();
|
||||
return true;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
float x = event.getX();
|
||||
float y = event.getY();
|
||||
|
||||
for (int i = 0; i < MAX_DOT_COUNT; i++) {
|
||||
int dx = (int) Math.abs(x - mDotX[i]);
|
||||
int dy = (int) Math.abs(y - mDotY[i]);
|
||||
if (dx <= DOT_RADIUS && dy <= DOT_RADIUS && mDotState[i] == 0) {
|
||||
mDotState[i] = 1;
|
||||
mDotCount++;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < mDotCount - 1; i++) {
|
||||
int a = -1, b = -1;
|
||||
for (int k = 0; k < MAX_DOT_COUNT; k++) {
|
||||
if (mDotState[k] == 1) {
|
||||
if (a < 0) a = k;
|
||||
else b = k;
|
||||
}
|
||||
}
|
||||
if (a >= 0 && b >= 0) {
|
||||
a = Math.min(a, b);
|
||||
b = Math.max(a, b);
|
||||
}
|
||||
if (mDotState[a] == 1 && mDotState[b] == 1) {
|
||||
int dx = mDotX[b] - mDotX[a];
|
||||
int dy = mDotY[b] - mDotY[a];
|
||||
if ((Math.abs(dx) <= 1 && Math.abs(dy) <= 1) ||
|
||||
(Math.abs(dx) <= 2 && Math.abs(dy) <= 1)) {
|
||||
if (mDotState[b] == 1) {
|
||||
for (int k = a + 1; k < b; k++) {
|
||||
if (mDotState[k] == 0) {
|
||||
mDotState[k] = 1;
|
||||
}
|
||||
}
|
||||
mDotCount += (b - a - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
invalidate();
|
||||
return true;
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
if (mDotCount < 4) {
|
||||
showErrorState();
|
||||
showErrorToast();
|
||||
}
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas canvas) {
|
||||
if (mPatternSize == 0) return;
|
||||
|
||||
int activeCount = 0;
|
||||
for (int i = 0; i < MAX_DOT_COUNT; i++) {
|
||||
if (mDotState[i] == 1) activeCount++;
|
||||
}
|
||||
|
||||
if (activeCount == 0) {
|
||||
mPaintErrorBackground.setAlpha(50);
|
||||
} else {
|
||||
mPaintErrorBackground.setAlpha(mIsInErrorState ? 80 : 60);
|
||||
}
|
||||
|
||||
canvas.clipRect(0, 0, mPatternSize * 80 / 100, mPatternSize * 80 / 100);
|
||||
|
||||
canvas.drawRect(0, 0, mPatternSize, mPatternSize, mPaintErrorBackground);
|
||||
|
||||
if (mDotBitmap != null) {
|
||||
for (int i = 0; i < MAX_DOT_COUNT; i++) {
|
||||
if (mDotState[i] == 1) {
|
||||
canvas.drawBitmap(mDotBitmap, mDotX[i], mDotY[i], mPaintConnector);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import android.view.View;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import cc.winboll.studio.libaes.utils.AESThemeUtil;
|
||||
import cc.winboll.studio.winboll.R;
|
||||
import android.app.Activity;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
@@ -13,6 +14,12 @@ import cc.winboll.studio.winboll.R;
|
||||
*/
|
||||
public class SettingsActivity extends BaseWinBoLLActivity {
|
||||
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public static final String TAG = "SettingsActivity";
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package cc.winboll.studio.winboll.applications;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.winboll.R;
|
||||
import cc.winboll.studio.winboll.termux.TermuxCommandExecutor;
|
||||
|
||||
public class MyTermuxActivity extends AppCompatActivity {
|
||||
|
||||
public static final String TAG = "MyTermuxActivity";
|
||||
|
||||
private Toolbar mToolbar;
|
||||
private Button mTermuxButton;
|
||||
private Button mTermuxWorkSpacesButton;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_my_termux);
|
||||
|
||||
// 初始化工具栏
|
||||
initToolbar();
|
||||
// 初始化按钮
|
||||
initView();
|
||||
}
|
||||
|
||||
private void initToolbar() {
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
if (mToolbar != null) {
|
||||
setSupportActionBar(mToolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "点击返回按钮");
|
||||
finish();
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "工具栏初始化完成");
|
||||
}
|
||||
}
|
||||
|
||||
private void initView() {
|
||||
mTermuxButton = findViewById(R.id.btn_termux);
|
||||
if (mTermuxButton != null) {
|
||||
mTermuxButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "点击 Termux 按钮");
|
||||
TermuxCommandExecutor.openTermuxBash(MyTermuxActivity.this, "cd ~");
|
||||
//TermuxCommandExecutor.openTermuxBash(MyTermuxActivity.this, "cd ~/TermuxWorkSpaces", "./TermuxWorkSpaces");
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "Termux 按钮初始化完成");
|
||||
}
|
||||
|
||||
mTermuxWorkSpacesButton = findViewById(R.id.btn_termuxworkspaces);
|
||||
if (mTermuxWorkSpacesButton != null) {
|
||||
mTermuxWorkSpacesButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "点击 TermuxWorkSpaces 按钮");
|
||||
TermuxCommandExecutor.openTermuxBash(MyTermuxActivity.this, "cd ~/TermuxWorkSpaces", "./TermuxWorkSpaces");
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "TermuxWorkSpaces 按钮初始化完成");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isTermuxAvailable() {
|
||||
return TermuxCommandExecutor.isTermuxInstalled(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package cc.winboll.studio.winboll.models;
|
||||
|
||||
/**
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026/03/15 08:46
|
||||
*/
|
||||
public class NfcTermuxCmd {
|
||||
public String script; // 要执行的预制脚本名(如 auth.sh)
|
||||
public String[] args; // 脚本参数
|
||||
public String workDir; // 工作目录
|
||||
public boolean background; // 是否后台执行
|
||||
public String resultDir; // 结果输出目录(可为 null)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
package cc.winboll.studio.winboll.models;
|
||||
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026/04/30 10:47
|
||||
*/
|
||||
public class TermuxButtonModel extends BaseBean {
|
||||
|
||||
public static final String TAG = "TermuxButtonModel";
|
||||
|
||||
String buttonName;
|
||||
String exeCommand;
|
||||
String workDir;
|
||||
|
||||
// 已修改:isCommit 改为规范过去式命名 isCommitted
|
||||
boolean isCommitted;
|
||||
String commitTitle;
|
||||
String commitInfo;
|
||||
|
||||
public TermuxButtonModel() {
|
||||
this.buttonName = "";
|
||||
this.exeCommand = "";
|
||||
this.workDir = "";
|
||||
// 默认初始化
|
||||
this.isCommitted = false;
|
||||
this.commitTitle = "";
|
||||
this.commitInfo = "";
|
||||
}
|
||||
|
||||
public void setButtonName(String buttonName) {
|
||||
this.buttonName = buttonName;
|
||||
}
|
||||
|
||||
public String getButtonName() {
|
||||
return buttonName;
|
||||
}
|
||||
|
||||
public void setExeCommand(String exeCommand) {
|
||||
this.exeCommand = exeCommand;
|
||||
}
|
||||
|
||||
public String getExeCommand() {
|
||||
return exeCommand;
|
||||
}
|
||||
|
||||
public void setWorkDir(String workDir) {
|
||||
this.workDir = workDir;
|
||||
}
|
||||
|
||||
public String getWorkDir() {
|
||||
return workDir;
|
||||
}
|
||||
|
||||
// ========== 已修改 对应 isCommitted 完整 Get & Set ==========
|
||||
public boolean isCommitted() {
|
||||
return isCommitted;
|
||||
}
|
||||
|
||||
public void setCommitted(boolean committed) {
|
||||
isCommitted = committed;
|
||||
}
|
||||
|
||||
public String getCommitTitle() {
|
||||
return commitTitle;
|
||||
}
|
||||
|
||||
public void setCommitTitle(String commitTitle) {
|
||||
this.commitTitle = commitTitle;
|
||||
}
|
||||
|
||||
public String getCommitInfo() {
|
||||
return commitInfo;
|
||||
}
|
||||
|
||||
public void setCommitInfo(String commitInfo) {
|
||||
this.commitInfo = commitInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return TermuxButtonModel.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
jsonWriter.name("buttonName").value(getButtonName());
|
||||
jsonWriter.name("exeCommand").value(getExeCommand());
|
||||
jsonWriter.name("workDir").value(getWorkDir());
|
||||
|
||||
// JSON写入同步修改
|
||||
jsonWriter.name("isCommitted").value(isCommitted());
|
||||
jsonWriter.name("commitTitle").value(getCommitTitle());
|
||||
jsonWriter.name("commitInfo").value(getCommitInfo());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
||||
if (super.initObjectsFromJsonReader(jsonReader, name)) {
|
||||
return true;
|
||||
} else {
|
||||
if (name.equals("buttonName")) {
|
||||
setButtonName(jsonReader.nextString());
|
||||
} else if (name.equals("exeCommand")) {
|
||||
setExeCommand(jsonReader.nextString());
|
||||
} else if (name.equals("workDir")) {
|
||||
setWorkDir(jsonReader.nextString());
|
||||
}
|
||||
// JSON解析字段同步修改
|
||||
else if (name.equals("isCommitted")) {
|
||||
setCommitted(jsonReader.nextBoolean());
|
||||
} else if (name.equals("commitTitle")) {
|
||||
setCommitTitle(jsonReader.nextString());
|
||||
} else if (name.equals("commitInfo")) {
|
||||
setCommitInfo(jsonReader.nextString());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
if (!initObjectsFromJsonReader(jsonReader, name)) {
|
||||
jsonReader.skipValue();
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
* 源码说明与描述:
|
||||
* NFC 与 Termux 桥接活动,用于接收外部应用(包调用)传递的 JSON 指令并执行 Termux 脚本命令。
|
||||
* 支持 ACTION_BUILD(后台执行)与 ACTION_BUILD_VIEW(终端窗口唤起)两种动作。
|
||||
*
|
||||
* 作者:豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* 创建时间:2025-03-15 14:00:00
|
||||
* 最后编辑时间:2026-03-16 10:00:00
|
||||
*/
|
||||
package cc.winboll.studio.winboll.termux;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.winboll.models.NfcTermuxCmd;
|
||||
import com.google.gson.Gson;
|
||||
|
||||
public class NfcTermuxBridgeActivity extends Activity {
|
||||
|
||||
// ========================= 常量与静态属性 =========================
|
||||
public static final String TAG = "NfcTermuxBridgeActivity";
|
||||
|
||||
// 外部应用调用时使用的 Action 常量
|
||||
public static final String ACTION_BUILD = "cc.winboll.studio.winboll.termux.NfcTermuxBridgeActivity.ACTION_BUILD";
|
||||
|
||||
private static final Gson GSON = new Gson();
|
||||
|
||||
// ========================= 生命周期方法 =========================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onCreate() 调用,savedInstanceState: " + (savedInstanceState != null ? "非空" : "空"));
|
||||
dispatchIntent(getIntent());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
LogUtils.d(TAG, "onNewIntent() 调用,intent: " + (intent != null ? intent.toString() : "null"));
|
||||
if (intent != null) {
|
||||
LogUtils.d(TAG, "onNewIntent() action: " + intent.getAction());
|
||||
LogUtils.d(TAG, "onNewIntent() data: " + intent.getDataString());
|
||||
LogUtils.d(TAG, "onNewIntent() extras: " + intent.getExtras());
|
||||
LogUtils.d(TAG, "onNewIntent() flags: " + intent.getFlags());
|
||||
LogUtils.d(TAG, "onNewIntent() component: " + intent.getComponent());
|
||||
} else {
|
||||
LogUtils.w(TAG, "onNewIntent() intent is null");
|
||||
}
|
||||
dispatchIntent(intent);
|
||||
}
|
||||
|
||||
// ========================= 统一 Intent 分发(合并去重) =========================
|
||||
/**
|
||||
* 统一分发 Intent,根据 Action 路由到不同业务逻辑
|
||||
* @param intent 外部传入的 Intent
|
||||
*/
|
||||
private void dispatchIntent(Intent intent) {
|
||||
LogUtils.d(TAG, "dispatchIntent() 分发 intent");
|
||||
if (intent == null) {
|
||||
LogUtils.w(TAG, "dispatchIntent() intent is null");
|
||||
return;
|
||||
}
|
||||
|
||||
String action = intent.getAction();
|
||||
if (ACTION_BUILD.equals(action)) {
|
||||
ToastUtils.show("ACTION_BUILD 命中");
|
||||
onOpenTermuxProjectBuild(intent);
|
||||
} else {
|
||||
LogUtils.w(TAG, "dispatchIntent() 未知 Action: " + action);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
// ========================= 核心业务方法 =========================
|
||||
/**
|
||||
* 处理 ACTION_BUILD 动作:后台执行 NFC 指令
|
||||
*/
|
||||
// private void handleNfcIntent(Intent intent) {
|
||||
// LogUtils.d(TAG, "handleNfcIntent() 调用");
|
||||
// if (intent == null) {
|
||||
// LogUtils.w(TAG, "handleNfcIntent() intent 为空");
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// try {
|
||||
// String json = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
// LogUtils.d(TAG, "handleNfcIntent() json: " + json);
|
||||
//
|
||||
// if (json == null || json.isEmpty()) {
|
||||
// LogUtils.e(TAG, "handleNfcIntent() 指令为空");
|
||||
// showToast("指令为空");
|
||||
// finish();
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// NfcTermuxCmd cmd = GSON.fromJson(json, NfcTermuxCmd.class);
|
||||
// LogUtils.d(TAG, "handleNfcIntent() cmd: " + cmd);
|
||||
//
|
||||
// if (cmd.script == null || cmd.script.isEmpty()) {
|
||||
// LogUtils.e(TAG, "handleNfcIntent() script 为空");
|
||||
// showToast("script 不能为空");
|
||||
// finish();
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// //String scriptPath = "/data/data/com.termux/files/home/TermuxWorkSpaces/BashShells/AutoNFC/" + cmd.script;
|
||||
// String scriptPath = "/data/data/com.termux/files/home/TermuxWorkSpaces/BashShells/AutoNFC/" + "BuildWinBoLLProject.sh";
|
||||
// LogUtils.d(TAG, "handleNfcIntent() 脚本路径: " + scriptPath);
|
||||
//
|
||||
// boolean success = TermuxCommandExecutor.executeCommand(
|
||||
// this, scriptPath, cmd.args, cmd.workDir, cmd.background, cmd.resultDir
|
||||
// );
|
||||
// LogUtils.d(TAG, "handleNfcIntent() 执行结果: " + success);
|
||||
//
|
||||
// if (success) {
|
||||
// showToast("指令已发送: " + cmd.script);
|
||||
// LogUtils.i(TAG, "执行成功: " + scriptPath);
|
||||
// } else {
|
||||
// showToast("指令发送失败");
|
||||
// LogUtils.e(TAG, "执行失败");
|
||||
// }
|
||||
//
|
||||
// } catch (Exception e) {
|
||||
// LogUtils.e(TAG, "handleNfcIntent() 异常: " + e.getMessage(), e);
|
||||
// showToast("解析失败");
|
||||
// } finally {
|
||||
// finish();
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* 处理 ACTION_BUILD_VIEW 动作:唤起 Termux 窗口执行命令
|
||||
*/
|
||||
public void onOpenTermuxProjectBuild(Intent intent) {
|
||||
LogUtils.d(TAG, "onOpenTermuxProjectBuildView() 调用");
|
||||
if (intent == null) {
|
||||
LogUtils.w(TAG, "onOpenTermuxProjectBuildView() intent 为空");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
String json = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
LogUtils.d(TAG, "onOpenTermuxProjectBuildView() json: " + json);
|
||||
|
||||
if (json == null || json.isEmpty()) {
|
||||
LogUtils.e(TAG, "onOpenTermuxProjectBuildView() 指令为空");
|
||||
showToast("指令为空");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
NfcTermuxCmd cmd = GSON.fromJson(json, NfcTermuxCmd.class);
|
||||
LogUtils.d(TAG, "onOpenTermuxProjectBuildView() cmd: " + cmd);
|
||||
|
||||
if (cmd.script == null || cmd.script.isEmpty()) {
|
||||
LogUtils.e(TAG, "onOpenTermuxProjectBuildView() script 为空");
|
||||
showToast("script 不能为空");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder targetCmd = new StringBuilder();
|
||||
String nfcScriptFolder = "/data/data/com.termux/files/home/TermuxWorkSpaces/BashShells/AutoNFC/";
|
||||
targetCmd.append("cd " + nfcScriptFolder + " && ");
|
||||
//targetCmd.append("stdbuf -o0 -e0 -i0 bash ").append(cmd.script).append(" ");
|
||||
targetCmd.append("stdbuf -o0 -e0 -i0 bash ").append("BuildWinBoLLProject.sh").append(" ");
|
||||
if (cmd.args != null) {
|
||||
for (String arg : cmd.args) {
|
||||
targetCmd.append(arg).append(" ");
|
||||
}
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "onOpenTermuxProjectBuildView() 命令: " + targetCmd);
|
||||
boolean cmdSuccess = TermuxCommandExecutor.executeTerminalCommand(this, targetCmd.toString());
|
||||
LogUtils.d(TAG, "onOpenTermuxProjectBuildView() 执行结果: " + cmdSuccess);
|
||||
|
||||
if (cmdSuccess) {
|
||||
showToast("指令已发送: " + cmd.script);
|
||||
} else {
|
||||
showToast("指令发送失败");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "onOpenTermuxProjectBuildView() 异常: " + e.getMessage(), e);
|
||||
showToast("解析失败");
|
||||
} finally {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
// ========================= 公共静态测试方法 =========================
|
||||
/**
|
||||
* 内部测试方法:发送 ACTION_BUILD 指令
|
||||
*/
|
||||
public static void testCommand(Context context) {
|
||||
LogUtils.d(TAG, "testCommand()");
|
||||
try {
|
||||
String testJson = "{\"script\":\"BuildWinBoLLProject.sh\",\"args\":[\"DebugTemp\"],\"workDir\":null,\"background\":true,\"resultDir\":null}";
|
||||
Intent intent = new Intent(context, NfcTermuxBridgeActivity.class);
|
||||
intent.setAction(ACTION_BUILD); // 指定 Action
|
||||
intent.putExtra(Intent.EXTRA_TEXT, testJson);
|
||||
context.startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "testCommand() 失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部测试方法:发送 ACTION_BUILD_VIEW 指令
|
||||
*/
|
||||
// public static void testViewCommand(Context context) {
|
||||
// LogUtils.d(TAG, "testViewCommand()");
|
||||
// try {
|
||||
// String testJson = "{\"script\":\"BuildWinBoLLProjectView.sh\",\"args\":[\"DebugTemp\"],\"workDir\":null,\"background\":true,\"resultDir\":null}";
|
||||
// Intent intent = new Intent(context, NfcTermuxBridgeActivity.class);
|
||||
// intent.setAction(ACTION_BUILD_VIEW); // 指定 Action
|
||||
// intent.putExtra(Intent.EXTRA_TEXT, testJson);
|
||||
// context.startActivity(intent);
|
||||
// } catch (Exception e) {
|
||||
// LogUtils.e(TAG, "testViewCommand() 失败: " + e.getMessage());
|
||||
// }
|
||||
// }
|
||||
|
||||
// ========================= 工具方法 =========================
|
||||
/**
|
||||
* 统一显示 Toast,确保在主线程调用
|
||||
*/
|
||||
private void showToast(final String message) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(NfcTermuxBridgeActivity.this, message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
package cc.winboll.studio.winboll.termux;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import cc.winboll.studio.libappbase.LogUtils; // 替换 Log 为 LogUtils(与 Activity 一致)
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.shell.command.ExecutionCommand.Runner;
|
||||
|
||||
/**
|
||||
* Termux 命令调用工具类(基于 RunCommandService 原型封装)
|
||||
* 用于向 Termux 发送命令执行请求,兼容 Termux RUN_COMMAND Intent 规范
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026/01/19 16:30:00
|
||||
* @LastEditTime 2026/01/20 10:15:00
|
||||
*/
|
||||
public class TermuxCommandExecutor {
|
||||
private static final String TAG = "TermuxCommandExecutor";
|
||||
// 核心修复:Termux 官方包名(无 .app 后缀)
|
||||
private static final String TERMUX_PACKAGE_NAME = "com.termux";
|
||||
// Termux RunCommandService 完整类名(包名+类名)
|
||||
private static final String TERMUX_RUN_CMD_SERVICE_CLASS = "com.termux.app.RunCommandService";
|
||||
private static final String TERMUX_RUN_CMD_ACTION = TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND;
|
||||
private static final String TERMUX_HOME_PATH = "/data/data/com.termux/files/home";
|
||||
|
||||
/**
|
||||
* 执行 Termux 命令(核心方法)
|
||||
* @param context 上下文(如 Activity、Service)
|
||||
* @param command 要执行的命令路径(如 "/bin/ls"、"/data/data/com.termux/files/usr/bin/bash")
|
||||
* @param args 命令参数(如 ["-l", "/data/data/com.termux/files/home"])
|
||||
* @param workDir 工作目录(可为 null,默认 Termux 主目录)
|
||||
* @param isBackground 是否后台执行(true=后台,false=终端会话执行)
|
||||
* @param resultDir 命令结果输出目录(可为 null,不输出到文件)
|
||||
* @return 是否成功发送命令请求
|
||||
*/
|
||||
public static boolean executeCommand(Context context, String command, String[] args, String workDir, boolean isBackground, String resultDir) {
|
||||
// 1. 校验上下文和命令合法性
|
||||
if (context == null || command == null || command.isEmpty()) {
|
||||
LogUtils.e(TAG, "执行命令失败:上下文或命令为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 校验 Termux 是否安装(新增:提前校验,避免白跑流程)
|
||||
if (!isTermuxInstalled(context)) {
|
||||
LogUtils.e(TAG, "执行命令失败:Termux 未安装");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. 创建 Intent 并设置目标 Service
|
||||
Intent intent = new Intent(TERMUX_RUN_CMD_ACTION);
|
||||
intent.setClassName(TERMUX_PACKAGE_NAME, TERMUX_RUN_CMD_SERVICE_CLASS); // 用正确包名
|
||||
intent.setPackage(TERMUX_PACKAGE_NAME); // 明确包名,避免歧义
|
||||
|
||||
// 4. 设置核心命令参数(遵循 Termux RunCommandService 规范)
|
||||
intent.putExtra(TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH, command);
|
||||
if (args != null && args.length > 0) {
|
||||
intent.putExtra(TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS, args);
|
||||
LogUtils.d(TAG, "命令参数:" + String.join(",", args));
|
||||
}
|
||||
if (workDir != null && !workDir.isEmpty()) {
|
||||
intent.putExtra(TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE.EXTRA_WORKDIR, workDir);
|
||||
LogUtils.d(TAG, "工作目录:" + workDir);
|
||||
}
|
||||
|
||||
// 5. 设置执行模式(后台/终端会话)
|
||||
intent.putExtra(TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, isBackground);
|
||||
String runner = isBackground ? Runner.APP_SHELL.getName() : Runner.TERMINAL_SESSION.getName();
|
||||
intent.putExtra(TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE.EXTRA_RUNNER, runner);
|
||||
LogUtils.d(TAG, "执行模式:" + (isBackground ? "后台" : "终端会话") + ",Runner:" + runner);
|
||||
|
||||
// 6. 设置命令结果输出(可选,输出到文件)
|
||||
if (resultDir != null && !resultDir.isEmpty()) {
|
||||
intent.putExtra(TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE.EXTRA_RESULT_DIRECTORY, resultDir);
|
||||
intent.putExtra(TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE.EXTRA_RESULT_SINGLE_FILE, true);
|
||||
intent.putExtra(TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_BASENAME, "authcenter_cmd_result");
|
||||
LogUtils.d(TAG, "结果输出目录:" + resultDir);
|
||||
}
|
||||
|
||||
// 7. 允许替换参数中的逗号替代字符
|
||||
intent.putExtra(TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE.EXTRA_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS, true);
|
||||
|
||||
// 8. 发送请求(区分 Android O 及以上的前台服务)
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(intent);
|
||||
LogUtils.d(TAG, "Android O+ 启动前台服务发送命令");
|
||||
} else {
|
||||
context.startService(intent);
|
||||
LogUtils.d(TAG, "启动普通服务发送命令");
|
||||
}
|
||||
LogUtils.i(TAG, "命令发送成功:command=" + command);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "命令发送失败:" + e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 简化方法:执行 Termux 终端命令(默认工作目录,终端会话执行)
|
||||
* @param context 上下文
|
||||
* @param command 命令(如 "ls -l /home"、"echo 'hello termux'")
|
||||
* @return 是否成功发送
|
||||
*/
|
||||
public static boolean executeTerminalCommand(Context context, String command) {
|
||||
LogUtils.d(TAG, "调用 executeTerminalCommand,命令:" + command);
|
||||
if (command == null || command.isEmpty()) {
|
||||
LogUtils.e(TAG, "命令为空,执行失败");
|
||||
return false;
|
||||
}
|
||||
// 通过 bash 执行任意终端命令
|
||||
String[] args = {"-c", command};
|
||||
return executeCommand(
|
||||
context,
|
||||
"/data/data/com.termux/files/usr/bin/bash", // Termux 默认 bash 路径(正确)
|
||||
args,
|
||||
TERMUX_HOME_PATH, // 默认工作目录
|
||||
false, // 终端会话执行(可见)
|
||||
null // 不输出到文件
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 简化方法:后台执行 Termux 命令(无输出文件)
|
||||
* @param context 上下文
|
||||
* @param command 命令路径
|
||||
* @param args 命令参数
|
||||
* @return 是否成功发送
|
||||
*/
|
||||
public static boolean executeBackgroundCommand(Context context, String command, String[] args) {
|
||||
LogUtils.d(TAG, "调用 executeBackgroundCommand,command=" + command);
|
||||
return executeCommand(
|
||||
context,
|
||||
command,
|
||||
args,
|
||||
null,
|
||||
true, // 后台执行
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Termux 是否安装(修复核心错误)
|
||||
* @param context 上下文
|
||||
* @return Termux 是否已安装
|
||||
*/
|
||||
public static boolean isTermuxInstalled(Context context) {
|
||||
LogUtils.d(TAG, "校验 Termux 是否安装,包名:" + TERMUX_PACKAGE_NAME);
|
||||
if (context == null) {
|
||||
LogUtils.e(TAG, "校验失败:上下文为空");
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
// 用正确的 Termux 包名查询(com.termux)
|
||||
context.getPackageManager().getPackageInfo(TERMUX_PACKAGE_NAME, PackageManager.GET_ACTIVITIES);
|
||||
LogUtils.d(TAG, "Termux 已安装");
|
||||
return true;
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
LogUtils.w(TAG, "Termux 未安装:" + e.getMessage());
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "校验 Termux 安装状态异常:" + e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Termux 是否允许外部应用调用
|
||||
* @return 校验提示信息
|
||||
*/
|
||||
public static String checkTermuxExternalAppPermission() {
|
||||
String tip = "请确保 Termux 已开启「允许外部应用调用」权限:\n1. 打开 Termux 输入:termux-setup-storage\n2. 编辑配置文件:echo \"allow-external-apps = true\" > ~/.termux/termux.properties\n3. 重启 Termux 生效";
|
||||
LogUtils.d(TAG, "外部应用调用权限提示:" + tip);
|
||||
return tip;
|
||||
}
|
||||
|
||||
public static boolean openTermuxBash(Context context, String command) {
|
||||
return openTermuxBash(context, command, "~");
|
||||
}
|
||||
|
||||
public static boolean openTermuxBash(Context context, String command, String workDir) {
|
||||
LogUtils.d(TAG, "openTermuxBash() 按钮点击,执行Gradle命令(实时输出)");
|
||||
|
||||
// 1. 校验Termux是否安装
|
||||
if (!TermuxCommandExecutor.isTermuxInstalled(context)) {
|
||||
LogUtils.e(TAG, "openTermuxBash() 错误:未安装Termux应用");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 定义核心路径(确保路径与Termux中一致)
|
||||
String projectPath = TERMUX_HOME_PATH;
|
||||
if (workDir.startsWith("~") || workDir.startsWith(".")) {
|
||||
projectPath = TERMUX_HOME_PATH + "/" + workDir.substring(1);
|
||||
}
|
||||
|
||||
// 3. 构造命令(核心:用stdbuf禁用缓冲,实现实时输出)
|
||||
String targetCmd = "";
|
||||
// 步骤1:进入项目目录(不存在则创建)
|
||||
targetCmd += "cd " + projectPath + " && ";
|
||||
// 步骤2:加载环境变量
|
||||
targetCmd += "source ~/.bashrc && ";
|
||||
// 步骤3:显式配置PATH
|
||||
targetCmd += "export PATH=/data/data/com.termux/files/usr/bin:$PATH && ";
|
||||
// 步骤4:用stdbuf禁用stdout/stderr缓冲(关键!),执行Gradle命令
|
||||
// -o0:stdout无缓冲;-e0:stderr无缓冲;-i0:stdin无缓冲
|
||||
//targetCmd += "stdbuf -o0 -e0 -i0 " + gradleFullPath + " task --all | grep assemble && ";
|
||||
//targetCmd += "stdbuf -o0 -e0 -i0 " + gradleFullPath + " -Pandroid.aapt2FromMavenOverride=/data/data/com.termux/files/home/android-sdk/build-tools/34.0.4/aapt2 assembleBetaDebug && ";
|
||||
targetCmd += "stdbuf -o0 -e0 -i0 bash && ";
|
||||
// 步骤5:执行成功提示
|
||||
targetCmd += "echo '\n✅ 命令执行完成!' && echo '\n📌 当前目录:" + projectPath + "' && read -p '按回车键关闭终端...'";
|
||||
|
||||
|
||||
// 4. 执行命令(终端会话模式,唤起Termux窗口)
|
||||
boolean cmdSuccess = TermuxCommandExecutor.executeTerminalCommand(context, targetCmd);
|
||||
if (!cmdSuccess) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,50 @@
|
||||
package cc.winboll.studio.winboll.unittest;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.winboll.MainActivity;
|
||||
import cc.winboll.studio.winboll.R;
|
||||
import cc.winboll.studio.winboll.activities.BaseWinBoLLActivity;
|
||||
import cc.winboll.studio.winboll.termux.NfcTermuxBridgeActivity;
|
||||
import cc.winboll.studio.winboll.termux.TermuxCommandExecutor;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026/01/19 11:11
|
||||
* @LastEditTime 2026/01/19 15:30
|
||||
* @Describe Termux 环境测试(新增:读取Termux主目录文件列表功能)
|
||||
* @Date 2026/01/19 11:11:00
|
||||
* @LastEditTime 2026/01/21 17:45:00
|
||||
* @Describe Termux环境测试工具(跨包+sharedUserId模式)
|
||||
* 适配不同应用包名(当前包:cc.winboll.studio.winboll.beta / Termux包:com.termux)
|
||||
* 支持Termux目录读取、Gradle命令实时输出执行(唤起窗口),基于sharedUserId实现跨包权限适配
|
||||
*/
|
||||
public class TermuxEnvTestActivity extends BaseWinBoLLActivity {
|
||||
|
||||
public static final String TAG = "TermuxEnvTestActivity";
|
||||
// Termux主目录固定路径
|
||||
private static final String TERMUX_HOME_PATH = "/data/data/com.termux/files/home/WinBoLLStudio";
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
// 常量属性(置顶排列)
|
||||
public static final String TAG = "TermuxEnvTestActivity";
|
||||
private static final String TERMUX_HOME_PATH = "/data/data/com.termux/files/home/TermuxWorkSpaces";
|
||||
private static final String CMD_RESULT_FILE = TERMUX_HOME_PATH + "/CMD_RESULT_FILE.log";
|
||||
|
||||
// 成员属性(常量后排列)
|
||||
private Toolbar mToolbar;
|
||||
private TextView tvMessage;
|
||||
private Handler mainHandler;
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
@@ -37,111 +54,401 @@ public class TermuxEnvTestActivity extends BaseWinBoLLActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onCreate() 调用,初始化Activity");
|
||||
setContentView(R.layout.activity_termux_env_test);
|
||||
initView();
|
||||
initToolbar();
|
||||
}
|
||||
|
||||
private void initToolbar() {
|
||||
LogUtils.d(TAG, "initToolbar() 开始初始化");
|
||||
mToolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
if (mToolbar == null) {
|
||||
LogUtils.e(TAG, "initToolbar() | Toolbar未找到");
|
||||
return;
|
||||
}
|
||||
setSupportActionBar(mToolbar);
|
||||
mToolbar.setSubtitle(getTag());
|
||||
((AppCompatActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "导航栏 点击返回按钮");
|
||||
getActivity().finish();
|
||||
WinBoLLActivityManager.getInstance().startWinBoLLActivity(getActivity(), MainActivity.class);
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "initToolbar() 配置完成");
|
||||
initTermuxDirectory();
|
||||
LogUtils.d(TAG, "onCreate() 执行完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心功能:读取Termux主目录文件列表,返回字符串格式
|
||||
* @return 目录路径+文件列表(换行分隔),异常时返回错误信息
|
||||
* 初始化视图组件
|
||||
*/
|
||||
public String readTermuxHomeFileList() {
|
||||
LogUtils.d(TAG, "开始读取Termux目录:" + TERMUX_HOME_PATH);
|
||||
private void initView() {
|
||||
LogUtils.d(TAG, "initView() 开始初始化视图");
|
||||
tvMessage = (TextView) findViewById(R.id.tv_message);
|
||||
mainHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
// 初始化提示信息
|
||||
StringBuilder initMsg = new StringBuilder();
|
||||
initMsg.append("Termux 测试工具(跨包+sharedUserId模式)\n");
|
||||
initMsg.append("-------------------------\n");
|
||||
initMsg.append("当前应用包名:");
|
||||
initMsg.append(getPackageName());
|
||||
initMsg.append("\n");
|
||||
initMsg.append("Termux应用包名:com.termux\n");
|
||||
initMsg.append("sharedUserId:com.termux(需一致)\n");
|
||||
initMsg.append("支持功能:目录读取、Gradle命令实时输出(唤起Termux窗口)\n");
|
||||
initMsg.append("-------------------------\n");
|
||||
tvMessage.setText(initMsg.toString());
|
||||
|
||||
LogUtils.d(TAG, "initView() 初始化完成,tvMessage初始值:" + initMsg.toString().trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化Toolbar组件
|
||||
*/
|
||||
private void initToolbar() {
|
||||
LogUtils.d(TAG, "initToolbar() 开始初始化Toolbar");
|
||||
mToolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
|
||||
if (mToolbar == null) {
|
||||
LogUtils.e(TAG, "initToolbar() 错误:未找到Toolbar组件");
|
||||
return;
|
||||
}
|
||||
|
||||
setSupportActionBar(mToolbar);
|
||||
mToolbar.setSubtitle(getTag());
|
||||
((AppCompatActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "initToolbar() 导航栏返回按钮点击");
|
||||
getActivity().finish();
|
||||
}
|
||||
});
|
||||
|
||||
LogUtils.d(TAG, "initToolbar() 初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化Termux目标目录(跨包+sharedUserId模式)
|
||||
*/
|
||||
private void initTermuxDirectory() {
|
||||
LogUtils.d(TAG, "initTermuxDirectory() 开始初始化Termux目录,路径:" + TERMUX_HOME_PATH);
|
||||
File termuxDir = new File(TERMUX_HOME_PATH);
|
||||
|
||||
if (termuxDir.exists()) {
|
||||
LogUtils.d(TAG, "initTermuxDirectory() Termux目录已存在,无需创建");
|
||||
return;
|
||||
}
|
||||
|
||||
tvMessage.append("正在创建Termux目标目录...\n");
|
||||
boolean createSuccess = termuxDir.mkdirs();
|
||||
if (createSuccess) {
|
||||
LogUtils.d(TAG, "initTermuxDirectory() Termux目录创建成功:" + TERMUX_HOME_PATH);
|
||||
tvMessage.append("Termux目录创建成功:");
|
||||
tvMessage.append(TERMUX_HOME_PATH);
|
||||
tvMessage.append("\n");
|
||||
} else {
|
||||
LogUtils.e(TAG, "initTermuxDirectory() 错误:Termux目录创建失败");
|
||||
tvMessage.append("警告:Termux目录创建失败!\n");
|
||||
tvMessage.append("请检查:\n");
|
||||
tvMessage.append("1.AndroidManifest.xml中sharedUserId是否为com.termux\n");
|
||||
tvMessage.append("2.设备是否已root(部分机型需root才能跨包写私有目录)\n");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试读取Termux目录(按钮点击事件)
|
||||
*/
|
||||
public void onTestTermuxEnv(View view) {
|
||||
LogUtils.d(TAG, "onTestTermuxEnv() 按钮点击,开始读取Termux目录");
|
||||
tvMessage.append("\n【测试:读取Termux目录】\n");
|
||||
|
||||
String fileListStr = readTermuxHomeFileList();
|
||||
tvMessage.append(fileListStr);
|
||||
tvMessage.append("\n-------------------------\n");
|
||||
|
||||
LogUtils.d(TAG, "onTestTermuxEnv() 执行完成,读取结果长度:" + fileListStr.length() + "字符");
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试执行Gradle命令(实时输出版,唤起Termux窗口)
|
||||
*/
|
||||
public void onTestTermuxGradleBuildCMD(View view) {
|
||||
LogUtils.d(TAG, "onTestTermuxCMD() 按钮点击,执行Gradle命令(实时输出)");
|
||||
tvMessage.append("\n【测试:执行Gradle命令(实时输出)】\n");
|
||||
|
||||
// 1. 校验Termux是否安装
|
||||
if (!TermuxCommandExecutor.isTermuxInstalled(this)) {
|
||||
LogUtils.e(TAG, "onTestTermuxCMD() 错误:未安装Termux应用");
|
||||
tvMessage.append("错误:未安装Termux应用(包名:com.termux)\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 定义核心路径(确保路径与Termux中一致)
|
||||
String gradleFullPath = "/data/data/com.termux/files/home/gradle/gradle-7.5.1/bin/gradle";
|
||||
String projectPath = TERMUX_HOME_PATH + "/Sources/DebugTemp"; // 项目目录
|
||||
|
||||
// 3. 构造命令(核心:用stdbuf禁用缓冲,实现实时输出)
|
||||
String targetCmd = "";
|
||||
// 步骤1:进入项目目录(不存在则创建)
|
||||
//targetCmd += "cd " + projectPath + " || (mkdir -p " + projectPath + " && cd " + projectPath + ") && ";
|
||||
targetCmd += "cd " + projectPath + " && ";
|
||||
// 步骤2:加载环境变量
|
||||
targetCmd += "source ~/.bashrc && ";
|
||||
// 步骤3:显式配置PATH
|
||||
targetCmd += "export PATH=/data/data/com.termux/files/usr/bin:/data/data/com.termux/files/home/gradle/gradle-7.5.1/bin:$PATH && ";
|
||||
// 步骤4:用stdbuf禁用stdout/stderr缓冲(关键!),执行Gradle命令
|
||||
// -o0:stdout无缓冲;-e0:stderr无缓冲;-i0:stdin无缓冲
|
||||
//targetCmd += "stdbuf -o0 -e0 -i0 " + gradleFullPath + " task --all | grep assemble && ";
|
||||
targetCmd += "stdbuf -o0 -e0 -i0 " + gradleFullPath + " -Pandroid.aapt2FromMavenOverride=/data/data/com.termux/files/home/android-sdk/build-tools/34.0.4/aapt2 assembleBetaDebug && ";
|
||||
// 步骤5:执行成功提示
|
||||
targetCmd += "echo '\n✅ 命令执行完成!' && echo '\n📌 项目目录:" + projectPath + "' && read -p '按回车键关闭终端...'";
|
||||
|
||||
LogUtils.d(TAG, "onTestTermuxCMD() 执行命令:" + targetCmd);
|
||||
|
||||
// 4. 执行命令(终端会话模式,唤起Termux窗口)
|
||||
boolean cmdSuccess = TermuxCommandExecutor.executeTerminalCommand(this, targetCmd);
|
||||
if (!cmdSuccess) {
|
||||
LogUtils.e(TAG, "onTestTermuxCMD() 错误:命令发送失败");
|
||||
tvMessage.append("命令发送失败!\n");
|
||||
tvMessage.append("可能原因:\n");
|
||||
tvMessage.append("1.Termux未开启外部应用调用权限(执行termux-setup-storage后配置)\n");
|
||||
tvMessage.append("2.sharedUserId配置不一致\n");
|
||||
tvMessage.append("3.Termux未安装stdbuf(执行pkg install coreutils)\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. 应用内提示(说明实时输出特性)
|
||||
tvMessage.append("已唤起Termux窗口,执行说明:\n");
|
||||
tvMessage.append("1. 自动创建/进入项目目录:");
|
||||
tvMessage.append(projectPath);
|
||||
tvMessage.append("\n");
|
||||
tvMessage.append("2. 禁用输出缓冲(stdbuf),实现Gradle实时输出\n");
|
||||
tvMessage.append("3. 筛选assemble相关任务(grep assemble)\n");
|
||||
tvMessage.append("⚙️ Gradle路径:");
|
||||
tvMessage.append(gradleFullPath);
|
||||
tvMessage.append("\n");
|
||||
tvMessage.append("💡 若未实时输出,请在Termux中执行:pkg install coreutils(安装stdbuf)\n");
|
||||
tvMessage.append("-------------------------\n");
|
||||
}
|
||||
|
||||
public void onTestWinBoLLProjectBuild(View view) {
|
||||
ToastUtils.show("onTestWinBoLLProjectBuild");
|
||||
NfcTermuxBridgeActivity.testCommand(this);
|
||||
}
|
||||
|
||||
// public void onTestWinBoLLProjectBuildView(View view) {
|
||||
// ToastUtils.show("onTestWinBoLLProjectBuildView");
|
||||
// NfcTermuxBridgeActivity.testViewCommand(this);
|
||||
// }
|
||||
|
||||
public void onOpenTermuxBash(View view) {
|
||||
LogUtils.d(TAG, "onTestTermuxCMD() 按钮点击,执行Gradle命令(实时输出)");
|
||||
tvMessage.append("\n【测试:执行Gradle命令(实时输出)】\n");
|
||||
|
||||
// 1. 校验Termux是否安装
|
||||
if (!TermuxCommandExecutor.isTermuxInstalled(this)) {
|
||||
LogUtils.e(TAG, "onTestTermuxCMD() 错误:未安装Termux应用");
|
||||
tvMessage.append("错误:未安装Termux应用(包名:com.termux)\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 定义核心路径(确保路径与Termux中一致)
|
||||
String gradleFullPath = "/data/data/com.termux/files/home/gradle/gradle-7.5.1/bin/gradle";
|
||||
String projectPath = TERMUX_HOME_PATH + "/"; // 项目目录
|
||||
|
||||
// 3. 构造命令(核心:用stdbuf禁用缓冲,实现实时输出)
|
||||
String targetCmd = "";
|
||||
// 步骤1:进入项目目录(不存在则创建)
|
||||
targetCmd += "cd " + projectPath + " && ";
|
||||
// 步骤2:加载环境变量
|
||||
targetCmd += "source ~/.bashrc && ";
|
||||
// 步骤3:显式配置PATH
|
||||
targetCmd += "export PATH=/data/data/com.termux/files/usr/bin:/data/data/com.termux/files/home/gradle/gradle-7.5.1/bin:$PATH && ";
|
||||
// 步骤4:用stdbuf禁用stdout/stderr缓冲(关键!),执行Gradle命令
|
||||
// -o0:stdout无缓冲;-e0:stderr无缓冲;-i0:stdin无缓冲
|
||||
//targetCmd += "stdbuf -o0 -e0 -i0 " + gradleFullPath + " task --all | grep assemble && ";
|
||||
//targetCmd += "stdbuf -o0 -e0 -i0 " + gradleFullPath + " -Pandroid.aapt2FromMavenOverride=/data/data/com.termux/files/home/android-sdk/build-tools/34.0.4/aapt2 assembleBetaDebug && ";
|
||||
targetCmd += "stdbuf -o0 -e0 -i0 bash && ";
|
||||
// 步骤5:执行成功提示
|
||||
targetCmd += "echo '\n✅ 命令执行完成!' && echo '\n📌 项目目录:" + projectPath + "' && read -p '按回车键关闭终端...'";
|
||||
|
||||
LogUtils.d(TAG, "onTestTermuxCMD() 执行命令:" + targetCmd);
|
||||
|
||||
// 4. 执行命令(终端会话模式,唤起Termux窗口)
|
||||
boolean cmdSuccess = TermuxCommandExecutor.executeTerminalCommand(this, targetCmd);
|
||||
if (!cmdSuccess) {
|
||||
LogUtils.e(TAG, "onTestTermuxCMD() 错误:命令发送失败");
|
||||
tvMessage.append("命令发送失败!\n");
|
||||
tvMessage.append("可能原因:\n");
|
||||
tvMessage.append("1.Termux未开启外部应用调用权限(执行termux-setup-storage后配置)\n");
|
||||
tvMessage.append("2.sharedUserId配置不一致\n");
|
||||
tvMessage.append("3.Termux未安装stdbuf(执行pkg install coreutils)\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. 应用内提示(说明实时输出特性)
|
||||
tvMessage.append("已唤起Termux窗口,执行说明:\n");
|
||||
tvMessage.append("1. 自动创建/进入项目目录:");
|
||||
tvMessage.append(projectPath);
|
||||
tvMessage.append("\n");
|
||||
tvMessage.append("2. 禁用输出缓冲(stdbuf),实现Gradle实时输出\n");
|
||||
tvMessage.append("3. 筛选assemble相关任务(grep assemble)\n");
|
||||
tvMessage.append("⚙️ Gradle路径:");
|
||||
tvMessage.append(gradleFullPath);
|
||||
tvMessage.append("\n");
|
||||
tvMessage.append("💡 若未实时输出,请在Termux中执行:pkg install coreutils(安装stdbuf)\n");
|
||||
tvMessage.append("-------------------------\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* 跨包读取Termux命令结果文件(保留原功能,兼容其他场景)
|
||||
*/
|
||||
private String readCmdResultDirectly() {
|
||||
LogUtils.d(TAG, "readCmdResultDirectly() 开始读取命令结果,文件路径:" + CMD_RESULT_FILE);
|
||||
File resultFile = new File(CMD_RESULT_FILE);
|
||||
|
||||
// 校验文件存在性
|
||||
if (!resultFile.exists()) {
|
||||
LogUtils.e(TAG, "readCmdResultDirectly() 错误:结果文件不存在");
|
||||
return "错误:结果文件不存在\n可能原因:\n1.命令执行失败\n2.延迟时间不足\n3.跨包写权限被拒绝(需root)";
|
||||
}
|
||||
|
||||
// 校验文件可读性
|
||||
if (!resultFile.canRead()) {
|
||||
LogUtils.e(TAG, "readCmdResultDirectly() 错误:无结果文件读取权限");
|
||||
return "错误:无结果文件读取权限(sharedUserId配置错误或未root)";
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
StringBuilder result = new StringBuilder();
|
||||
BufferedReader br = null;
|
||||
try {
|
||||
br = new BufferedReader(new InputStreamReader(new FileInputStream(resultFile), StandardCharsets.UTF_8));
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
result.append(line);
|
||||
result.append("\n");
|
||||
}
|
||||
LogUtils.d(TAG, "readCmdResultDirectly() 文件读取完成,结果长度:" + result.length() + "字符");
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "readCmdResultDirectly() 错误:文件读取异常", e);
|
||||
return "错误:读取文件异常 → " + e.getMessage();
|
||||
} finally {
|
||||
if (br != null) {
|
||||
try {
|
||||
br.close();
|
||||
LogUtils.d(TAG, "readCmdResultDirectly() BufferedReader资源已关闭");
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "readCmdResultDirectly() 错误:BufferedReader关闭异常", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String resultStr = result.toString().trim();
|
||||
return resultStr.isEmpty() ? "命令执行成功,但无输出" : resultStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除命令结果临时文件(保留原功能)
|
||||
*/
|
||||
private void deleteCmdResultFile() {
|
||||
LogUtils.d(TAG, "deleteCmdResultFile() 开始删除临时文件,路径:" + CMD_RESULT_FILE);
|
||||
File resultFile = new File(CMD_RESULT_FILE);
|
||||
|
||||
if (!resultFile.exists()) {
|
||||
LogUtils.d(TAG, "deleteCmdResultFile() 临时文件不存在,无需删除");
|
||||
return;
|
||||
}
|
||||
|
||||
if (resultFile.canWrite()) {
|
||||
boolean deleteSuccess = resultFile.delete();
|
||||
if (deleteSuccess) {
|
||||
LogUtils.d(TAG, "deleteCmdResultFile() 临时文件删除成功");
|
||||
} else {
|
||||
LogUtils.w(TAG, "deleteCmdResultFile() 警告:临时文件删除失败");
|
||||
tvMessage.append("警告:临时结果文件删除失败\n");
|
||||
}
|
||||
} else {
|
||||
LogUtils.w(TAG, "deleteCmdResultFile() 警告:无临时文件删除权限");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 跨包读取Termux目录文件列表(保留原功能)
|
||||
*/
|
||||
private String readTermuxHomeFileList() {
|
||||
LogUtils.d(TAG, "readTermuxHomeFileList() 开始读取目录,路径:" + TERMUX_HOME_PATH);
|
||||
File termuxHomeDir = new File(TERMUX_HOME_PATH);
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
// 1. 检查目录是否存在
|
||||
// 基础校验
|
||||
if (!termuxHomeDir.exists()) {
|
||||
String errorMsg = "错误:Termux目录不存在 → " + TERMUX_HOME_PATH;
|
||||
LogUtils.e(TAG, errorMsg);
|
||||
return errorMsg;
|
||||
LogUtils.e(TAG, "readTermuxHomeFileList() 错误:目录不存在");
|
||||
return "错误:Termux目录不存在 → " + TERMUX_HOME_PATH;
|
||||
}
|
||||
|
||||
// 2. 检查是否为目录
|
||||
if (!termuxHomeDir.isDirectory()) {
|
||||
String errorMsg = "错误:指定路径不是目录 → " + TERMUX_HOME_PATH;
|
||||
LogUtils.e(TAG, errorMsg);
|
||||
return errorMsg;
|
||||
LogUtils.e(TAG, "readTermuxHomeFileList() 错误:指定路径不是目录");
|
||||
return "错误:指定路径不是目录 → " + TERMUX_HOME_PATH;
|
||||
}
|
||||
|
||||
// 3. 检查读写权限
|
||||
if (!termuxHomeDir.canRead()) {
|
||||
String errorMsg = "错误:无目录读取权限 → " + TERMUX_HOME_PATH;
|
||||
LogUtils.e(TAG, errorMsg);
|
||||
return errorMsg;
|
||||
LogUtils.e(TAG, "readTermuxHomeFileList() 错误:无目录读取权限");
|
||||
return "错误:无目录读取权限(需满足:1.sharedUserId=com.termux 2.设备root)";
|
||||
}
|
||||
|
||||
// 4. 获取目录下所有文件/子目录
|
||||
// 获取文件列表
|
||||
File[] files = termuxHomeDir.listFiles();
|
||||
if (files == null || files.length == 0) {
|
||||
String emptyMsg = "Termux目录为空 → " + TERMUX_HOME_PATH;
|
||||
LogUtils.d(TAG, emptyMsg);
|
||||
return emptyMsg;
|
||||
LogUtils.d(TAG, "readTermuxHomeFileList() 目录为空");
|
||||
return "Termux目录为空 → " + TERMUX_HOME_PATH;
|
||||
}
|
||||
|
||||
// 5. 拼接文件列表字符串(包含类型、名称、路径)
|
||||
result.append("Termux主目录:").append(TERMUX_HOME_PATH).append("\n");
|
||||
result.append("文件/子目录总数:").append(files.length).append("\n");
|
||||
// 拼接结果字符串
|
||||
result.append("Termux主目录:");
|
||||
result.append(TERMUX_HOME_PATH);
|
||||
result.append("\n");
|
||||
result.append("文件/子目录总数:");
|
||||
result.append(files.length);
|
||||
result.append("\n");
|
||||
result.append("目录权限:");
|
||||
result.append("读:");
|
||||
result.append(termuxHomeDir.canRead());
|
||||
result.append(" | 写:");
|
||||
result.append(termuxHomeDir.canWrite());
|
||||
result.append("\n");
|
||||
result.append("-------------------------\n");
|
||||
|
||||
for (File file : files) {
|
||||
String fileType = file.isDirectory() ? "[目录]" : "[文件]";
|
||||
String fileName = file.getName();
|
||||
String filePath = file.getAbsolutePath();
|
||||
result.append(fileType).append(" ").append(fileName).append(" → ").append(filePath).append("\n");
|
||||
String fileSize = file.isFile() ? " | 大小:" + formatFileSize(file.length()) : "";
|
||||
String filePerm = " | 权限:r:" + file.canRead() + "/w:" + file.canWrite();
|
||||
|
||||
result.append(fileType);
|
||||
result.append(" ");
|
||||
result.append(fileName);
|
||||
result.append(fileSize);
|
||||
result.append(filePerm);
|
||||
result.append(" → ");
|
||||
result.append(filePath);
|
||||
result.append("\n");
|
||||
}
|
||||
|
||||
// 6. 去除末尾多余换行
|
||||
String fileListStr = result.toString().trim();
|
||||
LogUtils.d(TAG, "Termux目录读取完成,结果长度:" + fileListStr.length());
|
||||
return fileListStr;
|
||||
LogUtils.d(TAG, "readTermuxHomeFileList() 读取完成,结果长度:" + result.length() + "字符");
|
||||
return result.toString().trim();
|
||||
}
|
||||
/**
|
||||
* 格式化文件大小(B→KB→MB)
|
||||
*/
|
||||
private String formatFileSize(long length) {
|
||||
LogUtils.d(TAG, "formatFileSize() 调用,文件长度:" + length + "B");
|
||||
String sizeStr;
|
||||
if (length < 1024) {
|
||||
sizeStr = length + "B";
|
||||
} else if (length < 1024 * 1024) {
|
||||
sizeStr = String.format("%.1fKB", length / 1024.0);
|
||||
} else {
|
||||
sizeStr = String.format("%.1fMB", length / (1024.0 * 1024));
|
||||
}
|
||||
LogUtils.d(TAG, "formatFileSize() 格式化结果:" + sizeStr);
|
||||
return sizeStr;
|
||||
}
|
||||
|
||||
public void onTestTermuxEnv(View view) {
|
||||
TextView tvMessage = findViewById(R.id.tv_message);
|
||||
// 测试:读取Termux目录文件列表并打印日志
|
||||
String fileListStr = readTermuxHomeFileList();
|
||||
tvMessage.append(fileListStr);
|
||||
}
|
||||
|
||||
public void onTestTermuxCMD(View view) {
|
||||
// pkg update && pkg upgrade -y
|
||||
// pkg install termux-api -y
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setPackage("com.termux");
|
||||
intent.setAction("com.termux.RUN_COMMAND");
|
||||
intent.addCategory(Intent.CATEGORY_DEFAULT);
|
||||
intent.putExtra("com.termux.RUN_COMMAND_PATH", "/data/data/com.termux/files/home");
|
||||
intent.putExtra("com.termux.RUN_COMMAND_ARGUMENTS", new String[]{"ls"});
|
||||
intent.putExtra("com.termux.RUN_COMMAND_WAIT_FOR_RESULT", false);
|
||||
intent.putExtra("com.termux.RUN_COMMAND_BACKGROUND", false);
|
||||
if (intent.resolveActivity(getPackageManager()) != null) {
|
||||
startActivity(intent);
|
||||
} else {
|
||||
Toast.makeText(this, "Termux或Termux:API未正确安装", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "onDestroy() 调用,清理资源");
|
||||
deleteCmdResultFile();
|
||||
if (mainHandler != null) {
|
||||
mainHandler.removeCallbacksAndMessages(null);
|
||||
LogUtils.d(TAG, "onDestroy() Handler资源已释放");
|
||||
}
|
||||
LogUtils.d(TAG, "onDestroy() 执行完成");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
package cc.winboll.studio.winboll.views;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.winboll.models.TermuxButtonModel;
|
||||
|
||||
/**
|
||||
* 自定义Termux功能按钮控件
|
||||
* 绑定TermuxButtonModel实体数据,拦截点击事件做确认弹窗逻辑判断
|
||||
* isCommitted为true直接执行点击事件,为false弹出确认对话框二次确认
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @CreateTime 2026/04/30 10:57:00
|
||||
* @EditTime 2026/04/30 13:52:15
|
||||
*/
|
||||
public class TermuxButton extends Button {
|
||||
|
||||
public static final String TAG = "TermuxButton";
|
||||
|
||||
/** 绑定按钮对应数据实体 */
|
||||
private TermuxButtonModel buttonModel;
|
||||
/** 保存外部设置的原始点击监听 */
|
||||
private OnClickListener originClickListener;
|
||||
|
||||
//==================== 构造方法 ====================
|
||||
/**
|
||||
* 代码动态创建控件构造
|
||||
* @param context 上下文
|
||||
*/
|
||||
public TermuxButton(Context context) {
|
||||
super(context);
|
||||
LogUtils.d(TAG, "TermuxButton 无参构造执行,上下文:" + context);
|
||||
initView(null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* XML布局引用控件基础构造
|
||||
* @param context 上下文
|
||||
* @param attrs XML属性集
|
||||
*/
|
||||
public TermuxButton(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
LogUtils.d(TAG, "TermuxButton XML构造执行");
|
||||
initView(attrs, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* XML布局带自定义属性构造
|
||||
* @param context 上下文
|
||||
* @param attrs XML属性集
|
||||
* @param defStyleAttr 默认样式属性
|
||||
*/
|
||||
public TermuxButton(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
LogUtils.d(TAG, "TermuxButton 带样式属性构造执行");
|
||||
initView(attrs, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 高版本Android完整全参构造
|
||||
* @param context 上下文
|
||||
* @param attrs XML属性集
|
||||
* @param defStyleAttr 默认样式属性
|
||||
* @param defStyleRes 默认样式资源
|
||||
*/
|
||||
public TermuxButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
LogUtils.d(TAG, "TermuxButton 全参构造执行");
|
||||
initView(attrs, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接传入Model初始化控件构造
|
||||
* @param context 上下文
|
||||
* @param model 按钮数据实体
|
||||
*/
|
||||
public TermuxButton(Context context, TermuxButtonModel model) {
|
||||
super(context);
|
||||
LogUtils.d(TAG, "TermuxButton Model入参构造执行");
|
||||
initView(null, model);
|
||||
}
|
||||
|
||||
//==================== 核心初始化 ====================
|
||||
/**
|
||||
* 控件统一初始化方法
|
||||
* @param attrs XML属性集合
|
||||
* @param model 绑定数据实体
|
||||
*/
|
||||
private void initView(AttributeSet attrs, TermuxButtonModel model) {
|
||||
this.buttonModel = model;
|
||||
|
||||
// 按钮基础默认配置
|
||||
setClickable(true);
|
||||
setFocusable(true);
|
||||
|
||||
// 解析XML布局自定义属性
|
||||
if (attrs != null) {
|
||||
parseXmlCustomAttr(attrs);
|
||||
}
|
||||
|
||||
// 同步Model内按钮名称到控件展示文本
|
||||
refreshButtonText();
|
||||
// 绑定自定义拦截点击事件
|
||||
setCustomClickEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析XML布局属性,读取原生android:text与自定义属性赋值到Model
|
||||
* @param attrs XML属性集
|
||||
*/
|
||||
private void parseXmlCustomAttr(AttributeSet attrs) {
|
||||
if (buttonModel == null) {
|
||||
buttonModel = new TermuxButtonModel();
|
||||
LogUtils.d(TAG, "自动初始化空的TermuxButtonModel实体");
|
||||
}
|
||||
|
||||
// 读取原生android:text作为按钮名称
|
||||
String androidText = attrs.getAttributeValue("http://schemas.android.com/apk/res/android", "text");
|
||||
// 读取自定义扩展属性
|
||||
String exeCommand = attrs.getAttributeValue("http://schemas.android.com/apk/res-auto", "exeCommand");
|
||||
String workDir = attrs.getAttributeValue("http://schemas.android.com/apk/res-auto", "workDir");
|
||||
String isCommittedStr = attrs.getAttributeValue("http://schemas.android.com/apk/res-auto", "isCommitted");
|
||||
String commitTitle = attrs.getAttributeValue("http://schemas.android.com/apk/res-auto", "commitTitle");
|
||||
String commitInfo = attrs.getAttributeValue("http://schemas.android.com/apk/res-auto", "commitInfo");
|
||||
|
||||
// 属性赋值绑定
|
||||
if (androidText != null) {
|
||||
buttonModel.setButtonName(androidText);
|
||||
}
|
||||
if (exeCommand != null) {
|
||||
buttonModel.setExeCommand(exeCommand);
|
||||
}
|
||||
if (workDir != null) {
|
||||
buttonModel.setWorkDir(workDir);
|
||||
}
|
||||
if (isCommittedStr != null) {
|
||||
buttonModel.setCommitted(Boolean.parseBoolean(isCommittedStr));
|
||||
}
|
||||
if (commitTitle != null) {
|
||||
buttonModel.setCommitTitle(commitTitle);
|
||||
}
|
||||
if (commitInfo != null) {
|
||||
buttonModel.setCommitInfo(commitInfo);
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "XML属性解析完成,按钮名称:" + androidText);
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步Model中buttonName,更新按钮展示文字
|
||||
*/
|
||||
private void refreshButtonText() {
|
||||
if (buttonModel != null) {
|
||||
setText(buttonModel.getButtonName());
|
||||
}
|
||||
}
|
||||
|
||||
//==================== 点击事件相关 ====================
|
||||
/**
|
||||
* 重写点击监听设置,保存外部原始点击事件
|
||||
* @param l 外部传入点击监听
|
||||
*/
|
||||
@Override
|
||||
public void setOnClickListener(OnClickListener l) {
|
||||
this.originClickListener = l;
|
||||
LogUtils.d(TAG, "保存外部原始按钮点击监听");
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义拦截按钮点击逻辑
|
||||
* isCommitted=true 直接执行原始点击事件
|
||||
* isCommitted=false 弹出确认二次弹窗
|
||||
*/
|
||||
private void setCustomClickEvent() {
|
||||
super.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (buttonModel == null) {
|
||||
LogUtils.d(TAG, "无绑定Model,直接执行原始点击事件");
|
||||
if (originClickListener != null) {
|
||||
originClickListener.onClick(view);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
boolean commitState = buttonModel.isCommitted();
|
||||
LogUtils.d(TAG, "按钮点击触发,isCommitted状态:" + commitState);
|
||||
if (commitState) {
|
||||
// 无需确认,直接执行原有点击任务
|
||||
if (originClickListener != null) {
|
||||
originClickListener.onClick(view);
|
||||
}
|
||||
} else {
|
||||
// 需要二次确认,弹出提示对话框
|
||||
showCommitDialog();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹出操作确认对话框
|
||||
* 标题:commitTitle 内容:commitInfo
|
||||
* 取消:关闭弹窗无操作 确定:执行原始点击事件
|
||||
*/
|
||||
private void showCommitDialog() {
|
||||
Context context = getContext();
|
||||
String dialogTitle = buttonModel.getCommitTitle();
|
||||
String dialogMsg = buttonModel.getCommitInfo();
|
||||
|
||||
// 空值默认兜底处理
|
||||
if (dialogTitle == null || "".equals(dialogTitle)) {
|
||||
dialogTitle = "温馨提示";
|
||||
}
|
||||
if (dialogMsg == null || "".equals(dialogMsg)) {
|
||||
dialogMsg = "确定要执行该操作吗?";
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "弹出确认对话框,标题:" + dialogTitle);
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(dialogTitle)
|
||||
.setMessage(dialogMsg)
|
||||
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
LogUtils.d(TAG, "对话框点击取消,终止操作");
|
||||
}
|
||||
})
|
||||
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
LogUtils.d(TAG, "对话框点击确定,继续执行操作");
|
||||
if (originClickListener != null) {
|
||||
originClickListener.onClick(TermuxButton.this);
|
||||
}
|
||||
}
|
||||
})
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
}
|
||||
|
||||
//==================== Getter & Setter ====================
|
||||
public TermuxButtonModel getButtonModel() {
|
||||
return buttonModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置绑定按钮数据实体,自动刷新按钮展示文字
|
||||
* @param buttonModel 数据实体类
|
||||
*/
|
||||
public void setButtonModel(TermuxButtonModel buttonModel) {
|
||||
this.buttonModel = buttonModel;
|
||||
LogUtils.d(TAG, "外部设置ButtonModel,自动刷新按钮文本");
|
||||
refreshButtonText();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
8
winboll/src/main/res/drawable/dot_background.xml
Normal file
8
winboll/src/main/res/drawable/dot_background.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="ring"
|
||||
android:innerRadiusRatio="3"
|
||||
android:thicknessRatio="8"
|
||||
android:useLevel="false">
|
||||
<solid android:color="#00000000"/>
|
||||
</shape>
|
||||
14
winboll/src/main/res/drawable/dot_darkgreen_dark.xml
Normal file
14
winboll/src/main/res/drawable/dot_darkgreen_dark.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<inset xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:insetLeft="2dp"
|
||||
android:insetTop="2dp"
|
||||
android:insetRight="2dp"
|
||||
android:insetBottom="2dp">
|
||||
<shape android:shape="ring"
|
||||
android:innerRadiusRatio="2"
|
||||
android:thicknessRatio="2.5"
|
||||
android:useLevel="false">
|
||||
<solid android:color="#006400"/>
|
||||
<stroke android:width="1dp" android:color="#006400"/>
|
||||
</shape>
|
||||
</inset>
|
||||
90
winboll/src/main/res/layout/activity_my_termux.xml
Normal file
90
winboll/src/main/res/layout/activity_my_termux.xml
Normal file
@@ -0,0 +1,90 @@
|
||||
<?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="wrap_content"
|
||||
android:gravity="top">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary"
|
||||
app:title="@string/app_name"
|
||||
app:subtitle="@string/my_termux_activity"
|
||||
app:titleTextColor="@android:color/white"
|
||||
app:subtitleTextColor="@android:color/white"/>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1.0">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1.0">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<cc.winboll.studio.winboll.views.TermuxButton
|
||||
android:id="@+id/btn_termux"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Termux"
|
||||
android:textSize="18sp"
|
||||
android:padding="16dp"
|
||||
android:backgroundTint="@android:color/holo_blue_dark"
|
||||
app:exeCommand="cd ~"
|
||||
app:workDir="~"
|
||||
app:isCommitted="true"
|
||||
app:commitTitle="打开 Termux"
|
||||
app:commitInfo="打开 Termux 应用"/>
|
||||
|
||||
<cc.winboll.studio.winboll.views.TermuxButton
|
||||
android:id="@+id/btn_termuxworkspaces"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="TermuxWorkSpaces"
|
||||
android:textSize="18sp"
|
||||
android:padding="16dp"
|
||||
android:backgroundTint="@android:color/holo_blue_dark"
|
||||
app:exeCommand="cd ~/TermuxWorkSpaces"
|
||||
app:workDir="~"
|
||||
app:isCommitted="false"
|
||||
app:commitTitle="打开 TermuxWorkSpaces"
|
||||
app:commitInfo="打开 Termux 应用,进入 TermuxWorkSpaces 目录。"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="+"
|
||||
android:id="@+id/btn_addtermuxbutton"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
25
winboll/src/main/res/layout/activity_pattern_lock.xml
Normal file
25
winboll/src/main/res/layout/activity_pattern_lock.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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="@android:color/black">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:gravity="center"
|
||||
app:titleTextColor="@android:color/white"
|
||||
app:subtitleTextColor="@android:color/white"/>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#FFFFFF"/>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -12,23 +12,39 @@
|
||||
android:id="@+id/toolbar"/>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="right">
|
||||
|
||||
<Button
|
||||
android:textAllCaps="false"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="TestTermuxCMD"
|
||||
android:onClick="onTestTermuxCMD"/>
|
||||
|
||||
android:text="Open Termux Bash"
|
||||
android:onClick="onOpenTermuxBash"/>
|
||||
|
||||
<Button
|
||||
android:textAllCaps="false"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="TestTermuxEnv"
|
||||
android:text="Test Termux Gradle Build CMD"
|
||||
android:onClick="onTestTermuxGradleBuildCMD"/>
|
||||
|
||||
<Button
|
||||
android:textAllCaps="false"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Test Termux Env"
|
||||
android:onClick="onTestTermuxEnv"/>
|
||||
|
||||
<Button
|
||||
android:textAllCaps="false"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Test WinBoLL Project Build"
|
||||
android:onClick="onTestWinBoLLProjectBuild"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ScrollView
|
||||
@@ -40,7 +56,8 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Text"
|
||||
android:id="@+id/tv_message"/>
|
||||
android:id="@+id/tv_message"
|
||||
android:textIsSelectable="true"/>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
android:id="@+id/item_home"
|
||||
android:title="HOME"/>
|
||||
<item
|
||||
android:id="@+id/item_mytermux"
|
||||
android:title="MyTermuxActivity"/>
|
||||
<item
|
||||
android:id="@+id/item_settings"
|
||||
android:title="Settings"/>
|
||||
<item
|
||||
|
||||
12
winboll/src/main/res/menu/toolbar_test.xml
Normal file
12
winboll/src/main/res/menu/toolbar_test.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item android:title="UnitTest">
|
||||
<menu >
|
||||
<item
|
||||
android:id="@+id/item_termux_env_test"
|
||||
android:title="TermuxEnvTestActivity"/>
|
||||
</menu>
|
||||
</item>
|
||||
</menu>
|
||||
@@ -5,5 +5,13 @@
|
||||
<attr name="toolbarTitleColor" format="color" />
|
||||
<attr name="toolbarBackgroundColor" format="color" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="TermuxButton">
|
||||
<attr name="exeCommand" format="string" />
|
||||
<attr name="workDir" format="string" />
|
||||
<attr name="isCommitted" format="boolean" />
|
||||
<attr name="commitTitle" format="string" />
|
||||
<attr name="commitInfo" format="string" />
|
||||
</declare-styleable>
|
||||
</resources>
|
||||
|
||||
|
||||
@@ -3,4 +3,5 @@
|
||||
<color name="colorPrimary">#009688</color>
|
||||
<color name="colorPrimaryDark">#00796B</color>
|
||||
<color name="colorAccent">#FF9800</color>
|
||||
<color name="pattern_lock_black">#000000</color>
|
||||
</resources>
|
||||
@@ -11,4 +11,6 @@
|
||||
<string name="cn2_switch_disabled">金抖云 X</string>
|
||||
<string name="tileservice_name">WinBoLL</string>
|
||||
<string name="toolbar_icon_description">WinBoLL APP</string>
|
||||
<string name="my_termux_activity">MyTermuxActivity</string>
|
||||
<string name="pattern_lock_title">图案密码设置</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user