Compare commits

..

52 Commits

Author SHA1 Message Date
4c5df10c54 <positions>APK 15.12.15 release Publish. 2026-03-31 13:51:27 +08:00
c0084cd160 添加任务删除确定对话框,简化对话框提示信息。 2026-03-31 13:49:38 +08:00
f89dbede9c 源码整理 2026-03-31 13:27:10 +08:00
c946d7af3a 添加删除位置记录的确定对话框。 2026-03-31 13:24:09 +08:00
b8ddd87e66 <positions>APK 15.12.14 release Publish. 2026-03-31 13:03:31 +08:00
5ffc049790 在任务编辑窗口里,添加当前时间辅助显示。 2026-03-31 13:00:55 +08:00
0fcf1c5952 Merge remote-tracking branch 'origin/winboll' into positions 2026-03-27 13:10:10 +08:00
610d3811db 栏目字体与分段调整 2026-03-18 15:49:42 +08:00
2d949eb5a3 分类栏目排版 2026-03-18 15:44:01 +08:00
e6940805d9 明确操作优先级 2026-03-18 15:41:48 +08:00
1641424276 通顺表达句法。 2026-03-18 15:36:58 +08:00
5d1cdff283 使用Markdown语法调整说明书显示格式。 2026-03-18 15:32:49 +08:00
da66cea1e5 详细解析说明 2026-03-18 12:05:42 +08:00
5eb7441dc7 更新说明书 2026-03-18 12:03:32 +08:00
5f3168e17f 更新说明书 2026-03-18 11:49:58 +08:00
e3c4bab6c9 更正项目说明书。 2026-03-18 11:41:23 +08:00
2af6427ca8 精简nfc action 数量 2026-03-17 04:03:20 +08:00
b8c70bef98 调整NFC接口窗体根据动作类型指定对应的脚本运行。 2026-03-16 16:42:02 +08:00
7713d6c460 基本实现NFC Build View 模块功能。 2026-03-15 20:25:16 +08:00
73c69bd665 20260315_193958_991 2026-03-15 19:40:11 +08:00
a076fe50cd 调整Termux 调用模块 UI显示与环境参数配置。 2026-03-15 13:45:49 +08:00
1512b76c36 编译参数修复 2026-03-15 11:55:10 +08:00
850b9af6ec 编译参数修复 2026-03-15 11:52:51 +08:00
31c1592086 Termux终端调用接口完成 2026-03-15 11:50:01 +08:00
b3976a8633 <winboll>APK 15.11.25 release Publish. 2026-03-15 11:48:40 +08:00
ea896228d7 <winboll>APK 15.11.24 release Publish. 2026-03-15 11:46:14 +08:00
d49ecb3943 <winboll>APK 15.11.23 release Publish. 2026-03-15 11:09:40 +08:00
ad3aecf867 <winboll>APK 15.11.22 release Publish. 2026-03-15 11:07:05 +08:00
c417d9732a <winboll>APK 15.11.21 release Publish. 2026-03-15 10:52:23 +08:00
7bd1357c8c <winboll>APK 15.11.20 release Publish. 2026-03-15 10:46:25 +08:00
16a2c3c0c8 <winboll>APK 15.11.19 release Publish. 2026-03-15 10:36:01 +08:00
b747d83972 <winboll>APK 15.11.18 release Publish. 2026-03-15 10:26:55 +08:00
f2788dda96 <winboll>APK 15.11.17 release Publish. 2026-03-15 10:09:28 +08:00
ea3a66bebe <winboll>APK 15.11.16 release Publish. 2026-03-15 10:07:00 +08:00
a53a0cbcdc <winboll>APK 15.11.15 release Publish. 2026-03-13 13:41:48 +08:00
94ac2d9f9c 更新编译说明 2026-02-04 13:13:58 +08:00
11d7846cd2 更新项目说明书 2026-02-04 12:38:17 +08:00
21e464bf5e <winboll>APK 15.11.14 release Publish. 2026-01-29 17:03:53 +08:00
47f27b6bcb 修复调试工具配置误差 2026-01-29 17:00:59 +08:00
013ad7ff1b <winboll>APK 15.11.13 release Publish. 2026-01-24 21:19:19 +08:00
ab6fd0c6b5 增加对签名证书修改后的证书识别能力。 2026-01-24 21:17:54 +08:00
3c1afc1c0b <winboll>APK 15.11.12 release Publish. 2026-01-24 13:25:26 +08:00
395920743b 更新类库,应用联网验证模块有改进。 2026-01-24 13:23:22 +08:00
25f4c79860 <winboll>APK 15.11.11 release Publish. 2026-01-23 06:00:53 +08:00
5440c1ad39 整理调试菜单配置 2026-01-23 05:52:42 +08:00
47dd4698b8 更新应用介绍窗口 2026-01-23 05:46:09 +08:00
861ccb5832 移除命令行里的文件夹创建命令。 2026-01-19 18:00:33 +08:00
8b1f119e44 更新 WinBoLL工作目录为/sdcard/WinBoLLStudio。添加 bash终端命令。 2026-01-19 17:39:11 +08:00
c6495d1859 Termux 下 Gradle 编译 WinBoLL 项目测试成功。 2026-01-19 15:47:18 +08:00
15197b123c gradle命令调用成功 2026-01-19 15:21:06 +08:00
df2e91d390 Termux 终端显示功能实现。 2026-01-19 14:51:24 +08:00
61ca0d2672 Termux应用包命令调用成功。 2026-01-19 14:43:50 +08:00
24 changed files with 1689 additions and 1207 deletions

