diff --git a/README.md b/README.md index 0a929c2..5a7755e 100644 --- a/README.md +++ b/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 源码地址 ☁ ☁ ☁ ☁ ☁ ☁ ☁ -# ☁ ☁ ☁ GitHub 源码地址 ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ -# ☁ ☁ ☁ 码云 源码地址 ☁ ☁ ☁ ☁ ☁ ☁ ☁ ☁ -# ☁ ☁ ☁ 在 jitpack.io 托管的 APPBase 类库源码 ☁ ☁ ☁ ☁ -# ☁ ☁ ☁ 在 jitpack.io 托管的 AES 类库源码 ☁ ☁ ☁ ☁ -## WinBoLL 提问 -同样是 /sdcard 目录,在开发 Android 应用时, -能否实现手机编译与电脑编译的源码同步。 -☁因而 WinBoLL 项目组诞生了。 +## 一、项目概述 -## WinBoLL 项目组研发计划 -致力于把 WinBoLL-APP 应用在手机端 Android 项目开发。 -也在探索 https://gitea.winboll.cc//APP.git 应用于 WinBoLL-APP APK 分发。 -更想进阶 https://github.com//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.properties-demo"(WinBoLL 项目已设置) -☁ WinBoLL 项目配置文件为 "/.winboll/winboll.properties" -☁ WinBoLL 项目配置文件设定为源码提交时忽略。(WinBoLL 项目已设置) -☁ Gradle 项目配置文件示例为 "/.winboll/local.properties-demo"(WinBoLL 项目已设置) -☁ Gradle 项目配置文件为 "/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. **优先修改配置文件**:优先修改应用测试项目(目录为 `"/<类库测试应用>/"`)内 `build.properties` 文件,设置对应的类库项目名称:`libraryProject=<类库项目模块名>`。 +#### 2. **编译优先启动步骤**:使用 Termux 应用,进入 `""`,运行 `$ bash .winboll/bashPublishAPKAddTag.sh <类库测试项目模块名>` 命令。运行后可生成测试项目与类库项目的编译参数文件 `build.properties`。生成的 `build.properties` 文件有两份,一份在测试项目模块的文件夹内,一份在类库项目本身的模块文件夹内。 +#### 3. **最后类库编译发布步骤**:使用 Termux 应用,进入 `""`,运行 `$ bash .winboll/bashPublishLIBAddTag.sh <类库项目模块名>` 命令。运行后可发布至 WinBoLL Nexus Maven 库、本地 maven 目录或者是通用默认的 Gradle Maven 库。 -### 若是 winboll.properties 文件的 [ExtraAPKOutputPath] 属性设置了路径。编译器也会复制一份 APK 到这个路径。 +### (2)单一应用型模块与类库测试型模块配置要点 +#### ☆ APK 编译方法: +使用 Termux 应用,进入 `""`,运行 `$ 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 渠道应用发布命令为: +("/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 ) diff --git a/winboll/README.md b/winboll/README.md index 2de1c76..7d41393 100644 --- a/winboll/README.md +++ b/winboll/README.md @@ -13,6 +13,9 @@ WinBoLL 网站浏览器。 阶段版编译命令 :bash .winboll/bashPublishAPKAddTag.sh winboll #### 使用说明 +3. Termux应用配置: +- 已安装Termux(包名 com.termux ); +- 执行  echo "allow-external-apps = true" > ~/.termux/termux.properties #### 参与贡献 diff --git a/winboll/build.gradle b/winboll/build.gradle index dc782b2..04e374a 100644 --- a/winboll/build.gradle +++ b/winboll/build.gradle @@ -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' diff --git a/winboll/build.properties b/winboll/build.properties index 1ddcdbd..0bd5fb1 100644 --- a/winboll/build.properties +++ b/winboll/build.properties @@ -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 diff --git a/winboll/src/main/AndroidManifest.xml b/winboll/src/main/AndroidManifest.xml index 6fac454..1477fdc 100644 --- a/winboll/src/main/AndroidManifest.xml +++ b/winboll/src/main/AndroidManifest.xml @@ -2,7 +2,8 @@ + xmlns:tools="http://schemas.android.com/tools" + android:sharedUserId="com.termux"> @@ -12,6 +13,11 @@ + + + + + + + + + + + + + + diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/MainActivity.java b/winboll/src/main/java/cc/winboll/studio/winboll/MainActivity.java index 356a756..b4a3d84 100644 --- a/winboll/src/main/java/cc/winboll/studio/winboll/MainActivity.java +++ b/winboll/src/main/java/cc/winboll/studio/winboll/MainActivity.java @@ -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); diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/activities/AboutActivity.java b/winboll/src/main/java/cc/winboll/studio/winboll/activities/AboutActivity.java index b1761b4..d7ef2e6 100644 --- a/winboll/src/main/java/cc/winboll/studio/winboll/activities/AboutActivity.java +++ b/winboll/src/main/java/cc/winboll/studio/winboll/activities/AboutActivity.java @@ -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 @@ -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() 配置完成"); diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/activities/BaseWinBoLLActivity.java b/winboll/src/main/java/cc/winboll/studio/winboll/activities/BaseWinBoLLActivity.java index f9edcd4..9d4f407 100644 --- a/winboll/src/main/java/cc/winboll/studio/winboll/activities/BaseWinBoLLActivity.java +++ b/winboll/src/main/java/cc/winboll/studio/winboll/activities/BaseWinBoLLActivity.java @@ -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(); } diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/activities/New2Activity.java b/winboll/src/main/java/cc/winboll/studio/winboll/activities/New2Activity.java index 7b19999..561cf64 100644 --- a/winboll/src/main/java/cc/winboll/studio/winboll/activities/New2Activity.java +++ b/winboll/src/main/java/cc/winboll/studio/winboll/activities/New2Activity.java @@ -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; diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/activities/NewActivity.java b/winboll/src/main/java/cc/winboll/studio/winboll/activities/NewActivity.java index 430a0e9..e3b6530 100644 --- a/winboll/src/main/java/cc/winboll/studio/winboll/activities/NewActivity.java +++ b/winboll/src/main/java/cc/winboll/studio/winboll/activities/NewActivity.java @@ -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; diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/activities/SettingsActivity.java b/winboll/src/main/java/cc/winboll/studio/winboll/activities/SettingsActivity.java index d3bb54e..c7d877d 100644 --- a/winboll/src/main/java/cc/winboll/studio/winboll/activities/SettingsActivity.java +++ b/winboll/src/main/java/cc/winboll/studio/winboll/activities/SettingsActivity.java @@ -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&豆包大模型 @@ -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 diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/models/NfcTermuxCmd.java b/winboll/src/main/java/cc/winboll/studio/winboll/models/NfcTermuxCmd.java new file mode 100644 index 0000000..c0ea16d --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/models/NfcTermuxCmd.java @@ -0,0 +1,14 @@ +package cc.winboll.studio.winboll.models; + +/** + * @Author 豆包&ZhanGSKen + * @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) +} + diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/termux/NfcTermuxBridgeActivity.java b/winboll/src/main/java/cc/winboll/studio/winboll/termux/NfcTermuxBridgeActivity.java new file mode 100644 index 0000000..38544f6 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/termux/NfcTermuxBridgeActivity.java @@ -0,0 +1,241 @@ +/* + * 源码说明与描述: + * NFC 与 Termux 桥接活动,用于接收外部应用(包调用)传递的 JSON 指令并执行 Termux 脚本命令。 + * 支持 ACTION_BUILD(后台执行)与 ACTION_BUILD_VIEW(终端窗口唤起)两种动作。 + * + * 作者:豆包&ZhanGSKen + * 创建时间: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(); + } + }); + } +} + diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/termux/TermuxCommandExecutor.java b/winboll/src/main/java/cc/winboll/studio/winboll/termux/TermuxCommandExecutor.java new file mode 100644 index 0000000..e4f3552 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/termux/TermuxCommandExecutor.java @@ -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 + * @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, "调用 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; + } +} + diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/unittest/TermuxEnvTestActivity.java b/winboll/src/main/java/cc/winboll/studio/winboll/unittest/TermuxEnvTestActivity.java index 94c432a..4b07046 100644 --- a/winboll/src/main/java/cc/winboll/studio/winboll/unittest/TermuxEnvTestActivity.java +++ b/winboll/src/main/java/cc/winboll/studio/winboll/unittest/TermuxEnvTestActivity.java @@ -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 - * @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() 执行完成"); + } +} diff --git a/winboll/src/main/res/layout/activity_termux_env_test.xml b/winboll/src/main/res/layout/activity_termux_env_test.xml index ff442a5..0ca36f3 100644 --- a/winboll/src/main/res/layout/activity_termux_env_test.xml +++ b/winboll/src/main/res/layout/activity_termux_env_test.xml @@ -12,23 +12,39 @@ android:id="@+id/toolbar"/>