diff --git a/gradlew b/gradlew
old mode 100644
new mode 100755
diff --git a/winboll/build.properties b/winboll/build.properties
index 49b4b72..224209f 100644
--- a/winboll/build.properties
+++ b/winboll/build.properties
@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
-#Wed Apr 08 17:37:24 GMT 2026
-stageCount=26
+#Thu Apr 30 14:58:25 CST 2026
+stageCount=27
libraryProject=
baseVersion=15.11
-publishVersion=15.11.25
-buildCount=30
-baseBetaVersion=15.11.26
+publishVersion=15.11.26
+buildCount=11
+baseBetaVersion=15.11.27
diff --git a/winboll/src/main/AndroidManifest.xml b/winboll/src/main/AndroidManifest.xml
index 1477fdc..2f5cc47 100644
--- a/winboll/src/main/AndroidManifest.xml
+++ b/winboll/src/main/AndroidManifest.xml
@@ -1,9 +1,9 @@
+ package="cc.winboll.studio.winboll"
+ android:sharedUserId="com.termux">
@@ -13,11 +13,15 @@
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
+
-
+
+
+
+
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/MainActivity.java b/winboll/src/main/java/cc/winboll/studio/winboll/MainActivity.java
index b4a3d84..6f3f370 100644
--- a/winboll/src/main/java/cc/winboll/studio/winboll/MainActivity.java
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/MainActivity.java
@@ -15,6 +15,7 @@ import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.winboll.R;
import cc.winboll.studio.winboll.activities.AboutActivity;
import cc.winboll.studio.winboll.activities.SettingsActivity;
+import cc.winboll.studio.winboll.applications.MyTermuxActivity;
import cc.winboll.studio.winboll.fragments.BrowserFragment;
import cc.winboll.studio.winboll.unittest.TermuxEnvTestActivity;
import java.util.ArrayList;
@@ -155,6 +156,10 @@ public class MainActivity extends DrawerFragmentActivity {
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);
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..2cd527b
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/activities/PatternLockActivity.java
@@ -0,0 +1,295 @@
+package cc.winboll.studio.winboll.activities;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.MotionEvent;
+import android.widget.FrameLayout;
+import androidx.appcompat.widget.Toolbar;
+import cc.winboll.studio.libaes.utils.AESThemeUtil;
+import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
+import cc.winboll.studio.winboll.R;
+
+public class PatternLockActivity extends BaseWinBoLLActivity {
+
+ public static final String TAG = "PatternLockActivity";
+
+ private static final int DOT_RADIUS = 8;
+ private static final int PATTERN_ERROR_DURATION = 1500;
+
+ private static final String PREFS_NAME = "pattern_lock_prefs";
+ static final String KEY_LOCK_PATTERN = "lock_pattern";
+ static final String KEY_ERROR_STATE = "error_state";
+ private static final String KEY_ERROR_REPEAT_PATTERN = "error_repeat_pattern";
+
+ private boolean mIsInErrorState;
+ private boolean mNeedRestart;
+ private Handler mHandler;
+
+ private FrameLayout mContainer;
+ private PatternView mPatternView;
+
+ public PatternLockActivity() {
+ mHandler = new Handler(Looper.getMainLooper());
+ }
+
+ PatternLockActivity(Context context) {
+ mHandler = new Handler(Looper.getMainLooper());
+ }
+
+ @Override
+ public Activity getActivity() {
+ return this;
+ }
+
+ @Override
+ public String getTag() {
+ return TAG;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ AESThemeUtil.applyAppTheme(this);
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_pattern_lock);
+
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ getSupportActionBar().setSubtitle(TAG);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ toolbar.setNavigationOnClickListener(v -> finish());
+
+ mContainer = findViewById(R.id.container);
+ mPatternView = new PatternView(this);
+ mContainer.addView(mPatternView);
+ mPatternView.invalidate();
+
+ mNeedRestart = false;
+ boolean isEnoughPoints = savedInstanceIsEnoughPoints();
+
+ if (savedInstanceState != null) {
+ mIsInErrorState = savedInstanceState.getBoolean(KEY_ERROR_STATE, false);
+ mNeedRestart = savedInstanceState.getBoolean(KEY_ERROR_REPEAT_PATTERN, false);
+ }
+
+ if (mIsInErrorState) {
+ mPatternView.invalidate();
+ }
+ }
+
+ boolean savedInstanceIsEnoughPoints() {
+ int count = 0;
+ if (mPatternView != null) {
+ for (int i = 0; i < 9; i++) {
+ if (mPatternView.mDotState[i] == 1) {
+ count++;
+ }
+ }
+ }
+ return count >= 4 || count == 0;
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ if (mIsInErrorState) {
+ outState.putBoolean(KEY_ERROR_STATE, mIsInErrorState);
+ }
+ if (mNeedRestart) {
+ outState.putBoolean(KEY_ERROR_REPEAT_PATTERN, mNeedRestart);
+ }
+ super.onSaveInstanceState(outState);
+ }
+
+ private void showErrorState() {
+ mIsInErrorState = true;
+ invalidatePattern();
+ mHandler.postDelayed(() -> {
+ mIsInErrorState = false;
+ SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
+ prefs.edit().putBoolean(KEY_ERROR_STATE, false).apply();
+ invalidatePattern();
+ if (mPatternView != null) mPatternView.invalidate();
+ }, PATTERN_ERROR_DURATION);
+ }
+
+ private void clearErrorState() {
+ mIsInErrorState = false;
+ SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
+ prefs.edit().putBoolean(KEY_ERROR_STATE, false).apply();
+ invalidatePattern();
+ if (mPatternView != null) mPatternView.invalidate();
+ }
+
+ private void showErrorToast() {
+ android.widget.Toast.makeText(this, "图案点数不足,请重新绘制",
+ android.widget.Toast.LENGTH_SHORT).show();
+ mNeedRestart = true;
+ }
+
+ private void showSuccessDialog() {
+ android.app.AlertDialog alertDialog = new android.app.AlertDialog.Builder(this)
+ .setTitle("设置成功")
+ .setMessage("图案密码已设置成功")
+ .setPositiveButton("确定", (dialog, which) -> finish())
+ .setCancelable(false)
+ .create();
+ alertDialog.show();
+ }
+
+ void finishWithRestart() {
+ finish();
+ }
+
+ private void invalidatePattern() {
+ if (mPatternView != null) {
+ mPatternView.invalidate();
+ }
+ }
+
+ class PatternView extends FrameLayout {
+ int mPatternSize = 0;
+ int MAX_DOT_COUNT = 9;
+ int[] mDotX = new int[MAX_DOT_COUNT];
+ int[] mDotY = new int[MAX_DOT_COUNT];
+ int[] mDotState = new int[MAX_DOT_COUNT];
+ Bitmap mDotBitmap;
+ Paint mPaintConnector;
+ Paint mPaintErrorBackground;
+ int mDotCount = 0;
+
+ PatternView(Context context) {
+ super(context);
+ setBackgroundColor(Color.WHITE);
+ for (int i = 0; i < MAX_DOT_COUNT; i++) {
+ mDotX[i] = -1;
+ mDotY[i] = -1;
+ mDotState[i] = 0;
+ }
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ if (w == 0 || h == 0) return;
+ mPatternSize = w > h ? h : w;
+ int grid = 3;
+ int cell = mPatternSize / grid;
+
+ for (int i = 0; i < MAX_DOT_COUNT; i++) {
+ mDotX[i] = (i % grid) * cell + cell / 2 - cell / 24;
+ mDotY[i] = (i / grid) * cell + cell / 2 - cell / 24;
+ mDotState[i] = 0;
+ }
+
+ if (mDotBitmap == null) {
+ mDotBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.dot_darkgreen_dark);
+ }
+ if (mPaintConnector == null) {
+ mPaintConnector = new Paint(Paint.FILTER_BITMAP_FLAG);
+ mPaintConnector.setColor(-0xFF006400);
+ }
+ if (mPaintErrorBackground == null) {
+ mPaintErrorBackground = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mPaintErrorBackground.setColor(Color.RED);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (mDotCount > 0) return false;
+
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ invalidate();
+ return true;
+ case MotionEvent.ACTION_MOVE:
+ float x = event.getX();
+ float y = event.getY();
+
+ for (int i = 0; i < MAX_DOT_COUNT; i++) {
+ int dx = (int) Math.abs(x - mDotX[i]);
+ int dy = (int) Math.abs(y - mDotY[i]);
+ if (dx <= DOT_RADIUS && dy <= DOT_RADIUS && mDotState[i] == 0) {
+ mDotState[i] = 1;
+ mDotCount++;
+ }
+ }
+
+ for (int i = 0; i < mDotCount - 1; i++) {
+ int a = -1, b = -1;
+ for (int k = 0; k < MAX_DOT_COUNT; k++) {
+ if (mDotState[k] == 1) {
+ if (a < 0) a = k;
+ else b = k;
+ }
+ }
+ if (a >= 0 && b >= 0) {
+ a = Math.min(a, b);
+ b = Math.max(a, b);
+ }
+ if (mDotState[a] == 1 && mDotState[b] == 1) {
+ int dx = mDotX[b] - mDotX[a];
+ int dy = mDotY[b] - mDotY[a];
+ if ((Math.abs(dx) <= 1 && Math.abs(dy) <= 1) ||
+ (Math.abs(dx) <= 2 && Math.abs(dy) <= 1)) {
+ if (mDotState[b] == 1) {
+ for (int k = a + 1; k < b; k++) {
+ if (mDotState[k] == 0) {
+ mDotState[k] = 1;
+ }
+ }
+ mDotCount += (b - a - 1);
+ }
+ }
+ }
+ }
+ invalidate();
+ return true;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ if (mDotCount < 4) {
+ showErrorState();
+ showErrorToast();
+ }
+ break;
+ }
+ return true;
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ if (mPatternSize == 0) return;
+
+ int activeCount = 0;
+ for (int i = 0; i < MAX_DOT_COUNT; i++) {
+ if (mDotState[i] == 1) activeCount++;
+ }
+
+ if (activeCount == 0) {
+ mPaintErrorBackground.setAlpha(50);
+ } else {
+ mPaintErrorBackground.setAlpha(mIsInErrorState ? 80 : 60);
+ }
+
+ canvas.clipRect(0, 0, mPatternSize * 80 / 100, mPatternSize * 80 / 100);
+
+ canvas.drawRect(0, 0, mPatternSize, mPatternSize, mPaintErrorBackground);
+
+ if (mDotBitmap != null) {
+ for (int i = 0; i < MAX_DOT_COUNT; i++) {
+ if (mDotState[i] == 1) {
+ canvas.drawBitmap(mDotBitmap, mDotX[i], mDotY[i], mPaintConnector);
+ }
+ }
+ }
+ }
+ }
+}
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..5e7bddd
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/applications/MyTermuxActivity.java
@@ -0,0 +1,77 @@
+package cc.winboll.studio.winboll.applications;
+
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import cc.winboll.studio.libappbase.LogUtils;
+import cc.winboll.studio.winboll.R;
+import cc.winboll.studio.winboll.termux.TermuxCommandExecutor;
+
+public class MyTermuxActivity extends AppCompatActivity {
+
+ public static final String TAG = "MyTermuxActivity";
+
+ private Toolbar mToolbar;
+ private Button mTermuxButton;
+ private Button mTermuxWorkSpacesButton;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_my_termux);
+
+ // 初始化工具栏
+ initToolbar();
+ // 初始化按钮
+ initView();
+ }
+
+ private void initToolbar() {
+ mToolbar = findViewById(R.id.toolbar);
+ if (mToolbar != null) {
+ setSupportActionBar(mToolbar);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LogUtils.d(TAG, "点击返回按钮");
+ finish();
+ }
+ });
+ LogUtils.d(TAG, "工具栏初始化完成");
+ }
+ }
+
+ private void initView() {
+ mTermuxButton = findViewById(R.id.btn_termux);
+ if (mTermuxButton != null) {
+ mTermuxButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LogUtils.d(TAG, "点击 Termux 按钮");
+ TermuxCommandExecutor.openTermuxBash(MyTermuxActivity.this, "cd ~");
+ //TermuxCommandExecutor.openTermuxBash(MyTermuxActivity.this, "cd ~/TermuxWorkSpaces", "./TermuxWorkSpaces");
+ }
+ });
+ LogUtils.d(TAG, "Termux 按钮初始化完成");
+ }
+
+ mTermuxWorkSpacesButton = findViewById(R.id.btn_termuxworkspaces);
+ if (mTermuxWorkSpacesButton != null) {
+ mTermuxWorkSpacesButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LogUtils.d(TAG, "点击 TermuxWorkSpaces 按钮");
+ TermuxCommandExecutor.openTermuxBash(MyTermuxActivity.this, "cd ~/TermuxWorkSpaces", "./TermuxWorkSpaces");
+ }
+ });
+ LogUtils.d(TAG, "TermuxWorkSpaces 按钮初始化完成");
+ }
+ }
+
+ private boolean isTermuxAvailable() {
+ return TermuxCommandExecutor.isTermuxInstalled(this);
+ }
+}
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..80a40a4
--- /dev/null
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/models/TermuxButtonModel.java
@@ -0,0 +1,142 @@
+package cc.winboll.studio.winboll.models;
+
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import cc.winboll.studio.libappbase.BaseBean;
+import java.io.IOException;
+
+/**
+ * @Author 豆包&ZhanGSKen
+ * @Date 2026/04/30 10:47
+ */
+public class TermuxButtonModel extends BaseBean {
+
+ public static final String TAG = "TermuxButtonModel";
+
+ String buttonName;
+ String exeCommand;
+ String workDir;
+
+ // 已修改:isCommit 改为规范过去式命名 isCommitted
+ boolean isCommitted;
+ String commitTitle;
+ String commitInfo;
+
+ public TermuxButtonModel() {
+ this.buttonName = "";
+ this.exeCommand = "";
+ this.workDir = "";
+ // 默认初始化
+ this.isCommitted = false;
+ this.commitTitle = "";
+ this.commitInfo = "";
+ }
+
+ public void setButtonName(String buttonName) {
+ this.buttonName = buttonName;
+ }
+
+ public String getButtonName() {
+ return buttonName;
+ }
+
+ public void setExeCommand(String exeCommand) {
+ this.exeCommand = exeCommand;
+ }
+
+ public String getExeCommand() {
+ return exeCommand;
+ }
+
+ public void setWorkDir(String workDir) {
+ this.workDir = workDir;
+ }
+
+ public String getWorkDir() {
+ return workDir;
+ }
+
+ // ========== 已修改 对应 isCommitted 完整 Get & Set ==========
+ public boolean isCommitted() {
+ return isCommitted;
+ }
+
+ public void setCommitted(boolean committed) {
+ isCommitted = committed;
+ }
+
+ public String getCommitTitle() {
+ return commitTitle;
+ }
+
+ public void setCommitTitle(String commitTitle) {
+ this.commitTitle = commitTitle;
+ }
+
+ public String getCommitInfo() {
+ return commitInfo;
+ }
+
+ public void setCommitInfo(String commitInfo) {
+ this.commitInfo = commitInfo;
+ }
+
+ @Override
+ public String getName() {
+ return TermuxButtonModel.class.getName();
+ }
+
+ @Override
+ public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
+ super.writeThisToJsonWriter(jsonWriter);
+ jsonWriter.name("buttonName").value(getButtonName());
+ jsonWriter.name("exeCommand").value(getExeCommand());
+ jsonWriter.name("workDir").value(getWorkDir());
+
+ // JSON写入同步修改
+ jsonWriter.name("isCommitted").value(isCommitted());
+ jsonWriter.name("commitTitle").value(getCommitTitle());
+ jsonWriter.name("commitInfo").value(getCommitInfo());
+ }
+
+ @Override
+ public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
+ if (super.initObjectsFromJsonReader(jsonReader, name)) {
+ return true;
+ } else {
+ if (name.equals("buttonName")) {
+ setButtonName(jsonReader.nextString());
+ } else if (name.equals("exeCommand")) {
+ setExeCommand(jsonReader.nextString());
+ } else if (name.equals("workDir")) {
+ setWorkDir(jsonReader.nextString());
+ }
+ // JSON解析字段同步修改
+ else if (name.equals("isCommitted")) {
+ setCommitted(jsonReader.nextBoolean());
+ } else if (name.equals("commitTitle")) {
+ setCommitTitle(jsonReader.nextString());
+ } else if (name.equals("commitInfo")) {
+ setCommitInfo(jsonReader.nextString());
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
+ jsonReader.beginObject();
+ while (jsonReader.hasNext()) {
+ String name = jsonReader.nextName();
+ if (!initObjectsFromJsonReader(jsonReader, name)) {
+ jsonReader.skipValue();
+ }
+ }
+ jsonReader.endObject();
+ return this;
+ }
+
+}
+
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
index e4f3552..5410ec2 100644
--- a/winboll/src/main/java/cc/winboll/studio/winboll/termux/TermuxCommandExecutor.java
+++ b/winboll/src/main/java/cc/winboll/studio/winboll/termux/TermuxCommandExecutor.java
@@ -22,6 +22,7 @@ public class TermuxCommandExecutor {
// 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 命令(核心方法)
@@ -114,7 +115,7 @@ public class TermuxCommandExecutor {
context,
"/data/data/com.termux/files/usr/bin/bash", // Termux 默认 bash 路径(正确)
args,
- "/data/data/com.termux/files/home", // 默认工作目录
+ TERMUX_HOME_PATH, // 默认工作目录
false, // 终端会话执行(可见)
null // 不输出到文件
);
@@ -173,5 +174,49 @@ public class TermuxCommandExecutor {
LogUtils.d(TAG, "外部应用调用权限提示:" + tip);
return tip;
}
+
+ public static boolean openTermuxBash(Context context, String command) {
+ return openTermuxBash(context, command, "~");
+ }
+
+ public static boolean openTermuxBash(Context context, String command, String workDir) {
+ LogUtils.d(TAG, "openTermuxBash() 按钮点击,执行Gradle命令(实时输出)");
+
+ // 1. 校验Termux是否安装
+ if (!TermuxCommandExecutor.isTermuxInstalled(context)) {
+ LogUtils.e(TAG, "openTermuxBash() 错误:未安装Termux应用");
+ return false;
+ }
+
+ // 2. 定义核心路径(确保路径与Termux中一致)
+ String projectPath = TERMUX_HOME_PATH;
+ if (workDir.startsWith("~") || workDir.startsWith(".")) {
+ projectPath = TERMUX_HOME_PATH + "/" + workDir.substring(1);
+ }
+
+ // 3. 构造命令(核心:用stdbuf禁用缓冲,实现实时输出)
+ String targetCmd = "";
+ // 步骤1:进入项目目录(不存在则创建)
+ targetCmd += "cd " + projectPath + " && ";
+ // 步骤2:加载环境变量
+ targetCmd += "source ~/.bashrc && ";
+ // 步骤3:显式配置PATH
+ targetCmd += "export PATH=/data/data/com.termux/files/usr/bin:$PATH && ";
+ // 步骤4:用stdbuf禁用stdout/stderr缓冲(关键!),执行Gradle命令
+ // -o0:stdout无缓冲;-e0:stderr无缓冲;-i0:stdin无缓冲
+ //targetCmd += "stdbuf -o0 -e0 -i0 " + gradleFullPath + " task --all | grep assemble && ";
+ //targetCmd += "stdbuf -o0 -e0 -i0 " + gradleFullPath + " -Pandroid.aapt2FromMavenOverride=/data/data/com.termux/files/home/android-sdk/build-tools/34.0.4/aapt2 assembleBetaDebug && ";
+ targetCmd += "stdbuf -o0 -e0 -i0 bash && ";
+ // 步骤5:执行成功提示
+ targetCmd += "echo '\n✅ 命令执行完成!' && echo '\n📌 当前目录:" + projectPath + "' && read -p '按回车键关闭终端...'";
+
+
+ // 4. 执行命令(终端会话模式,唤起Termux窗口)
+ boolean cmdSuccess = TermuxCommandExecutor.executeTerminalCommand(context, targetCmd);
+ if (!cmdSuccess) {
+ return true;
+ }
+ return false;
+ }
}
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/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/layout/activity_my_termux.xml b/winboll/src/main/res/layout/activity_my_termux.xml
new file mode 100644
index 0000000..4334b51
--- /dev/null
+++ b/winboll/src/main/res/layout/activity_my_termux.xml
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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/menu/toolbar_main.xml b/winboll/src/main/res/menu/toolbar_main.xml
index bda3b26..05c1c77 100644
--- a/winboll/src/main/res/menu/toolbar_main.xml
+++ b/winboll/src/main/res/menu/toolbar_main.xml
@@ -5,6 +5,9 @@
android:id="@+id/item_home"
android:title="HOME"/>
+
+
+
+
+
+
+
+
+
diff --git a/winboll/src/main/res/values/colors.xml b/winboll/src/main/res/values/colors.xml
index 479769a..b49a043 100644
--- a/winboll/src/main/res/values/colors.xml
+++ b/winboll/src/main/res/values/colors.xml
@@ -3,4 +3,5 @@
#009688
#00796B
#FF9800
+ #000000
\ No newline at end of file
diff --git a/winboll/src/main/res/values/strings.xml b/winboll/src/main/res/values/strings.xml
index daf6947..0cc6cdd 100644
--- a/winboll/src/main/res/values/strings.xml
+++ b/winboll/src/main/res/values/strings.xml
@@ -11,4 +11,6 @@
金抖云 X
WinBoLL
WinBoLL APP
+ MyTermuxActivity
+ 图案密码设置