236
README.md
View File

@@ -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/
#### ☆ 源码地址:
★ Giteahttps://gitea.winboll.cc/Studio/WinBoLL.git
★ GitHubhttps://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 

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Tue Mar 17 15:21:11 HKT 2026
stageCount=14
#Tue Mar 31 13:51:27 HKT 2026
stageCount=16
libraryProject=
baseVersion=15.12
publishVersion=15.12.13
publishVersion=15.12.15
buildCount=0
baseBetaVersion=15.12.14
baseBetaVersion=15.12.16

View File

@@ -19,6 +19,7 @@ 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;
@@ -27,7 +28,10 @@ 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.MainService;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
/**
@@ -225,26 +229,37 @@ public class LocationActivity extends WinBoLLActivity implements IWinBoLLActivit
// 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) {
YesNoAlertDialog.show(LocationActivity.this, "删除位置提示", "是否删除此项锚点位置?", new YesNoAlertDialog.OnDialogResultListener(){
@Override
public void onNo() {
}
@Override
public void onYes() {
// 安全校验(索引有效+服务绑定+缓存非空)
if (position < 0 || position >= mLocalPosCache.size() || !isServiceBound.get() || mMainService == null) {
LogUtils.w(TAG, "删除位置失败:索引无效/服务未就绪(索引=" + position + ",缓存量=" + mLocalPosCache.size() + "");
return;
}
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() + "(服务+缓存已同步)");
}
}
});
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() + "(服务+缓存已同步)");
}
}
});
@@ -365,6 +380,12 @@ public class LocationActivity extends WinBoLLActivity implements IWinBoLLActivit
// 安全更新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()));
// 设置格式化后的时间字符串
long currentTime = System.currentTimeMillis();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault());
String timeStr = sdf.format(new Date(currentTime));
// 设置当前时间
((TextView)findViewById(R.id.tv_timenow)).setText("现在时间:" + timeStr);
}
@Override

View File

@@ -230,24 +230,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;
}
}
}
/**

View File

@@ -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=当前绑定位置IDfalse=不匹配
*/
// ---------------------- 内部工具 ----------------------
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,302 +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() ? "状态:已启用" : "状态:已禁用");
// 修改为从colors.xml读取颜色
simpleHolder.tvSimpleIsEnable.setTextColor(task.isEnable()
? getContext().getResources().getColor(R.color.colorEnableGreen)
: getContext().getResources().getColor(R.color.colorGrayText));
// 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());
if (mOnTaskUpdatedListener != null && mBindPositionId != null) {
mOnTaskUpdatedListener.onTaskUpdated(mBindPositionId, new ArrayList<>(mAdapterData));
}
showToast("任务已删除");
LogUtils.d(TAG, "Adapter已移除任务刷新列表位置索引=" + position + "");
} catch (Exception e) {
LogUtils.e(TAG, "删除异常: " + e.getMessage(), e);
showToast("删除失败,请重试");
syncTasksFromMainService();
}
}
// 步骤4通知外部如Activity任务已更新
if (mOnTaskUpdatedListener != null && mBindPositionId != null) {
mOnTaskUpdatedListener.onTaskUpdated(mBindPositionId, new ArrayList<PositionTaskModel>(mAdapterData));
}
showToast("任务已删除(已同步至服务)");
} 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小时制对应参数中的 hour0-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) {
@@ -585,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;
@@ -613,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();
}
}
}

