From 7a1a5912338116339594ae2b7ce601d1ba1be1e5 Mon Sep 17 00:00:00 2001 From: qinglong Date: Wed, 3 Jun 2026 22:00:09 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=88=E5=B9=B6=20WinBoLL=20=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libwinboll/build.gradle | 9 +- libwinboll/build.properties | 8 +- winboll/build.gradle | 11 +- winboll/build.properties | 8 +- winboll/src/main/AndroidManifest.xml | 18 +- .../java/cc/winboll/studio/winboll/App.java | 67 ++- .../winboll/studio/winboll/MainActivity.java | 9 +- .../activities/PatternLockActivity.java | 2 - .../winboll/activities/SettingsActivity.java | 2 - .../winboll/models/TermuxButtonModel.java | 13 + .../winboll/termux/MyTermuxActivity.java | 554 ++++++++++++++++++ .../winboll/termux/TermuxCommandExecutor.java | 180 +++++- winboll/src/main/res/values-zh/strings.xml | 12 + winboll/src/main/res/values/strings.xml | 12 + 14 files changed, 831 insertions(+), 74 deletions(-) create mode 100644 winboll/src/main/java/cc/winboll/studio/winboll/termux/MyTermuxActivity.java diff --git a/libwinboll/build.gradle b/libwinboll/build.gradle index 9345b33..a2eece2 100644 --- a/libwinboll/build.gradle +++ b/libwinboll/build.gradle @@ -44,13 +44,12 @@ dependencies { //annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0' // WinBoLL库 nexus.winboll.cc 地址 - api 'cc.winboll.studio:libappbase:15.20.21' - api 'cc.winboll.studio:libaes:15.20.9' + 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.20' - //api 'com.github.ZhanGSKen:libaes:aes-v15.20.8' - + //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 index 4ddb6dd..0c9fce3 100644 --- a/libwinboll/build.properties +++ b/libwinboll/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Mon Jun 01 20:31:36 HKT 2026 -stageCount=5 +#Wed Jun 03 07:32:48 HKT 2026 +stageCount=8 libraryProject=libwinboll baseVersion=15.20 -publishVersion=15.20.4 +publishVersion=15.20.7 buildCount=0 -baseBetaVersion=15.20.5 +baseBetaVersion=15.20.8 diff --git a/winboll/build.gradle b/winboll/build.gradle index 0e408a8..15cc45d 100644 --- a/winboll/build.gradle +++ b/winboll/build.gradle @@ -103,14 +103,17 @@ 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' - api 'cc.winboll.studio:libaes:15.20.10' + 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.21' - //api 'com.github.ZhanGSKen:libaes:aes-v15.20.9' + //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 index 4ddb6dd..0c9fce3 100644 --- a/winboll/build.properties +++ b/winboll/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Mon Jun 01 20:31:36 HKT 2026 -stageCount=5 +#Wed Jun 03 07:32:48 HKT 2026 +stageCount=8 libraryProject=libwinboll baseVersion=15.20 -publishVersion=15.20.4 +publishVersion=15.20.7 buildCount=0 -baseBetaVersion=15.20.5 +baseBetaVersion=15.20.8 diff --git a/winboll/src/main/AndroidManifest.xml b/winboll/src/main/AndroidManifest.xml index a949d1a..2c6884e 100644 --- a/winboll/src/main/AndroidManifest.xml +++ b/winboll/src/main/AndroidManifest.xml @@ -11,6 +11,9 @@ + + + @@ -330,9 +333,20 @@ - + android:exported="true" + android:launchMode="singleTask"> + + + + + + + + + + diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/App.java b/winboll/src/main/java/cc/winboll/studio/winboll/App.java index 81b0930..39cb81f 100644 --- a/winboll/src/main/java/cc/winboll/studio/winboll/App.java +++ b/winboll/src/main/java/cc/winboll/studio/winboll/App.java @@ -14,7 +14,6 @@ import android.os.Handler; import android.os.Looper; import android.text.TextUtils; import android.util.Log; -import android.view.Gravity; import android.view.Menu; import android.view.MenuItem; import android.view.ViewGroup; @@ -22,8 +21,12 @@ 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; @@ -33,45 +36,63 @@ 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; -import cc.winboll.studio.libaes.utils.WinBoLLActivityManager; 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() { - super.onCreate(); - //setIsDebugging(BuildConfig.DEBUG); - //setIsDebugging(false); - - WinBoLLActivityManager.init(this); - - // 初始化 Toast 框架 - ToastUtils.init(this); - // 设置 Toast 布局样式 - //ToastUtils.setView(R.layout.view_toast); - //ToastUtils.setStyle(new WhiteToastStyle()); - //ToastUtils.setGravity(Gravity.BOTTOM, 0, 200); - - //CrashHandler.getInstance().registerGlobal(this); - //CrashHandler.getInstance().registerPart(this); + 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 @@ -79,8 +100,8 @@ public class App extends GlobalApplication { super.onTerminate(); ToastUtils.release(); } - - + + public static void write(InputStream input, OutputStream output) throws IOException { byte[] buf = new byte[1024 * 8]; 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 3ffc723..7f34285 100644 --- a/winboll/src/main/java/cc/winboll/studio/winboll/MainActivity.java +++ b/winboll/src/main/java/cc/winboll/studio/winboll/MainActivity.java @@ -17,7 +17,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.termux.MyTermuxActivity; import cc.winboll.studio.winboll.fragments.BrowserFragment; import cc.winboll.studio.winboll.unittest.TermuxEnvTestActivity; import java.util.ArrayList; @@ -37,7 +37,7 @@ public class MainActivity extends DrawerFragmentActivity { @Override protected void onCreate(Bundle savedInstanceState) { - setTheme(cc.winboll.studio.winboll.theme.WinBoLLThemeUtil.getThemeTypeID(this)); + setTheme(cc.winboll.studio.winboll.theme.WinBoLLThemeUtil.getThemeTypeID(getApplicationContext())); super.onCreate(savedInstanceState); initMainHandler(); if (mBrowserFragment == null) { @@ -150,11 +150,12 @@ public class MainActivity extends DrawerFragmentActivity { @Override public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.toolbar_main, menu); + super.onCreateOptionsMenu(menu); if (App.isDebugging()) { getMenuInflater().inflate(R.menu.toolbar_test, menu); } - return super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.toolbar_main, menu); + return true; } @Override 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 index 6581c20..a52414f 100644 --- a/winboll/src/main/java/cc/winboll/studio/winboll/activities/PatternLockActivity.java +++ b/winboll/src/main/java/cc/winboll/studio/winboll/activities/PatternLockActivity.java @@ -14,7 +14,6 @@ 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; @@ -57,7 +56,6 @@ public class PatternLockActivity extends BaseWinBoLLActivity { @Override protected void onCreate(Bundle savedInstanceState) { - AESThemeUtil.applyAppTheme(this); super.onCreate(savedInstanceState); setContentView(R.layout.activity_pattern_lock); diff --git a/winboll/src/main/java/cc/winboll/studio/winboll/activities/SettingsActivity.java b/winboll/src/main/java/cc/winboll/studio/winboll/activities/SettingsActivity.java index c7d877d..1dab4e6 100644 --- a/winboll/src/main/java/cc/winboll/studio/winboll/activities/SettingsActivity.java +++ b/winboll/src/main/java/cc/winboll/studio/winboll/activities/SettingsActivity.java @@ -3,7 +3,6 @@ package cc.winboll.studio.winboll.activities; import android.os.Bundle; import android.view.View; import androidx.appcompat.widget.Toolbar; -import cc.winboll.studio.libaes.utils.AESThemeUtil; import cc.winboll.studio.winboll.R; import android.app.Activity; @@ -29,7 +28,6 @@ public class SettingsActivity extends BaseWinBoLLActivity { @Override protected void onCreate(Bundle savedInstanceState) { - AESThemeUtil.applyAppTheme(this); super.onCreate(savedInstanceState); setContentView(R.layout.activity_settings); 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 index 5262a48..d375a71 100644 --- a/winboll/src/main/java/cc/winboll/studio/winboll/models/TermuxButtonModel.java +++ b/winboll/src/main/java/cc/winboll/studio/winboll/models/TermuxButtonModel.java @@ -16,6 +16,7 @@ public class TermuxButtonModel extends BaseBean { String buttonName; String exeCommand; String workDir; + String iconPath; // 已修改:isCommit 改为规范过去式命名 isCommitted boolean isCommitted; @@ -26,6 +27,7 @@ public class TermuxButtonModel extends BaseBean { this.buttonName = ""; this.exeCommand = ""; this.workDir = ""; + this.iconPath = ""; // 默认初始化 this.isCommitted = false; this.commitTitle = ""; @@ -56,6 +58,14 @@ public class TermuxButtonModel extends BaseBean { return workDir; } + public void setIconPath(String iconPath) { + this.iconPath = iconPath; + } + + public String getIconPath() { + return iconPath; + } + // ========== 已修改 对应 isCommitted 完整 Get & Set ========== public boolean isCommitted() { return isCommitted; @@ -92,6 +102,7 @@ public class TermuxButtonModel extends BaseBean { 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()); @@ -110,6 +121,8 @@ public class TermuxButtonModel extends BaseBean { 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")) { 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/TermuxCommandExecutor.java b/winboll/src/main/java/cc/winboll/studio/winboll/termux/TermuxCommandExecutor.java index 5410ec2..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 = ""; @@ -202,21 +234,121 @@ public class TermuxCommandExecutor { 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; + // 步骤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"; } - return false; + + // 步骤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/res/values-zh/strings.xml b/winboll/src/main/res/values-zh/strings.xml index 045e125..c014edd 100644 --- a/winboll/src/main/res/values-zh/strings.xml +++ b/winboll/src/main/res/values-zh/strings.xml @@ -1,3 +1,15 @@ + 创建桌面快捷方式 + 系统不支持创建桌面快捷方式 + 创建桌面快捷方式失败 + 未找到对应的按钮,请先打开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..50c6e64 100644 --- a/winboll/src/main/res/values/strings.xml +++ b/winboll/src/main/res/values/strings.xml @@ -17,6 +17,7 @@ 执行 编辑 删除 + 创建桌面快捷方式 取消 确认删除 确定要删除 @@ -30,4 +31,15 @@ 工作目录(默认 ~) 已删除 按钮名称和执行命令不能为空 + 系统不支持创建桌面快捷方式 + 创建桌面快捷方式失败 + 未找到对应的按钮,请先打开MyTermuxActivity + 指纹验证失败 + 指纹验证 + 名称:%1$s\n命令:%2$s + 开始指纹验证 + Icon + Select Icon + Clear Icon + Failed to import icon