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