View File

@@ -24,14 +24,23 @@
android:layout_centerHorizontal="true"
android:layout_marginTop="30dp"
android:orientation="vertical"
android:gravity="center_horizontal">
android:gravity="left">
<TextView
android:layout_width="wrap_content"
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="实时位置信息"
android:textSize="22sp"
android:textStyle="bold"/>
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_longitude"
@@ -49,6 +58,14 @@
android:textSize="18sp"
android:layout_marginTop="10dp"/>
<TextView
android:id="@+id/tv_timenow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="现在时间:等待更新..."
android:textSize="18sp"
android:layout_marginTop="10dp"/>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView

View File

@@ -13,6 +13,9 @@ WinBoLL 网站浏览器。
阶段版编译命令 bash .winboll/bashPublishAPKAddTag.sh winboll
#### 使用说明
3. Termux应用配置
- 已安装Termux包名 com.termux 
- 执行  echo "allow-external-apps = true" > ~/.termux/termux.properties
#### 参与贡献

View File

@@ -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'

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Mon Jan 19 03:57:58 GMT 2026
stageCount=11
#Mon Mar 16 19:52:21 GMT 2026
stageCount=26
libraryProject=
baseVersion=15.11
publishVersion=15.11.10
buildCount=12
baseBetaVersion=15.11.11
publishVersion=15.11.25
buildCount=29
baseBetaVersion=15.11.26

View File

@@ -2,7 +2,8 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.winboll"
android:sharedUserId="com.termux">
xmlns:tools="http://schemas.android.com/tools"
android:sharedUserId="com.termux">
<!-- 拥有完全的网络访问权限 -->
<uses-permission android:name="android.permission.INTERNET"/>
@@ -12,6 +13,11 @@
<!-- 对正在运行的应用重新排序 -->
<uses-permission android:name="android.permission.REORDER_TASKS"/>
<!-- Android 11+ 查询已安装应用权限 -->
<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"
@@ -285,6 +291,18 @@
<activity android:name="cc.winboll.studio.winboll.unittest.TermuxEnvTestActivity"/>
<activity
android:name=".termux.NfcTermuxBridgeActivity"
android:exported="true"> <!-- 必须设置为 true允许外部应用调用 -->
<!-- 接收 ACTION_BUILD 意图 -->
<intent-filter>
<action android:name="cc.winboll.studio.winboll.termux.NfcTermuxBridgeActivity.ACTION_BUILD" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -23,7 +23,7 @@ public class MainActivity extends DrawerFragmentActivity {
public static final String TAG = "MainActivity";
BrowserFragment mBrowserFragment;
// ------------------- 新增Handler 消息定义接收URL历史更新消息 -------------------
@@ -45,12 +45,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 +122,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,7 +153,7 @@ 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_termux_env_test) {
Intent intent = new Intent(getApplicationContext(), TermuxEnvTestActivity.class);

View File

@@ -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() 配置完成");

View File

@@ -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();
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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
}

View File

@@ -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();
}
});
}
}

View File

@@ -0,0 +1,177 @@
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;
/**
* 执行 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,
"/data/data/com.termux/files/home", // 默认工作目录
false, // 终端会话执行(可见)
null // 不输出到文件
);
}
/**
* 简化方法:后台执行 Termux 命令(无输出文件)
* @param context 上下文
* @param command 命令路径
* @param args 命令参数
* @return 是否成功发送
*/
public static boolean executeBackgroundCommand(Context context, String command, String[] args) {
LogUtils.d(TAG, "调用 executeBackgroundCommandcommand=" + 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;
}
}

View File

@@ -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("sharedUserIdcom.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命令
// -o0stdout无缓冲-e0stderr无缓冲-i0stdin无缓冲
//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命令
// -o0stdout无缓冲-e0stderr无缓冲-i0stdin无缓冲
//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() 执行完成");
}
}

View File

@@ -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>

View File

@@ -1,9 +0,0 @@
<?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"/>
</menu>

View 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>