diff --git a/libwinboll/build.gradle b/libwinboll/build.gradle index ff8b1e0..49309e0 100644 --- a/libwinboll/build.gradle +++ b/libwinboll/build.gradle @@ -45,11 +45,11 @@ dependencies { // WinBoLL库 nexus.winboll.cc 地址 //api 'cc.winboll.studio:libappbase:15.20.22' - //api 'cc.winboll.studio:libaes:15.20.11' + //api 'cc.winboll.studio:libaes:15.20.12' // 备用库 jitpack.io 地址 api 'com.github.ZhanGSKen:libappbase:appbase-v15.20.22' - api 'com.github.ZhanGSKen:libaes:aes-v15.20.11' + api 'com.github.ZhanGSKen:libaes:aes-v15.20.12' api fileTree(dir: 'libs', include: ['*.jar']) } diff --git a/libwinboll/build.properties b/libwinboll/build.properties index 43bd693..f01933c 100644 --- a/libwinboll/build.properties +++ b/libwinboll/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Tue Jun 02 03:18:21 HKT 2026 -stageCount=6 +#Tue Jun 02 20:00:52 HKT 2026 +stageCount=7 libraryProject=libwinboll baseVersion=15.20 -publishVersion=15.20.5 +publishVersion=15.20.6 buildCount=0 -baseBetaVersion=15.20.6 +baseBetaVersion=15.20.7 diff --git a/winboll/build.gradle b/winboll/build.gradle index 04bf997..0c18261 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.11' + //api 'cc.winboll.studio:libaes:15.20.12' // 备用库 jitpack.io 地址 api 'com.github.ZhanGSKen:libappbase:appbase-v15.20.22' - api 'com.github.ZhanGSKen:libaes:aes-v15.20.11' + api 'com.github.ZhanGSKen:libaes:aes-v15.20.12' api fileTree(dir: 'libs', include: ['*.jar']) } diff --git a/winboll/build.properties b/winboll/build.properties index 43bd693..f01933c 100644 --- a/winboll/build.properties +++ b/winboll/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Tue Jun 02 03:18:21 HKT 2026 -stageCount=6 +#Tue Jun 02 20:00:52 HKT 2026 +stageCount=7 libraryProject=libwinboll baseVersion=15.20 -publishVersion=15.20.5 +publishVersion=15.20.6 buildCount=0 -baseBetaVersion=15.20.6 +baseBetaVersion=15.20.7 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/MainActivity.java b/winboll/src/main/java/cc/winboll/studio/winboll/MainActivity.java index 414241b..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; @@ -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/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