diff --git a/README.md b/README.md index 0a929c2..89c3f01 100644 --- a/README.md +++ b/README.md @@ -1,175 +1,98 @@ -## 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 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 分发。 - -## WinBoLL-APP 汗下... -#### ☁应用何置如此呢。且观用户云云。 - -#### ☁ 正当下 ☁ ### -#### ☁ 且容傻家叙说 ☁ WinBoLL-APP 应用场景 -### ☁ WinBoLL 设备资源概述 -#### ☁ 1. Raid Disk. -概述:这是一个矩阵存储类设备。 -优点:该设备具有数据容错存储功能, - 数据存储具有特长持久性。 -缺点:设备使用能源消耗比较高, - 设备存取速度一般。 - -#### ☁ 2. Data Disk. -概述:这是一个普通硬盘存储设备 -优点:该设备独立于操作系统, - 数据持久性一般, - 存取能源消耗小于 Raid Disk。 -缺点:数据存储速度一般,存储能源消耗一般。 - -#### ☁ 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. - -#### ☁ 5. WinBoLL-APP 用户资源概述。 -1> /sdcard 挂载用户手机 SD 存储/storage/emulated/0 - -### ☁ 稍稍歇 ☁ ### -### ☁ 急急停 ☁ 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 项目已设置) - -### ☁ 登高处 ☁ 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 应用已提供) - -### ☁ 看远方 ☁ ### -### ☁ 心忧虑 ☁ 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-APP 应用需求规划 -☁ 如要使用 WinBoLL Android 项目的 Gradle 编译功能,则需要设置以下两个文件夹。 -☁ 1. 则需要建立数据存储目录 /sdcard/WinBoLLStudio/APKs。 - WinBoLL 项目源码编译出来的安装包会拷贝一份到 /sdcard/WinBoLLStudio/APKs 目录下。 -☁ 2. 则需要建立数据存储目录 /sdcard/AppProjects。 - WinBoLL 项目源码编译出来的安装包会拷贝一份并命名 "app.apk" 的安装文件为到 /sdcard/AppProjects 目录下。 - - -### ☁ 吁! ☁ WinBoLL-APP 共享计划前景 -☁ WinBoLL-APP 将会实现 https://winboll.cc/api 访问功能。 -☁ WinBoLL-APP 将会实现手机端 Android 应用的开发与管理功能。 - -## ☁ 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 文件额外输出路径。 - - -# ☆类库型项目编译方法 -## 先编译类库对应的模块测试项目 -### 修改模块测试项目的 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 到这个路径。 - -## ☆应用调试编译方法 -使用以下命令编译调试: - -### Beta调试使用: -$ bash gradlew assembleBetaDebug - -### Stage调试使用: -$ bash gradlew assembleStageDebug - -### 若是 winboll.properties 文件的 [ExtraAPKOutputPath] 属性设置了路径。编译器也会复制一份 APK 到这个路径。 - -# 应用版本号命名方式 -## statge 渠道 -V<应用开发环境编号><应用功能变更号><应用调试阶段号> -如:APPBase_15.7.0 -## beta 渠道 -V<应用开发环境编号><应用功能变更号><应用调试阶段号>-beta<调试编译计数>_<调试编译时间(分钟+秒钟)> -如:APPBase_15.9.6-beta8_5413 +WinBoLL 源生态计划项目说明书 + +一、项目概述 + +1. 核心定位 + +【OriginMaster】WinBoLL 源生态计划,旨在通过核心项目 WinBoLL 联动系列开发库,构建手机端 Android 项目开发与多端编译同步的完整生态,实现手机与电脑的源码同步开发。 + +2. 仓库架构 + +仓库类型 包含仓库 功能说明 +开发库 WinBoLL、APPBase、AES、PowerBell、Positions 核心开发依赖库,其中 WinBoLL 可作为应用开发的基础继承模板 +分支汇总存档库 OriginMaster 仅用于汇总各开发库分支,不适宜作为开发库克隆使用,非应用开发基础库 + +3. 源码推送路径 + +- WinBoLL → APPBase → OriginMaster +- WinBoLL → AES → OriginMaster +- WinBoLL → PowerBell → OriginMaster +- WinBoLL → Positions → OriginMaster + +二、WinBoLL APP 核心信息 + +1. 项目简介 + +WinBoLL Studio Android 应用开源项目,专注于手机端 Android 开发与多端编译同步。 + +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 +- 托管类库源码: +- APPBase(jitpack.io):https://github.com/ZhanGSKen/APPBase.git +- AES(jitpack.io):https://github.com/ZhanGSKen/AES.git + +三、通用特征文件夹前置(/sdcard) + +- Linux 系统文件夹直接使用  /sdcard 。 +- 手机 SD 卡存储( /storage/emulated/0 )挂载的别名也可为  /sdcard 。 + +四、前置条件 + +1. WinBoLL-APP 配置 + +- APK 编译输出目录: /sdcard/WinBoLLStudio/APKs/ ,以及  /sdcard/AppProjects/ (命名为  app.apk ) +- 签名与命名空间:支持应用签名验证定制化,与衍生 APP 共享  cc.winboll.studio  命名空间 + +五、核心需求规划 + +1. 主机端需求 + +- 支持  winboll.cc  域名的用户注册登录服务 +- 支持  https://console.winboll.cc/api  访问 + +2. APP 端需求 + +- 实现手机端 Android 应用开发与管理功能 + +六、编译与使用指南 + +1. 项目初始化(必须) + +1. 复制  settings.gradle-demo  为  settings.gradle ,取消对应项目模块注释 +2. 复制  gradle.properties-androidx-demo  或  gradle.properties-android-demo  为  gradle.properties  +3. (可选)复制  local.properties-demo  为  local.properties ,配置 Android SDK 目录 +4. 签名设置: +- 调试编译:进入 GenKeyStore 目录执行  bash gen_debug_keystore.sh  +- 非必须:clone keystore 模块,拷贝  appkey.jks  与  appkey.keystore  到项目根目录 + +2. 编译命令 + +(1)类库型项目 + +1. 修改测试项目  build.properties ,设置  libraryProject=<类库项目模块名>  +2. 编译测试项目: bash .winboll/bashPublishAPKAddTag.sh <应用项目模块名>  +3. 编译类库项目: bash .winboll/bashPublishLIBAddTag.sh <类库项目模块名> (发布至 WinBoLL Nexus Maven 库) + +(2)应用型项目 + +- 编译命令: bash .winboll/bashPublishAPKAddTag.sh <应用项目模块名>  + +(3)调试编译 + +- Beta 调试: bash gradlew assembleBetaDebug  +- Stage 调试: bash gradlew assembleStageDebug  + +3. 编译输出路径 + +- 默认路径: /sdcard/WinBoLLStudio/APKs/<项目根目录名称>/tag/  +- 额外路径:若  winboll.properties  配置  ExtraAPKOutputPath ,APK 同步拷贝至该ExtraAPKOutputPath路径 + +4. 版本号命名规则 + +- 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 820fc34..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,14 +89,31 @@ 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.12.13' - api 'cc.winboll.studio:libappbase:15.14.2' + 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.12.9' - //api 'com.github.ZhanGSKen:APPBase:appbase-v15.14.1' + //api 'com.github.ZhanGSKen:AES:aes-v15.15.7' + //api 'com.github.ZhanGSKen:APPBase:appbase-v15.15.4' api fileTree(dir: 'libs', include: ['*.jar']) } diff --git a/winboll/build.properties b/winboll/build.properties index 844e6ff..dd56b55 100644 --- a/winboll/build.properties +++ b/winboll/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Mon Jan 12 05:14:13 GMT 2026 -stageCount=9 +#Thu Jan 29 17:03:53 HKT 2026 +stageCount=15 libraryProject= baseVersion=15.11 -publishVersion=15.11.8 -buildCount=15 -baseBetaVersion=15.11.9 +publishVersion=15.11.14 +buildCount=0 +baseBetaVersion=15.11.15 diff --git a/winboll/src/main/AndroidManifest.xml b/winboll/src/main/AndroidManifest.xml index 8236f00..74b3ee5 100644 --- a/winboll/src/main/AndroidManifest.xml +++ b/winboll/src/main/AndroidManifest.xml @@ -1,7 +1,9 @@ + package="cc.winboll.studio.winboll" + xmlns:tools="http://schemas.android.com/tools" + android:sharedUserId="com.termux"> @@ -11,7 +13,12 @@ - + + + + + + + - \ No newline at end of file + diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/CustomToolbar.java b/winboll/src/main/java/cc/winboll/studio/winboll/CustomToolbar.java deleted file mode 100644 index d9d255b..0000000 --- a/winboll/src/main/java/cc/winboll/studio/winboll/CustomToolbar.java +++ /dev/null @@ -1,53 +0,0 @@ -package cc.winboll.studio.winboll; - -/** - * @Author ZhanGSKen - * @Date 2025/05/22 13:08 - */ -import android.content.Context; -import android.content.res.TypedArray; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.TextView; -import androidx.annotation.Nullable; -import androidx.appcompat.widget.Toolbar; - -public class CustomToolbar extends Toolbar { - - private View viewMain; - - public CustomToolbar(Context context) { - this(context, null); - } - - public CustomToolbar(Context context, @Nullable AttributeSet attrs) { - this(context, attrs, 0); - } - - public CustomToolbar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - initView(context, attrs); - } - - private void initView(Context context, AttributeSet attrs) { - TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomToolbar); - - // 获取属性值 - String toolbarTitle = typedArray.getString(R.styleable.CustomToolbar_toolbarTitle); - int toolbarTitleColor = typedArray.getColor(R.styleable.CustomToolbar_toolbarTitleColor, android.graphics.Color.WHITE); - int toolbarBackgroundColor = typedArray.getColor(R.styleable.CustomToolbar_toolbarBackgroundColor, android.graphics.Color.BLUE); - - // 加载布局 - viewMain = LayoutInflater.from(context).inflate(R.layout.view_toolbar, this, true); - - // 应用属性值 - TextView toolbarTitleTextView = viewMain.findViewById(R.id.toolbar_title); - toolbarTitleTextView.setText(toolbarTitle); - toolbarTitleTextView.setTextColor(toolbarTitleColor); - viewMain.setBackgroundColor(toolbarBackgroundColor); - - // 释放 TypedArray - typedArray.recycle(); - } -} 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 935b86f..b4a3d84 100644 --- a/winboll/src/main/java/cc/winboll/studio/winboll/MainActivity.java +++ b/winboll/src/main/java/cc/winboll/studio/winboll/MainActivity.java @@ -8,16 +8,15 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; -import cc.winboll.studio.libaes.activitys.AboutActivity; import cc.winboll.studio.libaes.activitys.DrawerFragmentActivity; -import cc.winboll.studio.libaes.models.APPInfo; import cc.winboll.studio.libaes.models.DrawerMenuBean; import cc.winboll.studio.libaes.utils.WinBoLLActivityManager; import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.winboll.R; +import cc.winboll.studio.winboll.activities.AboutActivity; import cc.winboll.studio.winboll.activities.SettingsActivity; -import cc.winboll.studio.winboll.activities.WXPayActivity; import cc.winboll.studio.winboll.fragments.BrowserFragment; +import cc.winboll.studio.winboll.unittest.TermuxEnvTestActivity; import java.util.ArrayList; public class MainActivity extends DrawerFragmentActivity { @@ -123,6 +122,9 @@ public class MainActivity extends DrawerFragmentActivity { @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.toolbar_main, menu); + if (App.isDebugging()) { + getMenuInflater().inflate(R.menu.toolbar_test, menu); + } return super.onCreateOptionsMenu(menu); } @@ -149,12 +151,13 @@ public class MainActivity extends DrawerFragmentActivity { } } else if (nItemId == R.id.item_settings) { WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), SettingsActivity.class); - } else if (nItemId == R.id.item_wxpayactivity) { - WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), WXPayActivity.class); - } else if (nItemId == cc.winboll.studio.libaes.R.id.item_about) { + } else if (nItemId == R.id.item_about) { Intent intent = new Intent(getApplicationContext(), AboutActivity.class); - APPInfo appInfo = genDefaultAPPInfo(); - intent.putExtra(AboutActivity.EXTRA_APPINFO, appInfo); + + WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), intent, AboutActivity.class); + } else if (nItemId == R.id.item_termux_env_test) { + Intent intent = new Intent(getApplicationContext(), TermuxEnvTestActivity.class); + WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), intent, AboutActivity.class); } else { return super.onOptionsItemSelected(item); @@ -163,23 +166,6 @@ public class MainActivity extends DrawerFragmentActivity { return true; } - - APPInfo genDefaultAPPInfo() { - String szBranchName = "winboll"; - APPInfo appInfo = new APPInfo(); - appInfo.setAppName("WinBoLL"); - appInfo.setAppIcon(cc.winboll.studio.libaes.R.drawable.ic_winboll); - appInfo.setAppDescription("WinBoLL 网站浏览器。"); - appInfo.setAppGitName("WinBoLL"); - appInfo.setAppGitOwner("Studio"); - appInfo.setAppGitAPPBranch(szBranchName); - appInfo.setAppGitAPPSubProjectFolder(szBranchName); - appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=WinBoLL"); - appInfo.setAppAPKName("WinBoLL"); - appInfo.setAppAPKFolderName("WinBoLL"); - return appInfo; - } - // ------------------- 新增:对外提供Handler(供其他组件发送消息,如BrowserFragment) ------------------- public Handler getMainHandler() { return _mMainHandler; 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 new file mode 100644 index 0000000..d7ef2e6 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/activities/AboutActivity.java @@ -0,0 +1,86 @@ +package cc.winboll.studio.winboll.activities; + +import android.os.Bundle; +import android.view.View; +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.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 + * @Date 2026/01/13 11:54 + * @Describe 应用介绍窗口 + */ +public class AboutActivity extends BaseWinBoLLActivity { + + @Override + public Activity getActivity() { + return this; + } + + + public static final String TAG = "AboutActivity"; + + private Toolbar mToolbar; + + @Override + public String getTag() { + return TAG; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_about); + + // 设置工具栏 + initToolbar(); + + AboutView aboutView = getActivity().findViewById(R.id.aboutview); + aboutView.setAPPInfo(genDefaultAppInfo()); + } + + 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(); + } + }); + LogUtils.d(TAG, "initToolbar() 配置完成"); + } + + private APPInfo genDefaultAppInfo() { + LogUtils.d(TAG, "genDefaultAppInfo() 调用"); + String branchName = "winboll"; + APPInfo appInfo = new APPInfo(); + appInfo.setAppName(getActivity().getString(R.string.app_name)); + appInfo.setAppIcon(R.drawable.ic_winboll); + appInfo.setAppDescription(getActivity().getString(R.string.app_description)); + appInfo.setAppGitName("WinBoLL"); + appInfo.setAppGitOwner("Studio"); + appInfo.setAppGitAPPBranch(branchName); + appInfo.setAppGitAPPSubProjectFolder(branchName); + appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=WinBoLL"); + appInfo.setAppAPKName("WinBoLL"); + appInfo.setAppAPKFolderName("WinBoLL"); + LogUtils.d(TAG, "genDefaultAppInfo: 应用信息已生成"); + return appInfo; + } +} 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 new file mode 100644 index 0000000..9d4f407 --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/activities/BaseWinBoLLActivity.java @@ -0,0 +1,56 @@ +package cc.winboll.studio.winboll.activities; + +import android.app.Activity; +import android.os.Bundle; +import androidx.appcompat.app.AppCompatActivity; +import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity; +import cc.winboll.studio.libaes.models.AESThemeBean; +import cc.winboll.studio.libaes.utils.AESThemeUtil; +import cc.winboll.studio.libaes.utils.WinBoLLActivityManager; + +/** + * @Author 豆包&ZhanGSKen + * @Date 2026/01/13 16:33 + * BaseWinBollActivity 【继承AppCompatActivity,保留核心能力,不额外暴露方法】 + * 继承链路:BaseWinBoLLActivity → AppCompatActivity → FragmentActivity,AppCompat能力天然继承可用 + */ +public abstract class BaseWinBoLLActivity extends AppCompatActivity implements IWinBoLLActivity { + public static final String TAG = "BaseWinBoLLActivity"; + + protected volatile AESThemeBean.ThemeType mThemeType; + + @Override + protected void onCreate(Bundle savedInstanceState) { + mThemeType = getThemeType(); + setThemeStyle(); + super.onCreate(savedInstanceState); + WinBoLLActivityManager.getInstance().add(this); + //ToastUtils.show(getTag() + ": onCreate"); + } + + AESThemeBean.ThemeType getThemeType() { + /*SharedPreferences sharedPreferences = getSharedPreferences( + SHAREDPREFERENCES_NAME, MODE_PRIVATE); + return AESThemeBean.ThemeType.values()[((sharedPreferences.getInt(DRAWER_THEME_TYPE, AESThemeBean.ThemeType.DEFAULT.ordinal())))]; + */ + return AESThemeBean.getThemeStyleType(AESThemeUtil.getThemeTypeID(getApplicationContext())); + } + + void setThemeStyle() { + //setTheme(AESThemeBean.getThemeStyle(getThemeType())); + setTheme(AESThemeUtil.getThemeTypeID(getApplicationContext())); + } + + @Override + protected void onDestroy() { + WinBoLLActivityManager.getInstance().registeRemove(this); + super.onDestroy(); + } + + // 子类必须实现getTag(),确保唯一标识 + @Override + public abstract String getTag(); + + 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 21620b5..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 @@ -5,20 +5,32 @@ package cc.winboll.studio.winboll.activities; * @Date 2025/03/25 11:46:40 * @Describe 测试窗口2 */ -import android.app.Activity; import android.os.Bundle; -import android.view.Menu; -import android.view.MenuItem; import android.view.View; -import android.widget.Toolbar; +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 class New2Activity extends WinBoLLActivity { public static final String TAG = "New2Activity"; Toolbar mToolbar; + + @Override + public String getTag() { + return TAG; + } + //LogView mLogView; + + @Override protected void onCreate(Bundle savedInstanceState) { @@ -28,15 +40,10 @@ public class New2Activity extends WinBoLLActivity { // mLogView = findViewById(R.id.logview); // mLogView.start(); mToolbar = findViewById(R.id.toolbar); - setActionBar(mToolbar); + setSupportActionBar(mToolbar); } - @Override - protected void onResume() { - super.onResume(); - //mLogView.start(); - } public void onCloseThisActivity(View view) { //WinBoLLActivityManager.getInstance().finish(this); @@ -49,17 +56,4 @@ public class New2Activity extends WinBoLLActivity { public void onNewActivity(View view) { //WinBoLLActivityManager.getInstance().startWinBoLLActivity(this, NewActivity.class); } - - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - //getMenuInflater().inflate(R.menu.toolbar_main, menu); - return super.onCreateOptionsMenu(menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // 在switch语句中处理每个ID,并在处理完后返回true,未处理的情况返回false。 - return super.onOptionsItemSelected(item); - } } 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 a120f7b..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 @@ -5,29 +5,26 @@ package cc.winboll.studio.winboll.activities; * @Date 2025/03/25 05:04:22 * @Describe */ -import android.app.Activity; import android.os.Bundle; -import android.view.Menu; -import android.view.MenuItem; import android.view.View; -import android.widget.Toolbar; -import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity; +import androidx.appcompat.widget.Toolbar; import cc.winboll.studio.libaes.utils.WinBoLLActivityManager; import cc.winboll.studio.winboll.R; -import cc.winboll.studio.winboll.App; +import android.app.Activity; + +public class NewActivity extends BaseWinBoLLActivity { + + @Override + public Activity getActivity() { + return this; + } -public class NewActivity extends WinBoLLActivity implements IWinBoLLActivity { public static final String TAG = "NewActivity"; Toolbar mToolbar; //LogView mLogView; - @Override - public Activity getActivity() { - return this; - } - @Override public String getTag() { return TAG; @@ -40,15 +37,10 @@ public class NewActivity extends WinBoLLActivity implements IWinBoLLActivity { // mLogView = findViewById(R.id.logview); // mLogView.start(); mToolbar = findViewById(R.id.toolbar); - setActionBar(mToolbar); + setSupportActionBar(mToolbar); } - @Override - protected void onResume() { - super.onResume(); - //mLogView.start(); - } public void onCloseThisActivity(View view) { WinBoLLActivityManager.getInstance().finish(this); @@ -61,17 +53,4 @@ public class NewActivity extends WinBoLLActivity implements IWinBoLLActivity { public void onNew2Activity(View view) { // WinBoLLActivityManager.getInstance().startWinBoLLActivity(App.getInstance(), New2Activity.class); } - - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - //getMenuInflater().inflate(R.menu.toolbar_main, menu); - return super.onCreateOptionsMenu(menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // 在switch语句中处理每个ID,并在处理完后返回true,未处理的情况返回false。 - return super.onOptionsItemSelected(item); - } } 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 19a2908..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 @@ -1,27 +1,27 @@ package cc.winboll.studio.winboll.activities; -import android.app.Activity; import android.os.Bundle; import android.view.View; import androidx.appcompat.widget.Toolbar; -import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity; import cc.winboll.studio.libaes.utils.AESThemeUtil; import cc.winboll.studio.winboll.R; +import android.app.Activity; /** * @Author ZhanGSKen&豆包大模型 * @Date 2025/12/05 18:48 * @Describe Settings Activity */ -public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivity { - - public static final String TAG = "SettingsActivity"; +public class SettingsActivity extends BaseWinBoLLActivity { @Override public Activity getActivity() { return this; } + + public static final String TAG = "SettingsActivity"; + @Override public String getTag() { return TAG; diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/activities/WXPayActivity.java b/winboll/src/main/java/cc/winboll/studio/winboll/activities/WXPayActivity.java deleted file mode 100644 index e881993..0000000 --- a/winboll/src/main/java/cc/winboll/studio/winboll/activities/WXPayActivity.java +++ /dev/null @@ -1,211 +0,0 @@ -package cc.winboll.studio.winboll.activities; - -import android.app.Activity; -import android.graphics.Bitmap; -import android.os.Bundle; -import android.os.Handler; -import android.os.Message; -import android.view.View; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.TextView; -import android.widget.Toast; -import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity; -import cc.winboll.studio.winboll.R; -import cc.winboll.studio.winboll.WxPayConfig; -import cc.winboll.studio.winboll.utils.SpecUtil; -import cc.winboll.studio.winboll.utils.WxPayApi; -import cc.winboll.studio.winboll.utils.ZXingUtils; -import java.util.Timer; -import java.util.TimerTask; - - - -/** - * 主界面:生成二维码 + 轮询查询支付结果 - * @Author ZhanGSKen - * @Date 2026/01/07 - */ -public class WXPayActivity extends WinBoLLActivity implements IWinBoLLActivity { - - private static final String TAG = "WXPayActivity"; - - // Handler消息标识 - private static final int MSG_POLL_TIMEOUT = 1001; - private static final int MSG_POLL_SUCCESS = 1002; - private static final int MSG_POLL_FAILED = 1003; - - private ImageView mIvQrCode; - private TextView mTvOrderNo; - private TextView mTvPayStatus; - private Button mBtnCreateOrder; - - private String mOutTradeNo; // 商户订单号 - private Timer mPollTimer; // 轮询定时器 - private long mPollStartTime; // 轮询开始时间 - - - - @Override - public Activity getActivity() { - return this; - } - - @Override - public String getTag() { - return TAG; - } - - private Handler mPollHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - super.handleMessage(msg); - switch (msg.what) { - case MSG_POLL_TIMEOUT: - stopPoll(); - mTvPayStatus.setText("支付状态:轮询超时"); - mTvPayStatus.setTextColor(getResources().getColor(android.R.color.darker_gray)); - Toast.makeText(WXPayActivity.this, "轮询超时,请手动查询", Toast.LENGTH_SHORT).show(); - break; - case MSG_POLL_SUCCESS: - boolean isPaySuccess = (boolean) msg.obj; - String tradeState = (String) msg.getData().getString("tradeState"); - stopPoll(); - if (isPaySuccess) { - mTvPayStatus.setText("支付状态:支付成功 ✅"); - mTvPayStatus.setTextColor(getResources().getColor(android.R.color.holo_green_dark)); - Toast.makeText(WXPayActivity.this, "支付成功!", Toast.LENGTH_SHORT).show(); - } else { - mTvPayStatus.setText("支付状态:" + tradeState); - mTvPayStatus.setTextColor(getResources().getColor(android.R.color.holo_red_dark)); - } - break; - case MSG_POLL_FAILED: - String errorMsg = (String) msg.obj; - mTvPayStatus.setText("查询失败:" + errorMsg); - break; - } - } - }; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_wxpay); - - initView(); - initListener(); - } - - private void initView() { - mIvQrCode = findViewById(R.id.iv_qrcode); - mTvOrderNo = findViewById(R.id.tv_order_no); - mTvPayStatus = findViewById(R.id.tv_pay_status); - mBtnCreateOrder = findViewById(R.id.btn_create_order); - } - - private void initListener() { - mBtnCreateOrder.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - createOrder(); - } - }); - } - - /** - * 统一下单,生成二维码 - */ - private void createOrder() { - mBtnCreateOrder.setEnabled(false); - mTvPayStatus.setText("支付状态:生成订单中..."); - mIvQrCode.setImageBitmap(null); - - WxPayApi.createOrder(new WxPayApi.OnCreateOrderCallback() { - @Override - public void onSuccess(String outTradeNo, String codeUrl) { - mOutTradeNo = outTradeNo; - mTvOrderNo.setText("商户订单号:" + outTradeNo); - mTvPayStatus.setText("支付状态:未支付,请扫码"); - - // 生成二维码 - Bitmap qrCodeBitmap = ZXingUtils.createQRCodeBitmap(codeUrl, 250, 250); - if (qrCodeBitmap != null) { - mIvQrCode.setImageBitmap(qrCodeBitmap); - // 开始轮询查询支付结果 - startPoll(); - } else { - mTvPayStatus.setText("支付状态:生成二维码失败"); - mBtnCreateOrder.setEnabled(true); - } - } - - @Override - public void onFailure(String errorMsg) { - SpecUtil.WWSpecLogError(TAG, "统一下单失败:" + errorMsg); - mTvPayStatus.setText("生成订单失败:" + errorMsg); - mBtnCreateOrder.setEnabled(true); - Toast.makeText(WXPayActivity.this, errorMsg, Toast.LENGTH_SHORT).show(); - } - }); - } - - /** - * 开始轮询查询支付结果 - */ - private void startPoll() { - stopPoll(); // 先停止之前的轮询 - mPollStartTime = System.currentTimeMillis(); - mPollTimer = new Timer(); - mPollTimer.scheduleAtFixedRate(new TimerTask() { - @Override - public void run() { - // 检查是否超时 - long elapsedTime = System.currentTimeMillis() - mPollStartTime; - if (elapsedTime >= WxPayConfig.POLL_TIMEOUT) { - mPollHandler.sendEmptyMessage(MSG_POLL_TIMEOUT); - return; - } - - // 查询订单状态 - WxPayApi.queryOrder(mOutTradeNo, new WxPayApi.OnQueryOrderCallback() { - @Override - public void onSuccess(boolean isPaySuccess, String tradeState) { - Message msg = Message.obtain(); - msg.what = MSG_POLL_SUCCESS; - msg.obj = isPaySuccess; - Bundle bundle = new Bundle(); - bundle.putString("tradeState", tradeState); - msg.setData(bundle); - mPollHandler.sendMessage(msg); - } - - @Override - public void onFailure(String errorMsg) { - Message msg = Message.obtain(); - msg.what = MSG_POLL_FAILED; - msg.obj = errorMsg; - mPollHandler.sendMessage(msg); - } - }); - } - }, 0, WxPayConfig.POLL_INTERVAL); - } - - /** - * 停止轮询 - */ - private void stopPoll() { - if (mPollTimer != null) { - mPollTimer.cancel(); - mPollTimer = null; - } - } - - @Override - protected void onDestroy() { - super.onDestroy(); - stopPoll(); - } -} - diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/activities/WinBoLLActivity.java b/winboll/src/main/java/cc/winboll/studio/winboll/activities/WinBoLLActivity.java deleted file mode 100644 index 8f47d37..0000000 --- a/winboll/src/main/java/cc/winboll/studio/winboll/activities/WinBoLLActivity.java +++ /dev/null @@ -1,47 +0,0 @@ -package cc.winboll.studio.winboll.activities; - -/** - * @Author ZhanGSKen - * @Date 2025/05/10 09:48 - * @Describe WinBoLL 窗口基础类 - */ -import android.app.Activity; -import android.os.Bundle; -import android.view.MenuItem; -import androidx.appcompat.app.AppCompatActivity; -import cc.winboll.studio.libappbase.LogUtils; - -public class WinBoLLActivity extends AppCompatActivity { - - public static final String TAG = "WinBoLLActivity"; - - @Override - protected void onResume() { - super.onResume(); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - /*if (item.getItemId() == R.id.item_log) { - WinBoLLActivityManager.getInstance().startLogActivity(this); - return true; - } else if (item.getItemId() == R.id.item_home) { - startActivity(new Intent(this, MainActivity.class)); - return true; - }*/ - // 在switch语句中处理每个ID,并在处理完后返回true,未处理的情况返回false。 - return super.onOptionsItemSelected(item); - } - - @Override - protected void onPostCreate(Bundle savedInstanceState) { - super.onPostCreate(savedInstanceState); - //WinBoLLActivityManager.getInstance().add(this); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - //WinBoLLActivityManager.getInstance().registeRemove(this); - } -} 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 new file mode 100644 index 0000000..16652ed --- /dev/null +++ b/winboll/src/main/java/cc/winboll/studio/winboll/unittest/TermuxEnvTestActivity.java @@ -0,0 +1,444 @@ +package cc.winboll.studio.winboll.unittest; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.View; +import android.widget.TextView; +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.winboll.MainActivity; +import cc.winboll.studio.winboll.R; +import cc.winboll.studio.winboll.activities.BaseWinBoLLActivity; +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; +import android.app.Activity; + +/** + * @Author 豆包&ZhanGSKen + * @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 { + + @Override + public Activity getActivity() { + return this; + } + + // 常量属性(置顶排列) + public static final String TAG = "TermuxEnvTestActivity"; + private static final String TERMUX_HOME_PATH = "/sdcard/WinBoLLStudio"; + private static final String CMD_RESULT_FILE = TERMUX_HOME_PATH + "/.authcenter_cmd_result.tmp"; + + // 成员属性(常量后排列) + private Toolbar mToolbar; + private TextView tvMessage; + private Handler mainHandler; + + @Override + public String getTag() { + return TAG; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + LogUtils.d(TAG, "onCreate() 调用,初始化Activity"); + setContentView(R.layout.activity_termux_env_test); + initView(); + initToolbar(); + initTermuxDirectory(); + LogUtils.d(TAG, "onCreate() 执行完成"); + } + + /** + * 初始化视图组件 + */ + 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 onTestTermuxCMD(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/WinBoLL"; // 项目目录 + + // 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 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 + "/Sources/WinBoLL"; // 项目目录 + + // 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(); + + // 基础校验 + if (!termuxHomeDir.exists()) { + LogUtils.e(TAG, "readTermuxHomeFileList() 错误:目录不存在"); + return "错误:Termux目录不存在 → " + TERMUX_HOME_PATH; + } + if (!termuxHomeDir.isDirectory()) { + LogUtils.e(TAG, "readTermuxHomeFileList() 错误:指定路径不是目录"); + return "错误:指定路径不是目录 → " + TERMUX_HOME_PATH; + } + if (!termuxHomeDir.canRead()) { + LogUtils.e(TAG, "readTermuxHomeFileList() 错误:无目录读取权限"); + return "错误:无目录读取权限(需满足:1.sharedUserId=com.termux 2.设备root)"; + } + + // 获取文件列表 + File[] files = termuxHomeDir.listFiles(); + if (files == null || files.length == 0) { + LogUtils.d(TAG, "readTermuxHomeFileList() 目录为空"); + return "Termux目录为空 → " + TERMUX_HOME_PATH; + } + + // 拼接结果字符串 + 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(); + 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"); + } + + 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; + } + + @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/java/cc/winboll/studio/winboll/unittest/TestWeWorkSpecSDK.java b/winboll/src/main/java/cc/winboll/studio/winboll/unittest/TestWeWorkSpecSDK.java index 5fdac27..08aa2a1 100644 --- a/winboll/src/main/java/cc/winboll/studio/winboll/unittest/TestWeWorkSpecSDK.java +++ b/winboll/src/main/java/cc/winboll/studio/winboll/unittest/TestWeWorkSpecSDK.java @@ -7,10 +7,10 @@ import android.os.Message; import android.view.View; import android.widget.Button; import android.widget.Toast; +import androidx.appcompat.app.AppCompatActivity; import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity; import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.winboll.R; -import cc.winboll.studio.winboll.activities.WinBoLLActivity; /** * @Author 豆包&ZhanGSKen @@ -18,7 +18,7 @@ import cc.winboll.studio.winboll.activities.WinBoLLActivity; * @Describe 企业微信SDK接口测试(基础调试版) * 包含:SDK初始化、基础接口调用、日志输出、主线程回调处理 */ -public class TestWeWorkSpecSDK extends WinBoLLActivity implements IWinBoLLActivity, View.OnClickListener { +public class TestWeWorkSpecSDK extends AppCompatActivity implements IWinBoLLActivity, View.OnClickListener { public static final String TAG = "TestWeWorkSpecSDK"; diff --git a/winboll/src/main/java/com/tencent/wework/SpecCallbackSDK.java b/winboll/src/main/java/com/tencent/wework/SpecCallbackSDK.java deleted file mode 100644 index 18faee5..0000000 --- a/winboll/src/main/java/com/tencent/wework/SpecCallbackSDK.java +++ /dev/null @@ -1,210 +0,0 @@ -package com.tencent.wework; - -import java.util.Map; -import java.util.HashMap;; - -/** - * @warning: 1. 不要修改成员变量名,native方法内有反射调用 - * 2. 调用本地方法需保持包结构,本工具需放在包com.tencent.wework内 - * 3. 不允许继承,类名和函数名均不可修改,会影响本地方法的引用,详见:javah生成本地方法头文件 - */ -public final class SpecCallbackSDK { - - /** - * @description 调用本地方法后实例化的对象指针 - */ - private long specCallbackSDKptr = 0; - - public long GetPtr() { return specCallbackSDKptr; } - - /** - * @description: 回包的headers - */ - private Map responseHeaders; - - public Map GetResponseHeaders() { return responseHeaders; } - - /** - * @description: 回包的加密后的body - */ - private String responseBody; - - public String GetResponseBody() { return responseBody; } - - /** - * @description: 每个请求构造一个SpecCallbackSDK示例, - * SpecCallbackSDK仅持有headers和body的引用, - * 因此需保证headers和body的生存期比SpecCallbackSDK长 - * @param method: 请求方法GET/POST - * @param headers: 请求header - * @param body: 请求body - * @example: - * SpecCallbackSDK sdk = new SpecCallbackSDK(method, headers, body); - * if (sdk.IsOk()) { - * String corpid = sdk.GetCorpId(); - * String agentid = sdk.GetAgentId(); - * String call_type = sdk.GetCallType(); - * String data = sdk.GetData(); - * //do something... - * } - * String response = ...; - * sdk.BuildResponseHeaderBody(response); - * Map responseHeaders = sdk.GetResponseHeaders(); - * String body = sdk.GetResponseBody(); - * //do response - * - * @return errorcode 示例如下: - * -920001: 未设置请求方法 - * -920002: 未设置请求header - * -920003: 未设置请求body - * */ - public SpecCallbackSDK(String method, Map headers, String body) { - try { - specCallbackSDKptr = NewCallbackSDK(method, headers, body); - } catch (Exception e) { - SpecUtil.WWSpecLogError("SpecCallbackSDK exception caught", e.getMessage()); - } - } - - private native long NewCallbackSDK(String method, Map headers, String body); - - /** - * @usage 在Java对象的内存回收前析构C++对象 - */ - @Override - protected void finalize() throws Throwable { - DeleteCPPInstance(specCallbackSDKptr); - super.finalize(); - } - - private native void DeleteCPPInstance(long specCallbackSDKptr); - - /** - * @description: 判断构造函数中传入的请求是否解析成功 - * @return: 成功与否 - * */ - public boolean IsOk() { - return IsOk(specCallbackSDKptr); - } - - private native boolean IsOk(long specCallbackSDKptr); - - /** - * @description: 获取请求的企业 - * @require: 仅当IsOk() == true可调用 - * @return: corpid - * */ - public String GetCorpId() { - return GetCorpId(specCallbackSDKptr); - } - - private native String GetCorpId(long specCallbackSDKptr); - - /** - * @description: 获取请求的应用 - * @require: 仅当IsOk() == true可调用 - * @return: agentid - * */ - public long GetAgentId() { - return GetAgentId(specCallbackSDKptr); - } - - private native long GetAgentId(long specCallbackSDKptr); - - /** - * @description: 获取请求的类型 - * @require: 仅当IsOk() == true可调用 - * @return: 1 - 来自[应用调用专区]的请求 - * 2 - 来自企业微信的回调事件 - * */ - public long GetCallType() { - return GetCallType(specCallbackSDKptr); - } - - private native long GetCallType(long specCallbackSDKptr); - - /** - * @description: 获取请求数据 - * @require: 仅当IsOk() == true可调用 - * @return: 请求数据,根据call_type可能是: - * - 企业微信回调事件 - * - [应用调用专区]接口中的request_data - * */ - public String GetData() { - return GetData(specCallbackSDKptr); - } - - private native String GetData(long specCallbackSDKptr); - - /** - * @description: 是否异步请求 - * @require: 仅当IsOk() == true可调用 - * @return: 是否异步请求 - * */ - public boolean GetIsAsync() { - return GetIsAsync(specCallbackSDKptr); - } - - private native boolean GetIsAsync(long specCallbackSDKptr); - - /** - * @description: 获取请求的job_info, - * @require: 仅当IsOk() == true可调用 - * @return: job_info,无需理解内容, - * 在同一个请求上下文中使用SpecSDK的时候传入 - * */ - public String GetJobInfo() { - return GetJobInfo(specCallbackSDKptr); - } - - private native String GetJobInfo(long specCallbackSDKptr); - - /** - * @description: 获取请求的ability_id,[应用调用专区]接口时指定 - * @require: 仅当IsOk() == true可调用 - * @return: ability_id - * */ - public String GetAbilityId() { - return GetAbilityId(specCallbackSDKptr); - } - - private native String GetAbilityId(long specCallbackSDKptr); - - /** - * @description: 获取请求的notify_id,用于[应用同步调用专区程序]接口 - * @require: 仅当IsOk() == true可调用 - * @return: notify_id - * */ - public String GetNotifyId() { - return GetNotifyId(specCallbackSDKptr); - } - - private native String GetNotifyId(long specCallbackSDKptr); - - /** - * @description: 对返回包计算签名&加密 - * @param response: 待加密的回包明文.如果IsOk()==false,传入空串即可 - * @note 本接口的执行问题可查看日志 - * */ - public void BuildResponseHeaderBody(String response) { - try { - responseHeaders = new HashMap(); - responseBody = ""; - BuildResponseHeaderBody(specCallbackSDKptr, response); - } catch (Exception e) { - SpecUtil.WWSpecLogError("SpecCallbackSDK exception caught", e.getMessage()); - } - } - - private native void BuildResponseHeaderBody(long specCallbackSDKptr, String response); - - // 静态代码块内还无法调用native日志函数,这里的日志在管理系统无法查询 - static { - try { - Class.forName("com.tencent.wework.SpecUtil"); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - System.exit(1); - } - } -} diff --git a/winboll/src/main/java/com/tencent/wework/SpecSDK.java b/winboll/src/main/java/com/tencent/wework/SpecSDK.java deleted file mode 100644 index 8cf555a..0000000 --- a/winboll/src/main/java/com/tencent/wework/SpecSDK.java +++ /dev/null @@ -1,163 +0,0 @@ -package com.tencent.wework; - -/** - * @warning: 1. 不要修改成员变量名,native方法内有反射调用 - * 2. 调用本地方法需保持包结构,本工具需放在包com.tencent.wework内 - * 3. 不允许继承,类名和函数名均不可修改,会影响本地方法的引用,详见:javah生成本地方法头文件 - */ -public final class SpecSDK { - - /** - * @description 调用本地方法后实例化的对象指针 - */ - private long specSDKptr = 0; - - /** - * @usage invoke的请求 - * @example "{\"limit\":1} - */ - private String request; - - public void SetRequest(String request) { - this.request = request; - } - - /** - * @usage 访问上一次invoke的结果 - */ - private String response; - - public String GetResponse() { - return response; - } - - /** - * @param corpid: 企业corpid,必选参数 - * @param agentid: 应用id,必选参数 - * @param ability_id: 能力ID,可选参数 - * @param job_info: job_info,可选参数 - * */ - public SpecSDK(String corpId, long agentId) { - specSDKptr = NewSDK1(corpId, agentId); - } - - private native long NewSDK1(String corpId, long agentId); - - public SpecSDK(String corpId, long agentId, String abilityId) { - specSDKptr = NewSDK2(corpId, agentId, abilityId); - } - - private native long NewSDK2(String corpId, long agentId, String abilityId); - - public SpecSDK(String corpId, long agentId, String abilityId, String jobInfo) { - specSDKptr = NewSDK3(corpId, agentId, abilityId, jobInfo); - } - - private native long NewSDK3(String corpId, long agentId, String abilityId, String jobInfo); - - /** - * @description 使用callback的请求来初始化 - * @param callback_sdk: 要求IsOk()==true - * @return C++内部指针,创建失败时指针仍为0,并输出错误日志 - * */ - public SpecSDK(SpecCallbackSDK callbackSDK) { - specSDKptr = NewSDK4(callbackSDK.GetPtr()); - } - - private native long NewSDK4(long callbackSDK); - - /** - * @usage 在Java对象的内存回收前析构C++对象 - */ - @Override - protected void finalize() throws Throwable { - DeleteCPPInstance(specSDKptr); - super.finalize(); - } - - private native void DeleteCPPInstance(long specSDKptr); - - /** - * @description 用于在专区内调用企业微信接口 - * @param api_name 接口名 - * @param request json格式的请求数据 - * @param response json格式的返回数据 - * @return errorcode 参考如下: - * 0: 成功 - * -910001: SDK没有初始化 - * -910002: 没有设置请求体 - * -910003: 没有设置请求的API - * -910004: 在SDK成员内找不到成员"response",注意lib内有反射机制,不要修改成员变量名 - * -910005: 使用未初始化的callback初始化SDK - * -910006: invoke调用失败,应检查日志查看具体原因 - * -910007: 响应体为空 - * @note 当返回0时,表示没有网络或请求协议层面或调用方法的失败, - * 调用方需继续检查response中的errcode字段确保业务层面的成功 - * - * @usage 当前版本sdk支持的接口列表,每个接口的具体协议请查看企业微信文档: - * https://developer.work.weixin.qq.com/document/path/91201 - * - * +--------------------------------+--------------------------------+ - * |接口名 |描述 | - * |--------------------------------|--------------------------------| - * |program_async_job_call_back |上报异步任务结果 | - * |sync_msg |获取会话记录 | - * |get_group_chat |获取内部群信息 | - * |get_agree_status_single |获取单聊会话同意情况 | - * |get_agree_status_room |获取群聊会话同意情况 | - * |set_hide_sensitiveinfo_config |设置成员会话组件敏感信息隐藏配置| - * |get_hide_sensitiveinfo_config |获取成员会话组件敏感信息隐藏配置| - * |search_chat |会话名称搜索 | - * |search_msg |会话消息搜索 | - * |create_rule |新增关键词规则 | - * |get_rule_list |获取关键词列表 | - * |get_rule_detail |获取关键词规则详情 | - * |update_rule |修改关键词规则 | - * |delete_rule |删除关键词规则 | - * |get_hit_msg_list |获取命中关键词规则的会话记录 | - * |create_sentiment_task |创建情感分析任务 | - * |get_sentiment_result |获取情感分析结果 | - * |create_summary_task |创建摘要提取任务 | - * |get_summary_result |获取摘要提取结果 | - * |create_customer_tag_task |创建标签匹配任务 | - * |get_customer_tag_result |获取标签任务结果 | - * |create_recommend_dialog_task |创建话术推荐任务 | - * |get_recommend_dialog_result |获取话术推荐结果 | - * |create_private_task |创建自定义模型任务 | - * |get_private_task_result |获取自定义模型结果 | - * |(废弃)document_list |获取知识集列表 | - * |create_spam_task |会话反垃圾创建分析任务 | - * |get_spam_result |会话反垃圾获取任务结果 | - * |create_chatdata_export_job |创建会话内容导出任务 | - * |get_chatdata_export_job_status |获取会话内容导出任务结果 | - * |spec_notify_app |专区通知应用 | - * |create_program_task |创建自定义程序任务 | - * |get_program_task_result |获取自定义程序结果 | - * |knowledge_base_list |获取企业授权给应用的知识集列表 | - * |knowledge_base_create |创建知识集 | - * |knowledge_base_detail |获取知识集详情 | - * |knowledge_base_add_doc |添加知识集內容 | - * |knowledge_base_remove_doc |删除知识集內容 | - * |knowledge_base_modify_name |修改知识集名称 | - * |knowledge_base_delete |删除知识集 | - * |search_contact_or_customer |员工或者客户名称搜索 | - * |create_ww_model_task |创建企微通用模型任务 | - * |get_ww_model_result |获取企微通用模型结果 | - * |get_msg_list_by_page_id |page_id获取消息列表 | - * +-----------------------------------------------------------------+ - * */ - public int Invoke(String apiName) { - return Invoke(specSDKptr, apiName, request); - } - - private native int Invoke(long sdk, String apiName, String request); - - // 静态代码块内还无法调用native日志函数,这里的日志在管理系统无法查询 - static { - try { - Class.forName("com.tencent.wework.SpecUtil"); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } - } -} diff --git a/winboll/src/main/java/com/tencent/wework/SpecUtil.java b/winboll/src/main/java/com/tencent/wework/SpecUtil.java deleted file mode 100644 index 85ea23b..0000000 --- a/winboll/src/main/java/com/tencent/wework/SpecUtil.java +++ /dev/null @@ -1,171 +0,0 @@ -package com.tencent.wework; - -//import java.lang.management.ManagementFactory; -//import java.lang.management.RuntimeMXBean; - -/** - * @warning: 1. 不要修改成员变量名,native方法内有反射调用 - * 2. 调用本地方法需保持包结构,本工具需放在包com.tencent.wework内 - * 3. 不允许继承,类名和函数名均不可修改,会影响本地方法的引用,详见:javah生成本地方法头文件 - * 4. 使用其他工具打印的日志将无法被查询,如需使用SLF4j风格的日志或性能更好的日志框架, - * 请自行封装SpecUtil.SpecLog或SpecUtil.SpecLogNative方法 - * - * @usage: 1. 获取SDK的版本号 - * 2. 打印三个级别的日志 - * 3. 开启调试模式 - */ -public final class SpecUtil { - - /** - * @description SDK版本号 - * @usage 可用于校对不同SDK版本,或后续针对不同的SDK版本添加业务逻辑 - */ - private static final String SDK_VERSION = "1.4.0"; - - public static String GetSDKVersion() { - return SDK_VERSION; - } - - /** - * @description 正确的包名,SDK必须存放在"com.tencent.wework"下,否则会影响本地方法的调用 - */ - private static final String EXPECTED_PACKAGE_NAME = "com.tencent.wework"; - - public static String GetExpectedPackageName() { - return EXPECTED_PACKAGE_NAME; - } - - private static final String LINE_SEPERATOR = System.getProperty("line.separator"); - - public static void WWSpecLogInfo(String... args) { - SpecLog('I', args); - } - - public static void WWSpecLogError(String... args) { - SpecLog('E', args); - } - - public static void WWSpecLogDebug(String... args) { - SpecLog('D', args); - } - - public static void WWSpecLogInfoWithReqId(String reqId, String... args) { - SpecLogWithReqId(reqId, 'I', args); - } - - public static void WWSpecLogErrorWithReqId(String reqId, String... args) { - SpecLogWithReqId(reqId, 'E', args); - } - - public static void WWSpecLogDebugWithReqId(String reqId, String... args) { - SpecLogWithReqId(reqId, 'D', args); - } - - /** - * @usage 打印标准日志 - * @note 只有使用SpecLog和SpecLogNative函数打印的日志能被调试平台查询,其他框架的日志仅能本地查看 - * @param logLevel 日志级别,使用char传递,目前支持I——INFO、E——ERROR、D——DEBUG - * @param args 自定义参数 - */ - public static void SpecLog(char logLevel, String... args) { - StackTraceElement element = Thread.currentThread().getStackTrace()[3]; - SpecLogNative( - logLevel, - element.getFileName(), - element.getLineNumber(), - String.join(",", args).replace(LINE_SEPERATOR, " ") - ); - } - - /** - * @usage 打印标准日志 - * @note 只有使用SpecLog和SpecLogNative函数打印的日志能被调试平台查询,其他框架的日志仅能本地查看 - * @param reqid 请求id - * @param logLevel 日志级别,使用char传递,目前支持I——INFO、E——ERROR、D——DEBUG - * @param args 自定义参数 - */ - public static void SpecLogWithReqId(String reqId, char logLevel, String... args) { - StackTraceElement element = Thread.currentThread().getStackTrace()[3]; - SpecLogNativeWithReqId( - reqId, - logLevel, - element.getFileName(), - element.getLineNumber(), - String.join(",", args).replace(LINE_SEPERATOR, " ") - ); - } - - /** - * @usage 打印标准日志 - * @note 只有使用SpecLog和SpecLogNative函数打印的日志能被调试平台查询,其他框架的日志仅能本地查看 - * 如需SLF4J风格的接口或对日志性能有进一步需求,开发者可以自行封装该函数 - * @param logLevel 日志级别,使用char传递,目前支持I——INFO、E——ERROR、D——DEBUG - * @param fileName 文件名(类名) - * @param lineNumber 行号 - * @param argsString 自定义参数 - */ - public static native void SpecLogNative(char logLevel, String fileName, int lineNumber, String argsString); - - /** - * @usage 打印标准日志 - * @note 只有使用SpecLog和SpecLogNative函数打印的日志能被调试平台查询,其他框架的日志仅能本地查看 - * 如需SLF4J风格的接口或对日志性能有进一步需求,开发者可以自行封装该函数 - * @param reqid 请求id - * @param logLevel 日志级别,使用char传递,目前支持I——INFO、E——ERROR、D——DEBUG - * @param fileName 文件名(类名) - * @param lineNumber 行号 - * @param argsString 自定义参数 - */ - public static native void SpecLogNativeWithReqId(String reqId, char logLevel, String fileName, int lineNumber, String argsString); - - - - /** - * @usage 开启调试模式,进程级别开关 - * @param debugToken 调试凭证,在管理端获取 - * @param accessToken 应用access token - * @return 是否开启成功 - */ - public static boolean SpecOpenDebugMode(String debugToken, String accessToken) { - return SpecOpenDebugModeNative(debugToken, accessToken); - } - - private static native boolean SpecOpenDebugModeNative(String debugToken, String accessToken); - - /** - * @usage 生成notify id。用户可调用本接口生成notify id,也可完全自定义生成 - * @return 新的notify id,支持纳秒级隔离,内部异常时会输出日志并返回空串 - * @note 1. 用户可先生成notify id,将其与回调数据关联存储后,再使用该notify id通知应用, - * 从而保证回调数据被请求时已存储完毕 - */ - public static String GenerateNotifyId() { - return GenerateNotifyIdNative(); - } - - private static native String GenerateNotifyIdNative(); - - static { - // 检查包名 - String packageName = SpecUtil.class.getPackage().getName(); - if (!EXPECTED_PACKAGE_NAME.equals(packageName)) { - // 静态代码块内还无法调用native日志函数,这里的日志在管理系统无法查询 - System.out.println("SpecUtil class must be in package com.tencent.wework"); - System.exit(1); - } - - // 加载so库 - try { - System.loadLibrary("WeWorkSpecSDK"); - } catch (UnsatisfiedLinkError e) { - System.out.println("libWeWorkSpecSDK.so not found in java.library.path"); - e.printStackTrace(); - System.exit(1); - } catch (Exception e) { - System.out.println("unexpected exception: " + e.getMessage()); - e.printStackTrace(); - System.exit(1); - } - - SpecUtil.WWSpecLogInfo("SDK init done", "packageName=" + packageName, "SDK_VERSION=" + SDK_VERSION); - } -} diff --git a/winboll/src/main/res/layout/activity_about.xml b/winboll/src/main/res/layout/activity_about.xml new file mode 100644 index 0000000..b38ba39 --- /dev/null +++ b/winboll/src/main/res/layout/activity_about.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/winboll/src/main/res/layout/activity_main.xml b/winboll/src/main/res/layout/activity_main.xml index 3ea6554..60d6fd3 100644 --- a/winboll/src/main/res/layout/activity_main.xml +++ b/winboll/src/main/res/layout/activity_main.xml @@ -6,14 +6,6 @@ android:layout_height="match_parent" android:orientation="vertical"> - - - diff --git a/winboll/src/main/res/layout/activity_new2.xml b/winboll/src/main/res/layout/activity_new2.xml index 697e613..8d9b4e5 100644 --- a/winboll/src/main/res/layout/activity_new2.xml +++ b/winboll/src/main/res/layout/activity_new2.xml @@ -6,7 +6,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - diff --git a/winboll/src/main/res/layout/activity_termux_env_test.xml b/winboll/src/main/res/layout/activity_termux_env_test.xml new file mode 100644 index 0000000..918a360 --- /dev/null +++ b/winboll/src/main/res/layout/activity_termux_env_test.xml @@ -0,0 +1,55 @@ + + + + + + + +