diff --git a/libwinboll/.gitignore b/libwinboll/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/libwinboll/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/libwinboll/build.gradle b/libwinboll/build.gradle
new file mode 100644
index 0000000..a2eece2
--- /dev/null
+++ b/libwinboll/build.gradle
@@ -0,0 +1,55 @@
+apply plugin: 'com.android.library'
+apply plugin: 'maven-publish'
+apply from: '../.winboll/winboll_lib_build.gradle'
+apply from: '../.winboll/winboll_lint_build.gradle'
+
+android {
+ // 适配MIUI12
+ compileSdkVersion 30
+ buildToolsVersion "30.0.3"
+
+ defaultConfig {
+ minSdkVersion 26
+ targetSdkVersion 30
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_7
+ targetCompatibility JavaVersion.VERSION_1_7
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ // 网络连接类库
+ api 'com.squareup.okhttp3:okhttp:4.4.1'
+ // Gson
+ api 'com.google.code.gson:gson:2.8.9'
+ // Html 解析
+ api 'org.jsoup:jsoup:1.13.1'
+ // 添加JSch依赖(SFTP核心,com.jcraft:jsch:0.1.54)
+ api 'com.jcraft:jsch:0.1.54'
+
+ // 米盟
+ api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk
+ //注意:以下5个库必须要引入
+ //implementation 'androidx.appcompat:appcompat:1.4.1'
+ api 'androidx.recyclerview:recyclerview:1.0.0'
+ api 'com.google.code.gson:gson:2.8.5'
+ api 'com.github.bumptech.glide:glide:4.9.0'
+ //annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
+
+ // WinBoLL库 nexus.winboll.cc 地址
+ api 'cc.winboll.studio:libappbase:15.20.25'
+ api 'cc.winboll.studio:libaes:15.20.14'
+
+ // 备用库 jitpack.io 地址
+ //api 'com.github.ZhanGSKen:libappbase:appbase-v15.20.25'
+ //api 'com.github.ZhanGSKen:libaes:aes-v15.20.14'
+
+ api fileTree(dir: 'libs', include: ['*.jar'])
+}
diff --git a/libwinboll/build.properties b/libwinboll/build.properties
new file mode 100644
index 0000000..0c9fce3
--- /dev/null
+++ b/libwinboll/build.properties
@@ -0,0 +1,8 @@
+#Created by .winboll/winboll_app_build.gradle
+#Wed Jun 03 07:32:48 HKT 2026
+stageCount=8
+libraryProject=libwinboll
+baseVersion=15.20
+publishVersion=15.20.7
+buildCount=0
+baseBetaVersion=15.20.8
diff --git a/libwinboll/proguard-rules.pro b/libwinboll/proguard-rules.pro
new file mode 100644
index 0000000..536058a
--- /dev/null
+++ b/libwinboll/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in C:/tools/adt-bundle-windows-x86_64-20131030/sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/libwinboll/src/main/AndroidManifest.xml b/libwinboll/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..2323080
--- /dev/null
+++ b/libwinboll/src/main/AndroidManifest.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/libwinboll/src/main/java/cc/winboll/studio/libwinboll/WinBoLLLibraryActivity.java b/libwinboll/src/main/java/cc/winboll/studio/libwinboll/WinBoLLLibraryActivity.java
new file mode 100644
index 0000000..bb86ae9
--- /dev/null
+++ b/libwinboll/src/main/java/cc/winboll/studio/libwinboll/WinBoLLLibraryActivity.java
@@ -0,0 +1,17 @@
+package cc.winboll.studio.libwinboll;
+
+import android.app.Activity;
+import android.os.Bundle;
+import cc.winboll.studio.libappbase.ToastUtils;
+
+public class WinBoLLLibraryActivity extends Activity
+{
+ @Override
+ protected void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_winbolllibrary);
+
+ ToastUtils.show("WinBoLLLibraryActivity onCreate");
+ }
+}
diff --git a/libwinboll/src/main/res/drawable-hdpi/ic_launcher.png b/libwinboll/src/main/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..96a442e
Binary files /dev/null and b/libwinboll/src/main/res/drawable-hdpi/ic_launcher.png differ
diff --git a/libwinboll/src/main/res/drawable-mdpi/ic_launcher.png b/libwinboll/src/main/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..359047d
Binary files /dev/null and b/libwinboll/src/main/res/drawable-mdpi/ic_launcher.png differ
diff --git a/libwinboll/src/main/res/drawable-xhdpi/ic_launcher.png b/libwinboll/src/main/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..71c6d76
Binary files /dev/null and b/libwinboll/src/main/res/drawable-xhdpi/ic_launcher.png differ
diff --git a/libwinboll/src/main/res/drawable-xxhdpi/ic_launcher.png b/libwinboll/src/main/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4df1894
Binary files /dev/null and b/libwinboll/src/main/res/drawable-xxhdpi/ic_launcher.png differ
diff --git a/libwinboll/src/main/res/layout/activity_winbolllibrary.xml b/libwinboll/src/main/res/layout/activity_winbolllibrary.xml
new file mode 100644
index 0000000..97dc11a
--- /dev/null
+++ b/libwinboll/src/main/res/layout/activity_winbolllibrary.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/libwinboll/src/main/res/values-v21/styles.xml b/libwinboll/src/main/res/values-v21/styles.xml
new file mode 100644
index 0000000..0948fdc
--- /dev/null
+++ b/libwinboll/src/main/res/values-v21/styles.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/libwinboll/src/main/res/values/strings.xml b/libwinboll/src/main/res/values/strings.xml
new file mode 100644
index 0000000..b677aa6
--- /dev/null
+++ b/libwinboll/src/main/res/values/strings.xml
@@ -0,0 +1,7 @@
+
+
+
+ libwinboll
+ Hello world!
+
+
diff --git a/libwinboll/src/main/res/values/styles.xml b/libwinboll/src/main/res/values/styles.xml
new file mode 100644
index 0000000..8d78246
--- /dev/null
+++ b/libwinboll/src/main/res/values/styles.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/winboll/README.md b/winboll/README.md
new file mode 100644
index 0000000..afbaba5
--- /dev/null
+++ b/winboll/README.md
@@ -0,0 +1,37 @@
+# WinBoLL
+
+#### 介绍
+WinBoLL 网站浏览器。
+
+#### 软件架构
+适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
+也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。
+
+
+#### Gradle 编译说明
+调试版编译命令 :gradle assembleBetaDebug
+阶段版编译命令 :bash .winboll/bashPublishAPKAddTag.sh winboll
+
+#### 使用说明
+3. Termux应用配置:
+- 已安装Termux(包名 com.termux );
+- 执行 echo "allow-external-apps = true" > ~/.termux/termux.properties
+
+#### 参与贡献
+
+1. Fork 本仓库
+2. 新建 Feat_xxx 分支
+3. 提交代码 : ZhanGSKen(ZhanGSKen)
+4. 新建 Pull Request
+
+
+#### 特技
+
+1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
+2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
+3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
+4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
+5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
+6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
+
+#### 参考文档
diff --git a/winboll/app_update_description.txt b/winboll/app_update_description.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/winboll/app_update_description.txt
@@ -0,0 +1 @@
+
diff --git a/winboll/build.gradle b/winboll/build.gradle
new file mode 100644
index 0000000..15cc45d
--- /dev/null
+++ b/winboll/build.gradle
@@ -0,0 +1,119 @@
+apply plugin: 'com.android.application'
+apply from: '../.winboll/winboll_app_build.gradle'
+apply from: '../.winboll/winboll_lint_build.gradle'
+
+def genVersionName(def versionName){
+ // 检查编译标志位配置
+ assert (winbollBuildProps['stageCount'] != null)
+ assert (winbollBuildProps['baseVersion'] != null)
+ // 保存基础版本号
+ winbollBuildProps.setProperty("baseVersion", "${versionName}");
+ //保存编译标志配置
+ FileOutputStream fos = new FileOutputStream(winbollBuildPropsFile)
+ winbollBuildProps.store(fos, "${winbollBuildPropsDesc}");
+ fos.close();
+
+ // 返回编译版本号
+ return "${versionName}." + winbollBuildProps['stageCount']
+}
+
+android {
+ // 适配MIUI12
+ compileSdkVersion 30
+ buildToolsVersion "30.0.3"
+
+ defaultConfig {
+ applicationId "cc.winboll.studio.winboll"
+ minSdkVersion 26
+ // 适配MIUI12
+ targetSdkVersion 30
+ versionCode 1
+ // versionName 更新后需要手动设置
+ // .winboll/winbollBuildProps.properties 文件的 stageCount=0
+ // Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
+ versionName "15.20"
+ if(true) {
+ versionName = genVersionName("${versionName}")
+ }
+ }
+
+ // 米盟 SDK
+ packagingOptions {
+ doNotStrip "*/*/libmimo_1011.so"
+ }
+
+ sourceSets {
+ main {
+ jniLibs.srcDirs = ['libs'] // 若SO库放在libs目录下
+ }
+ }
+}
+
+dependencies {
+ api project(':libwinboll')
+ api 'com.google.code.gson:gson:2.10.1'
+
+ // 下拉控件
+ api 'com.baoyz.pullrefreshlayout:library:1.2.0'
+
+ // SSH
+ api 'com.jcraft:jsch:0.1.55'
+ // Html 解析
+ api 'org.jsoup:jsoup:1.13.1'
+ // 二维码类库
+ api 'com.google.zxing:core:3.4.1'
+ api 'com.journeyapps:zxing-android-embedded:3.6.0'
+ // 应用介绍页类库
+ api 'io.github.medyo:android-about-page:2.0.0'
+ // 网络连接类库
+ api 'com.squareup.okhttp3:okhttp:4.4.1'
+ // FastJSON解析
+ implementation 'com.alibaba:fastjson:1.2.76'
+
+ // AndroidX 类库
+ /*api 'androidx.appcompat:appcompat:1.1.0'
+ //api 'com.google.android.material:material:1.4.0'
+ //api 'androidx.viewpager:viewpager:1.0.0'
+ //api 'androidx.vectordrawable:vectordrawable:1.1.0'
+ //api 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
+ //api 'androidx.fragment:fragment:1.1.0'*/
+
+
+ // 米盟
+ api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk
+ //注意:以下5个库必须要引入
+ //implementation 'androidx.appcompat:appcompat:1.4.1'
+ api 'androidx.recyclerview:recyclerview:1.0.0'
+ api 'com.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'
+
+ // Biometric (指纹识别)
+ implementation 'androidx.biometric:biometric:1.1.0'
+
+ // WinBoLL库 nexus.winboll.cc 地址
+ api 'cc.winboll.studio:libappbase:15.20.25'
+ api 'cc.winboll.studio:libaes:15.20.14'
+
+ // 备用库 jitpack.io 地址
+ //api 'com.github.ZhanGSKen:libappbase:appbase-v15.20.25'
+ //api 'com.github.ZhanGSKen:libaes:aes-v15.20.14'
+
+ api fileTree(dir: 'libs', include: ['*.jar'])
+}
diff --git a/winboll/build.properties b/winboll/build.properties
new file mode 100644
index 0000000..0c9fce3
--- /dev/null
+++ b/winboll/build.properties
@@ -0,0 +1,8 @@
+#Created by .winboll/winboll_app_build.gradle
+#Wed Jun 03 07:32:48 HKT 2026
+stageCount=8
+libraryProject=libwinboll
+baseVersion=15.20
+publishVersion=15.20.7
+buildCount=0
+baseBetaVersion=15.20.8
diff --git a/winboll/libs/libWeWorkSpecSDK.so b/winboll/libs/libWeWorkSpecSDK.so
new file mode 100644
index 0000000..d9f592b
Binary files /dev/null and b/winboll/libs/libWeWorkSpecSDK.so differ
diff --git a/winboll/proguard-rules.pro b/winboll/proguard-rules.pro
new file mode 100644
index 0000000..a18de74
--- /dev/null
+++ b/winboll/proguard-rules.pro
@@ -0,0 +1,137 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in C:\tools\adt-bundle-windows-x86_64-20131030\sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# ============================== 基础通用规则 ==============================
+# 保留系统组件
+-keep public class * extends android.app.Activity
+-keep public class * extends android.app.Service
+-keep public class * extends android.content.BroadcastReceiver
+-keep public class * extends android.content.ContentProvider
+-keep public class * extends android.app.backup.BackupAgentHelper
+-keep public class * extends android.preference.Preference
+
+# 保留 WinBoLL 核心包及子类(合并简化规则)
+-keep class cc.winboll.studio.** { *; }
+-keepclassmembers class cc.winboll.studio.** { *; }
+
+# 保留所有类中的 public static final String TAG 字段(便于日志定位)
+-keepclassmembers class * {
+ public static final java.lang.String TAG;
+}
+
+# 保留序列化类(避免Parcelable/Gson解析异常)
+-keep class * implements android.os.Parcelable {
+ public static final android.os.Parcelable$Creator *;
+}
+-keepclassmembers class * implements java.io.Serializable {
+ static final long serialVersionUID;
+ private static final java.io.ObjectStreamField[] serialPersistentFields;
+ private void writeObject(java.io.ObjectOutputStream);
+ private void readObject(java.io.ObjectInputStream);
+ java.lang.Object writeReplace();
+ java.lang.Object readResolve();
+}
+
+# 保留 R 文件(避免资源ID混淆)
+-keepclassmembers class **.R$* {
+ public static ;
+}
+
+# 保留 native 方法(避免JNI调用失败)
+-keepclasseswithmembernames class * {
+ native ;
+}
+
+# 保留注解和泛型(避免反射/序列化异常)
+-keepattributes *Annotation*
+-keepattributes Signature
+
+# 屏蔽 Java 8+ 警告(适配 Java 7 语法)
+-dontwarn java.lang.invoke.*
+-dontwarn android.support.v8.renderscript.*
+-dontwarn java.util.function.**
+
+# ============================== 第三方框架专项规则 ==============================
+# OkHttp 4.4.1(米盟广告请求依赖,完善Lambda兼容)
+-keep class okhttp3.** { *; }
+-keep interface okhttp3.** { *; }
+-keep class okhttp3.internal.** { *; }
+-keep class okio.** { *; }
+-dontwarn okhttp3.internal.platform.**
+-dontwarn okio.**
+
+# Glide 4.9.0(米盟广告图片加载依赖)
+-keep public class * implements com.bumptech.glide.module.GlideModule
+-keep public class * extends com.bumptech.glide.module.AppGlideModule
+-keep public enum com.bumptech.glide.load.ImageHeaderParser$ImageType {
+ **[] $VALUES;
+ public *;
+}
+-keepclassmembers class * implements com.bumptech.glide.module.AppGlideModule {
+ ();
+}
+-dontwarn com.bumptech.glide.**
+
+# Gson 2.8.5(米盟广告数据序列化依赖)
+-keep class com.google.gson.** { *; }
+-keep interface com.google.gson.** { *; }
+-keepclassmembers class * {
+ @com.google.gson.annotations.SerializedName ;
+}
+
+# 米盟 SDK(核心广告组件,完整保留避免加载失败)
+-keep class com.miui.zeus.** { *; }
+-keep interface com.miui.zeus.** { *; }
+# 保留米盟日志字段(便于广告加载失败排查)
+-keepclassmembers class com.miui.zeus.mimo.sdk.** {
+ public static final java.lang.String TAG;
+}
+
+# RecyclerView 1.0.0(米盟广告布局渲染依赖)
+-keep class androidx.recyclerview.** { *; }
+-keep interface androidx.recyclerview.** { *; }
+-keepclassmembers class androidx.recyclerview.widget.RecyclerView$Adapter {
+ public *;
+}
+
+# 其他第三方框架(按引入依赖保留,无则可删除)
+# XXPermissions 18.63
+-keep class com.hjq.permissions.** { *; }
+-keep interface com.hjq.permissions.** { *; }
+
+# ZXing 二维码(核心解析组件)
+-keep class com.google.zxing.** { *; }
+-keep class com.journeyapps.zxing.** { *; }
+
+# Jsoup HTML解析
+-keep class org.jsoup.** { *; }
+
+# Pinyin4j 拼音搜索
+-keep class net.sourceforge.pinyin4j.** { *; }
+
+# JSch SSH组件
+-keep class com.jcraft.jsch.** { *; }
+
+# AndroidX 基础组件
+-keep class androidx.appcompat.** { *; }
+-keep interface androidx.appcompat.** { *; }
+
+# ============================== 优化与调试配置 ==============================
+# 优化级别(平衡混淆效果与性能)
+-optimizationpasses 5
+-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
+
+# 调试辅助(保留行号便于崩溃定位)
+-verbose
+-dontpreverify
+-dontusemixedcaseclassnames
+-keepattributes SourceFile,LineNumberTable
+
diff --git a/winboll/src/beta/AndroidManifest.xml b/winboll/src/beta/AndroidManifest.xml
new file mode 100644
index 0000000..be35225
--- /dev/null
+++ b/winboll/src/beta/AndroidManifest.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/winboll/src/beta/java/cc/winboll/studio/winboll/activities/OllamaWindowActivity.java b/winboll/src/beta/java/cc/winboll/studio/winboll/activities/OllamaWindowActivity.java
new file mode 100644
index 0000000..07b804d
--- /dev/null
+++ b/winboll/src/beta/java/cc/winboll/studio/winboll/activities/OllamaWindowActivity.java
@@ -0,0 +1,32 @@
+package cc.winboll.studio.winboll.activities;
+
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+
+import cc.winboll.studio.winboll.R;
+
+/**
+ * Ollama 模型对话窗口
+ */
+public class OllamaWindowActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_ollama_window);
+
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ getSupportActionBar().setTitle("Ollama 窗口");
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_ollama_window, menu);
+ return true;
+ }
+}
diff --git a/winboll/src/beta/res/layout/activity_ollama_window.xml b/winboll/src/beta/res/layout/activity_ollama_window.xml
new file mode 100644
index 0000000..86f97cd
--- /dev/null
+++ b/winboll/src/beta/res/layout/activity_ollama_window.xml
@@ -0,0 +1,140 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/winboll/src/beta/res/menu/menu_ollama_window.xml b/winboll/src/beta/res/menu/menu_ollama_window.xml
new file mode 100644
index 0000000..160ab23
--- /dev/null
+++ b/winboll/src/beta/res/menu/menu_ollama_window.xml
@@ -0,0 +1,11 @@
+
+
diff --git a/winboll/src/beta/res/values-zh/strings.xml b/winboll/src/beta/res/values-zh/strings.xml
new file mode 100644
index 0000000..045e125
--- /dev/null
+++ b/winboll/src/beta/res/values-zh/strings.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/winboll/src/beta/res/values/strings.xml b/winboll/src/beta/res/values/strings.xml
new file mode 100644
index 0000000..42cbd7a
--- /dev/null
+++ b/winboll/src/beta/res/values/strings.xml
@@ -0,0 +1,8 @@
+
+
+
+ WinBoLL+
+ 筋斗云★
+ 金抖云☆
+
+
diff --git a/winboll/src/beta/res/xml/shortcutsmaincn1.xml b/winboll/src/beta/res/xml/shortcutsmaincn1.xml
new file mode 100644
index 0000000..6b6dc9a
--- /dev/null
+++ b/winboll/src/beta/res/xml/shortcutsmaincn1.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/winboll/src/beta/res/xml/shortcutsmaincn2.xml b/winboll/src/beta/res/xml/shortcutsmaincn2.xml
new file mode 100644
index 0000000..2dbb833
--- /dev/null
+++ b/winboll/src/beta/res/xml/shortcutsmaincn2.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/winboll/src/beta/res/xml/shortcutsmainen1.xml b/winboll/src/beta/res/xml/shortcutsmainen1.xml
new file mode 100644
index 0000000..4684c41
--- /dev/null
+++ b/winboll/src/beta/res/xml/shortcutsmainen1.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/winboll/src/main/AndroidManifest.xml b/winboll/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..2c6884e
--- /dev/null
+++ b/winboll/src/main/AndroidManifest.xml
@@ -0,0 +1,353 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/App.java b/winboll/src/main/java/cc/winboll/studio/winboll/App.java
new file mode 100644
index 0000000..39cb81f
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/App.java
@@ -0,0 +1,387 @@
+package cc.winboll.studio.winboll;
+
+import android.app.Activity;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.res.Resources;
+import android.graphics.Typeface;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.ViewGroup;
+import android.widget.HorizontalScrollView;
+import android.widget.ScrollView;
+import android.widget.TextView;
+import android.widget.Toast;
+import cc.winboll.studio.libaes.utils.AESThemeUtil;
+import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
+import cc.winboll.studio.libappbase.GlobalApplication;
+import cc.winboll.studio.libappbase.GlobalCrashActivity;
+import cc.winboll.studio.libappbase.ToastUtils;
+import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.Thread.UncaughtExceptionHandler;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class App extends GlobalApplication {
+
+ public static final String TAG = "App";
+
+ public static final String COMPONENT_EN1 = "cc.winboll.studio.winboll.MainActivityEN1";
+ public static final String COMPONENT_CN1 = "cc.winboll.studio.winboll.MainActivityCN1";
+ public static final String COMPONENT_CN2 = "cc.winboll.studio.winboll.MainActivityCN2";
+ public static final String ACTION_SWITCHTO_EN1 = "cc.winboll.studio.winboll.App.ACTION_SWITCHTO_EN1";
+ public static final String ACTION_SWITCHTO_CN1 = "cc.winboll.studio.winboll.App.ACTION_SWITCHTO_CN1";
+ public static final String ACTION_SWITCHTO_CN2 = "cc.winboll.studio.winboll.App.ACTION_SWITCHTO_CN2";
+
+ private static Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
+
+ @Override
+ public void onCreate() {
+ try {
+ super.onCreate();
+
+ ToastUtils.init(this);
+
+ WinBoLLActivityManager.init(this);
+
+ // 初始化 AES 主题工具(注入当前应用命名空间的主题ID列表,按 ThemeType.ordinal() 顺序)
+ ArrayList themeStyleList = new ArrayList();
+ themeStyleList.add(R.style.MyAppTheme); // AES(0)
+ themeStyleList.add(R.style.MyDepthAppTheme); // DEPTH(1)
+ themeStyleList.add(R.style.MySkyAppTheme); // SKY(2)
+ themeStyleList.add(R.style.MyGoldenAppTheme); // GOLDEN(3)
+ themeStyleList.add(R.style.MyBearingAppTheme); // BEARING(4)
+ themeStyleList.add(R.style.MyMemorAppTheme); // MEMOR(5)
+ themeStyleList.add(R.style.MyTaoAppTheme); // TAO(6)
+ AESThemeUtil.init(themeStyleList);
+
+ } catch (Throwable e) {
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ e.printStackTrace(pw);
+ pw.close();
+ String stackTraceStr = sw.toString();
+ CrashHandleNotifyUtils.handleUncaughtException(
+ this,
+ getPackageName(),
+ stackTraceStr,
+ GlobalCrashActivity.class
+ );
+ }
+ }
+
+ @Override
+ public void onTerminate() {
+ super.onTerminate();
+ ToastUtils.release();
+ }
+
+
+
+ public static void write(InputStream input, OutputStream output) throws IOException {
+ byte[] buf = new byte[1024 * 8];
+ int len;
+ while ((len = input.read(buf)) != -1) {
+ output.write(buf, 0, len);
+ }
+ }
+
+ public static void write(File file, byte[] data) throws IOException {
+ File parent = file.getParentFile();
+ if (parent != null && !parent.exists()) parent.mkdirs();
+
+ ByteArrayInputStream input = new ByteArrayInputStream(data);
+ FileOutputStream output = new FileOutputStream(file);
+ try {
+ write(input, output);
+ } finally {
+ closeIO(input, output);
+ }
+ }
+
+ public static String toString(InputStream input) throws IOException {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ write(input, output);
+ try {
+ return output.toString("UTF-8");
+ } finally {
+ closeIO(input, output);
+ }
+ }
+
+ public static void closeIO(Closeable... closeables) {
+ for (Closeable closeable : closeables) {
+ try {
+ if (closeable != null) closeable.close();
+ } catch (IOException ignored) {}
+ }
+ }
+
+ public static class CrashHandler {
+
+ public static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER = Thread.getDefaultUncaughtExceptionHandler();
+
+ private static CrashHandler sInstance;
+
+ private PartCrashHandler mPartCrashHandler;
+
+ public static CrashHandler getInstance() {
+ if (sInstance == null) {
+ sInstance = new CrashHandler();
+ }
+ return sInstance;
+ }
+
+ public void registerGlobal(Context context) {
+ registerGlobal(context, null);
+ }
+
+ public void registerGlobal(Context context, String crashDir) {
+ Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandlerImpl(context.getApplicationContext(), crashDir));
+ }
+
+ public void unregister() {
+ Thread.setDefaultUncaughtExceptionHandler(DEFAULT_UNCAUGHT_EXCEPTION_HANDLER);
+ }
+
+ public void registerPart(Context context) {
+ unregisterPart(context);
+ mPartCrashHandler = new PartCrashHandler(context.getApplicationContext());
+ MAIN_HANDLER.postAtFrontOfQueue(mPartCrashHandler);
+ }
+
+ public void unregisterPart(Context context) {
+ if (mPartCrashHandler != null) {
+ mPartCrashHandler.isRunning.set(false);
+ mPartCrashHandler = null;
+ }
+ }
+
+ private static class PartCrashHandler implements Runnable {
+
+ private final Context mContext;
+
+ public AtomicBoolean isRunning = new AtomicBoolean(true);
+
+ public PartCrashHandler(Context context) {
+ this.mContext = context;
+ }
+
+ @Override
+ public void run() {
+ while (isRunning.get()) {
+ try {
+ Looper.loop();
+ } catch (final Throwable e) {
+ e.printStackTrace();
+ if (isRunning.get()) {
+ MAIN_HANDLER.post(new Runnable(){
+
+ @Override
+ public void run() {
+ Toast.makeText(mContext, e.toString(), Toast.LENGTH_LONG).show();
+ }
+ });
+ } else {
+ if (e instanceof RuntimeException) {
+ throw (RuntimeException)e;
+ } else {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private static class UncaughtExceptionHandlerImpl implements UncaughtExceptionHandler {
+
+ private static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss");
+
+ private final Context mContext;
+
+ private final File mCrashDir;
+
+ public UncaughtExceptionHandlerImpl(Context context, String crashDir) {
+ this.mContext = context;
+ this.mCrashDir = TextUtils.isEmpty(crashDir) ? new File(mContext.getExternalCacheDir(), "crash") : new File(crashDir);
+ }
+
+ @Override
+ public void uncaughtException(Thread thread, Throwable throwable) {
+ try {
+
+ String log = buildLog(throwable);
+ writeLog(log);
+
+ try {
+ Intent intent = new Intent(mContext, CrashActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.putExtra(Intent.EXTRA_TEXT, log);
+ mContext.startActivity(intent);
+ } catch (Throwable e) {
+ e.printStackTrace();
+ writeLog(e.toString());
+ }
+
+ throwable.printStackTrace();
+ android.os.Process.killProcess(android.os.Process.myPid());
+ System.exit(0);
+
+ } catch (Throwable e) {
+ if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
+ }
+ }
+
+ private String buildLog(Throwable throwable) {
+ String time = DATE_FORMAT.format(new Date());
+
+ String versionName = "unknown";
+ long versionCode = 0;
+ try {
+ PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
+ versionName = packageInfo.versionName;
+ versionCode = Build.VERSION.SDK_INT >= 28 ? packageInfo.getLongVersionCode() : packageInfo.versionCode;
+ } catch (Throwable ignored) {}
+
+ LinkedHashMap head = new LinkedHashMap();
+ head.put("Time Of Crash", time);
+ head.put("Device", String.format("%s, %s", Build.MANUFACTURER, Build.MODEL));
+ head.put("Android Version", String.format("%s (%d)", Build.VERSION.RELEASE, Build.VERSION.SDK_INT));
+ head.put("App Version", String.format("%s (%d)", versionName, versionCode));
+ head.put("Kernel", getKernel());
+ head.put("Support Abis", Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_ABIS != null ? Arrays.toString(Build.SUPPORTED_ABIS): "unknown");
+ head.put("Fingerprint", Build.FINGERPRINT);
+
+ StringBuilder builder = new StringBuilder();
+
+ for (String key : head.keySet()) {
+ if (builder.length() != 0) builder.append("\n");
+ builder.append(key);
+ builder.append(" : ");
+ builder.append(head.get(key));
+ }
+
+ builder.append("\n\n");
+ builder.append(Log.getStackTraceString(throwable));
+
+ return builder.toString();
+ }
+
+ private void writeLog(String log) {
+ String time = DATE_FORMAT.format(new Date());
+ File file = new File(mCrashDir, "crash_" + time + ".txt");
+ try {
+ write(file, log.getBytes("UTF-8"));
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+ }
+
+ private static String getKernel() {
+ try {
+ return App.toString(new FileInputStream("/proc/version")).trim();
+ } catch (Throwable e) {
+ return e.getMessage();
+ }
+ }
+ }
+ }
+
+ public static final class CrashActivity extends Activity {
+
+ private String mLog;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setTheme(android.R.style.Theme_DeviceDefault);
+ setTitle("App Crash");
+
+ mLog = getIntent().getStringExtra(Intent.EXTRA_TEXT);
+
+ ScrollView contentView = new ScrollView(this);
+ contentView.setFillViewport(true);
+
+ HorizontalScrollView horizontalScrollView = new HorizontalScrollView(this);
+
+ TextView textView = new TextView(this);
+ int padding = dp2px(16);
+ textView.setPadding(padding, padding, padding, padding);
+ textView.setText(mLog);
+ textView.setTextIsSelectable(true);
+ textView.setTypeface(Typeface.DEFAULT);
+ textView.setLinksClickable(true);
+
+ horizontalScrollView.addView(textView);
+ contentView.addView(horizontalScrollView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+
+ setContentView(contentView);
+ }
+
+ private void restart() {
+ Intent intent = getPackageManager().getLaunchIntentForPackage(getPackageName());
+ if (intent != null) {
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ }
+ finish();
+ android.os.Process.killProcess(android.os.Process.myPid());
+ System.exit(0);
+ }
+
+ private static int dp2px(float dpValue) {
+ final float scale = Resources.getSystem().getDisplayMetrics().density;
+ return (int) (dpValue * scale + 0.5f);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(0, android.R.id.copy, 0, android.R.string.copy)
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.copy:
+ ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
+ cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onBackPressed() {
+ restart();
+ }
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/AssistantService.java b/winboll/src/main/java/cc/winboll/studio/winboll/AssistantService.java
new file mode 100644
index 0000000..fb23c79
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/AssistantService.java
@@ -0,0 +1,97 @@
+package cc.winboll.studio.winboll;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/03/28 19:12:12
+ * @Describe 应用主要服务组件类守护进程服务组件类
+ */
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import cc.winboll.studio.libaes.models.WinBoLLClientServiceBean;
+import cc.winboll.studio.winboll.WinBoLLClientService;
+import cc.winboll.studio.winboll.utils.ServiceUtils;
+
+public class AssistantService extends Service {
+
+ public final static String TAG = "AssistantService";
+
+ WinBoLLClientServiceBean mWinBoLLServiceBean;
+ MyServiceConnection mMyServiceConnection;
+ volatile boolean mIsServiceRunning;
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mWinBoLLServiceBean = WinBoLLClientServiceBean.loadWinBoLLClientServiceBean(this);
+ if (mMyServiceConnection == null) {
+ mMyServiceConnection = new MyServiceConnection();
+ }
+ // 设置运行参数
+ mIsServiceRunning = false;
+ run();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ run();
+ return START_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ mIsServiceRunning = false;
+ super.onDestroy();
+ }
+
+ //
+ // 运行服务内容
+ //
+ void run() {
+ mWinBoLLServiceBean = WinBoLLClientServiceBean.loadWinBoLLClientServiceBean(this);
+ if (mWinBoLLServiceBean.isEnable()) {
+ if (mIsServiceRunning == false) {
+ // 设置运行状态
+ mIsServiceRunning = true;
+ // 唤醒和绑定主进程
+ wakeupAndBindMain();
+ }
+ }
+ }
+
+ //
+ // 唤醒和绑定主进程
+ //
+ void wakeupAndBindMain() {
+ if (ServiceUtils.isServiceAlive(getApplicationContext(), WinBoLLClientService.class.getName()) == false) {
+ startForegroundService(new Intent(AssistantService.this, WinBoLLClientService.class));
+ }
+
+ bindService(new Intent(AssistantService.this, WinBoLLClientService.class), mMyServiceConnection, Context.BIND_IMPORTANT);
+ }
+
+ //
+ // 主进程与守护进程连接时需要用到此类
+ //
+ class MyServiceConnection implements ServiceConnection {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ mWinBoLLServiceBean = WinBoLLClientServiceBean.loadWinBoLLClientServiceBean(AssistantService.this);
+ if (mWinBoLLServiceBean.isEnable()) {
+ wakeupAndBindMain();
+ }
+ }
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/EWUIStatusIconDrawable.java b/winboll/src/main/java/cc/winboll/studio/winboll/EWUIStatusIconDrawable.java
new file mode 100644
index 0000000..20a70d8
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/EWUIStatusIconDrawable.java
@@ -0,0 +1,35 @@
+package cc.winboll.studio.winboll;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/03/28 19:11:27
+ * @Describe WinBoLL UI 状态图标枚举
+ */
+import cc.winboll.studio.libaes.R;
+
+public enum EWUIStatusIconDrawable {
+ NORMAL(0),
+ NEWS(1)
+ ;
+
+ static final String TAG = "WUIStatusIconDrawable";
+
+ static String[] _mlistCNName = { "正常", "新的消息" };
+
+ private int value = 0;
+ private EWUIStatusIconDrawable(int value) { //必须是private的,否则编译错误
+ this.value = value;
+ }
+
+ public static int getIconDrawableId(EWUIStatusIconDrawable drawableId) {
+ int res;
+ switch(drawableId){
+ case NEWS :
+ res = R.drawable.ic_winbollbeta;
+ break;
+ default :
+ res = R.drawable.ic_winboll;
+ }
+ return res;
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/MainActivity.java b/winboll/src/main/java/cc/winboll/studio/winboll/MainActivity.java
new file mode 100644
index 0000000..7f34285
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/MainActivity.java
@@ -0,0 +1,222 @@
+package cc.winboll.studio.winboll;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import cc.winboll.studio.libaes.activitys.DrawerFragmentActivity;
+import cc.winboll.studio.libaes.models.DrawerMenuBean;
+import cc.winboll.studio.libaes.utils.AESThemeUtil;
+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.termux.MyTermuxActivity;
+import cc.winboll.studio.winboll.fragments.BrowserFragment;
+import cc.winboll.studio.winboll.unittest.TermuxEnvTestActivity;
+import java.util.ArrayList;
+
+public class MainActivity extends DrawerFragmentActivity {
+
+
+ public static final String TAG = "MainActivity";
+
+ BrowserFragment mBrowserFragment;
+
+ // ------------------- 新增:Handler 消息定义(接收URL历史更新消息) -------------------
+ // 消息标识:URL加载历史更新(刷新抽屉菜单的历史列表)
+ public static final int MSG_URLLOADHISTORY_UPDATE = 1002;
+ // 自定义Handler(接收应用内消息,如BrowserFragment发送的历史更新消息)
+ private static Handler _mMainHandler;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ setTheme(cc.winboll.studio.winboll.theme.WinBoLLThemeUtil.getThemeTypeID(getApplicationContext()));
+ super.onCreate(savedInstanceState);
+ initMainHandler();
+ if (mBrowserFragment == null) {
+ String externalUrl = extractExternalUrl(getIntent());
+ if (externalUrl != null) {
+ mBrowserFragment = BrowserFragment.newInstance(externalUrl);
+ } else {
+ mBrowserFragment = BrowserFragment.newInstance();
+ }
+ addFragment(mBrowserFragment);
+ }
+ showFragment(mBrowserFragment);
+ }
+
+// @Override
+// protected void onNewIntent(Intent intent) {
+// super.onNewIntent(intent);
+// String externalUrl = extractExternalUrl(intent);
+// if (externalUrl != null && mBrowserFragment != null && mBrowserFragment.getBrowserHandler() != null) {
+// Message msg = Message.obtain();
+// msg.what = BrowserFragment.MSG_OPEN_URL;
+// msg.obj = externalUrl;
+// mBrowserFragment.getBrowserHandler().sendMessage(msg);
+// }
+// }
+
+ private String extractExternalUrl(Intent intent) {
+ if (intent != null && Intent.ACTION_VIEW.equals(intent.getAction())) {
+ Uri uri = intent.getData();
+ if (uri != null) {
+ return uri.toString();
+ }
+ }
+ return null;
+ }
+
+ public static void sendMessage(Message msg) {
+ _mMainHandler.sendMessage(msg);
+ }
+
+ /**
+ * 初始化Handler(接收MSG_URLLOADHISTORY_UPDATE消息,刷新抽屉历史菜单)
+ */
+ private void initMainHandler() {
+ // 清理旧数据
+ if (_mMainHandler != null) {
+ _mMainHandler.removeCallbacksAndMessages(null);
+ _mMainHandler = null;
+ }
+
+ // Java 7 匿名内部类实现Handler(主线程创建,安全更新UI/抽屉菜单)
+ _mMainHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ super.handleMessage(msg);
+ switch (msg.what) {
+ case MSG_URLLOADHISTORY_UPDATE:
+ // 处理URL历史更新消息:刷新抽屉菜单的历史列表
+ LogUtils.d(TAG, "收到URL历史更新消息,刷新抽屉菜单");
+ refreshUrlHistoryDrawerMenu();
+ break;
+ default:
+ break;
+ }
+ }
+ };
+ }
+
+ @Override
+ public void initDrawerMenuItemList(ArrayList listDrawerMenu) {
+ super.initDrawerMenuItemList(listDrawerMenu);
+ //LogUtils.d(TAG, "initDrawerMenuItemList");
+ // 加载URL历史菜单(初始化时加载)
+ refreshUrlHistoryDrawerMenu();
+ notifyDrawerMenuDataChanged();
+ }
+
+ @Override
+ public void reinitDrawerMenuItemList(ArrayList listDrawerMenu) {
+ super.reinitDrawerMenuItemList(listDrawerMenu);
+ //LogUtils.d(TAG, "reinitDrawerMenuItemList");
+ // 重新加载URL历史菜单(菜单重置时加载)
+ refreshUrlHistoryDrawerMenu();
+ notifyDrawerMenuDataChanged();
+ }
+
+ void loadUrlLoadHistotyMenu(ArrayList listDrawerMenu) {
+ listDrawerMenu.clear();
+ if (BrowserFragment._mUrlLoadHistory != null) {
+ for (String url : BrowserFragment._mUrlLoadHistory) {
+ listDrawerMenu.add(new DrawerMenuBean(R.drawable.ic_launcher, url));
+ }
+ }
+ }
+
+ // ------------------- 新增:刷新URL历史抽屉菜单(提取独立方法,复用) -------------------
+ private void refreshUrlHistoryDrawerMenu() {
+ // 获取抽屉菜单列表,重新加载历史数据并刷新
+ ArrayList drawerMenuList = super.malDrawerMenuItem; // 假设父类提供获取菜单列表的方法
+ if (drawerMenuList != null) {
+ loadUrlLoadHistotyMenu(drawerMenuList); // 重新加载更新后的历史数据
+ notifyDrawerMenuDataChanged(); // 通知抽屉菜单刷新UI
+ }
+ }
+
+ @Override
+ public DrawerFragmentActivity.ActivityType initActivityType() {
+ return DrawerFragmentActivity.ActivityType.Main;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ if (App.isDebugging()) {
+ getMenuInflater().inflate(R.menu.toolbar_test, menu);
+ }
+ getMenuInflater().inflate(R.menu.toolbar_main, menu);
+ return true;
+ }
+
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ super.onItemClick(parent, view, position, id);
+ if (mBrowserFragment != null && mBrowserFragment.getBrowserHandler() != null) {
+ Message msg = Message.obtain();
+ msg.what = BrowserFragment.MSG_HISTORY_POSITION;
+ msg.obj = position;
+ mBrowserFragment.getBrowserHandler().sendMessage(msg);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (AESThemeUtil.onWinBoLLThemeItemSelected(this, item)) {
+ recreate();
+ } else {
+ int nItemId = item.getItemId();
+ if (nItemId == R.id.item_home) {
+ if (mBrowserFragment != null && mBrowserFragment.getBrowserHandler() != null) {
+ Message msg = Message.obtain();
+ msg.what = BrowserFragment.MSG_HOMEPAGE;
+ mBrowserFragment.getBrowserHandler().sendMessage(msg);
+ }
+ } else if (nItemId == R.id.item_settings) {
+ WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), SettingsActivity.class);
+ } else if (nItemId == R.id.item_about) {
+ Intent intent = new Intent(getApplicationContext(), AboutActivity.class);
+ WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), intent, AboutActivity.class);
+ } else if (nItemId == R.id.item_mytermux) {
+ Intent intent = new Intent(getApplicationContext(), MyTermuxActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ } else if (nItemId == R.id.item_termux_env_test) {
+ Intent intent = new Intent(getApplicationContext(), TermuxEnvTestActivity.class);
+ WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), intent, AboutActivity.class);
+ } else if (nItemId == R.id.item_library_activity) {
+ Intent intent = new Intent(getApplicationContext(), cc.winboll.studio.libwinboll.WinBoLLLibraryActivity.class);
+ WinBoLLActivityManager.getInstance().startWinBoLLActivity(getApplicationContext(), intent, AboutActivity.class);
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+ return true;
+ }
+
+ // ------------------- 新增:对外提供Handler(供其他组件发送消息,如BrowserFragment) -------------------
+ public Handler getMainHandler() {
+ return _mMainHandler;
+ }
+
+ // ------------------- 新增:生命周期管理(防止Handler内存泄漏) -------------------
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // 清除Handler所有消息和回调,避免内存泄漏
+ if (_mMainHandler != null) {
+ _mMainHandler.removeCallbacksAndMessages(null);
+ _mMainHandler = null;
+ }
+ }
+}
+
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/MyTileService.java b/winboll/src/main/java/cc/winboll/studio/winboll/MyTileService.java
new file mode 100644
index 0000000..f17f234
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/MyTileService.java
@@ -0,0 +1,80 @@
+package cc.winboll.studio.winboll;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/02/13 19:30:10
+ */
+import android.content.Context;
+import cc.winboll.studio.winboll.R;
+import android.service.quicksettings.Tile;
+import android.service.quicksettings.TileService;
+import cc.winboll.studio.winboll.models.MainServiceBean;
+import cc.winboll.studio.winboll.services.MainService;
+
+public class MyTileService extends TileService {
+ public static final String TAG = "MyTileService";
+
+ volatile static MyTileService _MyTileService;
+
+ @Override
+ public void onStartListening() {
+ super.onStartListening();
+ _MyTileService = this;
+ Tile tile = getQsTile();
+ MainServiceBean bean = MainServiceBean.loadBean(this, MainServiceBean.class);
+ if (bean != null && bean.isEnable()) {
+ //MainService.startMainService(context);
+ tile.setState(Tile.STATE_ACTIVE);
+ tile.setIcon(android.graphics.drawable.Icon.createWithResource(this, R.drawable.ic_cloud));
+ } else {
+ //MainService.stopMainService(context);
+ tile.setState(Tile.STATE_INACTIVE);
+ tile.setIcon(android.graphics.drawable.Icon.createWithResource(this, R.drawable.ic_cloud_outline));
+ }
+ tile.updateTile();
+// Tile tile = getQsTile();
+// tile.setState(Tile.STATE_INACTIVE);
+// tile.setLabel(getString(R.string.tileservice_name));
+// tile.setIcon(android.graphics.drawable.Icon.createWithResource(this, R.drawable.ic_cloud_outline));
+// tile.updateTile();
+
+ }
+
+ @Override
+ public void onClick() {
+ super.onClick();
+ Tile tile = getQsTile();
+ MainServiceBean bean = MainServiceBean.loadBean(this, MainServiceBean.class);
+ if (bean == null) {
+ bean = new MainServiceBean();
+ }
+
+ if (tile.getState() == Tile.STATE_ACTIVE) {
+ bean.setIsEnable(false);
+ MainServiceBean.saveBean(this, bean);
+ MainService.stopMainService(this);
+ } else if (tile.getState() == Tile.STATE_INACTIVE) {
+ bean.setIsEnable(true);
+ MainServiceBean.saveBean(this, bean);
+ MainService.startMainService(this);
+ }
+ updateServiceIconStatus(this);
+ }
+
+ public static void updateServiceIconStatus(Context context) {
+ if (_MyTileService == null) {
+ return;
+ }
+
+ Tile tile = _MyTileService.getQsTile();
+ MainServiceBean bean = MainServiceBean.loadBean(context, MainServiceBean.class);
+ if (bean != null && bean.isEnable()) {
+ tile.setState(Tile.STATE_ACTIVE);
+ tile.setIcon(android.graphics.drawable.Icon.createWithResource(context, R.drawable.ic_cloud));
+ } else {
+ tile.setState(Tile.STATE_INACTIVE);
+ tile.setIcon(android.graphics.drawable.Icon.createWithResource(context, R.drawable.ic_cloud_outline));
+ }
+ tile.updateTile();
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/WinBoLL.java b/winboll/src/main/java/cc/winboll/studio/winboll/WinBoLL.java
new file mode 100644
index 0000000..cbdff70
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/WinBoLL.java
@@ -0,0 +1,40 @@
+package cc.winboll.studio.winboll;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/05/10 10:13
+ * @Describe WinBoLL 系列应用通用管理类
+ */
+import android.content.Context;
+import android.content.Intent;
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.winboll.models.WinBoLLModel;
+
+public class WinBoLL {
+
+ public static final String TAG = "WinBoLL";
+
+ public static final String ACTION_BIND = WinBoLL.class.getName() + ".ACTION_BIND";
+ public static final String EXTRA_WINBOLLMODEL = "EXTRA_WINBOLLMODEL";
+
+ public static void bindToAPPBase(Context context, String appMainService) {
+ LogUtils.d(TAG, "bindToAPPBase(...)");
+ String toPackage = "cc.winboll.studio.appbase";
+ startBind(context, toPackage, appMainService);
+ }
+
+ public static void bindToAPPBaseBeta(Context context, String appMainService) {
+ LogUtils.d(TAG, "bindToAPPBaseBeta(...)");
+ String toPackage = "cc.winboll.studio.appbase.beta";
+ startBind(context, toPackage, appMainService);
+ }
+
+ static void startBind(Context context, String toPackage, String appMainService) {
+ Intent intent = new Intent(ACTION_BIND);
+ intent.putExtra(EXTRA_WINBOLLMODEL, (new WinBoLLModel(toPackage, appMainService)).toString());
+ intent.setPackage(toPackage);
+ LogUtils.d(TAG, String.format("ACTION_BIND :\nTo Package : %s\nAPP Main Service : %s", toPackage, appMainService));
+ context.sendBroadcast(intent);
+ }
+
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/WinBoLLClientService.java b/winboll/src/main/java/cc/winboll/studio/winboll/WinBoLLClientService.java
new file mode 100644
index 0000000..c9d29de
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/WinBoLLClientService.java
@@ -0,0 +1,21 @@
+package cc.winboll.studio.winboll;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/05/03 19:28
+ */
+public class WinBoLLClientService extends Service {
+
+ public static final String TAG = "WinBoLLClientService";
+
+ @Override
+ public IBinder onBind(Intent intent) {
+
+ return null;
+ }
+
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/WinBoLLServiceStatusView.java b/winboll/src/main/java/cc/winboll/studio/winboll/WinBoLLServiceStatusView.java
new file mode 100644
index 0000000..e939b7e
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/WinBoLLServiceStatusView.java
@@ -0,0 +1,38 @@
+package cc.winboll.studio.winboll;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/05/03 19:14
+ */
+public class WinBoLLServiceStatusView extends LinearLayout {
+
+ public static final String TAG = "WinBoLLServiceStatusView";
+
+ public WinBoLLServiceStatusView(Context context) {
+ super(context);
+ }
+
+ public WinBoLLServiceStatusView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public WinBoLLServiceStatusView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public WinBoLLServiceStatusView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+
+ void setServerHost(String szWinBoLLServerHost) {
+
+ }
+
+ void setAuthInfo(String szDevUserName, String szDevUserPassword) {
+
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/WxPayConfig.java b/winboll/src/main/java/cc/winboll/studio/winboll/WxPayConfig.java
new file mode 100644
index 0000000..69c7493
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/WxPayConfig.java
@@ -0,0 +1,28 @@
+package cc.winboll.studio.winboll;
+
+/**
+ * 微信支付配置类
+ * @Author ZhanGSKen
+ * @Date 2026/01/07
+ */
+public class WxPayConfig {
+ // ========== 核心修改点:替换为你的服务端地址 ==========
+ // 服务端IP/域名 + 端口(Docker部署的服务端,需确保安卓端可访问)
+ public static final String BASE_URL = "https://wxpay.winboll.cc";
+
+ // 统一下单接口路径(对应服务端的测试接口)
+ public static final String CREATE_ORDER_URL = BASE_URL + "/pay/createOrder";
+
+ // 订单查询接口路径
+ public static final String QUERY_ORDER_URL = BASE_URL + "/pay/queryOrder";
+
+ // ========== 固定支付配置 ==========
+ public static final String ORDER_BODY = "定额测试支付"; // 商品描述
+ public static final int TOTAL_FEE = 1; // 固定金额:1分(沙箱环境推荐)
+ public static final String TRADE_TYPE = "NATIVE"; // 支付类型:二维码
+
+ // ========== 轮询配置 ==========
+ public static final long POLL_INTERVAL = 10000; // 轮询间隔:10秒
+ public static final long POLL_TIMEOUT = 45000; // 轮询超时:45秒
+}
+
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..7dedd62
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/activities/BaseWinBoLLActivity.java
@@ -0,0 +1,51 @@
+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.WinBoLLActivityManager;
+import cc.winboll.studio.winboll.theme.WinBoLLThemeUtil;
+
+/**
+ * @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() {
+ return WinBoLLThemeUtil.getThemeStyleType(WinBoLLThemeUtil.getThemeTypeID(getApplicationContext()));
+ }
+
+ void setThemeStyle() {
+ setTheme(WinBoLLThemeUtil.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/LogonActivity.java b/winboll/src/main/java/cc/winboll/studio/winboll/activities/LogonActivity.java
new file mode 100644
index 0000000..7097711
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/activities/LogonActivity.java
@@ -0,0 +1,150 @@
+package cc.winboll.studio.winboll.activities;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.RadioButton;
+import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
+import cc.winboll.studio.libappbase.BuildConfig;
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.libappbase.LogView;
+import cc.winboll.studio.winboll.R;
+import cc.winboll.studio.winboll.models.UserInfoModel;
+import cc.winboll.studio.winboll.utils.RSAUtils;
+import cc.winboll.studio.winboll.utils.YunUtils;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/06/04 13:29
+ * @Describe 用户登录框
+ */
+public class LogonActivity extends Activity implements IWinBoLLActivity {
+
+ public static final String TAG = "LogonActivity";
+
+ public static final String DEBUG_HOST = "http://10.8.0.250:456";
+ public static final String YUN_HOST = "https://yun.winboll.cc";
+
+
+ String mHost = "";
+ RadioButton mrbYunHost;
+ RadioButton mrbDebugHost;
+ LogView mLogView;
+
+ @Override
+ public Activity getActivity() {
+ return this;
+ }
+
+ @Override
+ public String getTag() {
+ return TAG;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_logon);
+ mLogView = findViewById(R.id.logview);
+ mLogView.start();
+
+ mHost = BuildConfig.DEBUG ? DEBUG_HOST: YUN_HOST;
+ if (BuildConfig.DEBUG) {
+ mrbYunHost = findViewById(R.id.rb_yunhost);
+ mrbDebugHost = findViewById(R.id.rb_debughost);
+ mrbYunHost.setChecked(!BuildConfig.DEBUG);
+ mrbDebugHost.setChecked(BuildConfig.DEBUG);
+ } else {
+ findViewById(R.id.ll_hostbar).setVisibility(View.GONE);
+ }
+ }
+
+ public void onSwitchHost(View view) {
+ if (view.getId() == R.id.rb_yunhost) {
+ mrbDebugHost.setChecked(false);
+ mHost = YUN_HOST;
+ } else if (view.getId() == R.id.rb_debughost) {
+ mrbYunHost.setChecked(false);
+ mHost = DEBUG_HOST;
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mLogView.start();
+ }
+
+ public void onTestLogin(View view) {
+ LogUtils.d(TAG, "onTestLogin");
+ final YunUtils yunUtils = YunUtils.getInstance(this);
+
+ UserInfoModel userInfoModel = new UserInfoModel();
+ userInfoModel.setUsername("jian");
+ userInfoModel.setPassword("kkiio");
+ userInfoModel.setToken("aaa111");
+ yunUtils.login(mHost, userInfoModel);
+ }
+
+ public void onTestRSA(View view) {
+ LogUtils.d(TAG, "onTestRSA");
+ RSAUtils utils = RSAUtils.getInstance(this);
+
+ try {
+ // 测试 1:首次生成密钥对
+ LogUtils.d(TAG, "==== 首次生成密钥对 ====");
+ if (utils.keysExist()) {
+ LogUtils.d(TAG, "密钥对已生成");
+ } else {
+ utils.generateAndSaveKeys();
+ LogUtils.d(TAG, "密钥对生成成功。");
+ }
+
+ // 测试 2:获取密钥对(自动读取已生成的文件)
+ KeyPair keyPair = utils.getOrGenerateKeys();
+ PublicKey publicKey = keyPair.getPublic();
+ PrivateKey privateKey = keyPair.getPrivate();
+
+ // 打印密钥信息
+ LogUtils.d(TAG, "\n==== 密钥信息 ====");
+ LogUtils.d(TAG, "公钥算法:" + publicKey.getAlgorithm());
+ LogUtils.d(TAG, "公钥编码长度:" + publicKey.getEncoded().length + "字节");
+ LogUtils.d(TAG, "私钥算法:" + privateKey.getAlgorithm());
+ LogUtils.d(TAG, "私钥编码长度:" + privateKey.getEncoded().length + "字节");
+
+ // 测试 3:重复调用时检查是否复用文件
+ LogUtils.d(TAG, "\n==== 二次调用 ====");
+ KeyPair reusedPair = utils.getOrGenerateKeys();
+ LogUtils.d(TAG, "是否为同一公钥:" + (publicKey.equals(reusedPair.getPublic()))); // true(单例引用)
+ LogUtils.d(TAG, "操作完成");
+
+ String testMessage = "Hello, RSA Encryption!";
+
+ // 1. 获取或生成密钥对
+ PublicKey publicKeyReused = reusedPair.getPublic();
+ PrivateKey privateKeyReused = reusedPair.getPrivate();
+
+ // 2. 公钥加密
+ byte[] encryptedData = utils.encryptWithPublicKey(testMessage, publicKeyReused);
+ LogUtils.d(TAG, "加密后数据(字节长度):" + encryptedData.length);
+
+ // 3. 私钥解密
+ String decryptedMessage = utils.decryptWithPrivateKey(encryptedData, privateKeyReused);
+ LogUtils.d(TAG, "解密结果: " + decryptedMessage);
+
+ // 4. 验证解密是否成功
+ if (testMessage.equals(decryptedMessage)) {
+ LogUtils.d(TAG, "加密解密测试通过!");
+ } else {
+ LogUtils.d(TAG, "测试失败:内容不一致");
+ }
+ } catch (Exception e) {
+ LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
+ }
+ }
+
+
+}
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
new file mode 100644
index 0000000..561cf64
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/activities/New2Activity.java
@@ -0,0 +1,59 @@
+package cc.winboll.studio.winboll.activities;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/03/25 11:46:40
+ * @Describe 测试窗口2
+ */
+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;
+
+ @Override
+ public String getTag() {
+ return TAG;
+ }
+
+ //LogView mLogView;
+
+
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_new2);
+
+// mLogView = findViewById(R.id.logview);
+// mLogView.start();
+ mToolbar = findViewById(R.id.toolbar);
+ setSupportActionBar(mToolbar);
+
+ }
+
+
+ public void onCloseThisActivity(View view) {
+ //WinBoLLActivityManager.getInstance().finish(this);
+ }
+
+ public void onCloseAllActivity(View view) {
+ //WinBoLLActivityManager.getInstance().finishAll();
+ }
+
+ public void onNewActivity(View view) {
+ //WinBoLLActivityManager.getInstance().startWinBoLLActivity(this, NewActivity.class);
+ }
+}
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
new file mode 100644
index 0000000..e3b6530
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/activities/NewActivity.java
@@ -0,0 +1,56 @@
+package cc.winboll.studio.winboll.activities;
+
+/**
+ * @Author ZhanGSKen&豆包大模型
+ * @Date 2025/03/25 05:04:22
+ * @Describe
+ */
+import android.os.Bundle;
+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;
+ //LogView mLogView;
+
+ @Override
+ public String getTag() {
+ return TAG;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_new);
+// mLogView = findViewById(R.id.logview);
+// mLogView.start();
+ mToolbar = findViewById(R.id.toolbar);
+ setSupportActionBar(mToolbar);
+
+ }
+
+
+ public void onCloseThisActivity(View view) {
+ WinBoLLActivityManager.getInstance().finish(this);
+ }
+
+ public void onCloseAllActivity(View view) {
+ WinBoLLActivityManager.getInstance().finishAll();
+ }
+
+ public void onNew2Activity(View view) {
+ // WinBoLLActivityManager.getInstance().startWinBoLLActivity(App.getInstance(), New2Activity.class);
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/activities/PatternLockActivity.java b/winboll/src/main/java/cc/winboll/studio/winboll/activities/PatternLockActivity.java
new file mode 100644
index 0000000..a52414f
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/activities/PatternLockActivity.java
@@ -0,0 +1,306 @@
+package cc.winboll.studio.winboll.activities;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.MotionEvent;
+import android.widget.FrameLayout;
+import androidx.appcompat.widget.Toolbar;
+import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
+import cc.winboll.studio.winboll.R;
+
+public class PatternLockActivity extends BaseWinBoLLActivity {
+
+ public static final String TAG = "PatternLockActivity";
+
+ private static final int DOT_RADIUS = 8;
+ private static final int PATTERN_ERROR_DURATION = 1500;
+
+ private static final String PREFS_NAME = "pattern_lock_prefs";
+ static final String KEY_LOCK_PATTERN = "lock_pattern";
+ static final String KEY_ERROR_STATE = "error_state";
+ private static final String KEY_ERROR_REPEAT_PATTERN = "error_repeat_pattern";
+
+ private boolean mIsInErrorState;
+ private boolean mNeedRestart;
+ private Handler mHandler;
+
+ private FrameLayout mContainer;
+ private PatternView mPatternView;
+
+ public PatternLockActivity() {
+ mHandler = new Handler(Looper.getMainLooper());
+ }
+
+ PatternLockActivity(Context context) {
+ mHandler = new Handler(Looper.getMainLooper());
+ }
+
+ @Override
+ public Activity getActivity() {
+ return this;
+ }
+
+ @Override
+ public String getTag() {
+ return TAG;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_pattern_lock);
+
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ getSupportActionBar().setSubtitle(TAG);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ toolbar.setNavigationOnClickListener(new android.view.View.OnClickListener() {
+ @Override
+ public void onClick(android.view.View v) {
+ finish();
+ }
+ });
+
+ mContainer = findViewById(R.id.container);
+ mPatternView = new PatternView(this);
+ mContainer.addView(mPatternView);
+ mPatternView.invalidate();
+
+ mNeedRestart = false;
+ boolean isEnoughPoints = savedInstanceIsEnoughPoints();
+
+ if (savedInstanceState != null) {
+ mIsInErrorState = savedInstanceState.getBoolean(KEY_ERROR_STATE, false);
+ mNeedRestart = savedInstanceState.getBoolean(KEY_ERROR_REPEAT_PATTERN, false);
+ }
+
+ if (mIsInErrorState) {
+ mPatternView.invalidate();
+ }
+ }
+
+ boolean savedInstanceIsEnoughPoints() {
+ int count = 0;
+ if (mPatternView != null) {
+ for (int i = 0; i < 9; i++) {
+ if (mPatternView.mDotState[i] == 1) {
+ count++;
+ }
+ }
+ }
+ return count >= 4 || count == 0;
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ if (mIsInErrorState) {
+ outState.putBoolean(KEY_ERROR_STATE, mIsInErrorState);
+ }
+ if (mNeedRestart) {
+ outState.putBoolean(KEY_ERROR_REPEAT_PATTERN, mNeedRestart);
+ }
+ super.onSaveInstanceState(outState);
+ }
+
+ private void showErrorState() {
+ mIsInErrorState = true;
+ invalidatePattern();
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ mIsInErrorState = false;
+ SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
+ prefs.edit().putBoolean(KEY_ERROR_STATE, false).apply();
+ invalidatePattern();
+ if (mPatternView != null) mPatternView.invalidate();
+ }
+ }, PATTERN_ERROR_DURATION);
+ }
+
+ private void clearErrorState() {
+ mIsInErrorState = false;
+ SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
+ prefs.edit().putBoolean(KEY_ERROR_STATE, false).apply();
+ invalidatePattern();
+ if (mPatternView != null) mPatternView.invalidate();
+ }
+
+ private void showErrorToast() {
+ android.widget.Toast.makeText(this, "图案点数不足,请重新绘制",
+ android.widget.Toast.LENGTH_SHORT).show();
+ mNeedRestart = true;
+ }
+
+ private void showSuccessDialog() {
+ android.app.AlertDialog alertDialog = new android.app.AlertDialog.Builder(this)
+ .setTitle("设置成功")
+ .setMessage("图案密码已设置成功")
+ .setPositiveButton("确定", new android.content.DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(android.content.DialogInterface dialog, int which) {
+ finish();
+ }
+ })
+ .setCancelable(false)
+ .create();
+ alertDialog.show();
+ }
+
+ void finishWithRestart() {
+ finish();
+ }
+
+ private void invalidatePattern() {
+ if (mPatternView != null) {
+ mPatternView.invalidate();
+ }
+ }
+
+ class PatternView extends FrameLayout {
+ int mPatternSize = 0;
+ int MAX_DOT_COUNT = 9;
+ int[] mDotX = new int[MAX_DOT_COUNT];
+ int[] mDotY = new int[MAX_DOT_COUNT];
+ int[] mDotState = new int[MAX_DOT_COUNT];
+ Bitmap mDotBitmap;
+ Paint mPaintConnector;
+ Paint mPaintErrorBackground;
+ int mDotCount = 0;
+
+ PatternView(Context context) {
+ super(context);
+ setBackgroundColor(Color.WHITE);
+ for (int i = 0; i < MAX_DOT_COUNT; i++) {
+ mDotX[i] = -1;
+ mDotY[i] = -1;
+ mDotState[i] = 0;
+ }
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ if (w == 0 || h == 0) return;
+ mPatternSize = w > h ? h : w;
+ int grid = 3;
+ int cell = mPatternSize / grid;
+
+ for (int i = 0; i < MAX_DOT_COUNT; i++) {
+ mDotX[i] = (i % grid) * cell + cell / 2 - cell / 24;
+ mDotY[i] = (i / grid) * cell + cell / 2 - cell / 24;
+ mDotState[i] = 0;
+ }
+
+ if (mDotBitmap == null) {
+ mDotBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.dot_darkgreen_dark);
+ }
+ if (mPaintConnector == null) {
+ mPaintConnector = new Paint(Paint.FILTER_BITMAP_FLAG);
+ mPaintConnector.setColor(-0xFF006400);
+ }
+ if (mPaintErrorBackground == null) {
+ mPaintErrorBackground = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mPaintErrorBackground.setColor(Color.RED);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (mDotCount > 0) return false;
+
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ invalidate();
+ return true;
+ case MotionEvent.ACTION_MOVE:
+ float x = event.getX();
+ float y = event.getY();
+
+ for (int i = 0; i < MAX_DOT_COUNT; i++) {
+ int dx = (int) Math.abs(x - mDotX[i]);
+ int dy = (int) Math.abs(y - mDotY[i]);
+ if (dx <= DOT_RADIUS && dy <= DOT_RADIUS && mDotState[i] == 0) {
+ mDotState[i] = 1;
+ mDotCount++;
+ }
+ }
+
+ for (int i = 0; i < mDotCount - 1; i++) {
+ int a = -1, b = -1;
+ for (int k = 0; k < MAX_DOT_COUNT; k++) {
+ if (mDotState[k] == 1) {
+ if (a < 0) a = k;
+ else b = k;
+ }
+ }
+ if (a >= 0 && b >= 0) {
+ a = Math.min(a, b);
+ b = Math.max(a, b);
+ }
+ if (mDotState[a] == 1 && mDotState[b] == 1) {
+ int dx = mDotX[b] - mDotX[a];
+ int dy = mDotY[b] - mDotY[a];
+ if ((Math.abs(dx) <= 1 && Math.abs(dy) <= 1) ||
+ (Math.abs(dx) <= 2 && Math.abs(dy) <= 1)) {
+ if (mDotState[b] == 1) {
+ for (int k = a + 1; k < b; k++) {
+ if (mDotState[k] == 0) {
+ mDotState[k] = 1;
+ }
+ }
+ mDotCount += (b - a - 1);
+ }
+ }
+ }
+ }
+ invalidate();
+ return true;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ if (mDotCount < 4) {
+ showErrorState();
+ showErrorToast();
+ }
+ break;
+ }
+ return true;
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ if (mPatternSize == 0) return;
+
+ int activeCount = 0;
+ for (int i = 0; i < MAX_DOT_COUNT; i++) {
+ if (mDotState[i] == 1) activeCount++;
+ }
+
+ if (activeCount == 0) {
+ mPaintErrorBackground.setAlpha(50);
+ } else {
+ mPaintErrorBackground.setAlpha(mIsInErrorState ? 80 : 60);
+ }
+
+ canvas.clipRect(0, 0, mPatternSize * 80 / 100, mPatternSize * 80 / 100);
+
+ canvas.drawRect(0, 0, mPatternSize, mPatternSize, mPaintErrorBackground);
+
+ if (mDotBitmap != null) {
+ for (int i = 0; i < MAX_DOT_COUNT; i++) {
+ if (mDotState[i] == 1) {
+ canvas.drawBitmap(mDotBitmap, mDotX[i], mDotY[i], mPaintConnector);
+ }
+ }
+ }
+ }
+ }
+}
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
new file mode 100644
index 0000000..1dab4e6
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/activities/SettingsActivity.java
@@ -0,0 +1,48 @@
+package cc.winboll.studio.winboll.activities;
+
+import android.os.Bundle;
+import android.view.View;
+import androidx.appcompat.widget.Toolbar;
+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 BaseWinBoLLActivity {
+
+ @Override
+ public Activity getActivity() {
+ return this;
+ }
+
+
+ public static final String TAG = "SettingsActivity";
+
+ @Override
+ public String getTag() {
+ return TAG;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_settings);
+
+ // 设置工具栏
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ getSupportActionBar().setSubtitle(TAG);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ toolbar.setNavigationOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ finish(); // 点击导航栏返回按钮,触发 finish()
+ }
+ });
+
+ }
+
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/activities/ShortcutActionActivity.java b/winboll/src/main/java/cc/winboll/studio/winboll/activities/ShortcutActionActivity.java
new file mode 100644
index 0000000..8ebd744
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/activities/ShortcutActionActivity.java
@@ -0,0 +1,50 @@
+package cc.winboll.studio.winboll.activities;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import cc.winboll.studio.libappbase.ToastUtils;
+import cc.winboll.studio.winboll.App;
+import cc.winboll.studio.winboll.R;
+import cc.winboll.studio.winboll.utils.APPPlusUtils;
+
+/**
+ * @Author ZhanGSKen&豆包大模型
+ * @Date 2025/11/27 09:00
+ * @Describe 应用快捷方式活动类
+ */
+public class ShortcutActionActivity extends Activity {
+
+ public static final String TAG = "ShortcutActionActivity";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // 处理应用级别的切换请求
+ handleSwitchRequest();
+ finish();
+ }
+
+ /**
+ * 处理应用图标快捷菜单的请求
+ */
+ private void handleSwitchRequest() {
+ //ToastUtils.show("handleSwitchRequest");
+ Intent intent = getIntent();
+ if (intent != null && "switchto_en1".equals(intent.getDataString())) {
+ APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_EN1);
+ ToastUtils.show("切换至" + getString(R.string.app_name) + "图标");
+ //moveTaskToBack(true);
+ }
+ if (intent != null && "switchto_cn1".equals(intent.getDataString())) {
+ APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_CN1);
+ ToastUtils.show("切换至" + getString(R.string.app_name_cn1) + "图标");
+ //moveTaskToBack(true);
+ }
+ if (intent != null && "switchto_cn2".equals(intent.getDataString())) {
+ APPPlusUtils.switchAppLauncherToComponent(this, App.COMPONENT_CN2);
+ ToastUtils.show("切换至" + getString(R.string.app_name_cn2) + "图标");
+ //moveTaskToBack(true);
+ }
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/activities/WinBoLLUnitTestActivity.java b/winboll/src/main/java/cc/winboll/studio/winboll/activities/WinBoLLUnitTestActivity.java
new file mode 100644
index 0000000..177c5a0
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/activities/WinBoLLUnitTestActivity.java
@@ -0,0 +1,172 @@
+package cc.winboll.studio.winboll.activities;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.CheckBox;
+import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
+import cc.winboll.studio.libappbase.CrashHandler;
+import cc.winboll.studio.libappbase.GlobalApplication;
+import cc.winboll.studio.libappbase.GlobalCrashActivity;
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.libappbase.ToastUtils;
+import cc.winboll.studio.winboll.App;
+import cc.winboll.studio.winboll.R;
+import cc.winboll.studio.winboll.services.MainService;
+import cc.winboll.studio.winboll.services.TestDemoBindService;
+import cc.winboll.studio.winboll.services.TestDemoService;
+import cc.winboll.studio.winboll.sos.SOS;
+import cc.winboll.studio.winboll.widgets.StatusWidget;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+
+public class WinBoLLUnitTestActivity extends AppCompatActivity {
+
+ public static final String TAG = "MainActivity";
+
+ Toolbar mToolbar;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ToastUtils.show("onCreate");
+ setContentView(R.layout.activity_winbollunittest);
+
+ mToolbar = findViewById(R.id.toolbar);
+ setSupportActionBar(mToolbar);
+
+ CheckBox cbIsDebugMode = findViewById(R.id.activitymainCheckBox1);
+ cbIsDebugMode.setChecked(GlobalApplication.isDebugging());
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ //getMenuInflater().inflate(R.menu.toolbar_main, menu);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+// if(item.getItemId() == R.id.item_yun) {
+// WinBoLLActivityManager.getInstance().startWinBoLLActivity(this, cc.winboll.studio.winboll.activities.YunActivity.class);
+// } else if(item.getItemId() == R.id.item_logon) {
+// WinBoLLActivityManager.getInstance().startWinBoLLActivity(this, cc.winboll.studio.winboll.activities.LogonActivity.class);
+// }
+ // 在switch语句中处理每个ID,并在处理完后返回true,未处理的情况返回false。
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ Intent intentAPPWidget = new Intent(this, StatusWidget.class);
+ intentAPPWidget.setAction(StatusWidget.ACTION_STATUS_UPDATE);
+ sendBroadcast(intentAPPWidget);
+ }
+
+ public void onSwitchDebugMode(View view) {
+ boolean isDebuging = ((CheckBox)view).isChecked();
+ GlobalApplication.setIsDebugging(isDebuging);
+ GlobalApplication.saveDebugStatus((App.getInstance()));
+ }
+
+ public void onPreviewGlobalCrashActivity(View view) {
+ Intent intent = new Intent(this, GlobalCrashActivity.class);
+ intent.putExtra(CrashHandler.EXTRA_CRASH_LOG, "Demo log...");
+ startActivity(intent);
+ }
+
+ public void onStartCenter(View view) {
+ MainService.startMainService(this);
+ }
+
+ public void onStopCenter(View view) {
+ MainService.stopMainService(this);
+ }
+
+ public void onTestStopMainServiceWithoutSettingEnable(View view) {
+ LogUtils.d(TAG, "onTestStopMainServiceWithoutSettingEnable");
+ stopService(new Intent(this, MainService.class));
+ }
+
+ public void onTestUseComponentStartService(View view) {
+ LogUtils.d(TAG, "onTestUseComponentStartService");
+
+ // 目标服务的包名和类名
+ String packageName = this.getPackageName();
+ String serviceClassName = TestDemoService.class.getName();
+
+ // 构建Intent
+ Intent intentService = new Intent();
+ intentService.setComponent(new ComponentName(packageName, serviceClassName));
+
+ startService(intentService);
+ }
+
+ public void onTestDemoServiceSOS(View view) {
+ Intent intent = new Intent(this, TestDemoService.class);
+ stopService(intent);
+ if (App.isDebugging()) {
+ SOS.sosToAppBaseBeta(this, TestDemoService.class.getName());
+ } else {
+ SOS.sosToAppBase(this, TestDemoService.class.getName());
+ }
+ }
+
+ public void onSartTestDemoService(View view) {
+ Intent intent = new Intent(this, TestDemoService.class);
+ intent.setAction(TestDemoService.ACTION_ENABLE);
+ startService(intent);
+
+ }
+
+ public void onStopTestDemoService(View view) {
+ Intent intent = new Intent(this, TestDemoService.class);
+ intent.setAction(TestDemoService.ACTION_DISABLE);
+ startService(intent);
+
+ Intent intentStop = new Intent(this, TestDemoService.class);
+ stopService(intentStop);
+ }
+
+ public void onStopTestDemoServiceNoSettings(View view) {
+ Intent intent = new Intent(this, TestDemoService.class);
+ stopService(intent);
+ }
+
+ public void onSartTestDemoBindService(View view) {
+ Intent intent = new Intent(this, TestDemoBindService.class);
+ intent.setAction(TestDemoBindService.ACTION_ENABLE);
+ startService(intent);
+
+ }
+
+ public void onStopTestDemoBindService(View view) {
+ Intent intent = new Intent(this, TestDemoBindService.class);
+ intent.setAction(TestDemoBindService.ACTION_DISABLE);
+ startService(intent);
+
+ Intent intentStop = new Intent(this, TestDemoBindService.class);
+ stopService(intentStop);
+ }
+
+ public void onStopTestDemoBindServiceNoSettings(View view) {
+ Intent intent = new Intent(this, TestDemoBindService.class);
+ stopService(intent);
+ }
+
+ public void onTestOpenNewActivity(View view) {
+ WinBoLLActivityManager.getInstance().startWinBoLLActivity(this, NewActivity.class);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ }
+
+
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/activities/YunActivity.java b/winboll/src/main/java/cc/winboll/studio/winboll/activities/YunActivity.java
new file mode 100644
index 0000000..8ab5fb6
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/activities/YunActivity.java
@@ -0,0 +1,126 @@
+package cc.winboll.studio.winboll.activities;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/06/04 11:06
+ * @Describe 云宝云
+ */
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.RadioButton;
+import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
+import cc.winboll.studio.libappbase.BuildConfig;
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.libappbase.LogView;
+import cc.winboll.studio.winboll.R;
+import java.io.IOException;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+
+public class YunActivity extends Activity implements IWinBoLLActivity {
+
+ public static final String TAG = "YunActivity";
+
+ public static final String DEBUG_HOST = "http://10.8.0.250:456";
+ public static final String YUN_HOST = "https://yun.winboll.cc";
+
+ String mHost = "";
+ RadioButton mrbYunHost;
+ RadioButton mrbDebugHost;
+ LogView mLogView;
+
+ @Override
+ public Activity getActivity() {
+ return this;
+ }
+
+ @Override
+ public String getTag() {
+ return TAG;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_yun);
+ mLogView = findViewById(R.id.logview);
+ mLogView.start();
+
+ mHost = BuildConfig.DEBUG ? DEBUG_HOST: YUN_HOST;
+ if (BuildConfig.DEBUG) {
+ mrbYunHost = findViewById(R.id.rb_yunhost);
+ mrbDebugHost = findViewById(R.id.rb_debughost);
+ mrbYunHost.setChecked(!BuildConfig.DEBUG);
+ mrbDebugHost.setChecked(BuildConfig.DEBUG);
+ } else {
+ findViewById(R.id.ll_hostbar).setVisibility(View.GONE);
+ }
+ }
+
+ public void onSwitchHost(View view) {
+ if (view.getId() == R.id.rb_yunhost) {
+ mrbDebugHost.setChecked(false);
+ mHost = YUN_HOST;
+ } else if (view.getId() == R.id.rb_debughost) {
+ mrbYunHost.setChecked(false);
+ mHost = DEBUG_HOST;
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mLogView.start();
+ }
+
+ public void onTestYun(View view) {
+ LogUtils.d(TAG, "onTestYun");
+ (new Thread(new Runnable(){
+ @Override
+ public void run() {
+ testYun();
+ }
+ })).start();
+ }
+
+ void testYun() {
+ OkHttpClient client = new OkHttpClient();
+ Request request = new Request.Builder()
+ .url(mHost + "/backups/")
+ .build();
+
+ Response response = null;
+ try {
+ response = client.newCall(request).execute();
+ if (response.isSuccessful()) {
+ String responseBody = "";
+ if (response.body() != null) {
+ responseBody = response.body().string();
+ }
+
+ // 正则匹配:任意主机名 -> Test OK(主机名部分匹配非空字符)
+ boolean isMatch = responseBody.matches(".+? -> Test OK");
+
+ if (isMatch) {
+ LogUtils.d(TAG, responseBody);
+ } else {
+ LogUtils.d(TAG, "响应内容不匹配,内容:" + responseBody);
+ }
+ } else {
+ LogUtils.d(TAG, "请求失败,状态码:" + response.code());
+ }
+ } catch (IOException e) {
+ LogUtils.d(TAG, "读取响应体失败:" + e.getMessage());
+ } catch (Exception e) {
+ LogUtils.d(TAG, "异常:" + e.getMessage());
+ e.printStackTrace(); // Java 7 需显式打印堆栈
+ } finally {
+ // 手动关闭 Response(Java 7 不支持 try-with-resources)
+ if (response != null && response.body() != null) {
+ response.body().close();
+ }
+ }
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/applications/MyTermuxActivity.java b/winboll/src/main/java/cc/winboll/studio/winboll/applications/MyTermuxActivity.java
new file mode 100644
index 0000000..6bd2b29
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/applications/MyTermuxActivity.java
@@ -0,0 +1,237 @@
+package cc.winboll.studio.winboll.applications;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.winboll.R;
+import cc.winboll.studio.winboll.models.TermuxButtonManager;
+import cc.winboll.studio.winboll.models.TermuxButtonModel;
+import cc.winboll.studio.winboll.termux.TermuxCommandExecutor;
+import java.util.ArrayList;
+
+public class MyTermuxActivity extends AppCompatActivity {
+
+ public static final String TAG = "MyTermuxActivity";
+
+ private Toolbar mToolbar;
+ private ListView mListView;
+ private Button mBtnAdd;
+ private ButtonAdapter mAdapter;
+ private ArrayList mButtonList;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_my_termux);
+
+ initToolbar();
+ initListView();
+ initAddButton();
+ refreshList();
+ }
+
+ private void initToolbar() {
+ mToolbar = findViewById(R.id.toolbar);
+ if (mToolbar != null) {
+ setSupportActionBar(mToolbar);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ finish();
+ }
+ });
+ }
+ }
+
+ private void initListView() {
+ mListView = findViewById(R.id.list_termux_buttons);
+ mButtonList = new ArrayList();
+ mAdapter = new ButtonAdapter();
+ mListView.setAdapter(mAdapter);
+
+ mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ TermuxButtonModel model = mButtonList.get(position);
+ TermuxCommandExecutor.openTermuxBash(MyTermuxActivity.this,
+ model.getExeCommand(), model.getWorkDir());
+ }
+ });
+
+ mListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
+ @Override
+ public boolean onItemLongClick(AdapterView> parent, View view, int position, long id) {
+ showContextMenu(position);
+ return true;
+ }
+ });
+ }
+
+ private void initAddButton() {
+ mBtnAdd = findViewById(R.id.btn_add_termux_button);
+ mBtnAdd.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ showButtonDialog(-1, null);
+ }
+ });
+ }
+
+ private void refreshList() {
+ mButtonList.clear();
+ ArrayList loaded = TermuxButtonManager.loadButtons(this);
+ if (loaded != null) {
+ mButtonList.addAll(loaded);
+ }
+ mAdapter.notifyDataSetChanged();
+ }
+
+ private void showContextMenu(final int position) {
+ final TermuxButtonModel model = mButtonList.get(position);
+ String[] items = new String[]{
+ getString(R.string.menu_execute),
+ getString(R.string.menu_edit),
+ getString(R.string.menu_delete),
+ getString(R.string.menu_cancel)
+ };
+ new AlertDialog.Builder(this)
+ .setTitle(model.getButtonName())
+ .setItems(items, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == 0) {
+ TermuxCommandExecutor.openTermuxBash(MyTermuxActivity.this,
+ model.getExeCommand(), model.getWorkDir());
+ } else if (which == 1) {
+ showButtonDialog(position, model);
+ } else if (which == 2) {
+ showDeleteConfirmDialog(position);
+ }
+ }
+ })
+ .show();
+ }
+
+ private void showDeleteConfirmDialog(final int position) {
+ new AlertDialog.Builder(this)
+ .setTitle(getString(R.string.dialog_delete_title))
+ .setMessage(getString(R.string.dialog_delete_message) + mButtonList.get(position).getButtonName())
+ .setPositiveButton(getString(R.string.dialog_confirm), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ TermuxButtonManager.deleteButton(MyTermuxActivity.this, mButtonList, position);
+ refreshList();
+ Toast.makeText(MyTermuxActivity.this, R.string.toast_deleted, Toast.LENGTH_SHORT).show();
+ }
+ })
+ .setNegativeButton(getString(R.string.dialog_cancel), null)
+ .show();
+ }
+
+ private void showButtonDialog(final int index, final TermuxButtonModel model) {
+ final boolean isEdit = (model != null);
+
+ LinearLayout layout = new LinearLayout(this);
+ layout.setOrientation(LinearLayout.VERTICAL);
+ layout.setPadding(40, 20, 40, 20);
+
+ final EditText etName = new EditText(this);
+ etName.setHint(R.string.hint_button_name);
+ if (model != null) {
+ etName.setText(model.getButtonName());
+ }
+ layout.addView(etName);
+
+ final EditText etCommand = new EditText(this);
+ etCommand.setHint(R.string.hint_exe_command);
+ if (model != null) {
+ etCommand.setText(model.getExeCommand());
+ }
+ layout.addView(etCommand);
+
+ final EditText etWorkDir = new EditText(this);
+ etWorkDir.setHint(R.string.hint_work_dir);
+ if (model != null) {
+ etWorkDir.setText(model.getWorkDir());
+ }
+ layout.addView(etWorkDir);
+
+ int titleResId = isEdit ? R.string.dialog_edit_title : R.string.dialog_add_title;
+ new AlertDialog.Builder(this)
+ .setTitle(titleResId)
+ .setView(layout)
+ .setPositiveButton(getString(R.string.dialog_save), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ String name = etName.getText().toString().trim();
+ String command = etCommand.getText().toString().trim();
+ String workDir = etWorkDir.getText().toString().trim();
+ if (name.isEmpty() || command.isEmpty()) {
+ Toast.makeText(MyTermuxActivity.this, R.string.toast_fields_required, Toast.LENGTH_SHORT).show();
+ return;
+ }
+ TermuxButtonModel newModel = new TermuxButtonModel();
+ newModel.setButtonName(name);
+ newModel.setExeCommand(command);
+ newModel.setWorkDir(workDir);
+ if (isEdit) {
+ TermuxButtonManager.updateButton(MyTermuxActivity.this, mButtonList, index, newModel);
+ } else {
+ TermuxButtonManager.addButton(MyTermuxActivity.this, mButtonList, newModel);
+ }
+ refreshList();
+ }
+ })
+ .setNegativeButton(getString(R.string.dialog_cancel), null)
+ .show();
+ }
+
+ private class ButtonAdapter extends BaseAdapter {
+ @Override
+ public int getCount() {
+ return mButtonList.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mButtonList.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ TextView tv;
+ if (convertView == null) {
+ tv = new TextView(MyTermuxActivity.this);
+ tv.setPadding(30, 20, 30, 20);
+ tv.setTextSize(16);
+ tv.setMinHeight(80);
+ } else {
+ tv = (TextView) convertView;
+ }
+
+ TermuxButtonModel model = mButtonList.get(position);
+ tv.setText(model.getButtonName() + "\n" + model.getExeCommand());
+ tv.setTextColor(getResources().getColor(android.R.color.white));
+ return tv;
+ }
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/fragments/BrowserFragment.java b/winboll/src/main/java/cc/winboll/studio/winboll/fragments/BrowserFragment.java
new file mode 100644
index 0000000..aa7b4ca
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/fragments/BrowserFragment.java
@@ -0,0 +1,350 @@
+package cc.winboll.studio.winboll.fragments;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import cc.winboll.studio.winboll.MainActivity;
+import cc.winboll.studio.winboll.R;
+import cc.winboll.studio.winboll.views.WinBoLLView;
+import java.util.ArrayList;
+import android.app.Activity;
+
+/**
+ * @Author ZhanGSKen&豆包大模型
+ * @Date 2025/11/27 11:09
+ * @Describe 浏览器Fragment(Java 7 语法完整版,新增Handler消息接收)
+ * 适配Java 7特性,支持接收应用内消息(如MSG_HOMEPAGE跳转首页)
+ */
+public class BrowserFragment extends Fragment implements View.OnClickListener, WinBoLLView.OnPageStatusListener {
+
+ // 控件声明(Java 7 成员变量显式声明)
+ private EditText mEtUrl;
+ private Button mBtnLoad;
+ private Button mBtnRefresh;
+ private Button mBtnStop;
+ private Button mBtnForward;
+ private Button mBtnBack;
+ private ProgressBar mProgressBar;
+ private WinBoLLView mWinBoLLView;
+ public static ArrayList _mUrlLoadHistory = new ArrayList();
+
+ // ------------------- 新增:Handler 消息定义(应用内通信) -------------------
+ // 消息标识:跳转首页(winboll.cc)
+ public static final int MSG_HOMEPAGE = 1001;
+ // 跳转到历史记录位置
+ public static final int MSG_HISTORY_POSITION = 1002;
+ // 打开外部应用传入的 URL
+ public static final int MSG_OPEN_URL = 1003;
+ // 自定义Handler(接收应用内其他页面发送的消息)
+ private Handler mBrowserHandler;
+
+ // 单例创建方法(Java 7 显式工厂模式)
+ public static BrowserFragment newInstance() {
+ return new BrowserFragment();
+ }
+
+ // 创建带初始URL的实例(供外部应用调用时使用)
+ public static BrowserFragment newInstance(String initialUrl) {
+ BrowserFragment fragment = new BrowserFragment();
+ Bundle args = new Bundle();
+ args.putString("initial_url", initialUrl);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ // 加载布局(Java 7 显式强转,无菱形语法)
+ View view = inflater.inflate(R.layout.fragment_browser, container, false);
+ // 清理旧历史记录
+ _mUrlLoadHistory.clear();
+
+ // 初始化控件
+ initViews(view);
+ // 绑定事件
+ initEvents();
+ // 初始化WinBoLLView
+ initWinBoLLView();
+ // ------------------- 新增:初始化Handler(关键) -------------------
+ initHandler();
+ return view;
+ }
+
+ /**
+ * 初始化控件(Java 7 显式绑定,无Stream简化)
+ */
+ private void initViews(View view) {
+ mEtUrl = (EditText) view.findViewById(R.id.et_url);
+ mBtnLoad = (Button) view.findViewById(R.id.btn_load);
+ mBtnRefresh = (Button) view.findViewById(R.id.btn_refresh);
+ mBtnStop = (Button) view.findViewById(R.id.btn_stop);
+ mBtnForward = (Button) view.findViewById(R.id.btn_forward);
+ mBtnBack = (Button) view.findViewById(R.id.btn_back);
+ mProgressBar = (ProgressBar) view.findViewById(R.id.progress_bar);
+ mWinBoLLView = (WinBoLLView) view.findViewById(R.id.winboll_webview);
+ }
+
+ /**
+ * 绑定点击事件(Java 7 匿名内部类,无Lambda)
+ */
+ private void initEvents() {
+ // 功能按钮点击事件
+ mBtnLoad.setOnClickListener(this);
+ mBtnRefresh.setOnClickListener(this);
+ mBtnStop.setOnClickListener(this);
+ mBtnForward.setOnClickListener(this);
+ mBtnBack.setOnClickListener(this);
+
+ // 输入框软键盘“前往”按钮事件(Java 7 匿名内部类实现)
+ mEtUrl.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, android.view.KeyEvent event) {
+ // 处理软键盘“前往”点击
+ loadUrlFromInput();
+ return true;
+ }
+ });
+ }
+
+ /**
+ * 初始化WinBoLLView(Java 7 显式调用,无方法引用)
+ */
+ private void initWinBoLLView() {
+ // 绑定进度条
+ mWinBoLLView.setProgressBar(mProgressBar);
+ // 设置页面状态监听(this 实现 OnPageStatusListener)
+ mWinBoLLView.setOnPageStatusListener(this);
+
+ // 检查是否有外部传入的初始 URL
+ String initialUrl = null;
+ if (getArguments() != null) {
+ initialUrl = getArguments().getString("initial_url");
+ }
+
+ if (initialUrl != null && !initialUrl.isEmpty()) {
+ // 使用外部传入的 URL
+ mWinBoLLView.loadUrlSafe(initialUrl);
+ mEtUrl.setText(initialUrl);
+ } else {
+ // 预加载默认页面(winboll.cc 首页)
+ String defaultUrl = "https://www.winboll.cc";
+ mWinBoLLView.loadUrlSafe(defaultUrl);
+ mEtUrl.setText(defaultUrl);
+ }
+ }
+
+ // ------------------- 新增:初始化Handler(接收应用内消息) -------------------
+ private void initHandler() {
+ // Java 7 匿名内部类实现Handler(主线程中创建,用于更新UI)
+ mBrowserHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ super.handleMessage(msg);
+ // 根据消息标识处理不同逻辑
+ switch (msg.what) {
+ case MSG_HOMEPAGE:
+ // 处理“跳转首页”消息:加载winboll.cc
+ String homeUrl = "https://www.winboll.cc";
+ mWinBoLLView.loadUrlSafe(homeUrl);
+ mEtUrl.setText(homeUrl);
+ showToast("已跳转至首页");
+ break;
+ case MSG_HISTORY_POSITION:
+ int position = (int)msg.obj;
+ if(-1 < position && position < _mUrlLoadHistory.size()) {
+ // 处理“跳转首页”消息:加载winboll.cc
+ String historyUrl = _mUrlLoadHistory.get(position);
+ mWinBoLLView.loadUrlSafe(historyUrl);
+ mEtUrl.setText(historyUrl);
+ //showToast("已跳转至" + historyUrl);
+ }
+ break;
+ case MSG_OPEN_URL:
+ String openUrl = (String) msg.obj;
+ if (openUrl != null && !openUrl.isEmpty()) {
+ mWinBoLLView.loadUrlSafe(openUrl);
+ mEtUrl.setText(openUrl);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ };
+ }
+
+ /**
+ * 点击事件处理(Java 7 switch-case 语句,无增强switch)
+ */
+ @Override
+ public void onClick(View v) {
+ int id = v.getId();
+ if (id == R.id.btn_load) {
+ // 加载输入框中的URL
+ loadUrlFromInput();
+ } else if (id == R.id.btn_refresh) {
+ // 刷新当前页面
+ mWinBoLLView.refreshPage();
+ } else if (id == R.id.btn_stop) {
+ // 停止页面加载
+ mWinBoLLView.stopPageLoad();
+ } else if (id == R.id.btn_forward) {
+ // 前进(无历史则提示)
+ if (!mWinBoLLView.goForwardSafe()) {
+ showToast("无前进历史");
+ }
+ } else if (id == R.id.btn_back) {
+ // 后退(无历史则提示)
+ if (!mWinBoLLView.goBackSafe()) {
+ showToast("无后退历史");
+ }
+ }
+ }
+
+ /**
+ * 从输入框获取URL并加载(Java 7 显式空值校验)
+ */
+ private void loadUrlFromInput() {
+ // 空值校验(Java 7 显式判断,无Objects.requireNonNull)
+ if (mEtUrl == null) {
+ showToast("控件初始化失败");
+ return;
+ }
+ String url = mEtUrl.getText().toString().trim();
+ // 调用WinBoLLView安全加载方法
+ mWinBoLLView.loadUrlSafe(url);
+ // 隐藏软键盘
+ hideSoftKeyboard();
+ }
+
+ /**
+ * 隐藏软键盘(Java 7 显式获取系统服务,无Lambda简化)
+ */
+ private void hideSoftKeyboard() {
+ if (getActivity() == null) {
+ return;
+ }
+ // 获取InputMethodManager(Java 7 显式强转)
+ InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(android.content.Context.INPUT_METHOD_SERVICE);
+ if (imm != null && getActivity().getCurrentFocus() != null) {
+ imm.hideSoftInputFromWindow(getActivity().getCurrentFocus().getWindowToken(), 0);
+ }
+ }
+
+ /**
+ * 显示Toast提示(Java 7 简化封装,避免重复代码)
+ */
+ private void showToast(String msg) {
+ if (getActivity() == null || msg == null) {
+ return;
+ }
+ // Java 7 显式创建Toast,无Toast.makeText简化链式调用
+ Toast toast = Toast.makeText(getActivity(), msg, Toast.LENGTH_SHORT);
+ toast.show();
+ }
+
+ // ------------------- WinBoLLView.OnPageStatusListener 实现(Java 7 显式重写) -------------------
+ @Override
+ public void onPageStarted(String url) {
+ // 页面开始加载:更新输入框URL(Java 7 显式非空判断)
+ if (mEtUrl != null && url != null) {
+ mEtUrl.setText(url);
+ }
+ }
+
+ @Override
+ public void onPageFinished(String url) {
+ // 页面加载完成:更新输入框URL
+ if (mEtUrl != null && url != null) {
+ mEtUrl.setText(url);
+ addUrlToHistory(url);
+ }
+ }
+
+ @Override
+ public void onPageError(String errorMsg) {
+ // 页面加载错误:显示错误提示
+ showToast("加载失败:" + errorMsg);
+ }
+
+ // ------------------- 新增:对外提供Handler(供其他页面获取并发送消息) -------------------
+ public Handler getBrowserHandler() {
+ return mBrowserHandler;
+ }
+
+ // ------------------- 生命周期管理(防止内存泄漏,Java 7 显式重写) -------------------
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ // 销毁WinBoLLView(释放资源,避免内存泄漏)
+ if (mWinBoLLView != null) {
+ mWinBoLLView.destroyWebView();
+ mWinBoLLView = null;
+ }
+ // ------------------- 新增:移除Handler消息(关键,防止内存泄漏) -------------------
+ if (mBrowserHandler != null) {
+ mBrowserHandler.removeCallbacksAndMessages(null); // 清除所有消息和回调
+ mBrowserHandler = null;
+ }
+ // 置空控件(帮助GC回收)
+ mEtUrl = null;
+ mBtnLoad = null;
+ mBtnRefresh = null;
+ mBtnStop = null;
+ mBtnForward = null;
+ mBtnBack = null;
+ mProgressBar = null;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ // 彻底释放资源
+ if (mWinBoLLView != null) {
+ mWinBoLLView.destroyWebView();
+ mWinBoLLView = null;
+ }
+ // 再次清除Handler(双重保险)
+ if (mBrowserHandler != null) {
+ mBrowserHandler.removeCallbacksAndMessages(null);
+ mBrowserHandler = null;
+ }
+ }
+
+ // 在 BrowserFragment 中添加以下代码(Java 7 语法)
+ /**
+ * 发送URL历史更新消息给MainActivity(当历史列表变化时调用)
+ */
+ private void sendUrlHistoryUpdateMsg() {
+ Message msg = Message.obtain();
+ msg.what = MainActivity.MSG_URLLOADHISTORY_UPDATE;
+ MainActivity.sendMessage(msg);
+ }
+
+ // 调用时机示例(在BrowserFragment加载URL并更新历史列表后调用)
+ // 假设BrowserFragment中有添加URL到历史的方法:
+ private void addUrlToHistory(String url) {
+ if (_mUrlLoadHistory == null) {
+ _mUrlLoadHistory = new ArrayList();
+ }
+ if (!_mUrlLoadHistory.contains(url)) {
+ _mUrlLoadHistory.add(0, url);
+ // 关键:添加历史后发送更新消息,通知MainActivity刷新抽屉菜单
+ sendUrlHistoryUpdateMsg();
+ }
+ }
+}
+
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/fragments/MainFragment.java b/winboll/src/main/java/cc/winboll/studio/winboll/fragments/MainFragment.java
new file mode 100644
index 0000000..4610993
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/fragments/MainFragment.java
@@ -0,0 +1,35 @@
+package cc.winboll.studio.winboll.fragments;
+
+/**
+ * @Author ZhanGSKen&豆包大模型
+ * @Date 2025/09/29 13:15
+ * @Describe MainFragment
+ */
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Switch;
+import androidx.fragment.app.Fragment;
+import cc.winboll.studio.libappbase.ToastUtils;
+import cc.winboll.studio.winboll.R;
+
+
+public class MainFragment extends Fragment {
+
+ public static final String TAG = "MainFragment";
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_main, container, false);
+ Switch swEnablePosition = view.findViewById(R.id.fragmentmainSwitch1);
+ swEnablePosition.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ ToastUtils.show("Position");
+ }
+ });
+ return view;
+ }
+
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/handlers/MainServiceHandler.java b/winboll/src/main/java/cc/winboll/studio/winboll/handlers/MainServiceHandler.java
new file mode 100644
index 0000000..effeb43
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/handlers/MainServiceHandler.java
@@ -0,0 +1,38 @@
+package cc.winboll.studio.winboll.handlers;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/02/14 03:51:40
+ */
+import android.os.Handler;
+import android.os.Message;
+import java.lang.ref.WeakReference;
+import cc.winboll.studio.winboll.services.MainService;
+
+public class MainServiceHandler extends Handler {
+ public static final String TAG = "MainServiceHandler";
+
+ public static final int MSG_REMINDTHREAD = 0;
+
+ WeakReference serviceWeakReference;
+ public MainServiceHandler(MainService service) {
+ serviceWeakReference = new WeakReference(service);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_REMINDTHREAD: // 处理下载完成消息,更新UI
+ {
+ // 显示提醒消息
+ //
+ //LogUtils.d(TAG, "显示提醒消息");
+ MainService mainService = serviceWeakReference.get();
+ if (mainService != null) {
+ mainService.appenMessage((String)msg.obj);
+ }
+ break;
+ }
+ }
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/models/MainServiceBean.java b/winboll/src/main/java/cc/winboll/studio/winboll/models/MainServiceBean.java
new file mode 100644
index 0000000..18ab7a5
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/models/MainServiceBean.java
@@ -0,0 +1,67 @@
+package cc.winboll.studio.winboll.models;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/02/13 07:06:13
+ */
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import cc.winboll.studio.libappbase.models.libs1520000.BaseBean;
+import java.io.IOException;
+
+public class MainServiceBean extends BaseBean {
+
+ public static final String TAG = "MainServiceBean";
+
+ boolean isEnable;
+
+ public MainServiceBean() {
+ this.isEnable = false;
+ }
+
+ public void setIsEnable(boolean isEnable) {
+ this.isEnable = isEnable;
+ }
+
+ public boolean isEnable() {
+ return isEnable;
+ }
+
+ @Override
+ public String getName() {
+ return MainServiceBean.class.getName();
+ }
+
+ @Override
+ public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
+ super.writeThisToJsonWriter(jsonWriter);
+ jsonWriter.name("isEnable").value(isEnable());
+
+ }
+
+ @Override
+ public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
+ if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else {
+ if (name.equals("isEnable")) {
+ setIsEnable(jsonReader.nextBoolean());
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
+ jsonReader.beginObject();
+ while (jsonReader.hasNext()) {
+ String name = jsonReader.nextName();
+ if (!initObjectsFromJsonReader(jsonReader, name)) {
+ jsonReader.skipValue();
+ }
+ }
+ // 结束 JSON 对象
+ jsonReader.endObject();
+ return this;
+ }
+}
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/models/ResponseData.java b/winboll/src/main/java/cc/winboll/studio/winboll/models/ResponseData.java
new file mode 100644
index 0000000..ed78bdc
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/models/ResponseData.java
@@ -0,0 +1,53 @@
+package cc.winboll.studio.winboll.models;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/06/05 11:26
+ */
+
+public class ResponseData {
+
+ public static final String STATUS_SUCCESS = "success";
+ public static final String STATUS_ERROR = "error";
+
+ private String status;
+ private String message;
+ private UserInfoModel data;
+
+ public ResponseData() {
+ this.status = "";
+ this.message = "";
+ this.data = new UserInfoModel();
+ }
+
+ public ResponseData(String status, String message, UserInfoModel data) {
+ this.status = status;
+ this.message = message;
+ this.data = data;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setData(UserInfoModel data) {
+ this.data = data;
+ }
+
+ public UserInfoModel getData() {
+ return data;
+ }
+}
+
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/models/TermuxButtonManager.java b/winboll/src/main/java/cc/winboll/studio/winboll/models/TermuxButtonManager.java
new file mode 100644
index 0000000..154824e
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/models/TermuxButtonManager.java
@@ -0,0 +1,38 @@
+package cc.winboll.studio.winboll.models;
+
+import android.content.Context;
+import cc.winboll.studio.libappbase.models.libs1520000.BaseBean;
+import cc.winboll.studio.winboll.models.TermuxButtonModel;
+import java.util.ArrayList;
+
+public class TermuxButtonManager {
+
+ public static ArrayList loadButtons(Context context) {
+ ArrayList list = new ArrayList();
+ BaseBean.loadBeanList(context, list, TermuxButtonModel.class);
+ return list;
+ }
+
+ public static boolean saveButtons(Context context, ArrayList list) {
+ return BaseBean.saveBeanList(context, list, TermuxButtonModel.class);
+ }
+
+ public static void addButton(Context context, ArrayList list, TermuxButtonModel button) {
+ list.add(button);
+ saveButtons(context, list);
+ }
+
+ public static void updateButton(Context context, ArrayList list, int index, TermuxButtonModel button) {
+ if (index >= 0 && index < list.size()) {
+ list.set(index, button);
+ saveButtons(context, list);
+ }
+ }
+
+ public static void deleteButton(Context context, ArrayList list, int index) {
+ if (index >= 0 && index < list.size()) {
+ list.remove(index);
+ saveButtons(context, list);
+ }
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/models/TermuxButtonModel.java b/winboll/src/main/java/cc/winboll/studio/winboll/models/TermuxButtonModel.java
new file mode 100644
index 0000000..d375a71
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/models/TermuxButtonModel.java
@@ -0,0 +1,155 @@
+package cc.winboll.studio.winboll.models;
+
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import cc.winboll.studio.libappbase.models.libs1520000.BaseBean;
+import java.io.IOException;
+
+/**
+ * @Author 豆包&ZhanGSKen
+ * @Date 2026/04/30 10:47
+ */
+public class TermuxButtonModel extends BaseBean {
+
+ public static final String TAG = "TermuxButtonModel";
+
+ String buttonName;
+ String exeCommand;
+ String workDir;
+ String iconPath;
+
+ // 已修改:isCommit 改为规范过去式命名 isCommitted
+ boolean isCommitted;
+ String commitTitle;
+ String commitInfo;
+
+ public TermuxButtonModel() {
+ this.buttonName = "";
+ this.exeCommand = "";
+ this.workDir = "";
+ this.iconPath = "";
+ // 默认初始化
+ this.isCommitted = false;
+ this.commitTitle = "";
+ this.commitInfo = "";
+ }
+
+ public void setButtonName(String buttonName) {
+ this.buttonName = buttonName;
+ }
+
+ public String getButtonName() {
+ return buttonName;
+ }
+
+ public void setExeCommand(String exeCommand) {
+ this.exeCommand = exeCommand;
+ }
+
+ public String getExeCommand() {
+ return exeCommand;
+ }
+
+ public void setWorkDir(String workDir) {
+ this.workDir = workDir;
+ }
+
+ public String getWorkDir() {
+ return workDir;
+ }
+
+ public void setIconPath(String iconPath) {
+ this.iconPath = iconPath;
+ }
+
+ public String getIconPath() {
+ return iconPath;
+ }
+
+ // ========== 已修改 对应 isCommitted 完整 Get & Set ==========
+ public boolean isCommitted() {
+ return isCommitted;
+ }
+
+ public void setCommitted(boolean committed) {
+ isCommitted = committed;
+ }
+
+ public String getCommitTitle() {
+ return commitTitle;
+ }
+
+ public void setCommitTitle(String commitTitle) {
+ this.commitTitle = commitTitle;
+ }
+
+ public String getCommitInfo() {
+ return commitInfo;
+ }
+
+ public void setCommitInfo(String commitInfo) {
+ this.commitInfo = commitInfo;
+ }
+
+ @Override
+ public String getName() {
+ return TermuxButtonModel.class.getName();
+ }
+
+ @Override
+ public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
+ super.writeThisToJsonWriter(jsonWriter);
+ jsonWriter.name("buttonName").value(getButtonName());
+ jsonWriter.name("exeCommand").value(getExeCommand());
+ jsonWriter.name("workDir").value(getWorkDir());
+ jsonWriter.name("iconPath").value(getIconPath() != null ? getIconPath() : "");
+
+ // JSON写入同步修改
+ jsonWriter.name("isCommitted").value(isCommitted());
+ jsonWriter.name("commitTitle").value(getCommitTitle());
+ jsonWriter.name("commitInfo").value(getCommitInfo());
+ }
+
+ @Override
+ public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
+ if (super.initObjectsFromJsonReader(jsonReader, name)) {
+ return true;
+ } else {
+ if (name.equals("buttonName")) {
+ setButtonName(jsonReader.nextString());
+ } else if (name.equals("exeCommand")) {
+ setExeCommand(jsonReader.nextString());
+ } else if (name.equals("workDir")) {
+ setWorkDir(jsonReader.nextString());
+ } else if (name.equals("iconPath")) {
+ setIconPath(jsonReader.nextString());
+ }
+ // JSON解析字段同步修改
+ else if (name.equals("isCommitted")) {
+ setCommitted(jsonReader.nextBoolean());
+ } else if (name.equals("commitTitle")) {
+ setCommitTitle(jsonReader.nextString());
+ } else if (name.equals("commitInfo")) {
+ setCommitInfo(jsonReader.nextString());
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
+ jsonReader.beginObject();
+ while (jsonReader.hasNext()) {
+ String name = jsonReader.nextName();
+ if (!initObjectsFromJsonReader(jsonReader, name)) {
+ jsonReader.skipValue();
+ }
+ }
+ jsonReader.endObject();
+ return this;
+ }
+
+}
+
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/models/TestDemoBindServiceBean.java b/winboll/src/main/java/cc/winboll/studio/winboll/models/TestDemoBindServiceBean.java
new file mode 100644
index 0000000..be7cb6c
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/models/TestDemoBindServiceBean.java
@@ -0,0 +1,67 @@
+package cc.winboll.studio.winboll.models;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/03/07 12:47:22
+ * @Describe TestServiceBean
+ */
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import cc.winboll.studio.libappbase.models.libs1520000.BaseBean;
+import java.io.IOException;
+
+public class TestDemoBindServiceBean extends BaseBean {
+
+ public static final String TAG = "TestServiceBean";
+
+ boolean isEnable;
+
+ public TestDemoBindServiceBean() {
+ this.isEnable = false;
+ }
+
+ public void setIsEnable(boolean isEnable) {
+ this.isEnable = isEnable;
+ }
+
+ public boolean isEnable() {
+ return isEnable;
+ }
+
+ @Override
+ public String getName() {
+ return TestDemoBindServiceBean.class.getName();
+ }
+
+ @Override
+ public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
+ super.writeThisToJsonWriter(jsonWriter);
+ jsonWriter.name("isEnable").value(isEnable());
+ }
+
+ @Override
+ public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
+ if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else {
+ if (name.equals("isEnable")) {
+ setIsEnable(jsonReader.nextBoolean());
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
+ jsonReader.beginObject();
+ while (jsonReader.hasNext()) {
+ String name = jsonReader.nextName();
+ if (!initObjectsFromJsonReader(jsonReader, name)) {
+ jsonReader.skipValue();
+ }
+ }
+ // 结束 JSON 对象
+ jsonReader.endObject();
+ return this;
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/models/TestDemoServiceBean.java b/winboll/src/main/java/cc/winboll/studio/winboll/models/TestDemoServiceBean.java
new file mode 100644
index 0000000..d7106b6
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/models/TestDemoServiceBean.java
@@ -0,0 +1,68 @@
+package cc.winboll.studio.winboll.models;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/03/07 12:49:21
+ * @Describe TestDemoServiceBean
+ */
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import cc.winboll.studio.libappbase.models.libs1520000.BaseBean;
+import java.io.IOException;
+
+public class TestDemoServiceBean extends BaseBean {
+
+ public static final String TAG = "TestDemoServiceBean";
+
+ boolean isEnable;
+
+ public TestDemoServiceBean() {
+ this.isEnable = false;
+ }
+
+ public void setIsEnable(boolean isEnable) {
+ this.isEnable = isEnable;
+ }
+
+ public boolean isEnable() {
+ return isEnable;
+ }
+
+ @Override
+ public String getName() {
+ return TestDemoServiceBean.class.getName();
+ }
+
+ @Override
+ public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
+ super.writeThisToJsonWriter(jsonWriter);
+ jsonWriter.name("isEnable").value(isEnable());
+
+ }
+
+ @Override
+ public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
+ if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else {
+ if (name.equals("isEnable")) {
+ setIsEnable(jsonReader.nextBoolean());
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
+ jsonReader.beginObject();
+ while (jsonReader.hasNext()) {
+ String name = jsonReader.nextName();
+ if (!initObjectsFromJsonReader(jsonReader, name)) {
+ jsonReader.skipValue();
+ }
+ }
+ // 结束 JSON 对象
+ jsonReader.endObject();
+ return this;
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/models/UserInfoModel.java b/winboll/src/main/java/cc/winboll/studio/winboll/models/UserInfoModel.java
new file mode 100644
index 0000000..93531b9
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/models/UserInfoModel.java
@@ -0,0 +1,92 @@
+package cc.winboll.studio.winboll.models;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/06/04 19:14
+ */
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import cc.winboll.studio.libappbase.models.libs1520000.BaseBean;
+import java.io.IOException;
+
+public class UserInfoModel extends BaseBean {
+
+ public static final String TAG = "UserInfoModel";
+
+ String username;
+ String password;
+ String token;
+
+ public UserInfoModel() {
+ this.username = "";
+ this.password = "";
+ this.token = "";
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setToken(String token) {
+ this.token = token;
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ @Override
+ public String getName() {
+ return UserInfoModel.class.getName();
+ }
+
+ @Override
+ public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
+ super.writeThisToJsonWriter(jsonWriter);
+ jsonWriter.name("username").value(getUsername());
+ jsonWriter.name("password").value(getPassword());
+ jsonWriter.name("token").value(getToken());
+ }
+
+ @Override
+ public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
+ if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else {
+ if (name.equals("username")) {
+ setUsername(jsonReader.nextString());
+ } else if (name.equals("password")) {
+ setPassword(jsonReader.nextString());
+ } else if (name.equals("token")) {
+ setToken(jsonReader.nextString());
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
+ jsonReader.beginObject();
+ while (jsonReader.hasNext()) {
+ String name = jsonReader.nextName();
+ if (!initObjectsFromJsonReader(jsonReader, name)) {
+ jsonReader.skipValue();
+ }
+ }
+ // 结束 JSON 对象
+ jsonReader.endObject();
+ return this;
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/models/WinBoLLModel.java b/winboll/src/main/java/cc/winboll/studio/winboll/models/WinBoLLModel.java
new file mode 100644
index 0000000..34673d1
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/models/WinBoLLModel.java
@@ -0,0 +1,92 @@
+package cc.winboll.studio.winboll.models;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/05/10 10:16
+ * @Describe WinBoLLModel
+ */
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import cc.winboll.studio.libappbase.APPModel;
+import cc.winboll.studio.libappbase.models.libs1520000.BaseBean;
+import java.io.IOException;
+
+public class WinBoLLModel extends BaseBean {
+
+ public static final String TAG = "WinBoLLModel";
+
+ String appPackageName;
+ String appMainServiveName;
+
+ public WinBoLLModel() {
+ this.appPackageName = "";
+ this.appMainServiveName = "";
+ }
+
+ public WinBoLLModel(boolean isDebuging, String appPackageName, String appMainServiveName) {
+ this.appPackageName = appPackageName;
+ this.appMainServiveName = appMainServiveName;
+ }
+
+ public WinBoLLModel(String appPackageName, String appMainServiveName) {
+ this.appPackageName = appPackageName;
+ this.appMainServiveName = appMainServiveName;
+ }
+
+ public void setAppPackageName(String appPackageName) {
+ this.appPackageName = appPackageName;
+ }
+
+ public String getAppPackageName() {
+ return appPackageName;
+ }
+
+ public void setAppMainServiveName(String appMainServiveName) {
+ this.appMainServiveName = appMainServiveName;
+ }
+
+ public String getAppMainServiveName() {
+ return appMainServiveName;
+ }
+
+ @Override
+ public String getName() {
+ return APPModel.class.getName();
+ }
+
+ @Override
+ public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
+ super.writeThisToJsonWriter(jsonWriter);
+ jsonWriter.name("appPackageName").value(getAppPackageName());
+ jsonWriter.name("appMainServiveName").value(getAppMainServiveName());
+ }
+
+ @Override
+ public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
+ if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else {
+ if (name.equals("appPackageName")) {
+ setAppPackageName(jsonReader.nextString());
+ } else if (name.equals("appMainServiveName")) {
+ setAppMainServiveName(jsonReader.nextString());
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
+ jsonReader.beginObject();
+ while (jsonReader.hasNext()) {
+ String name = jsonReader.nextName();
+ if (!initObjectsFromJsonReader(jsonReader, name)) {
+ jsonReader.skipValue();
+ }
+ }
+ // 结束 JSON 对象
+ jsonReader.endObject();
+ return this;
+ }
+}
+
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/models/WinBoLLNewsBean.java b/winboll/src/main/java/cc/winboll/studio/winboll/models/WinBoLLNewsBean.java
new file mode 100644
index 0000000..14d54cc
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/models/WinBoLLNewsBean.java
@@ -0,0 +1,71 @@
+package cc.winboll.studio.winboll.models;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/05/10 09:36
+ * @Describe WinBoLL 应用消息数据模型
+ */
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import cc.winboll.studio.libappbase.models.libs1520000.BaseBean;
+import java.io.IOException;
+
+public class WinBoLLNewsBean extends BaseBean {
+
+ public static final String TAG = "WinBoLLNewsBean";
+
+ String message;
+
+ public WinBoLLNewsBean() {
+ this.message = "";
+ }
+
+ public WinBoLLNewsBean(String message) {
+ this.message = message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ @Override
+ public String getName() {
+ return WinBoLLNewsBean.class.getName();
+ }
+
+ @Override
+ public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
+ super.writeThisToJsonWriter(jsonWriter);
+ jsonWriter.name("message").value(getMessage());
+ }
+
+ @Override
+ public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
+ if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else {
+ if (name.equals("message")) {
+ setMessage(jsonReader.nextString());
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
+ jsonReader.beginObject();
+ while (jsonReader.hasNext()) {
+ String name = jsonReader.nextName();
+ if (!initObjectsFromJsonReader(jsonReader, name)) {
+ jsonReader.skipValue();
+ }
+ }
+ // 结束 JSON 对象
+ jsonReader.endObject();
+ return this;
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/receivers/APPNewsWidgetClickListener.java b/winboll/src/main/java/cc/winboll/studio/winboll/receivers/APPNewsWidgetClickListener.java
new file mode 100644
index 0000000..4e3681d
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/receivers/APPNewsWidgetClickListener.java
@@ -0,0 +1,36 @@
+package cc.winboll.studio.winboll.receivers;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/03/24 07:11:44
+ */
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.winboll.widgets.APPNewsWidget;
+
+public class APPNewsWidgetClickListener extends BroadcastReceiver {
+
+ public static final String TAG = "APPNewsWidgetClickListener";
+ public static final String ACTION_PRE = APPNewsWidgetClickListener.class.getName() + ".ACTION_PRE";
+ public static final String ACTION_NEXT = APPNewsWidgetClickListener.class.getName() + ".ACTION_NEXT";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action == null) {
+ LogUtils.d(TAG, String.format("action %s", action));
+ return;
+ }
+ if (action.equals(ACTION_PRE)) {
+ LogUtils.d(TAG, "ACTION_PRE");
+ APPNewsWidget.prePage(context);
+ } else if (action.equals(ACTION_NEXT)) {
+ LogUtils.d(TAG, "ACTION_NEXT");
+ APPNewsWidget.nextPage(context);
+ } else {
+ LogUtils.d(TAG, String.format("action %s", action));
+ }
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/receivers/MainReceiver.java b/winboll/src/main/java/cc/winboll/studio/winboll/receivers/MainReceiver.java
new file mode 100644
index 0000000..a0b9edb
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/receivers/MainReceiver.java
@@ -0,0 +1,117 @@
+package cc.winboll.studio.winboll.receivers;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/02/13 06:58:04
+ * @Describe 主要广播接收器
+ */
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.libappbase.ToastUtils;
+import cc.winboll.studio.winboll.WinBoLL;
+import cc.winboll.studio.winboll.models.WinBoLLModel;
+import cc.winboll.studio.winboll.models.WinBoLLNewsBean;
+import cc.winboll.studio.winboll.services.MainService;
+import cc.winboll.studio.winboll.sos.SOS;
+import cc.winboll.studio.winboll.sos.SOSObject;
+import cc.winboll.studio.winboll.widgets.APPNewsWidget;
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+public class MainReceiver extends BroadcastReceiver {
+
+ public static final String TAG = "MainReceiver";
+
+ public static final String ACTION_BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED";
+
+ WeakReference mwrService;
+
+ public MainReceiver(MainService service) {
+ mwrService = new WeakReference(service);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String szAction = intent.getAction();
+ if (szAction.equals(ACTION_BOOT_COMPLETED)) {
+ ToastUtils.show("ACTION_BOOT_COMPLETED");
+ } else if (szAction.equals(IWinBoLLActivity.ACTION_BIND)) {
+ LogUtils.d(TAG, "ACTION_BIND");
+ LogUtils.d(TAG, String.format("context.getPackageName() %s", context.getPackageName()));
+ LogUtils.d(TAG, String.format("intent.getAction() %s", intent.getAction()));
+ String szWinBoLLModel = intent.getStringExtra(WinBoLL.EXTRA_WINBOLLMODEL);
+ LogUtils.d(TAG, String.format("szAPPModel %s", szWinBoLLModel));
+ if (szWinBoLLModel != null && !szWinBoLLModel.equals("")) {
+ try {
+ WinBoLLModel bean = WinBoLLModel.parseStringToBean(szWinBoLLModel, WinBoLLModel.class);
+ if (bean != null) {
+ String szAppPackageName = bean.getAppPackageName();
+ LogUtils.d(TAG, String.format("szAppPackageName %s", szAppPackageName));
+ String szAppMainServiveName = bean.getAppMainServiveName();
+ LogUtils.d(TAG, String.format("szAppMainServiveName %s", szAppMainServiveName));
+ mwrService.get().bindWinBoLLModelConnection(bean);
+ }
+ } catch (IOException e) {
+ LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
+ }
+ }
+ } else if (intent.getAction().equals(SOS.ACTION_SOS)) {
+ LogUtils.d(TAG, "ACTION_SOS");
+// String sos = intent.getStringExtra(SOS.EXTRA_OBJECT);
+// LogUtils.d(TAG, String.format("SOS %s", sos));
+// if (sos != null && !sos.equals("")) {
+// SOSObject bean = SOS.parseSOSObject(sos);
+// if (bean != null) {
+// String szObjectPackageName = bean.getObjectPackageName();
+// LogUtils.d(TAG, String.format("szObjectPackageName %s", szObjectPackageName));
+// String szObjectServiveName = bean.getObjectServiveName();
+// LogUtils.d(TAG, String.format("szObjectServiveName %s", szObjectServiveName));
+//
+// Intent intentService = new Intent();
+// intentService.setComponent(new ComponentName(szObjectPackageName, szObjectServiveName));
+// context.startService(intentService);
+//
+// String appName = AppUtils.getAppNameByPackageName(context, szObjectPackageName);
+// LogUtils.d(TAG, String.format("appName %s", appName));
+// WinBoLLNewsBean appWinBoLLNewsBean = new WinBoLLNewsBean(appName);
+// SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
+// String currentTime = sdf.format(new Date());
+// StringBuilder sbLine = new StringBuilder();
+// sbLine.append("[");
+// sbLine.append(currentTime);
+// sbLine.append("] Power to ");
+// sbLine.append(appName);
+// appWinBoLLNewsBean.setMessage(sbLine.toString());
+//
+// APPNewsWidget.addWinBoLLNewsBean(context, appWinBoLLNewsBean);
+//
+// Intent intentWidget = new Intent(context, APPNewsWidget.class);
+// intentWidget.setAction(APPNewsWidget.ACTION_RELOAD_REPORT);
+// context.sendBroadcast(intentWidget);
+// }
+//
+//
+// }
+ } else {
+ ToastUtils.show(szAction);
+ }
+ }
+
+ // 注册 Receiver
+ //
+ public void registerAction(MainService service) {
+ IntentFilter filter=new IntentFilter();
+ filter.addAction(ACTION_BOOT_COMPLETED);
+ filter.addAction(SOS.ACTION_SOS);
+ filter.addAction(WinBoLL.ACTION_BIND);
+ //filter.addAction(Intent.ACTION_BATTERY_CHANGED);
+ service.registerReceiver(this, filter);
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/receivers/MyBroadcastReceiver.java b/winboll/src/main/java/cc/winboll/studio/winboll/receivers/MyBroadcastReceiver.java
new file mode 100644
index 0000000..974ddfd
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/receivers/MyBroadcastReceiver.java
@@ -0,0 +1,29 @@
+package cc.winboll.studio.winboll.receivers;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/02/13 21:19:09
+ * @Describe MyBroadcastReceiver
+ */
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.libappbase.R;
+
+public class MyBroadcastReceiver extends BroadcastReceiver {
+
+ public static final String TAG = "MyBroadcastReceiver";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+// if (context.getString(R.string.action_sos).equals(intent.getAction())) {
+// String message = intent.getStringExtra("message");
+// String sosPackage = intent.getStringExtra("sosPackage");
+//
+// // 处理接收到的广播消息
+// LogUtils.d(TAG, String.format("MyBroadcastReceiver action %s \n%s\n%s", intent.getAction(), sosPackage, message));
+// }
+ }
+}
+
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/services/AssistantService.java b/winboll/src/main/java/cc/winboll/studio/winboll/services/AssistantService.java
new file mode 100644
index 0000000..2fd6076
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/services/AssistantService.java
@@ -0,0 +1,136 @@
+package cc.winboll.studio.winboll.services;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/02/14 03:38:31
+ * @Describe 守护进程服务
+ */
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import cc.winboll.studio.libappbase.LogUtils;
+import android.os.Binder;
+import cc.winboll.studio.winboll.models.MainServiceBean;
+
+public class AssistantService extends Service {
+
+ public static final String TAG = "AssistantService";
+
+ MainServiceBean mMainServiceBean;
+ MyServiceConnection mMyServiceConnection;
+ MainService mMainService;
+ boolean isBound = false;
+ volatile boolean isThreadAlive = false;
+
+ public synchronized void setIsThreadAlive(boolean isThreadAlive) {
+ LogUtils.d(TAG, "setIsThreadAlive(...)");
+ LogUtils.d(TAG, String.format("isThreadAlive %s", isThreadAlive));
+ this.isThreadAlive = isThreadAlive;
+ }
+
+ public boolean isThreadAlive() {
+ return isThreadAlive;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return new MyBinder();
+ }
+
+ @Override
+ public void onCreate() {
+ LogUtils.d(TAG, "onCreate");
+ super.onCreate();
+
+ //mMyBinder = new MyBinder();
+ if (mMyServiceConnection == null) {
+ mMyServiceConnection = new MyServiceConnection();
+ }
+ // 设置运行参数
+ setIsThreadAlive(false);
+ assistantService();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ LogUtils.d(TAG, "call onStartCommand(...)");
+ assistantService();
+ return START_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ //LogUtils.d(TAG, "onDestroy");
+ setIsThreadAlive(false);
+ // 解除绑定
+ if (isBound) {
+ unbindService(mMyServiceConnection);
+ isBound = false;
+ }
+ super.onDestroy();
+ }
+
+ // 运行服务内容
+ //
+ void assistantService() {
+ LogUtils.d(TAG, "assistantService()");
+ mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
+ LogUtils.d(TAG, String.format("mMainServiceBean.isEnable() %s", mMainServiceBean.isEnable()));
+ if (mMainServiceBean.isEnable()) {
+ LogUtils.d(TAG, String.format("mIsThreadAlive %s", isThreadAlive()));
+ if (isThreadAlive() == false) {
+ // 设置运行状态
+ setIsThreadAlive(true);
+ // 唤醒和绑定主进程
+ wakeupAndBindMain();
+ }
+ }
+ }
+
+ // 唤醒和绑定主进程
+ //
+ void wakeupAndBindMain() {
+ LogUtils.d(TAG, "wakeupAndBindMain()");
+ // 绑定服务的Intent
+ Intent intent = new Intent(this, MainService.class);
+ startService(new Intent(this, MainService.class));
+ bindService(intent, mMyServiceConnection, Context.BIND_IMPORTANT);
+
+// startService(new Intent(this, MainService.class));
+// bindService(new Intent(AssistantService.this, MainService.class), mMyServiceConnection, Context.BIND_IMPORTANT);
+ }
+
+ // 主进程与守护进程连接时需要用到此类
+ //
+ class MyServiceConnection implements ServiceConnection {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ LogUtils.d(TAG, "onServiceConnected(...)");
+ MainService.MyBinder binder = (MainService.MyBinder) service;
+ mMainService = binder.getService();
+ isBound = true;
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ LogUtils.d(TAG, "onServiceDisconnected(...)");
+ mMainServiceBean = MainServiceBean.loadBean(AssistantService.this, MainServiceBean.class);
+ if (mMainServiceBean.isEnable()) {
+ wakeupAndBindMain();
+ }
+ isBound = false;
+ mMainService = null;
+ }
+ }
+
+ // 用于返回服务实例的Binder
+ public class MyBinder extends Binder {
+ AssistantService getService() {
+ LogUtils.d(TAG, "AssistantService MyBinder getService()");
+ return AssistantService.this;
+ }
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/services/MainService.java b/winboll/src/main/java/cc/winboll/studio/winboll/services/MainService.java
new file mode 100644
index 0000000..d287377
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/services/MainService.java
@@ -0,0 +1,316 @@
+package cc.winboll.studio.winboll.services;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/02/13 06:56:41
+ * @Describe 拨号主服务
+ * 参考:
+ * 进程保活-双进程守护的正确姿势
+ * https://blog.csdn.net/sinat_35159441/article/details/75267380
+ * Android Service之onStartCommand方法研究
+ * https://blog.csdn.net/cyp331203/article/details/38920491
+ */
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Binder;
+import android.os.IBinder;
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.winboll.MyTileService;
+import cc.winboll.studio.winboll.handlers.MainServiceHandler;
+import cc.winboll.studio.winboll.models.MainServiceBean;
+import cc.winboll.studio.winboll.models.WinBoLLModel;
+import cc.winboll.studio.winboll.receivers.MainReceiver;
+import cc.winboll.studio.winboll.services.AssistantService;
+import cc.winboll.studio.winboll.threads.MainServiceThread;
+import cc.winboll.studio.winboll.widgets.APPNewsWidget;
+import java.util.ArrayList;
+
+public class MainService extends Service {
+
+ public static final String TAG = "MainService";
+
+ public static final int MSG_UPDATE_STATUS = 0;
+
+ static MainService _mControlCenterService;
+
+ volatile boolean isServiceRunning;
+
+ MainServiceBean mMainServiceBean;
+ MainServiceThread mMainServiceThread;
+ MainServiceHandler mMainServiceHandler;
+ MyServiceConnection mMyServiceConnection;
+ AssistantService mAssistantService;
+ boolean isBound = false;
+ MainReceiver mMainReceiver;
+ ArrayList mAPPModelConnectionList;
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return new MyBinder();
+ }
+
+ public MainServiceThread getRemindThread() {
+ return mMainServiceThread;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ LogUtils.d(TAG, "onCreate()");
+ mAPPModelConnectionList = new ArrayList();
+
+ _mControlCenterService = MainService.this;
+ isServiceRunning = false;
+ mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
+
+ if (mMyServiceConnection == null) {
+ mMyServiceConnection = new MyServiceConnection();
+ }
+ mMainServiceHandler = new MainServiceHandler(this);
+
+ // 运行服务内容
+ mainService();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ LogUtils.d(TAG, "onStartCommand(...)");
+ // 运行服务内容
+ mainService();
+ return (mMainServiceBean.isEnable()) ? START_STICKY : super.onStartCommand(intent, flags, startId);
+ }
+
+ // 运行服务内容
+ //
+ void mainService() {
+ LogUtils.d(TAG, "mainService()");
+ mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
+ if (mMainServiceBean.isEnable() && isServiceRunning == false) {
+ LogUtils.d(TAG, "mainService() start running");
+ isServiceRunning = true;
+ // 唤醒守护进程
+ wakeupAndBindAssistant();
+
+ if (mMainReceiver == null) {
+ // 注册广播接收器
+ mMainReceiver = new MainReceiver(this);
+ mMainReceiver.registerAction(this);
+ }
+
+ // 启动小部件
+ Intent intentTimeWidget = new Intent(this, APPNewsWidget.class);
+ intentTimeWidget.setAction(APPNewsWidget.ACTION_RELOAD_REPORT);
+ this.sendBroadcast(intentTimeWidget);
+
+ startMainServiceThread();
+
+ MyTileService.updateServiceIconStatus(this);
+
+ LogUtils.i(TAG, "Main Service Is Start.");
+ }
+ }
+
+ // 唤醒和绑定守护进程
+ //
+ void wakeupAndBindAssistant() {
+ LogUtils.d(TAG, "wakeupAndBindAssistant()");
+
+ Intent intent = new Intent(this, AssistantService.class);
+ startService(intent);
+ // 绑定服务的Intent
+ bindService(intent, mMyServiceConnection, Context.BIND_IMPORTANT);
+ }
+
+ // 开启提醒铃声线程
+ //
+ public void startMainServiceThread() {
+ LogUtils.d(TAG, "startMainServiceThread");
+ if (mMainServiceThread == null) {
+ mMainServiceThread = new MainServiceThread(this, mMainServiceHandler);
+ LogUtils.d(TAG, "new MainServiceThread");
+ } else {
+ if (mMainServiceThread.isExist() == true) {
+ mMainServiceThread = new MainServiceThread(this, mMainServiceHandler);
+ LogUtils.d(TAG, "renew MainServiceThread");
+ } else {
+ // 提醒进程正在进行中就更新状态后退出
+ LogUtils.d(TAG, "A mMainServiceThread running.");
+ return;
+ }
+ }
+ mMainServiceThread.start();
+ }
+
+ public void stopRemindThread() {
+ if (mMainServiceThread != null) {
+ mMainServiceThread.setIsExist(true);
+ mMainServiceThread = null;
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ //LogUtils.d(TAG, "onDestroy");
+ mMainServiceBean = MainServiceBean.loadBean(this, MainServiceBean.class);
+ if (mMainServiceBean.isEnable() == false) {
+ // 设置运行状态
+ isServiceRunning = false;// 解除绑定
+ if (isBound) {
+ unbindService(mMyServiceConnection);
+ isBound = false;
+ }
+ // 停止守护进程
+ Intent intent = new Intent(this, AssistantService.class);
+ stopService(intent);
+ // 停止Receiver
+ if (mMainReceiver != null) {
+ unregisterReceiver(mMainReceiver);
+ mMainReceiver = null;
+ }
+ // 停止前台通知栏
+ stopForeground(true);
+ // 停止消息提醒进程
+ stopRemindThread();
+
+ MyTileService.updateServiceIconStatus(this);
+
+ super.onDestroy();
+ //LogUtils.d(TAG, "onDestroy done");
+ }
+ }
+
+ public void bindWinBoLLModelConnection(WinBoLLModel bean) {
+ LogUtils.d(TAG, "bindAPPModelConnection(...)");
+ // 清理旧的绑定链接
+ for (int i = mAPPModelConnectionList.size() - 1; i > -1; i--) {
+ APPConnection item = mAPPModelConnectionList.get(i);
+ if (item.isBindToAPP(bean)) {
+ LogUtils.d(TAG, "Bind Servive exist.");
+ unbindService(item);
+ mAPPModelConnectionList.remove(i);
+ }
+ }
+
+ // 绑定服务
+ APPConnection appConnection = new APPConnection();
+ Intent intentService = new Intent();
+ intentService.setComponent(new ComponentName(bean.getAppPackageName(), bean.getAppMainServiveName()));
+ bindService(intentService, appConnection, Context.BIND_IMPORTANT);
+ mAPPModelConnectionList.add(appConnection);
+
+ Intent intentWidget = new Intent(this, APPNewsWidget.class);
+ intentWidget.setAction(APPNewsWidget.ACTION_WAKEUP_SERVICE);
+ WinBoLLModel appSOSBean = new WinBoLLModel(bean.getAppPackageName(), bean.getAppMainServiveName());
+ intentWidget.putExtra("APPSOSBean", appSOSBean.toString());
+ sendBroadcast(intentWidget);
+ }
+
+ public class APPConnection implements ServiceConnection {
+
+ ComponentName mComponentName;
+
+ boolean isBindToAPP(WinBoLLModel bean) {
+ return mComponentName != null
+ && mComponentName.getClassName().equals(bean.getAppMainServiveName())
+ && mComponentName.getPackageName().equals(bean.getAppPackageName());
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ LogUtils.d(TAG, "onServiceConnected(...)");
+ mComponentName = name;
+ LogUtils.d(TAG, String.format("onServiceConnected : \ngetClassName %s\ngetPackageName %s", name.getClassName(), name.getPackageName()));
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ LogUtils.d(TAG, "onServiceDisconnected(...)");
+ LogUtils.d(TAG, String.format("onServiceDisconnected : \ngetClassName %s\ngetPackageName %s", name.getClassName(), name.getPackageName()));
+
+ // 尝试无参数启动一下服务
+ String appPackage = mComponentName.getPackageName();
+ LogUtils.d(TAG, String.format("appPackage %s", appPackage));
+ String appMainServiceClassName = mComponentName.getClassName();
+ LogUtils.d(TAG, String.format("appMainServiceClassName %s", appMainServiceClassName));
+
+ Intent intentService = new Intent();
+ intentService.setComponent(new ComponentName(appPackage, appMainServiceClassName));
+ startService(intentService);
+ }
+
+ }
+
+ // 主进程与守护进程连接时需要用到此类
+ //
+ private class MyServiceConnection implements ServiceConnection {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ LogUtils.d(TAG, "onServiceConnected(...)");
+ AssistantService.MyBinder binder = (AssistantService.MyBinder) service;
+ mAssistantService = binder.getService();
+ isBound = true;
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ LogUtils.d(TAG, "onServiceDisconnected(...)");
+
+ if (mMainServiceBean.isEnable()) {
+ // 唤醒守护进程
+ wakeupAndBindAssistant();
+ }
+ isBound = false;
+ mAssistantService = null;
+ }
+
+ }
+
+
+ // 用于返回服务实例的Binder
+ public class MyBinder extends Binder {
+ MainService getService() {
+ LogUtils.d(TAG, "MainService MyBinder getService()");
+ return MainService.this;
+ }
+ }
+
+// //
+// // 启动服务
+// //
+// public static void startControlCenterService(Context context) {
+// Intent intent = new Intent(context, MainService.class);
+// context.startForegroundService(intent);
+// }
+//
+// //
+// // 停止服务
+// //
+// public static void stopControlCenterService(Context context) {
+// Intent intent = new Intent(context, MainService.class);
+// context.stopService(intent);
+// }
+
+ public void appenMessage(String message) {
+ LogUtils.d(TAG, String.format("Message : %s", message));
+ }
+
+ public static void stopMainService(Context context) {
+ LogUtils.d(TAG, "stopMainService");
+ MainServiceBean bean = new MainServiceBean();
+ bean.setIsEnable(false);
+ MainServiceBean.saveBean(context, bean);
+ context.stopService(new Intent(context, MainService.class));
+ }
+
+ public static void startMainService(Context context) {
+ LogUtils.d(TAG, "startMainService");
+ MainServiceBean bean = new MainServiceBean();
+ bean.setIsEnable(true);
+ MainServiceBean.saveBean(context, bean);
+ context.startService(new Intent(context, MainService.class));
+ }
+}
+
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/services/TestDemoBindService.java b/winboll/src/main/java/cc/winboll/studio/winboll/services/TestDemoBindService.java
new file mode 100644
index 0000000..8896879
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/services/TestDemoBindService.java
@@ -0,0 +1,179 @@
+package cc.winboll.studio.winboll.services;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/03/07 12:45:49
+ * @Describe 启动时申请绑定到APPBase主服务的服务示例
+ */
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.winboll.App;
+import cc.winboll.studio.winboll.WinBoLL;
+import cc.winboll.studio.winboll.models.TestDemoBindServiceBean;
+import cc.winboll.studio.winboll.services.TestDemoBindService;
+import cc.winboll.studio.winboll.sos.SOS;
+
+public class TestDemoBindService extends Service {
+
+ public static final String TAG = "TestDemoBindService";
+
+ public static final String ACTION_ENABLE = TestDemoBindService.class.getName() + ".ACTION_ENABLE";
+ public static final String ACTION_DISABLE = TestDemoBindService.class.getName() + ".ACTION_DISABLE";
+
+ volatile static TestThread _TestThread;
+
+ volatile static boolean _IsRunning;
+
+ public synchronized static void setIsRunning(boolean isRunning) {
+ _IsRunning = isRunning;
+ }
+
+ public static boolean isRunning() {
+ return _IsRunning;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return new MyBinder();
+ }
+
+ public class MyBinder extends Binder {
+ public TestDemoBindService getService() {
+ return TestDemoBindService.this;
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ LogUtils.d(TAG, "onCreate()");
+
+ run();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ LogUtils.d(TAG, "onStartCommand(...)");
+ TestDemoBindServiceBean bean = TestDemoBindServiceBean.loadBean(this, TestDemoBindServiceBean.class);
+ if (bean == null) {
+ bean = new TestDemoBindServiceBean();
+ }
+
+ if (intent.getAction() != null) {
+ if (intent.getAction().equals(ACTION_ENABLE)) {
+ bean.setIsEnable(true);
+ LogUtils.d(TAG, "setIsEnable(true);");
+ TestDemoBindServiceBean.saveBean(this, bean);
+ } else if (intent.getAction().equals(ACTION_DISABLE)) {
+ bean.setIsEnable(false);
+ LogUtils.d(TAG, "setIsEnable(false);");
+ TestDemoBindServiceBean.saveBean(this, bean);
+ }
+ }
+
+ run();
+
+ return (bean.isEnable()) ? START_STICKY : super.onStartCommand(intent, flags, startId);
+ //return super.onStartCommand(intent, flags, startId);
+ }
+
+ void run() {
+ LogUtils.d(TAG, "run()");
+ TestDemoBindServiceBean bean = TestDemoBindServiceBean.loadBean(this, TestDemoBindServiceBean.class);
+ if (bean == null) {
+ bean = new TestDemoBindServiceBean();
+ TestDemoBindServiceBean.saveBean(this, bean);
+ }
+ if (bean.isEnable()) {
+ LogUtils.d(TAG, "run() bean.isEnable()");
+ TestThread.getInstance(this).start();
+ LogUtils.d(TAG, "_TestThread.start()");
+ }
+ }
+
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ LogUtils.d(TAG, "onDestroy()");
+ TestDemoBindServiceBean bean = TestDemoBindServiceBean.loadBean(this, TestDemoBindServiceBean.class);
+ if (bean == null) {
+ bean = new TestDemoBindServiceBean();
+ }
+
+ TestThread.getInstance(this).setIsExit(true);
+
+ // 预防 APPBase 应用重启绑定失效。
+ // 所以退出时检查本服务是否配置启用,如果启用就发送一个 SOS 信号。
+ // 这样 APPBase 就会用组件方式启动本服务。
+ if (bean.isEnable()) {
+ if (App.isDebugging()) {
+ SOS.sosToAppBaseBeta(this, TestDemoBindService.class.getName());
+ } else {
+ SOS.sosToAppBase(this, TestDemoBindService.class.getName());
+ }
+ }
+
+ _IsRunning = false;
+ }
+
+ static class TestThread extends Thread {
+
+ volatile static TestThread _TestThread;
+ Context mContext;
+ volatile boolean isStarted = false;
+ volatile boolean isExit = false;
+
+ TestThread(Context context) {
+ super();
+ mContext = context;
+ }
+
+ public static synchronized TestThread getInstance(Context context) {
+ if (_TestThread != null) {
+ _TestThread.setIsExit(true);
+ }
+ _TestThread = new TestThread(context);
+
+ return _TestThread;
+ }
+
+ public synchronized void setIsExit(boolean isExit) {
+ this.isExit = isExit;
+ }
+
+ public boolean isExit() {
+ return isExit;
+ }
+
+ @Override
+ public void run() {
+ if (isStarted == false) {
+ isStarted = true;
+ super.run();
+ LogUtils.d(TAG, "run() start");
+ if (App.isDebugging()) {
+ WinBoLL.bindToAPPBaseBeta(mContext, TestDemoBindService.class.getName());
+ } else {
+ WinBoLL.bindToAPPBase(mContext, TestDemoBindService.class.getName());
+ }
+
+ while (!isExit()) {
+ LogUtils.d(TAG, "run()");
+
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
+ }
+ }
+
+ LogUtils.d(TAG, "run() exit");
+ }
+ }
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/services/TestDemoService.java b/winboll/src/main/java/cc/winboll/studio/winboll/services/TestDemoService.java
new file mode 100644
index 0000000..7364b9b
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/services/TestDemoService.java
@@ -0,0 +1,156 @@
+package cc.winboll.studio.winboll.services;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/03/07 12:39:24
+ * @Describe 普通服务示例
+ */
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+import cc.winboll.studio.winboll.models.TestDemoServiceBean;
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.winboll.models.TestDemoServiceBean;
+
+public class TestDemoService extends Service {
+
+ public static final String TAG = "TestDemoService";
+
+ public static final String ACTION_ENABLE = TestDemoService.class.getName() + ".ACTION_ENABLE";
+ public static final String ACTION_DISABLE = TestDemoService.class.getName() + ".ACTION_DISABLE";
+
+ volatile static TestThread _TestThread;
+
+ volatile static boolean _IsRunning;
+
+ public synchronized static void setIsRunning(boolean isRunning) {
+ _IsRunning = isRunning;
+ }
+
+ public static boolean isRunning() {
+ return _IsRunning;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return new MyBinder();
+ }
+
+ public class MyBinder extends Binder {
+ public TestDemoService getService() {
+ return TestDemoService.this;
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ LogUtils.d(TAG, "onCreate()");
+
+
+ run();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ LogUtils.d(TAG, "onStartCommand(...)");
+ TestDemoServiceBean bean = TestDemoServiceBean.loadBean(this, TestDemoServiceBean.class);
+ if (bean == null) {
+ bean = new TestDemoServiceBean();
+ }
+
+ if (intent.getAction() != null) {
+ if (intent.getAction().equals(ACTION_ENABLE)) {
+ bean.setIsEnable(true);
+ LogUtils.d(TAG, "setIsEnable(true);");
+ TestDemoServiceBean.saveBean(this, bean);
+ } else if (intent.getAction().equals(ACTION_DISABLE)) {
+ bean.setIsEnable(false);
+ LogUtils.d(TAG, "setIsEnable(false);");
+ TestDemoServiceBean.saveBean(this, bean);
+ }
+ }
+
+ run();
+
+ return (bean.isEnable()) ? START_STICKY : super.onStartCommand(intent, flags, startId);
+ //return super.onStartCommand(intent, flags, startId);
+ }
+
+ void run() {
+ LogUtils.d(TAG, "run()");
+ TestDemoServiceBean bean = TestDemoServiceBean.loadBean(this, TestDemoServiceBean.class);
+ if (bean == null) {
+ bean = new TestDemoServiceBean();
+ TestDemoServiceBean.saveBean(this, bean);
+ }
+ if (bean.isEnable()) {
+ LogUtils.d(TAG, "run() bean.isEnable()");
+ TestThread.getInstance(this).start();
+ LogUtils.d(TAG, "_TestThread.start()");
+ }
+ }
+
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ LogUtils.d(TAG, "onDestroy()");
+ TestThread.getInstance(this).setIsExit(true);
+
+ _IsRunning = false;
+ }
+
+ static class TestThread extends Thread {
+
+ volatile static TestThread _TestThread;
+ Context mContext;
+ volatile boolean isStarted = false;
+ volatile boolean isExit = false;
+
+ TestThread(Context context) {
+ super();
+ mContext = context;
+ }
+
+ public static synchronized TestThread getInstance(Context context) {
+ if (_TestThread != null) {
+ _TestThread.setIsExit(true);
+ }
+ _TestThread = new TestThread(context);
+
+ return _TestThread;
+ }
+
+ public synchronized void setIsExit(boolean isExit) {
+ this.isExit = isExit;
+ }
+
+ public boolean isExit() {
+ return isExit;
+ }
+
+ @Override
+ public void run() {
+ if (isStarted == false) {
+ isStarted = true;
+ super.run();
+ LogUtils.d(TAG, "run() start");
+
+ while (!isExit()) {
+ LogUtils.d(TAG, "run()");
+
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
+ }
+ }
+
+ LogUtils.d(TAG, "run() exit");
+ }
+ }
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOS.java b/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOS.java
new file mode 100644
index 0000000..c45d8eb
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOS.java
@@ -0,0 +1,59 @@
+package cc.winboll.studio.winboll.sos;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/03/02 09:36:29
+ * @Describe WinBoLL 应用 SOS 机理保护类
+ */
+import android.content.Context;
+import android.content.Intent;
+import cc.winboll.studio.libappbase.LogUtils;
+import java.io.IOException;
+
+public class SOS {
+
+ public static final String TAG = "SOS";
+
+ public static final String ACTION_SOS = SOS.class.getName() + ".ACTION_SOS";
+ public static final String EXTRA_OBJECT = "EXTRA_OBJECT";
+
+ public static void sosToAppBase(Context context, String sosService) {
+ LogUtils.d(TAG, "sosToAppBase()");
+ String szToPackage = "cc.winboll.studio.appbase";
+ sos(context, szToPackage, sosService);
+
+ }
+
+ public static void sosToAppBaseBeta(Context context, String sosService) {
+ LogUtils.d(TAG, "sosToAppBaseBeta()");
+ String szToPackage = "cc.winboll.studio.appbase.beta";
+ sos(context, szToPackage, sosService);
+
+ }
+
+ static void sos(Context context, String szToPackage, String sosService) {
+ LogUtils.d(TAG, "sos(...)");
+ Intent intent = new Intent(ACTION_SOS);
+ intent.putExtra(EXTRA_OBJECT, genSOSObject(context.getPackageName(), sosService));
+ intent.setPackage(szToPackage);
+ LogUtils.d(TAG, String.format("ACTION_SOS :\nTo Package : %sSOS Service : %s\n", szToPackage, sosService));
+ context.sendBroadcast(intent);
+ }
+
+ public static SOSObject parseSOSObject(String szSOSObject) {
+ try {
+ return SOSObject.parseStringToBean(szSOSObject, SOSObject.class);
+ } catch (IOException e) {
+ LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
+ }
+ return null;
+ }
+
+ public static String sosObjectToString(SOSObject object) {
+ return object.toString();
+ }
+
+ public static String genSOSObject(String objectPackageName, String objectServiveName) {
+ return (new SOSObject(objectPackageName, objectServiveName)).toString();
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOSCenterService.java b/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOSCenterService.java
new file mode 100644
index 0000000..039d8e6
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOSCenterService.java
@@ -0,0 +1,182 @@
+package cc.winboll.studio.winboll.sos;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/02/27 14:00:21
+ * @Describe Simple Operate Signal Service Center.
+ * 简单操作信号服务中心
+ */
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.Parcel;
+import android.os.RemoteException;
+import java.io.FileDescriptor;
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import cc.winboll.studio.libappbase.LogUtils;
+
+public class SOSCenterService extends Service {
+
+ public static final String TAG = "SOSCenterService";
+
+ private final IBinder binder =(IBinder)new SOSBinder();
+
+ SOSCenterServiceModel mSOSCenterServiceModel;
+ static MainThread _MainThread;
+ public static synchronized MainThread getMainThreadInstance() {
+ if (_MainThread == null) {
+ _MainThread = new MainThread();
+ }
+ return _MainThread;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return binder;
+ }
+
+ public class SOSBinder implements IBinder {
+
+ @Override
+ public void dump(FileDescriptor fileDescriptor, String[] string) throws RemoteException {
+ }
+
+ @Override
+ public void dumpAsync(FileDescriptor fileDescriptor, String[] string) throws RemoteException {
+ }
+
+ @Override
+ public String getInterfaceDescriptor() throws RemoteException {
+ return null;
+ }
+
+ @Override
+ public boolean isBinderAlive() {
+ return false;
+ }
+
+ @Override
+ public void linkToDeath(IBinder.DeathRecipient deathRecipient, int p) throws RemoteException {
+ }
+
+ @Override
+ public boolean pingBinder() {
+ return false;
+ }
+
+ @Override
+ public IInterface queryLocalInterface(String string) {
+ return null;
+ }
+
+ @Override
+ public boolean transact(int p, Parcel parcel, Parcel parcel1, int p1) throws RemoteException {
+ return false;
+ }
+
+ @Override
+ public boolean unlinkToDeath(IBinder.DeathRecipient deathRecipient, int p) {
+ return false;
+ }
+
+ public static final String TAG = "SOSBinder";
+ SOSCenterService getService() {
+ return SOSCenterService.this;
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ LogUtils.d(TAG, "onCreate");
+ mSOSCenterServiceModel = SOSCenterServiceModel.loadBean(this, SOSCenterServiceModel.class);
+ if(mSOSCenterServiceModel == null) {
+ mSOSCenterServiceModel = new SOSCenterServiceModel();
+ SOSCenterServiceModel.saveBean(this, mSOSCenterServiceModel);
+ }
+ runMainThread();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ LogUtils.d(TAG, "onStartCommand");
+
+ runMainThread();
+
+ return mSOSCenterServiceModel.isEnable() ? Service.START_STICKY: super.onStartCommand(intent, flags, startId);
+ }
+
+ void runMainThread() {
+ mSOSCenterServiceModel = mSOSCenterServiceModel.loadBean(this, SOSCenterServiceModel.class);
+ if (mSOSCenterServiceModel.isEnable()
+ && _MainThread == null) {
+ getMainThreadInstance().start();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ LogUtils.d(TAG, "onDestroy");
+ mSOSCenterServiceModel = SOSCenterServiceModel.loadBean(this, SOSCenterServiceModel.class);
+ if (mSOSCenterServiceModel.isEnable()) {
+ LogUtils.d(TAG, "mSOSCenterServiceModel.isEnable()");
+// ISOSAPP iSOSAPP = (ISOSAPP)getApplication();
+// iSOSAPP.helpISOSService(getISOSServiceIntentWhichAskForHelp());
+ }
+ if (_MainThread != null) {
+ _MainThread.isExist = true;
+ _MainThread = null;
+ }
+ }
+
+ public static void stopISOSService(Context context) {
+ LogUtils.d(TAG, "stopISOSService");
+ SOSCenterServiceModel bean = new SOSCenterServiceModel();
+ bean.setIsEnable(false);
+ SOSCenterServiceModel.saveBean(context, bean);
+ context.stopService(new Intent(context, SOSCenterServiceModel.class));
+ }
+
+ public static void startISOSService(Context context) {
+ LogUtils.d(TAG, "startISOSService");
+ SOSCenterServiceModel bean = new SOSCenterServiceModel();
+ bean.setIsEnable(true);
+ SOSCenterServiceModel.saveBean(context, bean);
+ context.startService(new Intent(context, SOSCenterServiceModel.class));
+ }
+
+ public String getMessage() {
+ return "Hello from SOSCenterServiceModel";
+ }
+
+ static class MainThread extends Thread {
+ volatile boolean isExist = false;
+
+ public void setIsExist(boolean isExist) {
+ this.isExist = isExist;
+ }
+
+ public boolean isExist() {
+ return isExist;
+ }
+
+ @Override
+ public void run() {
+ super.run();
+ while (!isExist) {
+ LogUtils.d(TAG, "run");
+ try {
+ sleep(1000);
+ } catch (InterruptedException e) {
+ LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
+ }
+ }
+ }
+
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOSCenterServiceModel.java b/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOSCenterServiceModel.java
new file mode 100644
index 0000000..b69b89b
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOSCenterServiceModel.java
@@ -0,0 +1,69 @@
+package cc.winboll.studio.winboll.sos;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/03/02 09:49:45
+ * @Describe SOSCenterServiceModel
+ * Simple Operate Signal Service Model.
+ */
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import cc.winboll.studio.libappbase.models.libs1520000.BaseBean;
+import java.io.IOException;
+
+public class SOSCenterServiceModel extends BaseBean {
+
+ public static final String TAG = "SOSCenterServiceModel";
+
+ boolean isEnable;
+
+ public SOSCenterServiceModel() {
+ this.isEnable = false;
+ }
+
+ public void setIsEnable(boolean isEnable) {
+ this.isEnable = isEnable;
+ }
+
+ public boolean isEnable() {
+ return isEnable;
+ }
+
+ @Override
+ public String getName() {
+ return SOSCenterServiceModel.class.getName();
+ }
+
+ @Override
+ public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
+ super.writeThisToJsonWriter(jsonWriter);
+ jsonWriter.name("isEnable").value(isEnable());
+
+ }
+
+ @Override
+ public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
+ if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else {
+ if (name.equals("isEnable")) {
+ setIsEnable(jsonReader.nextBoolean());
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
+ jsonReader.beginObject();
+ while (jsonReader.hasNext()) {
+ String name = jsonReader.nextName();
+ if (!initObjectsFromJsonReader(jsonReader, name)) {
+ jsonReader.skipValue();
+ }
+ }
+ // 结束 JSON 对象
+ jsonReader.endObject();
+ return this;
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOSCenterServiceReceiver.java b/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOSCenterServiceReceiver.java
new file mode 100644
index 0000000..14dd36a
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOSCenterServiceReceiver.java
@@ -0,0 +1,29 @@
+package cc.winboll.studio.winboll.sos;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/02/27 14:04:35
+ * @Describe SOSCenterServiceReceiver
+ */
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import cc.winboll.studio.libappbase.LogUtils;
+
+public class SOSCenterServiceReceiver extends BroadcastReceiver {
+
+ public static final String TAG = "SOSCenterServiceReceiver";
+
+ public static final String ACTION_SOS = SOSCenterServiceReceiver.class.getName() + ".ACTION_SOS";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action.equals(ACTION_SOS)) {
+ // 处理接收到的广播消息
+ LogUtils.d(TAG, String.format("Action %s \n%s\n%s", action));
+ } else {
+ LogUtils.d(TAG, String.format("%s", action));
+ }
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOSObject.java b/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOSObject.java
new file mode 100644
index 0000000..caeeac4
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/sos/SOSObject.java
@@ -0,0 +1,86 @@
+package cc.winboll.studio.winboll.sos;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/02/27 14:12:05
+ * @Describe SOSBean
+ */
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import cc.winboll.studio.libappbase.models.libs1520000.BaseBean;
+import java.io.IOException;
+
+public class SOSObject extends BaseBean {
+
+ public static final String TAG = "SOSObject";
+
+ String objectPackageName;
+ String objectServiveName;
+
+ public SOSObject() {
+ this.objectPackageName = "";
+ this.objectServiveName = "";
+ }
+
+ public SOSObject(String objectPackageName, String objectServiveName) {
+ this.objectPackageName = objectPackageName;
+ this.objectServiveName = objectServiveName;
+ }
+
+ public void setObjectPackageName(String objectPackageName) {
+ this.objectPackageName = objectPackageName;
+ }
+
+ public String getObjectPackageName() {
+ return objectPackageName;
+ }
+
+ public void setObjectServiveName(String objectServiveName) {
+ this.objectServiveName = objectServiveName;
+ }
+
+ public String getObjectServiveName() {
+ return objectServiveName;
+ }
+
+ @Override
+ public String getName() {
+ return SOSObject.class.getName();
+ }
+
+ @Override
+ public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
+ super.writeThisToJsonWriter(jsonWriter);
+ jsonWriter.name("objectPackageName").value(getObjectPackageName());
+ jsonWriter.name("objectServiveName").value(getObjectServiveName());
+
+ }
+
+ @Override
+ public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
+ if (super.initObjectsFromJsonReader(jsonReader, name)) { return true; } else {
+ if (name.equals("objectPackageName")) {
+ setObjectPackageName(jsonReader.nextString());
+ } else if (name.equals("objectServiveName")) {
+ setObjectServiveName(jsonReader.nextString());
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
+ jsonReader.beginObject();
+ while (jsonReader.hasNext()) {
+ String name = jsonReader.nextName();
+ if (!initObjectsFromJsonReader(jsonReader, name)) {
+ jsonReader.skipValue();
+ }
+ }
+ // 结束 JSON 对象
+ jsonReader.endObject();
+ return this;
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/termux/MyTermuxActivity.java b/winboll/src/main/java/cc/winboll/studio/winboll/termux/MyTermuxActivity.java
new file mode 100644
index 0000000..78d9013
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/termux/MyTermuxActivity.java
@@ -0,0 +1,554 @@
+package cc.winboll.studio.winboll.termux;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.ShortcutManager;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.winboll.R;
+import cc.winboll.studio.winboll.models.TermuxButtonManager;
+import cc.winboll.studio.winboll.models.TermuxButtonModel;
+import cc.winboll.studio.winboll.termux.TermuxCommandExecutor;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+public class MyTermuxActivity extends AppCompatActivity {
+
+ public static final String TAG = "MyTermuxActivity";
+ public static final String EXTRA_BUTTON_NAME = "extra_button_name";
+ public static final String ACTION_EXECUTE_SHORTCUT =
+ "cc.winboll.studio.winboll.action.EXECUTE_TERMUX_BUTTON";
+
+ private Toolbar mToolbar;
+ private ListView mListView;
+ private Button mBtnAdd;
+ private ButtonAdapter mAdapter;
+ private ArrayList mButtonList;
+
+ private static final int REQUEST_PICK_ICON = 100;
+ private String mDialogIconPath = "";
+ private ImageView mDialogIconView;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_my_termux);
+
+ initToolbar();
+ initListView();
+ initAddButton();
+ refreshList();
+ handleShortcutIntent();
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ setIntent(intent);
+ handleShortcutIntent();
+ }
+
+ private void handleShortcutIntent() {
+ Intent intent = getIntent();
+ if (intent != null && ACTION_EXECUTE_SHORTCUT.equals(intent.getAction())) {
+ String buttonName = intent.getStringExtra(EXTRA_BUTTON_NAME);
+ if (buttonName != null && buttonName.length() > 0) {
+ TermuxButtonModel model = findButtonByName(buttonName);
+ if (model != null) {
+ TermuxCommandExecutor.openTermuxBash(this,
+ model.getButtonName(), model.getExeCommand(),
+ model.getWorkDir(), true);
+ } else {
+ Toast.makeText(this, R.string.toast_shortcut_not_found,
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+ }
+
+ private TermuxButtonModel findButtonByName(String name) {
+ for (int i = 0; i < mButtonList.size(); i++) {
+ if (name.equals(mButtonList.get(i).getButtonName())) {
+ return mButtonList.get(i);
+ }
+ }
+ return null;
+ }
+
+ private void initToolbar() {
+ mToolbar = findViewById(R.id.toolbar);
+ if (mToolbar != null) {
+ setSupportActionBar(mToolbar);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ finish();
+ }
+ });
+ }
+ }
+
+ private void initListView() {
+ mListView = findViewById(R.id.list_termux_buttons);
+ mButtonList = new ArrayList();
+ mAdapter = new ButtonAdapter();
+ mListView.setAdapter(mAdapter);
+
+ mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ TermuxButtonModel model = mButtonList.get(position);
+ TermuxCommandExecutor.openTermuxBash(MyTermuxActivity.this,
+ model.getButtonName(), model.getExeCommand(),
+ model.getWorkDir(), true);
+ }
+ });
+
+ mListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
+ @Override
+ public boolean onItemLongClick(AdapterView> parent, View view, int position, long id) {
+ showContextMenu(position);
+ return true;
+ }
+ });
+ }
+
+ private void initAddButton() {
+ mBtnAdd = findViewById(R.id.btn_add_termux_button);
+ mBtnAdd.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ showButtonDialog(-1, null);
+ }
+ });
+ }
+
+ private void refreshList() {
+ mButtonList.clear();
+ ArrayList loaded = TermuxButtonManager.loadButtons(this);
+ if (loaded != null) {
+ mButtonList.addAll(loaded);
+ }
+ mAdapter.notifyDataSetChanged();
+ }
+
+ private void showContextMenu(final int position) {
+ final TermuxButtonModel model = mButtonList.get(position);
+ final String[] items = new String[]{
+ getString(R.string.menu_execute),
+ getString(R.string.menu_edit),
+ getString(R.string.menu_delete),
+ getString(R.string.menu_create_shortcut),
+ getString(R.string.menu_cancel)
+ };
+ new AlertDialog.Builder(this)
+ .setTitle(model.getButtonName())
+ .setItems(items, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == 0) {
+ TermuxCommandExecutor.openTermuxBash(MyTermuxActivity.this,
+ model.getButtonName(), model.getExeCommand(),
+ model.getWorkDir(), true);
+ } else if (which == 1) {
+ showButtonDialog(position, model);
+ } else if (which == 2) {
+ showDeleteConfirmDialog(position);
+ } else if (which == 3) {
+ createDesktopShortcut(model);
+ }
+ }
+ })
+ .show();
+ }
+
+ private Bitmap loadButtonIcon(TermuxButtonModel model) {
+ String iconPath = model.getIconPath();
+ if (iconPath == null || iconPath.length() == 0) return null;
+ File iconFile = getIconFile(iconPath);
+ if (!iconFile.exists()) return null;
+ return BitmapFactory.decodeFile(iconFile.getAbsolutePath());
+ }
+
+ private void createDesktopShortcut(TermuxButtonModel model) {
+ Intent shortcutIntent = new Intent(this, MyTermuxActivity.class);
+ shortcutIntent.setAction(ACTION_EXECUTE_SHORTCUT);
+ shortcutIntent.putExtra(EXTRA_BUTTON_NAME, model.getButtonName());
+ shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ try {
+ ShortcutManager manager = getSystemService(ShortcutManager.class);
+ if (manager == null || !manager.isRequestPinShortcutSupported()) {
+ Toast.makeText(this, R.string.toast_shortcut_not_supported,
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ Icon icon;
+ Bitmap bmp = loadButtonIcon(model);
+ if (bmp != null) {
+ icon = Icon.createWithBitmap(bmp);
+ } else {
+ icon = Icon.createWithResource(this,
+ android.R.drawable.ic_menu_manage);
+ }
+ String shortcutId = "termux_" + model.getButtonName();
+ android.content.pm.ShortcutInfo info =
+ new android.content.pm.ShortcutInfo.Builder(this, shortcutId)
+ .setShortLabel(model.getButtonName())
+ .setLongLabel(model.getButtonName())
+ .setIcon(icon)
+ .setIntent(shortcutIntent)
+ .build();
+ manager.requestPinShortcut(info, null);
+ } catch (Exception e) {
+ LogUtils.e(TAG, "createDesktopShortcut error: " + e.getMessage());
+ Toast.makeText(this, R.string.toast_shortcut_failed,
+ Toast.LENGTH_SHORT).show();
+ }
+ } else {
+ try {
+ Intent installIntent =
+ new Intent("com.android.launcher.action.INSTALL_SHORTCUT");
+ installIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
+ installIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME,
+ model.getButtonName());
+ Bitmap bmp = loadButtonIcon(model);
+ if (bmp != null) {
+ installIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON, bmp);
+ } else {
+ installIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
+ Intent.ShortcutIconResource.fromContext(this,
+ android.R.drawable.ic_menu_manage));
+ }
+ installIntent.putExtra("duplicate", false);
+ sendBroadcast(installIntent);
+ } catch (Exception e) {
+ LogUtils.e(TAG, "createDesktopShortcut error: " + e.getMessage());
+ Toast.makeText(this, R.string.toast_shortcut_failed,
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+
+ private void showDeleteConfirmDialog(final int position) {
+ new AlertDialog.Builder(this)
+ .setTitle(getString(R.string.dialog_delete_title))
+ .setMessage(getString(R.string.dialog_delete_message) + mButtonList.get(position).getButtonName())
+ .setPositiveButton(getString(R.string.dialog_confirm), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ TermuxButtonManager.deleteButton(MyTermuxActivity.this, mButtonList, position);
+ refreshList();
+ Toast.makeText(MyTermuxActivity.this, R.string.toast_deleted, Toast.LENGTH_SHORT).show();
+ }
+ })
+ .setNegativeButton(getString(R.string.dialog_cancel), null)
+ .show();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == REQUEST_PICK_ICON && resultCode == RESULT_OK
+ && data != null && data.getData() != null) {
+ Uri uri = data.getData();
+ String fileName = copyIconToAppStorage(uri);
+ if (fileName != null) {
+ mDialogIconPath = fileName;
+ File iconFile = getIconFile(fileName);
+ if (iconFile.exists() && mDialogIconView != null) {
+ Bitmap bitmap = BitmapFactory.decodeFile(
+ iconFile.getAbsolutePath());
+ if (bitmap != null) {
+ mDialogIconView.setImageBitmap(bitmap);
+ }
+ }
+ } else {
+ Toast.makeText(this, R.string.toast_icon_copy_failed,
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+
+ private String copyIconToAppStorage(Uri uri) {
+ try {
+ File iconDir = new File(getFilesDir(), "termux_icons");
+ if (!iconDir.exists()) {
+ iconDir.mkdirs();
+ }
+ String fileName = System.currentTimeMillis() + ".png";
+ File destFile = new File(iconDir, fileName);
+ InputStream in = null;
+ FileOutputStream out = null;
+ try {
+ in = getContentResolver().openInputStream(uri);
+ out = new FileOutputStream(destFile);
+ byte[] buffer = new byte[8192];
+ int count;
+ while ((count = in.read(buffer)) > 0) {
+ out.write(buffer, 0, count);
+ }
+ } finally {
+ if (in != null) {
+ try { in.close(); } catch (Exception ignored) {}
+ }
+ if (out != null) {
+ try { out.close(); } catch (Exception ignored) {}
+ }
+ }
+ return fileName;
+ } catch (Exception e) {
+ LogUtils.e(TAG, "copyIconToAppStorage error: " + e.getMessage());
+ return null;
+ }
+ }
+
+ private File getIconFile(String iconPath) {
+ return new File(getFilesDir(), "termux_icons/" + iconPath);
+ }
+
+ private void showButtonDialog(final int index, final TermuxButtonModel model) {
+ final boolean isEdit = (model != null);
+ mDialogIconPath = (model != null && model.getIconPath() != null)
+ ? model.getIconPath() : "";
+
+ LinearLayout layout = new LinearLayout(this);
+ layout.setOrientation(LinearLayout.VERTICAL);
+ layout.setPadding(40, 20, 40, 20);
+
+ final EditText etName = new EditText(this);
+ etName.setHint(R.string.hint_button_name);
+ if (model != null) {
+ etName.setText(model.getButtonName());
+ }
+ layout.addView(etName);
+
+ final EditText etCommand = new EditText(this);
+ etCommand.setHint(R.string.hint_exe_command);
+ if (model != null) {
+ etCommand.setText(model.getExeCommand());
+ }
+ layout.addView(etCommand);
+
+ final EditText etWorkDir = new EditText(this);
+ etWorkDir.setHint(R.string.hint_work_dir);
+ if (model != null) {
+ etWorkDir.setText(model.getWorkDir());
+ }
+ layout.addView(etWorkDir);
+
+ // Icon section
+ final TextView tvIconLabel = new TextView(this);
+ tvIconLabel.setText(R.string.label_icon);
+ tvIconLabel.setTextSize(14);
+ tvIconLabel.setTextColor(Color.GRAY);
+ layout.addView(tvIconLabel);
+
+ LinearLayout iconRow = new LinearLayout(this);
+ iconRow.setOrientation(LinearLayout.HORIZONTAL);
+
+ final ImageView ivIcon = new ImageView(this);
+ LinearLayout.LayoutParams iconLp = new LinearLayout.LayoutParams(100, 100);
+ iconLp.setMargins(0, 8, 16, 8);
+ ivIcon.setLayoutParams(iconLp);
+ ivIcon.setScaleType(ImageView.ScaleType.FIT_CENTER);
+ if (mDialogIconPath.length() > 0) {
+ File iconFile = getIconFile(mDialogIconPath);
+ if (iconFile.exists()) {
+ ivIcon.setImageBitmap(BitmapFactory.decodeFile(
+ iconFile.getAbsolutePath()));
+ }
+ }
+ iconRow.addView(ivIcon);
+ mDialogIconView = ivIcon;
+
+ LinearLayout iconBtnLayout = new LinearLayout(this);
+ iconBtnLayout.setOrientation(LinearLayout.VERTICAL);
+
+ Button btnSelectIcon = new Button(this);
+ btnSelectIcon.setText(R.string.btn_select_icon);
+ btnSelectIcon.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType("image/*");
+ startActivityForResult(intent, REQUEST_PICK_ICON);
+ }
+ });
+ iconBtnLayout.addView(btnSelectIcon);
+
+ Button btnClearIcon = new Button(this);
+ btnClearIcon.setText(R.string.btn_clear_icon);
+ btnClearIcon.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mDialogIconPath = "";
+ mDialogIconView.setImageDrawable(null);
+ }
+ });
+ iconBtnLayout.addView(btnClearIcon);
+
+ iconRow.addView(iconBtnLayout);
+ layout.addView(iconRow);
+
+ int titleResId = isEdit ? R.string.dialog_edit_title : R.string.dialog_add_title;
+ new AlertDialog.Builder(this)
+ .setTitle(titleResId)
+ .setView(layout)
+ .setPositiveButton(getString(R.string.dialog_save), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ String name = etName.getText().toString().trim();
+ String command = etCommand.getText().toString().trim();
+ String workDir = etWorkDir.getText().toString().trim();
+ if (name.isEmpty() || command.isEmpty()) {
+ Toast.makeText(MyTermuxActivity.this, R.string.toast_fields_required, Toast.LENGTH_SHORT).show();
+ return;
+ }
+ TermuxButtonModel newModel = new TermuxButtonModel();
+ newModel.setButtonName(name);
+ newModel.setExeCommand(command);
+ newModel.setWorkDir(workDir);
+ newModel.setIconPath(mDialogIconPath);
+ if (isEdit) {
+ TermuxButtonManager.updateButton(MyTermuxActivity.this, mButtonList, index, newModel);
+ } else {
+ TermuxButtonManager.addButton(MyTermuxActivity.this, mButtonList, newModel);
+ }
+ refreshList();
+ }
+ })
+ .setNegativeButton(getString(R.string.dialog_cancel), null)
+ .show();
+ }
+
+ private class ButtonAdapter extends BaseAdapter {
+ @Override
+ public int getCount() {
+ return mButtonList.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mButtonList.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ImageView ivIcon;
+ TextView tv;
+
+ if (convertView instanceof LinearLayout) {
+ LinearLayout ll = (LinearLayout) convertView;
+ ivIcon = (ImageView) ll.getChildAt(0);
+ tv = (TextView) ll.getChildAt(1);
+ } else {
+ LinearLayout ll = new LinearLayout(MyTermuxActivity.this);
+ ll.setOrientation(LinearLayout.HORIZONTAL);
+ ll.setGravity(Gravity.CENTER_VERTICAL);
+ ll.setPadding(30, 16, 30, 16);
+
+ ivIcon = new ImageView(MyTermuxActivity.this);
+ ivIcon.setScaleType(ImageView.ScaleType.FIT_CENTER);
+ ivIcon.setVisibility(View.GONE);
+ ll.addView(ivIcon);
+
+ tv = new TextView(MyTermuxActivity.this);
+ tv.setTextSize(16);
+ tv.setMinHeight(80);
+ tv.setGravity(Gravity.CENTER_VERTICAL);
+ tv.setPadding(16, 0, 0, 0);
+ ll.addView(tv);
+
+ convertView = ll;
+ }
+
+ TermuxButtonModel model = mButtonList.get(position);
+ String name = model.getButtonName();
+ String cmd = model.getExeCommand();
+ String fullText = name + "\n" + cmd;
+ SpannableString sp = new SpannableString(fullText);
+ sp.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ sp.setSpan(new ForegroundColorSpan(Color.BLUE), 0, name.length(),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ tv.setText(sp);
+
+ String iconPath = model.getIconPath();
+ if (iconPath != null && iconPath.length() > 0) {
+ File iconFile = getIconFile(iconPath);
+ if (iconFile.exists()) {
+ Bitmap bitmap = BitmapFactory.decodeFile(
+ iconFile.getAbsolutePath());
+ if (bitmap != null) {
+ float density = getResources()
+ .getDisplayMetrics().density;
+ int marginPx = (int) (5 * density + 0.5f);
+ int estHeight = tv.getMinHeight();
+ int iconWidth = estHeight * bitmap.getWidth()
+ / bitmap.getHeight();
+ iconWidth = Math.max(iconWidth, (int)(32*density));
+ iconWidth = Math.min(iconWidth, (int)(120*density));
+ ivIcon.setImageBitmap(bitmap);
+ ivIcon.setScaleType(
+ ImageView.ScaleType.FIT_CENTER);
+ LinearLayout.LayoutParams lp =
+ new LinearLayout.LayoutParams(iconWidth,
+ LinearLayout.LayoutParams.MATCH_PARENT);
+ lp.setMargins(marginPx, marginPx,
+ marginPx, marginPx);
+ ivIcon.setLayoutParams(lp);
+ ivIcon.setVisibility(View.VISIBLE);
+ } else {
+ ivIcon.setImageDrawable(null);
+ ivIcon.setVisibility(View.GONE);
+ }
+ } else {
+ ivIcon.setImageDrawable(null);
+ ivIcon.setVisibility(View.GONE);
+ }
+ } else {
+ ivIcon.setImageDrawable(null);
+ ivIcon.setVisibility(View.GONE);
+ }
+
+ return convertView;
+ }
+ }
+}
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..5720787
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/termux/TermuxCommandExecutor.java
@@ -0,0 +1,354 @@
+package cc.winboll.studio.winboll.termux;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.os.Build;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
+import android.widget.TextView;
+import android.widget.Toast;
+import androidx.biometric.BiometricPrompt;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.FragmentActivity;
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.winboll.R;
+import com.termux.shared.termux.TermuxConstants;
+import com.termux.shared.shell.command.ExecutionCommand.Runner;
+import java.util.concurrent.Executor;
+
+/**
+ * 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;
+ private static final String TERMUX_HOME_PATH = "/data/data/com.termux/files/home";
+
+ /**
+ * 执行 Termux 命令(核心方法)
+ * @param context 上下文(如 Activity、Service)
+ * @param command 要执行的命令路径(如 "/bin/ls"、"/data/data/com.termux/files/usr/bin/bash")
+ * @param args 命令参数(如 ["-l", "/data/data/com.termux/files/home"])
+ * @param workDir 工作目录(可为 null,默认 Termux 主目录)
+ * @param isBackground 是否后台执行(true=后台,false=终端会话执行)
+ * @param resultDir 命令结果输出目录(可为 null,不输出到文件)
+ * @return 是否成功发送命令请求
+ */
+ public static boolean executeCommand(Context context, String command, String[] args, String workDir, boolean isBackground, String resultDir) {
+ // 1. 校验上下文和命令合法性
+ if (context == null || command == null || command.isEmpty()) {
+ LogUtils.e(TAG, "执行命令失败:上下文或命令为空");
+ return false;
+ }
+
+ // 2. 校验 Termux 是否安装(新增:提前校验,避免白跑流程)
+ if (!isTermuxInstalled(context)) {
+ LogUtils.e(TAG, "执行命令失败:Termux 未安装");
+ return false;
+ }
+
+ // 3. 创建 Intent 并设置目标 Service
+ Intent intent = new Intent(TERMUX_RUN_CMD_ACTION);
+ intent.setClassName(TERMUX_PACKAGE_NAME, TERMUX_RUN_CMD_SERVICE_CLASS); // 用正确包名
+ intent.setPackage(TERMUX_PACKAGE_NAME); // 明确包名,避免歧义
+
+ // 4. 设置核心命令参数(遵循 Termux RunCommandService 规范)
+ intent.putExtra(TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH, command);
+ if (args != null && args.length > 0) {
+ intent.putExtra(TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS, args);
+ LogUtils.d(TAG, "命令参数:" + String.join(",", args));
+ }
+ if (workDir != null && !workDir.isEmpty()) {
+ intent.putExtra(TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE.EXTRA_WORKDIR, workDir);
+ LogUtils.d(TAG, "工作目录:" + workDir);
+ }
+
+ // 5. 设置执行模式(后台/终端会话)
+ intent.putExtra(TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, isBackground);
+ String runner = isBackground ? Runner.APP_SHELL.getName() : Runner.TERMINAL_SESSION.getName();
+ intent.putExtra(TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE.EXTRA_RUNNER, runner);
+ LogUtils.d(TAG, "执行模式:" + (isBackground ? "后台" : "终端会话") + ",Runner:" + runner);
+
+ // 6. 设置命令结果输出(可选,输出到文件)
+ if (resultDir != null && !resultDir.isEmpty()) {
+ intent.putExtra(TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE.EXTRA_RESULT_DIRECTORY, resultDir);
+ intent.putExtra(TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE.EXTRA_RESULT_SINGLE_FILE, true);
+ intent.putExtra(TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_BASENAME, "authcenter_cmd_result");
+ LogUtils.d(TAG, "结果输出目录:" + resultDir);
+ }
+
+ // 7. 强制创建新终端会话(而非复用已有会话),避免第二次点击直接弹出旧窗口
+ if (!isBackground) {
+ intent.putExtra("com.termux.RUN_COMMAND_SESSION_ACTION", 0);
+ }
+
+ // 8. 允许替换参数中的逗号替代字符
+ intent.putExtra(TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE.EXTRA_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS, true);
+
+ // 8. 发送请求(区分 Android O 及以上的前台服务)
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ context.startForegroundService(intent);
+ LogUtils.d(TAG, "Android O+ 启动前台服务发送命令");
+ } else {
+ context.startService(intent);
+ LogUtils.d(TAG, "启动普通服务发送命令");
+ }
+ LogUtils.i(TAG, "命令发送成功:command=" + command);
+ return true;
+ } catch (Exception e) {
+ LogUtils.e(TAG, "命令发送失败:" + e.getMessage(), e);
+ return false;
+ }
+ }
+
+ /**
+ * 简化方法:执行 Termux 终端命令(默认工作目录,终端会话执行)
+ * @param context 上下文
+ * @param command 命令(如 "ls -l /home"、"echo 'hello termux'")
+ * @return 是否成功发送
+ */
+ public static boolean executeTerminalCommand(Context context, String command) {
+ LogUtils.d(TAG, "调用 executeTerminalCommand,命令:" + command);
+ if (command == null || command.isEmpty()) {
+ LogUtils.e(TAG, "命令为空,执行失败");
+ return false;
+ }
+ // 通过 bash 执行任意终端命令
+ String[] args = {"-c", command};
+ return executeCommand(
+ context,
+ "/data/data/com.termux/files/usr/bin/bash", // Termux 默认 bash 路径(正确)
+ args,
+ TERMUX_HOME_PATH, // 默认工作目录
+ false, // 终端会话执行(可见)
+ null // 不输出到文件
+ );
+ }
+
+ /**
+ * 简化方法:后台执行 Termux 命令(无输出文件)
+ * @param context 上下文
+ * @param command 命令路径
+ * @param args 命令参数
+ * @return 是否成功发送
+ */
+ public static boolean executeBackgroundCommand(Context context, String command, String[] args) {
+ LogUtils.d(TAG, "调用 executeBackgroundCommand,command=" + command);
+ return executeCommand(
+ context,
+ command,
+ args,
+ null,
+ true, // 后台执行
+ null
+ );
+ }
+
+ /**
+ * 校验 Termux 是否安装(修复核心错误)
+ * @param context 上下文
+ * @return Termux 是否已安装
+ */
+ public static boolean isTermuxInstalled(Context context) {
+ LogUtils.d(TAG, "校验 Termux 是否安装,包名:" + TERMUX_PACKAGE_NAME);
+ if (context == null) {
+ LogUtils.e(TAG, "校验失败:上下文为空");
+ return false;
+ }
+ try {
+ // 用正确的 Termux 包名查询(com.termux)
+ context.getPackageManager().getPackageInfo(TERMUX_PACKAGE_NAME, PackageManager.GET_ACTIVITIES);
+ LogUtils.d(TAG, "Termux 已安装");
+ return true;
+ } catch (PackageManager.NameNotFoundException e) {
+ LogUtils.w(TAG, "Termux 未安装:" + e.getMessage());
+ return false;
+ } catch (Exception e) {
+ LogUtils.e(TAG, "校验 Termux 安装状态异常:" + e.getMessage(), e);
+ return false;
+ }
+ }
+
+ /**
+ * 校验 Termux 是否允许外部应用调用
+ * @return 校验提示信息
+ */
+ public static String checkTermuxExternalAppPermission() {
+ String tip = "请确保 Termux 已开启「允许外部应用调用」权限:\n1. 打开 Termux 输入:termux-setup-storage\n2. 编辑配置文件:echo \"allow-external-apps = true\" > ~/.termux/termux.properties\n3. 重启 Termux 生效";
+ LogUtils.d(TAG, "外部应用调用权限提示:" + tip);
+ return tip;
+ }
+
+ private static String pendingTargetCmd;
+ private static String pendingDisplayCmd;
+ private static String pendingDisplayName;
+
+ public static boolean openTermuxBash(Context context, String command) {
+ return openTermuxBash(context, null, command, "~", true);
+ }
+
+ public static boolean openTermuxBash(Context context, String command, String workDir) {
+ return openTermuxBash(context, null, command, workDir, true);
+ }
+
+ public static boolean openTermuxBash(Context context, String command, String workDir, boolean keepAlive) {
+ return openTermuxBash(context, null, command, workDir, keepAlive);
+ }
+
+ public static boolean openTermuxBash(Context context, String displayName, String command, String workDir, boolean keepAlive) {
+ LogUtils.d(TAG, "openTermuxBash() 按钮点击,执行Gradle命令(实时输出)");
+
+ // 1. 校验Termux是否安装
+ if (!TermuxCommandExecutor.isTermuxInstalled(context)) {
+ LogUtils.e(TAG, "openTermuxBash() 错误:未安装Termux应用");
+ return false;
+ }
+
+ // 2. 定义核心路径(确保路径与Termux中一致)
+ String projectPath = TERMUX_HOME_PATH;
+ if (workDir.startsWith("~") || workDir.startsWith(".")) {
+ projectPath = TERMUX_HOME_PATH + "/" + workDir.substring(1);
+ }
+
+ // 3. 构造命令(核心:用stdbuf禁用缓冲,实现实时输出)
+ String targetCmd = "";
+ // 步骤1:进入项目目录(不存在则创建)
+ targetCmd += "cd " + projectPath + " && ";
+ // 步骤2:加载环境变量
+ targetCmd += "source ~/.bashrc && ";
+ // 步骤3:显式配置PATH
+ targetCmd += "export PATH=/data/data/com.termux/files/usr/bin:$PATH && ";
+ // 步骤4:将用户输入的字面\n转换为shell命令分隔符,
+ // 确保"cd ~/Sources\npwd"这类输入能分段执行
+ String execCommand = command.replace("\\n", "; ");
+ // 步骤5:执行设定的命令(直接由外层bash解释,避免stdbuf对shell内置命令无效)
+ targetCmd += execCommand;
+ // 步骤6:需要保持终端可见时追加交互式bash
+ if (keepAlive) {
+ targetCmd += "; stdbuf -o0 -e0 -i0 bash";
+ }
+
+ // 步骤7:指纹验证后执行命令
+ if (context instanceof FragmentActivity) {
+ pendingTargetCmd = targetCmd;
+ pendingDisplayCmd = execCommand;
+ pendingDisplayName = displayName;
+ showFingerprintAndExecute((FragmentActivity) context);
+ return true;
+ } else {
+ return TermuxCommandExecutor.executeTerminalCommand(context, targetCmd);
+ }
+ }
+
+ private static void showFingerprintAndExecute(final FragmentActivity activity) {
+ String displayName = pendingDisplayName != null
+ ? pendingDisplayName : "";
+ final StringBuilder sb = new StringBuilder();
+ if (pendingDisplayCmd != null) {
+ sb.append(pendingDisplayCmd);
+ }
+ final String cmdText = sb.toString();
+
+ SpannableString message = new SpannableString(
+ activity.getString(R.string.biometric_description,
+ displayName, cmdText));
+ int nameIdx = message.toString().indexOf(displayName);
+ if (nameIdx >= 0 && displayName.length() > 0) {
+ message.setSpan(new StyleSpan(Typeface.BOLD), nameIdx,
+ nameIdx + displayName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ message.setSpan(new ForegroundColorSpan(Color.BLUE), nameIdx,
+ nameIdx + displayName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ final AlertDialog dialog = new AlertDialog.Builder(activity)
+ .setTitle(R.string.biometric_title)
+ .setMessage(message)
+ .setPositiveButton(R.string.biometric_start,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ startBiometricAuth(activity);
+ }
+ })
+ .setNegativeButton(R.string.dialog_cancel,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ clearPending();
+ }
+ })
+ .setCancelable(false)
+ .show();
+
+ TextView tv = (TextView) dialog.findViewById(android.R.id.message);
+ if (tv != null) {
+ tv.setText(message);
+ }
+ }
+
+ private static void startBiometricAuth(final FragmentActivity activity) {
+ Executor executor = ContextCompat.getMainExecutor(activity);
+ final BiometricPrompt biometricPrompt = new BiometricPrompt(activity,
+ executor, new BiometricPrompt.AuthenticationCallback() {
+ @Override
+ public void onAuthenticationSucceeded(
+ BiometricPrompt.AuthenticationResult result) {
+ super.onAuthenticationSucceeded(result);
+ executePendingCommand(activity);
+ }
+
+ @Override
+ public void onAuthenticationError(int errorCode,
+ CharSequence errString) {
+ super.onAuthenticationError(errorCode, errString);
+ clearPending();
+ Toast.makeText(activity, R.string.toast_auth_failed,
+ Toast.LENGTH_SHORT).show();
+ }
+
+ @Override
+ public void onAuthenticationFailed() {
+ super.onAuthenticationFailed();
+ }
+ });
+
+ BiometricPrompt.PromptInfo promptInfo =
+ new BiometricPrompt.PromptInfo.Builder()
+ .setTitle(activity.getString(R.string.biometric_title))
+ .setNegativeButtonText(activity.getString(R.string.dialog_cancel))
+ .setConfirmationRequired(false)
+ .build();
+
+ biometricPrompt.authenticate(promptInfo);
+ }
+
+ private static void executePendingCommand(Context context) {
+ if (pendingTargetCmd != null) {
+ String cmd = pendingTargetCmd;
+ clearPending();
+ executeTerminalCommand(context, cmd);
+ }
+ }
+
+ private static void clearPending() {
+ pendingTargetCmd = null;
+ pendingDisplayCmd = null;
+ }
+}
+
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/theme/WinBoLLThemeBean.java b/winboll/src/main/java/cc/winboll/studio/winboll/theme/WinBoLLThemeBean.java
new file mode 100644
index 0000000..1d748d7
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/theme/WinBoLLThemeBean.java
@@ -0,0 +1,49 @@
+package cc.winboll.studio.winboll.theme;
+
+import cc.winboll.studio.libaes.models.AESThemeBean;
+import cc.winboll.studio.winboll.R;
+
+public class WinBoLLThemeBean {
+
+ public static final String TAG = "WinBoLLThemeBean";
+
+ public static int getDefaultThemeStyleID() {
+ return R.style.MyAppTheme;
+ }
+
+ public static int getThemeStyleID(AESThemeBean.ThemeType themeType) {
+ int themeStyleID = getDefaultThemeStyleID();
+ if (AESThemeBean.ThemeType.DEPTH == themeType) {
+ themeStyleID = R.style.MyDepthAppTheme;
+ } else if (AESThemeBean.ThemeType.SKY == themeType) {
+ themeStyleID = R.style.MySkyAppTheme;
+ } else if (AESThemeBean.ThemeType.GOLDEN == themeType) {
+ themeStyleID = R.style.MyGoldenAppTheme;
+ } else if (AESThemeBean.ThemeType.BEARING == themeType) {
+ themeStyleID = R.style.MyBearingAppTheme;
+ } else if (AESThemeBean.ThemeType.MEMOR == themeType) {
+ themeStyleID = R.style.MyMemorAppTheme;
+ } else if (AESThemeBean.ThemeType.TAO == themeType) {
+ themeStyleID = R.style.MyTaoAppTheme;
+ }
+ return themeStyleID;
+ }
+
+ public static AESThemeBean.ThemeType getThemeStyleType(int nThemeStyleID) {
+ AESThemeBean.ThemeType themeStyle = AESThemeBean.ThemeType.AES;
+ if (R.style.MyDepthAppTheme == nThemeStyleID) {
+ themeStyle = AESThemeBean.ThemeType.DEPTH;
+ } else if (R.style.MySkyAppTheme == nThemeStyleID) {
+ themeStyle = AESThemeBean.ThemeType.SKY;
+ } else if (R.style.MyGoldenAppTheme == nThemeStyleID) {
+ themeStyle = AESThemeBean.ThemeType.GOLDEN;
+ } else if (R.style.MyBearingAppTheme == nThemeStyleID) {
+ themeStyle = AESThemeBean.ThemeType.BEARING;
+ } else if (R.style.MyMemorAppTheme == nThemeStyleID) {
+ themeStyle = AESThemeBean.ThemeType.MEMOR;
+ } else if (R.style.MyTaoAppTheme == nThemeStyleID) {
+ themeStyle = AESThemeBean.ThemeType.TAO;
+ }
+ return themeStyle;
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/theme/WinBoLLThemeUtil.java b/winboll/src/main/java/cc/winboll/studio/winboll/theme/WinBoLLThemeUtil.java
new file mode 100644
index 0000000..1e6640d
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/theme/WinBoLLThemeUtil.java
@@ -0,0 +1,36 @@
+package cc.winboll.studio.winboll.theme;
+
+import android.content.Context;
+import cc.winboll.studio.libaes.models.AESThemeBean;
+import cc.winboll.studio.libaes.utils.AESThemeUtil;
+
+public class WinBoLLThemeUtil {
+
+ public static final String TAG = "WinBoLLThemeUtil";
+
+ public static int getThemeTypeID(Context context) {
+ AESThemeBean bean = AESThemeBean.loadBean(context, AESThemeBean.class);
+ int themeTypeID;
+ if (bean == null) {
+ themeTypeID = WinBoLLThemeBean.getDefaultThemeStyleID();
+ } else {
+ int aesStyleID = bean.getCurrentThemeTypeID();
+ AESThemeBean.ThemeType themeType = WinBoLLThemeBean.getThemeStyleType(aesStyleID);
+ themeTypeID = WinBoLLThemeBean.getThemeStyleID(themeType);
+ }
+ return themeTypeID;
+ }
+
+ public static void saveThemeStyleID(Context context, int nThemeTypeID) {
+ AESThemeBean bean = new AESThemeBean(nThemeTypeID);
+ AESThemeBean.saveBean(context, bean);
+ }
+
+ public static AESThemeBean.ThemeType getThemeStyleType(int nThemeStyleID) {
+ return WinBoLLThemeBean.getThemeStyleType(nThemeStyleID);
+ }
+
+ public static int getThemeStyleID(AESThemeBean.ThemeType themeType) {
+ return WinBoLLThemeBean.getThemeStyleID(themeType);
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/threads/MainServiceThread.java b/winboll/src/main/java/cc/winboll/studio/winboll/threads/MainServiceThread.java
new file mode 100644
index 0000000..80ecc3f
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/threads/MainServiceThread.java
@@ -0,0 +1,54 @@
+package cc.winboll.studio.winboll.threads;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/02/14 03:46:44
+ */
+import android.content.Context;
+import cc.winboll.studio.winboll.handlers.MainServiceHandler;
+import cc.winboll.studio.libappbase.LogUtils;
+import java.lang.ref.WeakReference;
+
+public class MainServiceThread extends Thread {
+
+ public static final String TAG = "MainServiceThread";
+
+ Context mContext;
+
+ // 控制线程是否退出的标志
+ volatile boolean isExist = false;
+
+ // 服务Handler, 用于线程发送消息使用
+ WeakReference mwrMainServiceHandler;
+
+ public void setIsExist(boolean isExist) {
+ this.isExist = isExist;
+ }
+
+ public boolean isExist() {
+ return isExist;
+ }
+
+ public MainServiceThread(Context context, MainServiceHandler handler) {
+ mContext = context;
+ mwrMainServiceHandler = new WeakReference(handler);
+ }
+
+ @Override
+ public void run() {
+ LogUtils.d(TAG, "run()");
+
+ while (!isExist()) {
+ //ToastUtils.show("run()");
+ //LogUtils.d(TAG, "run()");
+
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
+ }
+ }
+ LogUtils.d(TAG, "run() exit.");
+ }
+
+}
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..4b07046
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/unittest/TermuxEnvTestActivity.java
@@ -0,0 +1,454 @@
+package cc.winboll.studio.winboll.unittest;
+
+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 androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.libappbase.ToastUtils;
+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: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 = "/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() {
+ 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 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();
+
+ // 基础校验
+ 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
new file mode 100644
index 0000000..08aa2a1
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/unittest/TestWeWorkSpecSDK.java
@@ -0,0 +1,311 @@
+package cc.winboll.studio.winboll.unittest;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.Handler;
+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;
+
+/**
+ * @Author 豆包&ZhanGSKen
+ * @Date 2026/01/03 10:52
+ * @Describe 企业微信SDK接口测试(基础调试版)
+ * 包含:SDK初始化、基础接口调用、日志输出、主线程回调处理
+ */
+public class TestWeWorkSpecSDK extends AppCompatActivity implements IWinBoLLActivity, View.OnClickListener {
+
+ public static final String TAG = "TestWeWorkSpecSDK";
+
+ // ------------------- 企业微信SDK配置常量(需替换为实际项目参数) -------------------
+ // 企业微信 CorpID(从企业微信管理后台获取)
+ private static final String CORP_ID = "wwb37c73f34c722852";
+ // 应用 AgentID(从企业微信应用管理后台获取)
+ private static final String AGENT_ID = "your_agent_id_here";
+ // 应用 Secret(从企业微信应用管理后台获取,注意保密)
+ private static final String APP_SECRET = "your_app_secret_here";
+
+ // ------------------- Handler消息标识(主线程处理SDK回调) -------------------
+ private static final int MSG_SDK_INIT_SUCCESS = 1001;
+ private static final int MSG_SDK_INIT_FAILED = 1002;
+ private static final int MSG_GET_CORP_INFO_SUCCESS = 1003;
+ private static final int MSG_GET_CORP_INFO_FAILED = 1004;
+
+ // ------------------- 控件声明 -------------------
+ private Button mBtnInitSDK;
+ private Button mBtnGetCorpInfo;
+ private Button mBtnCheckAuth;
+
+ // ------------------- 主线程Handler(处理SDK异步回调) -------------------
+ private Handler mWeWorkHandler;
+
+
+ @Override
+ public Activity getActivity() {
+ return this;
+ }
+
+ @Override
+ public String getTag() {
+ return TAG;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_test_weworkspecsdk);
+
+ // 初始化控件
+ initViews();
+ // 绑定点击事件
+ initEvents();
+ // 初始化Handler(主线程处理回调,更新UI)
+ initHandler();
+ // 初始化SDK(可选:启动时自动初始化,或点击按钮初始化)
+ // initWeWorkSDK();
+ }
+
+ /**
+ * 初始化控件(Java 7 显式绑定)
+ */
+ private void initViews() {
+ mBtnInitSDK = (Button) findViewById(R.id.btn_init_sdk);
+ mBtnGetCorpInfo = (Button) findViewById(R.id.btn_get_corp_info);
+ mBtnCheckAuth = (Button) findViewById(R.id.btn_check_auth);
+ }
+
+ /**
+ * 绑定点击事件(Java 7 匿名内部类)
+ */
+ private void initEvents() {
+ mBtnInitSDK.setOnClickListener(this);
+ mBtnGetCorpInfo.setOnClickListener(this);
+ mBtnCheckAuth.setOnClickListener(this);
+ }
+
+ /**
+ * 初始化主线程Handler(处理SDK异步回调,安全更新UI)
+ */
+ private void initHandler() {
+ mWeWorkHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ super.handleMessage(msg);
+ switch (msg.what) {
+ case MSG_SDK_INIT_SUCCESS:
+ showToast("企业微信SDK初始化成功");
+ LogUtils.d(TAG, "SDK初始化成功");
+ break;
+ case MSG_SDK_INIT_FAILED:
+ String initError = (String) msg.obj;
+ showToast("SDK初始化失败:" + initError);
+ LogUtils.e(TAG, "SDK初始化失败:" + initError);
+ break;
+ case MSG_GET_CORP_INFO_SUCCESS:
+ String corpInfo = (String) msg.obj;
+ showToast("获取企业信息成功");
+ LogUtils.d(TAG, "企业信息:" + corpInfo);
+ break;
+ case MSG_GET_CORP_INFO_FAILED:
+ String corpError = (String) msg.obj;
+ showToast("获取企业信息失败:" + corpError);
+ LogUtils.e(TAG, "获取企业信息失败:" + corpError);
+ break;
+ default:
+ break;
+ }
+ }
+ };
+ }
+
+ // ------------------- 企业微信SDK核心接口调用 -------------------
+
+ /**
+ * 初始化企业微信SDK(异步操作,通过Handler回调结果)
+ */
+ private void initWeWorkSDK() {
+ showToast("开始初始化企业微信SDK...");
+ // 模拟SDK异步初始化(实际项目中替换为企业微信SDK的真实初始化接口)
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ // 真实SDK初始化逻辑示例:
+ // WeWorkSDK.init(TestWeWorkSpecSDK.this, CORP_ID, AGENT_ID, new WeWorkSDKCallback() {
+ // @Override
+ // public void onSuccess() {
+ // mWeWorkHandler.sendEmptyMessage(MSG_SDK_INIT_SUCCESS);
+ // }
+ //
+ // @Override
+ // public void onFailure(String errorMsg) {
+ // Message msg = Message.obtain();
+ // msg.what = MSG_SDK_INIT_FAILED;
+ // msg.obj = errorMsg;
+ // mWeWorkHandler.sendMessage(msg);
+ // }
+ // });
+
+ // 调试模拟:休眠1秒,模拟异步初始化
+ Thread.sleep(1000);
+ // 模拟初始化成功(如需测试失败,替换为发送MSG_SDK_INIT_FAILED)
+ mWeWorkHandler.sendEmptyMessage(MSG_SDK_INIT_SUCCESS);
+ // 模拟初始化失败
+ // Message msg = Message.obtain();
+ // msg.what = MSG_SDK_INIT_FAILED;
+ // msg.obj = "CorpID或AgentID错误";
+ // mWeWorkHandler.sendMessage(msg);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ Message msg = Message.obtain();
+ msg.what = MSG_SDK_INIT_FAILED;
+ msg.obj = "线程中断:" + e.getMessage();
+ mWeWorkHandler.sendMessage(msg);
+ }
+ }
+ }).start();
+ }
+
+ /**
+ * 获取企业基本信息(异步操作,需先初始化SDK)
+ */
+ private void getCorpInfo() {
+ if (!isSDKInitialized()) {
+ showToast("请先初始化SDK");
+ return;
+ }
+ showToast("开始获取企业信息...");
+ // 模拟SDK异步获取企业信息(实际项目中替换为真实接口)
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ // 真实SDK接口示例:
+ // WeWorkSDK.getCorpInfo(APP_SECRET, new CorpInfoCallback() {
+ // @Override
+ // public void onSuccess(CorpInfo info) {
+ // Message msg = Message.obtain();
+ // msg.what = MSG_GET_CORP_INFO_SUCCESS;
+ // msg.obj = "企业名称:" + info.getCorpName() + ",企业ID:" + info.getCorpId();
+ // mWeWorkHandler.sendMessage(msg);
+ // }
+ //
+ // @Override
+ // public void onFailure(String errorMsg) {
+ // Message msg = Message.obtain();
+ // msg.what = MSG_GET_CORP_INFO_FAILED;
+ // msg.obj = errorMsg;
+ // mWeWorkHandler.sendMessage(msg);
+ // }
+ // });
+
+ // 调试模拟:休眠1秒,模拟异步获取
+ Thread.sleep(1000);
+ // 模拟获取成功
+ Message successMsg = Message.obtain();
+ successMsg.what = MSG_GET_CORP_INFO_SUCCESS;
+ successMsg.obj = "企业名称:WinBoLL Studio,企业ID:" + CORP_ID;
+ mWeWorkHandler.sendMessage(successMsg);
+ // 模拟获取失败
+ // Message failMsg = Message.obtain();
+ // failMsg.what = MSG_GET_CORP_INFO_FAILED;
+ // failMsg.obj = "AppSecret错误或权限不足";
+ // mWeWorkHandler.sendMessage(failMsg);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ Message msg = Message.obtain();
+ msg.what = MSG_GET_CORP_INFO_FAILED;
+ msg.obj = "线程中断:" + e.getMessage();
+ mWeWorkHandler.sendMessage(msg);
+ }
+ }
+ }).start();
+ }
+
+ /**
+ * 检查当前用户是否已授权(同步操作,示例)
+ */
+ private void checkAuthStatus() {
+ if (!isSDKInitialized()) {
+ showToast("请先初始化SDK");
+ return;
+ }
+ // 真实SDK接口示例:
+ // boolean isAuthorized = WeWorkSDK.isAuthorized();
+ // 调试模拟:默认返回true
+ boolean isAuthorized = true;
+
+ if (isAuthorized) {
+ showToast("用户已授权");
+ LogUtils.d(TAG, "当前用户已授权企业微信应用");
+ } else {
+ showToast("用户未授权,请先授权");
+ LogUtils.d(TAG, "当前用户未授权企业微信应用");
+ // 真实项目中可调用授权接口:
+ // WeWorkSDK.requestAuth(TestWeWorkSpecSDK.this, new AuthCallback() {
+ // @Override
+ // public void onSuccess(String code) {
+ // showToast("授权成功,code:" + code);
+ // }
+ //
+ // @Override
+ // public void onFailure(String errorMsg) {
+ // showToast("授权失败:" + errorMsg);
+ // }
+ // });
+ }
+ }
+
+ // ------------------- 工具方法 -------------------
+
+ /**
+ * 检查SDK是否已初始化(模拟方法,实际项目中替换为SDK的真实状态检查)
+ */
+ private boolean isSDKInitialized() {
+ // 真实SDK可通过静态方法检查状态:
+ // return WeWorkSDK.isInitialized();
+ // 调试模拟:假设Handler不为空即表示已初始化
+ return mWeWorkHandler != null;
+ }
+
+ /**
+ * 显示Toast提示(Java 7 简化封装)
+ */
+ private void showToast(String msg) {
+ Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
+ }
+
+ // ------------------- 点击事件处理 -------------------
+
+ @Override
+ public void onClick(View v) {
+ int id = v.getId();
+ if (id == R.id.btn_init_sdk) {
+ initWeWorkSDK();
+ } else if (id == R.id.btn_get_corp_info) {
+ getCorpInfo();
+ } else if (id == R.id.btn_check_auth) {
+ checkAuthStatus();
+ }
+ }
+
+ // ------------------- 生命周期管理 -------------------
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // 释放Handler资源,避免内存泄漏
+ if (mWeWorkHandler != null) {
+ mWeWorkHandler.removeCallbacksAndMessages(null);
+ mWeWorkHandler = null;
+ }
+ // 真实SDK需调用销毁方法:
+ // WeWorkSDK.destroy();
+ }
+}
+
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/utils/APPPlusUtils.java b/winboll/src/main/java/cc/winboll/studio/winboll/utils/APPPlusUtils.java
new file mode 100644
index 0000000..2004f93
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/utils/APPPlusUtils.java
@@ -0,0 +1,163 @@
+package cc.winboll.studio.winboll.utils;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.widget.Toast;
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.winboll.App;
+import cc.winboll.studio.winboll.R;
+
+/**
+ * @Author ZhanGSKen&豆包大模型
+ * @Date 2025/11/27 08:56
+ * @Describe APPPlusUtils
+ */
+public class APPPlusUtils {
+ public static final String TAG = "APPPlusUtils";
+
+ // 快捷方式配置(名称+图标,需与实际资源匹配)
+// private static final String PLUS_SHORTCUT_NAME = "位置服务-Laojun";
+// private static final int PLUS_SHORTCUT_ICON = R.mipmap.ic_launcher; // Laojun 图标资源
+
+ /**
+ * 添加Plus组件与图标
+ */
+ public static boolean switchAppLauncherToComponent(Context context, String componentName) {
+ if (context == null) {
+ LogUtils.d(TAG, "切换失败:上下文为空");
+ Toast.makeText(context, context.getString(R.string.app_name) + "图标切换失败", Toast.LENGTH_SHORT).show();
+ return false;
+ }
+
+ PackageManager pm = context.getPackageManager();
+
+ ComponentName plusComponentSwitchTo = new ComponentName(context, componentName);
+ ComponentName plusComponentEN1 = new ComponentName(context, App.COMPONENT_EN1);
+ ComponentName plusComponentCN1 = new ComponentName(context, App.COMPONENT_CN1);
+ ComponentName plusComponentCN2 = new ComponentName(context, App.COMPONENT_CN2);
+
+ try {
+ disableComponent(pm, plusComponentEN1);
+ disableComponent(pm, plusComponentCN1);
+ disableComponent(pm, plusComponentCN2);
+ enableComponent(pm, plusComponentSwitchTo);
+
+ return true;
+
+ } catch (Exception e) {
+ LogUtils.e(TAG, "图标切换失败:" + e.getMessage());
+ Toast.makeText(context, context.getString(R.string.app_name) + "图标切换失败" + e.getMessage(), Toast.LENGTH_SHORT).show();
+ return false;
+ }
+ }
+
+ /**
+ * 创建指定组件的桌面快捷方式(自动去重,兼容 Android 8.0+)
+ * @param component 目标组件(如 LAOJUN_ACTIVITY)
+ * @param name 快捷方式名称
+ * @param iconRes 快捷方式图标资源ID
+ * @return 是否创建成功
+ */
+ private static boolean createComponentShortcut(Context context, ComponentName component, String name, int iconRes) {
+ if (context == null || component == null || name == null || iconRes == 0) {
+ LogUtils.d(TAG, "快捷方式创建失败:参数为空");
+ return false;
+ }
+
+ // Android 8.0+(API 26+):使用 ShortcutManager(系统推荐)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ try {
+ PackageManager pm = context.getPackageManager();
+ android.content.pm.ShortcutManager shortcutManager = context.getSystemService(android.content.pm.ShortcutManager.class);
+ if (shortcutManager == null || !shortcutManager.isRequestPinShortcutSupported()) {
+ LogUtils.d(TAG, "系统不支持创建快捷方式");
+ return false;
+ }
+
+ // 检查是否已存在该组件的快捷方式(去重)
+ for (android.content.pm.ShortcutInfo info : shortcutManager.getPinnedShortcuts()) {
+ if (component.getClassName().equals(info.getIntent().getComponent().getClassName())) {
+ LogUtils.d(TAG, "快捷方式已存在:" + component.getClassName());
+ return true;
+ }
+ }
+
+ // 构建启动目标组件的意图
+ Intent launchIntent = new Intent(Intent.ACTION_MAIN)
+ .setComponent(component)
+ .addCategory(Intent.CATEGORY_LAUNCHER)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+ // 构建快捷方式信息
+ android.content.pm.ShortcutInfo shortcutInfo = new android.content.pm.ShortcutInfo.Builder(context, component.getClassName())
+ .setShortLabel(name)
+ .setLongLabel(name)
+ .setIcon(android.graphics.drawable.Icon.createWithResource(context, iconRes))
+ .setIntent(launchIntent)
+ .build();
+
+ // 请求创建快捷方式(需用户确认)
+ shortcutManager.requestPinShortcut(shortcutInfo, null);
+ return true;
+
+ } catch (Exception e) {
+ LogUtils.d(TAG, "Android O+ 快捷方式创建失败:" + e.getMessage());
+ return false;
+ }
+ } else {
+ // Android 8.0 以下:使用广播(兼容旧机型)
+ try {
+ // 构建启动目标组件的意图
+ Intent launchIntent = new Intent(Intent.ACTION_MAIN)
+ .setComponent(component)
+ .addCategory(Intent.CATEGORY_LAUNCHER)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+ // 构建创建快捷方式的广播意图
+ Intent installIntent = new Intent("com.android.launcher.action.INSTALL_SHORTCUT");
+ installIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, launchIntent);
+ installIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name);
+ installIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
+ Intent.ShortcutIconResource.fromContext(context, iconRes));
+ installIntent.putExtra("duplicate", false); // 禁止重复创建
+
+ context.sendBroadcast(installIntent);
+ return true;
+
+ } catch (Exception e) {
+ LogUtils.d(TAG, "Android O- 快捷方式创建失败:" + e.getMessage());
+ return false;
+ }
+ }
+ }
+
+ /**
+ * 启用组件(带状态检查,避免重复操作)
+ */
+ private static void enableComponent(PackageManager pm, ComponentName component) {
+ if (pm.getComponentEnabledSetting(component) != PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
+ pm.setComponentEnabledSetting(
+ component,
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+ PackageManager.DONT_KILL_APP | PackageManager.SYNCHRONOUS
+ );
+ }
+ }
+
+ /**
+ * 禁用组件(带状态检查,避免重复操作)
+ */
+ private static void disableComponent(PackageManager pm, ComponentName component) {
+ if (pm.getComponentEnabledSetting(component) != PackageManager.COMPONENT_ENABLED_STATE_DISABLED) {
+ pm.setComponentEnabledSetting(
+ component,
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+ PackageManager.DONT_KILL_APP | PackageManager.SYNCHRONOUS
+ );
+ }
+ }
+}
+
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/utils/OkHttpUtil.java b/winboll/src/main/java/cc/winboll/studio/winboll/utils/OkHttpUtil.java
new file mode 100644
index 0000000..4e23dcb
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/utils/OkHttpUtil.java
@@ -0,0 +1,83 @@
+package cc.winboll.studio.winboll.utils;
+
+import android.os.Handler;
+import android.os.Looper;
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * OkHttp网络请求工具类
+ * @Author ZhanGSKen
+ * @Date 2026/01/07
+ */
+public class OkHttpUtil {
+
+ private static OkHttpClient sOkHttpClient;
+ private static Handler sMainHandler = new Handler(Looper.getMainLooper());
+
+ static {
+ sOkHttpClient = new OkHttpClient.Builder()
+ .connectTimeout(10, TimeUnit.SECONDS)
+ .readTimeout(10, TimeUnit.SECONDS)
+ .writeTimeout(10, TimeUnit.SECONDS)
+ .build();
+ }
+
+ /**
+ * GET请求
+ * @param url 请求地址
+ * @param callback 回调
+ */
+ public static void get(String url, final OnResultCallback callback) {
+ Request request = new Request.Builder()
+ .url(url)
+ .get()
+ .build();
+
+ sOkHttpClient.newCall(request).enqueue(new Callback() {
+ @Override
+ public void onFailure(Call call, final IOException e) {
+ sMainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (callback != null) {
+ callback.onFailure(e.getMessage());
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onResponse(Call call, final Response response) throws IOException {
+ final String result = response.body().string();
+ sMainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (callback != null) {
+ if (response.isSuccessful()) {
+ callback.onSuccess(result);
+ } else {
+ callback.onFailure("请求失败:" + response.code());
+ }
+ }
+ }
+ });
+ }
+ });
+ }
+
+ /**
+ * 回调接口
+ */
+ public interface OnResultCallback {
+ void onSuccess(String result);
+ void onFailure(String errorMsg);
+ }
+}
+
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/utils/RSAUtils.java b/winboll/src/main/java/cc/winboll/studio/winboll/utils/RSAUtils.java
new file mode 100644
index 0000000..5258e2b
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/utils/RSAUtils.java
@@ -0,0 +1,222 @@
+package cc.winboll.studio.winboll.utils;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/06/04 13:36
+ * @Describe RSA加密工具
+ */
+import android.content.Context;
+import android.util.Base64;
+import cc.winboll.studio.libappbase.LogUtils;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Objects;
+import javax.crypto.Cipher;
+
+public class RSAUtils {
+ private static final String TAG = "RSAUtils";
+ private static final int KEY_SIZE = 2048;
+ private static final String KEY_ALGORITHM = "RSA";
+ private static final String PUBLIC_KEY_FILE = "public.key";
+ private static final String PRIVATE_KEY_FILE = "private.key";
+ private static final String CIPHER_ALGORITHM = KEY_ALGORITHM + "/ECB/PKCS1Padding"; // 保留原加密方式
+
+ private final String keyPath;
+ private static volatile RSAUtils INSTANCE;
+
+ /**
+ * 构造方法:初始化密钥存储路径(内部存储)
+ */
+ private RSAUtils(Context context) {
+ keyPath = context.getFilesDir() + File.separator + "keys" + File.separator; // 修正路径格式
+ }
+
+ /**
+ * 获取单例实例
+ */
+ public static synchronized RSAUtils getInstance(Context context) {
+ if (INSTANCE == null) {
+ INSTANCE = new RSAUtils(context);
+ }
+ return INSTANCE;
+ }
+
+ /**
+ * 检查密钥文件是否存在
+ */
+ public boolean keysExist() {
+ File publicKeyFile = new File(keyPath + PUBLIC_KEY_FILE);
+ File privateKeyFile = new File(keyPath + PRIVATE_KEY_FILE);
+ return publicKeyFile.exists() && privateKeyFile.exists();
+ }
+
+ /**
+ * 生成密钥对并保存到文件
+ */
+ public void generateAndSaveKeys() throws Exception {
+ LogUtils.d(TAG, "开始生成 RSA 密钥对(2048位)");
+ KeyPairGenerator generator = KeyPairGenerator.getInstance(KEY_ALGORITHM);
+ generator.initialize(KEY_SIZE);
+ KeyPair keyPair = generator.generateKeyPair();
+
+ saveKey(PUBLIC_KEY_FILE, keyPair.getPublic().getEncoded());
+ saveKey(PRIVATE_KEY_FILE, keyPair.getPrivate().getEncoded());
+ LogUtils.d(TAG, "密钥对生成并保存成功");
+ }
+
+ /**
+ * 获取或生成密钥对(线程安全)
+ */
+ public KeyPair getOrGenerateKeys() throws Exception {
+ if (!keysExist()) {
+ synchronized (RSAUtils.class) { // 双重检查锁,避免多线程重复生成
+ if (!keysExist()) {
+ generateAndSaveKeys();
+ }
+ }
+ }
+ return readKeysFromFile();
+ }
+
+ /**
+ * 从文件读取密钥对
+ */
+ private KeyPair readKeysFromFile() throws Exception {
+ LogUtils.d(TAG, "读取密钥对文件");
+ try {
+ byte[] publicKeyBytes = readFileToBytes(keyPath + PUBLIC_KEY_FILE);
+ byte[] privateKeyBytes = readFileToBytes(keyPath + PRIVATE_KEY_FILE);
+
+ X509EncodedKeySpec publicSpec = new X509EncodedKeySpec(publicKeyBytes);
+ PKCS8EncodedKeySpec privateSpec = new PKCS8EncodedKeySpec(privateKeyBytes);
+
+ KeyFactory factory = KeyFactory.getInstance(KEY_ALGORITHM);
+ PublicKey publicKey = factory.generatePublic(publicSpec);
+ PrivateKey privateKey = factory.generatePrivate(privateSpec);
+
+ return new KeyPair(publicKey, privateKey);
+ } catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException e) {
+ LogUtils.e(TAG, "密钥文件读取失败:" + e.getMessage());
+ throw new Exception("密钥文件损坏或格式错误", e);
+ }
+ }
+
+ /**
+ * 保存密钥到文件(通用方法)
+ */
+ private void saveKey(String fileName, byte[] keyBytes) throws IOException {
+ Objects.requireNonNull(keyBytes, "密钥字节数据不可为空");
+ File dir = new File(keyPath);
+ if (!dir.exists() && !dir.mkdirs()) {
+ throw new IOException("创建密钥目录失败:" + keyPath);
+ }
+
+ FileOutputStream fos = null;
+ try {
+ fos = new FileOutputStream(keyPath + fileName);
+ fos.write(keyBytes);
+ } finally {
+ if (fos != null) {
+ try {
+ fos.close();
+ } catch (IOException e) {
+ LogUtils.e(TAG, "关闭文件流失败:" + e.getMessage());
+ }
+ }
+ }
+ }
+
+ /**
+ * 读取文件为字节数组(Java 7 兼容)
+ */
+ private byte[] readFileToBytes(String filePath) throws IOException {
+ File file = new File(filePath);
+ if (!file.exists() || file.isDirectory()) {
+ throw new IOException("文件不存在或为目录:" + filePath);
+ }
+
+ FileInputStream fis = null;
+ try {
+ fis = new FileInputStream(file);
+ byte[] data = new byte[(int) file.length()];
+ int bytesRead = fis.read(data);
+ if (bytesRead != data.length) {
+ throw new IOException("文件读取不完整");
+ }
+ return data;
+ } finally {
+ if (fis != null) {
+ try {
+ fis.close();
+ } catch (IOException e) {
+ LogUtils.e(TAG, "关闭文件流失败:" + e.getMessage());
+ }
+ }
+ }
+ }
+
+ /**
+ * 公钥加密(带参数校验)
+ */
+ public byte[] encryptWithPublicKey(String plainText, PublicKey publicKey) throws Exception {
+ Objects.requireNonNull(plainText, "明文不可为空");
+ Objects.requireNonNull(publicKey, "公钥不可为空");
+
+ Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+ cipher.init(Cipher.ENCRYPT_MODE, publicKey);
+
+ // 检查数据长度是否超过 RSA 限制(2048位密钥最大明文为 214字节,PKCS1Padding)
+ int maxPlainTextSize = cipher.getBlockSize() - 11; // PKCS1Padding 固定填充长度
+ if (plainText.getBytes("UTF-8").length > maxPlainTextSize) {
+ throw new IllegalArgumentException("明文过长,最大支持 " + maxPlainTextSize + " 字节");
+ }
+
+ return cipher.doFinal(plainText.getBytes("UTF-8"));
+ }
+
+ /**
+ * 私钥解密(带参数校验)
+ */
+ public String decryptWithPrivateKey(byte[] encryptedData, PrivateKey privateKey) throws Exception {
+ Objects.requireNonNull(encryptedData, "密文不可为空");
+ Objects.requireNonNull(privateKey, "私钥不可为空");
+
+ Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+ cipher.init(Cipher.DECRYPT_MODE, privateKey);
+ byte[] decryptedBytes = cipher.doFinal(encryptedData);
+ return new String(decryptedBytes, "UTF-8");
+ }
+ /**
+ * 将 HTTP 传输的 Base64 字符串还原为加密字节数组(Java 7 兼容)
+ * @param httpString Base64 字符串(非 null)
+ * @return 加密字节数组
+ * @throws IllegalArgumentException 解码失败时抛出
+ */
+ public byte[] httpStringToEncryptBytes(String httpString) {
+ Objects.requireNonNull(httpString, "HTTP 字符串不可为空");
+
+ // 计算缺失的填充符数量(Java 7 不支持 repeat(),手动拼接)
+ int pad = httpString.length() % 4;
+ StringBuilder paddedString = new StringBuilder(httpString);
+ if (pad != 0) {
+ for (int i = 0; i < pad; i++) {
+ paddedString.append('='); // 补全 '='
+ }
+ }
+
+ // 使用 Base64 解码(Android 原生 Base64 类兼容 Java 7)
+ return Base64.decode(paddedString.toString(), Base64.URL_SAFE);
+ }
+}
+
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/utils/ServiceUtils.java b/winboll/src/main/java/cc/winboll/studio/winboll/utils/ServiceUtils.java
new file mode 100644
index 0000000..74c8065
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/utils/ServiceUtils.java
@@ -0,0 +1,35 @@
+package cc.winboll.studio.winboll.utils;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import java.util.List;
+
+/**
+ * @Author ZhanGSKen&豆包大模型
+ * @Date 2025/11/27 10:25
+ * @Describe ServiceUtils
+ */
+public class ServiceUtils {
+
+ public static final String TAG = ServiceUtils.class.getSimpleName();
+
+ public static boolean isServiceAlive(Context context, String szServiceName) {
+ // 获取Activity管理者对象
+ ActivityManager manager = (ActivityManager) context
+ .getSystemService(Context.ACTIVITY_SERVICE);
+ // 获取正在运行的服务(此处设置最多取1000个)
+ List runningServices = manager
+ .getRunningServices(1000);
+ if (runningServices.size() <= 0) {
+ return false;
+ }
+ // 遍历,若存在名字和传入的serviceName的一致则说明存在
+ for (ActivityManager.RunningServiceInfo runningServiceInfo : runningServices) {
+ if (runningServiceInfo.service.getClassName().equals(szServiceName)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/utils/SpecUtil.java b/winboll/src/main/java/cc/winboll/studio/winboll/utils/SpecUtil.java
new file mode 100644
index 0000000..279f286
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/utils/SpecUtil.java
@@ -0,0 +1,26 @@
+package cc.winboll.studio.winboll.utils;
+
+import cc.winboll.studio.libappbase.LogUtils;
+
+/**
+ * 日志工具类(适配项目规范)
+ * @Author ZhanGSKen
+ * @Date 2026/01/07
+ */
+public class SpecUtil {
+
+ private static final boolean isDebug = true;
+
+ public static void WWSpecLogInfo(String tag, String msg) {
+ if (isDebug) {
+ LogUtils.i(tag, msg);
+ }
+ }
+
+ public static void WWSpecLogError(String tag, String msg) {
+ if (isDebug) {
+ LogUtils.e(tag, msg);
+ }
+ }
+}
+
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/utils/TermuxUtils.java b/winboll/src/main/java/cc/winboll/studio/winboll/utils/TermuxUtils.java
new file mode 100644
index 0000000..959169a
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/utils/TermuxUtils.java
@@ -0,0 +1,33 @@
+package cc.winboll.studio.winboll.utils;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/06/08 09:05
+ * @Describe Termux 应用操作工具集
+ */
+import android.content.Intent;
+
+public abstract class TermuxUtils {
+
+ public static final String TAG = "TermuxUtils";
+
+ private void runTermuxCommand(String command) {
+ // 1. 创建 Intent,指定 Termux 的 RunCommandService
+ /*Intent intent = new Intent("com.termux.RUN_COMMAND");
+ intent.setPackage("com.termux"); // Termux 应用的包名
+
+ // 2. 传递命令参数(必填)
+ //intent.putExtra("command", command);
+ intent.putExtra("cd ~/WinBoLL&&echo 'WinBoLL cmd exec at (data", command);
+
+ // 3. 可选:设置工作目录(默认为 Termux 的 home 目录)
+ intent.putExtra("dir", "/data/data/com.termux/files/home/WinBoLL");
+
+ // 4. 发送 Intent(需处理可能的安全异常或 ActivityNotFoundException)
+ try {
+ getApplicationContext().startService(intent);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }*/
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/utils/WxPayApi.java b/winboll/src/main/java/cc/winboll/studio/winboll/utils/WxPayApi.java
new file mode 100644
index 0000000..dd0b508
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/utils/WxPayApi.java
@@ -0,0 +1,100 @@
+package cc.winboll.studio.winboll.utils;
+
+import cc.winboll.studio.winboll.WxPayConfig;
+import com.alibaba.fastjson.JSONObject;
+
+/**
+ * 微信支付服务端接口封装
+ * @Author ZhanGSKen
+ * @Date 2026/01/07
+ */
+public class WxPayApi {
+
+ /**
+ * 统一下单(生成二维码)
+ * @param callback 回调
+ */
+ public static void createOrder(final OnCreateOrderCallback callback) {
+ // 拼接请求参数(服务端测试接口需支持GET传参,若为POST需修改为表单/JSON)
+ String url = WxPayConfig.CREATE_ORDER_URL +
+ "?body=" + WxPayConfig.ORDER_BODY +
+ "&totalFee=" + WxPayConfig.TOTAL_FEE +
+ "&tradeType=" + WxPayConfig.TRADE_TYPE;
+
+ OkHttpUtil.get(url, new OkHttpUtil.OnResultCallback() {
+ @Override
+ public void onSuccess(String result) {
+ try {
+ JSONObject jsonObject = JSONObject.parseObject(result);
+ String outTradeNo = jsonObject.getString("out_trade_no");
+ String codeUrl = jsonObject.getString("code_url");
+ if (callback != null) {
+ callback.onSuccess(outTradeNo, codeUrl);
+ }
+ } catch (Exception e) {
+ if (callback != null) {
+ callback.onFailure("解析统一下单结果失败:" + e.getMessage());
+ }
+ }
+ }
+
+ @Override
+ public void onFailure(String errorMsg) {
+ if (callback != null) {
+ callback.onFailure("统一下单请求失败:" + errorMsg);
+ }
+ }
+ });
+ }
+
+ /**
+ * 订单查询
+ * @param outTradeNo 商户订单号
+ * @param callback 回调
+ */
+ public static void queryOrder(String outTradeNo, final OnQueryOrderCallback callback) {
+ String url = WxPayConfig.QUERY_ORDER_URL + "?outTradeNo=" + outTradeNo;
+
+ OkHttpUtil.get(url, new OkHttpUtil.OnResultCallback() {
+ @Override
+ public void onSuccess(String result) {
+ try {
+ JSONObject jsonObject = JSONObject.parseObject(result);
+ String tradeState = jsonObject.getString("trade_state");
+ boolean isSuccess = "SUCCESS".equals(tradeState);
+ if (callback != null) {
+ callback.onSuccess(isSuccess, tradeState);
+ }
+ } catch (Exception e) {
+ if (callback != null) {
+ callback.onFailure("解析订单查询结果失败:" + e.getMessage());
+ }
+ }
+ }
+
+ @Override
+ public void onFailure(String errorMsg) {
+ if (callback != null) {
+ callback.onFailure("订单查询请求失败:" + errorMsg);
+ }
+ }
+ });
+ }
+
+ /**
+ * 统一下单回调接口
+ */
+ public interface OnCreateOrderCallback {
+ void onSuccess(String outTradeNo, String codeUrl);
+ void onFailure(String errorMsg);
+ }
+
+ /**
+ * 订单查询回调接口
+ */
+ public interface OnQueryOrderCallback {
+ void onSuccess(boolean isPaySuccess, String tradeState);
+ void onFailure(String errorMsg);
+ }
+}
+
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/utils/YunUtils.java b/winboll/src/main/java/cc/winboll/studio/winboll/utils/YunUtils.java
new file mode 100644
index 0000000..9c67b01
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/utils/YunUtils.java
@@ -0,0 +1,282 @@
+package cc.winboll.studio.winboll.utils;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/06/04 17:21
+ * @Describe 应用登录与接口工具
+ */
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.winboll.models.ResponseData;
+import cc.winboll.studio.winboll.models.UserInfoModel;
+import com.google.gson.Gson;
+import java.io.File;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.util.concurrent.TimeUnit;
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+
+public class YunUtils {
+ public static final String TAG = "YunUtils";
+ // 私有静态实例,类加载时创建
+ private static volatile YunUtils INSTANCE;
+ Context mContext;
+ UserInfoModel mUserInfoModel;
+ String token = "";
+ String mDataFolderPath = "";
+ String mUserInfoModelPath = "";
+
+ private static final int CONNECT_TIMEOUT = 15; // 连接超时时间(秒)
+ private static final int READ_TIMEOUT = 20; // 读取超时时间(秒)
+ private static volatile YunUtils instance;
+ private OkHttpClient okHttpClient;
+ private Handler mainHandler; // 主线程 Handler
+
+ // 私有构造方法,防止外部实例化
+ private YunUtils(Context context) {
+ LogUtils.d(TAG, "YunUtils");
+ mContext = context;
+ mDataFolderPath = mContext.getExternalFilesDir(TAG).toString();
+ File fTest = new File(mDataFolderPath);
+ if (!fTest.exists()) {
+ fTest.mkdirs();
+ }
+ mUserInfoModelPath = mDataFolderPath + File.separator + "UserInfoModel.rsajson";
+
+ okHttpClient = new OkHttpClient.Builder()
+ .connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)
+ .readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
+ .build();
+ mainHandler = new Handler(Looper.getMainLooper()); // 获取主线程 Looper
+ }
+
+ // 公共静态方法,返回唯一实例
+ public static synchronized YunUtils getInstance(Context context) {
+ LogUtils.d(TAG, "getInstance");
+ if (INSTANCE == null) {
+ INSTANCE = new YunUtils(context);
+ }
+ return INSTANCE;
+ }
+
+ public void checkLoginStatus() {
+ String token = getLocalToken();
+ LogUtils.d(TAG, String.format("checkLoginStatus token is %s", token));
+ }
+
+ String getLocalToken() {
+ UserInfoModel userInfoModel = loadUserInfoModel();
+ return (userInfoModel == null) ?"": userInfoModel.getToken();
+ }
+
+ public void login(String host, UserInfoModel userInfoModel) {
+ LogUtils.d(TAG, "login");
+
+ // 发送 POST 请求
+ String apiUrl = host + "/login/index.php";
+ // 序列化对象为JSON
+ Gson gson = new Gson();
+ String jsonData = gson.toJson(userInfoModel); // 自动生成标准JSON
+ //String jsonData = userInfoModel.toString();
+ LogUtils.d(TAG, "要发送的数据 : " + jsonData);
+
+ sendPostRequest(apiUrl, jsonData, new OnResponseListener() {
+ // 成功回调(主线程)
+ @Override
+ public void onSuccess(String responseBody) {
+ LogUtils.d(TAG, "onSuccess");
+ LogUtils.d(TAG, String.format("responseBody %s", responseBody));
+ Gson gson = new Gson();
+ ResponseData result = gson.fromJson(responseBody, ResponseData.class); // 转为 Result 实例
+ if(result.getStatus().equals(ResponseData.STATUS_SUCCESS)) {
+
+ UserInfoModel userInfoModel = result.getData();
+ if (userInfoModel != null) {
+ LogUtils.d(TAG, "收到网站 UserInfoModel");
+ String token = userInfoModel.getToken();
+ saveLocalToken(token);
+ checkLoginStatus();
+ }
+
+ } else if(result.getStatus().equals(ResponseData.STATUS_ERROR)) {
+ try {
+ String decodedMessage = URLDecoder.decode(result.getMessage(), "UTF-8");
+ LogUtils.d(TAG, "服务器返回信息: " + decodedMessage);
+ } catch (UnsupportedEncodingException e) {
+ LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
+ }
+ }
+ }
+
+ // 失败回调(主线程)
+ @Override
+ public void onFailure(String errorMsg) {
+ LogUtils.d(TAG, errorMsg);
+ // 处理错误
+ }
+ });
+ }
+
+ public void saveLocalToken(String token) {
+ UserInfoModel userInfoModel = new UserInfoModel();
+ userInfoModel.setToken(token);
+ saveUserInfoModel(userInfoModel);
+ }
+
+ UserInfoModel loadUserInfoModel() {
+// LogUtils.d(TAG, "loadUserInfoModel");
+// if (new File(mUserInfoModelPath).exists()) {
+// try {
+// // 加载加密后的模型数据
+// byte[] encryptedData = FileUtils.readByteArrayFromFile(mUserInfoModelPath);
+// // 加载 RSA 工具
+// RSAUtils utils = RSAUtils.getInstance(mContext);
+// KeyPair keyPair = utils.getOrGenerateKeys();
+// //PublicKey publicKey = keyPair.getPublic();
+// PrivateKey privateKey = keyPair.getPrivate();
+// // 私钥解密模型数据
+// String szInfo = utils.decryptWithPrivateKey(encryptedData, keyPair.getPrivate());
+// LogUtils.d(TAG, String.format("szInfo %s", szInfo));
+// mUserInfoModel = UserInfoModel.parseStringToBean(szInfo, UserInfoModel.class);
+// if (mUserInfoModel == null) {
+// LogUtils.d(TAG, "模型数据解析为空数据。");
+// }
+// LogUtils.d(TAG, "UserInfoModel 解密加载结束。");
+// } catch (Exception e) {
+// LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
+// }
+// } else {
+// LogUtils.d(TAG, "云服务登录信息不存在。");
+// mUserInfoModel = null;
+// }
+// return mUserInfoModel;
+ return null;
+ }
+
+ void saveUserInfoModel(UserInfoModel userInfoModel) {
+// LogUtils.d(TAG, "saveUserInfoModel");
+// try {
+// String szInfo = userInfoModel.toString();
+// LogUtils.d(TAG, "原始数据: " + szInfo);
+//
+// RSAUtils utils = RSAUtils.getInstance(mContext);
+// KeyPair keyPair = utils.getOrGenerateKeys();
+// PublicKey publicKey = keyPair.getPublic();
+//
+// // 公钥加密(传入字节数组,避免中间字符串转换)
+// byte[] encryptedData = utils.encryptWithPublicKey(szInfo, publicKey);
+//
+// // 保存加密字节数组到文件(直接操作字节,无需转字符串)
+// FileUtils.writeByteArrayToFile(encryptedData, mUserInfoModelPath);
+// LogUtils.d(TAG, "加密数据已保存");
+//
+// // 测试解密(仅调试用)
+// String szInfo2 = utils.decryptWithPrivateKey(encryptedData, keyPair.getPrivate());
+// LogUtils.d(TAG, "解密结果: " + szInfo2);
+//
+// mUserInfoModel = UserInfoModel.parseStringToBean(szInfo2, UserInfoModel.class);
+// if (mUserInfoModel == null) {
+// LogUtils.d(TAG, "模型解析失败");
+// }
+// } catch (Exception e) {
+// LogUtils.d(TAG, "加密/解密失败: " + e.getMessage());
+// }
+ }
+
+ // 发送 POST 请求(JSON 数据)
+ public void sendPostRequest(String url, String data, OnResponseListener listener) {
+ RequestBody requestBody = RequestBody.create(
+ MediaType.parse("application/json; charset=utf-8"), // 关键头信息
+ data.getBytes(StandardCharsets.UTF_8)
+ );
+
+ Request request = new Request.Builder()
+ .url(url)
+ .post(requestBody)
+ .addHeader("Content-Type", "application/json") // 显式添加头
+ .build();
+
+ executeRequest(request, listener);
+ }
+
+ // 发送 GET 请求
+ public void sendGetRequest(String url, OnResponseListener listener) {
+ Request request = new Request.Builder()
+ .url(url)
+ .get()
+ .build();
+ executeRequest(request, listener);
+ }
+
+ // 执行请求(子线程处理)
+ private void executeRequest(final Request request, final OnResponseListener listener) {
+ okHttpClient.newCall(request).enqueue(new Callback() {
+ // 响应成功(子线程)
+ @Override
+ public void onResponse(Call call, Response response) throws IOException {
+ try {
+ if (!response.isSuccessful()) {
+ postFailure(listener, "响应码错误:" + response.code());
+ return;
+ }
+ String responseBody = response.body().string();
+ postSuccess(listener, responseBody);
+ } catch (Exception e) {
+ postFailure(listener, "解析失败:" + e.getMessage());
+ }
+ }
+
+ // 响应失败(子线程)
+ @Override
+ public void onFailure(Call call, IOException e) {
+ postFailure(listener, "网络失败:" + e.getMessage());
+ }
+
+ // 主线程回调(使用 Handler)
+ private void postSuccess(final OnResponseListener listener, final String msg) {
+ mainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onSuccess(msg);
+ }
+ });
+ }
+
+ private void postFailure(final OnResponseListener listener, final String msg) {
+ mainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onFailure(msg);
+ }
+ });
+ }
+ });
+ }
+
+ public interface OnResponseListener {
+ /**
+ * 成功响应(主线程回调)
+ * @param responseBody 响应体字符串
+ */
+ void onSuccess(String responseBody);
+
+ /**
+ * 失败回调(包含错误信息)
+ * @param errorMsg 错误描述
+ */
+ void onFailure(String errorMsg);
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/utils/ZXingUtils.java b/winboll/src/main/java/cc/winboll/studio/winboll/utils/ZXingUtils.java
new file mode 100644
index 0000000..389298d
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/utils/ZXingUtils.java
@@ -0,0 +1,75 @@
+package cc.winboll.studio.winboll.utils;
+
+import android.graphics.Bitmap;
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.EncodeHintType;
+import com.google.zxing.WriterException;
+import com.google.zxing.common.BitMatrix;
+import com.google.zxing.qrcode.QRCodeWriter;
+import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
+import com.journeyapps.barcodescanner.BarcodeEncoder;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * ZXing二维码生成工具类
+ * 依赖:com.google.zxing:core:3.4.1 + com.journeyapps:zxing-android-embedded:3.6.0
+ * @Author ZhanGSKen
+ * @Date 2026/01/07
+ */
+public class ZXingUtils {
+
+ /**
+ * 生成二维码Bitmap(核心方法,使用journeyapps工具类)
+ * @param content 内容(如微信支付的code_url)
+ * @param width 二维码宽度(px)
+ * @param height 二维码高度(px)
+ * @return 二维码Bitmap,失败返回null
+ */
+ public static Bitmap createQRCodeBitmap(String content, int width, int height) {
+ // 1. 入参合法性校验
+ if (content == null || content.trim().isEmpty()) {
+ return null;
+ }
+ if (width <= 0 || height <= 0) {
+ return null;
+ }
+
+ // 2. 配置二维码参数
+ Map hints = new HashMap<>();
+ hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); // 字符编码
+ hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); // 高容错级别(H级可容忍30%遮挡)
+ hints.put(EncodeHintType.MARGIN, 1); // 边距(值越小,二维码越紧凑,建议1-4)
+
+ try {
+ // 3. 生成BitMatrix(二维码矩阵)
+ QRCodeWriter qrCodeWriter = new QRCodeWriter();
+ BitMatrix bitMatrix = qrCodeWriter.encode(
+ content,
+ BarcodeFormat.QR_CODE,
+ width,
+ height,
+ hints
+ );
+
+ // 4. 转换BitMatrix为Bitmap(关键:使用journeyapps的BarcodeEncoder)
+ BarcodeEncoder barcodeEncoder = new BarcodeEncoder();
+ return barcodeEncoder.createBitmap(bitMatrix);
+
+ } catch (WriterException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ /**
+ * 重载方法:生成正方形二维码(宽度=高度)
+ * @param content 内容
+ * @param size 二维码边长(px)
+ * @return 二维码Bitmap
+ */
+ public static Bitmap createQRCodeBitmap(String content, int size) {
+ return createQRCodeBitmap(content, size, size);
+ }
+}
+
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/views/TermuxButton.java b/winboll/src/main/java/cc/winboll/studio/winboll/views/TermuxButton.java
new file mode 100644
index 0000000..719ffd1
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/views/TermuxButton.java
@@ -0,0 +1,264 @@
+package cc.winboll.studio.winboll.views;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.Button;
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.winboll.models.TermuxButtonModel;
+
+/**
+ * 自定义Termux功能按钮控件
+ * 绑定TermuxButtonModel实体数据,拦截点击事件做确认弹窗逻辑判断
+ * isCommitted为true直接执行点击事件,为false弹出确认对话框二次确认
+ * @Author 豆包&ZhanGSKen
+ * @CreateTime 2026/04/30 10:57:00
+ * @EditTime 2026/04/30 13:52:15
+ */
+public class TermuxButton extends Button {
+
+ public static final String TAG = "TermuxButton";
+
+ /** 绑定按钮对应数据实体 */
+ private TermuxButtonModel buttonModel;
+ /** 保存外部设置的原始点击监听 */
+ private OnClickListener originClickListener;
+
+ //==================== 构造方法 ====================
+ /**
+ * 代码动态创建控件构造
+ * @param context 上下文
+ */
+ public TermuxButton(Context context) {
+ super(context);
+ LogUtils.d(TAG, "TermuxButton 无参构造执行,上下文:" + context);
+ initView(null, null);
+ }
+
+ /**
+ * XML布局引用控件基础构造
+ * @param context 上下文
+ * @param attrs XML属性集
+ */
+ public TermuxButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ LogUtils.d(TAG, "TermuxButton XML构造执行");
+ initView(attrs, null);
+ }
+
+ /**
+ * XML布局带自定义属性构造
+ * @param context 上下文
+ * @param attrs XML属性集
+ * @param defStyleAttr 默认样式属性
+ */
+ public TermuxButton(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ LogUtils.d(TAG, "TermuxButton 带样式属性构造执行");
+ initView(attrs, null);
+ }
+
+ /**
+ * 高版本Android完整全参构造
+ * @param context 上下文
+ * @param attrs XML属性集
+ * @param defStyleAttr 默认样式属性
+ * @param defStyleRes 默认样式资源
+ */
+ public TermuxButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ LogUtils.d(TAG, "TermuxButton 全参构造执行");
+ initView(attrs, null);
+ }
+
+ /**
+ * 直接传入Model初始化控件构造
+ * @param context 上下文
+ * @param model 按钮数据实体
+ */
+ public TermuxButton(Context context, TermuxButtonModel model) {
+ super(context);
+ LogUtils.d(TAG, "TermuxButton Model入参构造执行");
+ initView(null, model);
+ }
+
+ //==================== 核心初始化 ====================
+ /**
+ * 控件统一初始化方法
+ * @param attrs XML属性集合
+ * @param model 绑定数据实体
+ */
+ private void initView(AttributeSet attrs, TermuxButtonModel model) {
+ this.buttonModel = model;
+
+ // 按钮基础默认配置
+ setClickable(true);
+ setFocusable(true);
+
+ // 解析XML布局自定义属性
+ if (attrs != null) {
+ parseXmlCustomAttr(attrs);
+ }
+
+ // 同步Model内按钮名称到控件展示文本
+ refreshButtonText();
+ // 绑定自定义拦截点击事件
+ setCustomClickEvent();
+ }
+
+ /**
+ * 解析XML布局属性,读取原生android:text与自定义属性赋值到Model
+ * @param attrs XML属性集
+ */
+ private void parseXmlCustomAttr(AttributeSet attrs) {
+ if (buttonModel == null) {
+ buttonModel = new TermuxButtonModel();
+ LogUtils.d(TAG, "自动初始化空的TermuxButtonModel实体");
+ }
+
+ // 读取原生android:text作为按钮名称
+ String androidText = attrs.getAttributeValue("http://schemas.android.com/apk/res/android", "text");
+ // 读取自定义扩展属性
+ String exeCommand = attrs.getAttributeValue("http://schemas.android.com/apk/res-auto", "exeCommand");
+ String workDir = attrs.getAttributeValue("http://schemas.android.com/apk/res-auto", "workDir");
+ String isCommittedStr = attrs.getAttributeValue("http://schemas.android.com/apk/res-auto", "isCommitted");
+ String commitTitle = attrs.getAttributeValue("http://schemas.android.com/apk/res-auto", "commitTitle");
+ String commitInfo = attrs.getAttributeValue("http://schemas.android.com/apk/res-auto", "commitInfo");
+
+ // 属性赋值绑定
+ if (androidText != null) {
+ buttonModel.setButtonName(androidText);
+ }
+ if (exeCommand != null) {
+ buttonModel.setExeCommand(exeCommand);
+ }
+ if (workDir != null) {
+ buttonModel.setWorkDir(workDir);
+ }
+ if (isCommittedStr != null) {
+ buttonModel.setCommitted(Boolean.parseBoolean(isCommittedStr));
+ }
+ if (commitTitle != null) {
+ buttonModel.setCommitTitle(commitTitle);
+ }
+ if (commitInfo != null) {
+ buttonModel.setCommitInfo(commitInfo);
+ }
+
+ LogUtils.d(TAG, "XML属性解析完成,按钮名称:" + androidText);
+ }
+
+ /**
+ * 同步Model中buttonName,更新按钮展示文字
+ */
+ private void refreshButtonText() {
+ if (buttonModel != null) {
+ setText(buttonModel.getButtonName());
+ }
+ }
+
+ //==================== 点击事件相关 ====================
+ /**
+ * 重写点击监听设置,保存外部原始点击事件
+ * @param l 外部传入点击监听
+ */
+ @Override
+ public void setOnClickListener(OnClickListener l) {
+ this.originClickListener = l;
+ LogUtils.d(TAG, "保存外部原始按钮点击监听");
+ }
+
+ /**
+ * 自定义拦截按钮点击逻辑
+ * isCommitted=true 直接执行原始点击事件
+ * isCommitted=false 弹出确认二次弹窗
+ */
+ private void setCustomClickEvent() {
+ super.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (buttonModel == null) {
+ LogUtils.d(TAG, "无绑定Model,直接执行原始点击事件");
+ if (originClickListener != null) {
+ originClickListener.onClick(view);
+ }
+ return;
+ }
+
+ boolean commitState = buttonModel.isCommitted();
+ LogUtils.d(TAG, "按钮点击触发,isCommitted状态:" + commitState);
+ if (commitState) {
+ // 无需确认,直接执行原有点击任务
+ if (originClickListener != null) {
+ originClickListener.onClick(view);
+ }
+ } else {
+ // 需要二次确认,弹出提示对话框
+ showCommitDialog();
+ }
+ }
+ });
+ }
+
+ /**
+ * 弹出操作确认对话框
+ * 标题:commitTitle 内容:commitInfo
+ * 取消:关闭弹窗无操作 确定:执行原始点击事件
+ */
+ private void showCommitDialog() {
+ Context context = getContext();
+ String dialogTitle = buttonModel.getCommitTitle();
+ String dialogMsg = buttonModel.getCommitInfo();
+
+ // 空值默认兜底处理
+ if (dialogTitle == null || "".equals(dialogTitle)) {
+ dialogTitle = "温馨提示";
+ }
+ if (dialogMsg == null || "".equals(dialogMsg)) {
+ dialogMsg = "确定要执行该操作吗?";
+ }
+
+ LogUtils.d(TAG, "弹出确认对话框,标题:" + dialogTitle);
+ new AlertDialog.Builder(context)
+ .setTitle(dialogTitle)
+ .setMessage(dialogMsg)
+ .setNegativeButton("取消", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ LogUtils.d(TAG, "对话框点击取消,终止操作");
+ }
+ })
+ .setPositiveButton("确定", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ LogUtils.d(TAG, "对话框点击确定,继续执行操作");
+ if (originClickListener != null) {
+ originClickListener.onClick(TermuxButton.this);
+ }
+ }
+ })
+ .setCancelable(false)
+ .show();
+ }
+
+ //==================== Getter & Setter ====================
+ public TermuxButtonModel getButtonModel() {
+ return buttonModel;
+ }
+
+ /**
+ * 设置绑定按钮数据实体,自动刷新按钮展示文字
+ * @param buttonModel 数据实体类
+ */
+ public void setButtonModel(TermuxButtonModel buttonModel) {
+ this.buttonModel = buttonModel;
+ LogUtils.d(TAG, "外部设置ButtonModel,自动刷新按钮文本");
+ refreshButtonText();
+ }
+
+}
+
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/views/WinBoLLView.java b/winboll/src/main/java/cc/winboll/studio/winboll/views/WinBoLLView.java
new file mode 100644
index 0000000..d4dc6f5
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/views/WinBoLLView.java
@@ -0,0 +1,232 @@
+package cc.winboll.studio.winboll.views;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.webkit.WebChromeClient;
+import android.webkit.WebResourceError;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.ProgressBar;
+
+/**
+ * @Author ZhanGSKen&豆包大模型
+ * @Date 2025/11/27 11:05
+ * @Describe 自定义WebView控件(WinBoLLView)
+ * 集成进度监听、页面加载控制、安全配置等核心能力
+ */
+public class WinBoLLView extends WebView {
+ public static final String TAG = "WinBoLLView";
+
+ private ProgressBar mProgressBar; // 页面加载进度条(可选,增强用户体验)
+ private OnPageStatusListener mStatusListener; // 页面状态监听回调
+ private boolean mIsLoading = false; // 自定义加载状态标记(替代系统isLoading())
+
+ // 构造方法(兼容代码创建和XML布局引用)
+ public WinBoLLView(Context context) {
+ super(context);
+ initWebViewSettings();
+ initWebViewClient();
+ }
+
+ public WinBoLLView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initWebViewSettings();
+ initWebViewClient();
+ }
+
+ public WinBoLLView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initWebViewSettings();
+ initWebViewClient();
+ }
+
+ /**
+ * 初始化WebView基础配置(安全+性能+兼容性)
+ */
+ private void initWebViewSettings() {
+ WebSettings settings = getSettings();
+
+ // 基础功能配置
+ settings.setJavaScriptEnabled(true); // 启用JS(根据需求决定是否开启)
+ settings.setSupportZoom(true); // 支持缩放
+ settings.setBuiltInZoomControls(true); // 显示缩放控件
+ settings.setDisplayZoomControls(false); // 隐藏系统缩放控件(优化UI)
+ settings.setLoadWithOverviewMode(true); // 自适应屏幕
+ settings.setUseWideViewPort(true); // 支持宽视角
+
+ // 缓存配置(提升加载速度)
+ settings.setCacheMode(WebSettings.LOAD_DEFAULT);
+ settings.setDomStorageEnabled(true); // 启用DOM存储
+ settings.setDatabaseEnabled(true); // 启用数据库存储
+
+ // 安全配置(防止XSS和恶意跳转)
+ settings.setJavaScriptCanOpenWindowsAutomatically(false); // 禁止JS自动打开窗口
+ setBackgroundColor(0x00000000); // 透明背景(避免白屏闪烁)
+
+ // 适配HTTPS和HTTP混合内容(Android 5.0+)
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
+ settings.setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE);
+ }
+ }
+
+ /**
+ * 初始化WebViewClient和ChromeClient(控制页面加载和进度)
+ */
+ private void initWebViewClient() {
+ // 控制页面跳转(不打开系统浏览器)
+ setWebViewClient(new WebViewClient() {
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
+ // 拦截URL加载,使用当前WebView打开
+ view.loadUrl(request.getUrl().toString());
+ return true;
+ }
+
+ @Override
+ public void onPageStarted(WebView view, String url, android.graphics.Bitmap favicon) {
+ super.onPageStarted(view, url, favicon);
+ // 页面开始加载:更新自定义加载状态标记
+ mIsLoading = true;
+ // 更新进度条+回调状态
+ if (mProgressBar != null) mProgressBar.setVisibility(VISIBLE);
+ if (mStatusListener != null) mStatusListener.onPageStarted(url);
+ }
+
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ super.onPageFinished(view, url);
+ // 页面加载完成:更新自定义加载状态标记
+ mIsLoading = false;
+ // 隐藏进度条+回调状态
+ if (mProgressBar != null) mProgressBar.setVisibility(GONE);
+ if (mStatusListener != null) mStatusListener.onPageFinished(url);
+ }
+
+ @Override
+ public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
+ super.onReceivedError(view, request, error);
+ // 页面加载错误:更新自定义加载状态标记
+ mIsLoading = false;
+ // 回调错误信息
+ if (mStatusListener != null) {
+ String errorMsg = error.getDescription().toString();
+ mStatusListener.onPageError(errorMsg);
+ }
+ }
+ });
+
+ // 监听页面加载进度
+ setWebChromeClient(new WebChromeClient() {
+ @Override
+ public void onProgressChanged(WebView view, int newProgress) {
+ super.onProgressChanged(view, newProgress);
+ // 更新进度条进度
+ if (mProgressBar != null) mProgressBar.setProgress(newProgress);
+ }
+ });
+ }
+
+ // ------------------- 对外暴露的控制方法(供Fragment调用) -------------------
+ /**
+ * 加载URL(增加空值校验+优先HTTPS协议)
+ */
+ public void loadUrlSafe(String url) {
+ if (url == null || url.trim().isEmpty()) {
+ if (mStatusListener != null) mStatusListener.onPageError("URL不能为空");
+ return;
+ }
+ // 协议补全:优先HTTPS,兼容HTTP(解决ERR_CLEARTEXT_NOT_PERMITTED)
+ if (!url.startsWith("http://") && !url.startsWith("https://")) {
+ url = "https://" + url; // 改为优先HTTPS,而非HTTP
+ }
+ super.loadUrl(url);
+ }
+
+ /**
+ * 刷新当前页面(使用自定义mIsLoading判断加载状态)
+ */
+ public void refreshPage() {
+ if (mIsLoading) { // 替换原isLoading(),使用自定义标记
+ stopLoading(); // 若正在加载,先停止再刷新
+ }
+ reload();
+ }
+
+ /**
+ * 停止页面加载(使用自定义mIsLoading判断加载状态)
+ */
+ public void stopPageLoad() {
+ if (mIsLoading) { // 替换原isLoading(),使用自定义标记
+ stopLoading();
+ mIsLoading = false; // 停止后更新状态标记
+ }
+ }
+
+ /**
+ * 前进(判断是否有前进历史)
+ */
+ public boolean goForwardSafe() {
+ if (canGoForward()) {
+ goForward();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * 后退(判断是否有后退历史)
+ */
+ public boolean goBackSafe() {
+ if (canGoBack()) {
+ goBack();
+ return true;
+ }
+ return false;
+ }
+
+ // ------------------- 辅助功能:进度条和状态监听 -------------------
+ /**
+ * 设置进度条(绑定Fragment中的进度条控件)
+ */
+ public void setProgressBar(ProgressBar progressBar) {
+ this.mProgressBar = progressBar;
+ if (mProgressBar != null) {
+ mProgressBar.setMax(100);
+ mProgressBar.setVisibility(GONE);
+ }
+ }
+
+ /**
+ * 设置页面状态监听(供Fragment接收加载状态)
+ */
+ public void setOnPageStatusListener(OnPageStatusListener listener) {
+ this.mStatusListener = listener;
+ }
+
+ /**
+ * 页面状态监听接口
+ */
+ public interface OnPageStatusListener {
+ void onPageStarted(String url); // 页面开始加载
+ void onPageFinished(String url); // 页面加载完成
+ void onPageError(String errorMsg); // 页面加载错误
+ }
+
+ // ------------------- 资源释放(防止内存泄漏) -------------------
+ /**
+ * 销毁WebView(必须在Fragment销毁时调用)
+ */
+ public void destroyWebView() {
+ // 停止加载并清空历史
+ stopLoading();
+ mIsLoading = false; // 销毁时重置加载状态
+ clearHistory();
+ // 移除所有WebViewClient(避免内存泄漏)
+ setWebViewClient(null);
+ setWebChromeClient(null);
+ // 销毁WebView
+ destroy();
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/widgets/APPNewsWidget.java b/winboll/src/main/java/cc/winboll/studio/winboll/widgets/APPNewsWidget.java
new file mode 100644
index 0000000..4796898
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/widgets/APPNewsWidget.java
@@ -0,0 +1,185 @@
+package cc.winboll.studio.winboll.widgets;
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/02/15 14:41:25
+ * @Describe TimeWidget
+ */
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.widget.RemoteViews;
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.winboll.R;
+import cc.winboll.studio.winboll.WinBoLL;
+import cc.winboll.studio.winboll.models.WinBoLLModel;
+import cc.winboll.studio.winboll.models.WinBoLLNewsBean;
+import cc.winboll.studio.winboll.receivers.APPNewsWidgetClickListener;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+
+public class APPNewsWidget extends AppWidgetProvider {
+
+ public static final String TAG = "APPNewsWidget";
+
+ public static final String ACTION_WAKEUP_SERVICE = APPNewsWidget.class.getName() + ".ACTION_WAKEUP_SERVICE";
+ public static final String ACTION_RELOAD_REPORT = APPNewsWidget.class.getName() + ".ACTION_RELOAD_REPORT";
+
+
+ volatile static ArrayList _WinBoLLNewsBeanList;
+ final static int _MAX_PAGES = 10;
+ final static int _OnePageLinesCount = 5;
+ volatile static int _CurrentPageIndex = 0;
+
+ @Override
+ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
+ initWinBoLLNewsBeanList(context);
+ for (int appWidgetId : appWidgetIds) {
+ updateAppWidget(context, appWidgetManager, appWidgetId);
+ }
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ super.onReceive(context, intent);
+ initWinBoLLNewsBeanList(context);
+ if (intent.getAction().equals(ACTION_RELOAD_REPORT)) {
+ LogUtils.d(TAG, "ACTION_RELOAD_REPORT");
+ AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+ int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context, APPNewsWidget.class));
+ for (int appWidgetId : appWidgetIds) {
+ updateAppWidget(context, appWidgetManager, appWidgetId);
+ }
+ }else if (intent.getAction().equals(ACTION_WAKEUP_SERVICE)) {
+// LogUtils.d(TAG, "ACTION_WAKEUP_SERVICE");
+// String szWinBoLLModel = intent.getStringExtra(WinBoLL.EXTRA_WINBOLLMODEL);
+// LogUtils.d(TAG, String.format("szWinBoLLModel %s", szWinBoLLModel));
+// if (szWinBoLLModel != null && !szWinBoLLModel.equals("")) {
+// try {
+// WinBoLLModel bean = WinBoLLModel.parseStringToBean(szWinBoLLModel, WinBoLLModel.class);
+// if (bean != null) {
+// String szAppPackageName = bean.getAppPackageName();
+// LogUtils.d(TAG, String.format("szAppPackageName %s", szAppPackageName));
+// String szAppMainServiveName = bean.getAppMainServiveName();
+// LogUtils.d(TAG, String.format("szAppMainServiveName %s", szAppMainServiveName));
+//
+//
+// String appName = AppUtils.getAppNameByPackageName(context, szAppPackageName);
+// LogUtils.d(TAG, String.format("appName %s", appName));
+// WinBoLLNewsBean winBollNewsBean = new WinBoLLNewsBean(appName);
+// SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
+// String currentTime = sdf.format(new Date());
+// StringBuilder sbLine = new StringBuilder();
+// sbLine.append("[");
+// sbLine.append(currentTime);
+// sbLine.append("] Wake up ");
+// sbLine.append(appName);
+// winBollNewsBean.setMessage(sbLine.toString());
+//
+// addWinBoLLNewsBean(context, winBollNewsBean);
+//
+// AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+// int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context, APPNewsWidget.class));
+// for (int appWidgetId : appWidgetIds) {
+// updateAppWidget(context, appWidgetManager, appWidgetId);
+// }
+// }
+// } catch (IOException e) {
+// LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
+// }
+// }
+ }
+ }
+
+ //
+ // 加入新报告信息
+ //
+ public synchronized static void addWinBoLLNewsBean(Context context, WinBoLLNewsBean bean) {
+ initWinBoLLNewsBeanList(context);
+ _WinBoLLNewsBeanList.add(0, bean);
+ // 控制记录总数
+ while (_WinBoLLNewsBeanList.size() > _MAX_PAGES * _OnePageLinesCount) {
+ _WinBoLLNewsBeanList.remove(_WinBoLLNewsBeanList.size() - 1);
+ }
+ WinBoLLNewsBean.saveBeanList(context, _WinBoLLNewsBeanList, WinBoLLNewsBean.class);
+ }
+
+ synchronized static void initWinBoLLNewsBeanList(Context context) {
+ if (_WinBoLLNewsBeanList == null) {
+ _WinBoLLNewsBeanList = new ArrayList();
+ WinBoLLNewsBean.loadBeanList(context, _WinBoLLNewsBeanList, WinBoLLNewsBean.class);
+ }
+ if (_WinBoLLNewsBeanList == null) {
+ _WinBoLLNewsBeanList = new ArrayList();
+ WinBoLLNewsBean.saveBeanList(context, _WinBoLLNewsBeanList, WinBoLLNewsBean.class);
+ }
+ }
+
+ private void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
+ LogUtils.d(TAG, "updateAppWidget(...)");
+
+ RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_news);
+ //设置按钮点击事件
+ Intent intentPre = new Intent(context, APPNewsWidgetClickListener.class);
+ intentPre.setAction(APPNewsWidgetClickListener.ACTION_PRE);
+ PendingIntent pendingIntentPre = PendingIntent.getBroadcast(context, 0, intentPre, PendingIntent.FLAG_UPDATE_CURRENT);
+ views.setOnClickPendingIntent(R.id.widget_button_pre, pendingIntentPre);
+ Intent intentNext = new Intent(context, APPNewsWidgetClickListener.class);
+ intentNext.setAction(APPNewsWidgetClickListener.ACTION_NEXT);
+ PendingIntent pendingIntentNext = PendingIntent.getBroadcast(context, 0, intentNext, PendingIntent.FLAG_UPDATE_CURRENT);
+ views.setOnClickPendingIntent(R.id.widget_button_next, pendingIntentNext);
+
+ views.setTextViewText(R.id.tv_msg, getPageInfo());
+ views.setTextViewText(R.id.tv_news, getMessage());
+ appWidgetManager.updateAppWidget(appWidgetId, views);
+ }
+
+ public static String getMessage() {
+ ArrayList msgTemp = new ArrayList();
+ if (_WinBoLLNewsBeanList != null) {
+ int start = _OnePageLinesCount * _CurrentPageIndex;
+ start = _WinBoLLNewsBeanList.size() > start ? start : _WinBoLLNewsBeanList.size() - 1;
+ for (int i = start, j = 0; i < _WinBoLLNewsBeanList.size() && j < _OnePageLinesCount && start > -1; i++, j++) {
+ msgTemp.add(_WinBoLLNewsBeanList.get(i).getMessage());
+ }
+ String message = String.join("\n", msgTemp);
+ return message;
+ }
+ return "";
+ }
+
+ public static void prePage(Context context) {
+ if (_WinBoLLNewsBeanList != null) {
+ if (_CurrentPageIndex > 0) {
+ _CurrentPageIndex = _CurrentPageIndex - 1;
+ }
+ Intent intentWidget = new Intent(context, APPNewsWidget.class);
+ intentWidget.setAction(APPNewsWidget.ACTION_RELOAD_REPORT);
+ context.sendBroadcast(intentWidget);
+ }
+ }
+
+ public static void nextPage(Context context) {
+ if (_WinBoLLNewsBeanList != null) {
+ if ((_CurrentPageIndex + 1) * _OnePageLinesCount < _WinBoLLNewsBeanList.size()) {
+ _CurrentPageIndex = _CurrentPageIndex + 1;
+ }
+ Intent intentWidget = new Intent(context, APPNewsWidget.class);
+ intentWidget.setAction(APPNewsWidget.ACTION_RELOAD_REPORT);
+ context.sendBroadcast(intentWidget);
+ }
+ }
+
+ String getPageInfo() {
+ if (_WinBoLLNewsBeanList == null) {
+ return "0/0";
+ }
+ int leftCount = _WinBoLLNewsBeanList.size() % _OnePageLinesCount;
+ int currentPageCount = _WinBoLLNewsBeanList.size() / _OnePageLinesCount + (leftCount == 0 ?0: 1);
+ return String.format("%d/%d", _CurrentPageIndex + 1, currentPageCount);
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/widgets/StatusWidget.java b/winboll/src/main/java/cc/winboll/studio/winboll/widgets/StatusWidget.java
new file mode 100644
index 0000000..454a78b
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/widgets/StatusWidget.java
@@ -0,0 +1,59 @@
+package cc.winboll.studio.winboll.widgets;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/02/17 20:32:12
+ */
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.widget.RemoteViews;
+import cc.winboll.studio.winboll.R;
+import cc.winboll.studio.libappbase.ToastUtils;
+
+public class StatusWidget extends AppWidgetProvider {
+
+ public static final String TAG = "StatusWidget";
+
+ public static final String ACTION_STATUS_UPDATE = "cc.winboll.studio.libappbase.widgets.APPWidget.ACTION_STATUS_UPDATE";
+
+ @Override
+ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
+ for (int appWidgetId : appWidgetIds) {
+ updateAppWidget(context, appWidgetManager, appWidgetId);
+ }
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ super.onReceive(context, intent);
+ if (intent.getAction().equals(ACTION_STATUS_UPDATE)) {
+ ToastUtils.show("Test");
+ AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+ int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context, StatusWidget.class));
+ for (int appWidgetId : appWidgetIds) {
+ updateAppWidget(context, appWidgetManager, appWidgetId);
+ }
+ }
+ }
+
+ private void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
+ RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_status);
+ //设置按钮点击事件
+ Intent intentAppButton = new Intent(context, StatusWidgetClickListener.class);
+ intentAppButton.setAction(StatusWidgetClickListener.ACTION_IVAPP);
+ PendingIntent pendingIntentAppButton = PendingIntent.getBroadcast(context, 0, intentAppButton, PendingIntent.FLAG_UPDATE_CURRENT);
+ views.setOnClickPendingIntent(R.id.ivapp, pendingIntentAppButton);
+
+// boolean isActive = ServiceUtils.isServiceRunning(context, TestService.class.getName());
+// if (isActive) {
+// views.setImageViewResource(R.id.ivapp, cc.winboll.studio.libappbase.R.drawable.ic_launcher);
+// } else {
+// views.setImageViewResource(R.id.ivapp, cc.winboll.studio.libappbase.R.drawable.ic_launcher_disable);
+// }
+ appWidgetManager.updateAppWidget(appWidgetId, views);
+ }
+}
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/widgets/StatusWidgetClickListener.java b/winboll/src/main/java/cc/winboll/studio/winboll/widgets/StatusWidgetClickListener.java
new file mode 100644
index 0000000..ae182ce
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/widgets/StatusWidgetClickListener.java
@@ -0,0 +1,33 @@
+package cc.winboll.studio.winboll.widgets;
+
+/**
+ * @Author ZhanGSKen
+ * @Date 2025/02/17 20:33:53
+ * @Describe APPWidgetClickListener
+ */
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.libappbase.ToastUtils;
+
+public class StatusWidgetClickListener extends BroadcastReceiver {
+
+ public static final String TAG = "APPWidgetClickListener";
+
+ public static final String ACTION_IVAPP = "cc.winboll.studio.libappbase.widgets.StatusWidgetClickListener.ACTION_IVAPP";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action == null) {
+ LogUtils.d(TAG, String.format("action %s", action));
+ return;
+ }
+ if (action.equals(ACTION_IVAPP)) {
+ ToastUtils.show("ACTION_LAUNCHER");
+ } else {
+ LogUtils.d(TAG, String.format("action %s", action));
+ }
+ }
+}
diff --git a/winboll/src/main/res/drawable/bg_browser_top.xml b/winboll/src/main/res/drawable/bg_browser_top.xml
new file mode 100644
index 0000000..4762398
--- /dev/null
+++ b/winboll/src/main/res/drawable/bg_browser_top.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/winboll/src/main/res/drawable/bg_container_border.xml b/winboll/src/main/res/drawable/bg_container_border.xml
new file mode 100644
index 0000000..09c5bdf
--- /dev/null
+++ b/winboll/src/main/res/drawable/bg_container_border.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/winboll/src/main/res/drawable/bg_edittext.xml b/winboll/src/main/res/drawable/bg_edittext.xml
new file mode 100644
index 0000000..5bf8014
--- /dev/null
+++ b/winboll/src/main/res/drawable/bg_edittext.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/winboll/src/main/res/drawable/bg_shadow.xml b/winboll/src/main/res/drawable/bg_shadow.xml
new file mode 100644
index 0000000..6d3d898
--- /dev/null
+++ b/winboll/src/main/res/drawable/bg_shadow.xml
@@ -0,0 +1,41 @@
+
+
+
+
+ -
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
diff --git a/winboll/src/main/res/drawable/dot_background.xml b/winboll/src/main/res/drawable/dot_background.xml
new file mode 100644
index 0000000..f9fc6fc
--- /dev/null
+++ b/winboll/src/main/res/drawable/dot_background.xml
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/winboll/src/main/res/drawable/dot_darkgreen_dark.xml b/winboll/src/main/res/drawable/dot_darkgreen_dark.xml
new file mode 100644
index 0000000..25f65b4
--- /dev/null
+++ b/winboll/src/main/res/drawable/dot_darkgreen_dark.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/drawable/ic_cloud.xml b/winboll/src/main/res/drawable/ic_cloud.xml
new file mode 100644
index 0000000..c116648
--- /dev/null
+++ b/winboll/src/main/res/drawable/ic_cloud.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/winboll/src/main/res/drawable/ic_cloud_outline.xml b/winboll/src/main/res/drawable/ic_cloud_outline.xml
new file mode 100644
index 0000000..a8ed00c
--- /dev/null
+++ b/winboll/src/main/res/drawable/ic_cloud_outline.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/winboll/src/main/res/drawable/ic_launcher.xml b/winboll/src/main/res/drawable/ic_launcher.xml
new file mode 100644
index 0000000..21f28b1
--- /dev/null
+++ b/winboll/src/main/res/drawable/ic_launcher.xml
@@ -0,0 +1,13 @@
+
+
+ -
+
+
diff --git a/winboll/src/main/res/drawable/ic_launcher_background.xml b/winboll/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..d5fccc5
--- /dev/null
+++ b/winboll/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/drawable/ic_launcher_beta.xml b/winboll/src/main/res/drawable/ic_launcher_beta.xml
new file mode 100644
index 0000000..3dd7f33
--- /dev/null
+++ b/winboll/src/main/res/drawable/ic_launcher_beta.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/winboll/src/main/res/drawable/ic_winboll_help.xml b/winboll/src/main/res/drawable/ic_winboll_help.xml
new file mode 100644
index 0000000..564175f
--- /dev/null
+++ b/winboll/src/main/res/drawable/ic_winboll_help.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
diff --git a/winboll/src/main/res/drawable/ic_winboll_jindouyun1.png b/winboll/src/main/res/drawable/ic_winboll_jindouyun1.png
new file mode 100644
index 0000000..6b4c005
Binary files /dev/null and b/winboll/src/main/res/drawable/ic_winboll_jindouyun1.png differ
diff --git a/winboll/src/main/res/drawable/ic_winboll_jindouyun2.png b/winboll/src/main/res/drawable/ic_winboll_jindouyun2.png
new file mode 100644
index 0000000..6f4efa0
Binary files /dev/null and b/winboll/src/main/res/drawable/ic_winboll_jindouyun2.png differ
diff --git a/winboll/src/main/res/drawable/ic_winboll_log.xml b/winboll/src/main/res/drawable/ic_winboll_log.xml
new file mode 100644
index 0000000..011f2b2
--- /dev/null
+++ b/winboll/src/main/res/drawable/ic_winboll_log.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/drawable/ic_winboll_logo.xml b/winboll/src/main/res/drawable/ic_winboll_logo.xml
new file mode 100644
index 0000000..ea28987
--- /dev/null
+++ b/winboll/src/main/res/drawable/ic_winboll_logo.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/drawable/ic_winboll_point.xml b/winboll/src/main/res/drawable/ic_winboll_point.xml
new file mode 100644
index 0000000..48028cc
--- /dev/null
+++ b/winboll/src/main/res/drawable/ic_winboll_point.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/winboll/src/main/res/drawable/progress_bar_style.xml b/winboll/src/main/res/drawable/progress_bar_style.xml
new file mode 100644
index 0000000..7e8f8a3
--- /dev/null
+++ b/winboll/src/main/res/drawable/progress_bar_style.xml
@@ -0,0 +1,8 @@
+
+
+
+ -
+
+
+
+
diff --git a/winboll/src/main/res/drawable/shape_gradient.xml b/winboll/src/main/res/drawable/shape_gradient.xml
new file mode 100644
index 0000000..c164fe9
--- /dev/null
+++ b/winboll/src/main/res/drawable/shape_gradient.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
diff --git a/winboll/src/main/res/drawable/view_border.xml b/winboll/src/main/res/drawable/view_border.xml
new file mode 100644
index 0000000..58b374a
--- /dev/null
+++ b/winboll/src/main/res/drawable/view_border.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
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_logon.xml b/winboll/src/main/res/layout/activity_logon.xml
new file mode 100644
index 0000000..2a2c2d1
--- /dev/null
+++ b/winboll/src/main/res/layout/activity_logon.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/layout/activity_main.xml b/winboll/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..60d6fd3
--- /dev/null
+++ b/winboll/src/main/res/layout/activity_main.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/layout/activity_my_termux.xml b/winboll/src/main/res/layout/activity_my_termux.xml
new file mode 100644
index 0000000..f7417f5
--- /dev/null
+++ b/winboll/src/main/res/layout/activity_my_termux.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/layout/activity_new.xml b/winboll/src/main/res/layout/activity_new.xml
new file mode 100644
index 0000000..23a1122
--- /dev/null
+++ b/winboll/src/main/res/layout/activity_new.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/layout/activity_new2.xml b/winboll/src/main/res/layout/activity_new2.xml
new file mode 100644
index 0000000..8d9b4e5
--- /dev/null
+++ b/winboll/src/main/res/layout/activity_new2.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/layout/activity_ollama_window.xml b/winboll/src/main/res/layout/activity_ollama_window.xml
new file mode 100644
index 0000000..0d6c7ed
--- /dev/null
+++ b/winboll/src/main/res/layout/activity_ollama_window.xml
@@ -0,0 +1,206 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/layout/activity_pattern_lock.xml b/winboll/src/main/res/layout/activity_pattern_lock.xml
new file mode 100644
index 0000000..2298d46
--- /dev/null
+++ b/winboll/src/main/res/layout/activity_pattern_lock.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/layout/activity_settings.xml b/winboll/src/main/res/layout/activity_settings.xml
new file mode 100644
index 0000000..b0da872
--- /dev/null
+++ b/winboll/src/main/res/layout/activity_settings.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
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..0ca36f3
--- /dev/null
+++ b/winboll/src/main/res/layout/activity_termux_env_test.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/layout/activity_test_weworkspecsdk.xml b/winboll/src/main/res/layout/activity_test_weworkspecsdk.xml
new file mode 100644
index 0000000..b1448e8
--- /dev/null
+++ b/winboll/src/main/res/layout/activity_test_weworkspecsdk.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/layout/activity_winbollunittest.xml b/winboll/src/main/res/layout/activity_winbollunittest.xml
new file mode 100644
index 0000000..135b845
--- /dev/null
+++ b/winboll/src/main/res/layout/activity_winbollunittest.xml
@@ -0,0 +1,215 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/layout/activity_wxpay.xml b/winboll/src/main/res/layout/activity_wxpay.xml
new file mode 100644
index 0000000..bfd2596
--- /dev/null
+++ b/winboll/src/main/res/layout/activity_wxpay.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/layout/activity_yun.xml b/winboll/src/main/res/layout/activity_yun.xml
new file mode 100644
index 0000000..ac73fab
--- /dev/null
+++ b/winboll/src/main/res/layout/activity_yun.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/layout/dialog_ollama_config.xml b/winboll/src/main/res/layout/dialog_ollama_config.xml
new file mode 100644
index 0000000..cdbef06
--- /dev/null
+++ b/winboll/src/main/res/layout/dialog_ollama_config.xml
@@ -0,0 +1,269 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/layout/fragment_browser.xml b/winboll/src/main/res/layout/fragment_browser.xml
new file mode 100644
index 0000000..04f0e27
--- /dev/null
+++ b/winboll/src/main/res/layout/fragment_browser.xml
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/layout/fragment_main.xml b/winboll/src/main/res/layout/fragment_main.xml
new file mode 100644
index 0000000..4beaf90
--- /dev/null
+++ b/winboll/src/main/res/layout/fragment_main.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/layout/view_toolbar.xml b/winboll/src/main/res/layout/view_toolbar.xml
new file mode 100644
index 0000000..955f81e
--- /dev/null
+++ b/winboll/src/main/res/layout/view_toolbar.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/layout/widget_news.xml b/winboll/src/main/res/layout/widget_news.xml
new file mode 100644
index 0000000..e85e3d2
--- /dev/null
+++ b/winboll/src/main/res/layout/widget_news.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/layout/widget_status.xml b/winboll/src/main/res/layout/widget_status.xml
new file mode 100644
index 0000000..b115a06
--- /dev/null
+++ b/winboll/src/main/res/layout/widget_status.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/menu/menu_main.xml b/winboll/src/main/res/menu/menu_main.xml
new file mode 100644
index 0000000..351f5c1
--- /dev/null
+++ b/winboll/src/main/res/menu/menu_main.xml
@@ -0,0 +1,6 @@
+
+
diff --git a/winboll/src/main/res/menu/toolbar_main.xml b/winboll/src/main/res/menu/toolbar_main.xml
new file mode 100644
index 0000000..05c1c77
--- /dev/null
+++ b/winboll/src/main/res/menu/toolbar_main.xml
@@ -0,0 +1,16 @@
+
+
diff --git a/winboll/src/main/res/menu/toolbar_test.xml b/winboll/src/main/res/menu/toolbar_test.xml
new file mode 100644
index 0000000..ccd0574
--- /dev/null
+++ b/winboll/src/main/res/menu/toolbar_test.xml
@@ -0,0 +1,15 @@
+
+
diff --git a/winboll/src/main/res/values-zh/strings.xml b/winboll/src/main/res/values-zh/strings.xml
new file mode 100644
index 0000000..c014edd
--- /dev/null
+++ b/winboll/src/main/res/values-zh/strings.xml
@@ -0,0 +1,15 @@
+
+
+ 创建桌面快捷方式
+ 系统不支持创建桌面快捷方式
+ 创建桌面快捷方式失败
+ 未找到对应的按钮,请先打开MyTermuxActivity
+ 指纹验证失败
+ 指纹验证
+ 名称:%1$s\n命令:%2$s
+ 开始指纹验证
+ 图标
+ 选择图标
+ 清除图标
+ 图标导入失败
+
diff --git a/winboll/src/main/res/values/attrs.xml b/winboll/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..8b8b40b
--- /dev/null
+++ b/winboll/src/main/res/values/attrs.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/values/colors.xml b/winboll/src/main/res/values/colors.xml
new file mode 100644
index 0000000..c31ed9d
--- /dev/null
+++ b/winboll/src/main/res/values/colors.xml
@@ -0,0 +1,13 @@
+
+
+ #009688
+ #00796B
+ #FF9800
+ #000000
+
+ #FFF5F5F5
+ #FF000000
+ #FF000000
+ #FF00B322
+ #FF808080
+
\ No newline at end of file
diff --git a/winboll/src/main/res/values/dimens.xml b/winboll/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..9781ee2
--- /dev/null
+++ b/winboll/src/main/res/values/dimens.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/winboll/src/main/res/values/strings.xml b/winboll/src/main/res/values/strings.xml
new file mode 100644
index 0000000..50c6e64
--- /dev/null
+++ b/winboll/src/main/res/values/strings.xml
@@ -0,0 +1,45 @@
+
+ WinBoLL
+ WinBoLL 网站浏览器。
+ 筋斗云
+ 金抖云
+ WinBoLL
+ 筋斗云
+ 金抖云
+ WinBoLL X
+ 筋斗云 X
+ 金抖云 X
+ WinBoLL
+ WinBoLL APP
+ MyTermuxActivity
+ 图案密码设置
+ + 添加Termux按钮
+ 执行
+ 编辑
+ 删除
+ 创建桌面快捷方式
+ 取消
+ 确认删除
+ 确定要删除
+ 添加Termux按钮
+ 编辑Termux按钮
+ 保存
+ 确定
+ 取消
+ 按钮名称
+ 执行命令
+ 工作目录(默认 ~)
+ 已删除
+ 按钮名称和执行命令不能为空
+ 系统不支持创建桌面快捷方式
+ 创建桌面快捷方式失败
+ 未找到对应的按钮,请先打开MyTermuxActivity
+ 指纹验证失败
+ 指纹验证
+ 名称:%1$s\n命令:%2$s
+ 开始指纹验证
+ Icon
+ Select Icon
+ Clear Icon
+ Failed to import icon
+
diff --git a/winboll/src/main/res/values/styles.xml b/winboll/src/main/res/values/styles.xml
new file mode 100644
index 0000000..fcf0822
--- /dev/null
+++ b/winboll/src/main/res/values/styles.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/xml/network_security_config.xml b/winboll/src/main/res/xml/network_security_config.xml
new file mode 100644
index 0000000..e19d7b8
--- /dev/null
+++ b/winboll/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,21 @@
+
+
+
+
+ winboll.cc
+ www.winboll.cc
+
+
+
+
+
+
+
+
+
+ 127.0.0.1
+ localhost
+
+
+
+
diff --git a/winboll/src/main/res/xml/shortcutsmaincn1.xml b/winboll/src/main/res/xml/shortcutsmaincn1.xml
new file mode 100644
index 0000000..3196673
--- /dev/null
+++ b/winboll/src/main/res/xml/shortcutsmaincn1.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/xml/shortcutsmaincn2.xml b/winboll/src/main/res/xml/shortcutsmaincn2.xml
new file mode 100644
index 0000000..331ab2e
--- /dev/null
+++ b/winboll/src/main/res/xml/shortcutsmaincn2.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/xml/shortcutsmainen1.xml b/winboll/src/main/res/xml/shortcutsmainen1.xml
new file mode 100644
index 0000000..57bef47
--- /dev/null
+++ b/winboll/src/main/res/xml/shortcutsmainen1.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/xml/widget_provider_info_sos.xml b/winboll/src/main/res/xml/widget_provider_info_sos.xml
new file mode 100644
index 0000000..e03bd34
--- /dev/null
+++ b/winboll/src/main/res/xml/widget_provider_info_sos.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/winboll/src/main/res/xml/widget_provider_info_status.xml b/winboll/src/main/res/xml/widget_provider_info_status.xml
new file mode 100644
index 0000000..5e19984
--- /dev/null
+++ b/winboll/src/main/res/xml/widget_provider_info_status.xml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/winboll/src/stage/AndroidManifest.xml b/winboll/src/stage/AndroidManifest.xml
new file mode 100644
index 0000000..ee78d9f
--- /dev/null
+++ b/winboll/src/stage/AndroidManifest.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/winboll/src/stage/res/values/strings.xml b/winboll/src/stage/res/values/strings.xml
new file mode 100644
index 0000000..ace0c41
--- /dev/null
+++ b/winboll/src/stage/res/values/strings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+