From f50d15d353aad36b043b16c6b7efef3058fd2a6f Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Fri, 26 Feb 2021 17:09:04 +0500 Subject: [PATCH 001/136] Fix "Duplicate finish request for ActivityRecord" errors --- .../java/com/termux/app/TermuxActivity.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java index 280d9378..b5d87681 100644 --- a/app/src/main/java/com/termux/app/TermuxActivity.java +++ b/app/src/main/java/com/termux/app/TermuxActivity.java @@ -414,7 +414,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection public void onSessionFinished(final TerminalSession finishedSession) { if (mTermService.mWantsToStop) { // The service wants to stop as soon as possible. - finish(); + finishActivityIfNotFinishing(); return; } if (mIsVisible && finishedSession != getCurrentTermSession()) { @@ -550,7 +550,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection }); } else { // The service connected while not in foreground - just bail out. - finish(); + finishActivityIfNotFinishing(); } } else { Intent i = getIntent(); @@ -586,7 +586,14 @@ public final class TermuxActivity extends Activity implements ServiceConnection @Override public void onServiceDisconnected(ComponentName name) { // Respect being stopped from the TermuxService notification action. - finish(); + finishActivityIfNotFinishing(); + } + + public void finishActivityIfNotFinishing() { + // prevent duplicate calls to finish() if called from multiple places + if (!TermuxActivity.this.isFinishing()) { + finish(); + } } @Nullable @@ -627,7 +634,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection if (getDrawer().isDrawerOpen(Gravity.LEFT)) { getDrawer().closeDrawers(); } else { - finish(); + finishActivityIfNotFinishing(); } } @@ -989,7 +996,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection mListViewAdapter.notifyDataSetChanged(); if (mTermService.getSessions().isEmpty()) { // There are no sessions to show, so finish the activity. - finish(); + finishActivityIfNotFinishing(); } else { if (index >= service.getSessions().size()) { index = service.getSessions().size() - 1; From 00194ebb902230c2f238c886c0a864f852cfc191 Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Sat, 27 Feb 2021 13:07:07 +0500 Subject: [PATCH 002/136] Add logcat errors in RunCommandService This commit adds `logcat` errors if an invalid intent action is passed or if `allow-external-apps` is not set to `true` while sending an intent to `RunCommandService`, so that users can detect issues. --- .../com/termux/app/RunCommandService.java | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/termux/app/RunCommandService.java b/app/src/main/java/com/termux/app/RunCommandService.java index 8079091a..df07325c 100644 --- a/app/src/main/java/com/termux/app/RunCommandService.java +++ b/app/src/main/java/com/termux/app/RunCommandService.java @@ -92,20 +92,30 @@ public class RunCommandService extends Service { // Run again in case service is already started and onCreate() is not called runStartForeground(); - if (allowExternalApps() && RUN_COMMAND_ACTION.equals(intent.getAction())) { - Uri programUri = new Uri.Builder().scheme("com.termux.file").path(parsePath(intent.getStringExtra(RUN_COMMAND_PATH))).build(); + // If wrong action passed, then just return + if (!RUN_COMMAND_ACTION.equals(intent.getAction())) { + Log.e("termux", "Unexpected intent action to RunCommandService: " + intent.getAction()); + return Service.START_NOT_STICKY; + } - Intent execIntent = new Intent(TermuxService.ACTION_EXECUTE, programUri); - execIntent.setClass(this, TermuxService.class); - execIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, intent.getStringArrayExtra(RUN_COMMAND_ARGUMENTS)); - execIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, parsePath(intent.getStringExtra(RUN_COMMAND_WORKDIR))); - execIntent.putExtra(TermuxService.EXTRA_EXECUTE_IN_BACKGROUND, intent.getBooleanExtra(RUN_COMMAND_BACKGROUND, false)); + // If allow-external-apps property to not set to "true" + if (!allowExternalApps()) { + Log.e("termux", "RunCommandService requires allow-external-apps property to be set to \"true\" in ~/.termux/termux.properties file."); + return Service.START_NOT_STICKY; + } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - this.startForegroundService(execIntent); - } else { - this.startService(execIntent); - } + Uri programUri = new Uri.Builder().scheme("com.termux.file").path(parsePath(intent.getStringExtra(RUN_COMMAND_PATH))).build(); + + Intent execIntent = new Intent(TermuxService.ACTION_EXECUTE, programUri); + execIntent.setClass(this, TermuxService.class); + execIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, intent.getStringArrayExtra(RUN_COMMAND_ARGUMENTS)); + execIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, parsePath(intent.getStringExtra(RUN_COMMAND_WORKDIR))); + execIntent.putExtra(TermuxService.EXTRA_EXECUTE_IN_BACKGROUND, intent.getBooleanExtra(RUN_COMMAND_BACKGROUND, false)); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + this.startForegroundService(execIntent); + } else { + this.startService(execIntent); } runStopForeground(); From 80858bab6b4194cce0371d7181ee65e531c0d72e Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Sat, 27 Feb 2021 13:10:57 +0500 Subject: [PATCH 003/136] Fix path expansion in RunCommandService This commit fixes `getExpandedTermuxPath()` (previously `parsePath()`) not expanding path if exactly `$PREFIX` is passed and addition of extra trailing slashes in some cases. --- .../main/java/com/termux/app/RunCommandService.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/termux/app/RunCommandService.java b/app/src/main/java/com/termux/app/RunCommandService.java index df07325c..0cee3be6 100644 --- a/app/src/main/java/com/termux/app/RunCommandService.java +++ b/app/src/main/java/com/termux/app/RunCommandService.java @@ -104,12 +104,12 @@ public class RunCommandService extends Service { return Service.START_NOT_STICKY; } - Uri programUri = new Uri.Builder().scheme("com.termux.file").path(parsePath(intent.getStringExtra(RUN_COMMAND_PATH))).build(); + Uri programUri = new Uri.Builder().scheme("com.termux.file").path(getExpandedTermuxPath(intent.getStringExtra(RUN_COMMAND_PATH))).build(); Intent execIntent = new Intent(TermuxService.ACTION_EXECUTE, programUri); execIntent.setClass(this, TermuxService.class); execIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, intent.getStringArrayExtra(RUN_COMMAND_ARGUMENTS)); - execIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, parsePath(intent.getStringExtra(RUN_COMMAND_WORKDIR))); + execIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, getExpandedTermuxPath(intent.getStringExtra(RUN_COMMAND_WORKDIR))); execIntent.putExtra(TermuxService.EXTRA_EXECUTE_IN_BACKGROUND, intent.getBooleanExtra(RUN_COMMAND_BACKGROUND, false)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -188,10 +188,12 @@ public class RunCommandService extends Service { } /** Replace "$PREFIX/" or "~/" prefix with termux absolute paths */ - private String parsePath(String path) { + public static String getExpandedTermuxPath(String path) { if(path != null && !path.isEmpty()) { - path = path.replaceAll("^\\$PREFIX\\/", TermuxService.PREFIX_PATH + "/"); - path = path.replaceAll("^~\\/", TermuxService.HOME_PATH + "/"); + path = path.replaceAll("^\\$PREFIX$", TermuxService.PREFIX_PATH); + path = path.replaceAll("^\\$PREFIX/", TermuxService.PREFIX_PATH + "/"); + path = path.replaceAll("^~/$", TermuxService.HOME_PATH); + path = path.replaceAll("^~/", TermuxService.HOME_PATH + "/"); } return path; From 108e4cb3916895c4e9a8702b5cd59617796e43a0 Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Sat, 27 Feb 2021 13:14:13 +0500 Subject: [PATCH 004/136] Fix workdir logic in RunCommandService This commit fixes the workdir logic to not send `EXTRA_CURRENT_WORKING_DIRECTORY` extra to `TermuxService` if workdir is empty, since that will raise `No such file or directory` exceptions if `cwd` is empty when targeting sdk `29`. --- app/src/main/java/com/termux/app/RunCommandService.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/termux/app/RunCommandService.java b/app/src/main/java/com/termux/app/RunCommandService.java index 0cee3be6..4b01d660 100644 --- a/app/src/main/java/com/termux/app/RunCommandService.java +++ b/app/src/main/java/com/termux/app/RunCommandService.java @@ -104,14 +104,18 @@ public class RunCommandService extends Service { return Service.START_NOT_STICKY; } - Uri programUri = new Uri.Builder().scheme("com.termux.file").path(getExpandedTermuxPath(intent.getStringExtra(RUN_COMMAND_PATH))).build(); + Uri programUri = new Uri.Builder().scheme("com.termux.file").path(getExpandedTermuxPath(intent.getStringExtra(RUN_COMMAND_PATH))).build(); Intent execIntent = new Intent(TermuxService.ACTION_EXECUTE, programUri); execIntent.setClass(this, TermuxService.class); execIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, intent.getStringArrayExtra(RUN_COMMAND_ARGUMENTS)); - execIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, getExpandedTermuxPath(intent.getStringExtra(RUN_COMMAND_WORKDIR))); execIntent.putExtra(TermuxService.EXTRA_EXECUTE_IN_BACKGROUND, intent.getBooleanExtra(RUN_COMMAND_BACKGROUND, false)); + String workingDirectory = intent.getStringExtra(RUN_COMMAND_WORKDIR); + if (workingDirectory != null && !workingDirectory.isEmpty()) { + execIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, getExpandedTermuxPath(workingDirectory)); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { this.startForegroundService(execIntent); } else { From 85b2c44ac74818d659ca106a151edb1a20190845 Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Sat, 27 Feb 2021 13:22:09 +0500 Subject: [PATCH 005/136] Fix current working directory default value This commit fixes the issue when `cwd` is empty and is passed to `Runtime.getRuntime().exec(progArray, env, new File(cwd));`, it raises the `No such file or directory` exceptions when targeting sdk `29`. --- app/src/main/java/com/termux/app/BackgroundJob.java | 4 ++-- app/src/main/java/com/termux/app/TermuxService.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/termux/app/BackgroundJob.java b/app/src/main/java/com/termux/app/BackgroundJob.java index cfb11cd4..faf0089b 100644 --- a/app/src/main/java/com/termux/app/BackgroundJob.java +++ b/app/src/main/java/com/termux/app/BackgroundJob.java @@ -36,7 +36,7 @@ public final class BackgroundJob { public BackgroundJob(String cwd, String fileToExecute, final String[] args, final TermuxService service, PendingIntent pendingIntent) { String[] env = buildEnvironment(false, cwd); - if (cwd == null) cwd = TermuxService.HOME_PATH; + if (cwd == null || cwd.isEmpty()) cwd = TermuxService.HOME_PATH; final String[] progArray = setupProcessArgs(fileToExecute, args); final String processDescription = Arrays.toString(progArray); @@ -136,7 +136,7 @@ public final class BackgroundJob { static String[] buildEnvironment(boolean failSafe, String cwd) { new File(TermuxService.HOME_PATH).mkdirs(); - if (cwd == null) cwd = TermuxService.HOME_PATH; + if (cwd == null || cwd.isEmpty()) cwd = TermuxService.HOME_PATH; List environment = new ArrayList<>(); diff --git a/app/src/main/java/com/termux/app/TermuxService.java b/app/src/main/java/com/termux/app/TermuxService.java index 955ce865..3cb714fa 100644 --- a/app/src/main/java/com/termux/app/TermuxService.java +++ b/app/src/main/java/com/termux/app/TermuxService.java @@ -282,7 +282,7 @@ public final class TermuxService extends Service implements SessionChangedCallba TerminalSession createTermSession(String executablePath, String[] arguments, String cwd, boolean failSafe) { new File(HOME_PATH).mkdirs(); - if (cwd == null) cwd = HOME_PATH; + if (cwd == null || cwd.isEmpty()) cwd = HOME_PATH; String[] env = BackgroundJob.buildEnvironment(failSafe, cwd); boolean isLoginShell = false; From 5a960750256398f4de762023b9c253e96d47ef2d Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Sat, 27 Feb 2021 13:23:37 +0500 Subject: [PATCH 006/136] Update RunCommandService documentation --- .../com/termux/app/RunCommandService.java | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/termux/app/RunCommandService.java b/app/src/main/java/com/termux/app/RunCommandService.java index 4b01d660..93b43cf6 100644 --- a/app/src/main/java/com/termux/app/RunCommandService.java +++ b/app/src/main/java/com/termux/app/RunCommandService.java @@ -21,23 +21,32 @@ import java.nio.charset.StandardCharsets; import java.util.Properties; /** - * When allow-external-apps property is set to "true" in ~/.termux/termux.properties, Termux - * is able to process execute intents sent by third-party applications. + * Third-party apps that are not part of termux world can run commands in termux context by either + * sending an intent to RunCommandService or becoming a plugin host for the termux-tasker plugin + * client. * - * Third-party program must declare com.termux.permission.RUN_COMMAND permission and it should be - * granted by user. + * For the RunCommandService intent to work, there are 2 main requirements: + * 1. The `allow-external-apps` property must be set to "true" in ~/.termux/termux.properties in + * termux app, regardless of if the executable path is inside or outside the `~/.termux/tasker/` + * directory. + * 2. The intent sender/third-party app must request the `com.termux.permission.RUN_COMMAND` + * permission in its `AndroidManifest.xml` and it should be granted by user to the app through the + * app's App Info permissions page in android settings, likely under Additional Permissions. * - * Absolute path of command or script must be given in "RUN_COMMAND_PATH" extra. - * The "RUN_COMMAND_ARGUMENTS", "RUN_COMMAND_WORKDIR" and "RUN_COMMAND_BACKGROUND" extras are + * The absolute path of executable or script must be given in "RUN_COMMAND_PATH" extra. + * The "RUN_COMMAND_ARGUMENTS", "RUN_COMMAND_WORKDIR" and "RUN_COMMAND_BACKGROUND" extras are * optional. The workdir defaults to termux home. The background mode defaults to "false". * The command path and workdir can optionally be prefixed with "$PREFIX/" or "~/" if an absolute * path is not to be given. * - * To automatically bring to foreground and start termux commands that were started with - * background mode "false" in android >= 10 without user having to click the notification manually, - * requires termux to be granted draw over apps permission due to new restrictions + * To automatically bring termux session to foreground and start termux commands that were started + * with background mode "false" in android >= 10 without user having to click the notification + * manually requires termux to be granted draw over apps permission due to new restrictions * of starting activities from the background, this also applies to Termux:Tasker plugin. * + * Check https://github.com/termux/termux-tasker for more details on allow-external-apps and draw + * over apps and other limitations. + * * To reduce the chance of termux being killed by android even further due to violation of not * being able to call startForeground() within ~5s of service start in android >= 8, the user * may disable battery optimizations for termux. @@ -53,12 +62,18 @@ import java.util.Properties; * startService(intent); * * Sample code to run command "top" with "am startservice" command: - * am startservice --user 0 -n com.termux/com.termux.app.RunCommandService - * -a com.termux.RUN_COMMAND - * --es com.termux.RUN_COMMAND_PATH '/data/data/com.termux/files/usr/bin/top' - * --esa com.termux.RUN_COMMAND_ARGUMENTS '-n,5' - * --es com.termux.RUN_COMMAND_WORKDIR '/data/data/com.termux/files/home' + * am startservice --user 0 -n com.termux/com.termux.app.RunCommandService \ + * -a com.termux.RUN_COMMAND \ + * --es com.termux.RUN_COMMAND_PATH '/data/data/com.termux/files/usr/bin/top' \ + * --esa com.termux.RUN_COMMAND_ARGUMENTS '-n,5' \ + * --es com.termux.RUN_COMMAND_WORKDIR '/data/data/com.termux/files/home' \ * --ez com.termux.RUN_COMMAND_BACKGROUND 'false' + * + * If your third-party app is targeting sdk 30 (android 11), then it needs to add `com.termux` + * package to the `queries` element or request `QUERY_ALL_PACKAGES` permission in its + * `AndroidManifest.xml`. Otherwise it will get `PackageSetting{...... com.termux/......} BLOCKED` + * errors in logcat and `RUN_COMMAND` won't work. + * https://developer.android.com/training/basics/intents/package-visibility#package-name */ public class RunCommandService extends Service { From 9fd2cf9834eb9faf56011669be3fa91c52fc038e Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Sat, 27 Feb 2021 13:30:11 +0500 Subject: [PATCH 007/136] Fix NoClassDefFoundError exceptions for TermuxActivity This commit fixes the non-crashing exception `Rejecting re-init on previously-failed class java.lang.Class: java.lang.NoClassDefFoundError: Failed resolution of: Landroid/view/View$OnUnhandledKeyEventListener;` on termux startup due to `setContentView()` call by `TermuxActivity.onCreate()`. The recommended solution seems to be to add `androidx.core:core` dependency, which has solved the issue. https://issuetracker.google.com/issues/117685087 --- app/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/app/build.gradle b/app/build.gradle index 0cbce793..24b58825 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,6 +10,7 @@ android { implementation "androidx.annotation:annotation:1.1.0" implementation "androidx.viewpager:viewpager:1.0.0" implementation "androidx.drawerlayout:drawerlayout:1.1.1" + implementation 'androidx.core:core:1.5.0-beta02' implementation project(":terminal-view") } From 356a442c6fc06582a4e24c6461fb30e37f4340d5 Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Mon, 1 Mar 2021 12:40:04 +0500 Subject: [PATCH 008/136] Request android.permission.DUMP permission This will allow users to run `dumpsys` command after running `adb shell pm grant com.termux android.permission.DUMP`. https://developer.android.com/studio/command-line/dumpsys --- app/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 035fc8da..0ca607b5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,6 +23,7 @@ + Date: Tue, 2 Mar 2021 16:48:15 +0500 Subject: [PATCH 009/136] Request android.permission.REQUEST_INSTALL_PACKAGES permission This will allow users to access `Android/obb` on android 11 after explicitly granting Termux the permission by going to Termux `App Info` in Android `Settings` -> `Advance` -> `Install unknown apps`. https://medium.com/androiddevelopers/android-11-storage-faq-78cefea52b7c --- app/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0ca607b5..59324ec7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -25,6 +25,7 @@ + Date: Sat, 6 Mar 2021 18:25:10 +0500 Subject: [PATCH 010/136] Partial refactor of the mess that is TerminalView - Decouple the `CursorController`, `TextSelectionCursorController`(previously `SelectionModifierCursorController`) and `TextSelectionHandleView` (previously `HandleView`) from `TerminalView` by moving them to their own class files. - Fixes #1501 which caused the `java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.` exception to be thrown when long pressing the down key while simultaneously long pressing the terminal view for text selection. --- .../com/termux/view/TerminalRenderer.java | 8 + .../java/com/termux/view/TerminalView.java | 1011 ++++------------- .../view/textselection/CursorController.java | 55 + .../TextSelectionCursorController.java | 382 +++++++ .../TextSelectionHandleView.java | 345 ++++++ 5 files changed, 999 insertions(+), 802 deletions(-) create mode 100644 terminal-view/src/main/java/com/termux/view/textselection/CursorController.java create mode 100644 terminal-view/src/main/java/com/termux/view/textselection/TextSelectionCursorController.java create mode 100644 terminal-view/src/main/java/com/termux/view/textselection/TextSelectionHandleView.java diff --git a/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java b/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java index 92df53f8..5c9caf1c 100644 --- a/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java +++ b/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java @@ -230,4 +230,12 @@ public final class TerminalRenderer { if (savedMatrix) canvas.restore(); } + + public float getFontWidth() { + return mFontWidth; + } + + public int getFontLineSpacing() { + return mFontLineSpacing; + } } diff --git a/terminal-view/src/main/java/com/termux/view/TerminalView.java b/terminal-view/src/main/java/com/termux/view/TerminalView.java index 5f2b5949..5522efbc 100644 --- a/terminal-view/src/main/java/com/termux/view/TerminalView.java +++ b/terminal-view/src/main/java/com/termux/view/TerminalView.java @@ -6,11 +6,8 @@ import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.graphics.Canvas; -import android.graphics.Rect; import android.graphics.Typeface; -import android.graphics.drawable.Drawable; import android.os.Build; -import android.os.SystemClock; import android.text.Editable; import android.text.InputType; import android.text.TextUtils; @@ -21,31 +18,24 @@ import android.view.HapticFeedbackConstants; import android.view.InputDevice; import android.view.KeyCharacterMap; import android.view.KeyEvent; -import android.view.Menu; -import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.view.ViewParent; import android.view.ViewTreeObserver; -import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; import android.view.autofill.AutofillValue; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; -import android.widget.PopupWindow; import android.widget.Scroller; import androidx.annotation.RequiresApi; import com.termux.terminal.EmulatorDebug; import com.termux.terminal.KeyHandler; -import com.termux.terminal.TerminalBuffer; import com.termux.terminal.TerminalEmulator; import com.termux.terminal.TerminalSession; -import com.termux.terminal.WcWidth; +import com.termux.view.textselection.TextSelectionCursorController; import java.io.File; import java.io.FileInputStream; @@ -57,28 +47,22 @@ import java.util.Properties; public final class TerminalView extends View { /** Log view key and IME events. */ - private static final boolean LOG_KEY_EVENTS = false; + private static final boolean LOG_KEY_EVENTS = true; /** The currently displayed terminal session, whose emulator is {@link #mEmulator}. */ - TerminalSession mTermSession; + public TerminalSession mTermSession; /** Our terminal emulator whose session is {@link #mTermSession}. */ - TerminalEmulator mEmulator; + public TerminalEmulator mEmulator; - TerminalRenderer mRenderer; + public TerminalRenderer mRenderer; - TerminalViewClient mClient; + public TerminalViewClient mClient; + + private TextSelectionCursorController mTextSelectionCursorController; /** The top row of text to display. Ranges from -activeTranscriptRows to 0. */ int mTopRow; - - boolean mIsSelectingText = false; - int mSelX1 = -1, mSelX2 = -1, mSelY1 = -1, mSelY2 = -1; - private ActionMode mActionMode; - Drawable mSelectHandleLeft; - Drawable mSelectHandleRight; - final int[] mTempCoords = new int[2]; - Rect mTempRect; - private SelectionModifierCursorController mSelectionModifierCursorController; + int[] mDefaultSelectors = new int[]{-1,-1,-1,-1}; float mScaleFactor = 1.f; final GestureAndScaleRecognizer mGestureRecognizer; @@ -96,7 +80,7 @@ public final class TerminalView extends View { /** If non-zero, this is the last unicode code point received if that was a combining character. */ int mCombiningAccent; - private boolean mAccessibilityEnabled; + private final boolean mAccessibilityEnabled; public TerminalView(Context context, AttributeSet attributes) { // NO_UCD (unused code) super(context, attributes); @@ -105,13 +89,13 @@ public final class TerminalView extends View { boolean scrolledWithFinger; @Override - public boolean onUp(MotionEvent e) { + public boolean onUp(MotionEvent event) { mScrollRemainder = 0.0f; - if (mEmulator != null && mEmulator.isMouseTrackingActive() && !mIsSelectingText && !scrolledWithFinger) { + if (mEmulator != null && mEmulator.isMouseTrackingActive() && !isSelectingText() && !scrolledWithFinger) { // Quick event processing when mouse tracking is active - do not wait for check of double tapping // for zooming. - sendMouseEventCode(e, TerminalEmulator.MOUSE_LEFT_BUTTON, true); - sendMouseEventCode(e, TerminalEmulator.MOUSE_LEFT_BUTTON, false); + sendMouseEventCode(event, TerminalEmulator.MOUSE_LEFT_BUTTON, true); + sendMouseEventCode(event, TerminalEmulator.MOUSE_LEFT_BUTTON, false); return true; } scrolledWithFinger = false; @@ -119,16 +103,17 @@ public final class TerminalView extends View { } @Override - public boolean onSingleTapUp(MotionEvent e) { + public boolean onSingleTapUp(MotionEvent event) { if (mEmulator == null) return true; - if (mIsSelectingText) { + + if (isSelectingText()) { stopTextSelectionMode(); return true; } requestFocus(); if (!mEmulator.isMouseTrackingActive()) { - if (!e.isFromSource(InputDevice.SOURCE_MOUSE)) { - mClient.onSingleTapUp(e); + if (!event.isFromSource(InputDevice.SOURCE_MOUSE)) { + mClient.onSingleTapUp(event); return true; } } @@ -156,7 +141,7 @@ public final class TerminalView extends View { @Override public boolean onScale(float focusX, float focusY, float scale) { - if (mEmulator == null || mIsSelectingText) return true; + if (mEmulator == null || isSelectingText()) return true; mScaleFactor *= scale; mScaleFactor = mClient.onScale(mScaleFactor); return true; @@ -200,22 +185,28 @@ public final class TerminalView extends View { @Override public boolean onDown(float x, float y) { + // Why is true not returned here? + // https://developer.android.com/training/gestures/detector.html#detect-a-subset-of-supported-gestures + // Although setting this to true still does not solve the following errors when long pressing in terminal view text area + // ViewDragHelper: Ignoring pointerId=0 because ACTION_DOWN was not received for this pointer before ACTION_MOVE + // Commenting out the call to mGestureDetector.onTouchEvent(event) in GestureAndScaleRecognizer#onTouchEvent() removes + // the error logging, so issue is related to GestureDetector return false; } @Override - public boolean onDoubleTap(MotionEvent e) { + public boolean onDoubleTap(MotionEvent event) { // Do not treat is as a single confirmed tap - it may be followed by zoom. return false; } @Override - public void onLongPress(MotionEvent e) { + public void onLongPress(MotionEvent event) { if (mGestureRecognizer.isInProgress()) return; - if (mClient.onLongPress(e)) return; - if (!mIsSelectingText) { + if (mClient.onLongPress(event)) return; + if (!isSelectingText()) { performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); - startSelectingText(e); + startTextSelectionMode(event); } } }); @@ -392,7 +383,7 @@ public final class TerminalView extends View { if (mTopRow < -rowsInHistory) mTopRow = -rowsInHistory; boolean skipScrolling = false; - if (mIsSelectingText) { + if (isSelectingText()) { // Do not scroll when selecting text. int rowShift = mEmulator.getScrollCounter(); if (-mTopRow + rowShift > rowsInHistory) { @@ -402,8 +393,7 @@ public final class TerminalView extends View { } else { skipScrolling = true; mTopRow -= rowShift; - mSelY1 -= rowShift; - mSelY2 -= rowShift; + decrementYTextSelectionCursors(rowShift); } } @@ -500,19 +490,19 @@ public final class TerminalView extends View { @SuppressLint("ClickableViewAccessibility") @Override @TargetApi(23) - public boolean onTouchEvent(MotionEvent ev) { + public boolean onTouchEvent(MotionEvent event) { if (mEmulator == null) return true; - final int action = ev.getAction(); + final int action = event.getAction(); - if (mIsSelectingText) { - updateFloatingToolbarVisibility(ev); - mGestureRecognizer.onTouchEvent(ev); + if (isSelectingText()) { + updateFloatingToolbarVisibility(event); + mGestureRecognizer.onTouchEvent(event); return true; - } else if (ev.isFromSource(InputDevice.SOURCE_MOUSE)) { - if (ev.isButtonPressed(MotionEvent.BUTTON_SECONDARY)) { + } else if (event.isFromSource(InputDevice.SOURCE_MOUSE)) { + if (event.isButtonPressed(MotionEvent.BUTTON_SECONDARY)) { if (action == MotionEvent.ACTION_DOWN) showContextMenu(); return true; - } else if (ev.isButtonPressed(MotionEvent.BUTTON_TERTIARY)) { + } else if (event.isButtonPressed(MotionEvent.BUTTON_TERTIARY)) { ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); ClipData clipData = clipboard.getPrimaryClip(); if (clipData != null) { @@ -520,20 +510,20 @@ public final class TerminalView extends View { if (!TextUtils.isEmpty(paste)) mEmulator.paste(paste.toString()); } } else if (mEmulator.isMouseTrackingActive()) { // BUTTON_PRIMARY. - switch (ev.getAction()) { + switch (event.getAction()) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_UP: - sendMouseEventCode(ev, TerminalEmulator.MOUSE_LEFT_BUTTON, ev.getAction() == MotionEvent.ACTION_DOWN); + sendMouseEventCode(event, TerminalEmulator.MOUSE_LEFT_BUTTON, event.getAction() == MotionEvent.ACTION_DOWN); break; case MotionEvent.ACTION_MOVE: - sendMouseEventCode(ev, TerminalEmulator.MOUSE_LEFT_BUTTON_MOVED, true); + sendMouseEventCode(event, TerminalEmulator.MOUSE_LEFT_BUTTON_MOVED, true); break; } return true; } } - mGestureRecognizer.onTouchEvent(ev); + mGestureRecognizer.onTouchEvent(event); return true; } @@ -544,7 +534,7 @@ public final class TerminalView extends View { if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "onKeyPreIme(keyCode=" + keyCode + ", event=" + event + ")"); if (keyCode == KeyEvent.KEYCODE_BACK) { - if (mIsSelectingText) { + if (isSelectingText()) { stopTextSelectionMode(); return true; } else if (mClient.shouldBackButtonBeMappedToEscape()) { @@ -570,7 +560,9 @@ public final class TerminalView extends View { if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "onKeyDown(keyCode=" + keyCode + ", isSystem()=" + event.isSystem() + ", event=" + event + ")"); if (mEmulator == null) return true; - stopTextSelectionMode(); + if (isSelectingText()) { + stopTextSelectionMode(); + } if (mClient.onKeyDown(keyCode, event, mTermSession)) { invalidate(); @@ -759,41 +751,18 @@ public final class TerminalView extends View { if (mEmulator == null) { canvas.drawColor(0XFF000000); } else { - mRenderer.render(mEmulator, canvas, mTopRow, mSelY1, mSelY2, mSelX1, mSelX2); - - - SelectionModifierCursorController selectionController = getSelectionController(); - if (selectionController != null && selectionController.isActive()) { - selectionController.updatePosition(); + // render the terminal view and highlight any selected text + int[] sel = mDefaultSelectors; + if (mTextSelectionCursorController != null) { + mTextSelectionCursorController.getSelectors(sel); } + mRenderer.render(mEmulator, canvas, mTopRow, sel[0], sel[1], sel[2], sel[3]); + + // render the text selection handles + renderTextSelection(); } } - /** Toggle text selection mode in the view. */ - @TargetApi(23) - public void startSelectingText(MotionEvent ev) { - int cx = (int) (ev.getX() / mRenderer.mFontWidth); - final boolean eventFromMouse = ev.isFromSource(InputDevice.SOURCE_MOUSE); - // Offset for finger: - final int SELECT_TEXT_OFFSET_Y = eventFromMouse ? 0 : -40; - int cy = (int) ((ev.getY() + SELECT_TEXT_OFFSET_Y) / mRenderer.mFontLineSpacing) + mTopRow; - - mSelX1 = mSelX2 = cx; - mSelY1 = mSelY2 = cy; - - TerminalBuffer screen = mEmulator.getScreen(); - if (!" ".equals(screen.getSelectedText(mSelX1, mSelY1, mSelX1, mSelY1))) { - // Selecting something other than whitespace. Expand to word. - while (mSelX1 > 0 && !"".equals(screen.getSelectedText(mSelX1 - 1, mSelY1, mSelX1 - 1, mSelY1))) { - mSelX1--; - } - while (mSelX2 < mEmulator.mColumns - 1 && !"".equals(screen.getSelectedText(mSelX2 + 1, mSelY1, mSelX2 + 1, mSelY1))) { - mSelX2++; - } - } - startTextSelectionMode(); - } - public TerminalSession getCurrentSession() { return mTermSession; } @@ -802,12 +771,137 @@ public final class TerminalView extends View { return mEmulator.getScreen().getSelectedText(0, mTopRow, mEmulator.mColumns, mTopRow + mEmulator.mRows); } + public int getCursorX(float x) { + return (int) (x / mRenderer.mFontWidth); + } + + public int getCursorY(float y) { + return (int) (((y - 40) / mRenderer.mFontLineSpacing) + mTopRow); + } + + public int getPointX(int cx) { + if (cx > mEmulator.mColumns) { + cx = mEmulator.mColumns; + } + return Math.round(cx * mRenderer.mFontWidth); + } + + public int getPointY(int cy) { + return Math.round((cy - mTopRow) * mRenderer.mFontLineSpacing); + } + + public int getTopRow() { + return mTopRow; + } + + public void setTopRow(int mTopRow) { + this.mTopRow = mTopRow; + } + + + + + /** + * Define functions required for AutoFill API + */ + @RequiresApi(api = Build.VERSION_CODES.O) + @Override + public void autofill(AutofillValue value) { + if (value.isText()) { + mTermSession.write(value.getTextValue().toString()); + } + } + + @RequiresApi(api = Build.VERSION_CODES.O) + @Override + public int getAutofillType() { + return AUTOFILL_TYPE_TEXT; + } + + @RequiresApi(api = Build.VERSION_CODES.O) + @Override + public AutofillValue getAutofillValue() { + return AutofillValue.forText(""); + } + + + + + /** + * Define functions required for text selection and its handles. + */ + TextSelectionCursorController getTextSelectionCursorController() { + if (mTextSelectionCursorController == null) { + mTextSelectionCursorController = new TextSelectionCursorController(this); + + final ViewTreeObserver observer = getViewTreeObserver(); + if (observer != null) { + observer.addOnTouchModeChangeListener(mTextSelectionCursorController); + } + } + + return mTextSelectionCursorController; + } + + private void showTextSelectionCursors(MotionEvent event) { + getTextSelectionCursorController().show(event); + } + + private boolean hideTextSelectionCursors() { + return getTextSelectionCursorController().hide(); + } + + private void renderTextSelection() { + if (mTextSelectionCursorController != null) + mTextSelectionCursorController.render(); + } + + public boolean isSelectingText() { + if (mTextSelectionCursorController != null) { + return mTextSelectionCursorController.isActive(); + } else { + return false; + } + } + + private ActionMode getTextSelectionActionMode() { + if (mTextSelectionCursorController != null) { + return mTextSelectionCursorController.getActionMode(); + } else { + return null; + } + } + + public void startTextSelectionMode(MotionEvent event) { + if (!requestFocus()) { + return; + } + + showTextSelectionCursors(event); + mClient.copyModeChanged(isSelectingText()); + + invalidate(); + } + + public void stopTextSelectionMode() { + if (hideTextSelectionCursors()) { + mClient.copyModeChanged(isSelectingText()); + invalidate(); + } + } + + private void decrementYTextSelectionCursors(int decrement) { + if (mTextSelectionCursorController != null) { + mTextSelectionCursorController.decrementYTextSelectionCursors(decrement); + } + } + @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); - if (mSelectionModifierCursorController != null) { - getViewTreeObserver().addOnTouchModeChangeListener(mSelectionModifierCursorController); + if (mTextSelectionCursorController != null) { + getViewTreeObserver().addOnTouchModeChangeListener(mTextSelectionCursorController); } } @@ -815,722 +909,50 @@ public final class TerminalView extends View { protected void onDetachedFromWindow() { super.onDetachedFromWindow(); - if (mSelectionModifierCursorController != null) { - getViewTreeObserver().removeOnTouchModeChangeListener(mSelectionModifierCursorController); - mSelectionModifierCursorController.onDetached(); + if (mTextSelectionCursorController != null) { + // Might solve the following exception + // android.view.WindowLeaked: Activity com.termux.app.TermuxActivity has leaked window android.widget.PopupWindow + stopTextSelectionMode(); + + getViewTreeObserver().removeOnTouchModeChangeListener(mTextSelectionCursorController); + mTextSelectionCursorController.onDetached(); } } - private int getCursorX(float x) { - return (int) (x / mRenderer.mFontWidth); - } - private int getCursorY(float y) { - return (int) (((y - 40) / mRenderer.mFontLineSpacing) + mTopRow); - } - - private int getPointX(int cx) { - if (cx > mEmulator.mColumns) { - cx = mEmulator.mColumns; - } - return Math.round(cx * mRenderer.mFontWidth); - } - - private int getPointY(int cy) { - return Math.round((cy - mTopRow) * mRenderer.mFontLineSpacing); - } /** - * A CursorController instance can be used to control a cursor in the text. - * It is not used outside of {@link TerminalView}. + * Define functions required for long hold toolbar. */ - private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener { - /** - * Makes the cursor controller visible on screen. Will be drawn by {@link #draw(Canvas)}. - * See also {@link #hide()}. - */ - void show(); - - /** - * Hide the cursor controller from screen. - * See also {@link #show()}. - */ - void hide(); - - /** - * @return true if the CursorController is currently visible - */ - boolean isActive(); - - /** - * Update the controller's position. - */ - void updatePosition(HandleView handle, int x, int y); - - void updatePosition(); - - /** - * This method is called by {@link #onTouchEvent(MotionEvent)} and gives the controller - * a chance to become active and/or visible. - * - * @param event The touch event - */ - boolean onTouchEvent(MotionEvent event); - - /** - * Called when the view is detached from window. Perform house keeping task, such as - * stopping Runnable thread that would otherwise keep a reference on the context, thus - * preventing the activity to be recycled. - */ - void onDetached(); - } - - private class HandleView extends View { - private Drawable mDrawable; - private PopupWindow mContainer; - private int mPointX; - private int mPointY; - private CursorController mController; - private boolean mIsDragging; - private float mTouchToWindowOffsetX; - private float mTouchToWindowOffsetY; - private float mHotspotX; - private float mHotspotY; - private float mTouchOffsetY; - private int mLastParentX; - private int mLastParentY; - - int mHandleWidth; - private final int mOrigOrient; - private int mOrientation; - - - public static final int LEFT = 0; - public static final int RIGHT = 2; - private int mHandleHeight; - - private long mLastTime; - - public HandleView(CursorController controller, int orientation) { - super(TerminalView.this.getContext()); - mController = controller; - mContainer = new PopupWindow(TerminalView.this.getContext(), null, - android.R.attr.textSelectHandleWindowStyle); - mContainer.setSplitTouchEnabled(true); - mContainer.setClippingEnabled(false); - mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL); - mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT); - mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); - - this.mOrigOrient = orientation; - setOrientation(orientation); - } - - public void setOrientation(int orientation) { - mOrientation = orientation; - int handleWidth = 0; - switch (orientation) { - case LEFT: { - if (mSelectHandleLeft == null) { - - mSelectHandleLeft = getContext().getDrawable( - R.drawable.text_select_handle_left_material); - } - // - mDrawable = mSelectHandleLeft; - handleWidth = mDrawable.getIntrinsicWidth(); - mHotspotX = (handleWidth * 3) / 4; - break; - } - - case RIGHT: { - if (mSelectHandleRight == null) { - mSelectHandleRight = getContext().getDrawable( - R.drawable.text_select_handle_right_material); - } - mDrawable = mSelectHandleRight; - handleWidth = mDrawable.getIntrinsicWidth(); - mHotspotX = handleWidth / 4; - break; - } - - } - - mHandleHeight = mDrawable.getIntrinsicHeight(); - - mHandleWidth = handleWidth; - mTouchOffsetY = -mHandleHeight * 0.3f; - mHotspotY = 0; - invalidate(); - } - - public void changeOrientation(int orientation) { - if (mOrientation != orientation) { - setOrientation(orientation); - } - } - - @Override - public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - setMeasuredDimension(mDrawable.getIntrinsicWidth(), - mDrawable.getIntrinsicHeight()); - } - - public void show() { - if (!isPositionVisible()) { - hide(); - return; - } - mContainer.setContentView(this); - final int[] coords = mTempCoords; - TerminalView.this.getLocationInWindow(coords); - coords[0] += mPointX; - coords[1] += mPointY; - mContainer.showAtLocation(TerminalView.this, 0, coords[0], coords[1]); - } - - public void hide() { - mIsDragging = false; - mContainer.dismiss(); - } - - public boolean isShowing() { - return mContainer.isShowing(); - } - - private void checkChangedOrientation(int posX, boolean force) { - if (!mIsDragging && !force) { - return; - } - long millis = SystemClock.currentThreadTimeMillis(); - if (millis - mLastTime < 50 && !force) { - return; - } - mLastTime = millis; - - final TerminalView hostView = TerminalView.this; - final int left = hostView.getLeft(); - final int right = hostView.getWidth(); - final int top = hostView.getTop(); - final int bottom = hostView.getHeight(); - - if (mTempRect == null) { - mTempRect = new Rect(); - } - final Rect clip = mTempRect; - clip.left = left + TerminalView.this.getPaddingLeft(); - clip.top = top + TerminalView.this.getPaddingTop(); - clip.right = right - TerminalView.this.getPaddingRight(); - clip.bottom = bottom - TerminalView.this.getPaddingBottom(); - - final ViewParent parent = hostView.getParent(); - if (parent == null || !parent.getChildVisibleRect(hostView, clip, null)) { - return; - } - - if (posX - mHandleWidth < clip.left) { - changeOrientation(RIGHT); - } else if (posX + mHandleWidth > clip.right) { - changeOrientation(LEFT); - } else { - changeOrientation(mOrigOrient); - } - } - - private boolean isPositionVisible() { - // Always show a dragging handle. - if (mIsDragging) { - return true; - } - - final TerminalView hostView = TerminalView.this; - final int left = 0; - final int right = hostView.getWidth(); - final int top = 0; - final int bottom = hostView.getHeight(); - - if (mTempRect == null) { - mTempRect = new Rect(); - } - final Rect clip = mTempRect; - clip.left = left + TerminalView.this.getPaddingLeft(); - clip.top = top + TerminalView.this.getPaddingTop(); - clip.right = right - TerminalView.this.getPaddingRight(); - clip.bottom = bottom - TerminalView.this.getPaddingBottom(); - - final ViewParent parent = hostView.getParent(); - if (parent == null || !parent.getChildVisibleRect(hostView, clip, null)) { - return false; - } - - final int[] coords = mTempCoords; - hostView.getLocationInWindow(coords); - final int posX = coords[0] + mPointX + (int) mHotspotX; - final int posY = coords[1] + mPointY + (int) mHotspotY; - - return posX >= clip.left && posX <= clip.right && - posY >= clip.top && posY <= clip.bottom; - } - - private void moveTo(int x, int y, boolean forceOrientationCheck) { - float oldHotspotX = mHotspotX; - checkChangedOrientation(x, forceOrientationCheck); - mPointX = (int) (x - (isShowing() ? oldHotspotX : mHotspotX)); - mPointY = y; - if (isPositionVisible()) { - int[] coords = null; - if (isShowing()) { - coords = mTempCoords; - TerminalView.this.getLocationInWindow(coords); - int x1 = coords[0] + mPointX; - int y1 = coords[1] + mPointY; - mContainer.update(x1, y1, - getWidth(), getHeight()); - } else { - show(); - } - - if (mIsDragging) { - if (coords == null) { - coords = mTempCoords; - TerminalView.this.getLocationInWindow(coords); - } - if (coords[0] != mLastParentX || coords[1] != mLastParentY) { - mTouchToWindowOffsetX += coords[0] - mLastParentX; - mTouchToWindowOffsetY += coords[1] - mLastParentY; - mLastParentX = coords[0]; - mLastParentY = coords[1]; - } - } - } else { - if (isShowing()) { - hide(); - } - } - } - - @Override - public void onDraw(Canvas c) { - final int drawWidth = mDrawable.getIntrinsicWidth(); - int height = mDrawable.getIntrinsicHeight(); - mDrawable.setBounds(0, 0, drawWidth, height); - mDrawable.draw(c); - - } - - @SuppressLint("ClickableViewAccessibility") - @Override - public boolean onTouchEvent(MotionEvent ev) { - updateFloatingToolbarVisibility(ev); - switch (ev.getActionMasked()) { - case MotionEvent.ACTION_DOWN: { - final float rawX = ev.getRawX(); - final float rawY = ev.getRawY(); - mTouchToWindowOffsetX = rawX - mPointX; - mTouchToWindowOffsetY = rawY - mPointY; - final int[] coords = mTempCoords; - TerminalView.this.getLocationInWindow(coords); - mLastParentX = coords[0]; - mLastParentY = coords[1]; - mIsDragging = true; - break; - } - - case MotionEvent.ACTION_MOVE: { - final float rawX = ev.getRawX(); - final float rawY = ev.getRawY(); - - final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX; - final float newPosY = rawY - mTouchToWindowOffsetY + mHotspotY + mTouchOffsetY; - - mController.updatePosition(this, Math.round(newPosX), Math.round(newPosY)); - - - break; - } - - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - mIsDragging = false; - } - return true; - } - - - public boolean isDragging() { - return mIsDragging; - } - - void positionAtCursor(final int cx, final int cy, boolean forceOrientationCheck) { - int left = getPointX(cx); - int bottom = getPointY(cy + 1); - moveTo(left, bottom, forceOrientationCheck); - } - } - - - private class SelectionModifierCursorController implements CursorController { - private final int mHandleHeight; - // The cursor controller images - private HandleView mStartHandle, mEndHandle; - // Whether selection anchors are active - private boolean mIsShowing; - - SelectionModifierCursorController() { - mStartHandle = new HandleView(this, HandleView.LEFT); - mEndHandle = new HandleView(this, HandleView.RIGHT); - - mHandleHeight = Math.max(mStartHandle.mHandleHeight, mEndHandle.mHandleHeight); - } - - public void show() { - mIsShowing = true; - mStartHandle.positionAtCursor(mSelX1, mSelY1, true); - mEndHandle.positionAtCursor(mSelX2 + 1, mSelY2, true); - - final ActionMode.Callback callback = new ActionMode.Callback() { - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - int show = MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT; - - ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); - menu.add(Menu.NONE, 1, Menu.NONE, R.string.copy_text).setShowAsAction(show); - menu.add(Menu.NONE, 2, Menu.NONE, R.string.paste_text).setEnabled(clipboard.hasPrimaryClip()).setShowAsAction(show); - menu.add(Menu.NONE, 3, Menu.NONE, R.string.text_selection_more); - return true; - } - - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - return false; - } - - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - if (!mIsSelectingText) { - // Fix issue where the dialog is pressed while being dismissed. - return true; - } - switch (item.getItemId()) { - case 1: - String selectedText = mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2).trim(); - mTermSession.clipboardText(selectedText); - break; - case 2: - ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clipData = clipboard.getPrimaryClip(); - if (clipData != null) { - CharSequence paste = clipData.getItemAt(0).coerceToText(getContext()); - if (!TextUtils.isEmpty(paste)) mEmulator.paste(paste.toString()); - } - break; - case 3: - showContextMenu(); - break; - } - stopTextSelectionMode(); - return true; - } - - @Override - public void onDestroyActionMode(ActionMode mode) { - } - - }; - mActionMode = startActionMode(new ActionMode.Callback2() { - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - return callback.onCreateActionMode(mode, menu); - } - - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - return false; - } - - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - return callback.onActionItemClicked(mode, item); - } - - @Override - public void onDestroyActionMode(ActionMode mode) { - // Ignore. - } - - @Override - public void onGetContentRect(ActionMode mode, View view, Rect outRect) { - int x1 = Math.round(mSelX1 * mRenderer.mFontWidth); - int x2 = Math.round(mSelX2 * mRenderer.mFontWidth); - int y1 = Math.round((mSelY1 - 1 - mTopRow) * mRenderer.mFontLineSpacing); - int y2 = Math.round((mSelY2 + 1 - mTopRow) * mRenderer.mFontLineSpacing); - - - if (x1 > x2) { - int tmp = x1; - x1 = x2; - x2 = tmp; - } - - outRect.set(x1, y1 + mHandleHeight, x2, y2 + mHandleHeight); - } - }, ActionMode.TYPE_FLOATING); - - } - - public void hide() { - mStartHandle.hide(); - mEndHandle.hide(); - mIsShowing = false; - if (mActionMode != null) { - // This will hide the mSelectionModifierCursorController - mActionMode.finish(); - } - - } - - public boolean isActive() { - return mIsShowing; - } - - - public void updatePosition(HandleView handle, int x, int y) { - - TerminalBuffer screen = mEmulator.getScreen(); - final int scrollRows = screen.getActiveRows() - mEmulator.mRows; - if (handle == mStartHandle) { - mSelX1 = getCursorX(x); - mSelY1 = getCursorY(y); - if (mSelX1 < 0) { - mSelX1 = 0; - } - - if (mSelY1 < -scrollRows) { - mSelY1 = -scrollRows; - - } else if (mSelY1 > mEmulator.mRows - 1) { - mSelY1 = mEmulator.mRows - 1; - - } - - - if (mSelY1 > mSelY2) { - mSelY1 = mSelY2; - } - if (mSelY1 == mSelY2 && mSelX1 > mSelX2) { - mSelX1 = mSelX2; - } - - if (!mEmulator.isAlternateBufferActive()) { - if (mSelY1 <= mTopRow) { - mTopRow--; - if (mTopRow < -scrollRows) { - mTopRow = -scrollRows; - } - } else if (mSelY1 >= mTopRow + mEmulator.mRows) { - mTopRow++; - if (mTopRow > 0) { - mTopRow = 0; - } - } - } - - - mSelX1 = getValidCurX(screen, mSelY1, mSelX1); - - } else { - mSelX2 = getCursorX(x); - mSelY2 = getCursorY(y); - if (mSelX2 < 0) { - mSelX2 = 0; - } - - - if (mSelY2 < -scrollRows) { - mSelY2 = -scrollRows; - } else if (mSelY2 > mEmulator.mRows - 1) { - mSelY2 = mEmulator.mRows - 1; - } - - if (mSelY1 > mSelY2) { - mSelY2 = mSelY1; - } - if (mSelY1 == mSelY2 && mSelX1 > mSelX2) { - mSelX2 = mSelX1; - } - - if (!mEmulator.isAlternateBufferActive()) { - if (mSelY2 <= mTopRow) { - mTopRow--; - if (mTopRow < -scrollRows) { - mTopRow = -scrollRows; - } - } else if (mSelY2 >= mTopRow + mEmulator.mRows) { - mTopRow++; - if (mTopRow > 0) { - mTopRow = 0; - } - } - } - - mSelX2 = getValidCurX(screen, mSelY2, mSelX2); - } - - invalidate(); - } - - - private int getValidCurX(TerminalBuffer screen, int cy, int cx) { - String line = screen.getSelectedText(0, cy, cx, cy); - if (!TextUtils.isEmpty(line)) { - int col = 0; - for (int i = 0, len = line.length(); i < len; i++) { - char ch1 = line.charAt(i); - if (ch1 == 0) { - break; - } - - - int wc; - if (Character.isHighSurrogate(ch1) && i + 1 < len) { - char ch2 = line.charAt(++i); - wc = WcWidth.width(Character.toCodePoint(ch1, ch2)); - } else { - wc = WcWidth.width(ch1); - } - - final int cend = col + wc; - if (cx > col && cx < cend) { - return cend; - } - if (cend == col) { - return col; - } - col = cend; - } - } - return cx; - } - - public void updatePosition() { - if (!isActive()) { - return; - } - - mStartHandle.positionAtCursor(mSelX1, mSelY1, false); - - mEndHandle.positionAtCursor(mSelX2 + 1, mSelY2, false); - - if (mActionMode != null) { - mActionMode.invalidate(); - } - - } - - public boolean onTouchEvent(MotionEvent event) { - - return false; - } - - - /** - * @return true iff this controller is currently used to move the selection start. - */ - public boolean isSelectionStartDragged() { - return mStartHandle.isDragging(); - } - - public boolean isSelectionEndDragged() { - return mEndHandle.isDragging(); - } - - public void onTouchModeChanged(boolean isInTouchMode) { - if (!isInTouchMode) { - hide(); - } - } - - @Override - public void onDetached() { - } - } - - SelectionModifierCursorController getSelectionController() { - if (mSelectionModifierCursorController == null) { - mSelectionModifierCursorController = new SelectionModifierCursorController(); - - final ViewTreeObserver observer = getViewTreeObserver(); - if (observer != null) { - observer.addOnTouchModeChangeListener(mSelectionModifierCursorController); - } - } - - return mSelectionModifierCursorController; - } - - private void hideSelectionModifierCursorController() { - if (mSelectionModifierCursorController != null && mSelectionModifierCursorController.isActive()) { - mSelectionModifierCursorController.hide(); - } - } - - - private void startTextSelectionMode() { - if (!requestFocus()) { - return; - } - - getSelectionController().show(); - - mIsSelectingText = true; - - mClient.copyModeChanged(mIsSelectingText); - - invalidate(); - } - - private void stopTextSelectionMode() { - if (mIsSelectingText) { - hideSelectionModifierCursorController(); - mSelX1 = mSelY1 = mSelX2 = mSelY2 = -1; - mIsSelectingText = false; - - mClient.copyModeChanged(mIsSelectingText); - - invalidate(); - } - } - - private final Runnable mShowFloatingToolbar = new Runnable() { @Override public void run() { - if (mActionMode != null) { - mActionMode.hide(0); // hide off. + if (getTextSelectionActionMode() != null) { + getTextSelectionActionMode().hide(0); // hide off. } } }; - void hideFloatingToolbar(int duration) { - if (mActionMode != null) { - removeCallbacks(mShowFloatingToolbar); - mActionMode.hide(duration); - } - } - private void showFloatingToolbar() { - if (mActionMode != null) { + if (getTextSelectionActionMode() != null) { int delay = ViewConfiguration.getDoubleTapTimeout(); postDelayed(mShowFloatingToolbar, delay); } } - private void updateFloatingToolbarVisibility(MotionEvent event) { - if (mActionMode != null) { + void hideFloatingToolbar() { + if (getTextSelectionActionMode() != null) { + removeCallbacks(mShowFloatingToolbar); + getTextSelectionActionMode().hide(-1); + } + } + + public void updateFloatingToolbarVisibility(MotionEvent event) { + if (getTextSelectionActionMode() != null) { switch (event.getActionMasked()) { case MotionEvent.ACTION_MOVE: - hideFloatingToolbar(-1); + hideFloatingToolbar(); break; case MotionEvent.ACTION_UP: // fall through case MotionEvent.ACTION_CANCEL: @@ -1539,6 +961,10 @@ public final class TerminalView extends View { } } + + + + private Properties getProperties() { File propsFile; Properties props = new Properties(); @@ -1567,23 +993,4 @@ public final class TerminalView extends View { return props; } - @RequiresApi(api = Build.VERSION_CODES.O) - @Override - public void autofill(AutofillValue value) { - if (value.isText()) { - mTermSession.write(value.getTextValue().toString()); - } - } - - @RequiresApi(api = Build.VERSION_CODES.O) - @Override - public int getAutofillType() { - return AUTOFILL_TYPE_TEXT; - } - - @RequiresApi(api = Build.VERSION_CODES.O) - @Override - public AutofillValue getAutofillValue() { - return AutofillValue.forText(""); - } } diff --git a/terminal-view/src/main/java/com/termux/view/textselection/CursorController.java b/terminal-view/src/main/java/com/termux/view/textselection/CursorController.java new file mode 100644 index 00000000..f0e1cc5e --- /dev/null +++ b/terminal-view/src/main/java/com/termux/view/textselection/CursorController.java @@ -0,0 +1,55 @@ +package com.termux.view.textselection; + +import android.view.MotionEvent; +import android.view.ViewTreeObserver; + +import com.termux.view.TerminalView; + +/** + * A CursorController instance can be used to control cursors in the text. + * It is not used outside of {@link TerminalView}. + */ +public interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener { + /** + * Show the cursors on screen. Will be drawn by {@link #render()} by a call during onDraw. + * See also {@link #hide()}. + */ + void show(MotionEvent event); + + /** + * Hide the cursors from screen. + * See also {@link #show(MotionEvent event)}. + */ + boolean hide(); + + /** + * Render the cursors. + */ + void render(); + + /** + * Update the cursor positions. + */ + void updatePosition(TextSelectionHandleView handle, int x, int y); + + /** + * This method is called by {@link #onTouchEvent(MotionEvent)} and gives the cursors + * a chance to become active and/or visible. + * + * @param event The touch event + */ + boolean onTouchEvent(MotionEvent event); + + /** + * Called when the view is detached from window. Perform house keeping task, such as + * stopping Runnable thread that would otherwise keep a reference on the context, thus + * preventing the activity to be recycled. + */ + void onDetached(); + + /** + * @return true if the cursors are currently active. + */ + boolean isActive(); + +} diff --git a/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionCursorController.java b/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionCursorController.java new file mode 100644 index 00000000..8ff36b7f --- /dev/null +++ b/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionCursorController.java @@ -0,0 +1,382 @@ +package com.termux.view.textselection; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.graphics.Rect; +import android.text.TextUtils; +import android.view.ActionMode; +import android.view.InputDevice; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; + +import com.termux.terminal.TerminalBuffer; +import com.termux.terminal.WcWidth; +import com.termux.view.R; +import com.termux.view.TerminalView; + +public class TextSelectionCursorController implements CursorController { + + private final TerminalView terminalView; + private final TextSelectionHandleView mStartHandle, mEndHandle; + private boolean mIsSelectingText = false; + private long mShowStartTime = System.currentTimeMillis(); + + private final int mHandleHeight; + private int mSelX1 = -1, mSelX2 = -1, mSelY1 = -1, mSelY2 = -1; + + private ActionMode mActionMode; + private final int ACTION_COPY = 1; + private final int ACTION_PASTE = 2; + private final int ACTION_MORE = 3; + + public TextSelectionCursorController(TerminalView terminalView) { + this.terminalView = terminalView; + mStartHandle = new TextSelectionHandleView(terminalView, this, TextSelectionHandleView.LEFT); + mEndHandle = new TextSelectionHandleView(terminalView, this, TextSelectionHandleView.RIGHT); + + mHandleHeight = Math.max(mStartHandle.getHandleHeight(), mEndHandle.getHandleHeight()); + } + + @Override + public void show(MotionEvent event) { + setInitialTextSelectionPosition(event); + mStartHandle.positionAtCursor(mSelX1, mSelY1, true); + mEndHandle.positionAtCursor(mSelX2 + 1, mSelY2, true); + + setActionModeCallBacks(); + mShowStartTime = System.currentTimeMillis(); + mIsSelectingText = true; + } + + @Override + public boolean hide() { + if (!isActive()) return false; + + // prevent hide calls right after a show call, like long pressing the down key + // 300ms seems long enough that it wouldn't cause hide problems if action button + // is quickly clicked after the show, otherwise decrease it + if (System.currentTimeMillis() - mShowStartTime < 300) { + return false; + } + + mStartHandle.hide(); + mEndHandle.hide(); + + if (mActionMode != null) { + // This will hide the TextSelectionCursorController + mActionMode.finish(); + } + + mSelX1 = mSelY1 = mSelX2 = mSelY2 = -1; + mIsSelectingText = false; + + return true; + } + + @Override + public void render() { + if (!isActive()) return; + + mStartHandle.positionAtCursor(mSelX1, mSelY1, false); + mEndHandle.positionAtCursor(mSelX2 + 1, mSelY2, false); + + if (mActionMode != null) { + mActionMode.invalidate(); + } + } + + public void setInitialTextSelectionPosition(MotionEvent event) { + int cx = (int) (event.getX() / terminalView.mRenderer.getFontWidth()); + final boolean eventFromMouse = event.isFromSource(InputDevice.SOURCE_MOUSE); + // Offset for finger: + final int SELECT_TEXT_OFFSET_Y = eventFromMouse ? 0 : -40; + int cy = (int) ((event.getY() + SELECT_TEXT_OFFSET_Y) / terminalView.mRenderer.getFontLineSpacing()) + terminalView.getTopRow(); + + mSelX1 = mSelX2 = cx; + mSelY1 = mSelY2 = cy; + + TerminalBuffer screen = terminalView.mEmulator.getScreen(); + if (!" ".equals(screen.getSelectedText(mSelX1, mSelY1, mSelX1, mSelY1))) { + // Selecting something other than whitespace. Expand to word. + while (mSelX1 > 0 && !"".equals(screen.getSelectedText(mSelX1 - 1, mSelY1, mSelX1 - 1, mSelY1))) { + mSelX1--; + } + while (mSelX2 < terminalView.mEmulator.mColumns - 1 && !"".equals(screen.getSelectedText(mSelX2 + 1, mSelY1, mSelX2 + 1, mSelY1))) { + mSelX2++; + } + } + } + + public void setActionModeCallBacks() { + final ActionMode.Callback callback = new ActionMode.Callback() { + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + int show = MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT; + + ClipboardManager clipboard = (ClipboardManager) terminalView.getContext().getSystemService(Context.CLIPBOARD_SERVICE); + menu.add(Menu.NONE, ACTION_COPY, Menu.NONE, R.string.copy_text).setShowAsAction(show); + menu.add(Menu.NONE, ACTION_PASTE, Menu.NONE, R.string.paste_text).setEnabled(clipboard.hasPrimaryClip()).setShowAsAction(show); + menu.add(Menu.NONE, ACTION_MORE, Menu.NONE, R.string.text_selection_more); + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + if (!isActive()) { + // Fix issue where the dialog is pressed while being dismissed. + return true; + } + + switch (item.getItemId()) { + case ACTION_COPY: + String selectedText = terminalView.mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2).trim(); + terminalView.mTermSession.clipboardText(selectedText); + terminalView.stopTextSelectionMode(); + break; + case ACTION_PASTE: + terminalView.stopTextSelectionMode(); + ClipboardManager clipboard = (ClipboardManager) terminalView.getContext().getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clipData = clipboard.getPrimaryClip(); + if (clipData != null) { + CharSequence paste = clipData.getItemAt(0).coerceToText(terminalView.getContext()); + if (!TextUtils.isEmpty(paste)) terminalView.mEmulator.paste(paste.toString()); + } + break; + case ACTION_MORE: + terminalView.stopTextSelectionMode(); //we stop text selection first, otherwise handles will show above popup + terminalView.showContextMenu(); + break; + } + + return true; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + } + + }; + + mActionMode = terminalView.startActionMode(new ActionMode.Callback2() { + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + return callback.onCreateActionMode(mode, menu); + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return callback.onActionItemClicked(mode, item); + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + // Ignore. + } + + @Override + public void onGetContentRect(ActionMode mode, View view, Rect outRect) { + int x1 = Math.round(mSelX1 * terminalView.mRenderer.getFontWidth()); + int x2 = Math.round(mSelX2 * terminalView.mRenderer.getFontWidth()); + int y1 = Math.round((mSelY1 - 1 - terminalView.getTopRow()) * terminalView.mRenderer.getFontLineSpacing()); + int y2 = Math.round((mSelY2 + 1 - terminalView.getTopRow()) * terminalView.mRenderer.getFontLineSpacing()); + + + if (x1 > x2) { + int tmp = x1; + x1 = x2; + x2 = tmp; + } + + outRect.set(x1, y1 + mHandleHeight, x2, y2 + mHandleHeight); + } + }, ActionMode.TYPE_FLOATING); + } + + @Override + public void updatePosition(TextSelectionHandleView handle, int x, int y) { + TerminalBuffer screen = terminalView.mEmulator.getScreen(); + final int scrollRows = screen.getActiveRows() - terminalView.mEmulator.mRows; + if (handle == mStartHandle) { + mSelX1 = terminalView.getCursorX(x); + mSelY1 = terminalView.getCursorY(y); + if (mSelX1 < 0) { + mSelX1 = 0; + } + + if (mSelY1 < -scrollRows) { + mSelY1 = -scrollRows; + + } else if (mSelY1 > terminalView.mEmulator.mRows - 1) { + mSelY1 = terminalView.mEmulator.mRows - 1; + + } + + if (mSelY1 > mSelY2) { + mSelY1 = mSelY2; + } + if (mSelY1 == mSelY2 && mSelX1 > mSelX2) { + mSelX1 = mSelX2; + } + + if (!terminalView.mEmulator.isAlternateBufferActive()) { + int topRow = terminalView.getTopRow(); + + if (mSelY1 <= topRow) { + topRow--; + if (topRow < -scrollRows) { + topRow = -scrollRows; + } + } else if (mSelY1 >= topRow + terminalView.mEmulator.mRows) { + topRow++; + if (topRow > 0) { + topRow = 0; + } + } + + terminalView.setTopRow(topRow); + } + + mSelX1 = getValidCurX(screen, mSelY1, mSelX1); + + } else { + mSelX2 = terminalView.getCursorX(x); + mSelY2 = terminalView.getCursorY(y); + if (mSelX2 < 0) { + mSelX2 = 0; + } + + if (mSelY2 < -scrollRows) { + mSelY2 = -scrollRows; + } else if (mSelY2 > terminalView.mEmulator.mRows - 1) { + mSelY2 = terminalView.mEmulator.mRows - 1; + } + + if (mSelY1 > mSelY2) { + mSelY2 = mSelY1; + } + if (mSelY1 == mSelY2 && mSelX1 > mSelX2) { + mSelX2 = mSelX1; + } + + if (!terminalView.mEmulator.isAlternateBufferActive()) { + int topRow = terminalView.getTopRow(); + + if (mSelY2 <= topRow) { + topRow--; + if (topRow < -scrollRows) { + topRow = -scrollRows; + } + } else if (mSelY2 >= topRow + terminalView.mEmulator.mRows) { + topRow++; + if (topRow > 0) { + topRow = 0; + } + } + + terminalView.setTopRow(topRow); + } + + mSelX2 = getValidCurX(screen, mSelY2, mSelX2); + } + + terminalView.invalidate(); + } + + private int getValidCurX(TerminalBuffer screen, int cy, int cx) { + String line = screen.getSelectedText(0, cy, cx, cy); + if (!TextUtils.isEmpty(line)) { + int col = 0; + for (int i = 0, len = line.length(); i < len; i++) { + char ch1 = line.charAt(i); + if (ch1 == 0) { + break; + } + + int wc; + if (Character.isHighSurrogate(ch1) && i + 1 < len) { + char ch2 = line.charAt(++i); + wc = WcWidth.width(Character.toCodePoint(ch1, ch2)); + } else { + wc = WcWidth.width(ch1); + } + + final int cend = col + wc; + if (cx > col && cx < cend) { + return cend; + } + if (cend == col) { + return col; + } + col = cend; + } + } + return cx; + } + + public void decrementYTextSelectionCursors(int decrement) { + mSelY1 -= decrement; + mSelY2 -= decrement; + } + + public boolean onTouchEvent(MotionEvent event) { + return false; + } + + public void onTouchModeChanged(boolean isInTouchMode) { + if (!isInTouchMode) { + terminalView.stopTextSelectionMode(); + } + } + + @Override + public void onDetached() { + } + + @Override + public boolean isActive() { + return mIsSelectingText; + } + + public void getSelectors(int[] sel) { + if (sel == null || sel.length != 4) { + return; + } + + sel[0] = mSelY1; + sel[1] = mSelY2; + sel[2] = mSelX1; + sel[3] = mSelX2; + } + + public ActionMode getActionMode() { + return mActionMode; + } + + /** + * @return true if this controller is currently used to move the start selection. + */ + public boolean isSelectionStartDragged() { + return mStartHandle.isDragging(); + } + + /** + * @return true if this controller is currently used to move the end selection. + */ + public boolean isSelectionEndDragged() { + return mEndHandle.isDragging(); + } + +} diff --git a/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionHandleView.java b/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionHandleView.java new file mode 100644 index 00000000..74634be2 --- /dev/null +++ b/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionHandleView.java @@ -0,0 +1,345 @@ +package com.termux.view.textselection; + +import android.annotation.SuppressLint; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.SystemClock; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.WindowManager; +import android.widget.PopupWindow; + +import com.termux.view.R; +import com.termux.view.TerminalView; + +@SuppressLint("ViewConstructor") +public class TextSelectionHandleView extends View { + private final TerminalView terminalView; + private PopupWindow mHandle; + private final CursorController mCursorController; + + private final Drawable mHandleLeftDrawable; + private final Drawable mHandleRightDrawable; + private Drawable mHandleDrawable; + + private boolean mIsDragging; + + final int[] mTempCoords = new int[2]; + Rect mTempRect; + + private int mPointX; + private int mPointY; + private float mTouchToWindowOffsetX; + private float mTouchToWindowOffsetY; + private float mHotspotX; + private float mHotspotY; + private float mTouchOffsetY; + private int mLastParentX; + private int mLastParentY; + + private int mHandleHeight; + private int mHandleWidth; + + private final int mInitialOrientation; + private int mOrientation; + + public static final int LEFT = 0; + public static final int RIGHT = 2; + + private long mLastTime; + + public TextSelectionHandleView(TerminalView terminalView, CursorController cursorController, int initialOrientation) { + super(terminalView.getContext()); + this.terminalView = terminalView; + mCursorController = cursorController; + mInitialOrientation = initialOrientation; + + mHandleLeftDrawable = getContext().getDrawable(R.drawable.text_select_handle_left_material); + mHandleRightDrawable = getContext().getDrawable(R.drawable.text_select_handle_right_material); + + setOrientation(mInitialOrientation); + } + + private void initHandle() { + mHandle = new PopupWindow(terminalView.getContext(), null, + android.R.attr.textSelectHandleWindowStyle); + mHandle.setSplitTouchEnabled(true); + mHandle.setClippingEnabled(false); + mHandle.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL); + mHandle.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT); + mHandle.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); + mHandle.setBackgroundDrawable(null); + mHandle.setAnimationStyle(0); + mHandle.setEnterTransition(null); + mHandle.setExitTransition(null); + mHandle.setContentView(this); + } + + public void setOrientation(int orientation) { + mOrientation = orientation; + int handleWidth = 0; + switch (orientation) { + case LEFT: { + mHandleDrawable = mHandleLeftDrawable; + handleWidth = mHandleDrawable.getIntrinsicWidth(); + mHotspotX = (handleWidth * 3) / (float) 4; + break; + } + + case RIGHT: { + mHandleDrawable = mHandleRightDrawable; + handleWidth = mHandleDrawable.getIntrinsicWidth(); + mHotspotX = handleWidth / (float) 4; + break; + } + } + + mHandleHeight = mHandleDrawable.getIntrinsicHeight(); + + mHandleWidth = handleWidth; + mTouchOffsetY = -mHandleHeight * 0.3f; + mHotspotY = 0; + invalidate(); + } + + public void show() { + if (!isPositionVisible()) { + hide(); + return; + } + + // We remove handle from its parent first otherwise the following exception may be thrown + // java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first. + removeFromParent(); + + initHandle(); // init the handle + invalidate(); // invalidate to make sure onDraw is called + + final int[] coords = mTempCoords; + terminalView.getLocationInWindow(coords); + coords[0] += mPointX; + coords[1] += mPointY; + + if(mHandle != null) + mHandle.showAtLocation(terminalView, 0, coords[0], coords[1]); + } + + public void hide() { + mIsDragging = false; + + if(mHandle != null) { + mHandle.dismiss(); + + // We remove handle from its parent, otherwise it may still be shown in some cases even after the dismiss call + removeFromParent(); + mHandle = null; // garbage collect the handle + } + invalidate(); + } + + public void removeFromParent() { + if(!isParentNull()) { + ((ViewGroup)this.getParent()).removeView(this); + } + } + + public void positionAtCursor(final int cx, final int cy, boolean forceOrientationCheck) { + int x = terminalView.getPointX(cx); + int y = terminalView.getPointY(cy + 1); + moveTo(x, y, forceOrientationCheck); + } + + private void moveTo(int x, int y, boolean forceOrientationCheck) { + float oldHotspotX = mHotspotX; + checkChangedOrientation(x, forceOrientationCheck); + mPointX = (int) (x - (isShowing() ? oldHotspotX : mHotspotX)); + mPointY = y; + + if (isPositionVisible()) { + int[] coords = null; + + if (isShowing()) { + coords = mTempCoords; + terminalView.getLocationInWindow(coords); + int x1 = coords[0] + mPointX; + int y1 = coords[1] + mPointY; + if (mHandle != null) + mHandle.update(x1, y1, getWidth(), getHeight()); + } else { + show(); + } + + if (mIsDragging) { + if (coords == null) { + coords = mTempCoords; + terminalView.getLocationInWindow(coords); + } + if (coords[0] != mLastParentX || coords[1] != mLastParentY) { + mTouchToWindowOffsetX += coords[0] - mLastParentX; + mTouchToWindowOffsetY += coords[1] - mLastParentY; + mLastParentX = coords[0]; + mLastParentY = coords[1]; + } + } + } else { + hide(); + } + } + + public void changeOrientation(int orientation) { + if (mOrientation != orientation) { + setOrientation(orientation); + } + } + + private void checkChangedOrientation(int posX, boolean force) { + if (!mIsDragging && !force) { + return; + } + long millis = SystemClock.currentThreadTimeMillis(); + if (millis - mLastTime < 50 && !force) { + return; + } + mLastTime = millis; + + final TerminalView hostView = terminalView; + final int left = hostView.getLeft(); + final int right = hostView.getWidth(); + final int top = hostView.getTop(); + final int bottom = hostView.getHeight(); + + if (mTempRect == null) { + mTempRect = new Rect(); + } + final Rect clip = mTempRect; + clip.left = left + terminalView.getPaddingLeft(); + clip.top = top + terminalView.getPaddingTop(); + clip.right = right - terminalView.getPaddingRight(); + clip.bottom = bottom - terminalView.getPaddingBottom(); + + final ViewParent parent = hostView.getParent(); + if (parent == null || !parent.getChildVisibleRect(hostView, clip, null)) { + return; + } + + if (posX - mHandleWidth < clip.left) { + changeOrientation(RIGHT); + } else if (posX + mHandleWidth > clip.right) { + changeOrientation(LEFT); + } else { + changeOrientation(mInitialOrientation); + } + } + + private boolean isPositionVisible() { + // Always show a dragging handle. + if (mIsDragging) { + return true; + } + + final TerminalView hostView = terminalView; + final int left = 0; + final int right = hostView.getWidth(); + final int top = 0; + final int bottom = hostView.getHeight(); + + if (mTempRect == null) { + mTempRect = new Rect(); + } + final Rect clip = mTempRect; + clip.left = left + terminalView.getPaddingLeft(); + clip.top = top + terminalView.getPaddingTop(); + clip.right = right - terminalView.getPaddingRight(); + clip.bottom = bottom - terminalView.getPaddingBottom(); + + final ViewParent parent = hostView.getParent(); + if (parent == null || !parent.getChildVisibleRect(hostView, clip, null)) { + return false; + } + + final int[] coords = mTempCoords; + hostView.getLocationInWindow(coords); + final int posX = coords[0] + mPointX + (int) mHotspotX; + final int posY = coords[1] + mPointY + (int) mHotspotY; + + return posX >= clip.left && posX <= clip.right && + posY >= clip.top && posY <= clip.bottom; + } + + @Override + public void onDraw(Canvas c) { + final int width = mHandleDrawable.getIntrinsicWidth(); + int height = mHandleDrawable.getIntrinsicHeight(); + mHandleDrawable.setBounds(0, 0, width, height); + mHandleDrawable.draw(c); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouchEvent(MotionEvent event) { + terminalView.updateFloatingToolbarVisibility(event); + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + final float rawX = event.getRawX(); + final float rawY = event.getRawY(); + mTouchToWindowOffsetX = rawX - mPointX; + mTouchToWindowOffsetY = rawY - mPointY; + final int[] coords = mTempCoords; + terminalView.getLocationInWindow(coords); + mLastParentX = coords[0]; + mLastParentY = coords[1]; + mIsDragging = true; + break; + } + + case MotionEvent.ACTION_MOVE: { + final float rawX = event.getRawX(); + final float rawY = event.getRawY(); + + final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX; + final float newPosY = rawY - mTouchToWindowOffsetY + mHotspotY + mTouchOffsetY; + + mCursorController.updatePosition(this, Math.round(newPosX), Math.round(newPosY)); + break; + } + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mIsDragging = false; + } + return true; + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(mHandleDrawable.getIntrinsicWidth(), + mHandleDrawable.getIntrinsicHeight()); + } + + public int getHandleHeight() { + return mHandleHeight; + } + + public int getHandleWidth() { + return mHandleWidth; + } + + public boolean isShowing() { + if (mHandle != null) + return mHandle.isShowing(); + else + return false; + } + + public boolean isParentNull() { + return this.getParent() == null; + } + + public boolean isDragging() { + return mIsDragging; + } + +} From 395759cc0aea9a6c2f89142cbfcb18bbf2ee3503 Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Sat, 6 Mar 2021 18:27:57 +0500 Subject: [PATCH 011/136] Restore keyboard input into terminal view when toggling extra-keys slider input with VOL_UP+q Check #1420 for details. --- app/src/main/java/com/termux/app/TermuxViewClient.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/termux/app/TermuxViewClient.java b/app/src/main/java/com/termux/app/TermuxViewClient.java index 3dcc406c..ec962a88 100644 --- a/app/src/main/java/com/termux/app/TermuxViewClient.java +++ b/app/src/main/java/com/termux/app/TermuxViewClient.java @@ -212,6 +212,7 @@ public final class TermuxViewClient implements TerminalViewClient { case 'q': case 'k': mActivity.toggleShowExtraKeys(); + mVirtualFnKeyDown=false; // force disable fn key down to restore keyboard input into terminal view, fixes termux/termux-app#1420 break; } From 14c49867f706c12b27d007ccede1c799041e857f Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Mon, 8 Mar 2021 15:09:22 +0500 Subject: [PATCH 012/136] Define `TermuxConstants` class to store all shared constants of Termux app and its plugins This commit removes almost all hardcoded paths in Termux app and moves the references to the `TermuxConstants` class. The `TermuxConstants` class should be imported by other termux plugin apps instead of copying and defining their own constants. The 3rd party apps can also import it for interacting with termux apps. App and sub class specific constants are defined in their own nested classes to keep them segregated from each other and for better readability. --- app/build.gradle | 9 + app/src/main/AndroidManifest.xml | 33 +-- .../java/com/termux/app/BackgroundJob.java | 18 +- .../com/termux/app/RunCommandService.java | 34 ++- .../java/com/termux/app/TermuxActivity.java | 25 +- .../java/com/termux/app/TermuxConstants.java | 232 ++++++++++++++++++ .../java/com/termux/app/TermuxInstaller.java | 6 +- .../com/termux/app/TermuxOpenReceiver.java | 2 +- .../com/termux/app/TermuxPreferences.java | 8 +- .../java/com/termux/app/TermuxService.java | 54 ++-- .../filepicker/TermuxDocumentsProvider.java | 6 +- .../TermuxFileReceiverActivity.java | 20 +- app/src/main/res/values/strings.xml | 26 +- app/src/main/res/xml/shortcuts.xml | 8 + 14 files changed, 361 insertions(+), 120 deletions(-) create mode 100644 app/src/main/java/com/termux/app/TermuxConstants.java diff --git a/app/build.gradle b/app/build.gradle index 24b58825..3d519ffa 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,6 +21,15 @@ android { versionCode 108 versionName "0.108" + manifestPlaceholders.TERMUX_PACKAGE_NAME = "com.termux" + manifestPlaceholders.TERMUX_APP_NAME = "Termux" + manifestPlaceholders.TERMUX_API_APP_NAME = "Termux:API" + manifestPlaceholders.TERMUX_BOOT_APP_NAME = "Termux:Boot" + manifestPlaceholders.TERMUX_FLOAT_APP_NAME = "Termux:Float" + manifestPlaceholders.TERMUX_STYLING_APP_NAME = "Termux:Styling" + manifestPlaceholders.TERMUX_TASKER_APP_NAME = "Termux:Tasker" + manifestPlaceholders.TERMUX_WIDGET_APP_NAME = "Termux:Widget" + externalNativeBuild { ndkBuild { cFlags "-std=c11", "-Wall", "-Wextra", "-Werror", "-Os", "-fno-stack-protector", "-Wl,--gc-sections" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 59324ec7..0a2e70a6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,13 +2,13 @@ xmlns:tools="http://schemas.android.com/tools" package="com.termux" android:installLocation="internalOnly" - android:sharedUserId="com.termux" + android:sharedUserId="${TERMUX_PACKAGE_NAME}" android:sharedUserLabel="@string/shared_user_label" > - @@ -112,7 +113,7 @@ @@ -122,25 +123,25 @@ + android:permission="${TERMUX_PACKAGE_NAME}.permission.RUN_COMMAND" > - + - + android:name=".app.TermuxOpenReceiver$ContentProvider" /> diff --git a/app/src/main/java/com/termux/app/BackgroundJob.java b/app/src/main/java/com/termux/app/BackgroundJob.java index faf0089b..bb4e19a8 100644 --- a/app/src/main/java/com/termux/app/BackgroundJob.java +++ b/app/src/main/java/com/termux/app/BackgroundJob.java @@ -36,7 +36,7 @@ public final class BackgroundJob { public BackgroundJob(String cwd, String fileToExecute, final String[] args, final TermuxService service, PendingIntent pendingIntent) { String[] env = buildEnvironment(false, cwd); - if (cwd == null || cwd.isEmpty()) cwd = TermuxService.HOME_PATH; + if (cwd == null || cwd.isEmpty()) cwd = TermuxConstants.HOME_PATH; final String[] progArray = setupProcessArgs(fileToExecute, args); final String processDescription = Arrays.toString(progArray); @@ -134,17 +134,17 @@ public final class BackgroundJob { } static String[] buildEnvironment(boolean failSafe, String cwd) { - new File(TermuxService.HOME_PATH).mkdirs(); + new File(TermuxConstants.HOME_PATH).mkdirs(); - if (cwd == null || cwd.isEmpty()) cwd = TermuxService.HOME_PATH; + if (cwd == null || cwd.isEmpty()) cwd = TermuxConstants.HOME_PATH; List environment = new ArrayList<>(); environment.add("TERMUX_VERSION=" + BuildConfig.VERSION_NAME); environment.add("TERM=xterm-256color"); environment.add("COLORTERM=truecolor"); - environment.add("HOME=" + TermuxService.HOME_PATH); - environment.add("PREFIX=" + TermuxService.PREFIX_PATH); + environment.add("HOME=" + TermuxConstants.HOME_PATH); + environment.add("PREFIX=" + TermuxConstants.PREFIX_PATH); environment.add("BOOTCLASSPATH=" + System.getenv("BOOTCLASSPATH")); environment.add("ANDROID_ROOT=" + System.getenv("ANDROID_ROOT")); environment.add("ANDROID_DATA=" + System.getenv("ANDROID_DATA")); @@ -164,9 +164,9 @@ public final class BackgroundJob { environment.add("PATH= " + System.getenv("PATH")); } else { environment.add("LANG=en_US.UTF-8"); - environment.add("PATH=" + TermuxService.PREFIX_PATH + "/bin"); + environment.add("PATH=" + TermuxConstants.PREFIX_PATH + "/bin"); environment.add("PWD=" + cwd); - environment.add("TMPDIR=" + TermuxService.PREFIX_PATH + "/tmp"); + environment.add("TMPDIR=" + TermuxConstants.PREFIX_PATH + "/tmp"); } return environment.toArray(new String[0]); @@ -215,7 +215,7 @@ public final class BackgroundJob { if (executable.startsWith("/usr") || executable.startsWith("/bin")) { String[] parts = executable.split("/"); String binary = parts[parts.length - 1]; - interpreter = TermuxService.PREFIX_PATH + "/bin/" + binary; + interpreter = TermuxConstants.PREFIX_PATH + "/bin/" + binary; } break; } @@ -225,7 +225,7 @@ public final class BackgroundJob { } } else { // No shebang and no ELF, use standard shell. - interpreter = TermuxService.PREFIX_PATH + "/bin/sh"; + interpreter = TermuxConstants.PREFIX_PATH + "/bin/sh"; } } } diff --git a/app/src/main/java/com/termux/app/RunCommandService.java b/app/src/main/java/com/termux/app/RunCommandService.java index 93b43cf6..eb6efc21 100644 --- a/app/src/main/java/com/termux/app/RunCommandService.java +++ b/app/src/main/java/com/termux/app/RunCommandService.java @@ -13,6 +13,8 @@ import android.os.IBinder; import android.util.Log; import com.termux.R; +import com.termux.app.TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE; +import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_SERVICE; import java.io.File; import java.io.FileInputStream; @@ -77,12 +79,6 @@ import java.util.Properties; */ public class RunCommandService extends Service { - public static final String RUN_COMMAND_ACTION = "com.termux.RUN_COMMAND"; - public static final String RUN_COMMAND_PATH = "com.termux.RUN_COMMAND_PATH"; - public static final String RUN_COMMAND_ARGUMENTS = "com.termux.RUN_COMMAND_ARGUMENTS"; - public static final String RUN_COMMAND_WORKDIR = "com.termux.RUN_COMMAND_WORKDIR"; - public static final String RUN_COMMAND_BACKGROUND = "com.termux.RUN_COMMAND_BACKGROUND"; - private static final String NOTIFICATION_CHANNEL_ID = "termux_run_command_notification_channel"; private static final int NOTIFICATION_ID = 1338; @@ -108,7 +104,7 @@ public class RunCommandService extends Service { runStartForeground(); // If wrong action passed, then just return - if (!RUN_COMMAND_ACTION.equals(intent.getAction())) { + if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) { Log.e("termux", "Unexpected intent action to RunCommandService: " + intent.getAction()); return Service.START_NOT_STICKY; } @@ -119,16 +115,16 @@ public class RunCommandService extends Service { return Service.START_NOT_STICKY; } - Uri programUri = new Uri.Builder().scheme("com.termux.file").path(getExpandedTermuxPath(intent.getStringExtra(RUN_COMMAND_PATH))).build(); + Uri programUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(getExpandedTermuxPath(intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH))).build(); - Intent execIntent = new Intent(TermuxService.ACTION_EXECUTE, programUri); + Intent execIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, programUri); execIntent.setClass(this, TermuxService.class); - execIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, intent.getStringArrayExtra(RUN_COMMAND_ARGUMENTS)); - execIntent.putExtra(TermuxService.EXTRA_EXECUTE_IN_BACKGROUND, intent.getBooleanExtra(RUN_COMMAND_BACKGROUND, false)); + execIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, intent.getStringArrayExtra(RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS)); + execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false)); - String workingDirectory = intent.getStringExtra(RUN_COMMAND_WORKDIR); + String workingDirectory = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_WORKDIR); if (workingDirectory != null && !workingDirectory.isEmpty()) { - execIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, getExpandedTermuxPath(workingDirectory)); + execIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, getExpandedTermuxPath(workingDirectory)); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -188,9 +184,9 @@ public class RunCommandService extends Service { } private boolean allowExternalApps() { - File propsFile = new File(TermuxService.HOME_PATH + "/.termux/termux.properties"); + File propsFile = new File(TermuxConstants.TERMUX_PROPERTIES_PRIMARY_PATH); if (!propsFile.exists()) - propsFile = new File(TermuxService.HOME_PATH + "/.config/termux/termux.properties"); + propsFile = new File(TermuxConstants.TERMUX_PROPERTIES_SECONDARY_PATH); Properties props = new Properties(); try { @@ -209,10 +205,10 @@ public class RunCommandService extends Service { /** Replace "$PREFIX/" or "~/" prefix with termux absolute paths */ public static String getExpandedTermuxPath(String path) { if(path != null && !path.isEmpty()) { - path = path.replaceAll("^\\$PREFIX$", TermuxService.PREFIX_PATH); - path = path.replaceAll("^\\$PREFIX/", TermuxService.PREFIX_PATH + "/"); - path = path.replaceAll("^~/$", TermuxService.HOME_PATH); - path = path.replaceAll("^~/", TermuxService.HOME_PATH + "/"); + path = path.replaceAll("^\\$PREFIX$", TermuxConstants.PREFIX_PATH); + path = path.replaceAll("^\\$PREFIX/", TermuxConstants.PREFIX_PATH + "/"); + path = path.replaceAll("^~/$", TermuxConstants.HOME_PATH); + path = path.replaceAll("^~/", TermuxConstants.HOME_PATH + "/"); } return path; diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java index b5d87681..a6e1d985 100644 --- a/app/src/main/java/com/termux/app/TermuxActivity.java +++ b/app/src/main/java/com/termux/app/TermuxActivity.java @@ -2,7 +2,6 @@ package com.termux.app; import android.Manifest; import android.annotation.SuppressLint; -import android.annotation.TargetApi; import android.app.Activity; import android.app.AlertDialog; import android.content.ActivityNotFoundException; @@ -48,6 +47,7 @@ import android.widget.TextView; import android.widget.Toast; import com.termux.R; +import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY; import com.termux.terminal.EmulatorDebug; import com.termux.terminal.TerminalColors; import com.termux.terminal.TerminalSession; @@ -84,8 +84,6 @@ import androidx.viewpager.widget.ViewPager; */ public final class TermuxActivity extends Activity implements ServiceConnection { - public static final String TERMUX_FAILSAFE_SESSION_ACTION = "com.termux.app.failsafe_session"; - private static final int CONTEXTMENU_SELECT_URL_ID = 0; private static final int CONTEXTMENU_SHARE_TRANSCRIPT_ID = 1; private static final int CONTEXTMENU_PASTE_ID = 3; @@ -100,9 +98,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection private static final int REQUESTCODE_PERMISSION_STORAGE = 1234; - private static final String RELOAD_STYLE_ACTION = "com.termux.app.reload_style"; - private static final String BROADCAST_TERMUX_OPENED = "com.termux.app.OPENED"; + private static final String BROADCAST_TERMUX_OPENED = TermuxConstants.TERMUX_PACKAGE_NAME + ".app.OPENED"; /** The main view of the activity showing the terminal. Initialized in onCreate(). */ @SuppressWarnings("NullableProblems") @@ -145,7 +142,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection @Override public void onReceive(Context context, Intent intent) { if (mIsVisible) { - String whatToReload = intent.getStringExtra(RELOAD_STYLE_ACTION); + String whatToReload = intent.getStringExtra(TERMUX_ACTIVITY.EXTRA_RELOAD_STYLE); if ("storage".equals(whatToReload)) { if (ensureStoragePermissionGranted()) TermuxInstaller.setupStorageSymlinks(TermuxActivity.this); @@ -163,8 +160,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection void checkForFontAndColors() { try { - @SuppressLint("SdCardPath") File fontFile = new File("/data/data/com.termux/files/home/.termux/font.ttf"); - @SuppressLint("SdCardPath") File colorsFile = new File("/data/data/com.termux/files/home/.termux/colors.properties"); + File colorsFile = new File(TermuxConstants.COLOR_PROPERTIES_PATH); + File fontFile = new File(TermuxConstants.FONT_PATH); final Properties props = new Properties(); if (colorsFile.isFile()) { @@ -541,7 +538,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection Bundle bundle = getIntent().getExtras(); boolean launchFailsafe = false; if (bundle != null) { - launchFailsafe = bundle.getBoolean(TERMUX_FAILSAFE_SESSION_ACTION, false); + launchFailsafe = bundle.getBoolean(TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION, false); } addNewSession(launchFailsafe, null); } catch (WindowManager.BadTokenException e) { @@ -556,7 +553,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection Intent i = getIntent(); if (i != null && Intent.ACTION_RUN.equals(i.getAction())) { // Android 7.1 app shortcut from res/xml/shortcuts.xml. - boolean failSafe = i.getBooleanExtra(TERMUX_FAILSAFE_SESSION_ACTION, false); + boolean failSafe = i.getBooleanExtra(TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION, false); addNewSession(failSafe, null); } else { switchToSession(getStoredCurrentSessionOrLast()); @@ -612,7 +609,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection mListViewAdapter.notifyDataSetChanged(); } - registerReceiver(mBroadcastReceiever, new IntentFilter(RELOAD_STYLE_ACTION)); + registerReceiver(mBroadcastReceiever, new IntentFilter(TERMUX_ACTIVITY.ACTION_RELOAD_STYLE)); // The current terminal session may have changed while being away, force // a refresh of the displayed terminal: @@ -914,14 +911,14 @@ public final class TermuxActivity extends Activity implements ServiceConnection } case CONTEXTMENU_STYLING_ID: { Intent stylingIntent = new Intent(); - stylingIntent.setClassName("com.termux.styling", "com.termux.styling.TermuxStyleActivity"); + stylingIntent.setClassName(TermuxConstants.TERMUX_STYLING_PACKAGE_NAME, TermuxConstants.TERMUX_STYLING.TERMUX_STYLING_ACTIVITY_NAME); try { startActivity(stylingIntent); } catch (ActivityNotFoundException | IllegalArgumentException e) { // The startActivity() call is not documented to throw IllegalArgumentException. // However, crash reporting shows that it sometimes does, so catch it here. - new AlertDialog.Builder(this).setMessage(R.string.styling_not_installed) - .setPositiveButton(R.string.styling_install, (dialog, which) -> startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://f-droid.org/en/packages/com.termux.styling/")))).setNegativeButton(android.R.string.cancel, null).show(); + new AlertDialog.Builder(this).setMessage(getString(R.string.styling_not_installed)) + .setPositiveButton(R.string.styling_install, (dialog, which) -> startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://f-droid.org/en/packages/" + TermuxConstants.TERMUX_STYLING_PACKAGE_NAME + " /")))).setNegativeButton(android.R.string.cancel, null).show(); } return true; } diff --git a/app/src/main/java/com/termux/app/TermuxConstants.java b/app/src/main/java/com/termux/app/TermuxConstants.java new file mode 100644 index 00000000..cf4da908 --- /dev/null +++ b/app/src/main/java/com/termux/app/TermuxConstants.java @@ -0,0 +1,232 @@ +package com.termux.app; + +import android.annotation.SuppressLint; + +import java.io.File; + +// Version: v0.1.0 + +/** + * A class that defines shared constants of the Termux app and its plugins. + * This class will be hosted by termux-app and should be imported by other termux plugin apps as is + * instead of copying constants to random classes. The 3rd party apps can also import it for + * interacting with termux apps. + * + * Termux app default package name is "com.termux" and is used in PREFIX_PATH. + * The binaries compiled for termux have PREFIX_PATH hardcoded in them but it can be changed during + * compilation. + * + * The TERMUX_PACKAGE_NAME must be the same as the applicationId of termux-app build.gradle since + * its also used by FILES_PATH. + * If TERMUX_PACKAGE_NAME is changed, then binaries, specially used in bootstrap need to be compiled + * appropriately. Check https://github.com/termux/termux-packages/wiki/Building-packages for more info. + * + * Ideally the only places where changes should be required if changing package name are the following: + * - The TERMUX_PACKAGE_NAME in TermuxConstants. + * - The "applicationId" in "build.gradle". This is package name that android and app stores will + * use and is also the final package name stored in "AndroidManifest.xml". + * - The "manifestPlaceholders" values for TERMUX_PACKAGE_NAME and *_APP_NAME in "build.gradle". + * - The "ENTITY" values for TERMUX_PACKAGE_NAME and *_APP_NAME in "strings.xml". + * - The "shortcut.xml" files like in termux-app since dynamic variables don't work in it. + * - Optionally the "package" in "AndroidManifest.xml" if modifying project structure. This is + * package name for java classes project structure and is prefixed if activity and service + * names use dot (.) notation. + * - Optionally the *_PATH variables in TermuxConstants containing the string "termux". + * + * Check https://developer.android.com/studio/build/application-id for info on "package" in + * "AndroidManifest.xml" and "applicationId" in "build.gradle". + * + * TERMUX_PACKAGE_NAME must be used in source code of Termux app and its plugins instead of hardcoded + * "com.termux" paths. + */ + +public final class TermuxConstants { + + /** + * Termux app and plugin app and package names. + */ + + public static final String TERMUX_APP_NAME = "Termux"; // Default: "Termux" + public static final String TERMUX_PACKAGE_NAME = "com.termux"; // Default: "com.termux" + + public static final String TERMUX_API_APP_NAME = "Termux:API"; // Default: "Termux:API" + public static final String TERMUX_API_PACKAGE_NAME = TERMUX_PACKAGE_NAME + ".api"; // Default: "com.termux.api" + + public static final String TERMUX_BOOT_APP_NAME = "Termux:Boot"; // Default: "Termux:Boot" + public static final String TERMUX_BOOT_PACKAGE_NAME = TERMUX_PACKAGE_NAME + ".boot"; // Default: "com.termux.boot" + + public static final String TERMUX_FLOAT_APP_NAME = "Termux:Float"; // Default: "Termux:Float" + public static final String TERMUX_FLOAT_PACKAGE_NAME = TERMUX_PACKAGE_NAME + ".window"; // Default: "com.termux.window" + + public static final String TERMUX_STYLING_APP_NAME = "Termux:Styling"; // Default: "Termux:Styling" + public static final String TERMUX_STYLING_PACKAGE_NAME = TERMUX_PACKAGE_NAME + ".styling"; // Default: "com.termux.styling" + + public static final String TERMUX_TASKER_APP_NAME = "Termux:Tasker"; // Default: "Termux:Tasker" + public static final String TERMUX_TASKER_PACKAGE_NAME = TERMUX_PACKAGE_NAME + ".tasker"; // Default: "com.termux.tasker" + + public static final String TERMUX_WIDGET_APP_NAME = "Termux:Widget"; // Default: "Termux:Widget" + public static final String TERMUX_WIDGET_PACKAGE_NAME = TERMUX_PACKAGE_NAME + ".widget"; // Default: "com.termux.widget" + + + + /** + * Termux app core paths. + */ + + @SuppressLint("SdCardPath") + public static final String FILES_PATH = "/data/data/" + TERMUX_PACKAGE_NAME + "/files"; // Default: "/data/data/com.termux/files" + public static final String PREFIX_PATH = FILES_PATH + "/usr"; // Termux $PREFIX path. Default: "/data/data/com.termux/files/usr" + public static final String HOME_PATH = FILES_PATH + "/home"; // Termux $HOME path. Default: "/data/data/com.termux/files/home" + public static final String DATA_HOME_PATH = HOME_PATH + "/.termux"; // Default: "/data/data/com.termux/files/home/.termux" + public static final String CONFIG_HOME_PATH = HOME_PATH + "/.config/termux"; // Default: "/data/data/com.termux/files/home/.config/termux" + + + + /** + * Termux app plugin specific paths. + */ + + // Path to store scripts to be run at boot by Termux:Boot + public static final String BOOT_SCRIPTS_PATH = DATA_HOME_PATH + "/boot"; // Default: "/data/data/com.termux/files/home/.termux/boot" + + // Path to store foreground scripts that can be run by the termux launcher widget provided by Termux:Widget + public static final String SHORTCUT_SCRIPTS_PATH = DATA_HOME_PATH + "/shortcuts"; // Default: "/data/data/com.termux/files/home/.termux/shortcuts" + + // Path to store background scripts that can be run by the termux launcher widget provided by Termux:Widget + public static final String SHORTCUT_TASKS_SCRIPTS_PATH = DATA_HOME_PATH + "/shortcuts/tasks"; // Default: "/data/data/com.termux/files/home/.termux/shortcuts/tasks" + + // Path to store scripts to be run by 3rd party twofortyfouram locale plugin host apps like Tasker app via the Termux:Tasker plugin client + public static final String TASKER_SCRIPTS_PATH = DATA_HOME_PATH + "/tasker"; // Default: "/data/data/com.termux/files/home/.termux/tasker" + + // Termux app termux.properties primary path + public static final String TERMUX_PROPERTIES_PRIMARY_PATH = DATA_HOME_PATH + "/termux.properties"; // Default: "/data/data/com.termux/files/home/.termux/termux.properties" + + // Termux app termux.properties secondary path + public static final String TERMUX_PROPERTIES_SECONDARY_PATH = CONFIG_HOME_PATH + "/termux.properties"; // Default: "/data/data/com.termux/files/home/.config/termux/termux.properties" + + // Termux app and Termux:Styling colors.properties path + public static final String COLOR_PROPERTIES_PATH = DATA_HOME_PATH + "/colors.properties"; // Default: "/data/data/com.termux/files/home/.termux/colors.properties" + + // Termux app and Termux:Styling font.ttf path + public static final String FONT_PATH = DATA_HOME_PATH + "/font.ttf"; // Default: "/data/data/com.termux/files/home/.termux/font.ttf" + + + + /** + * Termux app plugin specific path File objects. + */ + + public static final File FILES_DIR = new File(FILES_PATH); + public static final File PREFIX_DIR = new File(PREFIX_PATH); + public static final File HOME_DIR = new File(HOME_PATH); + public static final File DATA_HOME_DIR = new File(DATA_HOME_PATH); + public static final File CONFIG_HOME_DIR = new File(CONFIG_HOME_PATH); + public static final File BOOT_SCRIPTS_DIR = new File(BOOT_SCRIPTS_PATH); + public static final File SHORTCUT_SCRIPTS_DIR = new File(SHORTCUT_SCRIPTS_PATH); + public static final File SHORTCUT_TASKS_SCRIPTS_DIR = new File(SHORTCUT_TASKS_SCRIPTS_PATH); + public static final File TASKER_SCRIPTS_DIR = new File(TASKER_SCRIPTS_PATH); + + + + // Android OS permission declared by Termux app in AndroidManifest.xml which can be requested by 3rd party apps to run various commands in Termux app context + public static final String PERMISSION_RUN_COMMAND = TERMUX_PACKAGE_NAME + ".permission.RUN_COMMAND"; // Default: "com.termux.permission.RUN_COMMAND" + + // Termux property defined in termux.properties file as a secondary check to PERMISSION_RUN_COMMAND to allow 3rd party apps to run various commands in Termux app context + public static final String PROP_ALLOW_EXTERNAL_APPS = "allow-external-apps"; // Default: "allow-external-apps" + public static final String PROP_DEFAULT_VALUE_ALLOW_EXTERNAL_APPS = "false"; // Default: "false" + + + /** + * Termux app constants. + */ + + public static final class TERMUX_APP { + + /** + * Termux app core activity. + */ + + public static final String TERMUX_ACTIVITY_NAME = TERMUX_PACKAGE_NAME + ".app.TermuxActivity"; // Default: "com.termux.app.TermuxActivity" + public static final class TERMUX_ACTIVITY { + + // Intent action to start termux failsafe session + public static final String ACTION_FAILSAFE_SESSION = TermuxConstants.TERMUX_PACKAGE_NAME + ".app.failsafe_session"; // Default: "com.termux.app.failsafe_session" + + // Intent action to make termux reload its termux session styling + public static final String ACTION_RELOAD_STYLE = TermuxConstants.TERMUX_PACKAGE_NAME + ".app.reload_style"; // Default: "com.termux.app.reload_style" + // Intent extra for what to reload for the TERMUX_ACTIVITY.ACTION_RELOAD_STYLE intent + public static final String EXTRA_RELOAD_STYLE = TermuxConstants.TERMUX_PACKAGE_NAME + ".app.reload_style"; // Default: "com.termux.app.reload_style" + + } + + + + /** + * Termux app core service. + */ + + public static final String TERMUX_SERVICE_NAME = TERMUX_PACKAGE_NAME + ".app.TermuxService"; // Default: "com.termux.app.TermuxService" + public static final class TERMUX_SERVICE { + + // Intent action to stop TERMUX_SERVICE + public static final String ACTION_STOP_SERVICE = TERMUX_PACKAGE_NAME + ".service_stop"; // Default: "com.termux.service_stop" + + // Intent action to make TERMUX_SERVICE acquire a wakelock + public static final String ACTION_WAKE_LOCK = TERMUX_PACKAGE_NAME + ".service_wake_lock"; // Default: "com.termux.service_wake_lock" + + // Intent action to make TERMUX_SERVICE release wakelock + public static final String ACTION_WAKE_UNLOCK = TERMUX_PACKAGE_NAME + ".service_wake_unlock"; // Default: "com.termux.service_wake_unlock" + + // Intent action to execute command with TERMUX_SERVICE + public static final String ACTION_SERVICE_EXECUTE = TERMUX_PACKAGE_NAME + ".service_execute"; // Default: "com.termux.service_execute" + // Uri scheme for paths sent via intent to TERMUX_SERVICE + public static final String URI_SCHEME_SERVICE_EXECUTE = TERMUX_PACKAGE_NAME + ".file"; // Default: "com.termux.file" + // Intent extra for command arguments for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent + public static final String EXTRA_ARGUMENTS = TERMUX_PACKAGE_NAME + ".execute.arguments"; // Default: "com.termux.execute.arguments" + // Intent extra for command current working directory for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent + public static final String EXTRA_WORKDIR = TERMUX_PACKAGE_NAME + ".execute.cwd"; // Default: "com.termux.execute.cwd" + // Intent extra for command background mode for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent + public static final String EXTRA_BACKGROUND = TERMUX_PACKAGE_NAME + ".execute.background"; // Default: "com.termux.execute.background" + + } + + + + /** + * Termux app service to receive commands sent by 3rd party apps. + */ + + public static final String RUN_COMMAND_SERVICE_NAME = TERMUX_PACKAGE_NAME + ".app.RunCommandService"; // Termux app service to receive commands from 3rd party apps "com.termux.app.RunCommandService" + public static final class RUN_COMMAND_SERVICE { + + // Intent action to execute command with RUN_COMMAND_SERVICE + public static final String ACTION_RUN_COMMAND = TERMUX_PACKAGE_NAME + ".RUN_COMMAND"; // Default: "com.termux.RUN_COMMAND" + // Intent extra for command path for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent + public static final String EXTRA_COMMAND_PATH = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_PATH"; // Default: "com.termux.RUN_COMMAND_PATH" + // Intent extra for command arguments for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent + public static final String EXTRA_ARGUMENTS = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_ARGUMENTS"; // Default: "com.termux.RUN_COMMAND_ARGUMENTS" + // Intent extra for command current working directory for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent + public static final String EXTRA_WORKDIR = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_WORKDIR"; // Default: "com.termux.RUN_COMMAND_WORKDIR" + // Intent extra for command background mode for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent + public static final String EXTRA_BACKGROUND = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_BACKGROUND"; // Default: "com.termux.RUN_COMMAND_BACKGROUND" + + } + } + + + + /** + * Termux:Styling app constants. + */ + + public static final class TERMUX_STYLING { + + /** + * Termux:Styling app core activity constants. + */ + + public static final String TERMUX_STYLING_ACTIVITY_NAME = TERMUX_STYLING_PACKAGE_NAME + ".TermuxStyleActivity"; // Default: "com.termux.styling.TermuxStyleActivity" + + } + +} diff --git a/app/src/main/java/com/termux/app/TermuxInstaller.java b/app/src/main/java/com/termux/app/TermuxInstaller.java index 6e50b22d..390eff12 100644 --- a/app/src/main/java/com/termux/app/TermuxInstaller.java +++ b/app/src/main/java/com/termux/app/TermuxInstaller.java @@ -58,7 +58,7 @@ final class TermuxInstaller { return; } - final File PREFIX_FILE = new File(TermuxService.PREFIX_PATH); + final File PREFIX_FILE = new File(TermuxConstants.PREFIX_PATH); if (PREFIX_FILE.isDirectory()) { whenDone.run(); return; @@ -69,7 +69,7 @@ final class TermuxInstaller { @Override public void run() { try { - final String STAGING_PREFIX_PATH = TermuxService.FILES_PATH + "/usr-staging"; + final String STAGING_PREFIX_PATH = TermuxConstants.FILES_PATH + "/usr-staging"; final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH); if (STAGING_PREFIX_FILE.exists()) { @@ -194,7 +194,7 @@ final class TermuxInstaller { new Thread() { public void run() { try { - File storageDir = new File(TermuxService.HOME_PATH, "storage"); + File storageDir = new File(TermuxConstants.HOME_PATH, "storage"); if (storageDir.exists()) { try { diff --git a/app/src/main/java/com/termux/app/TermuxOpenReceiver.java b/app/src/main/java/com/termux/app/TermuxOpenReceiver.java index 6b8bf227..26f5fec9 100644 --- a/app/src/main/java/com/termux/app/TermuxOpenReceiver.java +++ b/app/src/main/java/com/termux/app/TermuxOpenReceiver.java @@ -178,7 +178,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver { String path = file.getCanonicalPath(); String storagePath = Environment.getExternalStorageDirectory().getCanonicalPath(); // See https://support.google.com/faqs/answer/7496913: - if (!(path.startsWith(TermuxService.FILES_PATH) || path.startsWith(storagePath))) { + if (!(path.startsWith(TermuxConstants.FILES_PATH) || path.startsWith(storagePath))) { throw new IllegalArgumentException("Invalid path: " + path); } } catch (IOException e) { diff --git a/app/src/main/java/com/termux/app/TermuxPreferences.java b/app/src/main/java/com/termux/app/TermuxPreferences.java index d3df7fc3..f4187eef 100644 --- a/app/src/main/java/com/termux/app/TermuxPreferences.java +++ b/app/src/main/java/com/termux/app/TermuxPreferences.java @@ -170,9 +170,9 @@ final class TermuxPreferences { } void reloadFromProperties(Context context) { - File propsFile = new File(TermuxService.HOME_PATH + "/.termux/termux.properties"); + File propsFile = new File(TermuxConstants.TERMUX_PROPERTIES_PRIMARY_PATH); if (!propsFile.exists()) - propsFile = new File(TermuxService.HOME_PATH + "/.config/termux/termux.properties"); + propsFile = new File(TermuxConstants.TERMUX_PROPERTIES_SECONDARY_PATH); Properties props = new Properties(); try { @@ -213,12 +213,12 @@ final class TermuxPreferences { mUseFullScreen = "true".equals(props.getProperty("fullscreen", "false").toLowerCase()); mUseFullScreenWorkAround = "true".equals(props.getProperty("use-fullscreen-workaround", "false").toLowerCase()); - mDefaultWorkingDir = props.getProperty("default-working-directory", TermuxService.HOME_PATH); + mDefaultWorkingDir = props.getProperty("default-working-directory", TermuxConstants.HOME_PATH); File workDir = new File(mDefaultWorkingDir); if (!workDir.exists() || !workDir.isDirectory()) { // Fallback to home directory if user configured working directory is not exist // or is a regular file. - mDefaultWorkingDir = TermuxService.HOME_PATH; + mDefaultWorkingDir = TermuxConstants.HOME_PATH; } String defaultExtraKeys = "[[ESC, TAB, CTRL, ALT, {key: '-', popup: '|'}, DOWN, UP]]"; diff --git a/app/src/main/java/com/termux/app/TermuxService.java b/app/src/main/java/com/termux/app/TermuxService.java index 3cb714fa..854659dc 100644 --- a/app/src/main/java/com/termux/app/TermuxService.java +++ b/app/src/main/java/com/termux/app/TermuxService.java @@ -22,6 +22,8 @@ import android.util.Log; import android.widget.ArrayAdapter; import com.termux.R; +import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY; +import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_SERVICE; import com.termux.terminal.EmulatorDebug; import com.termux.terminal.TerminalSession; import com.termux.terminal.TerminalSession.SessionChangedCallback; @@ -45,26 +47,8 @@ import java.util.List; public final class TermuxService extends Service implements SessionChangedCallback { private static final String NOTIFICATION_CHANNEL_ID = "termux_notification_channel"; - - /** Note that this is a symlink on the Android M preview. */ - @SuppressLint("SdCardPath") - public static final String FILES_PATH = "/data/data/com.termux/files"; - public static final String PREFIX_PATH = FILES_PATH + "/usr"; - public static final String HOME_PATH = FILES_PATH + "/home"; - private static final int NOTIFICATION_ID = 1337; - private static final String ACTION_STOP_SERVICE = "com.termux.service_stop"; - private static final String ACTION_LOCK_WAKE = "com.termux.service_wake_lock"; - private static final String ACTION_UNLOCK_WAKE = "com.termux.service_wake_unlock"; - /** Intent action to launch a new terminal session. Executed from TermuxWidgetProvider. */ - public static final String ACTION_EXECUTE = "com.termux.service_execute"; - - public static final String EXTRA_ARGUMENTS = "com.termux.execute.arguments"; - - public static final String EXTRA_CURRENT_WORKING_DIRECTORY = "com.termux.execute.cwd"; - public static final String EXTRA_EXECUTE_IN_BACKGROUND = "com.termux.execute.background"; - /** This service is only bound from inside the same process and never uses IPC. */ class LocalBinder extends Binder { public final TermuxService service = TermuxService.this; @@ -91,19 +75,19 @@ public final class TermuxService extends Service implements SessionChangedCallba private PowerManager.WakeLock mWakeLock; private WifiManager.WifiLock mWifiLock; - /** If the user has executed the {@link #ACTION_STOP_SERVICE} intent. */ + /** If the user has executed the {@link TermuxConstants.TERMUX_APP.TERMUX_SERVICE#ACTION_STOP_SERVICE} intent. */ boolean mWantsToStop = false; @SuppressLint("Wakelock") @Override public int onStartCommand(Intent intent, int flags, int startId) { String action = intent.getAction(); - if (ACTION_STOP_SERVICE.equals(action)) { + if (TERMUX_SERVICE.ACTION_STOP_SERVICE.equals(action)) { mWantsToStop = true; for (int i = 0; i < mTerminalSessions.size(); i++) mTerminalSessions.get(i).finishIfRunning(); stopSelf(); - } else if (ACTION_LOCK_WAKE.equals(action)) { + } else if (TERMUX_SERVICE.ACTION_WAKE_LOCK.equals(action)) { if (mWakeLock == null) { PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, EmulatorDebug.LOG_TAG + ":service-wakelock"); @@ -130,7 +114,7 @@ public final class TermuxService extends Service implements SessionChangedCallba updateNotification(); } - } else if (ACTION_UNLOCK_WAKE.equals(action)) { + } else if (TERMUX_SERVICE.ACTION_WAKE_UNLOCK.equals(action)) { if (mWakeLock != null) { mWakeLock.release(); mWakeLock = null; @@ -140,19 +124,19 @@ public final class TermuxService extends Service implements SessionChangedCallba updateNotification(); } - } else if (ACTION_EXECUTE.equals(action)) { + } else if (TERMUX_SERVICE.ACTION_SERVICE_EXECUTE.equals(action)) { Uri executableUri = intent.getData(); String executablePath = (executableUri == null ? null : executableUri.getPath()); - String[] arguments = (executableUri == null ? null : intent.getStringArrayExtra(EXTRA_ARGUMENTS)); - String cwd = intent.getStringExtra(EXTRA_CURRENT_WORKING_DIRECTORY); + String[] arguments = (executableUri == null ? null : intent.getStringArrayExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS)); + String cwd = intent.getStringExtra(TERMUX_SERVICE.EXTRA_WORKDIR); - if (intent.getBooleanExtra(EXTRA_EXECUTE_IN_BACKGROUND, false)) { + if (intent.getBooleanExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, false)) { BackgroundJob task = new BackgroundJob(cwd, executablePath, arguments, this, intent.getParcelableExtra("pendingIntent")); mBackgroundTasks.add(task); updateNotification(); } else { - boolean failsafe = intent.getBooleanExtra(TermuxActivity.TERMUX_FAILSAFE_SESSION_ACTION, false); + boolean failsafe = intent.getBooleanExtra(TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION, false); TerminalSession newSession = createTermSession(executablePath, arguments, cwd, failsafe); // Transform executable path to session name, e.g. "/bin/do-something.sh" => "do something.sh". @@ -238,10 +222,10 @@ public final class TermuxService extends Service implements SessionChangedCallba } Resources res = getResources(); - Intent exitIntent = new Intent(this, TermuxService.class).setAction(ACTION_STOP_SERVICE); + Intent exitIntent = new Intent(this, TermuxService.class).setAction(TERMUX_SERVICE.ACTION_STOP_SERVICE); builder.addAction(android.R.drawable.ic_delete, res.getString(R.string.notification_action_exit), PendingIntent.getService(this, 0, exitIntent, 0)); - String newWakeAction = wakeLockHeld ? ACTION_UNLOCK_WAKE : ACTION_LOCK_WAKE; + String newWakeAction = wakeLockHeld ? TERMUX_SERVICE.ACTION_WAKE_UNLOCK : TERMUX_SERVICE.ACTION_WAKE_LOCK; Intent toggleWakeLockIntent = new Intent(this, TermuxService.class).setAction(newWakeAction); String actionTitle = res.getString(wakeLockHeld ? R.string.notification_action_wake_unlock : @@ -254,7 +238,7 @@ public final class TermuxService extends Service implements SessionChangedCallba @Override public void onDestroy() { - File termuxTmpDir = new File(TermuxService.PREFIX_PATH + "/tmp"); + File termuxTmpDir = new File(TermuxConstants.PREFIX_PATH + "/tmp"); if (termuxTmpDir.exists()) { try { @@ -280,9 +264,9 @@ public final class TermuxService extends Service implements SessionChangedCallba } TerminalSession createTermSession(String executablePath, String[] arguments, String cwd, boolean failSafe) { - new File(HOME_PATH).mkdirs(); + new File(TermuxConstants.HOME_PATH).mkdirs(); - if (cwd == null || cwd.isEmpty()) cwd = HOME_PATH; + if (cwd == null || cwd.isEmpty()) cwd = TermuxConstants.HOME_PATH; String[] env = BackgroundJob.buildEnvironment(failSafe, cwd); boolean isLoginShell = false; @@ -290,7 +274,7 @@ public final class TermuxService extends Service implements SessionChangedCallba if (executablePath == null) { if (!failSafe) { for (String shellBinary : new String[]{"login", "bash", "zsh"}) { - File shellFile = new File(PREFIX_PATH + "/bin/" + shellBinary); + File shellFile = new File(TermuxConstants.PREFIX_PATH + "/bin/" + shellBinary); if (shellFile.canExecute()) { executablePath = shellFile.getAbsolutePath(); break; @@ -320,8 +304,8 @@ public final class TermuxService extends Service implements SessionChangedCallba updateNotification(); // Make sure that terminal styling is always applied. - Intent stylingIntent = new Intent("com.termux.app.reload_style"); - stylingIntent.putExtra("com.termux.app.reload_style", "styling"); + Intent stylingIntent = new Intent(TERMUX_ACTIVITY.ACTION_RELOAD_STYLE); + stylingIntent.putExtra(TERMUX_ACTIVITY.EXTRA_RELOAD_STYLE, "styling"); sendBroadcast(stylingIntent); return session; diff --git a/app/src/main/java/com/termux/filepicker/TermuxDocumentsProvider.java b/app/src/main/java/com/termux/filepicker/TermuxDocumentsProvider.java index b72442ac..1a314e8c 100644 --- a/app/src/main/java/com/termux/filepicker/TermuxDocumentsProvider.java +++ b/app/src/main/java/com/termux/filepicker/TermuxDocumentsProvider.java @@ -12,7 +12,7 @@ import android.provider.DocumentsProvider; import android.webkit.MimeTypeMap; import com.termux.R; -import com.termux.app.TermuxService; +import com.termux.app.TermuxConstants; import java.io.File; import java.io.FileNotFoundException; @@ -35,7 +35,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider { private static final String ALL_MIME_TYPES = "*/*"; - private static final File BASE_DIR = new File(TermuxService.HOME_PATH); + private static final File BASE_DIR = new File(TermuxConstants.HOME_PATH); // The default columns to return information about a root if no specific @@ -171,7 +171,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider { // through the whole SD card). boolean isInsideHome; try { - isInsideHome = file.getCanonicalPath().startsWith(TermuxService.HOME_PATH); + isInsideHome = file.getCanonicalPath().startsWith(TermuxConstants.HOME_PATH); } catch (IOException e) { isInsideHome = true; } diff --git a/app/src/main/java/com/termux/filepicker/TermuxFileReceiverActivity.java b/app/src/main/java/com/termux/filepicker/TermuxFileReceiverActivity.java index e1ef5d42..c5493ae7 100644 --- a/app/src/main/java/com/termux/filepicker/TermuxFileReceiverActivity.java +++ b/app/src/main/java/com/termux/filepicker/TermuxFileReceiverActivity.java @@ -11,6 +11,8 @@ import android.util.Patterns; import com.termux.R; import com.termux.app.DialogUtils; +import com.termux.app.TermuxConstants; +import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_SERVICE; import com.termux.app.TermuxService; import java.io.ByteArrayInputStream; @@ -25,9 +27,9 @@ import java.util.regex.Pattern; public class TermuxFileReceiverActivity extends Activity { - static final String TERMUX_RECEIVEDIR = TermuxService.FILES_PATH + "/home/downloads"; - static final String EDITOR_PROGRAM = TermuxService.HOME_PATH + "/bin/termux-file-editor"; - static final String URL_OPENER_PROGRAM = TermuxService.HOME_PATH + "/bin/termux-url-opener"; + static final String TERMUX_RECEIVEDIR = TermuxConstants.FILES_PATH + "/home/downloads"; + static final String EDITOR_PROGRAM = TermuxConstants.HOME_PATH + "/bin/termux-file-editor"; + static final String URL_OPENER_PROGRAM = TermuxConstants.HOME_PATH + "/bin/termux-url-opener"; /** * If the activity should be finished when the name input dialog is dismissed. This is disabled @@ -131,17 +133,17 @@ public class TermuxFileReceiverActivity extends Activity { final Uri scriptUri = new Uri.Builder().scheme("file").path(EDITOR_PROGRAM).build(); - Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE, scriptUri); + Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, scriptUri); executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class); - executeIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, new String[]{outFile.getAbsolutePath()}); + executeIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, new String[]{outFile.getAbsolutePath()}); startService(executeIntent); finish(); }, R.string.file_received_open_folder_button, text -> { if (saveStreamWithName(in, text) == null) return; - Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE); - executeIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, TERMUX_RECEIVEDIR); + Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE); + executeIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, TERMUX_RECEIVEDIR); executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class); startService(executeIntent); finish(); @@ -188,9 +190,9 @@ public class TermuxFileReceiverActivity extends Activity { final Uri urlOpenerProgramUri = new Uri.Builder().scheme("file").path(URL_OPENER_PROGRAM).build(); - Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE, urlOpenerProgramUri); + Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, urlOpenerProgramUri); executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class); - executeIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, new String[]{url}); + executeIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, new String[]{url}); startService(executeIntent); finish(); } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 50e0283e..a34c5d11 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,9 +1,21 @@ + + + + + + + + + + ]> + - Termux - Termux user - Run commands in Termux environment - execute arbitrary commands within Termux environment + &TERMUX_APP_NAME; + &TERMUX_APP_NAME; user + Run commands in &TERMUX_APP_NAME; environment + execute arbitrary commands within &TERMUX_APP_NAME; environment New session Failsafe Keyboard @@ -16,10 +28,10 @@ Installing… Unable to install - Termux was unable to install the bootstrap packages. + &TERMUX_APP_NAME; was unable to install the bootstrap packages. Abort Try again - Termux can only be installed on the primary user account. + &TERMUX_APP_NAME; can only be installed on the primary user account. Max terminals reached Close down existing ones before creating new. @@ -41,7 +53,7 @@ New named session Create - The Termux:Style add-on is not installed. + The &TERMUX_STYLING_APP_NAME; Plugin App is not installed. Install Exit diff --git a/app/src/main/res/xml/shortcuts.xml b/app/src/main/res/xml/shortcuts.xml index ae2e42f1..dfdab62a 100644 --- a/app/src/main/res/xml/shortcuts.xml +++ b/app/src/main/res/xml/shortcuts.xml @@ -1,6 +1,14 @@ + Date: Wed, 10 Mar 2021 02:32:33 +0500 Subject: [PATCH 013/136] Move terminal input related classed to com.termux.app.input package --- .../java/com/termux/app/TermuxActivity.java | 5 +- .../java/com/termux/app/TermuxViewClient.java | 1 + .../{BellUtil.java => input/BellHandler.java} | 12 +-- .../app/{ => input}/FullScreenWorkAround.java | 10 +- .../termux/app/input/KeyboardShortcut.java | 13 +++ .../app/input/extrakeys/ExtraKeyButton.java | 92 ++++++++++++++++++ .../extrakeys/ExtraKeysInfo.java} | 97 +------------------ .../{ => input/extrakeys}/ExtraKeysView.java | 13 ++- app/src/main/res/layout/extra_keys_main.xml | 2 +- 9 files changed, 138 insertions(+), 107 deletions(-) rename app/src/main/java/com/termux/app/{BellUtil.java => input/BellHandler.java} (81%) rename app/src/main/java/com/termux/app/{ => input}/FullScreenWorkAround.java (91%) create mode 100644 app/src/main/java/com/termux/app/input/KeyboardShortcut.java create mode 100644 app/src/main/java/com/termux/app/input/extrakeys/ExtraKeyButton.java rename app/src/main/java/com/termux/app/{ExtraKeysInfos.java => input/extrakeys/ExtraKeysInfo.java} (77%) rename app/src/main/java/com/termux/app/{ => input/extrakeys}/ExtraKeysView.java (96%) diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java index a6e1d985..292cdf9e 100644 --- a/app/src/main/java/com/termux/app/TermuxActivity.java +++ b/app/src/main/java/com/termux/app/TermuxActivity.java @@ -48,6 +48,9 @@ import android.widget.Toast; import com.termux.R; import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY; +import com.termux.app.input.BellHandler; +import com.termux.app.input.extrakeys.ExtraKeysView; +import com.termux.app.input.FullScreenWorkAround; import com.termux.terminal.EmulatorDebug; import com.termux.terminal.TerminalColors; import com.termux.terminal.TerminalSession; @@ -455,7 +458,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f); break; case TermuxPreferences.BELL_VIBRATE: - BellUtil.getInstance(TermuxActivity.this).doBell(); + BellHandler.getInstance(TermuxActivity.this).doBell(); break; case TermuxPreferences.BELL_IGNORE: // Ignore the bell character. diff --git a/app/src/main/java/com/termux/app/TermuxViewClient.java b/app/src/main/java/com/termux/app/TermuxViewClient.java index ec962a88..3e92c933 100644 --- a/app/src/main/java/com/termux/app/TermuxViewClient.java +++ b/app/src/main/java/com/termux/app/TermuxViewClient.java @@ -8,6 +8,7 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.inputmethod.InputMethodManager; +import com.termux.app.input.extrakeys.ExtraKeysView; import com.termux.terminal.KeyHandler; import com.termux.terminal.TerminalEmulator; import com.termux.terminal.TerminalSession; diff --git a/app/src/main/java/com/termux/app/BellUtil.java b/app/src/main/java/com/termux/app/input/BellHandler.java similarity index 81% rename from app/src/main/java/com/termux/app/BellUtil.java rename to app/src/main/java/com/termux/app/input/BellHandler.java index 666124ce..0610971e 100644 --- a/app/src/main/java/com/termux/app/BellUtil.java +++ b/app/src/main/java/com/termux/app/input/BellHandler.java @@ -1,4 +1,4 @@ -package com.termux.app; +package com.termux.app.input; import android.content.Context; import android.os.Handler; @@ -6,15 +6,15 @@ import android.os.Looper; import android.os.SystemClock; import android.os.Vibrator; -public class BellUtil { - private static BellUtil instance = null; +public class BellHandler { + private static BellHandler instance = null; private static final Object lock = new Object(); - public static BellUtil getInstance(Context context) { + public static BellHandler getInstance(Context context) { if (instance == null) { synchronized (lock) { if (instance == null) { - instance = new BellUtil((Vibrator) context.getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE)); + instance = new BellHandler((Vibrator) context.getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE)); } } } @@ -29,7 +29,7 @@ public class BellUtil { private long lastBell = 0; private final Runnable bellRunnable; - private BellUtil(final Vibrator vibrator) { + private BellHandler(final Vibrator vibrator) { bellRunnable = new Runnable() { @Override public void run() { diff --git a/app/src/main/java/com/termux/app/FullScreenWorkAround.java b/app/src/main/java/com/termux/app/input/FullScreenWorkAround.java similarity index 91% rename from app/src/main/java/com/termux/app/FullScreenWorkAround.java rename to app/src/main/java/com/termux/app/input/FullScreenWorkAround.java index 006918ee..6002f8c8 100644 --- a/app/src/main/java/com/termux/app/FullScreenWorkAround.java +++ b/app/src/main/java/com/termux/app/input/FullScreenWorkAround.java @@ -1,9 +1,11 @@ -package com.termux.app; +package com.termux.app.input; import android.graphics.Rect; import android.view.View; import android.view.ViewGroup; +import com.termux.app.TermuxActivity; + /** * Work around for fullscreen mode in Termux to fix ExtraKeysView not being visible. * This class is derived from: @@ -13,11 +15,11 @@ import android.view.ViewGroup; * For more information, see https://issuetracker.google.com/issues/36911528 */ public class FullScreenWorkAround { - private View mChildOfContent; + private final View mChildOfContent; private int mUsableHeightPrevious; - private ViewGroup.LayoutParams mViewGroupLayoutParams; + private final ViewGroup.LayoutParams mViewGroupLayoutParams; - private int mNavBarHeight; + private final int mNavBarHeight; public static void apply(TermuxActivity activity) { diff --git a/app/src/main/java/com/termux/app/input/KeyboardShortcut.java b/app/src/main/java/com/termux/app/input/KeyboardShortcut.java new file mode 100644 index 00000000..7308c94c --- /dev/null +++ b/app/src/main/java/com/termux/app/input/KeyboardShortcut.java @@ -0,0 +1,13 @@ +package com.termux.app.input; + +public class KeyboardShortcut { + + public final int codePoint; + public final int shortcutAction; + + public KeyboardShortcut(int codePoint, int shortcutAction) { + this.codePoint = codePoint; + this.shortcutAction = shortcutAction; + } + +} diff --git a/app/src/main/java/com/termux/app/input/extrakeys/ExtraKeyButton.java b/app/src/main/java/com/termux/app/input/extrakeys/ExtraKeyButton.java new file mode 100644 index 00000000..1218ef78 --- /dev/null +++ b/app/src/main/java/com/termux/app/input/extrakeys/ExtraKeyButton.java @@ -0,0 +1,92 @@ +package com.termux.app.input.extrakeys; + +import android.text.TextUtils; + +import androidx.annotation.Nullable; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Arrays; +import java.util.stream.Collectors; + +public class ExtraKeyButton { + + /** + * The key that will be sent to the terminal, either a control character + * defined in ExtraKeysView.keyCodesForString (LEFT, RIGHT, PGUP...) or + * some text. + */ + private final String key; + + /** + * If the key is a macro, i.e. a sequence of keys separated by space. + */ + private final boolean macro; + + /** + * The text that will be shown on the button. + */ + private final String display; + + /** + * The information of the popup (triggered by swipe up). + */ + @Nullable + private ExtraKeyButton popup = null; + + public ExtraKeyButton(ExtraKeysInfo.CharDisplayMap charDisplayMap, JSONObject config) throws JSONException { + this(charDisplayMap, config, null); + } + + public ExtraKeyButton(ExtraKeysInfo.CharDisplayMap charDisplayMap, JSONObject config, @Nullable ExtraKeyButton popup) throws JSONException { + String keyFromConfig = config.optString("key", null); + String macroFromConfig = config.optString("macro", null); + String[] keys; + if (keyFromConfig != null && macroFromConfig != null) { + throw new JSONException("Both key and macro can't be set for the same key"); + } else if (keyFromConfig != null) { + keys = new String[]{keyFromConfig}; + this.macro = false; + } else if (macroFromConfig != null) { + keys = macroFromConfig.split(" "); + this.macro = true; + } else { + throw new JSONException("All keys have to specify either key or macro"); + } + + for (int i = 0; i < keys.length; i++) { + keys[i] = ExtraKeysInfo.replaceAlias(keys[i]); + } + + this.key = TextUtils.join(" ", keys); + + String displayFromConfig = config.optString("display", null); + if (displayFromConfig != null) { + this.display = displayFromConfig; + } else { + this.display = Arrays.stream(keys) + .map(key -> charDisplayMap.get(key, key)) + .collect(Collectors.joining(" ")); + } + + this.popup = popup; + } + + public String getKey() { + return key; + } + + public boolean isMacro() { + return macro; + } + + public String getDisplay() { + return display; + } + + @Nullable + public ExtraKeyButton getPopup() { + return popup; + } +} diff --git a/app/src/main/java/com/termux/app/ExtraKeysInfos.java b/app/src/main/java/com/termux/app/input/extrakeys/ExtraKeysInfo.java similarity index 77% rename from app/src/main/java/com/termux/app/ExtraKeysInfos.java rename to app/src/main/java/com/termux/app/input/extrakeys/ExtraKeysInfo.java index 1274c224..c71f3b1f 100644 --- a/app/src/main/java/com/termux/app/ExtraKeysInfos.java +++ b/app/src/main/java/com/termux/app/input/extrakeys/ExtraKeysInfo.java @@ -1,31 +1,24 @@ -package com.termux.app; - -import android.text.TextUtils; - -import androidx.annotation.Nullable; +package com.termux.app.input.extrakeys; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import java.util.Arrays; import java.util.HashMap; -import java.util.Map; -import java.util.stream.Collectors; -public class ExtraKeysInfos { +public class ExtraKeysInfo { /** * Matrix of buttons displayed */ - private ExtraKeyButton[][] buttons; + private final ExtraKeyButton[][] buttons; /** * This corresponds to one of the CharMapDisplay below */ private String style = "default"; - public ExtraKeysInfos(String propertiesInfo, String style) throws JSONException { + public ExtraKeysInfo(String propertiesInfo, String style) throws JSONException { this.style = style; // Convert String propertiesInfo to Array of Arrays @@ -151,7 +144,7 @@ public class ExtraKeysInfos { put("-", "―"); // U+2015 ― HORIZONTAL BAR }}; - /** + /* * Multiple maps are available to quickly change * the style of the keys. */ @@ -258,83 +251,3 @@ public class ExtraKeysInfos { } } -class ExtraKeyButton { - - /** - * The key that will be sent to the terminal, either a control character - * defined in ExtraKeysView.keyCodesForString (LEFT, RIGHT, PGUP...) or - * some text. - */ - private String key; - - /** - * If the key is a macro, i.e. a sequence of keys separated by space. - */ - private boolean macro; - - /** - * The text that will be shown on the button. - */ - private String display; - - /** - * The information of the popup (triggered by swipe up). - */ - @Nullable - private ExtraKeyButton popup = null; - - public ExtraKeyButton(ExtraKeysInfos.CharDisplayMap charDisplayMap, JSONObject config) throws JSONException { - this(charDisplayMap, config, null); - } - - public ExtraKeyButton(ExtraKeysInfos.CharDisplayMap charDisplayMap, JSONObject config, ExtraKeyButton popup) throws JSONException { - String keyFromConfig = config.optString("key", null); - String macroFromConfig = config.optString("macro", null); - String[] keys; - if (keyFromConfig != null && macroFromConfig != null) { - throw new JSONException("Both key and macro can't be set for the same key"); - } else if (keyFromConfig != null) { - keys = new String[]{keyFromConfig}; - this.macro = false; - } else if (macroFromConfig != null) { - keys = macroFromConfig.split(" "); - this.macro = true; - } else { - throw new JSONException("All keys have to specify either key or macro"); - } - - for (int i = 0; i < keys.length; i++) { - keys[i] = ExtraKeysInfos.replaceAlias(keys[i]); - } - - this.key = TextUtils.join(" ", keys); - - String displayFromConfig = config.optString("display", null); - if (displayFromConfig != null) { - this.display = displayFromConfig; - } else { - this.display = Arrays.stream(keys) - .map(key -> charDisplayMap.get(key, key)) - .collect(Collectors.joining(" ")); - } - - this.popup = popup; - } - - public String getKey() { - return key; - } - - public boolean isMacro() { - return macro; - } - - public String getDisplay() { - return display; - } - - @Nullable - public ExtraKeyButton getPopup() { - return popup; - } -} diff --git a/app/src/main/java/com/termux/app/ExtraKeysView.java b/app/src/main/java/com/termux/app/input/extrakeys/ExtraKeysView.java similarity index 96% rename from app/src/main/java/com/termux/app/ExtraKeysView.java rename to app/src/main/java/com/termux/app/input/extrakeys/ExtraKeysView.java index e475e32a..92e1c370 100644 --- a/app/src/main/java/com/termux/app/ExtraKeysView.java +++ b/app/src/main/java/com/termux/app/input/extrakeys/ExtraKeysView.java @@ -1,4 +1,4 @@ -package com.termux.app; +package com.termux.app.input.extrakeys; import android.annotation.SuppressLint; import android.content.Context; @@ -78,6 +78,7 @@ public final class ExtraKeysView extends GridLayout { put("F12", KeyEvent.KEYCODE_F12); }}; + @SuppressLint("RtlHardcoded") private void sendKey(View view, String keyName, boolean forceCtrlDown, boolean forceLeftAltDown) { TerminalView terminalView = view.findViewById(R.id.terminal_view); if ("KEYBOARD".equals(keyName)) { @@ -87,7 +88,8 @@ public final class ExtraKeysView extends GridLayout { DrawerLayout drawer = view.findViewById(R.id.drawer_layout); drawer.openDrawer(Gravity.LEFT); } else if (keyCodesForString.containsKey(keyName)) { - int keyCode = keyCodesForString.get(keyName); + Integer keyCode = keyCodesForString.get(keyName); + if (keyCode == null) return; int metaState = 0; if (forceCtrlDown) { metaState |= KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON; @@ -172,6 +174,7 @@ public final class ExtraKeysView extends GridLayout { private Button createSpecialButton(String buttonKey, boolean needUpdate) { SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonKey)); + if (state == null) return null; state.isOn = true; Button button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle); button.setTextColor(state.isActive ? INTERESTING_COLOR : TEXT_COLOR); @@ -187,6 +190,7 @@ public final class ExtraKeysView extends GridLayout { Button button; if(isSpecialButton(extraButton)) { button = createSpecialButton(extraButton.getKey(), false); + if (button == null) return; } else { button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle); button.setTextColor(TEXT_COLOR); @@ -235,7 +239,7 @@ public final class ExtraKeysView extends GridLayout { * "-_-" will input the string "-_-" */ @SuppressLint("ClickableViewAccessibility") - void reload(ExtraKeysInfos infos) { + public void reload(ExtraKeysInfo infos) { if(infos == null) return; @@ -256,6 +260,7 @@ public final class ExtraKeysView extends GridLayout { Button button; if(isSpecialButton(buttonInfo)) { button = createSpecialButton(buttonInfo.getKey(), true); + if (button == null) return; } else { button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle); } @@ -282,6 +287,7 @@ public final class ExtraKeysView extends GridLayout { View root = getRootView(); if (isSpecialButton(buttonInfo)) { SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonInfo.getKey())); + if (state == null) return; state.setIsActive(!state.isActive); } else { sendKey(root, buttonInfo); @@ -343,6 +349,7 @@ public final class ExtraKeysView extends GridLayout { if (buttonInfo.getPopup() != null) { if (isSpecialButton(buttonInfo.getPopup())) { SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonInfo.getPopup().getKey())); + if (state == null) return true; state.setIsActive(!state.isActive); } else { sendKey(root, buttonInfo.getPopup()); diff --git a/app/src/main/res/layout/extra_keys_main.xml b/app/src/main/res/layout/extra_keys_main.xml index 45dfab6c..90584317 100644 --- a/app/src/main/res/layout/extra_keys_main.xml +++ b/app/src/main/res/layout/extra_keys_main.xml @@ -1,5 +1,5 @@ - Date: Wed, 10 Mar 2021 02:33:45 +0500 Subject: [PATCH 014/136] Fix potential null exceptions --- .../java/com/termux/app/TermuxActivity.java | 27 +++++++++++++------ .../com/termux/terminal/TerminalOutput.java | 1 + 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java index 292cdf9e..c4b98347 100644 --- a/app/src/main/java/com/termux/app/TermuxActivity.java +++ b/app/src/main/java/com/termux/app/TermuxActivity.java @@ -105,8 +105,6 @@ public final class TermuxActivity extends Activity implements ServiceConnection private static final String BROADCAST_TERMUX_OPENED = TermuxConstants.TERMUX_PACKAGE_NAME + ".app.OPENED"; /** The main view of the activity showing the terminal. Initialized in onCreate(). */ - @SuppressWarnings("NullableProblems") - @NonNull TerminalView mTerminalView; ExtraKeysView mExtraKeysView; @@ -366,7 +364,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection for (ResolveInfo info : matches) { Intent explicitBroadcast = new Intent(broadcast); ComponentName cname = new ComponentName(info.activityInfo.applicationInfo.packageName, - info.activityInfo.name); + info.activityInfo.name); explicitBroadcast.setComponent(cname); sendBroadcast(explicitBroadcast); } @@ -488,7 +486,10 @@ public final class TermuxActivity extends Activity implements ServiceConnection } TerminalSession sessionAtRow = getItem(position); - boolean sessionRunning = sessionAtRow.isRunning(); + if (sessionAtRow == null) return row; + + boolean sessionRunning = false; + sessionRunning = sessionAtRow.isRunning(); TextView firstLineView = row.findViewById(R.id.row_line); if (mIsUsingBlackUI) { @@ -577,6 +578,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection @SuppressLint("InflateParams") void renameSession(final TerminalSession sessionToRename) { + if (sessionToRename == null) return; DialogUtils.textInput(this, R.string.session_rename_title, sessionToRename.mSessionName, R.string.session_rename_positive_button, text -> { sessionToRename.mSessionName = text; mListViewAdapter.notifyDataSetChanged(); @@ -629,6 +631,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection getDrawer().closeDrawers(); } + @SuppressLint("RtlHardcoded") @Override public void onBackPressed() { if (getDrawer().isDrawerOpen(Gravity.LEFT)) { @@ -825,7 +828,10 @@ public final class TermuxActivity extends Activity implements ServiceConnection } void showUrlSelection() { - String text = getCurrentTermSession().getEmulator().getScreen().getTranscriptTextWithFullLinesJoined(); + String text = null; + if (getCurrentTermSession() != null) { + text = getCurrentTermSession().getEmulator().getScreen().getTranscriptTextWithFullLinesJoined(); + } LinkedHashSet urlSet = extractUrls(text); if (urlSet.isEmpty()) { new AlertDialog.Builder(this).setMessage(R.string.select_url_no_found).show(); @@ -896,11 +902,14 @@ public final class TermuxActivity extends Activity implements ServiceConnection return true; case CONTEXTMENU_KILL_PROCESS_ID: final AlertDialog.Builder b = new AlertDialog.Builder(this); + final TerminalSession terminalSession = getCurrentTermSession(); + if (terminalSession == null) return true; + b.setIcon(android.R.drawable.ic_dialog_alert); b.setMessage(R.string.confirm_kill_process); b.setPositiveButton(android.R.string.yes, (dialog, id) -> { dialog.dismiss(); - getCurrentTermSession().finishIfRunning(); + terminalSession.finishIfRunning(); }); b.setNegativeButton(android.R.string.no, null); b.show(); @@ -952,7 +961,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == REQUESTCODE_PERMISSION_STORAGE && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { TermuxInstaller.setupStorageSymlinks(this); } @@ -969,7 +978,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection if (clipData == null) return; CharSequence paste = clipData.getItemAt(0).coerceToText(this); if (!TextUtils.isEmpty(paste)) - getCurrentTermSession().getEmulator().paste(paste.toString()); + if (getCurrentTermSession() != null) { + getCurrentTermSession().getEmulator().paste(paste.toString()); + } } /** The current session as stored or the last one if that does not exist. */ diff --git a/terminal-emulator/src/main/java/com/termux/terminal/TerminalOutput.java b/terminal-emulator/src/main/java/com/termux/terminal/TerminalOutput.java index f1c7d03e..cec0282b 100644 --- a/terminal-emulator/src/main/java/com/termux/terminal/TerminalOutput.java +++ b/terminal-emulator/src/main/java/com/termux/terminal/TerminalOutput.java @@ -7,6 +7,7 @@ public abstract class TerminalOutput { /** Write a string using the UTF-8 encoding to the terminal client. */ public final void write(String data) { + if (data == null) return; byte[] bytes = data.getBytes(StandardCharsets.UTF_8); write(bytes, 0, bytes.length); } From 7b4acb53c9da03c31960979313ccb0a7d5f045e5 Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Thu, 11 Mar 2021 18:22:11 +0500 Subject: [PATCH 015/136] Move Termux Properties to com.termux.app.settings.properties package The termux properties handling was mixed in with termux preferences. They are now moved out of into a separate sub package, the following classes are added: - `SharedProperties` class which is an implementation similar to android's `SharedPreferences` interface for reading from ".properties" files which also maintains an in-memory cache for the key/value pairs. Two types of in-memory cache maps are maintained, one for the literal `String` values found in the file for the keys and an additional one that stores (near) primitive `Object` values for internal use by the caller. Write support is currently not implemented, but may be added if we provide users a GUI to modify the properties. We cannot just overwrite the ".properties" files, since comments also exits, so in-place editing would be required. - `SharedPropertiesParser` interface that the caller of `SharedProperties` must implement. It is currently only used to map `String` values to internal `Object` values. - `TermuxPropertyConstants` class that defines shared constants of the properties used by Termux app and its plugins. This class should be imported by other termux plugin apps instead of copying and defining their own constants. - `TermuxSharedProperties` class that acts as manager for handling termux properties. It implements the `SharedPropertiesParser` interface and acts as the wrapper for the `SharedProperties` class. --- app/build.gradle | 1 + .../java/com/termux/app/TermuxActivity.java | 36 +- .../com/termux/app/TermuxPreferences.java | 192 +----- .../java/com/termux/app/TermuxViewClient.java | 22 +- .../settings/properties/SharedProperties.java | 312 ++++++++++ .../properties/SharedPropertiesParser.java | 23 + .../properties/TermuxPropertyConstants.java | 228 +++++++ .../properties/TermuxSharedProperties.java | 569 ++++++++++++++++++ 8 files changed, 1173 insertions(+), 210 deletions(-) create mode 100644 app/src/main/java/com/termux/app/settings/properties/SharedProperties.java create mode 100644 app/src/main/java/com/termux/app/settings/properties/SharedPropertiesParser.java create mode 100644 app/src/main/java/com/termux/app/settings/properties/TermuxPropertyConstants.java create mode 100644 app/src/main/java/com/termux/app/settings/properties/TermuxSharedProperties.java diff --git a/app/build.gradle b/app/build.gradle index 3d519ffa..c1f0c6df 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,6 +11,7 @@ android { implementation "androidx.viewpager:viewpager:1.0.0" implementation "androidx.drawerlayout:drawerlayout:1.1.1" implementation 'androidx.core:core:1.5.0-beta02' + implementation 'com.google.guava:guava:24.1-jre' implementation project(":terminal-view") } diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java index c4b98347..e3396ab0 100644 --- a/app/src/main/java/com/termux/app/TermuxActivity.java +++ b/app/src/main/java/com/termux/app/TermuxActivity.java @@ -51,6 +51,8 @@ import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY; import com.termux.app.input.BellHandler; import com.termux.app.input.extrakeys.ExtraKeysView; import com.termux.app.input.FullScreenWorkAround; +import com.termux.app.settings.properties.TermuxPropertyConstants; +import com.termux.app.settings.properties.TermuxSharedProperties; import com.termux.terminal.EmulatorDebug; import com.termux.terminal.TerminalColors; import com.termux.terminal.TerminalSession; @@ -110,6 +112,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection ExtraKeysView mExtraKeysView; TermuxPreferences mSettings; + TermuxSharedProperties mProperties; /** * The connection to the {@link TermuxService}. Requested in {@link #onCreate(Bundle)} with a call to @@ -144,16 +147,19 @@ public final class TermuxActivity extends Activity implements ServiceConnection public void onReceive(Context context, Intent intent) { if (mIsVisible) { String whatToReload = intent.getStringExtra(TERMUX_ACTIVITY.EXTRA_RELOAD_STYLE); + Log.d("termux", "Reloading termux style for: " + whatToReload); if ("storage".equals(whatToReload)) { if (ensureStoragePermissionGranted()) TermuxInstaller.setupStorageSymlinks(TermuxActivity.this); return; } + checkForFontAndColors(); - mSettings.reloadFromProperties(TermuxActivity.this); + + mProperties.loadTermuxPropertiesFromDisk(); if (mExtraKeysView != null) { - mExtraKeysView.reload(mSettings.mExtraKeys); + mExtraKeysView.reload(mProperties.getExtraKeysInfo()); } } } @@ -205,7 +211,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection @Override public void onCreate(Bundle bundle) { mSettings = new TermuxPreferences(this); - mIsUsingBlackUI = mSettings.isUsingBlackUI(); + mProperties = new TermuxSharedProperties(this); + + mIsUsingBlackUI = mProperties.isUsingBlackUI(); if (mIsUsingBlackUI) { this.setTheme(R.style.Theme_Termux_Black); } else { @@ -223,7 +231,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection return insets; }); - if (mSettings.isUsingFullScreen()) { + if (mProperties.isUsingFullScreen()) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); } @@ -245,7 +253,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection ViewGroup.LayoutParams layoutParams = viewPager.getLayoutParams(); - layoutParams.height = layoutParams.height * (mSettings.mExtraKeys == null ? 0 : mSettings.mExtraKeys.getMatrix().length); + layoutParams.height = layoutParams.height * (mProperties.getExtraKeysInfo() == null ? 0 : mProperties.getExtraKeysInfo().getMatrix().length); viewPager.setLayoutParams(layoutParams); viewPager.setAdapter(new PagerAdapter() { @@ -266,10 +274,10 @@ public final class TermuxActivity extends Activity implements ServiceConnection View layout; if (position == 0) { layout = mExtraKeysView = (ExtraKeysView) inflater.inflate(R.layout.extra_keys_main, collection, false); - mExtraKeysView.reload(mSettings.mExtraKeys); + mExtraKeysView.reload(mProperties.getExtraKeysInfo()); // apply extra keys fix if enabled in prefs - if (mSettings.isUsingFullScreen() && mSettings.isUsingFullScreenWorkAround()) { + if (mProperties.isUsingFullScreen() && mProperties.isUsingFullScreenWorkAround()) { FullScreenWorkAround.apply(TermuxActivity.this); } @@ -451,14 +459,14 @@ public final class TermuxActivity extends Activity implements ServiceConnection public void onBell(TerminalSession session) { if (!mIsVisible) return; - switch (mSettings.mBellBehaviour) { - case TermuxPreferences.BELL_BEEP: - mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f); - break; - case TermuxPreferences.BELL_VIBRATE: + switch (mProperties.getBellBehaviour()) { + case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_VIBRATE: BellHandler.getInstance(TermuxActivity.this).doBell(); break; - case TermuxPreferences.BELL_IGNORE: + case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_BEEP: + mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f); + break; + case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_IGNORE: // Ignore the bell character. break; } @@ -665,7 +673,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection String workingDirectory; if (currentSession == null) { - workingDirectory = mSettings.mDefaultWorkingDir; + workingDirectory = mProperties.getDefaultWorkingDirectory(); } else { workingDirectory = currentSession.getCwd(); } diff --git a/app/src/main/java/com/termux/app/TermuxPreferences.java b/app/src/main/java/com/termux/app/TermuxPreferences.java index f4187eef..5567439d 100644 --- a/app/src/main/java/com/termux/app/TermuxPreferences.java +++ b/app/src/main/java/com/termux/app/TermuxPreferences.java @@ -2,61 +2,13 @@ package com.termux.app; import android.content.Context; import android.content.SharedPreferences; -import android.content.res.Configuration; import android.preference.PreferenceManager; -import android.util.Log; import android.util.TypedValue; -import android.widget.Toast; + import com.termux.terminal.TerminalSession; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Properties; - -import androidx.annotation.IntDef; - -import static com.termux.terminal.EmulatorDebug.LOG_TAG; final class TermuxPreferences { - @IntDef({BELL_VIBRATE, BELL_BEEP, BELL_IGNORE}) - @Retention(RetentionPolicy.SOURCE) - @interface AsciiBellBehaviour { - } - - final static class KeyboardShortcut { - - KeyboardShortcut(int codePoint, int shortcutAction) { - this.codePoint = codePoint; - this.shortcutAction = shortcutAction; - } - - final int codePoint; - final int shortcutAction; - } - - static final int SHORTCUT_ACTION_CREATE_SESSION = 1; - static final int SHORTCUT_ACTION_NEXT_SESSION = 2; - static final int SHORTCUT_ACTION_PREVIOUS_SESSION = 3; - static final int SHORTCUT_ACTION_RENAME_SESSION = 4; - - static final int BELL_VIBRATE = 1; - static final int BELL_BEEP = 2; - static final int BELL_IGNORE = 3; - private final int MIN_FONTSIZE; private static final int MAX_FONTSIZE = 256; @@ -65,34 +17,11 @@ final class TermuxPreferences { private static final String CURRENT_SESSION_KEY = "current_session"; private static final String SCREEN_ALWAYS_ON_KEY = "screen_always_on"; - private boolean mUseDarkUI; private boolean mScreenAlwaysOn; private int mFontSize; - - private boolean mUseFullScreen; - private boolean mUseFullScreenWorkAround; - - @AsciiBellBehaviour - int mBellBehaviour = BELL_VIBRATE; - - boolean mBackIsEscape; - boolean mDisableVolumeVirtualKeys; boolean mShowExtraKeys; - String mDefaultWorkingDir; - - ExtraKeysInfos mExtraKeys; - - final List shortcuts = new ArrayList<>(); - - /** - * If value is not in the range [min, max], set it to either min or max. - */ - static int clamp(int value, int min, int max) { - return Math.min(Math.max(value, min), max); - } TermuxPreferences(Context context) { - reloadFromProperties(context); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, context.getResources().getDisplayMetrics()); @@ -139,18 +68,6 @@ final class TermuxPreferences { return mScreenAlwaysOn; } - boolean isUsingBlackUI() { - return mUseDarkUI; - } - - boolean isUsingFullScreen() { - return mUseFullScreen; - } - - boolean isUsingFullScreenWorkAround() { - return mUseFullScreenWorkAround; - } - void setScreenAlwaysOn(Context context, boolean newValue) { mScreenAlwaysOn = newValue; PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SCREEN_ALWAYS_ON_KEY, newValue).apply(); @@ -169,108 +86,11 @@ final class TermuxPreferences { return null; } - void reloadFromProperties(Context context) { - File propsFile = new File(TermuxConstants.TERMUX_PROPERTIES_PRIMARY_PATH); - if (!propsFile.exists()) - propsFile = new File(TermuxConstants.TERMUX_PROPERTIES_SECONDARY_PATH); - - Properties props = new Properties(); - try { - if (propsFile.isFile() && propsFile.canRead()) { - try (FileInputStream in = new FileInputStream(propsFile)) { - props.load(new InputStreamReader(in, StandardCharsets.UTF_8)); - } - } - } catch (Exception e) { - Toast.makeText(context, "Could not open properties file termux.properties: " + e.getMessage(), Toast.LENGTH_LONG).show(); - Log.e("termux", "Error loading props", e); - } - - switch (props.getProperty("bell-character", "vibrate")) { - case "beep": - mBellBehaviour = BELL_BEEP; - break; - case "ignore": - mBellBehaviour = BELL_IGNORE; - break; - default: // "vibrate". - mBellBehaviour = BELL_VIBRATE; - break; - } - - switch (props.getProperty("use-black-ui", "").toLowerCase()) { - case "true": - mUseDarkUI = true; - break; - case "false": - mUseDarkUI = false; - break; - default: - int nightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - mUseDarkUI = nightMode == Configuration.UI_MODE_NIGHT_YES; - } - - mUseFullScreen = "true".equals(props.getProperty("fullscreen", "false").toLowerCase()); - mUseFullScreenWorkAround = "true".equals(props.getProperty("use-fullscreen-workaround", "false").toLowerCase()); - - mDefaultWorkingDir = props.getProperty("default-working-directory", TermuxConstants.HOME_PATH); - File workDir = new File(mDefaultWorkingDir); - if (!workDir.exists() || !workDir.isDirectory()) { - // Fallback to home directory if user configured working directory is not exist - // or is a regular file. - mDefaultWorkingDir = TermuxConstants.HOME_PATH; - } - - String defaultExtraKeys = "[[ESC, TAB, CTRL, ALT, {key: '-', popup: '|'}, DOWN, UP]]"; - - try { - String extrakeyProp = props.getProperty("extra-keys", defaultExtraKeys); - String extraKeysStyle = props.getProperty("extra-keys-style", "default"); - mExtraKeys = new ExtraKeysInfos(extrakeyProp, extraKeysStyle); - } catch (JSONException e) { - Toast.makeText(context, "Could not load the extra-keys property from the config: " + e.toString(), Toast.LENGTH_LONG).show(); - Log.e("termux", "Error loading props", e); - - try { - mExtraKeys = new ExtraKeysInfos(defaultExtraKeys, "default"); - } catch (JSONException e2) { - e2.printStackTrace(); - Toast.makeText(context, "Can't create default extra keys", Toast.LENGTH_LONG).show(); - mExtraKeys = null; - } - } - - mBackIsEscape = "escape".equals(props.getProperty("back-key", "back")); - mDisableVolumeVirtualKeys = "volume".equals(props.getProperty("volume-keys", "virtual")); - - shortcuts.clear(); - parseAction("shortcut.create-session", SHORTCUT_ACTION_CREATE_SESSION, props); - parseAction("shortcut.next-session", SHORTCUT_ACTION_NEXT_SESSION, props); - parseAction("shortcut.previous-session", SHORTCUT_ACTION_PREVIOUS_SESSION, props); - parseAction("shortcut.rename-session", SHORTCUT_ACTION_RENAME_SESSION, props); - } - - private void parseAction(String name, int shortcutAction, Properties props) { - String value = props.getProperty(name); - if (value == null) return; - String[] parts = value.toLowerCase().trim().split("\\+"); - String input = parts.length == 2 ? parts[1].trim() : null; - if (!(parts.length == 2 && parts[0].trim().equals("ctrl")) || input.isEmpty() || input.length() > 2) { - Log.e("termux", "Keyboard shortcut '" + name + "' is not Ctrl+"); - return; - } - - char c = input.charAt(0); - int codePoint = c; - if (Character.isLowSurrogate(c)) { - if (input.length() != 2 || Character.isHighSurrogate(input.charAt(1))) { - Log.e("termux", "Keyboard shortcut '" + name + "' is not Ctrl+"); - return; - } else { - codePoint = Character.toCodePoint(input.charAt(1), c); - } - } - shortcuts.add(new KeyboardShortcut(codePoint, shortcutAction)); + /** + * If value is not in the range [min, max], set it to either min or max. + */ + static int clamp(int value, int min, int max) { + return Math.min(Math.max(value, min), max); } } diff --git a/app/src/main/java/com/termux/app/TermuxViewClient.java b/app/src/main/java/com/termux/app/TermuxViewClient.java index 3e92c933..b6eea4d3 100644 --- a/app/src/main/java/com/termux/app/TermuxViewClient.java +++ b/app/src/main/java/com/termux/app/TermuxViewClient.java @@ -8,7 +8,9 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.inputmethod.InputMethodManager; +import com.termux.app.input.KeyboardShortcut; import com.termux.app.input.extrakeys.ExtraKeysView; +import com.termux.app.settings.properties.TermuxPropertyConstants; import com.termux.terminal.KeyHandler; import com.termux.terminal.TerminalEmulator; import com.termux.terminal.TerminalSession; @@ -47,7 +49,7 @@ public final class TermuxViewClient implements TerminalViewClient { @Override public boolean shouldBackButtonBeMappedToEscape() { - return mActivity.mSettings.mBackIsEscape; + return mActivity.mProperties.isBackKeyTheEscapeKey(); } @Override @@ -230,23 +232,23 @@ public final class TermuxViewClient implements TerminalViewClient { return true; } - List shortcuts = mActivity.mSettings.shortcuts; + List shortcuts = mActivity.mProperties.getSessionShortcuts(); if (!shortcuts.isEmpty()) { int codePointLowerCase = Character.toLowerCase(codePoint); for (int i = shortcuts.size() - 1; i >= 0; i--) { - TermuxPreferences.KeyboardShortcut shortcut = shortcuts.get(i); + KeyboardShortcut shortcut = shortcuts.get(i); if (codePointLowerCase == shortcut.codePoint) { switch (shortcut.shortcutAction) { - case TermuxPreferences.SHORTCUT_ACTION_CREATE_SESSION: + case TermuxPropertyConstants.ACTION_SHORTCUT_CREATE_SESSION: mActivity.addNewSession(false, null); return true; - case TermuxPreferences.SHORTCUT_ACTION_PREVIOUS_SESSION: - mActivity.switchToSession(false); - return true; - case TermuxPreferences.SHORTCUT_ACTION_NEXT_SESSION: + case TermuxPropertyConstants.ACTION_SHORTCUT_NEXT_SESSION: mActivity.switchToSession(true); return true; - case TermuxPreferences.SHORTCUT_ACTION_RENAME_SESSION: + case TermuxPropertyConstants.ACTION_SHORTCUT_PREVIOUS_SESSION: + mActivity.switchToSession(false); + return true; + case TermuxPropertyConstants.ACTION_SHORTCUT_RENAME_SESSION: mActivity.renameSession(mActivity.getCurrentTermSession()); return true; } @@ -266,7 +268,7 @@ public final class TermuxViewClient implements TerminalViewClient { /** Handle dedicated volume buttons as virtual keys if applicable. */ private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) { InputDevice inputDevice = event.getDevice(); - if (mActivity.mSettings.mDisableVolumeVirtualKeys) { + if (mActivity.mProperties.areVirtualVolumeKeysDisabled()) { return false; } else if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) { // Do not steal dedicated buttons from a full external keyboard. diff --git a/app/src/main/java/com/termux/app/settings/properties/SharedProperties.java b/app/src/main/java/com/termux/app/settings/properties/SharedProperties.java new file mode 100644 index 00000000..db6eb637 --- /dev/null +++ b/app/src/main/java/com/termux/app/settings/properties/SharedProperties.java @@ -0,0 +1,312 @@ +package com.termux.app.settings.properties; + +import android.content.Context; +import android.util.Log; +import android.widget.Toast; + +import com.google.common.primitives.Primitives; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * An implementation similar to android's {@link android.content.SharedPreferences} interface for + * reading and writing to and from ".properties" files which also maintains an in-memory cache for + * the key/value pairs. Operations are does under synchronization locks and should be thread safe. + * + * Two types of in-memory cache maps are maintained, one for the literal {@link String} values found + * in the file for the keys and an additional one that stores (near) primitive {@link Object} values for + * internal use by the caller. + * + * This currently only has read support, write support can/will be added later if needed. Check android's + * SharedPreferencesImpl class for reference implementation. + * + * https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:frameworks/base/core/java/android/app/SharedPreferencesImpl.java + */ +public class SharedProperties { + + /** + * The {@link Properties} object that maintains an in-memory cache of values loaded from the + * {@link #mPropertiesFile} file. The key/value pairs are of any keys defined by + * {@link #mPropertiesList} that are found in the file against their literal values in the file. + */ + private Properties mProperties; + + /** + * The {@link HashMap<>} object that maintains an in-memory cache of internal values for the values + * loaded from the {@link #mPropertiesFile} file. The key/value pairs are of any keys defined by + * {@link #mPropertiesList} that are found in the file against their internal {@link Object} values + * returned by the call to + * {@link SharedPropertiesParser#getInternalPropertyValueFromValue(Context, String, String)} interface. + */ + private Map mMap; + + private final Context mContext; + private final File mPropertiesFile; + private final Set mPropertiesList; + private final SharedPropertiesParser mSharedPropertiesParser; + + private final Object mLock = new Object(); + + /** + * Constructor for the SharedProperties class. + * + * @param context The Context for operations. + * @param propertiesFile The {@link File} object to load properties from. + * @param propertiesList The {@link Set} object that defined which properties to load. + * @param sharedPropertiesParser that implements the {@link SharedPropertiesParser} interface. + */ + public SharedProperties(@Nonnull Context context, @Nullable File propertiesFile, @Nonnull Set propertiesList, @Nonnull SharedPropertiesParser sharedPropertiesParser) { + mContext = context; + mPropertiesFile = propertiesFile; + mPropertiesList = propertiesList; + mSharedPropertiesParser = sharedPropertiesParser; + + mProperties = new Properties(); + mMap = new HashMap(); + } + + /** + * Load the properties defined by {@link #mPropertiesList} from the {@link #mPropertiesFile} file + * to update the {@link #mProperties} and {@link #mMap} in-memory cache. + * Properties are not loading automatically when constructor is called and must be manually called. + */ + public void loadPropertiesFromDisk() { + synchronized (mLock) { + // Get properties from mPropertiesFile + Properties properties = getProperties(false); + + // We still need to load default values into mMap, so we assume no properties defined if + // reading from mPropertiesFile failed + if (properties == null) + properties = new Properties(); + + HashMap map = new HashMap(); + Properties newProperties = new Properties(); + + String value; + Object internalValue; + for (String key : mPropertiesList) { + value = properties.getProperty(key); // value will be null if key does not exist in propertiesFile + Log.d("termux", key + " : " + value); + + // Call the {@link SharedPropertiesParser#getInternalPropertyValueFromValue(Context,String,String)} + // interface method to get the internal value to store in the {@link #mMap}. + internalValue = mSharedPropertiesParser.getInternalPropertyValueFromValue(mContext, key, value); + + // If the internal value was successfully added to map, then also add value to newProperties + // We only store values in-memory defined by {@link #mPropertiesList} + if (putToMap(map, key, internalValue)) { // null internalValue will be put into map + putToProperties(newProperties, key, value); // null value will **not** be into properties + } + } + + mMap = map; + mProperties = newProperties; + } + } + + /** + * Put a value in a {@link #mMap}. + * The key cannot be {@code null}. + * Only {@code null}, primitive or their wrapper classes or String class objects are allowed to be added to + * the map, although this limitation may be changed. + * + * @param map The {@link Map} object to add value to. + * @param key The key for which to add the value to the map. + * @param value The {@link Object} to add to the map. + * @return Returns {@code true} if value was successfully added, otherwise {@code false}. + */ + public static boolean putToMap(HashMap map, String key, Object value) { + + if (map == null) { + Log.e("termux", "Map passed to SharedProperties.putToProperties() is null"); + return false; + } + + // null keys are not allowed to be stored in mMap + if (key == null) { + Log.e("termux", "Cannot put a null key into properties map"); + return false; + } + + boolean put = false; + if (value != null) { + Class clazz = value.getClass(); + if (clazz.isPrimitive() || Primitives.isWrapperType(clazz) || value instanceof String) { + put = true; + } + } else { + put = true; + } + + if (put) { + map.put(key, value); + return true; + } else { + Log.e("termux", "Cannot put a non-primitive value for the key \"" + key + "\" into properties map"); + return false; + } + } + + /** + * Put a value in a {@link Map}. + * The key cannot be {@code null}. + * Passing {@code null} as the value argument is equivalent to removing the key from the + * properties. + * + * @param properties The {@link Properties} object to add value to. + * @param key The key for which to add the value to the properties. + * @param value The {@link String} to add to the properties. + * @return Returns {@code true} if value was successfully added, otherwise {@code false}. + */ + public static boolean putToProperties(Properties properties, String key, String value) { + + if (properties == null) { + Log.e("termux", "Properties passed to SharedProperties.putToProperties() is null"); + return false; + } + + // null keys are not allowed to be stored in mMap + if (key == null) { + Log.e("termux", "Cannot put a null key into properties"); + return false; + } + + if (value != null) { + properties.put(key, value); + return true; + } else { + properties.remove(key); + } + + return true; + } + + /** + * A static function to get the {@link Properties} object for the propertiesFile. A lock is not + * taken when this function is called. + * + * @param context The {@link Context} to use to show a flash if an exception is raised while + * reading the file. If context is {@code null}, then flash will not be shown. + * @param propertiesFile The {@link File} to read the {@link Properties} from. + * @return Returns the {@link Properties} object. It will be {@code null} if an exception is + * raised while reading the file. + */ + public static Properties getPropertiesFromFile(Context context, File propertiesFile) { + Properties properties = new Properties(); + + if (propertiesFile == null) { + Log.e("termux", "Not loading properties since file is null"); + return properties; + } + + try { + try (FileInputStream in = new FileInputStream(propertiesFile)) { + Log.v("termux", "Loading properties from \"" + propertiesFile.getAbsolutePath() + "\" file"); + properties.load(new InputStreamReader(in, StandardCharsets.UTF_8)); + } + } catch (Exception e) { + if(context != null) + Toast.makeText(context, "Could not open properties file \"" + propertiesFile.getAbsolutePath() + "\": " + e.getMessage(), Toast.LENGTH_LONG).show(); + Log.e("termux", "Error loading properties file \"" + propertiesFile.getAbsolutePath() + "\"", e); + return null; + } + + return properties; + } + + /** + * Get the {@link Properties} object for the {@link #mPropertiesFile}. The {@link Properties} + * object will also contain properties not defined by the {@link #mPropertiesList} if cache + * value is {@code false}. + * + * @param cached If {@code true}, then the {@link #mProperties} in-memory cache is returned. Otherwise + * the {@link Properties} object is directly read from the {@link #mPropertiesFile}. + * @return Returns the {@link Properties} object if read from file, otherwise a copy of {@link #mProperties}. + */ + public Properties getProperties(boolean cached) { + synchronized (mLock) { + if (cached) { + if (mProperties == null) mProperties = new Properties(); + return getPropertiesCopy(mProperties); + } else { + return getPropertiesFromFile(mContext, mPropertiesFile); + } + } + } + + /** + * Get the {@link String} value for the key passed from the {@link #mPropertiesFile}. + * + * @param key The key to read from the {@link Properties} object. + * @param cached If {@code true}, then the value is returned from the {@link #mProperties} in-memory cache. + * Otherwise the {@link Properties} object is read directly from the {@link #mPropertiesFile} + * and value is returned from it against the key. + * @return Returns the {@link String} object. This will be {@code null} if key is not found. + */ + public String getProperty(String key, boolean cached) { + synchronized (mLock) { + return (String) getProperties(cached).get(key); + } + } + + /** + * Get the {@link #mMap} object for the {@link #mPropertiesFile}. A call to + * {@link #loadPropertiesFromDisk()} must be made before this. + * + * @return Returns a copy of {@link #mMap} object. + */ + public Map getInternalProperties() { + synchronized (mLock) { + if (mMap == null) mMap = new HashMap(); + return getMapCopy(mMap); + } + } + + /** + * Get the internal {@link Object} value for the key passed from the {@link #mPropertiesFile}. + * The value is returned from the {@link #mMap} in-memory cache, so a call to + * {@link #loadPropertiesFromDisk()} must be made before this. + * + * @param key The key to read from the {@link #mMap} object. + * @return Returns the {@link Object} object. This will be {@code null} if key is not found or + * if object was {@code null}. Use {@link HashMap#containsKey(Object)} to detect the later. + * situation. + */ + public Object getInternalProperty(String key) { + synchronized (mLock) { + // null keys are not allowed to be stored in mMap + if (key != null) + return getInternalProperties().get(key); + else + return null; + } + } + + public static Properties getPropertiesCopy(Properties inputProperties) { + if(inputProperties == null) return null; + + Properties outputProperties = new Properties(); + for (String key : inputProperties.stringPropertyNames()) { + outputProperties.put(key, inputProperties.get(key)); + } + + return outputProperties; + } + + public static Map getMapCopy(Map map) { + if(map == null) return null; + return new HashMap(map); + } + +} diff --git a/app/src/main/java/com/termux/app/settings/properties/SharedPropertiesParser.java b/app/src/main/java/com/termux/app/settings/properties/SharedPropertiesParser.java new file mode 100644 index 00000000..a82141bf --- /dev/null +++ b/app/src/main/java/com/termux/app/settings/properties/SharedPropertiesParser.java @@ -0,0 +1,23 @@ +package com.termux.app.settings.properties; + +import android.content.Context; + +import java.util.HashMap; + +/** + * An interface that must be defined by the caller of the {@link SharedProperties} class. + */ +public interface SharedPropertiesParser { + + /** + * A function that should return the internal {@link Object} to be stored for a key/value pair + * read from properties file in the {@link HashMap <>} in-memory cache. + * + * @param context The context for operations. + * @param key The key for which the internal object is required. + * @param value The literal value for the property found is the properties file. + * @return Returns the {@link Object} object to store in the {@link HashMap <>} in-memory cache. + */ + public Object getInternalPropertyValueFromValue(Context context, String key, String value); + +} diff --git a/app/src/main/java/com/termux/app/settings/properties/TermuxPropertyConstants.java b/app/src/main/java/com/termux/app/settings/properties/TermuxPropertyConstants.java new file mode 100644 index 00000000..21a06ad8 --- /dev/null +++ b/app/src/main/java/com/termux/app/settings/properties/TermuxPropertyConstants.java @@ -0,0 +1,228 @@ +package com.termux.app.settings.properties; + +import android.util.Log; + +import com.google.common.collect.ImmutableBiMap; +import com.termux.app.TermuxConstants; + +import java.io.File; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; + +/* + * Version: v0.1.0 + * + * Changelog + * + * - 0.1.0 (2021-03-08) + * - Initial Release + * + */ + +/** + * A class that defines shared constants of the properties used by Termux app and its plugins. + * This class will be hosted by termux-app and should be imported by other termux plugin apps as is + * instead of copying constants to random classes. The 3rd party apps can also import it for + * interacting with termux apps. If changes are made to this file, increment the version number + * * and add an entry in the Changelog section above. + * + * The properties are loaded from the first file found at + * {@link TermuxConstants#TERMUX_PROPERTIES_PRIMARY_PATH} or + * {@link TermuxConstants#TERMUX_PROPERTIES_SECONDARY_PATH} + */ +public final class TermuxPropertyConstants { + + /** Defines the bidirectional map for boolean values and their internal values */ + public static final ImmutableBiMap MAP_GENERIC_BOOLEAN = + new ImmutableBiMap.Builder() + .put("true", true) + .put("false", false) + .build(); + + /** Defines the bidirectional map for inverted boolean values and their internal values */ + public static final ImmutableBiMap MAP_GENERIC_INVERTED_BOOLEAN = + new ImmutableBiMap.Builder() + .put("true", false) + .put("false", true) + .build(); + + + + /** Defines the key for whether to use back key as the escape key */ + public static final String KEY_USE_BACK_KEY_AS_ESCAPE_KEY = "back-key"; // Default: "back-key" + + public static final String VALUE_BACK_KEY_BEHAVIOUR_BACK = "back"; + public static final String VALUE_BACK_KEY_BEHAVIOUR_ESCAPE = "escape"; + + + + /** Defines the key for whether to use black UI */ + public static final String KEY_USE_BLACK_UI = "use-black-ui"; // Default: "use-black-ui" + + + + /** Defines the key for whether to use fullscreen */ + public static final String KEY_USE_FULLSCREEN = "fullscreen"; // Default: "fullscreen" + + + + /** Defines the key for whether to use fullscreen workaround */ + public static final String KEY_USE_FULLSCREEN_WORKAROUND = "use-fullscreen-workaround"; // Default: "use-fullscreen-workaround" + + + + /** Defines the key for whether virtual volume keys are disabled */ + public static final String KEY_VIRTUAL_VOLUME_KEYS_DISABLED = "volume-keys"; // Default: "volume-keys" + + public static final String VALUE_VOLUME_KEY_BEHAVIOUR_VOLUME = "volume"; + public static final String VALUE_VOLUME_KEY_BEHAVIOUR_VIRTUAL = "virtual"; + + + + /** Defines the key for the bell behaviour */ + public static final String KEY_BELL_BEHAVIOUR = "bell-character"; // Default: "bell-character" + + public static final String VALUE_BELL_BEHAVIOUR_VIBRATE = "vibrate"; + public static final String VALUE_BELL_BEHAVIOUR_BEEP = "beep"; + public static final String VALUE_BELL_BEHAVIOUR_IGNORE = "ignore"; + public static final String DEFAULT_VALUE_BELL_BEHAVIOUR = VALUE_BELL_BEHAVIOUR_VIBRATE; + + public static final int IVALUE_BELL_BEHAVIOUR_VIBRATE = 1; + public static final int IVALUE_BELL_BEHAVIOUR_BEEP = 2; + public static final int IVALUE_BELL_BEHAVIOUR_IGNORE = 3; + public static final int DEFAULT_IVALUE_BELL_BEHAVIOUR = IVALUE_BELL_BEHAVIOUR_VIBRATE; + + /** Defines the bidirectional map for bell behaviour values and their internal values */ + public static final ImmutableBiMap MAP_BELL_BEHAVIOUR = + new ImmutableBiMap.Builder() + .put(VALUE_BELL_BEHAVIOUR_VIBRATE, IVALUE_BELL_BEHAVIOUR_VIBRATE) + .put(VALUE_BELL_BEHAVIOUR_BEEP, IVALUE_BELL_BEHAVIOUR_BEEP) + .put(VALUE_BELL_BEHAVIOUR_IGNORE, IVALUE_BELL_BEHAVIOUR_IGNORE) + .build(); + + + + /** Defines the key for create session shortcut */ + public static final String KEY_SHORTCUT_CREATE_SESSION = "shortcut.create-session"; // Default: "shortcut.create-session" + /** Defines the key for next session shortcut */ + public static final String KEY_SHORTCUT_NEXT_SESSION = "shortcut.next-session"; // Default: "shortcut.next-session" + /** Defines the key for previous session shortcut */ + public static final String KEY_SHORTCUT_PREVIOUS_SESSION = "shortcut.previous-session"; // Default: "shortcut.previous-session" + /** Defines the key for rename session shortcut */ + public static final String KEY_SHORTCUT_RENAME_SESSION = "shortcut.rename-session"; // Default: "shortcut.rename-session" + + public static final int ACTION_SHORTCUT_CREATE_SESSION = 1; + public static final int ACTION_SHORTCUT_NEXT_SESSION = 2; + public static final int ACTION_SHORTCUT_PREVIOUS_SESSION = 3; + public static final int ACTION_SHORTCUT_RENAME_SESSION = 4; + + /** Defines the bidirectional map for session shortcut values and their internal actions */ + public static final ImmutableBiMap MAP_SESSION_SHORTCUTS = + new ImmutableBiMap.Builder() + .put(KEY_SHORTCUT_CREATE_SESSION, ACTION_SHORTCUT_CREATE_SESSION) + .put(KEY_SHORTCUT_NEXT_SESSION, ACTION_SHORTCUT_NEXT_SESSION) + .put(KEY_SHORTCUT_PREVIOUS_SESSION, ACTION_SHORTCUT_PREVIOUS_SESSION) + .put(KEY_SHORTCUT_RENAME_SESSION, ACTION_SHORTCUT_RENAME_SESSION) + .build(); + + + + /** Defines the key for the default working directory */ + public static final String KEY_DEFAULT_WORKING_DIRECTORY = "default-working-directory"; // Default: "default-working-directory" + /** Defines the default working directory */ + public static final String DEFAULT_IVALUE_DEFAULT_WORKING_DIRECTORY = TermuxConstants.HOME_PATH; + + + + /** Defines the key for extra keys */ + public static final String KEY_EXTRA_KEYS = "extra-keys"; // Default: "extra-keys" + /** Defines the key for extra keys style */ + public static final String KEY_EXTRA_KEYS_STYLE = "extra-keys-style"; // Default: "extra-keys-style" + public static final String DEFAULT_IVALUE_EXTRA_KEYS = "[[ESC, TAB, CTRL, ALT, {key: '-', popup: '|'}, DOWN, UP]]"; + public static final String DEFAULT_IVALUE_EXTRA_KEYS_STYLE = "default"; + + + + + + /** Defines the set for keys loaded by termux + * Setting this to {@code null} will make {@link SharedProperties} throw an exception. + * */ + public static final Set TERMUX_PROPERTIES_LIST = new HashSet<>(Arrays.asList( + // boolean + KEY_USE_BACK_KEY_AS_ESCAPE_KEY, + KEY_USE_BLACK_UI, + KEY_USE_FULLSCREEN, + KEY_USE_FULLSCREEN_WORKAROUND, + KEY_VIRTUAL_VOLUME_KEYS_DISABLED, + TermuxConstants.PROP_ALLOW_EXTERNAL_APPS, + + // int + KEY_BELL_BEHAVIOUR, + + // Integer + KEY_SHORTCUT_CREATE_SESSION, + KEY_SHORTCUT_NEXT_SESSION, + KEY_SHORTCUT_PREVIOUS_SESSION, + KEY_SHORTCUT_RENAME_SESSION, + + // String + KEY_DEFAULT_WORKING_DIRECTORY, + KEY_EXTRA_KEYS, + KEY_EXTRA_KEYS_STYLE + )); + + /** Defines the set for keys loaded by termux that have default boolean behaviour + * "true" -> true + * "false" -> false + * default: false + * */ + public static final Set TERMUX_DEFAULT_BOOLEAN_BEHAVIOUR_PROPERTIES_LIST = new HashSet<>(Arrays.asList( + KEY_USE_FULLSCREEN, + KEY_USE_FULLSCREEN_WORKAROUND, + TermuxConstants.PROP_ALLOW_EXTERNAL_APPS + )); + + /** Defines the set for keys loaded by termux that have default inverted boolean behaviour + * "false" -> true + * "true" -> false + * default: true + * */ + public static final Set TERMUX_DEFAULT_INVERETED_BOOLEAN_BEHAVIOUR_PROPERTIES_LIST = new HashSet<>(Arrays.asList( + )); + + + + + /** Returns the first {@link File} found at + * {@link TermuxConstants#TERMUX_PROPERTIES_PRIMARY_PATH} or + * {@link TermuxConstants#TERMUX_PROPERTIES_SECONDARY_PATH} + * from which termux properties can be loaded. + * If the {@link File} found is not a regular file or is not readable then null is returned. + * + * @return Returns the {@link File} object for termux properties. + */ + public static File getTermuxPropertiesFile() { + String[] possiblePropertiesFileLocations = { + TermuxConstants.TERMUX_PROPERTIES_PRIMARY_PATH, + TermuxConstants.TERMUX_PROPERTIES_SECONDARY_PATH + }; + + File propertiesFile = new File(possiblePropertiesFileLocations[0]); + int i = 0; + while (!propertiesFile.exists() && i < possiblePropertiesFileLocations.length) { + propertiesFile = new File(possiblePropertiesFileLocations[i]); + i += 1; + } + + if (propertiesFile.isFile() && propertiesFile.canRead()) { + return propertiesFile; + } else { + Log.d("termux", "No readable termux.properties file found"); + return null; + } + } + +} diff --git a/app/src/main/java/com/termux/app/settings/properties/TermuxSharedProperties.java b/app/src/main/java/com/termux/app/settings/properties/TermuxSharedProperties.java new file mode 100644 index 00000000..4e2e9c7d --- /dev/null +++ b/app/src/main/java/com/termux/app/settings/properties/TermuxSharedProperties.java @@ -0,0 +1,569 @@ +package com.termux.app.settings.properties; + +import android.content.Context; +import android.content.res.Configuration; +import android.util.Log; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.termux.app.input.extrakeys.ExtraKeysInfo; +import com.termux.app.input.KeyboardShortcut; + +import org.json.JSONException; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import javax.annotation.Nonnull; + +public class TermuxSharedProperties implements SharedPropertiesParser { + + private final Context mContext; + private final SharedProperties mSharedProperties; + private final File mPropertiesFile; + + private ExtraKeysInfo mExtraKeysInfo; + private final List mSessionShortcuts = new ArrayList<>(); + + public TermuxSharedProperties(@Nonnull Context context) { + mContext = context; + mPropertiesFile = TermuxPropertyConstants.getTermuxPropertiesFile(); + mSharedProperties = new SharedProperties(context, mPropertiesFile, TermuxPropertyConstants.TERMUX_PROPERTIES_LIST, this); + loadTermuxPropertiesFromDisk(); + } + + + + + /** + * Reload the termux properties from disk into an in-memory cache. + */ + public void loadTermuxPropertiesFromDisk() { + mSharedProperties.loadPropertiesFromDisk(); + dumpPropertiesToLog(); + dumpInternalPropertiesToLog(); + setExtraKeys(); + setSessionShortcuts(); + } + + /** + * Set the terminal extra keys and style. + */ + private void setExtraKeys() { + mExtraKeysInfo = null; + + try { + // The mMap stores the extra key and style string values while loading properties + // Check {@link #getExtraKeysInternalPropertyValueFromValue(String)} and + // {@link #getExtraKeysStyleInternalPropertyValueFromValue(String)} + String extrakeys = (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS, true); + String extraKeysStyle = (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE, true); + mExtraKeysInfo = new ExtraKeysInfo(extrakeys, extraKeysStyle); + } catch (JSONException e) { + Toast.makeText(mContext, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: " + e.toString(), Toast.LENGTH_LONG).show(); + Log.e("termux", "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: ", e); + + try { + mExtraKeysInfo = new ExtraKeysInfo(TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE); + } catch (JSONException e2) { + Toast.makeText(mContext, "Can't create default extra keys", Toast.LENGTH_LONG).show(); + Log.e("termux", "Could create default extra keys: ", e); + mExtraKeysInfo = null; + } + } + } + + /** + * Set the terminal sessions shortcuts. + */ + private void setSessionShortcuts() { + mSessionShortcuts.clear(); + // The {@link TermuxPropertyConstants#MAP_SESSION_SHORTCUTS} stores the session shortcut key and action pair + for (Map.Entry entry : TermuxPropertyConstants.MAP_SESSION_SHORTCUTS.entrySet()) { + // The mMap stores the code points for the session shortcuts while loading properties + Integer codePoint = (Integer) getInternalPropertyValue(entry.getKey(), true); + // If codePoint is null, then session shortcut did not exist in properties or was invalid + // as parsed by {@link #getCodePointForSessionShortcuts(String,String)} + // If codePoint is not null, then get the action for the MAP_SESSION_SHORTCUTS key and + // add the code point to sessionShortcuts + if (codePoint != null) + mSessionShortcuts.add(new KeyboardShortcut(codePoint, entry.getValue())); + } + } + + + + + + /** + * A static function to get the {@link Properties} from the propertiesFile file. + * + * @param context The {@link Context} for the {@link SharedProperties#getPropertiesFromFile(Context,File)} call. + * @param propertiesFile The {@link File} to read the {@link Properties} from. + * @return Returns the {@link Properties} object. It will be {@code null} if an exception is + * raised while reading the file. + */ + public static Properties getPropertiesFromFile(Context context, File propertiesFile) { + return SharedProperties.getPropertiesFromFile(context, propertiesFile); + } + + /** + * A static function to get the {@link String} value for the {@link Properties} key read from + * the propertiesFile file. + * + * @param context The {@link Context} for the {@link SharedProperties#getPropertiesFromFile(Context,File)} call. + * @param propertiesFile The {@link File} to read the {@link Properties} from. + * @param key The key to read. + * @param def The default value. + * @return Returns the {@link String} object. This will be {@code null} if key is not found. + */ + public static String getPropertyValue(Context context, File propertiesFile, String key, String def) { + return (String) getDefaultIfNull(getDefaultIfNull(getPropertiesFromFile(context, propertiesFile), new Properties()).get(key), def); + } + + /** + * A static function to check if the value is {@code true} for {@link Properties} key read from + * the propertiesFile file. + * + * @param context The {@link Context} for the {@link SharedProperties#getPropertiesFromFile(Context,File)}call. + * @param propertiesFile The {@link File} to read the {@link Properties} from. + * @param key The key to read. + * @return Returns the {@code true} if the {@link Properties} key {@link String} value equals "true", + * regardless of case. If the key does not exist in the file or does not equal "true", then + * {@code false} will be returned. + */ + public static boolean isPropertyValueTrue(Context context, File propertiesFile, String key) { + return (boolean) getBooleanValueForStringValue((String) getPropertyValue(context, propertiesFile, key, null), false); + } + + /** + * A static function to check if the value is {@code false} for {@link Properties} key read from + * the propertiesFile file. + * + * @param context The {@link Context} for the {@link SharedProperties#getPropertiesFromFile(Context,File)} call. + * @param propertiesFile The {@link File} to read the {@link Properties} from. + * @param key The key to read. + * @return Returns the {@code true} if the {@link Properties} key {@link String} value equals "false", + * regardless of case. If the key does not exist in the file or does not equal "false", then + * {@code true} will be returned. + */ + public static boolean isPropertyValueFalse(Context context, File propertiesFile, String key) { + return (boolean) getInvertedBooleanValueForStringValue((String) getPropertyValue(context, propertiesFile, key, null), true); + } + + + + + + /** + * Get the {@link Properties} from the {@link #mPropertiesFile} file. + * + * @param cached If {@code true}, then the {@link Properties} in-memory cache is returned. + * Otherwise the {@link Properties} object is read directly from the + * {@link #mPropertiesFile} file. + * @return Returns the {@link Properties} object. It will be {@code null} if an exception is + * raised while reading the file. + */ + public Properties getProperties(boolean cached) { + return mSharedProperties.getProperties(cached); + } + + /** + * Get the {@link String} value for the key passed from the {@link #mPropertiesFile} file. + * + * @param key The key to read. + * @param def The default value. + * @param cached If {@code true}, then the value is returned from the the {@link Properties} in-memory cache. + * Otherwise the {@link Properties} object is read directly from the file + * and value is returned from it against the key. + * @return Returns the {@link String} object. This will be {@code null} if key is not found. + */ + public String getPropertyValue(String key, String def, boolean cached) { + return getDefaultIfNull(mSharedProperties.getProperty(key, cached), def); + } + + /** + * A function to check if the value is {@code true} for {@link Properties} key read from + * the {@link #mPropertiesFile} file. + * + * @param key The key to read. + * @param cached If {@code true}, then the value is checked from the the {@link Properties} in-memory cache. + * Otherwise the {@link Properties} object is read directly from the file + * and value is checked from it. + * @return Returns the {@code true} if the {@link Properties} key {@link String} value equals "true", + * regardless of case. If the key does not exist in the file or does not equal "true", then + * {@code false} will be returned. + */ + public boolean isPropertyValueTrue(String key, boolean cached) { + return (boolean) getBooleanValueForStringValue((String) getPropertyValue(key, null, cached), false); + } + + /** + * A function to check if the value is {@code false} for {@link Properties} key read from + * the {@link #mPropertiesFile} file. + * + * @param key The key to read. + * @param cached If {@code true}, then the value is checked from the the {@link Properties} in-memory cache. + * Otherwise the {@link Properties} object is read directly from the file + * and value is checked from it. + * @return Returns {@code true} if the {@link Properties} key {@link String} value equals "false", + * regardless of case. If the key does not exist in the file or does not equal "false", then + * {@code true} will be returned. + */ + public boolean isPropertyValueFalse(String key, boolean cached) { + return (boolean) getInvertedBooleanValueForStringValue((String) getPropertyValue(key, null, cached), true); + } + + + + + + /** + * Get the internal value {@link Object} {@link HashMap <>} in-memory cache for the + * {@link #mPropertiesFile} file. A call to {@link #loadTermuxPropertiesFromDisk()} must be made + * before this. + * + * @return Returns a copy of {@link Map} object. + */ + public Map getInternalProperties() { + return mSharedProperties.getInternalProperties(); + } + + /** + * Get the internal {@link Object} value for the key passed from the {@link #mPropertiesFile} file. + * If cache is {@code true}, then value is returned from the {@link HashMap <>} in-memory cache, + * so a call to {@link #loadTermuxPropertiesFromDisk()} must be made before this. + * + * @param key The key to read from the {@link HashMap<>} in-memory cache. + * @param cached If {@code true}, then the value is returned from the the {@link HashMap <>} in-memory cache, + * but if the value is null, then an attempt is made to return the default value. + * If {@code false}, then the {@link Properties} object is read directly from the file + * and internal value is returned for the property value against the key. + * @return Returns the {@link Object} object. This will be {@code null} if key is not found or + * the object stored against the key is {@code null}. + */ + public Object getInternalPropertyValue(String key, boolean cached) { + Object value; + if (cached) { + value = mSharedProperties.getInternalProperty(key); + // If the value is not null since key was found or if the value was null since the + // object stored for the key was itself null, we detect the later by checking if the key + // exists in the map. + if (value != null || mSharedProperties.getInternalProperties().containsKey(key)) { + return value; + } else { + // This should not happen normally unless mMap was modified after the + // {@link #loadTermuxPropertiesFromDisk()} call + // A null value can still be returned by + // {@link #getInternalPropertyValueFromValue(Context,String,String)} for some keys + value = getInternalPropertyValueFromValue(mContext, key, null); + Log.w("termux", "The value for \"" + key + "\" not found in SharedProperties cahce, force returning default value: `" + value + "`"); + return value; + } + } else { + // We get the property value directly from file and return its internal value + return getInternalPropertyValueFromValue(mContext, key, mSharedProperties.getProperty(key, false)); + } + } + + /** + * Override the + * {@link SharedPropertiesParser#getInternalPropertyValueFromValue(Context,String,String)} + * interface function. + */ + @Override + public Object getInternalPropertyValueFromValue(Context context, String key, String value) { + return getInternalTermuxPropertyValueFromValue(context, key, value); + } + + /** + * A static function that should return the internal termux {@link Object} for a key/value pair + * read from properties file. + * + * @param context The context for operations. + * @param key The key for which the internal object is required. + * @param value The literal value for the property found is the properties file. + * @return Returns the internal termux {@link Object} object. + */ + public static Object getInternalTermuxPropertyValueFromValue(Context context, String key, String value) { + if(key == null) return null; + /* + For keys where a MAP_* is checked by respective functions. Note that value to this function + would actually be the key for the MAP_*: + - If the value is currently null, then searching MAP_* should also return null and internal default value will be used. + - If the value is not null and does not exist in MAP_*, then internal default value will be used. + - If the value is not null and does exist in MAP_*, then internal value returned by map will be used. + */ + switch (key) { + // boolean + case TermuxPropertyConstants.KEY_USE_BACK_KEY_AS_ESCAPE_KEY: + return (boolean) getUseBackKeyAsEscapeKeyInternalPropertyValueFromValue(value); + case TermuxPropertyConstants.KEY_USE_BLACK_UI: + return (boolean) getUseBlackUIInternalPropertyValueFromValue(context, value); + case TermuxPropertyConstants.KEY_VIRTUAL_VOLUME_KEYS_DISABLED: + return (boolean) getVolumeKeysDisabledInternalPropertyValueFromValue(value); + + // int + case TermuxPropertyConstants.KEY_BELL_BEHAVIOUR: + return (int) getBellBehaviourInternalPropertyValueFromValue(value); + + // Integer (may be null) + case TermuxPropertyConstants.KEY_SHORTCUT_CREATE_SESSION: + case TermuxPropertyConstants.KEY_SHORTCUT_NEXT_SESSION: + case TermuxPropertyConstants.KEY_SHORTCUT_PREVIOUS_SESSION: + case TermuxPropertyConstants.KEY_SHORTCUT_RENAME_SESSION: + return (Integer) getCodePointForSessionShortcuts(key, value); + + // String (may be null) + case TermuxPropertyConstants.KEY_DEFAULT_WORKING_DIRECTORY: + return (String) getDefaultWorkingDirectoryInternalPropertyValueFromValue(value); + case TermuxPropertyConstants.KEY_EXTRA_KEYS: + return (String) getExtraKeysInternalPropertyValueFromValue(value); + case TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE: + return (String) getExtraKeysStyleInternalPropertyValueFromValue(value); + default: + // default boolean behaviour + if(TermuxPropertyConstants.TERMUX_DEFAULT_BOOLEAN_BEHAVIOUR_PROPERTIES_LIST.contains(key)) + return (boolean) getBooleanValueForStringValue(value, false); + // default inverted boolean behaviour + else if(TermuxPropertyConstants.TERMUX_DEFAULT_INVERETED_BOOLEAN_BEHAVIOUR_PROPERTIES_LIST.contains(key)) + return (boolean) getInvertedBooleanValueForStringValue(value, true); + // just use String object as is (may be null) + else + return value; + } + } + + + + + + /** + * Get the boolean value for the {@link String} value. + * + * @param value The {@link String} value to convert. + * @param def The default {@link boolean} value to return. + * @return Returns {@code true} or {@code false} if value is the literal string "true" or "false" respectively, + * regardless of case. Otherwise returns default value. + */ + public static boolean getBooleanValueForStringValue(String value, boolean def) { + return (boolean) getDefaultIfNull(TermuxPropertyConstants.MAP_GENERIC_BOOLEAN.get(toLowerCase(value)), def); + } + + /** + * Get the inverted boolean value for the {@link String} value. + * + * @param value The {@link String} value to convert. + * @param def The default {@link boolean} value to return. + * @return Returns {@code true} or {@code false} if value is the literal string "false" or "true" respectively, + * regardless of case. Otherwise returns default value. + */ + public static boolean getInvertedBooleanValueForStringValue(String value, boolean def) { + return (boolean) getDefaultIfNull(TermuxPropertyConstants.MAP_GENERIC_INVERTED_BOOLEAN.get(toLowerCase(value)), def); + } + + /** + * Returns {@code true} if value is not {@code null} and equals {@link TermuxPropertyConstants#VALUE_BACK_KEY_BEHAVIOUR_ESCAPE}, otherwise false. + * + * @param value The {@link String} value to convert. + * @return Returns the internal value for value. + */ + public static boolean getUseBackKeyAsEscapeKeyInternalPropertyValueFromValue(String value) { + return getDefaultIfNull(value, TermuxPropertyConstants.VALUE_BACK_KEY_BEHAVIOUR_BACK).equals(TermuxPropertyConstants.VALUE_BACK_KEY_BEHAVIOUR_ESCAPE); + } + + /** + * Returns {@code true} or {@code false} if value is the literal string "true" or "false" respectively regardless of case. + * Otherwise returns {@code true} if the night mode is currently enabled in the system. + * + * @param value The {@link String} value to convert. + * @return Returns the internal value for value. + */ + public static boolean getUseBlackUIInternalPropertyValueFromValue(Context context, String value) { + int nightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; + return getBooleanValueForStringValue(value, nightMode == Configuration.UI_MODE_NIGHT_YES); + } + + /** + * Returns {@code true} if value is not {@code null} and equals {@link TermuxPropertyConstants#VALUE_VOLUME_KEY_BEHAVIOUR_VOLUME}, otherwise {@code false}. + * + * @param value The {@link String} value to convert. + * @return Returns the internal value for value. + */ + public static boolean getVolumeKeysDisabledInternalPropertyValueFromValue(String value) { + return getDefaultIfNull(value, TermuxPropertyConstants.VALUE_VOLUME_KEY_BEHAVIOUR_VIRTUAL).equals(TermuxPropertyConstants.VALUE_VOLUME_KEY_BEHAVIOUR_VOLUME); + } + + /** + * Returns {@code true} if value is not {@code null} and equals {@link TermuxPropertyConstants#VALUE_BACK_KEY_BEHAVIOUR_ESCAPE}, otherwise {@code false}. + * + * @param value The {@link String} value to convert. + * @return Returns the internal value for value. + */ + public static int getBellBehaviourInternalPropertyValueFromValue(String value) { + return getDefaultIfNull(TermuxPropertyConstants.MAP_BELL_BEHAVIOUR.get(toLowerCase(value)), TermuxPropertyConstants.DEFAULT_IVALUE_BELL_BEHAVIOUR); + } + + /** + * Returns the code point for the value if key is not {@code null} and value is not {@code null} and is valid, + * otherwise returns {@code null}. + * + * @param key The key for session shortcut. + * @param value The {@link String} value to convert. + * @return Returns the internal value for value. + */ + public static Integer getCodePointForSessionShortcuts(String key, String value) { + if (key == null) return null; + if (value == null) return null; + String[] parts = value.toLowerCase().trim().split("\\+"); + String input = parts.length == 2 ? parts[1].trim() : null; + if (!(parts.length == 2 && parts[0].trim().equals("ctrl")) || input.isEmpty() || input.length() > 2) { + Log.e("termux", "Keyboard shortcut '" + key + "' is not Ctrl+"); + return null; + } + + char c = input.charAt(0); + int codePoint = c; + if (Character.isLowSurrogate(c)) { + if (input.length() != 2 || Character.isHighSurrogate(input.charAt(1))) { + Log.e("termux", "Keyboard shortcut '" + key + "' is not Ctrl+"); + return null; + } else { + codePoint = Character.toCodePoint(input.charAt(1), c); + } + } + + return codePoint; + } + + /** + * Returns the path itself if a directory exists at it and is readable, otherwise returns + * {@link TermuxPropertyConstants#DEFAULT_IVALUE_DEFAULT_WORKING_DIRECTORY}. + * + * @param path The {@link String} path to check. + * @return Returns the internal value for value. + */ + public static String getDefaultWorkingDirectoryInternalPropertyValueFromValue(String path) { + if (path == null || path.isEmpty()) return TermuxPropertyConstants.DEFAULT_IVALUE_DEFAULT_WORKING_DIRECTORY; + File workDir = new File(path); + if (!workDir.exists() || !workDir.isDirectory() || !workDir.canRead()) { + // Fallback to default directory if user configured working directory does not exist + // or is not a directory or is not readable. + return TermuxPropertyConstants.DEFAULT_IVALUE_DEFAULT_WORKING_DIRECTORY; + } else { + return path; + } + } + + /** + * Returns the value itself if it is not {@code null}, otherwise returns {@link TermuxPropertyConstants#DEFAULT_IVALUE_EXTRA_KEYS}. + * + * @param value The {@link String} value to convert. + * @return Returns the internal value for value. + */ + public static String getExtraKeysInternalPropertyValueFromValue(String value) { + return getDefaultIfNull(value, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS); + } + + /** + * Returns the value itself if it is not {@code null}, otherwise returns {@link TermuxPropertyConstants#DEFAULT_IVALUE_EXTRA_KEYS_STYLE}. + * + * @param value {@link String} value to convert. + * @return Returns the internal value for value. + */ + public static String getExtraKeysStyleInternalPropertyValueFromValue(String value) { + return getDefaultIfNull(value, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE); + } + + + + + + public boolean isBackKeyTheEscapeKey() { + return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_USE_BACK_KEY_AS_ESCAPE_KEY, true); + } + + public boolean isUsingBlackUI() { + return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_USE_BLACK_UI, true); + } + + public boolean isUsingFullScreen() { + return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_USE_FULLSCREEN, true); + } + + public boolean isUsingFullScreenWorkAround() { + return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_USE_FULLSCREEN_WORKAROUND, true); + } + + public boolean areVirtualVolumeKeysDisabled() { + return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_VIRTUAL_VOLUME_KEYS_DISABLED, true); + } + + public int getBellBehaviour() { + return (int) getInternalPropertyValue(TermuxPropertyConstants.KEY_BELL_BEHAVIOUR, true); + } + + public List getSessionShortcuts() { + return mSessionShortcuts; + } + + public String getDefaultWorkingDirectory() { + return (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_DEFAULT_WORKING_DIRECTORY, true); + } + + public ExtraKeysInfo getExtraKeysInfo() { + return mExtraKeysInfo; + } + + + + + + public static T getDefaultIfNull(@Nullable T object, @Nullable T def) { + return (object == null) ? def : object; + } + + private static String toLowerCase(String value) { + if (value == null) return null; else return value.toLowerCase(); + } + + + + + public void dumpPropertiesToLog() { + Properties properties = getProperties(true); + StringBuilder propertiesDump = new StringBuilder(); + + propertiesDump.append("Termux Properties:"); + if (properties != null) { + for (String key : properties.stringPropertyNames()) { + propertiesDump.append("\n").append(key).append(": `").append(properties.get(key)).append("`"); + } + } else { + propertiesDump.append(" null"); + } + + Log.d("termux", propertiesDump.toString()); + } + + public void dumpInternalPropertiesToLog() { + HashMap internalProperties = (HashMap) getInternalProperties(); + StringBuilder internalPropertiesDump = new StringBuilder(); + + internalPropertiesDump.append("Termux Internal Properties:"); + if (internalProperties != null) { + for (String key : internalProperties.keySet()) { + internalPropertiesDump.append("\n").append(key).append(": `").append(internalProperties.get(key)).append("`"); + } + } + + Log.d("termux", internalPropertiesDump.toString()); + } + +} From ef9e40630002829fae66f189be53301ce33d792f Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Thu, 11 Mar 2021 20:11:59 +0500 Subject: [PATCH 016/136] Update TermuxConstants The `TermuxConstants` and `TermuxPropertyConstants` classes have both been updated to `v0.2.0`. Check their Changelog sections for info on changes. Some other hardcoded termux paths have been removed as well and are now referenced from `TermuxConstants` class. --- .../java/com/termux/app/BackgroundJob.java | 18 +- .../com/termux/app/RunCommandService.java | 12 +- .../java/com/termux/app/TermuxActivity.java | 4 +- .../java/com/termux/app/TermuxConstants.java | 341 ++++++++++++++---- .../java/com/termux/app/TermuxInstaller.java | 6 +- .../com/termux/app/TermuxOpenReceiver.java | 2 +- .../java/com/termux/app/TermuxService.java | 8 +- .../properties/TermuxPropertyConstants.java | 25 +- .../filepicker/TermuxDocumentsProvider.java | 4 +- .../TermuxFileReceiverActivity.java | 6 +- 10 files changed, 310 insertions(+), 116 deletions(-) diff --git a/app/src/main/java/com/termux/app/BackgroundJob.java b/app/src/main/java/com/termux/app/BackgroundJob.java index bb4e19a8..3f502d16 100644 --- a/app/src/main/java/com/termux/app/BackgroundJob.java +++ b/app/src/main/java/com/termux/app/BackgroundJob.java @@ -36,7 +36,7 @@ public final class BackgroundJob { public BackgroundJob(String cwd, String fileToExecute, final String[] args, final TermuxService service, PendingIntent pendingIntent) { String[] env = buildEnvironment(false, cwd); - if (cwd == null || cwd.isEmpty()) cwd = TermuxConstants.HOME_PATH; + if (cwd == null || cwd.isEmpty()) cwd = TermuxConstants.TERMUX_HOME_DIR_PATH; final String[] progArray = setupProcessArgs(fileToExecute, args); final String processDescription = Arrays.toString(progArray); @@ -134,17 +134,17 @@ public final class BackgroundJob { } static String[] buildEnvironment(boolean failSafe, String cwd) { - new File(TermuxConstants.HOME_PATH).mkdirs(); + TermuxConstants.TERMUX_HOME_DIR.mkdirs(); - if (cwd == null || cwd.isEmpty()) cwd = TermuxConstants.HOME_PATH; + if (cwd == null || cwd.isEmpty()) cwd = TermuxConstants.TERMUX_HOME_DIR_PATH; List environment = new ArrayList<>(); environment.add("TERMUX_VERSION=" + BuildConfig.VERSION_NAME); environment.add("TERM=xterm-256color"); environment.add("COLORTERM=truecolor"); - environment.add("HOME=" + TermuxConstants.HOME_PATH); - environment.add("PREFIX=" + TermuxConstants.PREFIX_PATH); + environment.add("HOME=" + TermuxConstants.TERMUX_HOME_DIR_PATH); + environment.add("PREFIX=" + TermuxConstants.TERMUX_PREFIX_DIR_PATH); environment.add("BOOTCLASSPATH=" + System.getenv("BOOTCLASSPATH")); environment.add("ANDROID_ROOT=" + System.getenv("ANDROID_ROOT")); environment.add("ANDROID_DATA=" + System.getenv("ANDROID_DATA")); @@ -164,9 +164,9 @@ public final class BackgroundJob { environment.add("PATH= " + System.getenv("PATH")); } else { environment.add("LANG=en_US.UTF-8"); - environment.add("PATH=" + TermuxConstants.PREFIX_PATH + "/bin"); + environment.add("PATH=" + TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH); environment.add("PWD=" + cwd); - environment.add("TMPDIR=" + TermuxConstants.PREFIX_PATH + "/tmp"); + environment.add("TMPDIR=" + TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH); } return environment.toArray(new String[0]); @@ -215,7 +215,7 @@ public final class BackgroundJob { if (executable.startsWith("/usr") || executable.startsWith("/bin")) { String[] parts = executable.split("/"); String binary = parts[parts.length - 1]; - interpreter = TermuxConstants.PREFIX_PATH + "/bin/" + binary; + interpreter = TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/" + binary; } break; } @@ -225,7 +225,7 @@ public final class BackgroundJob { } } else { // No shebang and no ELF, use standard shell. - interpreter = TermuxConstants.PREFIX_PATH + "/bin/sh"; + interpreter = TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/sh"; } } } diff --git a/app/src/main/java/com/termux/app/RunCommandService.java b/app/src/main/java/com/termux/app/RunCommandService.java index eb6efc21..d1c523ee 100644 --- a/app/src/main/java/com/termux/app/RunCommandService.java +++ b/app/src/main/java/com/termux/app/RunCommandService.java @@ -184,9 +184,9 @@ public class RunCommandService extends Service { } private boolean allowExternalApps() { - File propsFile = new File(TermuxConstants.TERMUX_PROPERTIES_PRIMARY_PATH); + File propsFile = new File(TermuxConstants.TERMUX_PROPERTIES_PRIMARY_FILE_PATH); if (!propsFile.exists()) - propsFile = new File(TermuxConstants.TERMUX_PROPERTIES_SECONDARY_PATH); + propsFile = new File(TermuxConstants.TERMUX_PROPERTIES_SECONDARY_FILE_PATH); Properties props = new Properties(); try { @@ -205,10 +205,10 @@ public class RunCommandService extends Service { /** Replace "$PREFIX/" or "~/" prefix with termux absolute paths */ public static String getExpandedTermuxPath(String path) { if(path != null && !path.isEmpty()) { - path = path.replaceAll("^\\$PREFIX$", TermuxConstants.PREFIX_PATH); - path = path.replaceAll("^\\$PREFIX/", TermuxConstants.PREFIX_PATH + "/"); - path = path.replaceAll("^~/$", TermuxConstants.HOME_PATH); - path = path.replaceAll("^~/", TermuxConstants.HOME_PATH + "/"); + path = path.replaceAll("^\\$PREFIX$", TermuxConstants.TERMUX_PREFIX_DIR_PATH); + path = path.replaceAll("^\\$PREFIX/", TermuxConstants.TERMUX_PREFIX_DIR_PATH + "/"); + path = path.replaceAll("^~/$", TermuxConstants.TERMUX_HOME_DIR_PATH); + path = path.replaceAll("^~/", TermuxConstants.TERMUX_HOME_DIR_PATH + "/"); } return path; diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java index e3396ab0..2c50350e 100644 --- a/app/src/main/java/com/termux/app/TermuxActivity.java +++ b/app/src/main/java/com/termux/app/TermuxActivity.java @@ -167,8 +167,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection void checkForFontAndColors() { try { - File colorsFile = new File(TermuxConstants.COLOR_PROPERTIES_PATH); - File fontFile = new File(TermuxConstants.FONT_PATH); + File colorsFile = TermuxConstants.TERMUX_COLOR_PROPERTIES_FILE; + File fontFile = TermuxConstants.TERMUX_FONT_FILE; final Properties props = new Properties(); if (colorsFile.isFile()) { diff --git a/app/src/main/java/com/termux/app/TermuxConstants.java b/app/src/main/java/com/termux/app/TermuxConstants.java index cf4da908..d1c3acb6 100644 --- a/app/src/main/java/com/termux/app/TermuxConstants.java +++ b/app/src/main/java/com/termux/app/TermuxConstants.java @@ -4,13 +4,33 @@ import android.annotation.SuppressLint; import java.io.File; -// Version: v0.1.0 +/* + * Version: v0.2.0 + * + * Changelog + * + * - 0.1.0 (2021-03-08) + * - Initial Release. + * + * - 0.2.0 (2021-03-11) + * - Added `_DIR` and `_FILE` substrings to paths. + * - Add INTERNAL_PRIVATE_APP_DATA_DIR*, TERMUX_CACHE_DIR*, TERMUX_DATABASES_DIR*, + * TERMUX_SHARED_PREFERENCES_DIR*, TERMUX_BIN_PREFIX_DIR*, TERMUX_ETC_DIR*, TERMUX_INCLUDE_DIR*, + * TERMUX_LIB_DIR*, TERMUX_LIBEXEC_DIR*, TERMUX_SHARE_DIR*, TERMUX_TMP_DIR*, TERMUX_VAR_DIR*, + * TERMUX_STAGING_PREFIX_DIR*, TERMUX_STORAGE_HOME_DIR*, TERMUX_DEFAULT_PREFERENCES_FILE_BASENAME*, + * TERMUX_DEFAULT_PREFERENCES_FILE + * - Renamed `DATA_HOME_PATH` to `TERMUX_DATA_HOME_DIR_PATH`. + * - Renamed `CONFIG_HOME_PATH` to `TERMUX_CONFIG_HOME_DIR_PATH`. + * - Updated javadocs and spacing. + * + */ /** * A class that defines shared constants of the Termux app and its plugins. * This class will be hosted by termux-app and should be imported by other termux plugin apps as is * instead of copying constants to random classes. The 3rd party apps can also import it for - * interacting with termux apps. + * interacting with termux apps. If changes are made to this file, increment the version number + * and add an entry in the Changelog section above. * * Termux app default package name is "com.termux" and is used in PREFIX_PATH. * The binaries compiled for termux have PREFIX_PATH hardcoded in them but it can be changed during @@ -39,175 +59,348 @@ import java.io.File; * TERMUX_PACKAGE_NAME must be used in source code of Termux app and its plugins instead of hardcoded * "com.termux" paths. */ - public final class TermuxConstants { - /** - * Termux app and plugin app and package names. + /* + * Termux and its plugin app and package names. */ + /** Termux app name */ public static final String TERMUX_APP_NAME = "Termux"; // Default: "Termux" + /** Termux app package name */ public static final String TERMUX_PACKAGE_NAME = "com.termux"; // Default: "com.termux" + + /** Termux API app name */ public static final String TERMUX_API_APP_NAME = "Termux:API"; // Default: "Termux:API" + /** Termux API app package name */ public static final String TERMUX_API_PACKAGE_NAME = TERMUX_PACKAGE_NAME + ".api"; // Default: "com.termux.api" + + /** Termux Boot app name */ public static final String TERMUX_BOOT_APP_NAME = "Termux:Boot"; // Default: "Termux:Boot" + /** Termux Boot app package name */ public static final String TERMUX_BOOT_PACKAGE_NAME = TERMUX_PACKAGE_NAME + ".boot"; // Default: "com.termux.boot" + + /** Termux Float app name */ public static final String TERMUX_FLOAT_APP_NAME = "Termux:Float"; // Default: "Termux:Float" + /** Termux Float app package name */ public static final String TERMUX_FLOAT_PACKAGE_NAME = TERMUX_PACKAGE_NAME + ".window"; // Default: "com.termux.window" + + /** Termux Styling app name */ public static final String TERMUX_STYLING_APP_NAME = "Termux:Styling"; // Default: "Termux:Styling" + /** Termux Styling app package name */ public static final String TERMUX_STYLING_PACKAGE_NAME = TERMUX_PACKAGE_NAME + ".styling"; // Default: "com.termux.styling" + + /** Termux Plugin app name */ public static final String TERMUX_TASKER_APP_NAME = "Termux:Tasker"; // Default: "Termux:Tasker" + /** Termux Plugin app package name */ public static final String TERMUX_TASKER_PACKAGE_NAME = TERMUX_PACKAGE_NAME + ".tasker"; // Default: "com.termux.tasker" + + /** Termux Widget app name */ public static final String TERMUX_WIDGET_APP_NAME = "Termux:Widget"; // Default: "Termux:Widget" + /** Termux Widget app package name */ public static final String TERMUX_WIDGET_PACKAGE_NAME = TERMUX_PACKAGE_NAME + ".widget"; // Default: "com.termux.widget" - /** - * Termux app core paths. + + + /* + * Termux app core directory paths. */ + /** Termux app internal private app data directory path */ @SuppressLint("SdCardPath") - public static final String FILES_PATH = "/data/data/" + TERMUX_PACKAGE_NAME + "/files"; // Default: "/data/data/com.termux/files" - public static final String PREFIX_PATH = FILES_PATH + "/usr"; // Termux $PREFIX path. Default: "/data/data/com.termux/files/usr" - public static final String HOME_PATH = FILES_PATH + "/home"; // Termux $HOME path. Default: "/data/data/com.termux/files/home" - public static final String DATA_HOME_PATH = HOME_PATH + "/.termux"; // Default: "/data/data/com.termux/files/home/.termux" - public static final String CONFIG_HOME_PATH = HOME_PATH + "/.config/termux"; // Default: "/data/data/com.termux/files/home/.config/termux" + public static final String INTERNAL_PRIVATE_APP_DATA_DIR_PATH = "/data/data/" + TERMUX_PACKAGE_NAME; // Default: "/data/data/com.termux" + /** Termux app internal private app data directory */ + public static final File INTERNAL_PRIVATE_APP_DATA_DIR = new File(INTERNAL_PRIVATE_APP_DATA_DIR_PATH); + + + /** Termux app cache directory path */ + public static final String TERMUX_CACHE_DIR_PATH = INTERNAL_PRIVATE_APP_DATA_DIR_PATH + "/cache"; // Default: "/data/data/com.termux/cache" + /** Termux app cache directory */ + public static final File TERMUX_CACHE_DIR = new File(TERMUX_CACHE_DIR_PATH); + + + /** Termux app database directory path */ + public static final String TERMUX_DATABASES_DIR_PATH = INTERNAL_PRIVATE_APP_DATA_DIR_PATH + "/databases"; // Default: "/data/data/com.termux/databases" + /** Termux app database directory */ + public static final File TERMUX_DATABASES_DIR = new File(TERMUX_DATABASES_DIR_PATH); + + + /** Termux app shared preferences directory path */ + public static final String TERMUX_SHARED_PREFERENCES_DIR_PATH = INTERNAL_PRIVATE_APP_DATA_DIR_PATH + "/shared_prefs"; // Default: "/data/data/com.termux/shared_prefs" + /** Termux app shared preferences directory */ + public static final File TERMUX_SHARED_PREFERENCES_DIR = new File(TERMUX_SHARED_PREFERENCES_DIR_PATH); + + + /** Termux app Files directory path */ + public static final String TERMUX_FILES_DIR_PATH = INTERNAL_PRIVATE_APP_DATA_DIR_PATH + "/files"; // Default: "/data/data/com.termux/files" + /** Termux app Files directory */ + public static final File TERMUX_FILES_DIR = new File(TERMUX_FILES_DIR_PATH); - /** + /** Termux app $PREFIX directory path */ + public static final String TERMUX_PREFIX_DIR_PATH = TERMUX_FILES_DIR_PATH + "/usr"; // Default: "/data/data/com.termux/files/usr" + /** Termux app $PREFIX directory */ + public static final File TERMUX_PREFIX_DIR = new File(TERMUX_PREFIX_DIR_PATH); + + + /** Termux app $PREFIX/bin directory path */ + public static final String TERMUX_BIN_PREFIX_DIR_PATH = TERMUX_PREFIX_DIR_PATH + "/bin"; // Default: "/data/data/com.termux/files/usr/bin" + /** Termux app $PREFIX/bin directory */ + public static final File TERMUX_BIN_PREFIX_DIR = new File(TERMUX_BIN_PREFIX_DIR_PATH); + + + /** Termux app $PREFIX/etc directory path */ + public static final String TERMUX_ETC_PREFIX_DIR_PATH = TERMUX_PREFIX_DIR_PATH + "/etc"; // Default: "/data/data/com.termux/files/usr/etc" + /** Termux app $PREFIX/etc directory */ + public static final File TERMUX_ETC_DIR = new File(TERMUX_ETC_PREFIX_DIR_PATH); + + + /** Termux app $PREFIX/include directory path */ + public static final String TERMUX_INCLUDE_PREFIX_DIR_PATH = TERMUX_PREFIX_DIR_PATH + "/include"; // Default: "/data/data/com.termux/files/usr/include" + /** Termux app $PREFIX/include directory */ + public static final File TERMUX_INCLUDE_DIR = new File(TERMUX_INCLUDE_PREFIX_DIR_PATH); + + + /** Termux app $PREFIX/lib directory path */ + public static final String TERMUX_LIB_PREFIX_DIR_PATH = TERMUX_PREFIX_DIR_PATH + "/lib"; // Default: "/data/data/com.termux/files/usr/lib" + /** Termux app $PREFIX/lib directory */ + public static final File TERMUX_LIB_DIR = new File(TERMUX_LIB_PREFIX_DIR_PATH); + + + /** Termux app $PREFIX/libexec directory path */ + public static final String TERMUX_LIBEXEC_PREFIX_DIR_PATH = TERMUX_PREFIX_DIR_PATH + "/libexec"; // Default: "/data/data/com.termux/files/usr/libexec" + /** Termux app $PREFIX/libexec directory */ + public static final File TERMUX_LIBEXEC_DIR = new File(TERMUX_LIBEXEC_PREFIX_DIR_PATH); + + + /** Termux app $PREFIX/share directory path */ + public static final String TERMUX_SHARE_PREFIX_DIR_PATH = TERMUX_PREFIX_DIR_PATH + "/share"; // Default: "/data/data/com.termux/files/usr/share" + /** Termux app $PREFIX/share directory */ + public static final File TERMUX_SHARE_DIR = new File(TERMUX_SHARE_PREFIX_DIR_PATH); + + + /** Termux app $PREFIX/tmp and $TMP directory path */ + public static final String TERMUX_TMP_PREFIX_DIR_PATH = TERMUX_PREFIX_DIR_PATH + "/tmp"; // Default: "/data/data/com.termux/files/usr/tmp" + /** Termux app $PREFIX/tmp and $TMP directory */ + public static final File TERMUX_TMP_DIR = new File(TERMUX_TMP_PREFIX_DIR_PATH); + + + /** Termux app $PREFIX/var directory path */ + public static final String TERMUX_VAR_PREFIX_DIR_PATH = TERMUX_PREFIX_DIR_PATH + "/var"; // Default: "/data/data/com.termux/files/usr/var" + /** Termux app $PREFIX/var directory */ + public static final File TERMUX_VAR_DIR = new File(TERMUX_VAR_PREFIX_DIR_PATH); + + + + /** Termux app usr-staging directory path */ + public static final String TERMUX_STAGING_PREFIX_DIR_PATH = TERMUX_FILES_DIR_PATH + "/usr-staging"; // Default: "/data/data/com.termux/files/usr-staging" + /** Termux app usr-staging directory */ + public static final File TERMUX_STAGING_PREFIX_DIR = new File(TERMUX_STAGING_PREFIX_DIR_PATH); + + + + /** Termux app $HOME directory path */ + public static final String TERMUX_HOME_DIR_PATH = TERMUX_FILES_DIR_PATH + "/home"; // Default: "/data/data/com.termux/files/home" + /** Termux app $HOME directory */ + public static final File TERMUX_HOME_DIR = new File(TERMUX_HOME_DIR_PATH); + + + /** Termux app config home directory path */ + public static final String TERMUX_CONFIG_HOME_DIR_PATH = TERMUX_HOME_DIR_PATH + "/.config/termux"; // Default: "/data/data/com.termux/files/home/.config/termux" + /** Termux app config home directory */ + public static final File TERMUX_CONFIG_HOME_DIR = new File(TERMUX_CONFIG_HOME_DIR_PATH); + + + /** Termux app data home directory path */ + public static final String TERMUX_DATA_HOME_DIR_PATH = TERMUX_HOME_DIR_PATH + "/.termux"; // Default: "/data/data/com.termux/files/home/.termux" + /** Termux app data home directory */ + public static final File TERMUX_DATA_HOME_DIR = new File(TERMUX_DATA_HOME_DIR_PATH); + + + /** Termux app storage home directory path */ + public static final String TERMUX_STORAGE_HOME_DIR_PATH = TERMUX_HOME_DIR_PATH + "/storage"; // Default: "/data/data/com.termux/files/home/storage" + /** Termux app storage home directory */ + public static final File TERMUX_STORAGE_HOME_DIR = new File(TERMUX_STORAGE_HOME_DIR_PATH); + + + + + + /* + * Termux app core file paths. + */ + + /* Termux app default SharedPreferences file basename */ + public static final String TERMUX_DEFAULT_PREFERENCES_FILE_BASENAME = TERMUX_PACKAGE_NAME + "_preferences.xml"; // Default: "com.termux_preferences.xml" + + /* Termux app default SharedPreferences file path */ + public static final String TERMUX_DEFAULT_PREFERENCES_FILE_PATH = TERMUX_SHARED_PREFERENCES_DIR_PATH + "/" + TERMUX_DEFAULT_PREFERENCES_FILE_BASENAME; // Default: "/data/data/com.termux/shared_prefs/com.termux_preferences.xml" + /* Termux app default SharedPreferences file */ + public static final File TERMUX_DEFAULT_PREFERENCES_FILE = new File(TERMUX_DEFAULT_PREFERENCES_FILE_PATH); + + /* Termux app termux.properties primary file path */ + public static final String TERMUX_PROPERTIES_PRIMARY_FILE_PATH = TERMUX_DATA_HOME_DIR_PATH + "/termux.properties"; // Default: "/data/data/com.termux/files/home/.termux/termux.properties" + /* Termux app termux.properties primary file */ + public static final File TERMUX_PROPERTIES_PRIMARY_FILE = new File(TERMUX_PROPERTIES_PRIMARY_FILE_PATH); + + /* Termux app termux.properties secondary file path */ + public static final String TERMUX_PROPERTIES_SECONDARY_FILE_PATH = TERMUX_CONFIG_HOME_DIR_PATH + "/termux.properties"; // Default: "/data/data/com.termux/files/home/.config/termux/termux.properties" + /* Termux app termux.properties secondary file */ + public static final File TERMUX_PROPERTIES_SECONDARY_FILE = new File(TERMUX_PROPERTIES_SECONDARY_FILE_PATH); + + /* Termux app and Termux:Styling colors.properties file path */ + public static final String TERMUX_COLOR_PROPERTIES_FILE_PATH = TERMUX_DATA_HOME_DIR_PATH + "/colors.properties"; // Default: "/data/data/com.termux/files/home/.termux/colors.properties" + /* Termux app and Termux:Styling colors.properties file */ + public static final File TERMUX_COLOR_PROPERTIES_FILE = new File(TERMUX_COLOR_PROPERTIES_FILE_PATH); + + /* Termux app and Termux:Styling font.ttf file path */ + public static final String TERMUX_FONT_FILE_PATH = TERMUX_DATA_HOME_DIR_PATH + "/font.ttf"; // Default: "/data/data/com.termux/files/home/.termux/font.ttf" + /* Termux app and Termux:Styling font.ttf file */ + public static final File TERMUX_FONT_FILE = new File(TERMUX_FONT_FILE_PATH); + + + + + + /* * Termux app plugin specific paths. */ - // Path to store scripts to be run at boot by Termux:Boot - public static final String BOOT_SCRIPTS_PATH = DATA_HOME_PATH + "/boot"; // Default: "/data/data/com.termux/files/home/.termux/boot" + /* Termux app directory path to store scripts to be run at boot by Termux:Boot */ + public static final String TERMUX_BOOT_SCRIPTS_DIR_PATH = TERMUX_DATA_HOME_DIR_PATH + "/boot"; // Default: "/data/data/com.termux/files/home/.termux/boot" + /* Termux app directory to store scripts to be run at boot by Termux:Boot */ + public static final File TERMUX_BOOT_SCRIPTS_DIR = new File(TERMUX_BOOT_SCRIPTS_DIR_PATH); - // Path to store foreground scripts that can be run by the termux launcher widget provided by Termux:Widget - public static final String SHORTCUT_SCRIPTS_PATH = DATA_HOME_PATH + "/shortcuts"; // Default: "/data/data/com.termux/files/home/.termux/shortcuts" - // Path to store background scripts that can be run by the termux launcher widget provided by Termux:Widget - public static final String SHORTCUT_TASKS_SCRIPTS_PATH = DATA_HOME_PATH + "/shortcuts/tasks"; // Default: "/data/data/com.termux/files/home/.termux/shortcuts/tasks" + /* Termux app directory path to store foreground scripts that can be run by the termux launcher widget provided by Termux:Widget */ + public static final String TERMUX_SHORTCUT_SCRIPTS_DIR_PATH = TERMUX_DATA_HOME_DIR_PATH + "/shortcuts"; // Default: "/data/data/com.termux/files/home/.termux/shortcuts" + /* Termux app directory to store foreground scripts that can be run by the termux launcher widget provided by Termux:Widget */ + public static final File TERMUX_SHORTCUT_SCRIPTS_DIR = new File(TERMUX_SHORTCUT_SCRIPTS_DIR_PATH); - // Path to store scripts to be run by 3rd party twofortyfouram locale plugin host apps like Tasker app via the Termux:Tasker plugin client - public static final String TASKER_SCRIPTS_PATH = DATA_HOME_PATH + "/tasker"; // Default: "/data/data/com.termux/files/home/.termux/tasker" - // Termux app termux.properties primary path - public static final String TERMUX_PROPERTIES_PRIMARY_PATH = DATA_HOME_PATH + "/termux.properties"; // Default: "/data/data/com.termux/files/home/.termux/termux.properties" + /* Termux app directory path to store background scripts that can be run by the termux launcher widget provided by Termux:Widget */ + public static final String TERMUX_SHORTCUT_TASKS_SCRIPTS_DIR_PATH = TERMUX_DATA_HOME_DIR_PATH + "/shortcuts/tasks"; // Default: "/data/data/com.termux/files/home/.termux/shortcuts/tasks" + /* Termux app directory to store background scripts that can be run by the termux launcher widget provided by Termux:Widget */ + public static final File TERMUX_SHORTCUT_TASKS_SCRIPTS_DIR = new File(TERMUX_SHORTCUT_TASKS_SCRIPTS_DIR_PATH); - // Termux app termux.properties secondary path - public static final String TERMUX_PROPERTIES_SECONDARY_PATH = CONFIG_HOME_PATH + "/termux.properties"; // Default: "/data/data/com.termux/files/home/.config/termux/termux.properties" - // Termux app and Termux:Styling colors.properties path - public static final String COLOR_PROPERTIES_PATH = DATA_HOME_PATH + "/colors.properties"; // Default: "/data/data/com.termux/files/home/.termux/colors.properties" - - // Termux app and Termux:Styling font.ttf path - public static final String FONT_PATH = DATA_HOME_PATH + "/font.ttf"; // Default: "/data/data/com.termux/files/home/.termux/font.ttf" + /* Termux app directory path to store scripts to be run by 3rd party twofortyfouram locale plugin host apps like Tasker app via the Termux:Tasker plugin client */ + public static final String TERMUX_TASKER_SCRIPTS_DIR_PATH = TERMUX_DATA_HOME_DIR_PATH + "/tasker"; // Default: "/data/data/com.termux/files/home/.termux/tasker" + /* Termux app directory to store scripts to be run by 3rd party twofortyfouram locale plugin host apps like Tasker app via the Termux:Tasker plugin client */ + public static final File TERMUX_TASKER_SCRIPTS_DIR = new File(TERMUX_TASKER_SCRIPTS_DIR_PATH); - /** - * Termux app plugin specific path File objects. + + + /* + * Termux app and plugins miscellaneous variables. */ - public static final File FILES_DIR = new File(FILES_PATH); - public static final File PREFIX_DIR = new File(PREFIX_PATH); - public static final File HOME_DIR = new File(HOME_PATH); - public static final File DATA_HOME_DIR = new File(DATA_HOME_PATH); - public static final File CONFIG_HOME_DIR = new File(CONFIG_HOME_PATH); - public static final File BOOT_SCRIPTS_DIR = new File(BOOT_SCRIPTS_PATH); - public static final File SHORTCUT_SCRIPTS_DIR = new File(SHORTCUT_SCRIPTS_PATH); - public static final File SHORTCUT_TASKS_SCRIPTS_DIR = new File(SHORTCUT_TASKS_SCRIPTS_PATH); - public static final File TASKER_SCRIPTS_DIR = new File(TASKER_SCRIPTS_PATH); - - - - // Android OS permission declared by Termux app in AndroidManifest.xml which can be requested by 3rd party apps to run various commands in Termux app context + /* Android OS permission declared by Termux app in AndroidManifest.xml which can be requested by 3rd party apps to run various commands in Termux app context */ public static final String PERMISSION_RUN_COMMAND = TERMUX_PACKAGE_NAME + ".permission.RUN_COMMAND"; // Default: "com.termux.permission.RUN_COMMAND" - // Termux property defined in termux.properties file as a secondary check to PERMISSION_RUN_COMMAND to allow 3rd party apps to run various commands in Termux app context + /* Termux property defined in termux.properties file as a secondary check to PERMISSION_RUN_COMMAND to allow 3rd party apps to run various commands in Termux app context */ public static final String PROP_ALLOW_EXTERNAL_APPS = "allow-external-apps"; // Default: "allow-external-apps" + /* Default value for {@link #PROP_ALLOW_EXTERNAL_APPS} */ public static final String PROP_DEFAULT_VALUE_ALLOW_EXTERNAL_APPS = "false"; // Default: "false" + + + + /** * Termux app constants. */ - public static final class TERMUX_APP { + /** Termux app core activity name. */ + public static final String TERMUX_ACTIVITY_NAME = TERMUX_PACKAGE_NAME + ".app.TermuxActivity"; // Default: "com.termux.app.TermuxActivity" + /** * Termux app core activity. */ - - public static final String TERMUX_ACTIVITY_NAME = TERMUX_PACKAGE_NAME + ".app.TermuxActivity"; // Default: "com.termux.app.TermuxActivity" public static final class TERMUX_ACTIVITY { - // Intent action to start termux failsafe session + /* Intent action to start termux failsafe session */ public static final String ACTION_FAILSAFE_SESSION = TermuxConstants.TERMUX_PACKAGE_NAME + ".app.failsafe_session"; // Default: "com.termux.app.failsafe_session" - // Intent action to make termux reload its termux session styling + + /* Intent action to make termux reload its termux session styling */ public static final String ACTION_RELOAD_STYLE = TermuxConstants.TERMUX_PACKAGE_NAME + ".app.reload_style"; // Default: "com.termux.app.reload_style" - // Intent extra for what to reload for the TERMUX_ACTIVITY.ACTION_RELOAD_STYLE intent + /* Intent extra for what to reload for the TERMUX_ACTIVITY.ACTION_RELOAD_STYLE intent */ public static final String EXTRA_RELOAD_STYLE = TermuxConstants.TERMUX_PACKAGE_NAME + ".app.reload_style"; // Default: "com.termux.app.reload_style" } + + + /** Termux app core service name. */ + public static final String TERMUX_SERVICE_NAME = TERMUX_PACKAGE_NAME + ".app.TermuxService"; // Default: "com.termux.app.TermuxService" + /** * Termux app core service. */ - - public static final String TERMUX_SERVICE_NAME = TERMUX_PACKAGE_NAME + ".app.TermuxService"; // Default: "com.termux.app.TermuxService" public static final class TERMUX_SERVICE { - // Intent action to stop TERMUX_SERVICE + /* Intent action to stop TERMUX_SERVICE */ public static final String ACTION_STOP_SERVICE = TERMUX_PACKAGE_NAME + ".service_stop"; // Default: "com.termux.service_stop" - // Intent action to make TERMUX_SERVICE acquire a wakelock + + /* Intent action to make TERMUX_SERVICE acquire a wakelock */ public static final String ACTION_WAKE_LOCK = TERMUX_PACKAGE_NAME + ".service_wake_lock"; // Default: "com.termux.service_wake_lock" - // Intent action to make TERMUX_SERVICE release wakelock + + /* Intent action to make TERMUX_SERVICE release wakelock */ public static final String ACTION_WAKE_UNLOCK = TERMUX_PACKAGE_NAME + ".service_wake_unlock"; // Default: "com.termux.service_wake_unlock" - // Intent action to execute command with TERMUX_SERVICE + + /* Intent action to execute command with TERMUX_SERVICE */ public static final String ACTION_SERVICE_EXECUTE = TERMUX_PACKAGE_NAME + ".service_execute"; // Default: "com.termux.service_execute" - // Uri scheme for paths sent via intent to TERMUX_SERVICE + /* Uri scheme for paths sent via intent to TERMUX_SERVICE */ public static final String URI_SCHEME_SERVICE_EXECUTE = TERMUX_PACKAGE_NAME + ".file"; // Default: "com.termux.file" - // Intent extra for command arguments for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent + /* Intent extra for command arguments for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */ public static final String EXTRA_ARGUMENTS = TERMUX_PACKAGE_NAME + ".execute.arguments"; // Default: "com.termux.execute.arguments" - // Intent extra for command current working directory for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent + /* Intent extra for command current working directory for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */ public static final String EXTRA_WORKDIR = TERMUX_PACKAGE_NAME + ".execute.cwd"; // Default: "com.termux.execute.cwd" - // Intent extra for command background mode for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent + /* Intent extra for command background mode for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */ public static final String EXTRA_BACKGROUND = TERMUX_PACKAGE_NAME + ".execute.background"; // Default: "com.termux.execute.background" } - /** - * Termux app service to receive commands sent by 3rd party apps. - */ + + /** Termux app run command service name. */ public static final String RUN_COMMAND_SERVICE_NAME = TERMUX_PACKAGE_NAME + ".app.RunCommandService"; // Termux app service to receive commands from 3rd party apps "com.termux.app.RunCommandService" + + /** + * Termux app run command service to receive commands sent by 3rd party apps. + */ public static final class RUN_COMMAND_SERVICE { - // Intent action to execute command with RUN_COMMAND_SERVICE + /* Intent action to execute command with RUN_COMMAND_SERVICE */ public static final String ACTION_RUN_COMMAND = TERMUX_PACKAGE_NAME + ".RUN_COMMAND"; // Default: "com.termux.RUN_COMMAND" - // Intent extra for command path for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent + /* Intent extra for command path for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */ public static final String EXTRA_COMMAND_PATH = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_PATH"; // Default: "com.termux.RUN_COMMAND_PATH" - // Intent extra for command arguments for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent + /* Intent extra for command arguments for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */ public static final String EXTRA_ARGUMENTS = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_ARGUMENTS"; // Default: "com.termux.RUN_COMMAND_ARGUMENTS" - // Intent extra for command current working directory for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent + /* Intent extra for command current working directory for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */ public static final String EXTRA_WORKDIR = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_WORKDIR"; // Default: "com.termux.RUN_COMMAND_WORKDIR" - // Intent extra for command background mode for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent + /* Intent extra for command background mode for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */ public static final String EXTRA_BACKGROUND = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_BACKGROUND"; // Default: "com.termux.RUN_COMMAND_BACKGROUND" } @@ -215,16 +408,14 @@ public final class TermuxConstants { + + /** * Termux:Styling app constants. */ - public static final class TERMUX_STYLING { - /** - * Termux:Styling app core activity constants. - */ - + /** Termux:Styling app core activity name. */ public static final String TERMUX_STYLING_ACTIVITY_NAME = TERMUX_STYLING_PACKAGE_NAME + ".TermuxStyleActivity"; // Default: "com.termux.styling.TermuxStyleActivity" } diff --git a/app/src/main/java/com/termux/app/TermuxInstaller.java b/app/src/main/java/com/termux/app/TermuxInstaller.java index 390eff12..c5bf3d17 100644 --- a/app/src/main/java/com/termux/app/TermuxInstaller.java +++ b/app/src/main/java/com/termux/app/TermuxInstaller.java @@ -58,7 +58,7 @@ final class TermuxInstaller { return; } - final File PREFIX_FILE = new File(TermuxConstants.PREFIX_PATH); + final File PREFIX_FILE = TermuxConstants.TERMUX_PREFIX_DIR; if (PREFIX_FILE.isDirectory()) { whenDone.run(); return; @@ -69,7 +69,7 @@ final class TermuxInstaller { @Override public void run() { try { - final String STAGING_PREFIX_PATH = TermuxConstants.FILES_PATH + "/usr-staging"; + final String STAGING_PREFIX_PATH = TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH; final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH); if (STAGING_PREFIX_FILE.exists()) { @@ -194,7 +194,7 @@ final class TermuxInstaller { new Thread() { public void run() { try { - File storageDir = new File(TermuxConstants.HOME_PATH, "storage"); + File storageDir = TermuxConstants.TERMUX_STORAGE_HOME_DIR; if (storageDir.exists()) { try { diff --git a/app/src/main/java/com/termux/app/TermuxOpenReceiver.java b/app/src/main/java/com/termux/app/TermuxOpenReceiver.java index 26f5fec9..960ab8e5 100644 --- a/app/src/main/java/com/termux/app/TermuxOpenReceiver.java +++ b/app/src/main/java/com/termux/app/TermuxOpenReceiver.java @@ -178,7 +178,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver { String path = file.getCanonicalPath(); String storagePath = Environment.getExternalStorageDirectory().getCanonicalPath(); // See https://support.google.com/faqs/answer/7496913: - if (!(path.startsWith(TermuxConstants.FILES_PATH) || path.startsWith(storagePath))) { + if (!(path.startsWith(TermuxConstants.TERMUX_FILES_DIR_PATH) || path.startsWith(storagePath))) { throw new IllegalArgumentException("Invalid path: " + path); } } catch (IOException e) { diff --git a/app/src/main/java/com/termux/app/TermuxService.java b/app/src/main/java/com/termux/app/TermuxService.java index 854659dc..0f4dd465 100644 --- a/app/src/main/java/com/termux/app/TermuxService.java +++ b/app/src/main/java/com/termux/app/TermuxService.java @@ -238,7 +238,7 @@ public final class TermuxService extends Service implements SessionChangedCallba @Override public void onDestroy() { - File termuxTmpDir = new File(TermuxConstants.PREFIX_PATH + "/tmp"); + File termuxTmpDir = TermuxConstants.TERMUX_TMP_DIR; if (termuxTmpDir.exists()) { try { @@ -264,9 +264,9 @@ public final class TermuxService extends Service implements SessionChangedCallba } TerminalSession createTermSession(String executablePath, String[] arguments, String cwd, boolean failSafe) { - new File(TermuxConstants.HOME_PATH).mkdirs(); + TermuxConstants.TERMUX_HOME_DIR.mkdirs(); - if (cwd == null || cwd.isEmpty()) cwd = TermuxConstants.HOME_PATH; + if (cwd == null || cwd.isEmpty()) cwd = TermuxConstants.TERMUX_HOME_DIR_PATH; String[] env = BackgroundJob.buildEnvironment(failSafe, cwd); boolean isLoginShell = false; @@ -274,7 +274,7 @@ public final class TermuxService extends Service implements SessionChangedCallba if (executablePath == null) { if (!failSafe) { for (String shellBinary : new String[]{"login", "bash", "zsh"}) { - File shellFile = new File(TermuxConstants.PREFIX_PATH + "/bin/" + shellBinary); + File shellFile = new File(TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH, shellBinary); if (shellFile.canExecute()) { executablePath = shellFile.getAbsolutePath(); break; diff --git a/app/src/main/java/com/termux/app/settings/properties/TermuxPropertyConstants.java b/app/src/main/java/com/termux/app/settings/properties/TermuxPropertyConstants.java index 21a06ad8..859eeb5f 100644 --- a/app/src/main/java/com/termux/app/settings/properties/TermuxPropertyConstants.java +++ b/app/src/main/java/com/termux/app/settings/properties/TermuxPropertyConstants.java @@ -8,16 +8,19 @@ import com.termux.app.TermuxConstants; import java.io.File; import java.util.Arrays; import java.util.HashSet; -import java.util.Properties; import java.util.Set; /* - * Version: v0.1.0 + * Version: v0.2.0 * * Changelog * - * - 0.1.0 (2021-03-08) - * - Initial Release + * - 0.1.0 (2021-03-11) + * - Initial Release. + * - 0.2.0 (2021-03-11) + * - Renamed `HOME_PATH` to `TERMUX_HOME_DIR_PATH` + * - Renamed `TERMUX_PROPERTIES_PRIMARY_PATH` to `TERMUX_PROPERTIES_PRIMARY_FILE_PATH` + * - Renamed `TERMUX_PROPERTIES_SECONDARY_FILE_PATH` to `TERMUX_PROPERTIES_SECONDARY_FILE_PATH` * */ @@ -29,8 +32,8 @@ import java.util.Set; * * and add an entry in the Changelog section above. * * The properties are loaded from the first file found at - * {@link TermuxConstants#TERMUX_PROPERTIES_PRIMARY_PATH} or - * {@link TermuxConstants#TERMUX_PROPERTIES_SECONDARY_PATH} + * {@link TermuxConstants#TERMUX_PROPERTIES_PRIMARY_FILE_PATH} or + * {@link TermuxConstants#TERMUX_PROPERTIES_SECONDARY_FILE_PATH} */ public final class TermuxPropertyConstants { @@ -132,7 +135,7 @@ public final class TermuxPropertyConstants { /** Defines the key for the default working directory */ public static final String KEY_DEFAULT_WORKING_DIRECTORY = "default-working-directory"; // Default: "default-working-directory" /** Defines the default working directory */ - public static final String DEFAULT_IVALUE_DEFAULT_WORKING_DIRECTORY = TermuxConstants.HOME_PATH; + public static final String DEFAULT_IVALUE_DEFAULT_WORKING_DIRECTORY = TermuxConstants.TERMUX_HOME_DIR_PATH; @@ -197,8 +200,8 @@ public final class TermuxPropertyConstants { /** Returns the first {@link File} found at - * {@link TermuxConstants#TERMUX_PROPERTIES_PRIMARY_PATH} or - * {@link TermuxConstants#TERMUX_PROPERTIES_SECONDARY_PATH} + * {@link TermuxConstants#TERMUX_PROPERTIES_PRIMARY_FILE_PATH} or + * {@link TermuxConstants#TERMUX_PROPERTIES_SECONDARY_FILE_PATH} * from which termux properties can be loaded. * If the {@link File} found is not a regular file or is not readable then null is returned. * @@ -206,8 +209,8 @@ public final class TermuxPropertyConstants { */ public static File getTermuxPropertiesFile() { String[] possiblePropertiesFileLocations = { - TermuxConstants.TERMUX_PROPERTIES_PRIMARY_PATH, - TermuxConstants.TERMUX_PROPERTIES_SECONDARY_PATH + TermuxConstants.TERMUX_PROPERTIES_PRIMARY_FILE_PATH, + TermuxConstants.TERMUX_PROPERTIES_SECONDARY_FILE_PATH }; File propertiesFile = new File(possiblePropertiesFileLocations[0]); diff --git a/app/src/main/java/com/termux/filepicker/TermuxDocumentsProvider.java b/app/src/main/java/com/termux/filepicker/TermuxDocumentsProvider.java index 1a314e8c..3bb14334 100644 --- a/app/src/main/java/com/termux/filepicker/TermuxDocumentsProvider.java +++ b/app/src/main/java/com/termux/filepicker/TermuxDocumentsProvider.java @@ -35,7 +35,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider { private static final String ALL_MIME_TYPES = "*/*"; - private static final File BASE_DIR = new File(TermuxConstants.HOME_PATH); + private static final File BASE_DIR = TermuxConstants.TERMUX_HOME_DIR; // The default columns to return information about a root if no specific @@ -171,7 +171,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider { // through the whole SD card). boolean isInsideHome; try { - isInsideHome = file.getCanonicalPath().startsWith(TermuxConstants.HOME_PATH); + isInsideHome = file.getCanonicalPath().startsWith(TermuxConstants.TERMUX_HOME_DIR_PATH); } catch (IOException e) { isInsideHome = true; } diff --git a/app/src/main/java/com/termux/filepicker/TermuxFileReceiverActivity.java b/app/src/main/java/com/termux/filepicker/TermuxFileReceiverActivity.java index c5493ae7..2367bdf0 100644 --- a/app/src/main/java/com/termux/filepicker/TermuxFileReceiverActivity.java +++ b/app/src/main/java/com/termux/filepicker/TermuxFileReceiverActivity.java @@ -27,9 +27,9 @@ import java.util.regex.Pattern; public class TermuxFileReceiverActivity extends Activity { - static final String TERMUX_RECEIVEDIR = TermuxConstants.FILES_PATH + "/home/downloads"; - static final String EDITOR_PROGRAM = TermuxConstants.HOME_PATH + "/bin/termux-file-editor"; - static final String URL_OPENER_PROGRAM = TermuxConstants.HOME_PATH + "/bin/termux-url-opener"; + static final String TERMUX_RECEIVEDIR = TermuxConstants.TERMUX_FILES_DIR_PATH + "/home/downloads"; + static final String EDITOR_PROGRAM = TermuxConstants.TERMUX_HOME_DIR_PATH + "/bin/termux-file-editor"; + static final String URL_OPENER_PROGRAM = TermuxConstants.TERMUX_HOME_DIR_PATH + "/bin/termux-url-opener"; /** * If the activity should be finished when the name input dialog is dismissed. This is disabled From 36be41d0deb175f47b2bbbfca43427cfdc38b43e Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Thu, 11 Mar 2021 20:30:16 +0500 Subject: [PATCH 017/136] Remove function that reads the "termux.properties" files from RunCommandService The `RunCommandService` will now call the `TermuxSharedProperties` for getting current value of `allow-external-apps`, instead of using its own duplicated function to read "termux.properties" files. --- .../com/termux/app/RunCommandService.java | 27 ++++--------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/termux/app/RunCommandService.java b/app/src/main/java/com/termux/app/RunCommandService.java index d1c523ee..f186e5b2 100644 --- a/app/src/main/java/com/termux/app/RunCommandService.java +++ b/app/src/main/java/com/termux/app/RunCommandService.java @@ -15,6 +15,8 @@ import android.util.Log; import com.termux.R; import com.termux.app.TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE; import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_SERVICE; +import com.termux.app.settings.properties.TermuxPropertyConstants; +import com.termux.app.settings.properties.TermuxSharedProperties; import java.io.File; import java.io.FileInputStream; @@ -109,9 +111,9 @@ public class RunCommandService extends Service { return Service.START_NOT_STICKY; } - // If allow-external-apps property to not set to "true" - if (!allowExternalApps()) { - Log.e("termux", "RunCommandService requires allow-external-apps property to be set to \"true\" in ~/.termux/termux.properties file."); + // If allow-external-apps property is not set to "true" + if (!TermuxSharedProperties.isPropertyValueTrue(this, TermuxPropertyConstants.getTermuxPropertiesFile(), TermuxConstants.PROP_ALLOW_EXTERNAL_APPS)) { + Log.e("termux", "RunCommandService requires allow-external-apps property to be set to \"true\" in \"" + TermuxConstants.TERMUX_PROPERTIES_PRIMARY_FILE_PATH + "\" file"); return Service.START_NOT_STICKY; } @@ -183,25 +185,6 @@ public class RunCommandService extends Service { manager.createNotificationChannel(channel); } - private boolean allowExternalApps() { - File propsFile = new File(TermuxConstants.TERMUX_PROPERTIES_PRIMARY_FILE_PATH); - if (!propsFile.exists()) - propsFile = new File(TermuxConstants.TERMUX_PROPERTIES_SECONDARY_FILE_PATH); - - Properties props = new Properties(); - try { - if (propsFile.isFile() && propsFile.canRead()) { - try (FileInputStream in = new FileInputStream(propsFile)) { - props.load(new InputStreamReader(in, StandardCharsets.UTF_8)); - } - } - } catch (Exception e) { - Log.e("termux", "Error loading props", e); - } - - return props.getProperty("allow-external-apps", "false").equals("true"); - } - /** Replace "$PREFIX/" or "~/" prefix with termux absolute paths */ public static String getExpandedTermuxPath(String path) { if(path != null && !path.isEmpty()) { From 319446fc15e81bf7456235d03d92716c356fd66d Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Thu, 11 Mar 2021 20:30:52 +0500 Subject: [PATCH 018/136] Disable TerminalView logging. --- terminal-view/src/main/java/com/termux/view/TerminalView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal-view/src/main/java/com/termux/view/TerminalView.java b/terminal-view/src/main/java/com/termux/view/TerminalView.java index 5522efbc..383e3bf6 100644 --- a/terminal-view/src/main/java/com/termux/view/TerminalView.java +++ b/terminal-view/src/main/java/com/termux/view/TerminalView.java @@ -47,7 +47,7 @@ import java.util.Properties; public final class TerminalView extends View { /** Log view key and IME events. */ - private static final boolean LOG_KEY_EVENTS = true; + private static final boolean LOG_KEY_EVENTS = false; /** The currently displayed terminal session, whose emulator is {@link #mEmulator}. */ public TerminalSession mTermSession; From 10d6eaa5d1969180f81ca25255a16a481aeecf6a Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Thu, 11 Mar 2021 21:06:42 +0500 Subject: [PATCH 019/136] Make TerminalView agnostic of "termux.properties" files. `TerminalView` will use the `TerminalViewClient` interface implemented by `TermuxViewClient` in termux-app to get "enforce-char-based-input" and "ctrl-space-workaround" property values. It will also not read the file every time it needs to get the property value and will get it from the in-memory cache of `TermuxSharedProperties`. --- .../java/com/termux/app/TermuxViewClient.java | 10 ++++ .../properties/TermuxPropertyConstants.java | 15 ++++++ .../properties/TermuxSharedProperties.java | 8 ++++ .../java/com/termux/view/TerminalView.java | 48 ++----------------- .../com/termux/view/TerminalViewClient.java | 4 ++ 5 files changed, 40 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/com/termux/app/TermuxViewClient.java b/app/src/main/java/com/termux/app/TermuxViewClient.java index b6eea4d3..1af1622e 100644 --- a/app/src/main/java/com/termux/app/TermuxViewClient.java +++ b/app/src/main/java/com/termux/app/TermuxViewClient.java @@ -52,6 +52,16 @@ public final class TermuxViewClient implements TerminalViewClient { return mActivity.mProperties.isBackKeyTheEscapeKey(); } + @Override + public boolean shouldEnforeCharBasedInput() { + return mActivity.mProperties.isEnforcingCharBasedInput(); + } + + @Override + public boolean shouldUseCtrlSpaceWorkaround() { + return mActivity.mProperties.isUsingCtrlSpaceWorkaround(); + } + @Override public void copyModeChanged(boolean copyMode) { // Disable drawer while copying. diff --git a/app/src/main/java/com/termux/app/settings/properties/TermuxPropertyConstants.java b/app/src/main/java/com/termux/app/settings/properties/TermuxPropertyConstants.java index 859eeb5f..9b8731a6 100644 --- a/app/src/main/java/com/termux/app/settings/properties/TermuxPropertyConstants.java +++ b/app/src/main/java/com/termux/app/settings/properties/TermuxPropertyConstants.java @@ -61,11 +61,22 @@ public final class TermuxPropertyConstants { + /** Defines the key for whether to enforce character based input to fix the issue where for some devices like Samsung, the letters might not appear until enter is pressed */ + public static final String KEY_ENFORCE_CHAR_BASED_INPUT = "enforce-char-based-input"; // Default: "enforce-char-based-input" + + + + /** Defines the key for whether to use black UI */ public static final String KEY_USE_BLACK_UI = "use-black-ui"; // Default: "use-black-ui" + /** Defines the key for whether to use ctrl space workaround to fix the issue where ctrl+space does not work on some ROMs */ + public static final String KEY_USE_CTRL_SPACE_WORKAROUND = "ctrl-space-workaround"; // Default: "ctrl-space-workaround" + + + /** Defines the key for whether to use fullscreen */ public static final String KEY_USE_FULLSCREEN = "fullscreen"; // Default: "fullscreen" @@ -155,8 +166,10 @@ public final class TermuxPropertyConstants { * */ public static final Set TERMUX_PROPERTIES_LIST = new HashSet<>(Arrays.asList( // boolean + KEY_ENFORCE_CHAR_BASED_INPUT, KEY_USE_BACK_KEY_AS_ESCAPE_KEY, KEY_USE_BLACK_UI, + KEY_USE_CTRL_SPACE_WORKAROUND, KEY_USE_FULLSCREEN, KEY_USE_FULLSCREEN_WORKAROUND, KEY_VIRTUAL_VOLUME_KEYS_DISABLED, @@ -183,6 +196,8 @@ public final class TermuxPropertyConstants { * default: false * */ public static final Set TERMUX_DEFAULT_BOOLEAN_BEHAVIOUR_PROPERTIES_LIST = new HashSet<>(Arrays.asList( + KEY_ENFORCE_CHAR_BASED_INPUT, + KEY_USE_CTRL_SPACE_WORKAROUND, KEY_USE_FULLSCREEN, KEY_USE_FULLSCREEN_WORKAROUND, TermuxConstants.PROP_ALLOW_EXTERNAL_APPS diff --git a/app/src/main/java/com/termux/app/settings/properties/TermuxSharedProperties.java b/app/src/main/java/com/termux/app/settings/properties/TermuxSharedProperties.java index 4e2e9c7d..7e8ccdd8 100644 --- a/app/src/main/java/com/termux/app/settings/properties/TermuxSharedProperties.java +++ b/app/src/main/java/com/termux/app/settings/properties/TermuxSharedProperties.java @@ -485,6 +485,10 @@ public class TermuxSharedProperties implements SharedPropertiesParser { + public boolean isEnforcingCharBasedInput() { + return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_ENFORCE_CHAR_BASED_INPUT, true); + } + public boolean isBackKeyTheEscapeKey() { return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_USE_BACK_KEY_AS_ESCAPE_KEY, true); } @@ -493,6 +497,10 @@ public class TermuxSharedProperties implements SharedPropertiesParser { return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_USE_BLACK_UI, true); } + public boolean isUsingCtrlSpaceWorkaround() { + return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_USE_CTRL_SPACE_WORKAROUND, true); + } + public boolean isUsingFullScreen() { return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_USE_FULLSCREEN, true); } diff --git a/terminal-view/src/main/java/com/termux/view/TerminalView.java b/terminal-view/src/main/java/com/termux/view/TerminalView.java index 383e3bf6..b9d18e69 100644 --- a/terminal-view/src/main/java/com/termux/view/TerminalView.java +++ b/terminal-view/src/main/java/com/termux/view/TerminalView.java @@ -37,12 +37,6 @@ import com.termux.terminal.TerminalEmulator; import com.termux.terminal.TerminalSession; import com.termux.view.textselection.TextSelectionCursorController; -import java.io.File; -import java.io.FileInputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.util.Properties; - /** View displaying and interacting with a {@link TerminalSession}. */ public final class TerminalView extends View { @@ -246,9 +240,7 @@ public final class TerminalView extends View { @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { - Properties props = getProperties(); - - if (props.getProperty("enforce-char-based-input", "false").equals("true")) { + if (mClient.shouldEnforeCharBasedInput()) { // Some keyboards seems do not reset the internal state on TYPE_NULL. // Affects mostly Samsung stock keyboards. // https://github.com/termux/termux-app/issues/686 @@ -529,8 +521,6 @@ public final class TerminalView extends View { @Override public boolean onKeyPreIme(int keyCode, KeyEvent event) { - Properties props = getProperties(); - if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "onKeyPreIme(keyCode=" + keyCode + ", event=" + event + ")"); if (keyCode == KeyEvent.KEYCODE_BACK) { @@ -546,9 +536,9 @@ public final class TerminalView extends View { return onKeyUp(keyCode, event); } } - } else if (props.getProperty("ctrl-space-workaround", "false").equals("true") && + } else if (mClient.shouldUseCtrlSpaceWorkaround() && keyCode == KeyEvent.KEYCODE_SPACE && event.isCtrlPressed()) { - /* ctrl + space does not work on some ROMs without this workaround. + /* ctrl+space does not work on some ROMs without this workaround. However, this breaks it on devices where it works out of the box. */ return onKeyDown(keyCode, event); } @@ -961,36 +951,4 @@ public final class TerminalView extends View { } } - - - - - private Properties getProperties() { - File propsFile; - Properties props = new Properties(); - String possiblePropLocations[] = { - getContext().getFilesDir() + "/home/.termux/termux.properties", - getContext().getFilesDir() + "/home/.config/termux/termux.properties" - }; - - propsFile = new File(possiblePropLocations[0]); - int i = 0; - while (!propsFile.exists() && i < possiblePropLocations.length) { - propsFile = new File(possiblePropLocations[i]); - i += 1; - } - - try { - if (propsFile.isFile() && propsFile.canRead()) { - try (FileInputStream in = new FileInputStream(propsFile)) { - props.load(new InputStreamReader(in, StandardCharsets.UTF_8)); - } - } - } catch (Exception e) { - Log.e("termux", "Error loading props", e); - } - - return props; - } - } diff --git a/terminal-view/src/main/java/com/termux/view/TerminalViewClient.java b/terminal-view/src/main/java/com/termux/view/TerminalViewClient.java index d2b62fa1..390e9157 100644 --- a/terminal-view/src/main/java/com/termux/view/TerminalViewClient.java +++ b/terminal-view/src/main/java/com/termux/view/TerminalViewClient.java @@ -25,6 +25,10 @@ public interface TerminalViewClient { boolean shouldBackButtonBeMappedToEscape(); + boolean shouldEnforeCharBasedInput(); + + boolean shouldUseCtrlSpaceWorkaround(); + void copyModeChanged(boolean copyMode); boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession session); From 5fd91b4f921eafe4b40222b74ad825821206eb5a Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Fri, 12 Mar 2021 00:07:35 +0500 Subject: [PATCH 020/136] Rename com.termux.app.input package to com.termux.app.terminal Also moves `TermuxViewClient` into com.termux.app.terminal package --- .../java/com/termux/app/TermuxActivity.java | 57 ++++++++++++------- .../properties/TermuxSharedProperties.java | 5 +- .../app/{input => terminal}/BellHandler.java | 2 +- .../FullScreenWorkAround.java | 2 +- .../{input => terminal}/KeyboardShortcut.java | 2 +- .../app/{ => terminal}/TermuxViewClient.java | 30 +++++----- .../extrakeys/ExtraKeyButton.java | 2 +- .../extrakeys/ExtraKeysInfo.java | 2 +- .../extrakeys/ExtraKeysView.java | 2 +- app/src/main/res/layout/extra_keys_main.xml | 2 +- 10 files changed, 62 insertions(+), 44 deletions(-) rename app/src/main/java/com/termux/app/{input => terminal}/BellHandler.java (98%) rename app/src/main/java/com/termux/app/{input => terminal}/FullScreenWorkAround.java (98%) rename app/src/main/java/com/termux/app/{input => terminal}/KeyboardShortcut.java (88%) rename app/src/main/java/com/termux/app/{ => terminal}/TermuxViewClient.java (90%) rename app/src/main/java/com/termux/app/{input => terminal}/extrakeys/ExtraKeyButton.java (98%) rename app/src/main/java/com/termux/app/{input => terminal}/extrakeys/ExtraKeysInfo.java (99%) rename app/src/main/java/com/termux/app/{input => terminal}/extrakeys/ExtraKeysView.java (99%) diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java index 2c50350e..f5df773d 100644 --- a/app/src/main/java/com/termux/app/TermuxActivity.java +++ b/app/src/main/java/com/termux/app/TermuxActivity.java @@ -48,9 +48,10 @@ import android.widget.Toast; import com.termux.R; import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY; -import com.termux.app.input.BellHandler; -import com.termux.app.input.extrakeys.ExtraKeysView; -import com.termux.app.input.FullScreenWorkAround; +import com.termux.app.terminal.BellHandler; +import com.termux.app.terminal.TermuxViewClient; +import com.termux.app.terminal.extrakeys.ExtraKeysView; +import com.termux.app.terminal.FullScreenWorkAround; import com.termux.app.settings.properties.TermuxPropertyConstants; import com.termux.app.settings.properties.TermuxSharedProperties; import com.termux.terminal.EmulatorDebug; @@ -106,14 +107,6 @@ public final class TermuxActivity extends Activity implements ServiceConnection private static final String BROADCAST_TERMUX_OPENED = TermuxConstants.TERMUX_PACKAGE_NAME + ".app.OPENED"; - /** The main view of the activity showing the terminal. Initialized in onCreate(). */ - TerminalView mTerminalView; - - ExtraKeysView mExtraKeysView; - - TermuxPreferences mSettings; - TermuxSharedProperties mProperties; - /** * The connection to the {@link TermuxService}. Requested in {@link #onCreate(Bundle)} with a call to * {@link #bindService(Intent, ServiceConnection, int)}, and obtained and stored in @@ -121,6 +114,15 @@ public final class TermuxActivity extends Activity implements ServiceConnection */ TermuxService mTermService; + /** The main view of the activity showing the terminal. Initialized in onCreate(). */ + TerminalView mTerminalView; + + ExtraKeysView mExtraKeysView; + + TermuxPreferences mSettings; + + TermuxSharedProperties mProperties; + /** Initialized in {@link #onServiceConnected(ComponentName, IBinder)}. */ ArrayAdapter mListViewAdapter; @@ -378,7 +380,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection } } - void toggleShowExtraKeys() { + public void toggleShowExtraKeys() { final ViewPager viewPager = findViewById(R.id.viewpager); final boolean showNow = mSettings.toggleShowExtraKeys(TermuxActivity.this); viewPager.setVisibility(showNow ? View.VISIBLE : View.GONE); @@ -585,7 +587,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection } @SuppressLint("InflateParams") - void renameSession(final TerminalSession sessionToRename) { + public void renameSession(final TerminalSession sessionToRename) { if (sessionToRename == null) return; DialogUtils.textInput(this, R.string.session_rename_title, sessionToRename.mSessionName, R.string.session_rename_positive_button, text -> { sessionToRename.mSessionName = text; @@ -607,7 +609,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection } @Nullable - TerminalSession getCurrentTermSession() { + public TerminalSession getCurrentTermSession() { return mTerminalView.getCurrentSession(); } @@ -660,11 +662,11 @@ public final class TermuxActivity extends Activity implements ServiceConnection unbindService(this); } - DrawerLayout getDrawer() { + public DrawerLayout getDrawer() { return (DrawerLayout) findViewById(R.id.drawer_layout); } - void addNewSession(boolean failSafe, String sessionName) { + public void addNewSession(boolean failSafe, String sessionName) { if (mTermService.getSessions().size() >= MAX_SESSIONS) { new AlertDialog.Builder(this).setTitle(R.string.max_terminals_reached_title).setMessage(R.string.max_terminals_reached_message) .setPositiveButton(android.R.string.ok, null).show(); @@ -688,7 +690,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection } /** Try switching to session and note about it, but do nothing if already displaying the session. */ - void switchToSession(TerminalSession session) { + public void switchToSession(TerminalSession session) { if (mTerminalView.attachSession(session)) { noteSessionInfo(); updateBackgroundColor(); @@ -835,7 +837,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection return urlSet; } - void showUrlSelection() { + public void showUrlSelection() { String text = null; if (getCurrentTermSession() != null) { text = getCurrentTermSession().getEmulator().getScreen().getTranscriptTextWithFullLinesJoined(); @@ -975,12 +977,12 @@ public final class TermuxActivity extends Activity implements ServiceConnection } } - void changeFontSize(boolean increase) { + public void changeFontSize(boolean increase) { mSettings.changeFontSize(this, increase); mTerminalView.setTextSize(mSettings.getFontSize()); } - void doPaste() { + public void doPaste() { ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); ClipData clipData = clipboard.getPrimaryClip(); if (clipData == null) return; @@ -1024,4 +1026,19 @@ public final class TermuxActivity extends Activity implements ServiceConnection } } + public TermuxService getTermService() { + return mTermService; + } + + public TerminalView getTerminalView() { + return mTerminalView; + } + + public ExtraKeysView getExtraKeysView() { + return mExtraKeysView; + } + public TermuxSharedProperties getProperties() { + return mProperties; + } + } diff --git a/app/src/main/java/com/termux/app/settings/properties/TermuxSharedProperties.java b/app/src/main/java/com/termux/app/settings/properties/TermuxSharedProperties.java index 7e8ccdd8..70a3dc3e 100644 --- a/app/src/main/java/com/termux/app/settings/properties/TermuxSharedProperties.java +++ b/app/src/main/java/com/termux/app/settings/properties/TermuxSharedProperties.java @@ -5,11 +5,10 @@ import android.content.res.Configuration; import android.util.Log; import android.widget.Toast; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.termux.app.input.extrakeys.ExtraKeysInfo; -import com.termux.app.input.KeyboardShortcut; +import com.termux.app.terminal.extrakeys.ExtraKeysInfo; +import com.termux.app.terminal.KeyboardShortcut; import org.json.JSONException; diff --git a/app/src/main/java/com/termux/app/input/BellHandler.java b/app/src/main/java/com/termux/app/terminal/BellHandler.java similarity index 98% rename from app/src/main/java/com/termux/app/input/BellHandler.java rename to app/src/main/java/com/termux/app/terminal/BellHandler.java index 0610971e..207f7e78 100644 --- a/app/src/main/java/com/termux/app/input/BellHandler.java +++ b/app/src/main/java/com/termux/app/terminal/BellHandler.java @@ -1,4 +1,4 @@ -package com.termux.app.input; +package com.termux.app.terminal; import android.content.Context; import android.os.Handler; diff --git a/app/src/main/java/com/termux/app/input/FullScreenWorkAround.java b/app/src/main/java/com/termux/app/terminal/FullScreenWorkAround.java similarity index 98% rename from app/src/main/java/com/termux/app/input/FullScreenWorkAround.java rename to app/src/main/java/com/termux/app/terminal/FullScreenWorkAround.java index 6002f8c8..c37c870c 100644 --- a/app/src/main/java/com/termux/app/input/FullScreenWorkAround.java +++ b/app/src/main/java/com/termux/app/terminal/FullScreenWorkAround.java @@ -1,4 +1,4 @@ -package com.termux.app.input; +package com.termux.app.terminal; import android.graphics.Rect; import android.view.View; diff --git a/app/src/main/java/com/termux/app/input/KeyboardShortcut.java b/app/src/main/java/com/termux/app/terminal/KeyboardShortcut.java similarity index 88% rename from app/src/main/java/com/termux/app/input/KeyboardShortcut.java rename to app/src/main/java/com/termux/app/terminal/KeyboardShortcut.java index 7308c94c..5db41bec 100644 --- a/app/src/main/java/com/termux/app/input/KeyboardShortcut.java +++ b/app/src/main/java/com/termux/app/terminal/KeyboardShortcut.java @@ -1,4 +1,4 @@ -package com.termux.app.input; +package com.termux.app.terminal; public class KeyboardShortcut { diff --git a/app/src/main/java/com/termux/app/TermuxViewClient.java b/app/src/main/java/com/termux/app/terminal/TermuxViewClient.java similarity index 90% rename from app/src/main/java/com/termux/app/TermuxViewClient.java rename to app/src/main/java/com/termux/app/terminal/TermuxViewClient.java index 1af1622e..567ebef8 100644 --- a/app/src/main/java/com/termux/app/TermuxViewClient.java +++ b/app/src/main/java/com/termux/app/terminal/TermuxViewClient.java @@ -1,5 +1,6 @@ -package com.termux.app; +package com.termux.app.terminal; +import android.annotation.SuppressLint; import android.content.Context; import android.media.AudioManager; import android.view.Gravity; @@ -8,8 +9,9 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.inputmethod.InputMethodManager; -import com.termux.app.input.KeyboardShortcut; -import com.termux.app.input.extrakeys.ExtraKeysView; +import com.termux.app.TermuxActivity; +import com.termux.app.TermuxService; +import com.termux.app.terminal.extrakeys.ExtraKeysView; import com.termux.app.settings.properties.TermuxPropertyConstants; import com.termux.terminal.KeyHandler; import com.termux.terminal.TerminalEmulator; @@ -44,22 +46,22 @@ public final class TermuxViewClient implements TerminalViewClient { @Override public void onSingleTapUp(MotionEvent e) { InputMethodManager mgr = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE); - mgr.showSoftInput(mActivity.mTerminalView, InputMethodManager.SHOW_IMPLICIT); + mgr.showSoftInput(mActivity.getTerminalView(), InputMethodManager.SHOW_IMPLICIT); } @Override public boolean shouldBackButtonBeMappedToEscape() { - return mActivity.mProperties.isBackKeyTheEscapeKey(); + return mActivity.getProperties().isBackKeyTheEscapeKey(); } @Override public boolean shouldEnforeCharBasedInput() { - return mActivity.mProperties.isEnforcingCharBasedInput(); + return mActivity.getProperties().isEnforcingCharBasedInput(); } @Override public boolean shouldUseCtrlSpaceWorkaround() { - return mActivity.mProperties.isUsingCtrlSpaceWorkaround(); + return mActivity.getProperties().isUsingCtrlSpaceWorkaround(); } @Override @@ -68,6 +70,7 @@ public final class TermuxViewClient implements TerminalViewClient { mActivity.getDrawer().setDrawerLockMode(copyMode ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED : DrawerLayout.LOCK_MODE_UNLOCKED); } + @SuppressLint("RtlHardcoded") @Override public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession currentSession) { if (handleVirtualKeys(keyCode, e, true)) return true; @@ -91,7 +94,7 @@ public final class TermuxViewClient implements TerminalViewClient { InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE); imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); } else if (unicodeChar == 'm'/* menu */) { - mActivity.mTerminalView.showContextMenu(); + mActivity.getTerminalView().showContextMenu(); } else if (unicodeChar == 'r'/* rename */) { mActivity.renameSession(currentSession); } else if (unicodeChar == 'c'/* create */) { @@ -108,7 +111,7 @@ public final class TermuxViewClient implements TerminalViewClient { mActivity.changeFontSize(false); } else if (unicodeChar >= '1' && unicodeChar <= '9') { int num = unicodeChar - '1'; - TermuxService service = mActivity.mTermService; + TermuxService service = mActivity.getTermService(); if (service.getSessions().size() > num) mActivity.switchToSession(service.getSessions().get(num)); } @@ -126,12 +129,12 @@ public final class TermuxViewClient implements TerminalViewClient { @Override public boolean readControlKey() { - return (mActivity.mExtraKeysView != null && mActivity.mExtraKeysView.readSpecialButton(ExtraKeysView.SpecialButton.CTRL)) || mVirtualControlKeyDown; + return (mActivity.getExtraKeysView() != null && mActivity.getExtraKeysView().readSpecialButton(ExtraKeysView.SpecialButton.CTRL)) || mVirtualControlKeyDown; } @Override public boolean readAltKey() { - return (mActivity.mExtraKeysView != null && mActivity.mExtraKeysView.readSpecialButton(ExtraKeysView.SpecialButton.ALT)); + return (mActivity.getExtraKeysView() != null && mActivity.getExtraKeysView().readSpecialButton(ExtraKeysView.SpecialButton.ALT)); } @Override @@ -242,7 +245,7 @@ public final class TermuxViewClient implements TerminalViewClient { return true; } - List shortcuts = mActivity.mProperties.getSessionShortcuts(); + List shortcuts = mActivity.getProperties().getSessionShortcuts(); if (!shortcuts.isEmpty()) { int codePointLowerCase = Character.toLowerCase(codePoint); for (int i = shortcuts.size() - 1; i >= 0; i--) { @@ -278,7 +281,7 @@ public final class TermuxViewClient implements TerminalViewClient { /** Handle dedicated volume buttons as virtual keys if applicable. */ private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) { InputDevice inputDevice = event.getDevice(); - if (mActivity.mProperties.areVirtualVolumeKeysDisabled()) { + if (mActivity.getProperties().areVirtualVolumeKeysDisabled()) { return false; } else if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) { // Do not steal dedicated buttons from a full external keyboard. @@ -293,5 +296,4 @@ public final class TermuxViewClient implements TerminalViewClient { return false; } - } diff --git a/app/src/main/java/com/termux/app/input/extrakeys/ExtraKeyButton.java b/app/src/main/java/com/termux/app/terminal/extrakeys/ExtraKeyButton.java similarity index 98% rename from app/src/main/java/com/termux/app/input/extrakeys/ExtraKeyButton.java rename to app/src/main/java/com/termux/app/terminal/extrakeys/ExtraKeyButton.java index 1218ef78..36457af2 100644 --- a/app/src/main/java/com/termux/app/input/extrakeys/ExtraKeyButton.java +++ b/app/src/main/java/com/termux/app/terminal/extrakeys/ExtraKeyButton.java @@ -1,4 +1,4 @@ -package com.termux.app.input.extrakeys; +package com.termux.app.terminal.extrakeys; import android.text.TextUtils; diff --git a/app/src/main/java/com/termux/app/input/extrakeys/ExtraKeysInfo.java b/app/src/main/java/com/termux/app/terminal/extrakeys/ExtraKeysInfo.java similarity index 99% rename from app/src/main/java/com/termux/app/input/extrakeys/ExtraKeysInfo.java rename to app/src/main/java/com/termux/app/terminal/extrakeys/ExtraKeysInfo.java index c71f3b1f..e1406e72 100644 --- a/app/src/main/java/com/termux/app/input/extrakeys/ExtraKeysInfo.java +++ b/app/src/main/java/com/termux/app/terminal/extrakeys/ExtraKeysInfo.java @@ -1,4 +1,4 @@ -package com.termux.app.input.extrakeys; +package com.termux.app.terminal.extrakeys; import org.json.JSONArray; import org.json.JSONException; diff --git a/app/src/main/java/com/termux/app/input/extrakeys/ExtraKeysView.java b/app/src/main/java/com/termux/app/terminal/extrakeys/ExtraKeysView.java similarity index 99% rename from app/src/main/java/com/termux/app/input/extrakeys/ExtraKeysView.java rename to app/src/main/java/com/termux/app/terminal/extrakeys/ExtraKeysView.java index 92e1c370..d1b1dee5 100644 --- a/app/src/main/java/com/termux/app/input/extrakeys/ExtraKeysView.java +++ b/app/src/main/java/com/termux/app/terminal/extrakeys/ExtraKeysView.java @@ -1,4 +1,4 @@ -package com.termux.app.input.extrakeys; +package com.termux.app.terminal.extrakeys; import android.annotation.SuppressLint; import android.content.Context; diff --git a/app/src/main/res/layout/extra_keys_main.xml b/app/src/main/res/layout/extra_keys_main.xml index 90584317..aaf57980 100644 --- a/app/src/main/res/layout/extra_keys_main.xml +++ b/app/src/main/res/layout/extra_keys_main.xml @@ -1,5 +1,5 @@ - Date: Fri, 12 Mar 2021 05:53:10 +0500 Subject: [PATCH 021/136] Update TermuxConstants The `TermuxConstants` classes has been updated to `v0.3.0`. Check its Changelog sections for info on changes. --- .../java/com/termux/app/TermuxConstants.java | 35 ++++++------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/termux/app/TermuxConstants.java b/app/src/main/java/com/termux/app/TermuxConstants.java index d1c3acb6..8695e715 100644 --- a/app/src/main/java/com/termux/app/TermuxConstants.java +++ b/app/src/main/java/com/termux/app/TermuxConstants.java @@ -5,7 +5,7 @@ import android.annotation.SuppressLint; import java.io.File; /* - * Version: v0.2.0 + * Version: v0.3.0 * * Changelog * @@ -23,6 +23,14 @@ import java.io.File; * - Renamed `CONFIG_HOME_PATH` to `TERMUX_CONFIG_HOME_DIR_PATH`. * - Updated javadocs and spacing. * + * - 0.3.0 (2021-03-12) + * - Remove TERMUX_CACHE_DIR_PATH*, TERMUX_DATABASES_DIR_PATH*, TERMUX_SHARED_PREFERENCES_DIR_PATH* + * since they may not be consistent on all devices. + * - Renamed `TERMUX_DEFAULT_PREFERENCES_FILE_BASENAME` to + * `TERMUX_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION`. This should be used for + * accessing shared preferences between Termux app and its plugins if ever needed by first + * getting shared package context with {@link Context.createPackageContext(String,int}). + * */ /** @@ -121,23 +129,6 @@ public final class TermuxConstants { public static final File INTERNAL_PRIVATE_APP_DATA_DIR = new File(INTERNAL_PRIVATE_APP_DATA_DIR_PATH); - /** Termux app cache directory path */ - public static final String TERMUX_CACHE_DIR_PATH = INTERNAL_PRIVATE_APP_DATA_DIR_PATH + "/cache"; // Default: "/data/data/com.termux/cache" - /** Termux app cache directory */ - public static final File TERMUX_CACHE_DIR = new File(TERMUX_CACHE_DIR_PATH); - - - /** Termux app database directory path */ - public static final String TERMUX_DATABASES_DIR_PATH = INTERNAL_PRIVATE_APP_DATA_DIR_PATH + "/databases"; // Default: "/data/data/com.termux/databases" - /** Termux app database directory */ - public static final File TERMUX_DATABASES_DIR = new File(TERMUX_DATABASES_DIR_PATH); - - - /** Termux app shared preferences directory path */ - public static final String TERMUX_SHARED_PREFERENCES_DIR_PATH = INTERNAL_PRIVATE_APP_DATA_DIR_PATH + "/shared_prefs"; // Default: "/data/data/com.termux/shared_prefs" - /** Termux app shared preferences directory */ - public static final File TERMUX_SHARED_PREFERENCES_DIR = new File(TERMUX_SHARED_PREFERENCES_DIR_PATH); - /** Termux app Files directory path */ public static final String TERMUX_FILES_DIR_PATH = INTERNAL_PRIVATE_APP_DATA_DIR_PATH + "/files"; // Default: "/data/data/com.termux/files" @@ -239,13 +230,9 @@ public final class TermuxConstants { * Termux app core file paths. */ - /* Termux app default SharedPreferences file basename */ - public static final String TERMUX_DEFAULT_PREFERENCES_FILE_BASENAME = TERMUX_PACKAGE_NAME + "_preferences.xml"; // Default: "com.termux_preferences.xml" + /* Termux app default SharedPreferences file basename without extension */ + public static final String TERMUX_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION = TERMUX_PACKAGE_NAME + "_preferences"; // Default: "com.termux_preferences" - /* Termux app default SharedPreferences file path */ - public static final String TERMUX_DEFAULT_PREFERENCES_FILE_PATH = TERMUX_SHARED_PREFERENCES_DIR_PATH + "/" + TERMUX_DEFAULT_PREFERENCES_FILE_BASENAME; // Default: "/data/data/com.termux/shared_prefs/com.termux_preferences.xml" - /* Termux app default SharedPreferences file */ - public static final File TERMUX_DEFAULT_PREFERENCES_FILE = new File(TERMUX_DEFAULT_PREFERENCES_FILE_PATH); /* Termux app termux.properties primary file path */ public static final String TERMUX_PROPERTIES_PRIMARY_FILE_PATH = TERMUX_DATA_HOME_DIR_PATH + "/termux.properties"; // Default: "/data/data/com.termux/files/home/.termux/termux.properties" From ebf2e472b3f33ca9af4d98b2cc15371af503d214 Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Fri, 12 Mar 2021 05:54:12 +0500 Subject: [PATCH 022/136] Fix typo in TermuxPropertyConstants --- .../app/settings/properties/TermuxPropertyConstants.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/com/termux/app/settings/properties/TermuxPropertyConstants.java b/app/src/main/java/com/termux/app/settings/properties/TermuxPropertyConstants.java index 9b8731a6..a68e0466 100644 --- a/app/src/main/java/com/termux/app/settings/properties/TermuxPropertyConstants.java +++ b/app/src/main/java/com/termux/app/settings/properties/TermuxPropertyConstants.java @@ -29,7 +29,7 @@ import java.util.Set; * This class will be hosted by termux-app and should be imported by other termux plugin apps as is * instead of copying constants to random classes. The 3rd party apps can also import it for * interacting with termux apps. If changes are made to this file, increment the version number - * * and add an entry in the Changelog section above. + * and add an entry in the Changelog section above. * * The properties are loaded from the first file found at * {@link TermuxConstants#TERMUX_PROPERTIES_PRIMARY_FILE_PATH} or @@ -66,7 +66,6 @@ public final class TermuxPropertyConstants { - /** Defines the key for whether to use black UI */ public static final String KEY_USE_BLACK_UI = "use-black-ui"; // Default: "use-black-ui" From 93b506a001e579f3211138a55bf4551d176f6139 Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Fri, 12 Mar 2021 06:01:31 +0500 Subject: [PATCH 023/136] Move Termux Preferences to com.termux.app.settings.preferences package The termux preferences handling was mixed in with termux properties before an earlier commit. They are now moved out of into a separate sub package, the following classes are added: - `TermuxPreferenceConstants` class that defines shared constants of the preferences used by Termux app and its plugins. This class should be imported by other termux plugin apps instead of copying and defining their own constants. - `TermuxSharedPreferences` class that acts as manager for handling termux preferences. --- .../java/com/termux/app/TermuxActivity.java | 38 +++-- .../com/termux/app/TermuxPreferences.java | 96 ------------ .../java/com/termux/app/TermuxService.java | 4 +- .../TermuxPreferenceConstants.java | 50 +++++++ .../preferences/TermuxSharedPreferences.java | 138 ++++++++++++++++++ 5 files changed, 215 insertions(+), 111 deletions(-) delete mode 100644 app/src/main/java/com/termux/app/TermuxPreferences.java create mode 100644 app/src/main/java/com/termux/app/settings/preferences/TermuxPreferenceConstants.java create mode 100644 app/src/main/java/com/termux/app/settings/preferences/TermuxSharedPreferences.java diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java index f5df773d..ed12ec61 100644 --- a/app/src/main/java/com/termux/app/TermuxActivity.java +++ b/app/src/main/java/com/termux/app/TermuxActivity.java @@ -48,6 +48,7 @@ import android.widget.Toast; import com.termux.R; import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY; +import com.termux.app.settings.preferences.TermuxSharedPreferences; import com.termux.app.terminal.BellHandler; import com.termux.app.terminal.TermuxViewClient; import com.termux.app.terminal.extrakeys.ExtraKeysView; @@ -119,9 +120,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection ExtraKeysView mExtraKeysView; - TermuxPreferences mSettings; + private TermuxSharedPreferences mPreferences; - TermuxSharedProperties mProperties; + private TermuxSharedProperties mProperties; /** Initialized in {@link #onServiceConnected(ComponentName, IBinder)}. */ ArrayAdapter mListViewAdapter; @@ -212,7 +213,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection @Override public void onCreate(Bundle bundle) { - mSettings = new TermuxPreferences(this); + mPreferences = new TermuxSharedPreferences(this); mProperties = new TermuxSharedProperties(this); mIsUsingBlackUI = mProperties.isUsingBlackUI(); @@ -246,12 +247,12 @@ public final class TermuxActivity extends Activity implements ServiceConnection mTerminalView = findViewById(R.id.terminal_view); mTerminalView.setOnKeyListener(new TermuxViewClient(this)); - mTerminalView.setTextSize(mSettings.getFontSize()); - mTerminalView.setKeepScreenOn(mSettings.isScreenAlwaysOn()); + mTerminalView.setTextSize(mPreferences.getFontSize()); + mTerminalView.setKeepScreenOn(mPreferences.getKeepScreenOn()); mTerminalView.requestFocus(); final ViewPager viewPager = findViewById(R.id.viewpager); - if (mSettings.mShowExtraKeys) viewPager.setVisibility(View.VISIBLE); + if (mPreferences.getShowExtraKeys()) viewPager.setVisibility(View.VISIBLE); ViewGroup.LayoutParams layoutParams = viewPager.getLayoutParams(); @@ -382,7 +383,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection public void toggleShowExtraKeys() { final ViewPager viewPager = findViewById(R.id.viewpager); - final boolean showNow = mSettings.toggleShowExtraKeys(TermuxActivity.this); + final boolean showNow = mPreferences.toggleShowExtraKeys(); viewPager.setVisibility(showNow ? View.VISIBLE : View.GONE); if (showNow && viewPager.getCurrentItem() == 1) { // Focus the text input view if just revealed. @@ -636,7 +637,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection super.onStop(); mIsVisible = false; TerminalSession currentSession = getCurrentTermSession(); - if (currentSession != null) TermuxPreferences.storeCurrentSession(this, currentSession); + if (currentSession != null) mPreferences.setCurrentSession(currentSession.mHandle); unregisterReceiver(mBroadcastReceiever); getDrawer().closeDrawers(); } @@ -739,7 +740,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection menu.add(Menu.NONE, CONTEXTMENU_RESET_TERMINAL_ID, Menu.NONE, R.string.reset_terminal); menu.add(Menu.NONE, CONTEXTMENU_KILL_PROCESS_ID, Menu.NONE, getResources().getString(R.string.kill_process, getCurrentTermSession().getPid())).setEnabled(currentSession.isRunning()); menu.add(Menu.NONE, CONTEXTMENU_STYLING_ID, Menu.NONE, R.string.style_terminal); - menu.add(Menu.NONE, CONTEXTMENU_TOGGLE_KEEP_SCREEN_ON, Menu.NONE, R.string.toggle_keep_screen_on).setCheckable(true).setChecked(mSettings.isScreenAlwaysOn()); + menu.add(Menu.NONE, CONTEXTMENU_TOGGLE_KEEP_SCREEN_ON, Menu.NONE, R.string.toggle_keep_screen_on).setCheckable(true).setChecked(mPreferences.getKeepScreenOn()); menu.add(Menu.NONE, CONTEXTMENU_HELP_ID, Menu.NONE, R.string.help); } @@ -950,10 +951,10 @@ public final class TermuxActivity extends Activity implements ServiceConnection case CONTEXTMENU_TOGGLE_KEEP_SCREEN_ON: { if(mTerminalView.getKeepScreenOn()) { mTerminalView.setKeepScreenOn(false); - mSettings.setScreenAlwaysOn(this, false); + mPreferences.setKeepScreenOn(false); } else { mTerminalView.setKeepScreenOn(true); - mSettings.setScreenAlwaysOn(this, true); + mPreferences.setKeepScreenOn(true); } return true; } @@ -978,8 +979,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection } public void changeFontSize(boolean increase) { - mSettings.changeFontSize(this, increase); - mTerminalView.setTextSize(mSettings.getFontSize()); + mPreferences.changeFontSize(this, increase); + mTerminalView.setTextSize(mPreferences.getFontSize()); } public void doPaste() { @@ -995,12 +996,21 @@ public final class TermuxActivity extends Activity implements ServiceConnection /** The current session as stored or the last one if that does not exist. */ public TerminalSession getStoredCurrentSessionOrLast() { - TerminalSession stored = TermuxPreferences.getCurrentSession(this); + TerminalSession stored = getCurrentSession(this); if (stored != null) return stored; List sessions = mTermService.getSessions(); return sessions.isEmpty() ? null : sessions.get(sessions.size() - 1); } + private TerminalSession getCurrentSession(TermuxActivity context) { + String sessionHandle = mPreferences.getCurrentSession(); + for (int i = 0, len = context.getTermService().getSessions().size(); i < len; i++) { + TerminalSession session = context.getTermService().getSessions().get(i); + if (session.mHandle.equals(sessionHandle)) return session; + } + return null; + } + /** Show a toast and dismiss the last one if still visible. */ void showToast(String text, boolean longDuration) { if (mLastToast != null) mLastToast.cancel(); diff --git a/app/src/main/java/com/termux/app/TermuxPreferences.java b/app/src/main/java/com/termux/app/TermuxPreferences.java deleted file mode 100644 index 5567439d..00000000 --- a/app/src/main/java/com/termux/app/TermuxPreferences.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.termux.app; - -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.util.TypedValue; - -import com.termux.terminal.TerminalSession; - -final class TermuxPreferences { - - private final int MIN_FONTSIZE; - private static final int MAX_FONTSIZE = 256; - - private static final String SHOW_EXTRA_KEYS_KEY = "show_extra_keys"; - private static final String FONTSIZE_KEY = "fontsize"; - private static final String CURRENT_SESSION_KEY = "current_session"; - private static final String SCREEN_ALWAYS_ON_KEY = "screen_always_on"; - - private boolean mScreenAlwaysOn; - private int mFontSize; - boolean mShowExtraKeys; - - TermuxPreferences(Context context) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - - float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, context.getResources().getDisplayMetrics()); - - // This is a bit arbitrary and sub-optimal. We want to give a sensible default for minimum font size - // to prevent invisible text due to zoom be mistake: - MIN_FONTSIZE = (int) (4f * dipInPixels); - - mShowExtraKeys = prefs.getBoolean(SHOW_EXTRA_KEYS_KEY, true); - mScreenAlwaysOn = prefs.getBoolean(SCREEN_ALWAYS_ON_KEY, false); - - // http://www.google.com/design/spec/style/typography.html#typography-line-height - int defaultFontSize = Math.round(12 * dipInPixels); - // Make it divisible by 2 since that is the minimal adjustment step: - if (defaultFontSize % 2 == 1) defaultFontSize--; - - try { - mFontSize = Integer.parseInt(prefs.getString(FONTSIZE_KEY, Integer.toString(defaultFontSize))); - } catch (NumberFormatException | ClassCastException e) { - mFontSize = defaultFontSize; - } - mFontSize = clamp(mFontSize, MIN_FONTSIZE, MAX_FONTSIZE); - } - - boolean toggleShowExtraKeys(Context context) { - mShowExtraKeys = !mShowExtraKeys; - PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SHOW_EXTRA_KEYS_KEY, mShowExtraKeys).apply(); - return mShowExtraKeys; - } - - int getFontSize() { - return mFontSize; - } - - void changeFontSize(Context context, boolean increase) { - mFontSize += (increase ? 1 : -1) * 2; - mFontSize = Math.max(MIN_FONTSIZE, Math.min(mFontSize, MAX_FONTSIZE)); - - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - prefs.edit().putString(FONTSIZE_KEY, Integer.toString(mFontSize)).apply(); - } - - boolean isScreenAlwaysOn() { - return mScreenAlwaysOn; - } - - void setScreenAlwaysOn(Context context, boolean newValue) { - mScreenAlwaysOn = newValue; - PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SCREEN_ALWAYS_ON_KEY, newValue).apply(); - } - - static void storeCurrentSession(Context context, TerminalSession session) { - PreferenceManager.getDefaultSharedPreferences(context).edit().putString(TermuxPreferences.CURRENT_SESSION_KEY, session.mHandle).apply(); - } - - static TerminalSession getCurrentSession(TermuxActivity context) { - String sessionHandle = PreferenceManager.getDefaultSharedPreferences(context).getString(TermuxPreferences.CURRENT_SESSION_KEY, ""); - for (int i = 0, len = context.mTermService.getSessions().size(); i < len; i++) { - TerminalSession session = context.mTermService.getSessions().get(i); - if (session.mHandle.equals(sessionHandle)) return session; - } - return null; - } - - /** - * If value is not in the range [min, max], set it to either min or max. - */ - static int clamp(int value, int min, int max) { - return Math.min(Math.max(value, min), max); - } - -} diff --git a/app/src/main/java/com/termux/app/TermuxService.java b/app/src/main/java/com/termux/app/TermuxService.java index 0f4dd465..3301b30a 100644 --- a/app/src/main/java/com/termux/app/TermuxService.java +++ b/app/src/main/java/com/termux/app/TermuxService.java @@ -24,6 +24,7 @@ import android.widget.ArrayAdapter; import com.termux.R; import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY; import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_SERVICE; +import com.termux.app.settings.preferences.TermuxSharedPreferences; import com.termux.terminal.EmulatorDebug; import com.termux.terminal.TerminalSession; import com.termux.terminal.TerminalSession.SessionChangedCallback; @@ -148,7 +149,8 @@ public final class TermuxService extends Service implements SessionChangedCallba } // Make the newly created session the current one to be displayed: - TermuxPreferences.storeCurrentSession(this, newSession); + TermuxSharedPreferences preferences = new TermuxSharedPreferences(this); + preferences.setCurrentSession(newSession.mHandle); // Launch the main Termux app, which will now show the current session: startActivity(new Intent(this, TermuxActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); diff --git a/app/src/main/java/com/termux/app/settings/preferences/TermuxPreferenceConstants.java b/app/src/main/java/com/termux/app/settings/preferences/TermuxPreferenceConstants.java new file mode 100644 index 00000000..f9ba174d --- /dev/null +++ b/app/src/main/java/com/termux/app/settings/preferences/TermuxPreferenceConstants.java @@ -0,0 +1,50 @@ +package com.termux.app.settings.preferences; + +import com.termux.app.TermuxConstants; + +/* + * Version: v0.1.0 + * + * Changelog + * + * - 0.1.0 (2021-03-12) + * - Initial Release. + * + */ + +/** + * A class that defines shared constants of the Shared preferences used by Termux app and its plugins. + * This class will be hosted by termux-app and should be imported by other termux plugin apps as is + * instead of copying constants to random classes. The 3rd party apps can also import it for + * interacting with termux apps. If changes are made to this file, increment the version number + * and add an entry in the Changelog section above. + * + * The properties are loaded from the first file found at + * {@link TermuxConstants#TERMUX_PROPERTIES_PRIMARY_FILE_PATH} or + * {@link TermuxConstants#TERMUX_PROPERTIES_SECONDARY_FILE_PATH} + */ +public final class TermuxPreferenceConstants { + + /** Defines the key for whether to show extra keys in termux terminal view */ + public static final String KEY_SHOW_EXTRA_KEYS = "show_extra_keys"; + public static final boolean DEFAULT_VALUE_SHOW_EXTRA_KEYS = true; + + + + /** Defines the key for whether to always keep screen on */ + public static final String KEY_KEEP_SCREEN_ON = "screen_always_on"; + public static final boolean DEFAULT_VALUE_KEEP_SCREEN_ON = true; + + + + /** Defines the key for font size of termux terminal view */ + public static final String KEY_FONTSIZE = "fontsize"; + + + + /** Defines the key for current termux terminal session */ + public static final String KEY_CURRENT_SESSION = "current_session"; + + + +} diff --git a/app/src/main/java/com/termux/app/settings/preferences/TermuxSharedPreferences.java b/app/src/main/java/com/termux/app/settings/preferences/TermuxSharedPreferences.java new file mode 100644 index 00000000..65fe9679 --- /dev/null +++ b/app/src/main/java/com/termux/app/settings/preferences/TermuxSharedPreferences.java @@ -0,0 +1,138 @@ +package com.termux.app.settings.preferences; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; +import android.util.TypedValue; + +import com.termux.app.TermuxConstants; +import com.termux.terminal.EmulatorDebug; + +import javax.annotation.Nonnull; + +public class TermuxSharedPreferences { + + private final Context mContext; + private final SharedPreferences mSharedPreferences; + + + private int MIN_FONTSIZE; + private int MAX_FONTSIZE; + private int DEFAULT_FONTSIZE; + + public TermuxSharedPreferences(@Nonnull Context context) { + Context mTempContext; + + try { + mTempContext = context.createPackageContext(TermuxConstants.TERMUX_PACKAGE_NAME, Context.CONTEXT_RESTRICTED); + } catch (Exception e) { + Log.e(EmulatorDebug.LOG_TAG, "Failed to get \"" + TermuxConstants.TERMUX_PACKAGE_NAME + "\" package context", e); + Log.e(EmulatorDebug.LOG_TAG, "Force using current context"); + mTempContext = context; + } + + mContext = mTempContext; + mSharedPreferences = getSharedPreferences(mContext); + + setFontVariables(context); + } + + private static SharedPreferences getSharedPreferences(Context context) { + return context.getSharedPreferences(TermuxConstants.TERMUX_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION, Context.MODE_PRIVATE); + } + + + + public boolean getShowExtraKeys() { + return mSharedPreferences.getBoolean(TermuxPreferenceConstants.KEY_SHOW_EXTRA_KEYS, TermuxPreferenceConstants.DEFAULT_VALUE_SHOW_EXTRA_KEYS); + } + + public void setShowExtraKeys(boolean value) { + mSharedPreferences.edit().putBoolean(TermuxPreferenceConstants.KEY_SHOW_EXTRA_KEYS, value).apply(); + } + + public boolean toggleShowExtraKeys() { + boolean currentValue = getShowExtraKeys(); + setShowExtraKeys(!currentValue); + return !currentValue; + } + + + + public boolean getKeepScreenOn() { + return mSharedPreferences.getBoolean(TermuxPreferenceConstants.KEY_KEEP_SCREEN_ON, TermuxPreferenceConstants.DEFAULT_VALUE_KEEP_SCREEN_ON); + } + + public void setKeepScreenOn(boolean value) { + mSharedPreferences.edit().putBoolean(TermuxPreferenceConstants.KEY_KEEP_SCREEN_ON, value).apply(); + } + + + + private void setFontVariables(Context context) { + float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, context.getResources().getDisplayMetrics()); + + // This is a bit arbitrary and sub-optimal. We want to give a sensible default for minimum font size + // to prevent invisible text due to zoom be mistake: + MIN_FONTSIZE = (int) (4f * dipInPixels); + + // http://www.google.com/design/spec/style/typography.html#typography-line-height + int defaultFontSize = Math.round(12 * dipInPixels); + // Make it divisible by 2 since that is the minimal adjustment step: + if (defaultFontSize % 2 == 1) defaultFontSize--; + + DEFAULT_FONTSIZE = defaultFontSize; + + MAX_FONTSIZE = 256; + } + + public int getFontSize() { + int fontSize; + String fontString; + + try { + fontString = mSharedPreferences.getString(TermuxPreferenceConstants.KEY_FONTSIZE, Integer.toString(DEFAULT_FONTSIZE)); + if(fontString != null) + fontSize = Integer.parseInt(fontString); + else + fontSize = DEFAULT_FONTSIZE; + } catch (NumberFormatException | ClassCastException e) { + fontSize = DEFAULT_FONTSIZE; + } + fontSize = clamp(fontSize, MIN_FONTSIZE, MAX_FONTSIZE); + + return fontSize; + } + + public void setFontSize(String value) { + mSharedPreferences.edit().putString(TermuxPreferenceConstants.KEY_FONTSIZE, value).apply(); + } + + public void changeFontSize(Context context, boolean increase) { + + int fontSize = getFontSize(); + + fontSize += (increase ? 1 : -1) * 2; + fontSize = Math.max(MIN_FONTSIZE, Math.min(fontSize, MAX_FONTSIZE)); + + setFontSize(Integer.toString(fontSize)); + } + + /** + * If value is not in the range [min, max], set it to either min or max. + */ + static int clamp(int value, int min, int max) { + return Math.min(Math.max(value, min), max); + } + + + + public String getCurrentSession() { + return mSharedPreferences.getString(TermuxPreferenceConstants.KEY_CURRENT_SESSION, ""); + } + + public void setCurrentSession(String value) { + mSharedPreferences.edit().putString(TermuxPreferenceConstants.KEY_CURRENT_SESSION, value).apply(); + } + +} From d39972b3bfe3361c1437e5faaf6ea8ffbcb0d8ef Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Sat, 13 Mar 2021 16:49:29 +0500 Subject: [PATCH 024/136] Implement GUI based Termux settings manager and a centralized logging framework The settings activity can be accessed by long pressing on terminal view and selecting "Settings" from the popup shown. It uses the Android's Preference framework. Currently only debugging preferences to set log level and enabling terminal view key logging are provided. The Preference framework by default uses the keys set in `app:key` attribute in the respective preferences XML file to store the values in the default `SharedPreferences` file of the app. However, since we rely on `TermuxPreferenceConstants` and `TermuxPropertyConstants` classes to define key names so that they can be easily shared between termux and its plugin apps, we provide our own `PreferenceDataStore` for storing key/value pairs. The key name in the XML file can optionally be the same. Check `DebuggingPreferencesFragment` class for a sample. Each new preference category fragment should be added to `app/settings/` with its data store. This commit may allow support to be added for modifying `termux.properties` file directly from the UI but that requires more work, since writing to property files with comments require in-place modification. The `Logger` class provides various static functions for logging that should be used from now on instead of directly calling android `Log.*` functions. The log level is automatically loaded from shared preferences at application startup via `TermuxApplication` and set in the static `Logger.CURRENT_LOG_LEVEL` variable. Changing the log level through the settings activity also changes the log level immediately. The 4 supported log levels are: - LOG_LEVEL_OFF which will log nothing. - LOG_LEVEL_NORMAL which will start logging error, warn and info messages and stacktraces. - LOG_LEVEL_DEBUG which will start logging debug messages. - LOG_LEVEL_VERBOSE which will start logging verbose messages. The default log level is `LOG_LEVEL_NORMAL` which will not log debug or verbose messages. Contributors can add useful log entries at those levels where ever they feel is appropriate so that it allows users and devs to more easily help solve issues or find bugs, specially without having to recompile termux after having to manually add general log entries to the source. DO NOT log data that may have private info of users like command arguments at log levels below debug, like `BackgroundJob` was doing previously. Logging to file support may be added later, will require log file rotation support and storage permissions. --- app/build.gradle | 2 + app/src/main/AndroidManifest.xml | 133 +++++++---- .../java/com/termux/app/BackgroundJob.java | 20 +- .../com/termux/app/RunCommandService.java | 12 +- .../java/com/termux/app/TermuxActivity.java | 29 ++- .../com/termux/app/TermuxApplication.java | 23 ++ .../java/com/termux/app/TermuxInstaller.java | 13 +- .../com/termux/app/TermuxOpenReceiver.java | 15 +- .../java/com/termux/app/TermuxService.java | 23 +- .../termux/app/TermuxSettingsActivity.java | 43 ++++ .../DebuggingPreferencesFragment.java | 122 ++++++++++ .../TermuxPreferenceConstants.java | 14 +- .../preferences/TermuxSharedPreferences.java | 43 +++- .../settings/properties/SharedProperties.java | 22 +- .../properties/TermuxPropertyConstants.java | 5 +- .../properties/TermuxSharedProperties.java | 23 +- .../termux/app/terminal/TermuxViewClient.java | 85 +++++-- .../java/com/termux/app/utils/Logger.java | 223 ++++++++++++++++++ .../com/termux/app/utils/TermuxUtils.java | 18 ++ .../TermuxFileReceiverActivity.java | 8 +- app/src/main/res/layout/settings_activity.xml | 9 + app/src/main/res/values/strings.xml | 31 +++ .../main/res/xml/debugging_preferences.xml | 21 ++ app/src/main/res/xml/root_preferences.xml | 8 + .../java/com/termux/view/TerminalView.java | 60 +++-- .../com/termux/view/TerminalViewClient.java | 29 ++- 26 files changed, 855 insertions(+), 179 deletions(-) create mode 100644 app/src/main/java/com/termux/app/TermuxApplication.java create mode 100644 app/src/main/java/com/termux/app/TermuxSettingsActivity.java create mode 100644 app/src/main/java/com/termux/app/settings/DebuggingPreferencesFragment.java create mode 100644 app/src/main/java/com/termux/app/utils/Logger.java create mode 100644 app/src/main/java/com/termux/app/utils/TermuxUtils.java create mode 100644 app/src/main/res/layout/settings_activity.xml create mode 100644 app/src/main/res/xml/debugging_preferences.xml create mode 100644 app/src/main/res/xml/root_preferences.xml diff --git a/app/build.gradle b/app/build.gradle index c1f0c6df..52c51d4b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -87,6 +87,8 @@ android { } dependencies { + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.preference:preference:1.1.1' testImplementation 'junit:junit:4.13.1' testImplementation 'org.robolectric:robolectric:4.4' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0a2e70a6..346f6d3b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,17 +1,23 @@ + + android:sharedUserLabel="@string/shared_user_label"> - - + + - @@ -20,65 +26,95 @@ - - + + + android:banner="@drawable/banner" + android:extractNativeLibs="true" + android:icon="@mipmap/ic_launcher" + android:label="@string/application_name" + android:roundIcon="@mipmap/ic_launcher_round" + android:supportsRtl="false" + android:theme="@style/Theme.Termux"> - - + + + android:windowSoftInputMode="adjustResize|stateAlwaysVisible"> + + - + + + + + + + + + + + + + + android:theme="@android:style/Theme.Material.Light.DarkActionBar" /> + + + android:taskAffinity="${TERMUX_PACKAGE_NAME}.filereceiver"> + - + + + @@ -89,8 +125,10 @@ - + + + @@ -99,23 +137,11 @@ - - - - - - - - - - @@ -125,11 +151,10 @@ - + android:permission="${TERMUX_PACKAGE_NAME}.permission.RUN_COMMAND"> @@ -137,13 +162,19 @@ - - - + + + + diff --git a/app/src/main/java/com/termux/app/BackgroundJob.java b/app/src/main/java/com/termux/app/BackgroundJob.java index 3f502d16..61e32317 100644 --- a/app/src/main/java/com/termux/app/BackgroundJob.java +++ b/app/src/main/java/com/termux/app/BackgroundJob.java @@ -4,9 +4,9 @@ import android.app.Activity; import android.app.PendingIntent; import android.content.Intent; import android.os.Bundle; -import android.util.Log; import com.termux.BuildConfig; +import com.termux.app.utils.Logger; import java.io.BufferedReader; import java.io.File; @@ -26,10 +26,10 @@ import java.util.List; */ public final class BackgroundJob { - private static final String LOG_TAG = "termux-task"; - final Process mProcess; + private static final String LOG_TAG = "BackgroundJob"; + public BackgroundJob(String cwd, String fileToExecute, final String[] args, final TermuxService service){ this(cwd, fileToExecute, args, service, null); } @@ -47,7 +47,7 @@ public final class BackgroundJob { } catch (IOException e) { mProcess = null; // TODO: Visible error message? - Log.e(LOG_TAG, "Failed running background job: " + processDescription, e); + Logger.logStackTraceWithMessage(LOG_TAG, "Failed running background job: " + processDescription, e); return; } @@ -67,7 +67,7 @@ public final class BackgroundJob { // FIXME: Long lines. while ((line = reader.readLine()) != null) { errResult.append(line).append('\n'); - Log.i(LOG_TAG, "[" + pid + "] stderr: " + line); + Logger.logDebug(LOG_TAG, "[" + pid + "] stderr: " + line); } } catch (IOException e) { // Ignore. @@ -79,7 +79,7 @@ public final class BackgroundJob { new Thread() { @Override public void run() { - Log.i(LOG_TAG, "[" + pid + "] starting: " + processDescription); + Logger.logDebug(LOG_TAG, "[" + pid + "] starting: " + processDescription); InputStream stdout = mProcess.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(stdout, StandardCharsets.UTF_8)); @@ -87,20 +87,20 @@ public final class BackgroundJob { try { // FIXME: Long lines. while ((line = reader.readLine()) != null) { - Log.i(LOG_TAG, "[" + pid + "] stdout: " + line); + Logger.logDebug(LOG_TAG, "[" + pid + "] stdout: " + line); outResult.append(line).append('\n'); } } catch (IOException e) { - Log.e(LOG_TAG, "Error reading output", e); + Logger.logStackTraceWithMessage(LOG_TAG, "Error reading output", e); } try { int exitCode = mProcess.waitFor(); service.onBackgroundJobExited(BackgroundJob.this); if (exitCode == 0) { - Log.i(LOG_TAG, "[" + pid + "] exited normally"); + Logger.logDebug(LOG_TAG, "[" + pid + "] exited normally"); } else { - Log.w(LOG_TAG, "[" + pid + "] exited with code: " + exitCode); + Logger.logDebug(LOG_TAG, "[" + pid + "] exited with code: " + exitCode); } result.putString("stdout", outResult.toString()); diff --git a/app/src/main/java/com/termux/app/RunCommandService.java b/app/src/main/java/com/termux/app/RunCommandService.java index f186e5b2..8745d31a 100644 --- a/app/src/main/java/com/termux/app/RunCommandService.java +++ b/app/src/main/java/com/termux/app/RunCommandService.java @@ -10,13 +10,13 @@ import android.net.Uri; import android.os.Binder; import android.os.Build; import android.os.IBinder; -import android.util.Log; import com.termux.R; import com.termux.app.TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE; import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_SERVICE; import com.termux.app.settings.properties.TermuxPropertyConstants; import com.termux.app.settings.properties.TermuxSharedProperties; +import com.termux.app.utils.Logger; import java.io.File; import java.io.FileInputStream; @@ -84,6 +84,8 @@ public class RunCommandService extends Service { private static final String NOTIFICATION_CHANNEL_ID = "termux_run_command_notification_channel"; private static final int NOTIFICATION_ID = 1338; + private static final String LOG_TAG = "RunCommandService"; + class LocalBinder extends Binder { public final RunCommandService service = RunCommandService.this; } @@ -107,13 +109,13 @@ public class RunCommandService extends Service { // If wrong action passed, then just return if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) { - Log.e("termux", "Unexpected intent action to RunCommandService: " + intent.getAction()); + Logger.logError(LOG_TAG, "Unexpected intent action to RunCommandService: " + intent.getAction()); return Service.START_NOT_STICKY; } // If allow-external-apps property is not set to "true" if (!TermuxSharedProperties.isPropertyValueTrue(this, TermuxPropertyConstants.getTermuxPropertiesFile(), TermuxConstants.PROP_ALLOW_EXTERNAL_APPS)) { - Log.e("termux", "RunCommandService requires allow-external-apps property to be set to \"true\" in \"" + TermuxConstants.TERMUX_PROPERTIES_PRIMARY_FILE_PATH + "\" file"); + Logger.logError(LOG_TAG, "RunCommandService requires allow-external-apps property to be set to \"true\" in \"" + TermuxConstants.TERMUX_PROPERTIES_PRIMARY_FILE_PATH + "\" file"); return Service.START_NOT_STICKY; } @@ -155,7 +157,7 @@ public class RunCommandService extends Service { private Notification buildNotification() { Notification.Builder builder = new Notification.Builder(this); - builder.setContentTitle(getText(R.string.application_name) + " Run Command"); + builder.setContentTitle(TermuxConstants.TERMUX_APP_NAME + " Run Command"); builder.setSmallIcon(R.drawable.ic_service_notification); // Use a low priority: @@ -177,7 +179,7 @@ public class RunCommandService extends Service { private void setupNotificationChannel() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return; - String channelName = "Termux Run Command"; + String channelName = TermuxConstants.TERMUX_APP_NAME + " Run Command"; int importance = NotificationManager.IMPORTANCE_LOW; NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, importance); diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java index ed12ec61..8b75f7ee 100644 --- a/app/src/main/java/com/termux/app/TermuxActivity.java +++ b/app/src/main/java/com/termux/app/TermuxActivity.java @@ -28,7 +28,6 @@ import android.text.SpannableString; import android.text.Spanned; import android.text.TextUtils; import android.text.style.StyleSpan; -import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.Gravity; @@ -55,7 +54,7 @@ import com.termux.app.terminal.extrakeys.ExtraKeysView; import com.termux.app.terminal.FullScreenWorkAround; import com.termux.app.settings.properties.TermuxPropertyConstants; import com.termux.app.settings.properties.TermuxSharedProperties; -import com.termux.terminal.EmulatorDebug; +import com.termux.app.utils.Logger; import com.termux.terminal.TerminalColors; import com.termux.terminal.TerminalSession; import com.termux.terminal.TerminalSession.SessionChangedCallback; @@ -100,6 +99,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection private static final int CONTEXTMENU_HELP_ID = 8; private static final int CONTEXTMENU_TOGGLE_KEEP_SCREEN_ON = 9; private static final int CONTEXTMENU_AUTOFILL_ID = 10; + private static final int CONTEXTMENU_SETTINGS_ID = 11; private static final int MAX_SESSIONS = 8; @@ -145,12 +145,14 @@ public final class TermuxActivity extends Activity implements ServiceConnection .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build(); int mBellSoundId; + private static final String LOG_TAG = "TermuxActivity"; + private final BroadcastReceiver mBroadcastReceiever = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (mIsVisible) { String whatToReload = intent.getStringExtra(TERMUX_ACTIVITY.EXTRA_RELOAD_STYLE); - Log.d("termux", "Reloading termux style for: " + whatToReload); + Logger.logDebug(LOG_TAG, "Reloading termux style for: " + whatToReload); if ("storage".equals(whatToReload)) { if (ensureStoragePermissionGranted()) TermuxInstaller.setupStorageSymlinks(TermuxActivity.this); @@ -190,7 +192,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection final Typeface newTypeface = (fontFile.exists() && fontFile.length() > 0) ? Typeface.createFromFile(fontFile) : Typeface.MONOSPACE; mTerminalView.setTypeface(newTypeface); } catch (Exception e) { - Log.e(EmulatorDebug.LOG_TAG, "Error in checkForFontAndColors()", e); + Logger.logStackTraceWithMessage(LOG_TAG, "Error in checkForFontAndColors()", e); } } @@ -245,10 +247,12 @@ public final class TermuxActivity extends Activity implements ServiceConnection } mTerminalView = findViewById(R.id.terminal_view); - mTerminalView.setOnKeyListener(new TermuxViewClient(this)); + + mTerminalView.setTerminalViewClient(new TermuxViewClient(this)); mTerminalView.setTextSize(mPreferences.getFontSize()); mTerminalView.setKeepScreenOn(mPreferences.getKeepScreenOn()); + mTerminalView.setIsTerminalViewKeyLoggingEnabled(mPreferences.getTerminalViewKeyLoggingEnabled()); mTerminalView.requestFocus(); final ViewPager viewPager = findViewById(R.id.viewpager); @@ -627,6 +631,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection registerReceiver(mBroadcastReceiever, new IntentFilter(TERMUX_ACTIVITY.ACTION_RELOAD_STYLE)); + mTerminalView.setIsTerminalViewKeyLoggingEnabled(mPreferences.getTerminalViewKeyLoggingEnabled()); + // The current terminal session may have changed while being away, force // a refresh of the displayed terminal: mTerminalView.onScreenUpdated(); @@ -742,6 +748,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection menu.add(Menu.NONE, CONTEXTMENU_STYLING_ID, Menu.NONE, R.string.style_terminal); menu.add(Menu.NONE, CONTEXTMENU_TOGGLE_KEEP_SCREEN_ON, Menu.NONE, R.string.toggle_keep_screen_on).setCheckable(true).setChecked(mPreferences.getKeepScreenOn()); menu.add(Menu.NONE, CONTEXTMENU_HELP_ID, Menu.NONE, R.string.help); + menu.add(Menu.NONE, CONTEXTMENU_SETTINGS_ID, Menu.NONE, R.string.settings); } /** Hook system menu to show context menu instead. */ @@ -948,6 +955,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection case CONTEXTMENU_HELP_ID: startActivity(new Intent(this, TermuxHelpActivity.class)); return true; + case CONTEXTMENU_SETTINGS_ID: + startActivity(new Intent(this, TermuxSettingsActivity.class)); + return true; case CONTEXTMENU_TOGGLE_KEEP_SCREEN_ON: { if(mTerminalView.getKeepScreenOn()) { mTerminalView.setKeepScreenOn(false); @@ -1036,6 +1046,10 @@ public final class TermuxActivity extends Activity implements ServiceConnection } } + public boolean isVisible() { + return mIsVisible; + } + public TermuxService getTermService() { return mTermService; } @@ -1047,8 +1061,13 @@ public final class TermuxActivity extends Activity implements ServiceConnection public ExtraKeysView getExtraKeysView() { return mExtraKeysView; } + public TermuxSharedProperties getProperties() { return mProperties; } + public TermuxSharedPreferences getPreferences() { + return mPreferences; + } + } diff --git a/app/src/main/java/com/termux/app/TermuxApplication.java b/app/src/main/java/com/termux/app/TermuxApplication.java new file mode 100644 index 00000000..94c4dbcc --- /dev/null +++ b/app/src/main/java/com/termux/app/TermuxApplication.java @@ -0,0 +1,23 @@ +package com.termux.app; + +import android.app.Application; + +import com.termux.app.settings.preferences.TermuxSharedPreferences; +import com.termux.app.utils.Logger; + + +public class TermuxApplication extends Application { + public void onCreate() { + super.onCreate(); + + updateLogLevel(); + } + + private void updateLogLevel() { + // Load the log level from shared preferences and set it to the {@link Loggger.CURRENT_LOG_LEVEL} + TermuxSharedPreferences preferences = new TermuxSharedPreferences(getApplicationContext()); + preferences.setLogLevel(null, preferences.getLogLevel()); + Logger.logDebug("Starting Application"); + } +} + diff --git a/app/src/main/java/com/termux/app/TermuxInstaller.java b/app/src/main/java/com/termux/app/TermuxInstaller.java index c5bf3d17..6b218a4f 100644 --- a/app/src/main/java/com/termux/app/TermuxInstaller.java +++ b/app/src/main/java/com/termux/app/TermuxInstaller.java @@ -7,12 +7,11 @@ import android.content.Context; import android.os.Environment; import android.os.UserManager; import android.system.Os; -import android.util.Log; import android.util.Pair; import android.view.WindowManager; import com.termux.R; -import com.termux.terminal.EmulatorDebug; +import com.termux.app.utils.Logger; import java.io.BufferedReader; import java.io.ByteArrayInputStream; @@ -46,6 +45,8 @@ import java.util.zip.ZipInputStream; */ final class TermuxInstaller { + private static final String LOG_TAG = "TermuxInstaller"; + /** Performs setup if necessary. */ static void setupIfNeeded(final Activity activity, final Runnable whenDone) { // Termux can only be run as the primary user (device owner) since only that @@ -130,7 +131,7 @@ final class TermuxInstaller { activity.runOnUiThread(whenDone); } catch (final Exception e) { - Log.e(EmulatorDebug.LOG_TAG, "Bootstrap error", e); + Logger.logStackTraceWithMessage(LOG_TAG, "Bootstrap error", e); activity.runOnUiThread(() -> { try { new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body) @@ -200,13 +201,13 @@ final class TermuxInstaller { try { deleteFolder(storageDir); } catch (IOException e) { - Log.e(LOG_TAG, "Could not delete old $HOME/storage, " + e.getMessage()); + Logger.logError(LOG_TAG, "Could not delete old $HOME/storage, " + e.getMessage()); return; } } if (!storageDir.mkdirs()) { - Log.e(LOG_TAG, "Unable to mkdirs() for $HOME/storage"); + Logger.logError(LOG_TAG, "Unable to mkdirs() for $HOME/storage"); return; } @@ -238,7 +239,7 @@ final class TermuxInstaller { } } } catch (Exception e) { - Log.e(LOG_TAG, "Error setting up link", e); + Logger.logStackTraceWithMessage(LOG_TAG, "Error setting up link", e); } } }.start(); diff --git a/app/src/main/java/com/termux/app/TermuxOpenReceiver.java b/app/src/main/java/com/termux/app/TermuxOpenReceiver.java index 960ab8e5..92ce70f8 100644 --- a/app/src/main/java/com/termux/app/TermuxOpenReceiver.java +++ b/app/src/main/java/com/termux/app/TermuxOpenReceiver.java @@ -11,10 +11,9 @@ import android.net.Uri; import android.os.Environment; import android.os.ParcelFileDescriptor; import android.provider.MediaStore; -import android.util.Log; import android.webkit.MimeTypeMap; -import com.termux.terminal.EmulatorDebug; +import com.termux.app.utils.Logger; import java.io.File; import java.io.FileNotFoundException; @@ -24,11 +23,13 @@ import androidx.annotation.NonNull; public class TermuxOpenReceiver extends BroadcastReceiver { + private static final String LOG_TAG = "TermuxOpenReceiver"; + @Override public void onReceive(Context context, Intent intent) { final Uri data = intent.getData(); if (data == null) { - Log.e(EmulatorDebug.LOG_TAG, "termux-open: Called without intent data"); + Logger.logError(LOG_TAG, "termux-open: Called without intent data"); return; } @@ -42,7 +43,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver { // Ok. break; default: - Log.e(EmulatorDebug.LOG_TAG, "Invalid action '" + intentAction + "', using 'view'"); + Logger.logError(LOG_TAG, "Invalid action '" + intentAction + "', using 'view'"); break; } @@ -59,14 +60,14 @@ public class TermuxOpenReceiver extends BroadcastReceiver { try { context.startActivity(urlIntent); } catch (ActivityNotFoundException e) { - Log.e(EmulatorDebug.LOG_TAG, "termux-open: No app handles the url " + data); + Logger.logError(LOG_TAG, "termux-open: No app handles the url " + data); } return; } final File fileToShare = new File(filePath); if (!(fileToShare.isFile() && fileToShare.canRead())) { - Log.e(EmulatorDebug.LOG_TAG, "termux-open: Not a readable file: '" + fileToShare.getAbsolutePath() + "'"); + Logger.logError(LOG_TAG, "termux-open: Not a readable file: '" + fileToShare.getAbsolutePath() + "'"); return; } @@ -103,7 +104,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver { try { context.startActivity(sendIntent); } catch (ActivityNotFoundException e) { - Log.e(EmulatorDebug.LOG_TAG, "termux-open: No app handles the url " + data); + Logger.logError(LOG_TAG, "termux-open: No app handles the url " + data); } } diff --git a/app/src/main/java/com/termux/app/TermuxService.java b/app/src/main/java/com/termux/app/TermuxService.java index 3301b30a..cc624d60 100644 --- a/app/src/main/java/com/termux/app/TermuxService.java +++ b/app/src/main/java/com/termux/app/TermuxService.java @@ -18,14 +18,13 @@ import android.os.Handler; import android.os.IBinder; import android.os.PowerManager; import android.provider.Settings; -import android.util.Log; import android.widget.ArrayAdapter; import com.termux.R; import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY; import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_SERVICE; import com.termux.app.settings.preferences.TermuxSharedPreferences; -import com.termux.terminal.EmulatorDebug; +import com.termux.app.utils.Logger; import com.termux.terminal.TerminalSession; import com.termux.terminal.TerminalSession.SessionChangedCallback; @@ -50,6 +49,8 @@ public final class TermuxService extends Service implements SessionChangedCallba private static final String NOTIFICATION_CHANNEL_ID = "termux_notification_channel"; private static final int NOTIFICATION_ID = 1337; + private static final String LOG_TAG = "TermuxService"; + /** This service is only bound from inside the same process and never uses IPC. */ class LocalBinder extends Binder { public final TermuxService service = TermuxService.this; @@ -91,12 +92,12 @@ public final class TermuxService extends Service implements SessionChangedCallba } else if (TERMUX_SERVICE.ACTION_WAKE_LOCK.equals(action)) { if (mWakeLock == null) { PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); - mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, EmulatorDebug.LOG_TAG + ":service-wakelock"); + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TermuxConstants.TERMUX_APP_NAME.toLowerCase() + ":service-wakelock"); mWakeLock.acquire(); // http://tools.android.com/tech-docs/lint-in-studio-2-3#TOC-WifiManager-Leak WifiManager wm = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); - mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, EmulatorDebug.LOG_TAG); + mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, TermuxConstants.TERMUX_APP_NAME.toLowerCase()); mWifiLock.acquire(); String packageName = getPackageName(); @@ -109,7 +110,7 @@ public final class TermuxService extends Service implements SessionChangedCallba try { startActivity(whitelist); } catch (ActivityNotFoundException e) { - Log.e(EmulatorDebug.LOG_TAG, "Failed to call ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS", e); + Logger.logStackTraceWithMessage(LOG_TAG, "Failed to call ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS", e); } } @@ -156,7 +157,7 @@ public final class TermuxService extends Service implements SessionChangedCallba startActivity(new Intent(this, TermuxActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); } } else if (action != null) { - Log.e(EmulatorDebug.LOG_TAG, "Unknown TermuxService action: '" + action + "'"); + Logger.logError(LOG_TAG, "Unknown TermuxService action: '" + action + "'"); } // If this service really do get killed, there is no point restarting it automatically - let the user do on next @@ -246,7 +247,7 @@ public final class TermuxService extends Service implements SessionChangedCallba try { TermuxInstaller.deleteFolder(termuxTmpDir.getCanonicalFile()); } catch (Exception e) { - Log.e(EmulatorDebug.LOG_TAG, "Error while removing file at " + termuxTmpDir.getAbsolutePath(), e); + Logger.logStackTraceWithMessage(LOG_TAG, "Error while removing file at " + termuxTmpDir.getAbsolutePath(), e); } termuxTmpDir.mkdirs(); @@ -367,8 +368,8 @@ public final class TermuxService extends Service implements SessionChangedCallba private void setupNotificationChannel() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return; - String channelName = "Termux"; - String channelDescription = "Notifications from Termux"; + String channelName = TermuxConstants.TERMUX_APP_NAME; + String channelDescription = "Notifications from " + TermuxConstants.TERMUX_APP_NAME; int importance = NotificationManager.IMPORTANCE_LOW; NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName,importance); @@ -376,4 +377,8 @@ public final class TermuxService extends Service implements SessionChangedCallba NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); manager.createNotificationChannel(channel); } + + public boolean wantsToStop() { + return mWantsToStop; + } } diff --git a/app/src/main/java/com/termux/app/TermuxSettingsActivity.java b/app/src/main/java/com/termux/app/TermuxSettingsActivity.java new file mode 100644 index 00000000..74604c96 --- /dev/null +++ b/app/src/main/java/com/termux/app/TermuxSettingsActivity.java @@ -0,0 +1,43 @@ +package com.termux.app; + +import android.os.Bundle; + +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.preference.PreferenceFragmentCompat; + +import com.termux.R; + +public class TermuxSettingsActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.settings_activity); + if (savedInstanceState == null) { + getSupportFragmentManager() + .beginTransaction() + .replace(R.id.settings, new RootPreferencesFragment()) + .commit(); + } + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setDisplayShowHomeEnabled(true); + } + } + + @Override + public boolean onSupportNavigateUp() { + onBackPressed(); + return true; + } + + public static class RootPreferencesFragment extends PreferenceFragmentCompat { + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + setPreferencesFromResource(R.xml.root_preferences, rootKey); + } + } + +} diff --git a/app/src/main/java/com/termux/app/settings/DebuggingPreferencesFragment.java b/app/src/main/java/com/termux/app/settings/DebuggingPreferencesFragment.java new file mode 100644 index 00000000..c976c30e --- /dev/null +++ b/app/src/main/java/com/termux/app/settings/DebuggingPreferencesFragment.java @@ -0,0 +1,122 @@ +package com.termux.app.settings; + +import android.content.Context; +import android.os.Bundle; + +import androidx.annotation.Nullable; +import androidx.preference.ListPreference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceDataStore; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceManager; + +import com.termux.R; +import com.termux.app.settings.preferences.TermuxPreferenceConstants; +import com.termux.app.settings.preferences.TermuxSharedPreferences; +import com.termux.app.utils.Logger; + +public class DebuggingPreferencesFragment extends PreferenceFragmentCompat { + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + PreferenceManager preferenceManager = getPreferenceManager(); + preferenceManager.setPreferenceDataStore(DebuggingPreferencesDataStore.getInstance(getContext())); + + setPreferencesFromResource(R.xml.debugging_preferences, rootKey); + + PreferenceCategory loggingCategory = findPreference("logging"); + + if (loggingCategory != null) { + final ListPreference logLevelListPreference = setLogLevelListPreferenceData(findPreference("log_level"), getActivity()); + loggingCategory.addPreference(logLevelListPreference); + } + } + + protected ListPreference setLogLevelListPreferenceData(ListPreference logLevelListPreference, Context context) { + if(logLevelListPreference == null) + logLevelListPreference = new ListPreference(context); + + CharSequence[] logLevels = Logger.getLogLevelsArray(); + CharSequence[] logLevelLabels = Logger.getLogLevelLabelsArray(context, logLevels, true); + + logLevelListPreference.setEntryValues(logLevels); + logLevelListPreference.setEntries(logLevelLabels); + + logLevelListPreference.setKey(TermuxPreferenceConstants.KEY_LOG_LEVEL); + logLevelListPreference.setValue(String.valueOf(Logger.getLogLevel())); + logLevelListPreference.setDefaultValue(Logger.getLogLevel()); + + return logLevelListPreference; + } + +} + +class DebuggingPreferencesDataStore extends PreferenceDataStore { + + private final Context mContext; + private final TermuxSharedPreferences mPreferences; + + private static DebuggingPreferencesDataStore mInstance; + + private DebuggingPreferencesDataStore(Context context) { + mContext = context; + mPreferences = new TermuxSharedPreferences(context); + } + + public static synchronized DebuggingPreferencesDataStore getInstance(Context context) { + if (mInstance == null) { + mInstance = new DebuggingPreferencesDataStore(context.getApplicationContext()); + } + return mInstance; + } + + @Override + @Nullable + public String getString(String key, @Nullable String defValue) { + if(key == null) return null; + + switch (key) { + case "log_level": + return String.valueOf(mPreferences.getLogLevel()); + default: + return null; + } + } + + @Override + public void putString(String key, @Nullable String value) { + if(key == null) return; + + switch (key) { + case "log_level": + if (value != null) { + mPreferences.setLogLevel(mContext, Integer.parseInt(value)); + } + break; + default: + break; + } + } + + @Override + public void putBoolean(String key, boolean value) { + if(key == null) return; + + switch (key) { + case "terminal_view_key_logging_enabled": + mPreferences.setTerminalViewKeyLoggingEnabled(value); + break; + default: + break; + } + } + + @Override + public boolean getBoolean(String key, boolean defValue) { + switch (key) { + case "terminal_view_key_logging_enabled": + return mPreferences.getTerminalViewKeyLoggingEnabled(); + default: + return false; + } + } +} diff --git a/app/src/main/java/com/termux/app/settings/preferences/TermuxPreferenceConstants.java b/app/src/main/java/com/termux/app/settings/preferences/TermuxPreferenceConstants.java index f9ba174d..a12aa954 100644 --- a/app/src/main/java/com/termux/app/settings/preferences/TermuxPreferenceConstants.java +++ b/app/src/main/java/com/termux/app/settings/preferences/TermuxPreferenceConstants.java @@ -3,13 +3,14 @@ package com.termux.app.settings.preferences; import com.termux.app.TermuxConstants; /* - * Version: v0.1.0 + * Version: v0.2.0 * * Changelog * * - 0.1.0 (2021-03-12) * - Initial Release. - * + * - 0.2.0 (2021-03-13) + * - Added `KEY_LOG_LEVEL` and `KEY_TERMINAL_VIEW_LOGGING_ENABLED` */ /** @@ -47,4 +48,13 @@ public final class TermuxPreferenceConstants { + /** Defines the key for current termux log level */ + public static final String KEY_LOG_LEVEL = "log_level"; + + + + /** Defines the key for whether termux terminal view key logging is enabled or not */ + public static final String KEY_TERMINAL_VIEW_KEY_LOGGING_ENABLED = "terminal_view_key_logging_enabled"; + public static final boolean DEFAULT_VALUE_TERMINAL_VIEW_KEY_LOGGING_ENABLED = false; + } diff --git a/app/src/main/java/com/termux/app/settings/preferences/TermuxSharedPreferences.java b/app/src/main/java/com/termux/app/settings/preferences/TermuxSharedPreferences.java index 65fe9679..1278dd3e 100644 --- a/app/src/main/java/com/termux/app/settings/preferences/TermuxSharedPreferences.java +++ b/app/src/main/java/com/termux/app/settings/preferences/TermuxSharedPreferences.java @@ -2,11 +2,11 @@ package com.termux.app.settings.preferences; import android.content.Context; import android.content.SharedPreferences; -import android.util.Log; import android.util.TypedValue; import com.termux.app.TermuxConstants; -import com.termux.terminal.EmulatorDebug; +import com.termux.app.utils.Logger; +import com.termux.app.utils.TermuxUtils; import javax.annotation.Nonnull; @@ -21,17 +21,7 @@ public class TermuxSharedPreferences { private int DEFAULT_FONTSIZE; public TermuxSharedPreferences(@Nonnull Context context) { - Context mTempContext; - - try { - mTempContext = context.createPackageContext(TermuxConstants.TERMUX_PACKAGE_NAME, Context.CONTEXT_RESTRICTED); - } catch (Exception e) { - Log.e(EmulatorDebug.LOG_TAG, "Failed to get \"" + TermuxConstants.TERMUX_PACKAGE_NAME + "\" package context", e); - Log.e(EmulatorDebug.LOG_TAG, "Force using current context"); - mTempContext = context; - } - - mContext = mTempContext; + mContext = TermuxUtils.getTermuxPackageContext(context); mSharedPreferences = getSharedPreferences(mContext); setFontVariables(context); @@ -135,4 +125,31 @@ public class TermuxSharedPreferences { mSharedPreferences.edit().putString(TermuxPreferenceConstants.KEY_CURRENT_SESSION, value).apply(); } + + + public int getLogLevel() { + try { + return mSharedPreferences.getInt(TermuxPreferenceConstants.KEY_LOG_LEVEL, Logger.DEFAULT_LOG_LEVEL); + } + catch (Exception e) { + Logger.logStackTraceWithMessage("Error getting \"" + TermuxPreferenceConstants.KEY_LOG_LEVEL + "\" from shared preferences", e); + return Logger.DEFAULT_LOG_LEVEL; + } + } + + public void setLogLevel(Context context, int logLevel) { + logLevel = Logger.setLogLevel(context, logLevel); + mSharedPreferences.edit().putInt(TermuxPreferenceConstants.KEY_LOG_LEVEL, logLevel).apply(); + } + + + + public boolean getTerminalViewKeyLoggingEnabled() { + return mSharedPreferences.getBoolean(TermuxPreferenceConstants.KEY_TERMINAL_VIEW_KEY_LOGGING_ENABLED, TermuxPreferenceConstants.DEFAULT_VALUE_TERMINAL_VIEW_KEY_LOGGING_ENABLED); + } + + public void setTerminalViewKeyLoggingEnabled(boolean value) { + mSharedPreferences.edit().putBoolean(TermuxPreferenceConstants.KEY_TERMINAL_VIEW_KEY_LOGGING_ENABLED, value).apply(); + } + } diff --git a/app/src/main/java/com/termux/app/settings/properties/SharedProperties.java b/app/src/main/java/com/termux/app/settings/properties/SharedProperties.java index db6eb637..bf47fa00 100644 --- a/app/src/main/java/com/termux/app/settings/properties/SharedProperties.java +++ b/app/src/main/java/com/termux/app/settings/properties/SharedProperties.java @@ -1,10 +1,10 @@ package com.termux.app.settings.properties; import android.content.Context; -import android.util.Log; import android.widget.Toast; import com.google.common.primitives.Primitives; +import com.termux.app.utils.Logger; import java.io.File; import java.io.FileInputStream; @@ -57,6 +57,8 @@ public class SharedProperties { private final Object mLock = new Object(); + private static final String LOG_TAG = "SharedProperties"; + /** * Constructor for the SharedProperties class. * @@ -97,7 +99,7 @@ public class SharedProperties { Object internalValue; for (String key : mPropertiesList) { value = properties.getProperty(key); // value will be null if key does not exist in propertiesFile - Log.d("termux", key + " : " + value); + Logger.logDebug(LOG_TAG, key + " : " + value); // Call the {@link SharedPropertiesParser#getInternalPropertyValueFromValue(Context,String,String)} // interface method to get the internal value to store in the {@link #mMap}. @@ -129,13 +131,13 @@ public class SharedProperties { public static boolean putToMap(HashMap map, String key, Object value) { if (map == null) { - Log.e("termux", "Map passed to SharedProperties.putToProperties() is null"); + Logger.logError(LOG_TAG, "Map passed to SharedProperties.putToProperties() is null"); return false; } // null keys are not allowed to be stored in mMap if (key == null) { - Log.e("termux", "Cannot put a null key into properties map"); + Logger.logError(LOG_TAG, "Cannot put a null key into properties map"); return false; } @@ -153,7 +155,7 @@ public class SharedProperties { map.put(key, value); return true; } else { - Log.e("termux", "Cannot put a non-primitive value for the key \"" + key + "\" into properties map"); + Logger.logError(LOG_TAG, "Cannot put a non-primitive value for the key \"" + key + "\" into properties map"); return false; } } @@ -172,13 +174,13 @@ public class SharedProperties { public static boolean putToProperties(Properties properties, String key, String value) { if (properties == null) { - Log.e("termux", "Properties passed to SharedProperties.putToProperties() is null"); + Logger.logError(LOG_TAG, "Properties passed to SharedProperties.putToProperties() is null"); return false; } // null keys are not allowed to be stored in mMap if (key == null) { - Log.e("termux", "Cannot put a null key into properties"); + Logger.logError(LOG_TAG, "Cannot put a null key into properties"); return false; } @@ -206,19 +208,19 @@ public class SharedProperties { Properties properties = new Properties(); if (propertiesFile == null) { - Log.e("termux", "Not loading properties since file is null"); + Logger.logError(LOG_TAG, "Not loading properties since file is null"); return properties; } try { try (FileInputStream in = new FileInputStream(propertiesFile)) { - Log.v("termux", "Loading properties from \"" + propertiesFile.getAbsolutePath() + "\" file"); + Logger.logVerbose(LOG_TAG, "Loading properties from \"" + propertiesFile.getAbsolutePath() + "\" file"); properties.load(new InputStreamReader(in, StandardCharsets.UTF_8)); } } catch (Exception e) { if(context != null) Toast.makeText(context, "Could not open properties file \"" + propertiesFile.getAbsolutePath() + "\": " + e.getMessage(), Toast.LENGTH_LONG).show(); - Log.e("termux", "Error loading properties file \"" + propertiesFile.getAbsolutePath() + "\"", e); + Logger.logStackTraceWithMessage(LOG_TAG, "Error loading properties file \"" + propertiesFile.getAbsolutePath() + "\"", e); return null; } diff --git a/app/src/main/java/com/termux/app/settings/properties/TermuxPropertyConstants.java b/app/src/main/java/com/termux/app/settings/properties/TermuxPropertyConstants.java index a68e0466..6e3dd673 100644 --- a/app/src/main/java/com/termux/app/settings/properties/TermuxPropertyConstants.java +++ b/app/src/main/java/com/termux/app/settings/properties/TermuxPropertyConstants.java @@ -1,9 +1,8 @@ package com.termux.app.settings.properties; -import android.util.Log; - import com.google.common.collect.ImmutableBiMap; import com.termux.app.TermuxConstants; +import com.termux.app.utils.Logger; import java.io.File; import java.util.Arrays; @@ -237,7 +236,7 @@ public final class TermuxPropertyConstants { if (propertiesFile.isFile() && propertiesFile.canRead()) { return propertiesFile; } else { - Log.d("termux", "No readable termux.properties file found"); + Logger.logDebug("No readable termux.properties file found"); return null; } } diff --git a/app/src/main/java/com/termux/app/settings/properties/TermuxSharedProperties.java b/app/src/main/java/com/termux/app/settings/properties/TermuxSharedProperties.java index 70a3dc3e..8c72793f 100644 --- a/app/src/main/java/com/termux/app/settings/properties/TermuxSharedProperties.java +++ b/app/src/main/java/com/termux/app/settings/properties/TermuxSharedProperties.java @@ -2,13 +2,12 @@ package com.termux.app.settings.properties; import android.content.Context; import android.content.res.Configuration; -import android.util.Log; -import android.widget.Toast; import androidx.annotation.Nullable; import com.termux.app.terminal.extrakeys.ExtraKeysInfo; import com.termux.app.terminal.KeyboardShortcut; +import com.termux.app.utils.Logger; import org.json.JSONException; @@ -30,6 +29,8 @@ public class TermuxSharedProperties implements SharedPropertiesParser { private ExtraKeysInfo mExtraKeysInfo; private final List mSessionShortcuts = new ArrayList<>(); + private static final String LOG_TAG = "TermuxSharedProperties"; + public TermuxSharedProperties(@Nonnull Context context) { mContext = context; mPropertiesFile = TermuxPropertyConstants.getTermuxPropertiesFile(); @@ -65,14 +66,14 @@ public class TermuxSharedProperties implements SharedPropertiesParser { String extraKeysStyle = (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE, true); mExtraKeysInfo = new ExtraKeysInfo(extrakeys, extraKeysStyle); } catch (JSONException e) { - Toast.makeText(mContext, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: " + e.toString(), Toast.LENGTH_LONG).show(); - Log.e("termux", "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: ", e); + Logger.showToast(mContext, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: " + e.toString(), true); + Logger.logStackTraceWithMessage(LOG_TAG, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: ", e); try { mExtraKeysInfo = new ExtraKeysInfo(TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE); } catch (JSONException e2) { - Toast.makeText(mContext, "Can't create default extra keys", Toast.LENGTH_LONG).show(); - Log.e("termux", "Could create default extra keys: ", e); + Logger.showToast(mContext, "Can't create default extra keys",true); + Logger.logStackTraceWithMessage(LOG_TAG, "Could create default extra keys: ", e); mExtraKeysInfo = null; } } @@ -262,7 +263,7 @@ public class TermuxSharedProperties implements SharedPropertiesParser { // A null value can still be returned by // {@link #getInternalPropertyValueFromValue(Context,String,String)} for some keys value = getInternalPropertyValueFromValue(mContext, key, null); - Log.w("termux", "The value for \"" + key + "\" not found in SharedProperties cahce, force returning default value: `" + value + "`"); + Logger.logWarn(LOG_TAG, "The value for \"" + key + "\" not found in SharedProperties cahce, force returning default value: `" + value + "`"); return value; } } else { @@ -423,7 +424,7 @@ public class TermuxSharedProperties implements SharedPropertiesParser { String[] parts = value.toLowerCase().trim().split("\\+"); String input = parts.length == 2 ? parts[1].trim() : null; if (!(parts.length == 2 && parts[0].trim().equals("ctrl")) || input.isEmpty() || input.length() > 2) { - Log.e("termux", "Keyboard shortcut '" + key + "' is not Ctrl+"); + Logger.logError(LOG_TAG, "Keyboard shortcut '" + key + "' is not Ctrl+"); return null; } @@ -431,7 +432,7 @@ public class TermuxSharedProperties implements SharedPropertiesParser { int codePoint = c; if (Character.isLowSurrogate(c)) { if (input.length() != 2 || Character.isHighSurrogate(input.charAt(1))) { - Log.e("termux", "Keyboard shortcut '" + key + "' is not Ctrl+"); + Logger.logError(LOG_TAG, "Keyboard shortcut '" + key + "' is not Ctrl+"); return null; } else { codePoint = Character.toCodePoint(input.charAt(1), c); @@ -556,7 +557,7 @@ public class TermuxSharedProperties implements SharedPropertiesParser { propertiesDump.append(" null"); } - Log.d("termux", propertiesDump.toString()); + Logger.logDebug(LOG_TAG, propertiesDump.toString()); } public void dumpInternalPropertiesToLog() { @@ -570,7 +571,7 @@ public class TermuxSharedProperties implements SharedPropertiesParser { } } - Log.d("termux", internalPropertiesDump.toString()); + Logger.logDebug(LOG_TAG, internalPropertiesDump.toString()); } } diff --git a/app/src/main/java/com/termux/app/terminal/TermuxViewClient.java b/app/src/main/java/com/termux/app/terminal/TermuxViewClient.java index 567ebef8..8d49b6df 100644 --- a/app/src/main/java/com/termux/app/terminal/TermuxViewClient.java +++ b/app/src/main/java/com/termux/app/terminal/TermuxViewClient.java @@ -13,6 +13,7 @@ import com.termux.app.TermuxActivity; import com.termux.app.TermuxService; import com.termux.app.terminal.extrakeys.ExtraKeysView; import com.termux.app.settings.properties.TermuxPropertyConstants; +import com.termux.app.utils.Logger; import com.termux.terminal.KeyHandler; import com.termux.terminal.TerminalEmulator; import com.termux.terminal.TerminalSession; @@ -20,6 +21,7 @@ import com.termux.view.TerminalViewClient; import java.util.List; + import androidx.drawerlayout.widget.DrawerLayout; public final class TermuxViewClient implements TerminalViewClient { @@ -43,6 +45,8 @@ public final class TermuxViewClient implements TerminalViewClient { return scale; } + + @Override public void onSingleTapUp(MotionEvent e) { InputMethodManager mgr = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE); @@ -64,12 +68,16 @@ public final class TermuxViewClient implements TerminalViewClient { return mActivity.getProperties().isUsingCtrlSpaceWorkaround(); } + + @Override public void copyModeChanged(boolean copyMode) { // Disable drawer while copying. mActivity.getDrawer().setDrawerLockMode(copyMode ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED : DrawerLayout.LOCK_MODE_UNLOCKED); } + + @SuppressLint("RtlHardcoded") @Override public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession currentSession) { @@ -127,6 +135,26 @@ public final class TermuxViewClient implements TerminalViewClient { return handleVirtualKeys(keyCode, e, false); } + /** Handle dedicated volume buttons as virtual keys if applicable. */ + private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) { + InputDevice inputDevice = event.getDevice(); + if (mActivity.getProperties().areVirtualVolumeKeysDisabled()) { + return false; + } else if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) { + // Do not steal dedicated buttons from a full external keyboard. + return false; + } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { + mVirtualControlKeyDown = down; + return true; + } else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) { + mVirtualFnKeyDown = down; + return true; + } + return false; + } + + + @Override public boolean readControlKey() { return (mActivity.getExtraKeysView() != null && mActivity.getExtraKeysView().readSpecialButton(ExtraKeysView.SpecialButton.CTRL)) || mVirtualControlKeyDown; @@ -137,6 +165,13 @@ public final class TermuxViewClient implements TerminalViewClient { return (mActivity.getExtraKeysView() != null && mActivity.getExtraKeysView().readSpecialButton(ExtraKeysView.SpecialButton.ALT)); } + @Override + public boolean onLongPress(MotionEvent event) { + return false; + } + + + @Override public boolean onCodePoint(final int codePoint, boolean ctrlDown, TerminalSession session) { if (mVirtualFnKeyDown) { @@ -273,27 +308,41 @@ public final class TermuxViewClient implements TerminalViewClient { return false; } + + @Override - public boolean onLongPress(MotionEvent event) { - return false; + public void logError(String tag, String message) { + Logger.logError(tag, message); } - /** Handle dedicated volume buttons as virtual keys if applicable. */ - private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) { - InputDevice inputDevice = event.getDevice(); - if (mActivity.getProperties().areVirtualVolumeKeysDisabled()) { - return false; - } else if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) { - // Do not steal dedicated buttons from a full external keyboard. - return false; - } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { - mVirtualControlKeyDown = down; - return true; - } else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) { - mVirtualFnKeyDown = down; - return true; - } - return false; + @Override + public void logWarn(String tag, String message) { + Logger.logWarn(tag, message); + } + + @Override + public void logInfo(String tag, String message) { + Logger.logInfo(tag, message); + } + + @Override + public void logDebug(String tag, String message) { + Logger.logDebug(tag, message); + } + + @Override + public void logVerbose(String tag, String message) { + Logger.logVerbose(tag, message); + } + + @Override + public void logStackTraceWithMessage(String tag, String message, Exception e) { + Logger.logStackTraceWithMessage(tag, message, e); + } + + @Override + public void logStackTrace(String tag, Exception e) { + Logger.logStackTrace(tag, e); } } diff --git a/app/src/main/java/com/termux/app/utils/Logger.java b/app/src/main/java/com/termux/app/utils/Logger.java new file mode 100644 index 00000000..aace3c25 --- /dev/null +++ b/app/src/main/java/com/termux/app/utils/Logger.java @@ -0,0 +1,223 @@ +package com.termux.app.utils; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.widget.Toast; + +import androidx.appcompat.app.AlertDialog; + +import com.termux.R; +import com.termux.app.TermuxConstants; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; + +public class Logger { + + public static final String DEFAULT_LOG_TAG = TermuxConstants.TERMUX_APP_NAME; + + public static final int LOG_LEVEL_OFF = 0; // log nothing + public static final int LOG_LEVEL_NORMAL = 1; // start logging error, warn and info messages and stacktraces + public static final int LOG_LEVEL_DEBUG = 2; // start logging debug messages + public static final int LOG_LEVEL_VERBOSE = 3; // start logging verbose messages + + public static final int DEFAULT_LOG_LEVEL = LOG_LEVEL_NORMAL; + + private static int CURRENT_LOG_LEVEL = DEFAULT_LOG_LEVEL; + + static public void logMesssage(int logLevel, String tag, String message) { + if(logLevel == Log.ERROR && CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL) + Log.e(tag, message); + else if(logLevel == Log.WARN && CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL) + Log.w(tag, message); + else if(logLevel == Log.INFO && CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL) + Log.i(tag, message); + else if(logLevel == Log.DEBUG && CURRENT_LOG_LEVEL >= LOG_LEVEL_DEBUG) + Log.d(tag, message); + else if(logLevel == Log.VERBOSE && CURRENT_LOG_LEVEL >= LOG_LEVEL_VERBOSE) + Log.v(tag, message); + } + + + + static public void logError(String tag, String message) { + logMesssage(Log.ERROR, tag, message); + } + + static public void logError(String message) { + logMesssage(Log.ERROR, DEFAULT_LOG_TAG, message); + } + + + + static public void logWarn(String tag, String message) { + logMesssage(Log.WARN, tag, message); + } + + static public void logWarn(String message) { + logMesssage(Log.WARN, DEFAULT_LOG_TAG, message); + } + + + + static public void logInfo(String tag, String message) { + logMesssage(Log.INFO, tag, message); + } + + static public void logInfo(String message) { + logMesssage(Log.INFO, DEFAULT_LOG_TAG, message); + } + + + + static public void logDebug(String tag, String message) { + logMesssage(Log.DEBUG, tag, message); + } + + static public void logDebug(String message) { + logMesssage(Log.DEBUG, DEFAULT_LOG_TAG, message); + } + + + + static public void logVerbose(String tag, String message) { + logMesssage(Log.VERBOSE, tag, message); + } + + static public void logVerbose(String message) { + logMesssage(Log.VERBOSE, DEFAULT_LOG_TAG, message); + } + + + + static public void logErrorAndShowToast(Context context, String tag, String message) { + if (context == null) return; + + if(CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL) { + logError(tag, message); + showToast(context, message, true); + } + } + + static public void logErrorAndShowToast(Context context, String message) { + logErrorAndShowToast(context, DEFAULT_LOG_TAG, message); + } + + + + static public void logDebugAndShowToast(Context context, String tag, String message) { + if (context == null) return; + + if(CURRENT_LOG_LEVEL >= LOG_LEVEL_DEBUG) { + logDebug(tag, message); + showToast(context, message, true); + } + } + + static public void logDebugAndShowToast(Context context, String message) { + logDebugAndShowToast(context, DEFAULT_LOG_TAG, message); + } + + + + static public void logStackTraceWithMessage(String tag, String message, Exception e) { + + if(CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL) + { + try { + StringWriter errors = new StringWriter(); + PrintWriter pw = new PrintWriter(errors); + e.printStackTrace(pw); + pw.close(); + if(message != null) + Log.e(tag, message + ":\n" + errors.toString()); + else + Log.e(tag, errors.toString()); + errors.close(); + } catch (IOException e1) { + e1.printStackTrace(); + } + } + } + + static public void logStackTraceWithMessage(String message, Exception e) { + logStackTraceWithMessage(DEFAULT_LOG_TAG, message, e); + } + + static public void logStackTrace(String tag, Exception e) { + logStackTraceWithMessage(tag, null, e); + } + + static public void logStackTrace(Exception e) { + logStackTraceWithMessage(DEFAULT_LOG_TAG, null, e); + } + + + + static public void showToast(final Context context, final String toastText, boolean longDuration) { + if (context == null) return; + + new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(context, toastText, longDuration ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show()); + } + + + + public static CharSequence[] getLogLevelsArray() { + return new CharSequence[]{ + String.valueOf(LOG_LEVEL_OFF), + String.valueOf(LOG_LEVEL_NORMAL), + String.valueOf(LOG_LEVEL_DEBUG), + String.valueOf(LOG_LEVEL_VERBOSE) + }; + } + + public static CharSequence[] getLogLevelLabelsArray(Context context, CharSequence[] logLevels, boolean addDefaultTag) { + if (logLevels == null) return null; + + CharSequence[] logLevelLabels = new CharSequence[logLevels.length]; + + for(int i=0; i= LOG_LEVEL_OFF && logLevel <= LOG_LEVEL_VERBOSE) + CURRENT_LOG_LEVEL = logLevel; + else + CURRENT_LOG_LEVEL = DEFAULT_LOG_LEVEL; + + if(context != null) + showToast(context, context.getString(R.string.log_level_value, getLogLevelLabel(context, CURRENT_LOG_LEVEL, false)),true); + + return CURRENT_LOG_LEVEL; + } + +} diff --git a/app/src/main/java/com/termux/app/utils/TermuxUtils.java b/app/src/main/java/com/termux/app/utils/TermuxUtils.java new file mode 100644 index 00000000..712b5b3a --- /dev/null +++ b/app/src/main/java/com/termux/app/utils/TermuxUtils.java @@ -0,0 +1,18 @@ +package com.termux.app.utils; + +import android.content.Context; + +import com.termux.app.TermuxConstants; + +public class TermuxUtils { + + public static Context getTermuxPackageContext(Context context) { + try { + return context.createPackageContext(TermuxConstants.TERMUX_PACKAGE_NAME, Context.CONTEXT_RESTRICTED); + } catch (Exception e) { + Logger.logStackTraceWithMessage("Failed to get \"" + TermuxConstants.TERMUX_PACKAGE_NAME + "\" package context. Force using current context.", e); + Logger.logError("Force using current context"); + return context; + } + } +} diff --git a/app/src/main/java/com/termux/filepicker/TermuxFileReceiverActivity.java b/app/src/main/java/com/termux/filepicker/TermuxFileReceiverActivity.java index 2367bdf0..f0f53dcb 100644 --- a/app/src/main/java/com/termux/filepicker/TermuxFileReceiverActivity.java +++ b/app/src/main/java/com/termux/filepicker/TermuxFileReceiverActivity.java @@ -6,7 +6,6 @@ import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.provider.OpenableColumns; -import android.util.Log; import android.util.Patterns; import com.termux.R; @@ -14,6 +13,7 @@ import com.termux.app.DialogUtils; import com.termux.app.TermuxConstants; import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_SERVICE; import com.termux.app.TermuxService; +import com.termux.app.utils.Logger; import java.io.ByteArrayInputStream; import java.io.File; @@ -39,6 +39,8 @@ public class TermuxFileReceiverActivity extends Activity { */ boolean mFinishOnDismissNameDialog = true; + private static final String LOG_TAG = "TermuxFileReceiverActivity"; + static boolean isSharedTextAnUrl(String sharedText) { return Patterns.WEB_URL.matcher(sharedText).matches() || Pattern.matches("magnet:\\?xt=urn:btih:.*?", sharedText); @@ -111,7 +113,7 @@ public class TermuxFileReceiverActivity extends Activity { promptNameAndSave(in, attachmentFileName); } catch (Exception e) { showErrorDialogAndQuit("Unable to handle shared content:\n\n" + e.getMessage()); - Log.e("termux", "handleContentUri(uri=" + uri + ") failed", e); + Logger.logStackTraceWithMessage(LOG_TAG, "handleContentUri(uri=" + uri + ") failed", e); } } @@ -171,7 +173,7 @@ public class TermuxFileReceiverActivity extends Activity { return outFile; } catch (IOException e) { showErrorDialogAndQuit("Error saving file:\n\n" + e); - Log.e("termux", "Error saving file", e); + Logger.logStackTraceWithMessage(LOG_TAG, "Error saving file", e); return null; } } diff --git a/app/src/main/res/layout/settings_activity.xml b/app/src/main/res/layout/settings_activity.xml new file mode 100644 index 00000000..7dc1a6c5 --- /dev/null +++ b/app/src/main/res/layout/settings_activity.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a34c5d11..968ac645 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -63,4 +63,35 @@ Save file in ~/downloads/ Edit Open folder + + + + + + + Settings + Termux Settings + + + + + Debugging + + + Logging + + + Log Level + "Off" + "Normal" + "Debug" + "Verbose" + "*Unknown*" + Logcat log level set to \"%1$s\" + + + Terminal View Key Logging + Logs will not have entries for terminal view keys. (Default) + Logcat logs will have entries for terminal view keys. These are very verbose and should be disabled under normal circumstances or will cause performance issues. + diff --git a/app/src/main/res/xml/debugging_preferences.xml b/app/src/main/res/xml/debugging_preferences.xml new file mode 100644 index 00000000..ba1f328d --- /dev/null +++ b/app/src/main/res/xml/debugging_preferences.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml new file mode 100644 index 00000000..3ea2b583 --- /dev/null +++ b/app/src/main/res/xml/root_preferences.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/terminal-view/src/main/java/com/termux/view/TerminalView.java b/terminal-view/src/main/java/com/termux/view/TerminalView.java index b9d18e69..995f8b9a 100644 --- a/terminal-view/src/main/java/com/termux/view/TerminalView.java +++ b/terminal-view/src/main/java/com/termux/view/TerminalView.java @@ -12,7 +12,6 @@ import android.text.Editable; import android.text.InputType; import android.text.TextUtils; import android.util.AttributeSet; -import android.util.Log; import android.view.ActionMode; import android.view.HapticFeedbackConstants; import android.view.InputDevice; @@ -31,7 +30,6 @@ import android.widget.Scroller; import androidx.annotation.RequiresApi; -import com.termux.terminal.EmulatorDebug; import com.termux.terminal.KeyHandler; import com.termux.terminal.TerminalEmulator; import com.termux.terminal.TerminalSession; @@ -40,8 +38,8 @@ import com.termux.view.textselection.TextSelectionCursorController; /** View displaying and interacting with a {@link TerminalSession}. */ public final class TerminalView extends View { - /** Log view key and IME events. */ - private static final boolean LOG_KEY_EVENTS = false; + /** Log terminal view key and IME events. */ + private static boolean TERMINAL_VIEW_KEY_LOGGING_ENABLED = false; /** The currently displayed terminal session, whose emulator is {@link #mEmulator}. */ public TerminalSession mTermSession; @@ -76,6 +74,8 @@ public final class TerminalView extends View { private final boolean mAccessibilityEnabled; + private static final String LOG_TAG = "TerminalView"; + public TerminalView(Context context, AttributeSet attributes) { // NO_UCD (unused code) super(context, attributes); mGestureRecognizer = new GestureAndScaleRecognizer(context, new GestureAndScaleRecognizer.Listener() { @@ -210,11 +210,23 @@ public final class TerminalView extends View { } /** - * @param onKeyListener Listener for all kinds of key events, both hardware and IME (which makes it different from that - * available with {@link View#setOnKeyListener(OnKeyListener)}. + * @param terminalViewClient Interface for communicating with the terminal view client. It allows + * for getting various configuration options from the client and + * for sending back data to the client like logs, key events, both + * hardware and IME (which makes it different from that available with + * {@link View#setOnKeyListener(OnKeyListener)}, etc. */ - public void setOnKeyListener(TerminalViewClient onKeyListener) { - this.mClient = onKeyListener; + public void setTerminalViewClient(TerminalViewClient terminalViewClient) { + this.mClient = terminalViewClient; + } + + /** + * Sets terminal view key logging is enabled or not. + * + * @param value The boolean value that defines the state. + */ + public void setIsTerminalViewKeyLoggingEnabled(boolean value) { + TERMINAL_VIEW_KEY_LOGGING_ENABLED = value; } /** @@ -264,7 +276,7 @@ public final class TerminalView extends View { @Override public boolean finishComposingText() { - if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "IME: finishComposingText()"); + if (TERMINAL_VIEW_KEY_LOGGING_ENABLED) mClient.logInfo(LOG_TAG, "IME: finishComposingText()"); super.finishComposingText(); sendTextToTerminal(getEditable()); @@ -274,8 +286,8 @@ public final class TerminalView extends View { @Override public boolean commitText(CharSequence text, int newCursorPosition) { - if (LOG_KEY_EVENTS) { - Log.i(EmulatorDebug.LOG_TAG, "IME: commitText(\"" + text + "\", " + newCursorPosition + ")"); + if (TERMINAL_VIEW_KEY_LOGGING_ENABLED) { + mClient.logInfo(LOG_TAG, "IME: commitText(\"" + text + "\", " + newCursorPosition + ")"); } super.commitText(text, newCursorPosition); @@ -289,8 +301,8 @@ public final class TerminalView extends View { @Override public boolean deleteSurroundingText(int leftLength, int rightLength) { - if (LOG_KEY_EVENTS) { - Log.i(EmulatorDebug.LOG_TAG, "IME: deleteSurroundingText(" + leftLength + ", " + rightLength + ")"); + if (TERMINAL_VIEW_KEY_LOGGING_ENABLED) { + mClient.logInfo(LOG_TAG, "IME: deleteSurroundingText(" + leftLength + ", " + rightLength + ")"); } // The stock Samsung keyboard with 'Auto check spelling' enabled sends leftLength > 1. KeyEvent deleteKey = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL); @@ -521,8 +533,8 @@ public final class TerminalView extends View { @Override public boolean onKeyPreIme(int keyCode, KeyEvent event) { - if (LOG_KEY_EVENTS) - Log.i(EmulatorDebug.LOG_TAG, "onKeyPreIme(keyCode=" + keyCode + ", event=" + event + ")"); + if (TERMINAL_VIEW_KEY_LOGGING_ENABLED) + mClient.logInfo(LOG_TAG, "onKeyPreIme(keyCode=" + keyCode + ", event=" + event + ")"); if (keyCode == KeyEvent.KEYCODE_BACK) { if (isSelectingText()) { stopTextSelectionMode(); @@ -547,8 +559,8 @@ public final class TerminalView extends View { @Override public boolean onKeyDown(int keyCode, KeyEvent event) { - if (LOG_KEY_EVENTS) - Log.i(EmulatorDebug.LOG_TAG, "onKeyDown(keyCode=" + keyCode + ", isSystem()=" + event.isSystem() + ", event=" + event + ")"); + if (TERMINAL_VIEW_KEY_LOGGING_ENABLED) + mClient.logInfo(LOG_TAG, "onKeyDown(keyCode=" + keyCode + ", isSystem()=" + event.isSystem() + ", event=" + event + ")"); if (mEmulator == null) return true; if (isSelectingText()) { stopTextSelectionMode(); @@ -575,7 +587,7 @@ public final class TerminalView extends View { if (event.isShiftPressed()) keyMod |= KeyHandler.KEYMOD_SHIFT; if (event.isNumLockOn()) keyMod |= KeyHandler.KEYMOD_NUM_LOCK; if (!event.isFunctionPressed() && handleKeyCode(keyCode, keyMod)) { - if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "handleKeyCode() took key event"); + if (TERMINAL_VIEW_KEY_LOGGING_ENABLED) mClient.logInfo(LOG_TAG, "handleKeyCode() took key event"); return true; } @@ -590,8 +602,8 @@ public final class TerminalView extends View { int effectiveMetaState = event.getMetaState() & ~bitsToClear; int result = event.getUnicodeChar(effectiveMetaState); - if (LOG_KEY_EVENTS) - Log.i(EmulatorDebug.LOG_TAG, "KeyEvent#getUnicodeChar(" + effectiveMetaState + ") returned: " + result); + if (TERMINAL_VIEW_KEY_LOGGING_ENABLED) + mClient.logInfo(LOG_TAG, "KeyEvent#getUnicodeChar(" + effectiveMetaState + ") returned: " + result); if (result == 0) { return false; } @@ -617,8 +629,8 @@ public final class TerminalView extends View { } public void inputCodePoint(int codePoint, boolean controlDownFromEvent, boolean leftAltDownFromEvent) { - if (LOG_KEY_EVENTS) { - Log.i(EmulatorDebug.LOG_TAG, "inputCodePoint(codePoint=" + codePoint + ", controlDownFromEvent=" + controlDownFromEvent + ", leftAltDownFromEvent=" + if (TERMINAL_VIEW_KEY_LOGGING_ENABLED) { + mClient.logInfo(LOG_TAG, "inputCodePoint(codePoint=" + codePoint + ", controlDownFromEvent=" + controlDownFromEvent + ", leftAltDownFromEvent=" + leftAltDownFromEvent + ")"); } @@ -692,8 +704,8 @@ public final class TerminalView extends View { */ @Override public boolean onKeyUp(int keyCode, KeyEvent event) { - if (LOG_KEY_EVENTS) - Log.i(EmulatorDebug.LOG_TAG, "onKeyUp(keyCode=" + keyCode + ", event=" + event + ")"); + if (TERMINAL_VIEW_KEY_LOGGING_ENABLED) + mClient.logInfo(LOG_TAG, "onKeyUp(keyCode=" + keyCode + ", event=" + event + ")"); if (mEmulator == null) return true; if (mClient.onKeyUp(keyCode, event)) { diff --git a/terminal-view/src/main/java/com/termux/view/TerminalViewClient.java b/terminal-view/src/main/java/com/termux/view/TerminalViewClient.java index 390e9157..25be7d9d 100644 --- a/terminal-view/src/main/java/com/termux/view/TerminalViewClient.java +++ b/terminal-view/src/main/java/com/termux/view/TerminalViewClient.java @@ -8,7 +8,7 @@ import com.termux.terminal.TerminalSession; /** * Input and scale listener which may be set on a {@link TerminalView} through - * {@link TerminalView#setOnKeyListener(TerminalViewClient)}. + * {@link TerminalView#setTerminalViewClient(TerminalViewClient)}. *

*/ public interface TerminalViewClient { @@ -18,6 +18,8 @@ public interface TerminalViewClient { */ float onScale(float scale); + + /** * On a single tap on the terminal if terminal mouse reporting not enabled. */ @@ -29,18 +31,41 @@ public interface TerminalViewClient { boolean shouldUseCtrlSpaceWorkaround(); + + void copyModeChanged(boolean copyMode); + + boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession session); boolean onKeyUp(int keyCode, KeyEvent e); + boolean onLongPress(MotionEvent event); + + + boolean readControlKey(); boolean readAltKey(); + boolean onCodePoint(int codePoint, boolean ctrlDown, TerminalSession session); - boolean onLongPress(MotionEvent event); + + + void logError(String tag, String message); + + void logWarn(String tag, String message); + + void logInfo(String tag, String message); + + void logDebug(String tag, String message); + + void logVerbose(String tag, String message); + + void logStackTraceWithMessage(String tag, String message, Exception e); + + void logStackTrace(String tag, Exception e); } From 0225a8b1fc9ad6f516adadba6b6178dbd7b22a25 Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Tue, 16 Mar 2021 03:41:17 +0500 Subject: [PATCH 025/136] Fix hardcoded value in strings.xml --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 968ac645..97b65a37 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -70,7 +70,7 @@ Settings - Termux Settings + &TERMUX_APP_NAME; Settings From 66f15d2a0815ae216d4343fc7c9fbc8e35d9d853 Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Tue, 16 Mar 2021 03:44:27 +0500 Subject: [PATCH 026/136] Remove moved files --- .../main/java/com/termux/app/DialogUtils.java | 71 ---- .../com/termux/app/terminal/BellHandler.java | 63 --- .../app/terminal/FullScreenWorkAround.java | 68 ---- .../termux/app/terminal/KeyboardShortcut.java | 13 - .../terminal/extrakeys/ExtraKeyButton.java | 92 ----- .../app/terminal/extrakeys/ExtraKeysInfo.java | 253 ------------ .../app/terminal/extrakeys/ExtraKeysView.java | 382 ------------------ app/src/main/res/layout/drawer_layout.xml | 80 ---- app/src/main/res/layout/extra_keys_main.xml | 8 - app/src/main/res/layout/extra_keys_right.xml | 16 - app/src/main/res/layout/line_in_drawer.xml | 9 - .../com/termux/terminal/EmulatorDebug.java | 10 - 12 files changed, 1065 deletions(-) delete mode 100644 app/src/main/java/com/termux/app/DialogUtils.java delete mode 100644 app/src/main/java/com/termux/app/terminal/BellHandler.java delete mode 100644 app/src/main/java/com/termux/app/terminal/FullScreenWorkAround.java delete mode 100644 app/src/main/java/com/termux/app/terminal/KeyboardShortcut.java delete mode 100644 app/src/main/java/com/termux/app/terminal/extrakeys/ExtraKeyButton.java delete mode 100644 app/src/main/java/com/termux/app/terminal/extrakeys/ExtraKeysInfo.java delete mode 100644 app/src/main/java/com/termux/app/terminal/extrakeys/ExtraKeysView.java delete mode 100644 app/src/main/res/layout/drawer_layout.xml delete mode 100644 app/src/main/res/layout/extra_keys_main.xml delete mode 100644 app/src/main/res/layout/extra_keys_right.xml delete mode 100644 app/src/main/res/layout/line_in_drawer.xml delete mode 100644 terminal-emulator/src/main/java/com/termux/terminal/EmulatorDebug.java diff --git a/app/src/main/java/com/termux/app/DialogUtils.java b/app/src/main/java/com/termux/app/DialogUtils.java deleted file mode 100644 index 4900f75d..00000000 --- a/app/src/main/java/com/termux/app/DialogUtils.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.termux.app; - -import android.app.Activity; -import android.app.AlertDialog; -import android.content.DialogInterface; -import android.text.Selection; -import android.util.TypedValue; -import android.view.KeyEvent; -import android.view.ViewGroup.LayoutParams; -import android.widget.EditText; -import android.widget.LinearLayout; - -public final class DialogUtils { - - public interface TextSetListener { - void onTextSet(String text); - } - - public static void textInput(Activity activity, int titleText, String initialText, - int positiveButtonText, final TextSetListener onPositive, - int neutralButtonText, final TextSetListener onNeutral, - int negativeButtonText, final TextSetListener onNegative, - final DialogInterface.OnDismissListener onDismiss) { - final EditText input = new EditText(activity); - input.setSingleLine(); - if (initialText != null) { - input.setText(initialText); - Selection.setSelection(input.getText(), initialText.length()); - } - - final AlertDialog[] dialogHolder = new AlertDialog[1]; - input.setImeActionLabel(activity.getResources().getString(positiveButtonText), KeyEvent.KEYCODE_ENTER); - input.setOnEditorActionListener((v, actionId, event) -> { - onPositive.onTextSet(input.getText().toString()); - dialogHolder[0].dismiss(); - return true; - }); - - float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, activity.getResources().getDisplayMetrics()); - // https://www.google.com/design/spec/components/dialogs.html#dialogs-specs - int paddingTopAndSides = Math.round(16 * dipInPixels); - int paddingBottom = Math.round(24 * dipInPixels); - - LinearLayout layout = new LinearLayout(activity); - layout.setOrientation(LinearLayout.VERTICAL); - layout.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); - layout.setPadding(paddingTopAndSides, paddingTopAndSides, paddingTopAndSides, paddingBottom); - layout.addView(input); - - AlertDialog.Builder builder = new AlertDialog.Builder(activity) - .setTitle(titleText).setView(layout) - .setPositiveButton(positiveButtonText, (d, whichButton) -> onPositive.onTextSet(input.getText().toString())); - - if (onNeutral != null) { - builder.setNeutralButton(neutralButtonText, (dialog, which) -> onNeutral.onTextSet(input.getText().toString())); - } - - if (onNegative == null) { - builder.setNegativeButton(android.R.string.cancel, null); - } else { - builder.setNegativeButton(negativeButtonText, (dialog, which) -> onNegative.onTextSet(input.getText().toString())); - } - - if (onDismiss != null) builder.setOnDismissListener(onDismiss); - - dialogHolder[0] = builder.create(); - dialogHolder[0].setCanceledOnTouchOutside(false); - dialogHolder[0].show(); - } - -} diff --git a/app/src/main/java/com/termux/app/terminal/BellHandler.java b/app/src/main/java/com/termux/app/terminal/BellHandler.java deleted file mode 100644 index 207f7e78..00000000 --- a/app/src/main/java/com/termux/app/terminal/BellHandler.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.termux.app.terminal; - -import android.content.Context; -import android.os.Handler; -import android.os.Looper; -import android.os.SystemClock; -import android.os.Vibrator; - -public class BellHandler { - private static BellHandler instance = null; - private static final Object lock = new Object(); - - public static BellHandler getInstance(Context context) { - if (instance == null) { - synchronized (lock) { - if (instance == null) { - instance = new BellHandler((Vibrator) context.getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE)); - } - } - } - - return instance; - } - - private static final long DURATION = 50; - private static final long MIN_PAUSE = 3 * DURATION; - - private final Handler handler = new Handler(Looper.getMainLooper()); - private long lastBell = 0; - private final Runnable bellRunnable; - - private BellHandler(final Vibrator vibrator) { - bellRunnable = new Runnable() { - @Override - public void run() { - if (vibrator != null) { - vibrator.vibrate(DURATION); - } - } - }; - } - - public synchronized void doBell() { - long now = now(); - long timeSinceLastBell = now - lastBell; - - if (timeSinceLastBell < 0) { - // there is a next bell pending; don't schedule another one - } else if (timeSinceLastBell < MIN_PAUSE) { - // there was a bell recently, scheudle the next one - handler.postDelayed(bellRunnable, MIN_PAUSE - timeSinceLastBell); - lastBell = lastBell + MIN_PAUSE; - } else { - // the last bell was long ago, do it now - bellRunnable.run(); - lastBell = now; - } - } - - private long now() { - return SystemClock.uptimeMillis(); - } -} diff --git a/app/src/main/java/com/termux/app/terminal/FullScreenWorkAround.java b/app/src/main/java/com/termux/app/terminal/FullScreenWorkAround.java deleted file mode 100644 index c37c870c..00000000 --- a/app/src/main/java/com/termux/app/terminal/FullScreenWorkAround.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.termux.app.terminal; - -import android.graphics.Rect; -import android.view.View; -import android.view.ViewGroup; - -import com.termux.app.TermuxActivity; - -/** - * Work around for fullscreen mode in Termux to fix ExtraKeysView not being visible. - * This class is derived from: - * https://stackoverflow.com/questions/7417123/android-how-to-adjust-layout-in-full-screen-mode-when-softkeyboard-is-visible - * and has some additional tweaks - * --- - * For more information, see https://issuetracker.google.com/issues/36911528 - */ -public class FullScreenWorkAround { - private final View mChildOfContent; - private int mUsableHeightPrevious; - private final ViewGroup.LayoutParams mViewGroupLayoutParams; - - private final int mNavBarHeight; - - - public static void apply(TermuxActivity activity) { - new FullScreenWorkAround(activity); - } - - private FullScreenWorkAround(TermuxActivity activity) { - ViewGroup content = activity.findViewById(android.R.id.content); - mChildOfContent = content.getChildAt(0); - mViewGroupLayoutParams = mChildOfContent.getLayoutParams(); - mNavBarHeight = activity.getNavBarHeight(); - mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(this::possiblyResizeChildOfContent); - } - - private void possiblyResizeChildOfContent() { - int usableHeightNow = computeUsableHeight(); - if (usableHeightNow != mUsableHeightPrevious) { - int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight(); - int heightDifference = usableHeightSansKeyboard - usableHeightNow; - if (heightDifference > (usableHeightSansKeyboard / 4)) { - // keyboard probably just became visible - - // ensures that usable layout space does not extend behind the - // soft keyboard, causing the extra keys to not be visible - mViewGroupLayoutParams.height = (usableHeightSansKeyboard - heightDifference) + getNavBarHeight(); - } else { - // keyboard probably just became hidden - mViewGroupLayoutParams.height = usableHeightSansKeyboard; - } - mChildOfContent.requestLayout(); - mUsableHeightPrevious = usableHeightNow; - } - } - - private int getNavBarHeight() { - return mNavBarHeight; - } - - private int computeUsableHeight() { - Rect r = new Rect(); - mChildOfContent.getWindowVisibleDisplayFrame(r); - return (r.bottom - r.top); - } - -} - diff --git a/app/src/main/java/com/termux/app/terminal/KeyboardShortcut.java b/app/src/main/java/com/termux/app/terminal/KeyboardShortcut.java deleted file mode 100644 index 5db41bec..00000000 --- a/app/src/main/java/com/termux/app/terminal/KeyboardShortcut.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.termux.app.terminal; - -public class KeyboardShortcut { - - public final int codePoint; - public final int shortcutAction; - - public KeyboardShortcut(int codePoint, int shortcutAction) { - this.codePoint = codePoint; - this.shortcutAction = shortcutAction; - } - -} diff --git a/app/src/main/java/com/termux/app/terminal/extrakeys/ExtraKeyButton.java b/app/src/main/java/com/termux/app/terminal/extrakeys/ExtraKeyButton.java deleted file mode 100644 index 36457af2..00000000 --- a/app/src/main/java/com/termux/app/terminal/extrakeys/ExtraKeyButton.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.termux.app.terminal.extrakeys; - -import android.text.TextUtils; - -import androidx.annotation.Nullable; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.Arrays; -import java.util.stream.Collectors; - -public class ExtraKeyButton { - - /** - * The key that will be sent to the terminal, either a control character - * defined in ExtraKeysView.keyCodesForString (LEFT, RIGHT, PGUP...) or - * some text. - */ - private final String key; - - /** - * If the key is a macro, i.e. a sequence of keys separated by space. - */ - private final boolean macro; - - /** - * The text that will be shown on the button. - */ - private final String display; - - /** - * The information of the popup (triggered by swipe up). - */ - @Nullable - private ExtraKeyButton popup = null; - - public ExtraKeyButton(ExtraKeysInfo.CharDisplayMap charDisplayMap, JSONObject config) throws JSONException { - this(charDisplayMap, config, null); - } - - public ExtraKeyButton(ExtraKeysInfo.CharDisplayMap charDisplayMap, JSONObject config, @Nullable ExtraKeyButton popup) throws JSONException { - String keyFromConfig = config.optString("key", null); - String macroFromConfig = config.optString("macro", null); - String[] keys; - if (keyFromConfig != null && macroFromConfig != null) { - throw new JSONException("Both key and macro can't be set for the same key"); - } else if (keyFromConfig != null) { - keys = new String[]{keyFromConfig}; - this.macro = false; - } else if (macroFromConfig != null) { - keys = macroFromConfig.split(" "); - this.macro = true; - } else { - throw new JSONException("All keys have to specify either key or macro"); - } - - for (int i = 0; i < keys.length; i++) { - keys[i] = ExtraKeysInfo.replaceAlias(keys[i]); - } - - this.key = TextUtils.join(" ", keys); - - String displayFromConfig = config.optString("display", null); - if (displayFromConfig != null) { - this.display = displayFromConfig; - } else { - this.display = Arrays.stream(keys) - .map(key -> charDisplayMap.get(key, key)) - .collect(Collectors.joining(" ")); - } - - this.popup = popup; - } - - public String getKey() { - return key; - } - - public boolean isMacro() { - return macro; - } - - public String getDisplay() { - return display; - } - - @Nullable - public ExtraKeyButton getPopup() { - return popup; - } -} diff --git a/app/src/main/java/com/termux/app/terminal/extrakeys/ExtraKeysInfo.java b/app/src/main/java/com/termux/app/terminal/extrakeys/ExtraKeysInfo.java deleted file mode 100644 index e1406e72..00000000 --- a/app/src/main/java/com/termux/app/terminal/extrakeys/ExtraKeysInfo.java +++ /dev/null @@ -1,253 +0,0 @@ -package com.termux.app.terminal.extrakeys; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.HashMap; - -public class ExtraKeysInfo { - - /** - * Matrix of buttons displayed - */ - private final ExtraKeyButton[][] buttons; - - /** - * This corresponds to one of the CharMapDisplay below - */ - private String style = "default"; - - public ExtraKeysInfo(String propertiesInfo, String style) throws JSONException { - this.style = style; - - // Convert String propertiesInfo to Array of Arrays - JSONArray arr = new JSONArray(propertiesInfo); - Object[][] matrix = new Object[arr.length()][]; - for (int i = 0; i < arr.length(); i++) { - JSONArray line = arr.getJSONArray(i); - matrix[i] = new Object[line.length()]; - for (int j = 0; j < line.length(); j++) { - matrix[i][j] = line.get(j); - } - } - - // convert matrix to buttons - this.buttons = new ExtraKeyButton[matrix.length][]; - for (int i = 0; i < matrix.length; i++) { - this.buttons[i] = new ExtraKeyButton[matrix[i].length]; - for (int j = 0; j < matrix[i].length; j++) { - Object key = matrix[i][j]; - - JSONObject jobject = normalizeKeyConfig(key); - - ExtraKeyButton button; - - if(! jobject.has("popup")) { - // no popup - button = new ExtraKeyButton(getSelectedCharMap(), jobject); - } else { - // a popup - JSONObject popupJobject = normalizeKeyConfig(jobject.get("popup")); - ExtraKeyButton popup = new ExtraKeyButton(getSelectedCharMap(), popupJobject); - button = new ExtraKeyButton(getSelectedCharMap(), jobject, popup); - } - - this.buttons[i][j] = button; - } - } - } - - /** - * "hello" -> {"key": "hello"} - */ - private static JSONObject normalizeKeyConfig(Object key) throws JSONException { - JSONObject jobject; - if(key instanceof String) { - jobject = new JSONObject(); - jobject.put("key", key); - } else if(key instanceof JSONObject) { - jobject = (JSONObject) key; - } else { - throw new JSONException("An key in the extra-key matrix must be a string or an object"); - } - return jobject; - } - - public ExtraKeyButton[][] getMatrix() { - return buttons; - } - - /** - * HashMap that implements Python dict.get(key, default) function. - * Default java.util .get(key) is then the same as .get(key, null); - */ - static class CleverMap extends HashMap { - V get(K key, V defaultValue) { - if(containsKey(key)) - return get(key); - else - return defaultValue; - } - } - - static class CharDisplayMap extends CleverMap {} - - /** - * Keys are displayed in a natural looking way, like "→" for "RIGHT" - */ - static final CharDisplayMap classicArrowsDisplay = new CharDisplayMap() {{ - // classic arrow keys (for ◀ ▶ ▲ ▼ @see arrowVariationDisplay) - put("LEFT", "←"); // U+2190 ← LEFTWARDS ARROW - put("RIGHT", "→"); // U+2192 → RIGHTWARDS ARROW - put("UP", "↑"); // U+2191 ↑ UPWARDS ARROW - put("DOWN", "↓"); // U+2193 ↓ DOWNWARDS ARROW - }}; - - static final CharDisplayMap wellKnownCharactersDisplay = new CharDisplayMap() {{ - // well known characters // https://en.wikipedia.org/wiki/{Enter_key, Tab_key, Delete_key} - put("ENTER", "↲"); // U+21B2 ↲ DOWNWARDS ARROW WITH TIP LEFTWARDS - put("TAB", "↹"); // U+21B9 ↹ LEFTWARDS ARROW TO BAR OVER RIGHTWARDS ARROW TO BAR - put("BKSP", "⌫"); // U+232B ⌫ ERASE TO THE LEFT sometimes seen and easy to understand - put("DEL", "⌦"); // U+2326 ⌦ ERASE TO THE RIGHT not well known but easy to understand - put("DRAWER", "☰"); // U+2630 ☰ TRIGRAM FOR HEAVEN not well known but easy to understand - put("KEYBOARD", "⌨"); // U+2328 ⌨ KEYBOARD not well known but easy to understand - }}; - - static final CharDisplayMap lessKnownCharactersDisplay = new CharDisplayMap() {{ - // https://en.wikipedia.org/wiki/{Home_key, End_key, Page_Up_and_Page_Down_keys} - // home key can mean "goto the beginning of line" or "goto first page" depending on context, hence the diagonal - put("HOME", "⇱"); // from IEC 9995 // U+21F1 ⇱ NORTH WEST ARROW TO CORNER - put("END", "⇲"); // from IEC 9995 // ⇲ // U+21F2 ⇲ SOUTH EAST ARROW TO CORNER - put("PGUP", "⇑"); // no ISO character exists, U+21D1 ⇑ UPWARDS DOUBLE ARROW will do the trick - put("PGDN", "⇓"); // no ISO character exists, U+21D3 ⇓ DOWNWARDS DOUBLE ARROW will do the trick - }}; - - static final CharDisplayMap arrowTriangleVariationDisplay = new CharDisplayMap() {{ - // alternative to classic arrow keys - put("LEFT", "◀"); // U+25C0 ◀ BLACK LEFT-POINTING TRIANGLE - put("RIGHT", "▶"); // U+25B6 ▶ BLACK RIGHT-POINTING TRIANGLE - put("UP", "▲"); // U+25B2 ▲ BLACK UP-POINTING TRIANGLE - put("DOWN", "▼"); // U+25BC ▼ BLACK DOWN-POINTING TRIANGLE - }}; - - static final CharDisplayMap notKnownIsoCharacters = new CharDisplayMap() {{ - // Control chars that are more clear as text // https://en.wikipedia.org/wiki/{Function_key, Alt_key, Control_key, Esc_key} - // put("FN", "FN"); // no ISO character exists - put("CTRL", "⎈"); // ISO character "U+2388 ⎈ HELM SYMBOL" is unknown to people and never printed on computers, however "U+25C7 ◇ WHITE DIAMOND" is a nice presentation, and "^" for terminal app and mac is often used - put("ALT", "⎇"); // ISO character "U+2387 ⎇ ALTERNATIVE KEY SYMBOL'" is unknown to people and only printed as the Option key "⌥" on Mac computer - put("ESC", "⎋"); // ISO character "U+238B ⎋ BROKEN CIRCLE WITH NORTHWEST ARROW" is unknown to people and not often printed on computers - }}; - - static final CharDisplayMap nicerLookingDisplay = new CharDisplayMap() {{ - // nicer looking for most cases - put("-", "―"); // U+2015 ― HORIZONTAL BAR - }}; - - /* - * Multiple maps are available to quickly change - * the style of the keys. - */ - - /** - * Some classic symbols everybody knows - */ - private static final CharDisplayMap defaultCharDisplay = new CharDisplayMap() {{ - putAll(classicArrowsDisplay); - putAll(wellKnownCharactersDisplay); - putAll(nicerLookingDisplay); - // all other characters are displayed as themselves - }}; - - /** - * Classic symbols and less known symbols - */ - private static final CharDisplayMap lotsOfArrowsCharDisplay = new CharDisplayMap() {{ - putAll(classicArrowsDisplay); - putAll(wellKnownCharactersDisplay); - putAll(lessKnownCharactersDisplay); // NEW - putAll(nicerLookingDisplay); - }}; - - /** - * Only arrows - */ - private static final CharDisplayMap arrowsOnlyCharDisplay = new CharDisplayMap() {{ - putAll(classicArrowsDisplay); - // putAll(wellKnownCharactersDisplay); // REMOVED - // putAll(lessKnownCharactersDisplay); // REMOVED - putAll(nicerLookingDisplay); - }}; - - /** - * Full Iso - */ - private static final CharDisplayMap fullIsoCharDisplay = new CharDisplayMap() {{ - putAll(classicArrowsDisplay); - putAll(wellKnownCharactersDisplay); - putAll(lessKnownCharactersDisplay); // NEW - putAll(nicerLookingDisplay); - putAll(notKnownIsoCharacters); // NEW - }}; - - /** - * Some people might call our keys differently - */ - static private final CharDisplayMap controlCharsAliases = new CharDisplayMap() {{ - put("ESCAPE", "ESC"); - put("CONTROL", "CTRL"); - put("RETURN", "ENTER"); // Technically different keys, but most applications won't see the difference - put("FUNCTION", "FN"); - // no alias for ALT - - // Directions are sometimes written as first and last letter for brevety - put("LT", "LEFT"); - put("RT", "RIGHT"); - put("DN", "DOWN"); - // put("UP", "UP"); well, "UP" is already two letters - - put("PAGEUP", "PGUP"); - put("PAGE_UP", "PGUP"); - put("PAGE UP", "PGUP"); - put("PAGE-UP", "PGUP"); - - // no alias for HOME - // no alias for END - - put("PAGEDOWN", "PGDN"); - put("PAGE_DOWN", "PGDN"); - put("PAGE-DOWN", "PGDN"); - - put("DELETE", "DEL"); - put("BACKSPACE", "BKSP"); - - // easier for writing in termux.properties - put("BACKSLASH", "\\"); - put("QUOTE", "\""); - put("APOSTROPHE", "'"); - }}; - - CharDisplayMap getSelectedCharMap() { - switch (style) { - case "arrows-only": - return arrowsOnlyCharDisplay; - case "arrows-all": - return lotsOfArrowsCharDisplay; - case "all": - return fullIsoCharDisplay; - case "none": - return new CharDisplayMap(); - default: - return defaultCharDisplay; - } - } - - /** - * Applies the 'controlCharsAliases' mapping to all the strings in *buttons* - * Modifies the array, doesn't return a new one. - */ - public static String replaceAlias(String key) { - return controlCharsAliases.get(key, key); - } -} - diff --git a/app/src/main/java/com/termux/app/terminal/extrakeys/ExtraKeysView.java b/app/src/main/java/com/termux/app/terminal/extrakeys/ExtraKeysView.java deleted file mode 100644 index d1b1dee5..00000000 --- a/app/src/main/java/com/termux/app/terminal/extrakeys/ExtraKeysView.java +++ /dev/null @@ -1,382 +0,0 @@ -package com.termux.app.terminal.extrakeys; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.os.Build; -import android.provider.Settings; -import android.util.AttributeSet; - -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.ScheduledExecutorService; - -import java.util.Map; -import java.util.HashMap; -import java.util.Arrays; -import java.util.stream.Collectors; - -import android.view.Gravity; -import android.view.HapticFeedbackConstants; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.widget.Button; -import android.widget.GridLayout; -import android.widget.PopupWindow; - -import com.termux.R; -import com.termux.view.TerminalView; - -import androidx.drawerlayout.widget.DrawerLayout; - -/** - * A view showing extra keys (such as Escape, Ctrl, Alt) not normally available on an Android soft - * keyboard. - */ -public final class ExtraKeysView extends GridLayout { - - private static final int TEXT_COLOR = 0xFFFFFFFF; - private static final int BUTTON_COLOR = 0x00000000; - private static final int INTERESTING_COLOR = 0xFF80DEEA; - private static final int BUTTON_PRESSED_COLOR = 0xFF7F7F7F; - - public ExtraKeysView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - static final Map keyCodesForString = new HashMap() {{ - put("SPACE", KeyEvent.KEYCODE_SPACE); - put("ESC", KeyEvent.KEYCODE_ESCAPE); - put("TAB", KeyEvent.KEYCODE_TAB); - put("HOME", KeyEvent.KEYCODE_MOVE_HOME); - put("END", KeyEvent.KEYCODE_MOVE_END); - put("PGUP", KeyEvent.KEYCODE_PAGE_UP); - put("PGDN", KeyEvent.KEYCODE_PAGE_DOWN); - put("INS", KeyEvent.KEYCODE_INSERT); - put("DEL", KeyEvent.KEYCODE_FORWARD_DEL); - put("BKSP", KeyEvent.KEYCODE_DEL); - put("UP", KeyEvent.KEYCODE_DPAD_UP); - put("LEFT", KeyEvent.KEYCODE_DPAD_LEFT); - put("RIGHT", KeyEvent.KEYCODE_DPAD_RIGHT); - put("DOWN", KeyEvent.KEYCODE_DPAD_DOWN); - put("ENTER", KeyEvent.KEYCODE_ENTER); - put("F1", KeyEvent.KEYCODE_F1); - put("F2", KeyEvent.KEYCODE_F2); - put("F3", KeyEvent.KEYCODE_F3); - put("F4", KeyEvent.KEYCODE_F4); - put("F5", KeyEvent.KEYCODE_F5); - put("F6", KeyEvent.KEYCODE_F6); - put("F7", KeyEvent.KEYCODE_F7); - put("F8", KeyEvent.KEYCODE_F8); - put("F9", KeyEvent.KEYCODE_F9); - put("F10", KeyEvent.KEYCODE_F10); - put("F11", KeyEvent.KEYCODE_F11); - put("F12", KeyEvent.KEYCODE_F12); - }}; - - @SuppressLint("RtlHardcoded") - private void sendKey(View view, String keyName, boolean forceCtrlDown, boolean forceLeftAltDown) { - TerminalView terminalView = view.findViewById(R.id.terminal_view); - if ("KEYBOARD".equals(keyName)) { - InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - imm.toggleSoftInput(0, 0); - } else if ("DRAWER".equals(keyName)) { - DrawerLayout drawer = view.findViewById(R.id.drawer_layout); - drawer.openDrawer(Gravity.LEFT); - } else if (keyCodesForString.containsKey(keyName)) { - Integer keyCode = keyCodesForString.get(keyName); - if (keyCode == null) return; - int metaState = 0; - if (forceCtrlDown) { - metaState |= KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON; - } - if (forceLeftAltDown) { - metaState |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON; - } - KeyEvent keyEvent = new KeyEvent(0, 0, KeyEvent.ACTION_UP, keyCode, 0, metaState); - terminalView.onKeyDown(keyCode, keyEvent); - } else { - // not a control char - keyName.codePoints().forEach(codePoint -> { - terminalView.inputCodePoint(codePoint, forceCtrlDown, forceLeftAltDown); - }); - } - } - - private void sendKey(View view, ExtraKeyButton buttonInfo) { - if (buttonInfo.isMacro()) { - String[] keys = buttonInfo.getKey().split(" "); - boolean ctrlDown = false; - boolean altDown = false; - for (String key : keys) { - if ("CTRL".equals(key)) { - ctrlDown = true; - } else if ("ALT".equals(key)) { - altDown = true; - } else { - sendKey(view, key, ctrlDown, altDown); - ctrlDown = false; - altDown = false; - } - } - } else { - sendKey(view, buttonInfo.getKey(), false, false); - } - } - - public enum SpecialButton { - CTRL, ALT, FN - } - - private static class SpecialButtonState { - boolean isOn = false; - boolean isActive = false; - List