From 756cf88b5562fe048da780ba4837a2ba6e7a0e67 Mon Sep 17 00:00:00 2001 From: BigPickle Date: Tue, 2 Jun 2026 19:32:01 +0800 Subject: [PATCH] =?UTF-8?q?feat(MyTermuxActivity):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=A1=8C=E9=9D=A2=E5=BF=AB=E6=8D=B7=E6=96=B9=E5=BC=8F=E5=8F=8A?= =?UTF-8?q?=E6=8C=87=E7=BA=B9=E9=AA=8C=E8=AF=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 列表项长按菜单新增"创建桌面快捷方式" - 支持 ShortcutManager(API 26+) 和 INSTALL_SHORTCUT 广播两种方式 - TermuxCommandExecutor 所有命令执行前增加指纹验证 - 自定义AlertDialog显示命令信息(名称蓝色粗体),通过后启动BiometricPrompt - 列表项名称蓝色粗体显示 - 新增 EXTRA_SESSION_ACTION 确保每次创建新终端会话 - MyTermuxActivity 添加 shortcut 意图处理(onCreate/onNewIntent) --- winboll/build.gradle | 3 + winboll/src/main/AndroidManifest.xml | 16 +- .../winboll/termux/MyTermuxActivity.java | 120 ++++++++++++- .../winboll/termux/TermuxCommandExecutor.java | 165 ++++++++++++++++-- winboll/src/main/res/values-zh/strings.xml | 8 + winboll/src/main/res/values/strings.xml | 8 + 6 files changed, 300 insertions(+), 20 deletions(-) diff --git a/winboll/build.gradle b/winboll/build.gradle index efcfea6..0c18261 100644 --- a/winboll/build.gradle +++ b/winboll/build.gradle @@ -103,6 +103,9 @@ dependencies { 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.22' diff --git a/winboll/src/main/AndroidManifest.xml b/winboll/src/main/AndroidManifest.xml index 01ecfaa..2c6884e 100644 --- a/winboll/src/main/AndroidManifest.xml +++ b/winboll/src/main/AndroidManifest.xml @@ -11,6 +11,9 @@ + + + @@ -332,7 +335,18 @@ + android:exported="true" + android:launchMode="singleTask"> + + + + + + + + + + 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 index 10763b8..c55bbda 100644 --- a/winboll/src/main/java/cc/winboll/studio/winboll/termux/MyTermuxActivity.java +++ b/winboll/src/main/java/cc/winboll/studio/winboll/termux/MyTermuxActivity.java @@ -2,7 +2,17 @@ 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.Color; +import android.graphics.Typeface; +import android.graphics.drawable.Icon; +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.View; import android.view.ViewGroup; import android.widget.AdapterView; @@ -25,6 +35,9 @@ 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; @@ -41,6 +54,41 @@ public class MyTermuxActivity extends AppCompatActivity { 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() { @@ -68,7 +116,8 @@ public class MyTermuxActivity extends AppCompatActivity { public void onItemClick(AdapterView parent, View view, int position, long id) { TermuxButtonModel model = mButtonList.get(position); TermuxCommandExecutor.openTermuxBash(MyTermuxActivity.this, - model.getExeCommand(), model.getWorkDir()); + model.getButtonName(), model.getExeCommand(), + model.getWorkDir(), true); } }); @@ -102,10 +151,11 @@ public class MyTermuxActivity extends AppCompatActivity { private void showContextMenu(final int position) { final TermuxButtonModel model = mButtonList.get(position); - String[] items = new String[]{ + 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) @@ -115,17 +165,70 @@ public class MyTermuxActivity extends AppCompatActivity { public void onClick(DialogInterface dialog, int which) { if (which == 0) { TermuxCommandExecutor.openTermuxBash(MyTermuxActivity.this, - model.getExeCommand(), model.getWorkDir()); + 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 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; + } + 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.createWithResource(this, + android.R.drawable.ic_menu_manage)) + .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()); + 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)) @@ -229,8 +332,15 @@ public class MyTermuxActivity extends AppCompatActivity { } TermuxButtonModel model = mButtonList.get(position); - tv.setText(model.getButtonName() + "\n" + model.getExeCommand()); - tv.setTextColor(getResources().getColor(android.R.color.black)); + 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); return tv; } } 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 f88aafc..5720787 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 @@ -1,12 +1,27 @@ 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 cc.winboll.studio.libappbase.LogUtils; // 替换 Log 为 LogUtils(与 Activity 一致) +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 原型封装) @@ -77,7 +92,12 @@ public class TermuxCommandExecutor { LogUtils.d(TAG, "结果输出目录:" + resultDir); } - // 7. 允许替换参数中的逗号替代字符 + // 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 及以上的前台服务) @@ -175,11 +195,23 @@ public class TermuxCommandExecutor { return tip; } - public static boolean openTermuxBash(Context context, String command) { - return openTermuxBash(context, command, "~"); - } + private static String pendingTargetCmd; + private static String pendingDisplayCmd; + private static String pendingDisplayName; - public static boolean openTermuxBash(Context context, String command, String workDir) { + 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是否安装 @@ -189,10 +221,10 @@ public class TermuxCommandExecutor { } // 2. 定义核心路径(确保路径与Termux中一致) - String projectPath = TERMUX_HOME_PATH; - if (workDir.startsWith("~") || workDir.startsWith(".")) { - projectPath = TERMUX_HOME_PATH + "/" + workDir.substring(1); - } + String projectPath = TERMUX_HOME_PATH; + if (workDir.startsWith("~") || workDir.startsWith(".")) { + projectPath = TERMUX_HOME_PATH + "/" + workDir.substring(1); + } // 3. 构造命令(核心:用stdbuf禁用缓冲,实现实时输出) String targetCmd = ""; @@ -206,12 +238,117 @@ public class TermuxCommandExecutor { // 确保"cd ~/Sources\npwd"这类输入能分段执行 String execCommand = command.replace("\\n", "; "); // 步骤5:执行设定的命令(直接由外层bash解释,避免stdbuf对shell内置命令无效) - // 步骤6:命令执行完后用stdbuf启动交互式bash,保持终端可见 - targetCmd += execCommand + "; stdbuf -o0 -e0 -i0 bash"; + 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); + } + } - // 4. 执行命令(终端会话模式,唤起Termux窗口) - 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/res/values-zh/strings.xml b/winboll/src/main/res/values-zh/strings.xml index 045e125..eb9caed 100644 --- a/winboll/src/main/res/values-zh/strings.xml +++ b/winboll/src/main/res/values-zh/strings.xml @@ -1,3 +1,11 @@ + 创建桌面快捷方式 + 系统不支持创建桌面快捷方式 + 创建桌面快捷方式失败 + 未找到对应的按钮,请先打开MyTermuxActivity + 指纹验证失败 + 指纹验证 + 名称:%1$s\n命令:%2$s + 开始指纹验证 diff --git a/winboll/src/main/res/values/strings.xml b/winboll/src/main/res/values/strings.xml index 02aac87..b50a4db 100644 --- a/winboll/src/main/res/values/strings.xml +++ b/winboll/src/main/res/values/strings.xml @@ -17,6 +17,7 @@ 执行 编辑 删除 + 创建桌面快捷方式 取消 确认删除 确定要删除 @@ -30,4 +31,11 @@ 工作目录(默认 ~) 已删除 按钮名称和执行命令不能为空 + 系统不支持创建桌面快捷方式 + 创建桌面快捷方式失败 + 未找到对应的按钮,请先打开MyTermuxActivity + 指纹验证失败 + 指纹验证 + 名称:%1$s\n命令:%2$s + 开始指纹验证