diff --git a/README.md b/README.md index edef1d5e1f..a9283d21af 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Termux application +Git Test for Jeon SeungYoon + [![Build status](https://github.com/termux/termux-app/workflows/Build/badge.svg)](https://github.com/termux/termux-app/actions) [![Testing status](https://github.com/termux/termux-app/workflows/Unit%20tests/badge.svg)](https://github.com/termux/termux-app/actions) [![Join the chat at https://gitter.im/termux/termux](https://badges.gitter.im/termux/termux.svg)](https://gitter.im/termux/termux) diff --git a/app/src/main/java/com/termux/app/RunCommandService.java b/app/src/main/java/com/termux/app/RunCommandService.java index 261729155c..f49bf05f06 100644 --- a/app/src/main/java/com/termux/app/RunCommandService.java +++ b/app/src/main/java/com/termux/app/RunCommandService.java @@ -36,6 +36,7 @@ public class RunCommandService extends Service { private static final String LOG_TAG = "RunCommandService"; + public static final int GRAY = 0xFF607D8B; class LocalBinder extends Binder { public final RunCommandService service = RunCommandService.this; @@ -71,125 +72,63 @@ public int onStartCommand(Intent intent, int flags, int startId) { Error error; String errmsg; - // If invalid action passed, then just return - if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) { - errmsg = this.getString(R.string.error_run_command_service_invalid_intent_action, intent.getAction()); - executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg); - TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false); - return stopService(); - } + Integer invalidAction = checkInvalidAction(intent, executionCommand); + if (invalidAction != null) return invalidAction; String executableExtra = executionCommand.executable = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH, null); executionCommand.arguments = IntentUtils.getStringArrayExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS, null); - /* - * If intent was sent with `am` command, then normal comma characters may have been replaced - * with alternate characters if a normal comma existed in an argument itself to prevent it - * splitting into multiple arguments by `am` command. - * If `tudo` or `sudo` are used, then simply using their `-r` and `--comma-alternative` command - * options can be used without passing the below extras, but native supports is helpful if - * they are not being used. - * https://github.com/agnostic-apollo/tudo#passing-arguments-using-run_command-intent - * https://android.googlesource.com/platform/frameworks/base/+/21bdaf1/cmds/am/src/com/android/commands/am/Am.java#572 - */ - boolean replaceCommaAlternativeCharsInArguments = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS, false); - if (replaceCommaAlternativeCharsInArguments) { - String commaAlternativeCharsInArguments = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS, null); - if (commaAlternativeCharsInArguments == null) - commaAlternativeCharsInArguments = TermuxConstants.COMMA_ALTERNATIVE; - // Replace any commaAlternativeCharsInArguments characters with normal commas - DataUtils.replaceSubStringsInStringArrayItems(executionCommand.arguments, commaAlternativeCharsInArguments, TermuxConstants.COMMA_NORMAL); - } + replaceComma2Char(intent, executionCommand); executionCommand.stdin = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_STDIN, null); executionCommand.workingDirectory = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_WORKDIR, null); - // If EXTRA_RUNNER is passed, use that, otherwise check EXTRA_BACKGROUND and default to Runner.TERMINAL_SESSION - executionCommand.runner = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RUNNER, - (intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false) ? Runner.APP_SHELL.getName() : Runner.TERMINAL_SESSION.getName())); - if (Runner.runnerOf(executionCommand.runner) == null) { - errmsg = this.getString(R.string.error_run_command_service_invalid_execution_command_runner, executionCommand.runner); - executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg); - TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false); - return stopService(); - } + Integer invalidRunner = checkRunner(intent, executionCommand); + if (invalidRunner != null) return invalidRunner; - executionCommand.backgroundCustomLogLevel = IntentUtils.getIntegerExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL, null); - executionCommand.sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION); - executionCommand.sessionName = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_SESSION_NAME, null); - executionCommand.sessionCreateMode = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_SESSION_CREATE_MODE, null); - executionCommand.commandLabel = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_LABEL, "RUN_COMMAND Execution Intent Command"); - executionCommand.commandDescription = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION, null); - executionCommand.commandHelp = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_HELP, null); - executionCommand.isPluginExecutionCommand = true; - executionCommand.resultConfig.resultPendingIntent = intent.getParcelableExtra(RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT); - executionCommand.resultConfig.resultDirectoryPath = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_DIRECTORY, null); - if (executionCommand.resultConfig.resultDirectoryPath != null) { - executionCommand.resultConfig.resultSingleFile = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_RESULT_SINGLE_FILE, false); - executionCommand.resultConfig.resultFileBasename = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_BASENAME, null); - executionCommand.resultConfig.resultFileOutputFormat = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, null); - executionCommand.resultConfig.resultFileErrorFormat = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, null); - executionCommand.resultConfig.resultFilesSuffix = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILES_SUFFIX, null); - } - - // If "allow-external-apps" property to not set to "true", then just return - // We enable force notifications if "allow-external-apps" policy is violated so that the - // user knows someone tried to run a command in termux context, since it may be malicious - // app or imported (tasker) plugin project and not the user himself. If a pending intent is - // also sent, then its creator is also logged and shown. - errmsg = TermuxPluginUtils.checkIfAllowExternalAppsPolicyIsViolated(this, LOG_TAG); - if (errmsg != null) { - executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg); - TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true); - return stopService(); - } + getSomeExecutionCommand(intent, executionCommand); + Integer InvalidAppPolicy = checkInvalidAppPolicy(executionCommand); + if (InvalidAppPolicy != null) return InvalidAppPolicy; - // If executable is null or empty, then exit here instead of getting canonical path which would expand to "/" - if (executionCommand.executable == null || executionCommand.executable.isEmpty()) { - errmsg = this.getString(R.string.error_run_command_service_mandatory_extra_missing, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH); - executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg); - TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false); - return stopService(); - } + Integer quitExecution = checkExecutable(executionCommand); + if (quitExecution != null) return quitExecution; // Get canonical path of executable executionCommand.executable = TermuxFileUtils.getCanonicalPath(executionCommand.executable, null, true); - // If executable is not a regular file, or is not readable or executable, then just return - // Setting of missing read and execute permissions is not done - error = FileUtils.validateRegularFileExistenceAndPermissions("executable", executionCommand.executable, null, - FileUtils.APP_EXECUTABLE_FILE_PERMISSIONS, true, true, - false); - if (error != null) { - executionCommand.setStateFailed(error); - TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false); - return stopService(); - } + Integer invalidFile = checkInvalidFile(executionCommand); + if (invalidFile != null) return invalidFile; + + + Integer invalidWorkingDirectory = getCanonicalPath(executionCommand); + if (invalidWorkingDirectory != null) return invalidWorkingDirectory; + + checkSymlink(executionCommand, executableExtra); + executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(executionCommand.executable).build(); + + Logger.logVerboseExtended(LOG_TAG, executionCommand.toString()); + startTermuxService(executionCommand); + return stopService(); + } + + private Integer getCanonicalPath(ExecutionCommand executionCommand) { // If workingDirectory is not null or empty if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) { // Get canonical path of workingDirectory executionCommand.workingDirectory = TermuxFileUtils.getCanonicalPath(executionCommand.workingDirectory, null, true); - // If workingDirectory is not a directory, or is not readable or writable, then just return - // Creation of missing directory and setting of read, write and execute permissions are only done if workingDirectory is - // under allowed termux working directory paths. - // We try to set execute permissions, but ignore if they are missing, since only read and write permissions are required - // for working directories. - error = TermuxFileUtils.validateDirectoryFileExistenceAndPermissions("working", executionCommand.workingDirectory, - true, true, true, - false, true); - if (error != null) { - executionCommand.setStateFailed(error); - TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false); - return stopService(); - } + Integer invalidWorkingDirectory = checkInvalidWorkingDirectory(executionCommand); + if (invalidWorkingDirectory != null) return invalidWorkingDirectory; } + return null; + } + private void checkSymlink(ExecutionCommand executionCommand, String executableExtra) { // If the executable passed as the extra was an applet for coreutils/busybox, then we must // use it instead of the canonical path above since otherwise arguments would be passed to // coreutils/busybox instead and command would fail. Broken symlinks would already have been @@ -199,11 +138,81 @@ public int onStartCommand(Intent intent, int flags, int startId) { Logger.logVerbose(LOG_TAG, "The executableExtra path \"" + executableExtra + "\" is a symlink so using it instead of the canonical path \"" + executionCommand.executable + "\""); executionCommand.executable = executableExtra; } + } - executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(executionCommand.executable).build(); + private Integer checkInvalidWorkingDirectory(ExecutionCommand executionCommand) { + Error error; + // If workingDirectory is not a directory, or is not readable or writable, then just return + // Creation of missing directory and setting of read, write and execute permissions are only done if workingDirectory is + // under allowed termux working directory paths. + // We try to set execute permissions, but ignore if they are missing, since only read and write permissions are required + // for working directories. + error = TermuxFileUtils.validateDirectoryFileExistenceAndPermissions("working", executionCommand.workingDirectory, + true, true, true, + false, true); + if (error != null) { + executionCommand.setStateFailed(error); + TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false); + return stopService(); + } + return null; + } - Logger.logVerboseExtended(LOG_TAG, executionCommand.toString()); + private void startTermuxService(ExecutionCommand executionCommand) { + Intent execIntent = createExecutionIntent(executionCommand); + // Start TERMUX_SERVICE and pass it execution intent + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + this.startForegroundService(execIntent); + } else { + this.startService(execIntent); + } + } + + private Integer checkInvalidFile(ExecutionCommand executionCommand) { + Error error; + // If executable is not a regular file, or is not readable or executable, then just return + // Setting of missing read and execute permissions is not done + error = FileUtils.validateRegularFileExistenceAndPermissions("executable", executionCommand.executable, null, + FileUtils.APP_EXECUTABLE_FILE_PERMISSIONS, true, true, + false); + if (error != null) { + executionCommand.setStateFailed(error); + TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false); + return stopService(); + } + return null; + } + + private Integer checkExecutable(ExecutionCommand executionCommand) { + String errmsg; + // If executable is null or empty, then exit here instead of getting canonical path which would expand to "/" + if (executionCommand.executable == null || executionCommand.executable.isEmpty()) { + errmsg = this.getString(R.string.error_run_command_service_mandatory_extra_missing, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH); + executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg); + TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false); + return stopService(); + } + return null; + } + + private Integer checkInvalidAppPolicy(ExecutionCommand executionCommand) { + String errmsg; + // If "allow-external-apps" property to not set to "true", then just return + // We enable force notifications if "allow-external-apps" policy is violated so that the + // user knows someone tried to run a command in termux context, since it may be malicious + // app or imported (tasker) plugin project and not the user himself. If a pending intent is + // also sent, then its creator is also logged and shown. + errmsg = TermuxPluginUtils.checkIfAllowExternalAppsPolicyIsViolated(this, LOG_TAG); + if (errmsg != null) { + executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg); + TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true); + return stopService(); + } + return null; + } + + private Intent createExecutionIntent(ExecutionCommand executionCommand) { // Create execution intent with the action TERMUX_SERVICE#ACTION_SERVICE_EXECUTE to be sent to the TERMUX_SERVICE Intent execIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, executionCommand.executableUri); execIntent.setClass(this, TermuxService.class); @@ -228,15 +237,74 @@ public int onStartCommand(Intent intent, int flags, int startId) { execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, executionCommand.resultConfig.resultFileErrorFormat); execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILES_SUFFIX, executionCommand.resultConfig.resultFilesSuffix); } + return execIntent; + } - // Start TERMUX_SERVICE and pass it execution intent - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - this.startForegroundService(execIntent); - } else { - this.startService(execIntent); + private void getSomeExecutionCommand(Intent intent, ExecutionCommand executionCommand) { + executionCommand.backgroundCustomLogLevel = IntentUtils.getIntegerExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL, null); + executionCommand.sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION); + executionCommand.sessionName = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_SESSION_NAME, null); + executionCommand.sessionCreateMode = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_SESSION_CREATE_MODE, null); + executionCommand.commandLabel = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_LABEL, "RUN_COMMAND Execution Intent Command"); + executionCommand.commandDescription = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION, null); + executionCommand.commandHelp = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_HELP, null); + executionCommand.isPluginExecutionCommand = true; + executionCommand.resultConfig.resultPendingIntent = intent.getParcelableExtra(RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT); + executionCommand.resultConfig.resultDirectoryPath = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_DIRECTORY, null); + if (executionCommand.resultConfig.resultDirectoryPath != null) { + executionCommand.resultConfig.resultSingleFile = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_RESULT_SINGLE_FILE, false); + executionCommand.resultConfig.resultFileBasename = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_BASENAME, null); + executionCommand.resultConfig.resultFileOutputFormat = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, null); + executionCommand.resultConfig.resultFileErrorFormat = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, null); + executionCommand.resultConfig.resultFilesSuffix = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILES_SUFFIX, null); } + } - return stopService(); + private Integer checkInvalidAction(Intent intent, ExecutionCommand executionCommand) { + String errmsg; + // If invalid action passed, then just return + if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) { + errmsg = this.getString(R.string.error_run_command_service_invalid_intent_action, intent.getAction()); + executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg); + TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false); + return stopService(); + } + return null; + } + + private void replaceComma2Char(Intent intent, ExecutionCommand executionCommand) { + /* + * If intent was sent with `am` command, then normal comma characters may have been replaced + * with alternate characters if a normal comma existed in an argument itself to prevent it + * splitting into multiple arguments by `am` command. + * If `tudo` or `sudo` are used, then simply using their `-r` and `--comma-alternative` command + * options can be used without passing the below extras, but native supports is helpful if + * they are not being used. + * https://github.com/agnostic-apollo/tudo#passing-arguments-using-run_command-intent + * https://android.googlesource.com/platform/frameworks/base/+/21bdaf1/cmds/am/src/com/android/commands/am/Am.java#572 + */ + boolean replaceCommaAlternativeCharsInArguments = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS, false); + if (replaceCommaAlternativeCharsInArguments) { + String commaAlternativeCharsInArguments = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS, null); + if (commaAlternativeCharsInArguments == null) + commaAlternativeCharsInArguments = TermuxConstants.COMMA_ALTERNATIVE; + // Replace any commaAlternativeCharsInArguments characters with normal commas + DataUtils.replaceSubStringsInStringArrayItems(executionCommand.arguments, commaAlternativeCharsInArguments, TermuxConstants.COMMA_NORMAL); + } + } + + private Integer checkRunner(Intent intent, ExecutionCommand executionCommand) { + String errmsg; + // If EXTRA_RUNNER is passed, use that, otherwise check EXTRA_BACKGROUND and default to Runner.TERMINAL_SESSION + executionCommand.runner = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RUNNER, + (intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false) ? Runner.APP_SHELL.getName() : Runner.TERMINAL_SESSION.getName())); + if (Runner.runnerOf(executionCommand.runner) == null) { + errmsg = this.getString(R.string.error_run_command_service_invalid_execution_command_runner, executionCommand.runner); + executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg); + TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false); + return stopService(); + } + return null; } private int stopService() { @@ -272,7 +340,7 @@ private Notification buildNotification() { builder.setSmallIcon(R.drawable.ic_service_notification); // Set background color for small notification icon - builder.setColor(0xFF607D8B); + builder.setColor(GRAY); return builder.build(); } diff --git a/app/src/main/java/com/termux/app/TermuxInstaller.java b/app/src/main/java/com/termux/app/TermuxInstaller.java index c3d112adb2..bb3a589eb7 100644 --- a/app/src/main/java/com/termux/app/TermuxInstaller.java +++ b/app/src/main/java/com/termux/app/TermuxInstaller.java @@ -96,7 +96,10 @@ static void setupBootstrapIfNeeded(final Activity activity, final Runnable whenD if (FileUtils.directoryFileExists(TERMUX_PREFIX_DIR_PATH, true)) { File[] PREFIX_FILE_LIST = TERMUX_PREFIX_DIR.listFiles(); // If prefix directory is empty or only contains the tmp directory - if(PREFIX_FILE_LIST == null || PREFIX_FILE_LIST.length == 0 || (PREFIX_FILE_LIST.length == 1 && TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH.equals(PREFIX_FILE_LIST[0].getAbsolutePath()))) { + boolean hasEmptyPrefixDirectory = PREFIX_FILE_LIST == null || PREFIX_FILE_LIST.length == 0; + boolean hasTmpDirectoryOnly = PREFIX_FILE_LIST.length == 1 && TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH.equals(PREFIX_FILE_LIST[0].getAbsolutePath()); + + if(hasEmptyPrefixDirectory || hasTmpDirectoryOnly) { Logger.logInfo(LOG_TAG, "The termux prefix directory \"" + TERMUX_PREFIX_DIR_PATH + "\" exists but is empty or only contains the tmp directory."); } else { whenDone.run(); @@ -110,38 +113,12 @@ static void setupBootstrapIfNeeded(final Activity activity, final Runnable whenD new Thread() { @Override public void run() { - try { - Logger.logInfo(LOG_TAG, "Installing " + TermuxConstants.TERMUX_APP_NAME + " bootstrap packages."); - - Error error; - - // Delete prefix staging directory or any file at its destination - error = FileUtils.deleteFile("termux prefix staging directory", TERMUX_STAGING_PREFIX_DIR_PATH, true); - if (error != null) { - showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error)); - return; - } - - // Delete prefix directory or any file at its destination - error = FileUtils.deleteFile("termux prefix directory", TERMUX_PREFIX_DIR_PATH, true); - if (error != null) { - showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error)); - return; - } + try {Error error; - // Create prefix staging directory if it does not already exist and set required permissions - error = TermuxFileUtils.isTermuxPrefixStagingDirectoryAccessible(true, true); - if (error != null) { - showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error)); - return; - } + if (hasPrefixError(activity, whenDone)) return; + Logger.logInfo(LOG_TAG, "Installing " + TermuxConstants.TERMUX_APP_NAME + " bootstrap packages."); - // Create prefix directory if it does not already exist and set required permissions - error = TermuxFileUtils.isTermuxPrefixDirectoryAccessible(true, true); - if (error != null) { - showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error)); - return; - } + Logger.logInfo(LOG_TAG, "Extracting bootstrap zip to prefix staging directory \"" + TERMUX_STAGING_PREFIX_DIR_PATH + "\"."); @@ -164,10 +141,7 @@ public void run() { symlinks.add(Pair.create(oldPath, newPath)); error = ensureDirectoryExists(new File(newPath).getParentFile()); - if (error != null) { - showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error)); - return; - } + if (hasBootstrapError(error, activity, whenDone)) return; } } else { String zipEntryName = zipEntry.getName(); @@ -175,10 +149,7 @@ public void run() { boolean isDirectory = zipEntry.isDirectory(); error = ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile()); - if (error != null) { - showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error)); - return; - } + if (hasBootstrapError(error, activity, whenDone)) return; if (!isDirectory) { try (FileOutputStream outStream = new FileOutputStream(targetFile)) { @@ -227,6 +198,36 @@ public void run() { }.start(); } + + + private static boolean hasPrefixError(Activity activity, Runnable whenDone) { + Error error; + // Delete prefix staging directory or any file at its destination + error = FileUtils.deleteFile("termux prefix staging directory", TERMUX_STAGING_PREFIX_DIR_PATH, true); + if (hasBootstrapError(error, activity, whenDone)) return true; + + // Delete prefix directory or any file at its destination + error = FileUtils.deleteFile("termux prefix directory", TERMUX_PREFIX_DIR_PATH, true); + if (hasBootstrapError(error, activity, whenDone)) return true; + + // Create prefix staging directory if it does not already exist and set required permissions + error = TermuxFileUtils.isTermuxPrefixStagingDirectoryAccessible(true, true); + if (hasBootstrapError(error, activity, whenDone)) return true; + + // Create prefix directory if it does not already exist and set required permissions + error = TermuxFileUtils.isTermuxPrefixDirectoryAccessible(true, true); + if (hasBootstrapError(error, activity, whenDone)) return true; + return false; + } + + private static boolean hasBootstrapError(Error error, Activity activity, Runnable whenDone) { + if (error != null) { + showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error)); + return true; + } + return false; + } + public static void showBootstrapErrorDialog(Activity activity, Runnable whenDone, String message) { Logger.logErrorExtended(LOG_TAG, "Bootstrap Error:\n" + message); diff --git a/app/src/main/java/com/termux/app/TermuxService.java b/app/src/main/java/com/termux/app/TermuxService.java index 59005c0a9e..a1985277d9 100644 --- a/app/src/main/java/com/termux/app/TermuxService.java +++ b/app/src/main/java/com/termux/app/TermuxService.java @@ -137,14 +137,20 @@ public int onStartCommand(Intent intent, int flags, int startId) { // Run again in case service is already started and onCreate() is not called runStartForeground(); - String action = null; - if (intent != null) { - Logger.logVerboseExtended(LOG_TAG, "Intent Received:\n" + IntentUtils.getIntentString(intent)); - action = intent.getAction(); - } + String chooseAction = null; + + chooseAction = getAction(intent, chooseAction); + ExecuteAction(intent, chooseAction); + + // If this service really do get killed, there is no point restarting it automatically - let the user do on next + // start of {@link Term): + return Service.START_NOT_STICKY; + } - if (action != null) { - switch (action) { + private void ExecuteAction(Intent intent, String chooseAction) { + boolean isAction = chooseAction != null; + if (isAction) { + switch (chooseAction) { case TERMUX_SERVICE.ACTION_STOP_SERVICE: Logger.logDebug(LOG_TAG, "ACTION_STOP_SERVICE intent received"); actionStopService(); @@ -162,24 +168,33 @@ public int onStartCommand(Intent intent, int flags, int startId) { actionServiceExecute(intent); break; default: - Logger.logError(LOG_TAG, "Invalid action: \"" + action + "\""); + + Logger.logError(LOG_TAG, "Invalid action: \"" + chooseAction + "\""); break; } } + } - // If this service really do get killed, there is no point restarting it automatically - let the user do on next - // start of {@link Term): - return Service.START_NOT_STICKY; + private String getAction(Intent intent, String chooseAction) { + boolean isIntent = intent != null; + if (isIntent) { + String getActionMessage = "Intent Received:\n" + IntentUtils.getIntentString(intent); + Logger.logVerboseExtended(LOG_TAG, getActionMessage); + chooseAction = intent.getAction(); + } + return chooseAction; } @Override public void onDestroy() { + boolean isStop = !mWantsToStop; Logger.logVerbose(LOG_TAG, "onDestroy"); TermuxShellUtils.clearTermuxTMPDIR(true); actionReleaseWakeLock(false); - if (!mWantsToStop) + + if (isStop) killAllTermuxExecutionCommands(); runStopForeground(); } @@ -262,25 +277,19 @@ private void actionStopService() { * * We make copies of each list since items are removed inside the loop. */ + + // Need to apply Design Pattern private synchronized void killAllTermuxExecutionCommands() { boolean processResult; Logger.logDebug(LOG_TAG, "Killing TermuxSessions=" + mTermuxSessions.size() + ", TermuxTasks=" + mTermuxTasks.size() + ", PendingPluginExecutionCommands=" + mPendingPluginExecutionCommands.size()); - List termuxSessions = new ArrayList<>(mTermuxSessions); - for (int i = 0; i < termuxSessions.size(); i++) { - ExecutionCommand executionCommand = termuxSessions.get(i).getExecutionCommand(); - processResult = mWantsToStop || executionCommand.isPluginExecutionCommandWithPendingResult(); - termuxSessions.get(i).killIfExecuting(this, processResult); - } - - List termuxTasks = new ArrayList<>(mTermuxTasks); - for (int i = 0; i < termuxTasks.size(); i++) { - ExecutionCommand executionCommand = termuxTasks.get(i).getExecutionCommand(); - if (executionCommand.isPluginExecutionCommandWithPendingResult()) - termuxTasks.get(i).killIfExecuting(this, true); - } + killTerumxSessions(); + killtermuxTasks(); + killpendingPluginExecutionCommands(); + } + private void killpendingPluginExecutionCommands() { List pendingPluginExecutionCommands = new ArrayList<>(mPendingPluginExecutionCommands); for (int i = 0; i < pendingPluginExecutionCommands.size(); i++) { ExecutionCommand executionCommand = pendingPluginExecutionCommands.get(i); @@ -292,8 +301,27 @@ private synchronized void killAllTermuxExecutionCommands() { } } + private void killtermuxTasks() { + List termuxTasks = new ArrayList<>(mTermuxTasks); + for (int i = 0; i < termuxTasks.size(); i++) { + ExecutionCommand executionCommand = termuxTasks.get(i).getExecutionCommand(); + if (executionCommand.isPluginExecutionCommandWithPendingResult()) + termuxTasks.get(i).killIfExecuting(this, true); + } + } + private void killTerumxSessions() { + boolean processResult; + List termuxSessions = new ArrayList<>(mTermuxSessions); + for (int i = 0; i < termuxSessions.size(); i++) { + ExecutionCommand executionCommand = termuxSessions.get(i).getExecutionCommand(); + processResult = mWantsToStop || executionCommand.isPluginExecutionCommandWithPendingResult(); + termuxSessions.get(i).killIfExecuting(this, processResult); + } + } + + //Need to apply Design Pattern /** Process action to acquire Power and Wi-Fi WakeLocks. */ @SuppressLint({"WakelockTimeout", "BatteryLife"}) private void actionAcquireWakeLock() { @@ -304,14 +332,10 @@ private void actionAcquireWakeLock() { Logger.logDebug(LOG_TAG, "Acquiring WakeLocks"); - PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); - mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TermuxConstants.TERMUX_APP_NAME.toLowerCase() + ":service-wakelock"); - mWakeLock.acquire(); + powerMangerAcquireWakeLock(); // 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, TermuxConstants.TERMUX_APP_NAME.toLowerCase()); - mWifiLock.acquire(); + wifiMangerAcquireWakeLock(); if (!PermissionUtils.checkIfBatteryOptimizationsDisabled(this)) { PermissionUtils.requestDisableBatteryOptimizations(this); @@ -323,6 +347,18 @@ private void actionAcquireWakeLock() { } + private void powerMangerAcquireWakeLock() { + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TermuxConstants.TERMUX_APP_NAME.toLowerCase() + ":service-wakelock"); + mWakeLock.acquire(); + } + + private void wifiMangerAcquireWakeLock() { + WifiManager wm = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); + mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, TermuxConstants.TERMUX_APP_NAME.toLowerCase()); + mWifiLock.acquire(); + } + /** Process action to release Power and Wi-Fi WakeLocks. */ private void actionReleaseWakeLock(boolean updateNotification) { if (mWakeLock == null && mWifiLock == null) { @@ -332,6 +368,15 @@ private void actionReleaseWakeLock(boolean updateNotification) { Logger.logDebug(LOG_TAG, "Releasing WakeLocks"); + releaseWakeLockAndWifiLock(); + + if (updateNotification) + updateNotification(); + + Logger.logDebug(LOG_TAG, "WakeLocks released successfully"); + } + + private void releaseWakeLockAndWifiLock() { if (mWakeLock != null) { mWakeLock.release(); mWakeLock = null; @@ -341,11 +386,6 @@ private void actionReleaseWakeLock(boolean updateNotification) { mWifiLock.release(); mWifiLock = null; } - - if (updateNotification) - updateNotification(); - - Logger.logDebug(LOG_TAG, "WakeLocks released successfully"); } /** Process {@link TERMUX_SERVICE#ACTION_SERVICE_EXECUTE} intent to execute a shell command in @@ -618,20 +658,23 @@ public synchronized int removeTermuxSession(TerminalSession sessionToRemove) { /** Callback received when a {@link TermuxSession} finishes. */ @Override public void onTermuxSessionExited(final TermuxSession termuxSession) { - if (termuxSession != null) { + boolean isTermuxSession = termuxSession != null; + if (isTermuxSession) { ExecutionCommand executionCommand = termuxSession.getExecutionCommand(); Logger.logVerbose(LOG_TAG, "The onTermuxSessionExited() callback called for \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession command"); // If the execution command was started for a plugin, then process the results - if (executionCommand != null && executionCommand.isPluginExecutionCommand) + boolean isExecutionCommand = executionCommand != null; + if (isExecutionCommand && executionCommand.isPluginExecutionCommand) TermuxPluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand); mTermuxSessions.remove(termuxSession); // Notify {@link TermuxSessionsListViewController} that sessions list has been updated if // activity in is foreground - if (mTermuxTerminalSessionClient != null) + boolean isTermuxTerminalSessionClient = mTermuxTerminalSessionClient != null; + if (isTermuxTerminalSessionClient) mTermuxTerminalSessionClient.termuxSessionListNotifyUpdated(); } diff --git a/app/src/main/java/com/termux/app/activities/SettingsActivity.java b/app/src/main/java/com/termux/app/activities/SettingsActivity.java index 8c871a449a..da60b2d1db 100644 --- a/app/src/main/java/com/termux/app/activities/SettingsActivity.java +++ b/app/src/main/java/com/termux/app/activities/SettingsActivity.java @@ -1,28 +1,14 @@ package com.termux.app.activities; -import android.content.Context; import android.os.Bundle; -import android.os.Environment; -import androidx.annotation.NonNull; + import androidx.appcompat.app.AppCompatActivity; -import androidx.preference.Preference; -import androidx.preference.PreferenceFragmentCompat; + import com.termux.R; -import com.termux.shared.activities.ReportActivity; -import com.termux.shared.file.FileUtils; -import com.termux.shared.models.ReportInfo; -import com.termux.app.models.UserAction; -import com.termux.shared.interact.ShareUtils; -import com.termux.shared.android.PackageUtils; -import com.termux.shared.termux.settings.preferences.TermuxAPIAppSharedPreferences; -import com.termux.shared.termux.settings.preferences.TermuxFloatAppSharedPreferences; -import com.termux.shared.termux.settings.preferences.TermuxTaskerAppSharedPreferences; -import com.termux.shared.termux.settings.preferences.TermuxWidgetAppSharedPreferences; -import com.termux.shared.android.AndroidUtils; -import com.termux.shared.termux.TermuxConstants; -import com.termux.shared.termux.TermuxUtils; + +import com.termux.app.fragments.settings.RootPreferencesFragment; import com.termux.shared.activity.media.AppCompatActivityUtils; import com.termux.shared.theme.NightMode; @@ -52,118 +38,4 @@ public boolean onSupportNavigateUp() { return true; } - public static class RootPreferencesFragment extends PreferenceFragmentCompat { - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - Context context = getContext(); - if (context == null) return; - - setPreferencesFromResource(R.xml.root_preferences, rootKey); - - new Thread() { - @Override - public void run() { - configureTermuxAPIPreference(context); - configureTermuxFloatPreference(context); - configureTermuxTaskerPreference(context); - configureTermuxWidgetPreference(context); - configureAboutPreference(context); - configureDonatePreference(context); - } - }.start(); - } - - private void configureTermuxAPIPreference(@NonNull Context context) { - Preference termuxAPIPreference = findPreference("termux_api"); - if (termuxAPIPreference != null) { - TermuxAPIAppSharedPreferences preferences = TermuxAPIAppSharedPreferences.build(context, false); - // If failed to get app preferences, then likely app is not installed, so do not show its preference - termuxAPIPreference.setVisible(preferences != null); - } - } - - private void configureTermuxFloatPreference(@NonNull Context context) { - Preference termuxFloatPreference = findPreference("termux_float"); - if (termuxFloatPreference != null) { - TermuxFloatAppSharedPreferences preferences = TermuxFloatAppSharedPreferences.build(context, false); - // If failed to get app preferences, then likely app is not installed, so do not show its preference - termuxFloatPreference.setVisible(preferences != null); - } - } - - private void configureTermuxTaskerPreference(@NonNull Context context) { - Preference termuxTaskerPreference = findPreference("termux_tasker"); - if (termuxTaskerPreference != null) { - TermuxTaskerAppSharedPreferences preferences = TermuxTaskerAppSharedPreferences.build(context, false); - // If failed to get app preferences, then likely app is not installed, so do not show its preference - termuxTaskerPreference.setVisible(preferences != null); - } - } - - private void configureTermuxWidgetPreference(@NonNull Context context) { - Preference termuxWidgetPreference = findPreference("termux_widget"); - if (termuxWidgetPreference != null) { - TermuxWidgetAppSharedPreferences preferences = TermuxWidgetAppSharedPreferences.build(context, false); - // If failed to get app preferences, then likely app is not installed, so do not show its preference - termuxWidgetPreference.setVisible(preferences != null); - } - } - - private void configureAboutPreference(@NonNull Context context) { - Preference aboutPreference = findPreference("about"); - if (aboutPreference != null) { - aboutPreference.setOnPreferenceClickListener(preference -> { - new Thread() { - @Override - public void run() { - String title = "About"; - - StringBuilder aboutString = new StringBuilder(); - aboutString.append(TermuxUtils.getAppInfoMarkdownString(context, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGES)); - aboutString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context)); - aboutString.append("\n\n").append(TermuxUtils.getImportantLinksMarkdownString(context)); - - String userActionName = UserAction.ABOUT.getName(); - - ReportInfo reportInfo = new ReportInfo(userActionName, - TermuxConstants.TERMUX_APP.TERMUX_SETTINGS_ACTIVITY_NAME, title); - reportInfo.setReportString(aboutString.toString()); - reportInfo.setReportSaveFileLabelAndPath(userActionName, - Environment.getExternalStorageDirectory() + "/" + - FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true)); - - ReportActivity.startReportActivity(context, reportInfo); - } - }.start(); - - return true; - }); - } - } - - private void configureDonatePreference(@NonNull Context context) { - Preference donatePreference = findPreference("donate"); - if (donatePreference != null) { - String signingCertificateSHA256Digest = PackageUtils.getSigningCertificateSHA256DigestForPackage(context); - if (signingCertificateSHA256Digest != null) { - // If APK is a Google Playstore release, then do not show the donation link - // since Termux isn't exempted from the playstore policy donation links restriction - // Check Fund solicitations: https://pay.google.com/intl/en_in/about/policy/ - String apkRelease = TermuxUtils.getAPKRelease(signingCertificateSHA256Digest); - if (apkRelease == null || apkRelease.equals(TermuxConstants.APK_RELEASE_GOOGLE_PLAYSTORE_SIGNING_CERTIFICATE_SHA256_DIGEST)) { - donatePreference.setVisible(false); - return; - } else { - donatePreference.setVisible(true); - } - } - - donatePreference.setOnPreferenceClickListener(preference -> { - ShareUtils.openUrl(context, TermuxConstants.TERMUX_DONATE_URL); - return true; - }); - } - } - } - } diff --git a/app/src/main/java/com/termux/app/datastore/termux/DebuggingPreferencesDataStore.java b/app/src/main/java/com/termux/app/datastore/termux/DebuggingPreferencesDataStore.java new file mode 100644 index 0000000000..0a0a9c4079 --- /dev/null +++ b/app/src/main/java/com/termux/app/datastore/termux/DebuggingPreferencesDataStore.java @@ -0,0 +1,90 @@ +package com.termux.app.datastore.termux; + +import android.content.Context; + +import androidx.annotation.Nullable; +import androidx.preference.PreferenceDataStore; + +import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences; + +public class DebuggingPreferencesDataStore extends PreferenceDataStore { + + private final Context mContext; + private final TermuxAppSharedPreferences mPreferences; + + private static DebuggingPreferencesDataStore mInstance; + + private DebuggingPreferencesDataStore(Context context) { + mContext = context; + mPreferences = TermuxAppSharedPreferences.build(context, true); + } + + public static synchronized DebuggingPreferencesDataStore getInstance(Context context) { + if (mInstance == null) { + mInstance = new DebuggingPreferencesDataStore(context); + } + return mInstance; + } + + + + @Override + @Nullable + public String getString(String key, @Nullable String defValue) { + if (isDisability(key)) return null; + return (key == "log_level") ? String.valueOf(mPreferences.getLogLevel()): null; + } + + private boolean isDisability(String key) { + return (mPreferences == null || key == null) ? true : false; + } + + @Override + public void putString(String key, @Nullable String value) { + if (isDisability(key)) return; + + if ( key == "log_level" ) + _put(value); + } + + private void _put(@Nullable String value) { + if (value != null) mPreferences.setLogLevel(mContext, Integer.parseInt(value)); + } + + + @Override + public void putBoolean(String key, boolean value) { + if (isDisability(key)) return; + + switch (key) { + case "terminal_view_key_logging_enabled": + mPreferences.setTerminalViewKeyLoggingEnabled(value); + break; + case "plugin_error_notifications_enabled": + mPreferences.setPluginErrorNotificationsEnabled(value); + break; + case "crash_report_notifications_enabled": + mPreferences.setCrashReportNotificationsEnabled(value); + break; + default: + break; + } + + } + + @Override + public boolean getBoolean(String key, boolean defValue) { + if (isDisability(key)) return false; + switch (key) { + case "terminal_view_key_logging_enabled": + return mPreferences.isTerminalViewKeyLoggingEnabled(); + case "plugin_error_notifications_enabled": + return mPreferences.arePluginErrorNotificationsEnabled(false); + case "crash_report_notifications_enabled": + return mPreferences.areCrashReportNotificationsEnabled(false); + default: + return false; + } + } + +} diff --git a/app/src/main/java/com/termux/app/datastore/termux/TerminalIOPreferencesDataStore.java b/app/src/main/java/com/termux/app/datastore/termux/TerminalIOPreferencesDataStore.java new file mode 100644 index 0000000000..13d154e376 --- /dev/null +++ b/app/src/main/java/com/termux/app/datastore/termux/TerminalIOPreferencesDataStore.java @@ -0,0 +1,63 @@ +package com.termux.app.datastore.termux; + +import android.content.Context; + +import androidx.preference.PreferenceDataStore; + +import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences; + +public class TerminalIOPreferencesDataStore extends PreferenceDataStore { + + private final Context mContext; + private final TermuxAppSharedPreferences mPreferences; + + private static TerminalIOPreferencesDataStore mInstance; + + private TerminalIOPreferencesDataStore(Context context) { + mContext = context; + mPreferences = TermuxAppSharedPreferences.build(context, true); + } + + public static synchronized TerminalIOPreferencesDataStore getInstance(Context context) { + if (mInstance == null) { + mInstance = new TerminalIOPreferencesDataStore(context); + } + return mInstance; + } + + + + @Override + public void putBoolean(String key, boolean value) { + if (isDisability(key)) return; + switch (key) { + case "soft_keyboard_enabled": + mPreferences.setSoftKeyboardEnabled(value); + break; + case "soft_keyboard_enabled_only_if_no_hardware": + mPreferences.setSoftKeyboardEnabledOnlyIfNoHardware(value); + break; + default: + break; + } + } + + private boolean isDisability(String key) { + return (mPreferences == null || key == null) ? true : false; + } + + @Override + public boolean getBoolean(String key, boolean defValue) { + if (mPreferences == null) return false; + + switch (key) { + case "soft_keyboard_enabled": + return mPreferences.isSoftKeyboardEnabled(); + case "soft_keyboard_enabled_only_if_no_hardware": + return mPreferences.isSoftKeyboardEnabledOnlyIfNoHardware(); + default: + return false; + } + } + +} diff --git a/app/src/main/java/com/termux/app/datastore/termux/TerminalViewPreferencesDataStore.java b/app/src/main/java/com/termux/app/datastore/termux/TerminalViewPreferencesDataStore.java new file mode 100644 index 0000000000..7f7709551f --- /dev/null +++ b/app/src/main/java/com/termux/app/datastore/termux/TerminalViewPreferencesDataStore.java @@ -0,0 +1,52 @@ +package com.termux.app.datastore.termux; + +import android.content.Context; + +import androidx.preference.PreferenceDataStore; + +import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences; + +public class TerminalViewPreferencesDataStore extends PreferenceDataStore { + + private final Context mContext; + private final TermuxAppSharedPreferences mPreferences; + + private static TerminalViewPreferencesDataStore mInstance; + + private TerminalViewPreferencesDataStore(Context context) { + mContext = context; + mPreferences = TermuxAppSharedPreferences.build(context, true); + } + + public static synchronized TerminalViewPreferencesDataStore getInstance(Context context) { + if (mInstance == null) { + mInstance = new TerminalViewPreferencesDataStore(context); + } + return mInstance; + } + + + + @Override + public void putBoolean(String key, boolean value) { + if (isDisability(key)) return; + + if(key == "terminal_margin_adjustment") + mPreferences.setTerminalMarginAdjustment(value); + + } + + private boolean isDisability(String key) { + return (mPreferences == null || key == null) ? true : false; + } + + @Override + public boolean getBoolean(String key, boolean defValue) { + if (mPreferences == null) return false; + return(key == "terminal_margin_adjustment")? mPreferences.isTerminalMarginAdjustmentEnabled(): false; + + } + } + + + diff --git a/app/src/main/java/com/termux/app/datastore/termux_api/DebuggingPreferencesDataStore.java b/app/src/main/java/com/termux/app/datastore/termux_api/DebuggingPreferencesDataStore.java new file mode 100644 index 0000000000..84b9188df0 --- /dev/null +++ b/app/src/main/java/com/termux/app/datastore/termux_api/DebuggingPreferencesDataStore.java @@ -0,0 +1,55 @@ +package com.termux.app.datastore.termux_api; + +import android.content.Context; + +import androidx.annotation.Nullable; +import androidx.preference.PreferenceDataStore; + +import com.termux.shared.termux.settings.preferences.TermuxAPIAppSharedPreferences; + +public class DebuggingPreferencesDataStore extends PreferenceDataStore { + + private final Context mContext; + private final TermuxAPIAppSharedPreferences mPreferences; + + private static DebuggingPreferencesDataStore mInstance; + + private DebuggingPreferencesDataStore(Context context) { + mContext = context; + mPreferences = TermuxAPIAppSharedPreferences.build(context, true); + } + + public static synchronized DebuggingPreferencesDataStore getInstance(Context context) { + if (mInstance == null) { + mInstance = new DebuggingPreferencesDataStore(context); + } + return mInstance; + } + + + + @Override + @Nullable + public String getString(String key, @Nullable String defValue) { + if (isDisability(key)) return null; + return (key =="log_level") ? String.valueOf(mPreferences.getLogLevel(true)): null; + } + + private boolean isDisability(String key) { + return (mPreferences == null || key == null) ? true : false; + } + + @Override + public void putString(String key, @Nullable String value) { + if (isDisability(key)) return; + if (key =="log_level") + _put(value); + + } + + private void _put(@Nullable String value) { + if (value != null) { + mPreferences.setLogLevel(mContext, Integer.parseInt(value), true); + } + } +} diff --git a/app/src/main/java/com/termux/app/datastore/termux_float/DebuggingPreferencesDataStore.java b/app/src/main/java/com/termux/app/datastore/termux_float/DebuggingPreferencesDataStore.java new file mode 100644 index 0000000000..cc998d7b93 --- /dev/null +++ b/app/src/main/java/com/termux/app/datastore/termux_float/DebuggingPreferencesDataStore.java @@ -0,0 +1,75 @@ +package com.termux.app.datastore.termux_float; + +import android.content.Context; + +import androidx.annotation.Nullable; +import androidx.preference.PreferenceDataStore; + +import com.termux.shared.termux.settings.preferences.TermuxFloatAppSharedPreferences; + +public class DebuggingPreferencesDataStore extends PreferenceDataStore { + + private final Context mContext; + private final TermuxFloatAppSharedPreferences mPreferences; + + private static DebuggingPreferencesDataStore mInstance; + + private DebuggingPreferencesDataStore(Context context) { + mContext = context; + mPreferences = TermuxFloatAppSharedPreferences.build(context, true); + } + + public static synchronized DebuggingPreferencesDataStore getInstance(Context context) { + if (mInstance == null) { + mInstance = new DebuggingPreferencesDataStore(context); + } + return mInstance; + } + + + + @Override + @Nullable + public String getString(String key, @Nullable String defValue) { + if (isDisability(key)) return null; + return (key == "log_level")? String.valueOf(mPreferences.getLogLevel(true)) : null; + + } + + @Override + public void putString(String key, @Nullable String value) { + if (isDisability(key)) return; + if (key == "log_level") { + _put(value); + + } + } + + private void _put(@Nullable String value) { + if (value != null) { + mPreferences.setLogLevel(mContext, Integer.parseInt(value), true); + } + } + + private boolean isDisability(String key) { + return (mPreferences == null || key == null) ? true : false; + + } + + @Override + public void putBoolean(String key, boolean value) { + if (isDisability(key)) return; + + if(key =="terminal_view_key_logging_enabled") + mPreferences.setTerminalViewKeyLoggingEnabled(value, true); + + } + + @Override + public boolean getBoolean(String key, boolean defValue) { + if (mPreferences == null) return false; + return (key =="terminal_view_key_logging_enabled") ? mPreferences.isTerminalViewKeyLoggingEnabled(true):false; + + } + +} diff --git a/app/src/main/java/com/termux/app/datastore/termux_tasker/DebuggingPreferencesDataStore.java b/app/src/main/java/com/termux/app/datastore/termux_tasker/DebuggingPreferencesDataStore.java new file mode 100644 index 0000000000..453a115341 --- /dev/null +++ b/app/src/main/java/com/termux/app/datastore/termux_tasker/DebuggingPreferencesDataStore.java @@ -0,0 +1,56 @@ +package com.termux.app.datastore.termux_tasker; + +import android.content.Context; + +import androidx.annotation.Nullable; +import androidx.preference.PreferenceDataStore; + +import com.termux.shared.termux.settings.preferences.TermuxTaskerAppSharedPreferences; + +public class DebuggingPreferencesDataStore extends PreferenceDataStore { + + private final Context mContext; + private final TermuxTaskerAppSharedPreferences mPreferences; + + private static DebuggingPreferencesDataStore mInstance; + + private DebuggingPreferencesDataStore(Context context) { + mContext = context; + mPreferences = TermuxTaskerAppSharedPreferences.build(context, true); + } + + public static synchronized DebuggingPreferencesDataStore getInstance(Context context) { + if (mInstance == null) { + mInstance = new DebuggingPreferencesDataStore(context); + } + return mInstance; + } + + + + @Override + @Nullable + public String getString(String key, @Nullable String defValue) { + if (isDisability(key)) return null; + return (key =="log_level") ? String.valueOf(mPreferences.getLogLevel(true)) : null; + } + + @Override + public void putString(String key, @Nullable String value) { + if (isDisability(key)) return; + if(key == "log_level") { + _put(value); + } + } + + private void _put(@Nullable String value) { + if (value != null) { + mPreferences.setLogLevel(mContext, Integer.parseInt(value), true); + } + } + + private boolean isDisability(String key) { + return (mPreferences == null || key == null) ? true : false; + } + +} diff --git a/app/src/main/java/com/termux/app/datastore/termux_widget/DebuggingPreferencesDataStore.java b/app/src/main/java/com/termux/app/datastore/termux_widget/DebuggingPreferencesDataStore.java new file mode 100644 index 0000000000..3d98efe066 --- /dev/null +++ b/app/src/main/java/com/termux/app/datastore/termux_widget/DebuggingPreferencesDataStore.java @@ -0,0 +1,55 @@ +package com.termux.app.datastore.termux_widget; + +import android.content.Context; + +import androidx.annotation.Nullable; +import androidx.preference.PreferenceDataStore; + +import com.termux.shared.termux.settings.preferences.TermuxWidgetAppSharedPreferences; + +public class DebuggingPreferencesDataStore extends PreferenceDataStore { + + private final Context mContext; + private final TermuxWidgetAppSharedPreferences mPreferences; + + private static DebuggingPreferencesDataStore mInstance; + + private DebuggingPreferencesDataStore(Context context) { + mContext = context; + mPreferences = TermuxWidgetAppSharedPreferences.build(context, true); + } + + public static synchronized DebuggingPreferencesDataStore getInstance(Context context) { + if (mInstance == null) { + mInstance = new DebuggingPreferencesDataStore(context); + } + return mInstance; + } + + + + @Override + @Nullable + public String getString(String key, @Nullable String defValue) { + if (isDisability(key)) return null; + return (key == "log_level") ? String.valueOf(mPreferences.getLogLevel(true)): null; + } + + private boolean isDisability(String key) { + return (mPreferences == null || key == null) ? true : false; + } + + @Override + public void putString(String key, @Nullable String value) { + if (isDisability(key)) return; + if (key == "log_level") { + _put(value); + } + } + + private void _put(@Nullable String value) { + if (value != null) { + mPreferences.setLogLevel(mContext, Integer.parseInt(value), true); } + } + +} diff --git a/app/src/main/java/com/termux/app/fragments/settings/RootPreferencesFragment.java b/app/src/main/java/com/termux/app/fragments/settings/RootPreferencesFragment.java new file mode 100644 index 0000000000..c806b39681 --- /dev/null +++ b/app/src/main/java/com/termux/app/fragments/settings/RootPreferencesFragment.java @@ -0,0 +1,138 @@ +package com.termux.app.fragments.settings; + +import android.content.Context; +import android.os.Bundle; +import android.os.Environment; + +import androidx.annotation.NonNull; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; + +import com.termux.R; +import com.termux.app.models.UserAction; +import com.termux.shared.activities.ReportActivity; +import com.termux.shared.android.AndroidUtils; +import com.termux.shared.android.PackageUtils; +import com.termux.shared.file.FileUtils; +import com.termux.shared.interact.ShareUtils; +import com.termux.shared.models.ReportInfo; +import com.termux.shared.termux.TermuxConstants; +import com.termux.shared.termux.TermuxUtils; +import com.termux.shared.termux.settings.preferences.TermuxAPIAppSharedPreferences; +import com.termux.shared.termux.settings.preferences.TermuxFloatAppSharedPreferences; +import com.termux.shared.termux.settings.preferences.TermuxTaskerAppSharedPreferences; +import com.termux.shared.termux.settings.preferences.TermuxWidgetAppSharedPreferences; + +public class RootPreferencesFragment extends PreferenceFragmentCompat { + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + Context context = getContext(); + if (context == null) return; + + setPreferencesFromResource(R.xml.root_preferences, rootKey); + + new configureTermuxAPIPreference(); + new Thread() { + @Override + public void run() { + configureTermuxFloatPreference(context); + configureTermuxTaskerPreference(context); + configureTermuxWidgetPreference(context); + configureAboutPreference(context); + configureDonatePreference(context); + } + }.start(); + } + +// private void configureTermuxAPIPreference(@NonNull Context context) { +// Preference termuxAPIPreference = findPreference("termux_api"); +// if (termuxAPIPreference != null) { +// TermuxAPIAppSharedPreferences preferences = TermuxAPIAppSharedPreferences.build(context, false); +// // If failed to get app preferences, then likely app is not installed, so do not show its preference +// termuxAPIPreference.setVisible(preferences != null); +// } +// } + + private void configureTermuxFloatPreference(@NonNull Context context) { + Preference termuxFloatPreference = findPreference("termux_float"); + if (termuxFloatPreference != null) { + TermuxFloatAppSharedPreferences preferences = TermuxFloatAppSharedPreferences.build(context, false); + // If failed to get app preferences, then likely app is not installed, so do not show its preference + termuxFloatPreference.setVisible(preferences != null); + } + } + + private void configureTermuxTaskerPreference(@NonNull Context context) { + Preference termuxTaskerPreference = findPreference("termux_tasker"); + if (termuxTaskerPreference != null) { + TermuxTaskerAppSharedPreferences preferences = TermuxTaskerAppSharedPreferences.build(context, false); + // If failed to get app preferences, then likely app is not installed, so do not show its preference + termuxTaskerPreference.setVisible(preferences != null); + } + } + + private void configureTermuxWidgetPreference(@NonNull Context context) { + Preference termuxWidgetPreference = findPreference("termux_widget"); + if (termuxWidgetPreference != null) { + TermuxWidgetAppSharedPreferences preferences = TermuxWidgetAppSharedPreferences.build(context, false); + // If failed to get app preferences, then likely app is not installed, so do not show its preference + termuxWidgetPreference.setVisible(preferences != null); + } + } + + private void configureAboutPreference(@NonNull Context context) { + Preference aboutPreference = findPreference("about"); + if (aboutPreference != null) { + aboutPreference.setOnPreferenceClickListener(preference -> { + new Thread() { + @Override + public void run() { + String title = "About"; + + StringBuilder aboutString = new StringBuilder(); + aboutString.append(TermuxUtils.getAppInfoMarkdownString(context, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGES)); + aboutString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context)); + aboutString.append("\n\n").append(TermuxUtils.getImportantLinksMarkdownString(context)); + + String userActionName = UserAction.ABOUT.getName(); + + ReportInfo reportInfo = new ReportInfo(userActionName, + TermuxConstants.TERMUX_APP.TERMUX_SETTINGS_ACTIVITY_NAME, title); + reportInfo.setReportString(aboutString.toString()); + reportInfo.setReportSaveFileLabelAndPath(userActionName, + Environment.getExternalStorageDirectory() + "/" + + FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true)); + + ReportActivity.startReportActivity(context, reportInfo); + } + }.start(); + + return true; + }); + } + } + + private void configureDonatePreference(@NonNull Context context) { + Preference donatePreference = findPreference("donate"); + if (donatePreference != null) { + String signingCertificateSHA256Digest = PackageUtils.getSigningCertificateSHA256DigestForPackage(context); + if (signingCertificateSHA256Digest != null) { + // If APK is a Google Playstore release, then do not show the donation link + // since Termux isn't exempted from the playstore policy donation links restriction + // Check Fund solicitations: https://pay.google.com/intl/en_in/about/policy/ + String apkRelease = TermuxUtils.getAPKRelease(signingCertificateSHA256Digest); + if (apkRelease == null || apkRelease.equals(TermuxConstants.APK_RELEASE_GOOGLE_PLAYSTORE_SIGNING_CERTIFICATE_SHA256_DIGEST)) { + donatePreference.setVisible(false); + return; + } else { + donatePreference.setVisible(true); + } + } + + donatePreference.setOnPreferenceClickListener(preference -> { + ShareUtils.openUrl(context, TermuxConstants.TERMUX_DONATE_URL); + return true; + }); + } + } +} diff --git a/app/src/main/java/com/termux/app/fragments/settings/configureTermuxAPIPreference.java b/app/src/main/java/com/termux/app/fragments/settings/configureTermuxAPIPreference.java new file mode 100644 index 0000000000..2ade5f35b6 --- /dev/null +++ b/app/src/main/java/com/termux/app/fragments/settings/configureTermuxAPIPreference.java @@ -0,0 +1,43 @@ +package com.termux.app.fragments.settings; + +import android.content.Context; +import android.os.Bundle; + +import androidx.preference.Preference; + +import com.termux.R; +import com.termux.shared.termux.settings.preferences.TermuxAPIAppSharedPreferences; + +public class configureTermuxAPIPreference extends configureTermuxPreference{ + private Preference termuxAPIPreference; + + @Override + Preference findPreferenceMethod() { + return termuxAPIPreference = findPreference("termux_api"); + } + + @Override + void build(Context context, Preference termuxPreference) { + if (termuxAPIPreference != null) { + TermuxAPIAppSharedPreferences preferences = TermuxAPIAppSharedPreferences.build(context, false); + termuxAPIPreference.setVisible(preferences != null); + } + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + setPreferencesFromResource(R.xml.root_preferences, rootKey); + Context context = getContext(); + generate(context); + new Thread(new Runnable() { + @Override + public void run() { + // TODO Auto-generated method stub + generate(context); + } + }).start(); + + } + + +} diff --git a/app/src/main/java/com/termux/app/fragments/settings/configureTermuxPreference.java b/app/src/main/java/com/termux/app/fragments/settings/configureTermuxPreference.java new file mode 100644 index 0000000000..995d873cfe --- /dev/null +++ b/app/src/main/java/com/termux/app/fragments/settings/configureTermuxPreference.java @@ -0,0 +1,16 @@ +package com.termux.app.fragments.settings; + +import android.content.Context; + +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; + +public abstract class configureTermuxPreference extends PreferenceFragmentCompat { + abstract Preference findPreferenceMethod(); + abstract void build(Context context, Preference termuxPreference ); + final public void generate(Context context) { + findPreferenceMethod(); + build(context, findPreferenceMethod()); + } + +} diff --git a/app/src/main/java/com/termux/app/fragments/settings/termux/DebuggingPreferencesFragment.java b/app/src/main/java/com/termux/app/fragments/settings/termux/DebuggingPreferencesFragment.java index 8afd568fdb..aba1b1f971 100644 --- a/app/src/main/java/com/termux/app/fragments/settings/termux/DebuggingPreferencesFragment.java +++ b/app/src/main/java/com/termux/app/fragments/settings/termux/DebuggingPreferencesFragment.java @@ -13,6 +13,7 @@ import androidx.preference.PreferenceManager; import com.termux.R; +import com.termux.app.datastore.termux.DebuggingPreferencesDataStore; import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences; import com.termux.shared.logger.Logger; @@ -47,8 +48,7 @@ private void configureLoggingPreferences(@NonNull Context context) { } public static ListPreference setLogLevelListPreferenceData(ListPreference logLevelListPreference, Context context, int logLevel) { - if (logLevelListPreference == null) - logLevelListPreference = new ListPreference(context); + logLevelListPreference = getLevelListPreference(logLevelListPreference, context); CharSequence[] logLevels = Logger.getLogLevelsArray(); CharSequence[] logLevelLabels = Logger.getLogLevelLabelsArray(context, logLevels, true); @@ -62,94 +62,13 @@ public static ListPreference setLogLevelListPreferenceData(ListPreference logLev return logLevelListPreference; } -} - -class DebuggingPreferencesDataStore extends PreferenceDataStore { - - private final Context mContext; - private final TermuxAppSharedPreferences mPreferences; - - private static DebuggingPreferencesDataStore mInstance; - - private DebuggingPreferencesDataStore(Context context) { - mContext = context; - mPreferences = TermuxAppSharedPreferences.build(context, true); - } - - public static synchronized DebuggingPreferencesDataStore getInstance(Context context) { - if (mInstance == null) { - mInstance = new DebuggingPreferencesDataStore(context); - } - return mInstance; - } - - - - @Override - @Nullable - public String getString(String key, @Nullable String defValue) { - if (mPreferences == null) return null; - 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 (mPreferences == null) return; - if (key == null) return; - - switch (key) { - case "log_level": - if (value != null) { - mPreferences.setLogLevel(mContext, Integer.parseInt(value)); - } - break; - default: - break; - } + @NonNull + private static ListPreference getLevelListPreference(ListPreference logLevelListPreference, Context context) { + if (logLevelListPreference == null) + logLevelListPreference = new ListPreference(context); + return logLevelListPreference; } +} - @Override - public void putBoolean(String key, boolean value) { - if (mPreferences == null) return; - if (key == null) return; - - switch (key) { - case "terminal_view_key_logging_enabled": - mPreferences.setTerminalViewKeyLoggingEnabled(value); - break; - case "plugin_error_notifications_enabled": - mPreferences.setPluginErrorNotificationsEnabled(value); - break; - case "crash_report_notifications_enabled": - mPreferences.setCrashReportNotificationsEnabled(value); - break; - default: - break; - } - } - - @Override - public boolean getBoolean(String key, boolean defValue) { - if (mPreferences == null) return false; - switch (key) { - case "terminal_view_key_logging_enabled": - return mPreferences.isTerminalViewKeyLoggingEnabled(); - case "plugin_error_notifications_enabled": - return mPreferences.arePluginErrorNotificationsEnabled(false); - case "crash_report_notifications_enabled": - return mPreferences.areCrashReportNotificationsEnabled(false); - default: - return false; - } - } - -} diff --git a/app/src/main/java/com/termux/app/fragments/settings/termux/TerminalIOPreferencesFragment.java b/app/src/main/java/com/termux/app/fragments/settings/termux/TerminalIOPreferencesFragment.java index f8504f43fd..44719d8e63 100644 --- a/app/src/main/java/com/termux/app/fragments/settings/termux/TerminalIOPreferencesFragment.java +++ b/app/src/main/java/com/termux/app/fragments/settings/termux/TerminalIOPreferencesFragment.java @@ -4,12 +4,11 @@ import android.os.Bundle; import androidx.annotation.Keep; -import androidx.preference.PreferenceDataStore; import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceManager; import com.termux.R; -import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences; +import com.termux.app.datastore.termux.TerminalIOPreferencesDataStore; @Keep public class TerminalIOPreferencesFragment extends PreferenceFragmentCompat { @@ -26,57 +25,3 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { } } - -class TerminalIOPreferencesDataStore extends PreferenceDataStore { - - private final Context mContext; - private final TermuxAppSharedPreferences mPreferences; - - private static TerminalIOPreferencesDataStore mInstance; - - private TerminalIOPreferencesDataStore(Context context) { - mContext = context; - mPreferences = TermuxAppSharedPreferences.build(context, true); - } - - public static synchronized TerminalIOPreferencesDataStore getInstance(Context context) { - if (mInstance == null) { - mInstance = new TerminalIOPreferencesDataStore(context); - } - return mInstance; - } - - - - @Override - public void putBoolean(String key, boolean value) { - if (mPreferences == null) return; - if (key == null) return; - - switch (key) { - case "soft_keyboard_enabled": - mPreferences.setSoftKeyboardEnabled(value); - break; - case "soft_keyboard_enabled_only_if_no_hardware": - mPreferences.setSoftKeyboardEnabledOnlyIfNoHardware(value); - break; - default: - break; - } - } - - @Override - public boolean getBoolean(String key, boolean defValue) { - if (mPreferences == null) return false; - - switch (key) { - case "soft_keyboard_enabled": - return mPreferences.isSoftKeyboardEnabled(); - case "soft_keyboard_enabled_only_if_no_hardware": - return mPreferences.isSoftKeyboardEnabledOnlyIfNoHardware(); - default: - return false; - } - } - -} diff --git a/app/src/main/java/com/termux/app/fragments/settings/termux/TerminalViewPreferencesFragment.java b/app/src/main/java/com/termux/app/fragments/settings/termux/TerminalViewPreferencesFragment.java index ff033fd369..941b74b7fe 100644 --- a/app/src/main/java/com/termux/app/fragments/settings/termux/TerminalViewPreferencesFragment.java +++ b/app/src/main/java/com/termux/app/fragments/settings/termux/TerminalViewPreferencesFragment.java @@ -9,6 +9,7 @@ import androidx.preference.PreferenceManager; import com.termux.R; +import com.termux.app.datastore.termux.TerminalViewPreferencesDataStore; import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences; @Keep @@ -27,51 +28,3 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { } -class TerminalViewPreferencesDataStore extends PreferenceDataStore { - - private final Context mContext; - private final TermuxAppSharedPreferences mPreferences; - - private static TerminalViewPreferencesDataStore mInstance; - - private TerminalViewPreferencesDataStore(Context context) { - mContext = context; - mPreferences = TermuxAppSharedPreferences.build(context, true); - } - - public static synchronized TerminalViewPreferencesDataStore getInstance(Context context) { - if (mInstance == null) { - mInstance = new TerminalViewPreferencesDataStore(context); - } - return mInstance; - } - - - - @Override - public void putBoolean(String key, boolean value) { - if (mPreferences == null) return; - if (key == null) return; - - switch (key) { - case "terminal_margin_adjustment": - mPreferences.setTerminalMarginAdjustment(value); - break; - default: - break; - } - } - - @Override - public boolean getBoolean(String key, boolean defValue) { - if (mPreferences == null) return false; - - switch (key) { - case "terminal_margin_adjustment": - return mPreferences.isTerminalMarginAdjustmentEnabled(); - default: - return false; - } - } - -} diff --git a/app/src/main/java/com/termux/app/fragments/settings/termux_api/DebuggingPreferencesFragment.java b/app/src/main/java/com/termux/app/fragments/settings/termux_api/DebuggingPreferencesFragment.java index 908c6ebd89..7c8030d026 100644 --- a/app/src/main/java/com/termux/app/fragments/settings/termux_api/DebuggingPreferencesFragment.java +++ b/app/src/main/java/com/termux/app/fragments/settings/termux_api/DebuggingPreferencesFragment.java @@ -13,6 +13,7 @@ import androidx.preference.PreferenceManager; import com.termux.R; +import com.termux.app.datastore.termux_api.DebuggingPreferencesDataStore; import com.termux.shared.termux.settings.preferences.TermuxAPIAppSharedPreferences; @Keep @@ -46,56 +47,3 @@ private void configureLoggingPreferences(@NonNull Context context) { } } } - -class DebuggingPreferencesDataStore extends PreferenceDataStore { - - private final Context mContext; - private final TermuxAPIAppSharedPreferences mPreferences; - - private static DebuggingPreferencesDataStore mInstance; - - private DebuggingPreferencesDataStore(Context context) { - mContext = context; - mPreferences = TermuxAPIAppSharedPreferences.build(context, true); - } - - public static synchronized DebuggingPreferencesDataStore getInstance(Context context) { - if (mInstance == null) { - mInstance = new DebuggingPreferencesDataStore(context); - } - return mInstance; - } - - - - @Override - @Nullable - public String getString(String key, @Nullable String defValue) { - if (mPreferences == null) return null; - if (key == null) return null; - - switch (key) { - case "log_level": - return String.valueOf(mPreferences.getLogLevel(true)); - default: - return null; - } - } - - @Override - public void putString(String key, @Nullable String value) { - if (mPreferences == null) return; - if (key == null) return; - - switch (key) { - case "log_level": - if (value != null) { - mPreferences.setLogLevel(mContext, Integer.parseInt(value), true); - } - break; - default: - break; - } - } - -} diff --git a/app/src/main/java/com/termux/app/fragments/settings/termux_float/DebuggingPreferencesFragment.java b/app/src/main/java/com/termux/app/fragments/settings/termux_float/DebuggingPreferencesFragment.java index 1d815aa09a..ddab05fd49 100644 --- a/app/src/main/java/com/termux/app/fragments/settings/termux_float/DebuggingPreferencesFragment.java +++ b/app/src/main/java/com/termux/app/fragments/settings/termux_float/DebuggingPreferencesFragment.java @@ -13,6 +13,7 @@ import androidx.preference.PreferenceManager; import com.termux.R; +import com.termux.app.datastore.termux_float.DebuggingPreferencesDataStore; import com.termux.shared.termux.settings.preferences.TermuxFloatAppSharedPreferences; @Keep @@ -47,80 +48,3 @@ private void configureLoggingPreferences(@NonNull Context context) { } } -class DebuggingPreferencesDataStore extends PreferenceDataStore { - - private final Context mContext; - private final TermuxFloatAppSharedPreferences mPreferences; - - private static DebuggingPreferencesDataStore mInstance; - - private DebuggingPreferencesDataStore(Context context) { - mContext = context; - mPreferences = TermuxFloatAppSharedPreferences.build(context, true); - } - - public static synchronized DebuggingPreferencesDataStore getInstance(Context context) { - if (mInstance == null) { - mInstance = new DebuggingPreferencesDataStore(context); - } - return mInstance; - } - - - - @Override - @Nullable - public String getString(String key, @Nullable String defValue) { - if (mPreferences == null) return null; - if (key == null) return null; - - switch (key) { - case "log_level": - return String.valueOf(mPreferences.getLogLevel(true)); - default: - return null; - } - } - - @Override - public void putString(String key, @Nullable String value) { - if (mPreferences == null) return; - if (key == null) return; - - switch (key) { - case "log_level": - if (value != null) { - mPreferences.setLogLevel(mContext, Integer.parseInt(value), true); - } - break; - default: - break; - } - } - - @Override - public void putBoolean(String key, boolean value) { - if (mPreferences == null) return; - if (key == null) return; - - switch (key) { - case "terminal_view_key_logging_enabled": - mPreferences.setTerminalViewKeyLoggingEnabled(value, true); - break; - default: - break; - } - } - - @Override - public boolean getBoolean(String key, boolean defValue) { - if (mPreferences == null) return false; - switch (key) { - case "terminal_view_key_logging_enabled": - return mPreferences.isTerminalViewKeyLoggingEnabled(true); - default: - return false; - } - } - -} diff --git a/app/src/main/java/com/termux/app/fragments/settings/termux_tasker/DebuggingPreferencesFragment.java b/app/src/main/java/com/termux/app/fragments/settings/termux_tasker/DebuggingPreferencesFragment.java index 7e2918392f..46c2a6fb08 100644 --- a/app/src/main/java/com/termux/app/fragments/settings/termux_tasker/DebuggingPreferencesFragment.java +++ b/app/src/main/java/com/termux/app/fragments/settings/termux_tasker/DebuggingPreferencesFragment.java @@ -13,6 +13,7 @@ import androidx.preference.PreferenceManager; import com.termux.R; +import com.termux.app.datastore.termux_tasker.DebuggingPreferencesDataStore; import com.termux.shared.termux.settings.preferences.TermuxTaskerAppSharedPreferences; @Keep @@ -47,55 +48,3 @@ private void configureLoggingPreferences(@NonNull Context context) { } } -class DebuggingPreferencesDataStore extends PreferenceDataStore { - - private final Context mContext; - private final TermuxTaskerAppSharedPreferences mPreferences; - - private static DebuggingPreferencesDataStore mInstance; - - private DebuggingPreferencesDataStore(Context context) { - mContext = context; - mPreferences = TermuxTaskerAppSharedPreferences.build(context, true); - } - - public static synchronized DebuggingPreferencesDataStore getInstance(Context context) { - if (mInstance == null) { - mInstance = new DebuggingPreferencesDataStore(context); - } - return mInstance; - } - - - - @Override - @Nullable - public String getString(String key, @Nullable String defValue) { - if (mPreferences == null) return null; - if (key == null) return null; - - switch (key) { - case "log_level": - return String.valueOf(mPreferences.getLogLevel(true)); - default: - return null; - } - } - - @Override - public void putString(String key, @Nullable String value) { - if (mPreferences == null) return; - if (key == null) return; - - switch (key) { - case "log_level": - if (value != null) { - mPreferences.setLogLevel(mContext, Integer.parseInt(value), true); - } - break; - default: - break; - } - } - -} diff --git a/app/src/main/java/com/termux/app/fragments/settings/termux_widget/DebuggingPreferencesFragment.java b/app/src/main/java/com/termux/app/fragments/settings/termux_widget/DebuggingPreferencesFragment.java index 50823d540e..dcd13c5d6c 100644 --- a/app/src/main/java/com/termux/app/fragments/settings/termux_widget/DebuggingPreferencesFragment.java +++ b/app/src/main/java/com/termux/app/fragments/settings/termux_widget/DebuggingPreferencesFragment.java @@ -13,6 +13,7 @@ import androidx.preference.PreferenceManager; import com.termux.R; +import com.termux.app.datastore.termux_widget.DebuggingPreferencesDataStore; import com.termux.shared.termux.settings.preferences.TermuxWidgetAppSharedPreferences; @Keep @@ -47,55 +48,3 @@ private void configureLoggingPreferences(@NonNull Context context) { } } -class DebuggingPreferencesDataStore extends PreferenceDataStore { - - private final Context mContext; - private final TermuxWidgetAppSharedPreferences mPreferences; - - private static DebuggingPreferencesDataStore mInstance; - - private DebuggingPreferencesDataStore(Context context) { - mContext = context; - mPreferences = TermuxWidgetAppSharedPreferences.build(context, true); - } - - public static synchronized DebuggingPreferencesDataStore getInstance(Context context) { - if (mInstance == null) { - mInstance = new DebuggingPreferencesDataStore(context); - } - return mInstance; - } - - - - @Override - @Nullable - public String getString(String key, @Nullable String defValue) { - if (mPreferences == null) return null; - if (key == null) return null; - - switch (key) { - case "log_level": - return String.valueOf(mPreferences.getLogLevel(true)); - default: - return null; - } - } - - @Override - public void putString(String key, @Nullable String value) { - if (mPreferences == null) return; - if (key == null) return; - - switch (key) { - case "log_level": - if (value != null) { - mPreferences.setLogLevel(mContext, Integer.parseInt(value), true); - } - break; - default: - break; - } - } - -} diff --git a/app/src/main/java/com/termux/app/terminal/TermuxActivityRootView.java b/app/src/main/java/com/termux/app/terminal/TermuxActivityRootView.java index 7e8b0e9d38..2fb1da847e 100644 --- a/app/src/main/java/com/termux/app/terminal/TermuxActivityRootView.java +++ b/app/src/main/java/com/termux/app/terminal/TermuxActivityRootView.java @@ -120,40 +120,35 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { public void onGlobalLayout() { if (mActivity == null || !mActivity.isVisible()) return; - View bottomSpaceView = mActivity.getTermuxActivityBottomSpaceView(); - if (bottomSpaceView == null) return; - - boolean root_view_logging_enabled = ROOT_VIEW_LOGGING_ENABLED; - - if (root_view_logging_enabled) - Logger.logVerbose(LOG_TAG, ":\nonGlobalLayout:"); - FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) getLayoutParams(); - // Get the position Rects of the bottom space view and the main window holding it - Rect[] windowAndViewRects = ViewUtils.getWindowAndViewRects(bottomSpaceView, mStatusBarHeight); - if (windowAndViewRects == null) - return; + Rect[] windowAndViewRects = getwindowAndViewRects(); + if (windowAndViewRects == null) return; Rect windowAvailableRect = windowAndViewRects[0]; Rect bottomSpaceViewRect = windowAndViewRects[1]; // If the bottomSpaceViewRect is inside the windowAvailableRect, then it must be completely visible - //boolean isVisible = windowAvailableRect.contains(bottomSpaceViewRect); // rect.right comparison often fails in landscape - boolean isVisible = ViewUtils.isRectAbove(windowAvailableRect, bottomSpaceViewRect); - boolean isVisibleBecauseMargin = (windowAvailableRect.bottom == bottomSpaceViewRect.bottom) && params.bottomMargin > 0; + + int bottomMargin = params.bottomMargin; + boolean isVisibleBecauseMargin = (windowAvailableRect.bottom == bottomSpaceViewRect.bottom) && bottomMargin > 0; boolean isVisibleBecauseExtraMargin = ((bottomSpaceViewRect.bottom - windowAvailableRect.bottom) < 0); + boolean root_view_logging_enabled = ROOT_VIEW_LOGGING_ENABLED; + if (root_view_logging_enabled) { + Logger.logVerbose(LOG_TAG, ":\nonGlobalLayout:"); Logger.logVerbose(LOG_TAG, "windowAvailableRect " + ViewUtils.toRectString(windowAvailableRect) + ", bottomSpaceViewRect " + ViewUtils.toRectString(bottomSpaceViewRect)); Logger.logVerbose(LOG_TAG, "windowAvailableRect.bottom " + windowAvailableRect.bottom + ", bottomSpaceViewRect.bottom " +bottomSpaceViewRect.bottom + - ", diff " + (bottomSpaceViewRect.bottom - windowAvailableRect.bottom) + ", bottom " + params.bottomMargin + + ", diff " + (bottomSpaceViewRect.bottom - windowAvailableRect.bottom) + ", bottom " + bottomMargin + ", isVisible " + windowAvailableRect.contains(bottomSpaceViewRect) + ", isRectAbove " + ViewUtils.isRectAbove(windowAvailableRect, bottomSpaceViewRect) + ", isVisibleBecauseMargin " + isVisibleBecauseMargin + ", isVisibleBecauseExtraMargin " + isVisibleBecauseExtraMargin); } // If the bottomSpaceViewRect is visible, then remove the margin if needed + //boolean isVisible = windowAvailableRect.contains(bottomSpaceViewRect); // rect.right comparison often fails in landscape + boolean isVisible = ViewUtils.isRectAbove(windowAvailableRect, bottomSpaceViewRect); if (isVisible) { // If visible because of margin, i.e the bottom of bottomSpaceViewRect equals that of windowAvailableRect // and a margin has been added @@ -165,111 +160,132 @@ public void onGlobalLayout() { // set appropriate margins when views are changed quickly since some changes // may be missed. if (isVisibleBecauseMargin) { - if (root_view_logging_enabled) - Logger.logVerbose(LOG_TAG, "Visible due to margin"); - - // Once the view has been redrawn with new margin, we set margin back to 0 so that - // when next time onMeasure() is called, margin 0 is used. This is necessary for - // cases when view has been redrawn with new margin because bottom space view was - // hidden by keyboard and then view was redrawn again due to layout change (like - // keyboard symbol view is switched to), android will add margin below its new position - // if its greater than 0, which was already above the keyboard creating x2x margin. - // Adding time check since moving split screen divider in landscape causes jitter - // and prevents some infinite loops - if ((System.currentTimeMillis() - lastMarginBottomTime) > 40) { - lastMarginBottomTime = System.currentTimeMillis(); - marginBottom = 0; - } else { - if (root_view_logging_enabled) - Logger.logVerbose(LOG_TAG, "Ignoring restoring marginBottom to 0 since called to quickly"); - } + addShortRootViewLogging(root_view_logging_enabled, "Visible due to margin"); + checkTime4PreventLoops(root_view_logging_enabled); return; } - boolean setMargin = params.bottomMargin != 0; - - // If visible because of extra margin, i.e the bottom of bottomSpaceViewRect is above that of windowAvailableRect - // onGlobalLayout: windowAvailableRect 1408, bottomSpaceViewRect 1232, diff -176, bottom 0, isVisible true, isVisibleBecauseMargin false, isVisibleBecauseExtraMargin false - // onGlobalLayout: Bottom margin already equals 0 - if (isVisibleBecauseExtraMargin) { - // Adding time check since prevents infinite loops, like in landscape mode in freeform mode in Taskbar - if ((System.currentTimeMillis() - lastMarginBottomExtraTime) > 40) { - if (root_view_logging_enabled) - Logger.logVerbose(LOG_TAG, "Resetting margin since visible due to extra margin"); - lastMarginBottomExtraTime = System.currentTimeMillis(); - // lastMarginBottom must be invalid. May also happen when keyboards are changed. - lastMarginBottom = null; - setMargin = true; - } else { - if (root_view_logging_enabled) - Logger.logVerbose(LOG_TAG, "Ignoring resetting margin since visible due to extra margin since called to quickly"); - } - } - - if (setMargin) { - if (root_view_logging_enabled) - Logger.logVerbose(LOG_TAG, "Setting bottom margin to 0"); - params.setMargins(0, 0, 0, 0); - setLayoutParams(params); - } else { - if (root_view_logging_enabled) - Logger.logVerbose(LOG_TAG, "Bottom margin already equals 0"); - // This is done so that when next time onMeasure() is called, lastMarginBottom is used. - // This is done since we **expect** the keyboard to have same dimensions next time layout - // changes, so best set margin while view is drawn the first time, otherwise it will - // cause a jitter when OnGlobalLayoutListener is called with margin 0 and it sets the - // likely same lastMarginBottom again and requesting a redraw. Hopefully, this logic - // works fine for all cases. - marginBottom = lastMarginBottom; - } + setMargin(params, bottomMargin, isVisibleBecauseExtraMargin, root_view_logging_enabled); } // ELse find the part of the extra keys/terminal that is hidden and add a margin accordingly else { int pxHidden = bottomSpaceViewRect.bottom - windowAvailableRect.bottom; + addShortRootViewLogging(root_view_logging_enabled, "pxHidden " + pxHidden + ", bottom " + bottomMargin); - if (root_view_logging_enabled) - Logger.logVerbose(LOG_TAG, "pxHidden " + pxHidden + ", bottom " + params.bottomMargin); - - boolean setMargin = params.bottomMargin != pxHidden; - - // If invisible despite margin, i.e a margin was added, but the bottom of bottomSpaceViewRect - // is still below that of windowAvailableRect, this will trigger OnGlobalLayoutListener - // again, so that margins are set properly. May happen when toolbar/extra keys is disabled - // and enabled from left drawer, just like case for isVisibleBecauseExtraMargin. - // onMeasure: Setting bottom margin to 176 - // onGlobalLayout: windowAvailableRect 1232, bottomSpaceViewRect 1408, diff 176, bottom 176, isVisible false, isVisibleBecauseMargin false, isVisibleBecauseExtraMargin false - // onGlobalLayout: Bottom margin already equals 176 - if (pxHidden > 0 && params.bottomMargin > 0) { - if (pxHidden != params.bottomMargin) { - if (root_view_logging_enabled) - Logger.logVerbose(LOG_TAG, "Force setting margin to 0 since not visible due to wrong margin"); - pxHidden = 0; - } else { - if (root_view_logging_enabled) - Logger.logVerbose(LOG_TAG, "Force setting margin since not visible despite required margin"); - } - setMargin = true; - } + boolean canSetMargin = (pxHidden > 0 && bottomMargin > 0)? true: (bottomMargin != pxHidden); + + pxHidden = getPxHidden(bottomMargin, root_view_logging_enabled, pxHidden); - if (pxHidden < 0) { - if (root_view_logging_enabled) - Logger.logVerbose(LOG_TAG, "Force setting margin to 0 since new margin is negative"); + + setMargin(params, root_view_logging_enabled, pxHidden, canSetMargin); + } + } + + private void setMargin(FrameLayout.LayoutParams params, boolean root_view_logging_enabled, int pxHidden, boolean canSetMargin) { + if (canSetMargin) { + addShortRootViewLogging(root_view_logging_enabled, "Setting bottom margin to " + pxHidden); + params.setMargins(0, 0, 0, pxHidden); + setLayoutParams(params); + lastMarginBottom = pxHidden; + } else { + addShortRootViewLogging(root_view_logging_enabled, "Bottom margin already equals " + pxHidden); + } + } + + private int getPxHidden(int bottomMargin, boolean root_view_logging_enabled, int pxHidden) { + // If invisible despite margin, i.e a margin was added, but the bottom of bottomSpaceViewRect + // is still below that of windowAvailableRect, this will trigger OnGlobalLayoutListener + // again, so that margins are set properly. May happen when toolbar/extra keys is disabled + // and enabled from left drawer, just like case for isVisibleBecauseExtraMargin. + // onMeasure: Setting bottom margin to 176 + // onGlobalLayout: windowAvailableRect 1232, bottomSpaceViewRect 1408, diff 176, bottom 176, isVisible false, isVisibleBecauseMargin false, isVisibleBecauseExtraMargin false + // onGlobalLayout: Bottom margin already equals 176 + if (pxHidden > 0 && bottomMargin > 0) { + if (pxHidden != bottomMargin) { + addShortRootViewLogging(root_view_logging_enabled, "Force setting margin to 0 since not visible due to wrong margin"); pxHidden = 0; + } else { + addShortRootViewLogging(root_view_logging_enabled, "Force setting margin since not visible despite required margin"); } + } + if (pxHidden < 0) { + addShortRootViewLogging(root_view_logging_enabled, "Force setting margin to 0 since new margin is negative"); + pxHidden = 0; + } + return pxHidden; + } + + private void setMargin(FrameLayout.LayoutParams params, int bottomMargin, boolean isVisibleBecauseExtraMargin, boolean root_view_logging_enabled) { + if (canSetMargin(bottomMargin, isVisibleBecauseExtraMargin, root_view_logging_enabled)) { + addShortRootViewLogging(root_view_logging_enabled, "Setting bottom margin to 0"); + params.setMargins(0, 0, 0, 0); + setLayoutParams(params); + } else { + addShortRootViewLogging(root_view_logging_enabled, "Bottom margin already equals 0"); + // This is done so that when next time onMeasure() is called, lastMarginBottom is used. + // This is done since we **expect** the keyboard to have same dimensions next time layout + // changes, so best set margin while view is drawn the first time, otherwise it will + // cause a jitter when OnGlobalLayoutListener is called with margin 0 and it sets the + // likely same lastMarginBottom again and requesting a redraw. Hopefully, this logic + // works fine for all cases. + marginBottom = lastMarginBottom; + } + } - if (setMargin) { - if (root_view_logging_enabled) - Logger.logVerbose(LOG_TAG, "Setting bottom margin to " + pxHidden); - params.setMargins(0, 0, 0, pxHidden); - setLayoutParams(params); - lastMarginBottom = pxHidden; + private boolean canSetMargin(int bottomMargin, boolean isVisibleBecauseExtraMargin, boolean root_view_logging_enabled) { + boolean canSetMargin = bottomMargin != 0; + + // If visible because of extra margin, i.e the bottom of bottomSpaceViewRect is above that of windowAvailableRect + // onGlobalLayout: windowAvailableRect 1408, bottomSpaceViewRect 1232, diff -176, bottom 0, isVisible true, isVisibleBecauseMargin false, isVisibleBecauseExtraMargin false + // onGlobalLayout: Bottom margin already equals 0 + if (isVisibleBecauseExtraMargin) { + // Adding time check since prevents infinite loops, like in landscape mode in freeform mode in Taskbar + if ((System.currentTimeMillis() - lastMarginBottomExtraTime) > 40) { + addShortRootViewLogging(root_view_logging_enabled, "Resetting margin since visible due to extra margin"); + lastMarginBottomExtraTime = System.currentTimeMillis(); + // lastMarginBottom must be invalid. May also happen when keyboards are changed. + lastMarginBottom = null; + canSetMargin = true; } else { - if (root_view_logging_enabled) - Logger.logVerbose(LOG_TAG, "Bottom margin already equals " + pxHidden); + addShortRootViewLogging(root_view_logging_enabled, "Ignoring resetting margin since visible due to extra margin since called to quickly"); } } + return canSetMargin; + } + + private void checkTime4PreventLoops(boolean root_view_logging_enabled) { + // Once the view has been redrawn with new margin, we set margin back to 0 so that + // when next time onMeasure() is called, margin 0 is used. This is necessary for + // cases when view has been redrawn with new margin because bottom space view was + // hidden by keyboard and then view was redrawn again due to layout change (like + // keyboard symbol view is switched to), android will add margin below its new position + // if its greater than 0, which was already above the keyboard creating x2x margin. + // Adding time check since moving split screen divider in landscape causes jitter + // and prevents some infinite loops + if ((System.currentTimeMillis() - lastMarginBottomTime) > 40) { + lastMarginBottomTime = System.currentTimeMillis(); + marginBottom = 0; + } else { + addShortRootViewLogging(root_view_logging_enabled, "Ignoring restoring marginBottom to 0 since called to quickly"); + } + } + + private Rect[] getwindowAndViewRects() { + View bottomSpaceView = mActivity.getTermuxActivityBottomSpaceView(); + if (bottomSpaceView == null) return null; + + // Get the position Rects of the bottom space view and the main window holding it + Rect[] windowAndViewRects = ViewUtils.getWindowAndViewRects(bottomSpaceView, mStatusBarHeight); + if (windowAndViewRects == null) + return null; + return windowAndViewRects; + } + + private void addShortRootViewLogging(boolean root_view_logging_enabled, String message) { + if (root_view_logging_enabled) + Logger.logVerbose(LOG_TAG, message); } public static class WindowInsetsListener implements View.OnApplyWindowInsetsListener { diff --git a/app/src/main/java/com/termux/app/terminal/TermuxSessionsListViewController.java b/app/src/main/java/com/termux/app/terminal/TermuxSessionsListViewController.java index bf914b977b..1e55f76444 100644 --- a/app/src/main/java/com/termux/app/terminal/TermuxSessionsListViewController.java +++ b/app/src/main/java/com/termux/app/terminal/TermuxSessionsListViewController.java @@ -43,11 +43,7 @@ public TermuxSessionsListViewController(TermuxActivity activity, List= 0) - mActivity.showToast(toToastTitle(finishedSession) + " - exited", true); - } + removeFinishedSessionByCondition(finishedSession, service, isPluginExecutionCommandWithPendingResult); + } + private void removeFinishedSessionByCondition(TerminalSession finishedSession, TermuxService service, boolean isPluginExecutionCommandWithPendingResult) { if (mActivity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) { // On Android TV devices we need to use older behaviour because we may // not be able to have multiple launcher icons. @@ -176,6 +166,29 @@ public void onSessionFinished(@NonNull TerminalSession finishedSession) { } } + // For plugin commands that expect the result back, we should immediately close the session + // and send the result back instead of waiting fo the user to press enter. + // The plugin can handle/show errors itself. + private boolean isSessionPendingResult(TerminalSession finishedSession, TermuxService service, int index) { + boolean isPluginExecutionCommandWithPendingResult = false; + TermuxSession termuxSession = service.getTermuxSession(index); + if (termuxSession != null) { + isPluginExecutionCommandWithPendingResult = termuxSession.getExecutionCommand().isPluginExecutionCommandWithPendingResult(); + if (isPluginExecutionCommandWithPendingResult) + Logger.logVerbose(LOG_TAG, "The \"" + finishedSession.mSessionName + "\" session will be force finished automatically since result in pending."); + } + return isPluginExecutionCommandWithPendingResult; + } + + private void showNonFinishedSessionToast(TerminalSession finishedSession, int index) { + if (mActivity.isVisible() && finishedSession != mActivity.getCurrentSession()) { + // Show toast for non-current sessions that exit. + // Verify that session was not removed before we got told about it finishing: + if (index >= 0) + mActivity.showToast(toToastTitle(finishedSession) + " - exited", true); + } + } + @Override public void onCopyTextToClipboard(@NonNull TerminalSession session, String text) { if (!mActivity.isVisible()) return; @@ -317,6 +330,12 @@ public void switchToSession(boolean forward) { TermuxService service = mActivity.getTermuxService(); if (service == null) return; + int index = getIndexWithDirection(forward, service); + + setCurrentSession(service, index); + } + + private int getIndexWithDirection(boolean forward, TermuxService service) { TerminalSession currentTerminalSession = mActivity.getCurrentSession(); int index = service.getIndexOfSession(currentTerminalSession); int size = service.getTermuxSessionsSize(); @@ -325,19 +344,14 @@ public void switchToSession(boolean forward) { } else { if (--index < 0) index = size - 1; } - - TermuxSession termuxSession = service.getTermuxSession(index); - if (termuxSession != null) - setCurrentSession(termuxSession.getTerminalSession()); + return index; } public void switchToSession(int index) { TermuxService service = mActivity.getTermuxService(); if (service == null) return; - TermuxSession termuxSession = service.getTermuxSession(index); - if (termuxSession != null) - setCurrentSession(termuxSession.getTerminalSession()); + setCurrentSession(service, index); } @SuppressLint("InflateParams") @@ -369,14 +383,7 @@ public void addNewSession(boolean isFailSafe, String sessionName) { new AlertDialog.Builder(mActivity).setTitle(R.string.title_max_terminals_reached).setMessage(R.string.msg_max_terminals_reached) .setPositiveButton(android.R.string.ok, null).show(); } else { - TerminalSession currentSession = mActivity.getCurrentSession(); - - String workingDirectory; - if (currentSession == null) { - workingDirectory = mActivity.getProperties().getDefaultWorkingDirectory(); - } else { - workingDirectory = currentSession.getCwd(); - } + String workingDirectory = getCurrentWorkingDirectory(); TermuxSession newTermuxSession = service.createTermuxSession(null, null, null, workingDirectory, isFailSafe, sessionName); if (newTermuxSession == null) return; @@ -388,6 +395,18 @@ public void addNewSession(boolean isFailSafe, String sessionName) { } } + private String getCurrentWorkingDirectory() { + TerminalSession currentSession = mActivity.getCurrentSession(); + + String workingDirectory; + if (currentSession == null) { + workingDirectory = mActivity.getProperties().getDefaultWorkingDirectory(); + } else { + workingDirectory = currentSession.getCwd(); + } + return workingDirectory; + } + public void setCurrentStoredSession() { TerminalSession currentSession = mActivity.getCurrentSession(); if (currentSession != null) @@ -445,12 +464,16 @@ public void removeFinishedSession(TerminalSession finishedSession) { if (index >= size) { index = size - 1; } - TermuxSession termuxSession = service.getTermuxSession(index); - if (termuxSession != null) - setCurrentSession(termuxSession.getTerminalSession()); + setCurrentSession(service, index); } } + private void setCurrentSession(TermuxService service, int index) { + TermuxSession termuxSession = service.getTermuxSession(index); + if (termuxSession != null) + setCurrentSession(termuxSession.getTerminalSession()); + } + public void termuxSessionListNotifyUpdated() { mActivity.termuxSessionListNotifyUpdated(); } @@ -479,12 +502,12 @@ String toToastTitle(TerminalSession session) { if (indexOfSession < 0) return null; StringBuilder toastTitle = new StringBuilder("[" + (indexOfSession + 1) + "]"); if (!TextUtils.isEmpty(session.mSessionName)) { - toastTitle.append(" ").append(session.mSessionName); + toastTitle.append(SPACE).append(session.mSessionName); } String title = session.getTitle(); if (!TextUtils.isEmpty(title)) { // Space to "[${NR}] or newline after session name: - toastTitle.append(session.mSessionName == null ? " " : "\n"); + toastTitle.append(session.mSessionName == null ? SPACE : NEWLINE); toastTitle.append(title); } return toastTitle.toString(); @@ -493,28 +516,38 @@ String toToastTitle(TerminalSession session) { public void checkForFontAndColors() { try { - File colorsFile = TermuxConstants.TERMUX_COLOR_PROPERTIES_FILE; - File fontFile = TermuxConstants.TERMUX_FONT_FILE; - - final Properties props = new Properties(); - if (colorsFile.isFile()) { - try (InputStream in = new FileInputStream(colorsFile)) { - props.load(in); - } - } + checkForColors(); + checkForFont(); + } catch (Exception e) { + Logger.logStackTraceWithMessage(LOG_TAG, "Error in checkForFontAndColors()", e); + } + } - TerminalColors.COLOR_SCHEME.updateWith(props); - TerminalSession session = mActivity.getCurrentSession(); - if (session != null && session.getEmulator() != null) { - session.getEmulator().mColors.reset(); + private void checkForFont() { + File fontFile = TermuxConstants.TERMUX_FONT_FILE; + + final Typeface newTypeface = (fontFile.exists() && fontFile.length() > 0) ? Typeface.createFromFile(fontFile) : Typeface.MONOSPACE; + mActivity.getTerminalView().setTypeface(newTypeface); + } + + private void checkForColors() throws IOException { + File colorsFile = TermuxConstants.TERMUX_COLOR_PROPERTIES_FILE; + + final Properties props = new Properties(); + if (colorsFile.isFile()) { + try (InputStream in = new FileInputStream(colorsFile)) { + props.load(in); + }catch(FileNotFoundException e){ + Logger.logStackTraceWithMessage(LOG_TAG, "Error in checkForColors(), colorsFile doesn't exist", e); } - updateBackgroundColor(); + } - final Typeface newTypeface = (fontFile.exists() && fontFile.length() > 0) ? Typeface.createFromFile(fontFile) : Typeface.MONOSPACE; - mActivity.getTerminalView().setTypeface(newTypeface); - } catch (Exception e) { - Logger.logStackTraceWithMessage(LOG_TAG, "Error in checkForFontAndColors()", e); + TerminalColors.COLOR_SCHEME.updateWith(props); + TerminalSession session = mActivity.getCurrentSession(); + if (session != null && session.getEmulator() != null) { + session.getEmulator().mColors.reset(); } + updateBackgroundColor(); } public void updateBackgroundColor() { @@ -522,6 +555,8 @@ public void updateBackgroundColor() { TerminalSession session = mActivity.getCurrentSession(); if (session != null && session.getEmulator() != null) { mActivity.getWindow().getDecorView().setBackgroundColor(session.getEmulator().mColors.mCurrentColors[TextStyle.COLOR_INDEX_BACKGROUND]); + }else{ + Logger.logVerbose(LOG_TAG, "There is no Session and Session emulator in updateBackgroundColor()"); } } diff --git a/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java b/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java index 1a3a8c3d8c..b06c34ff70 100644 --- a/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java +++ b/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java @@ -98,12 +98,12 @@ public void onCreate() { public void onStart() { // Set {@link TerminalView#TERMINAL_VIEW_KEY_LOGGING_ENABLED} value // Also required if user changed the preference from {@link TermuxSettings} activity and returns - boolean isTerminalViewKeyLoggingEnabled = mActivity.getPreferences().isTerminalViewKeyLoggingEnabled(); - mActivity.getTerminalView().setIsTerminalViewKeyLoggingEnabled(isTerminalViewKeyLoggingEnabled); + boolean isKeyLoggingEnabled = mActivity.getPreferences().isTerminalViewKeyLoggingEnabled(); + mActivity.getTerminalView().setIsTerminalViewKeyLoggingEnabled(isKeyLoggingEnabled); // Piggyback on the terminal view key logging toggle for now, should add a separate toggle in future - mActivity.getTermuxActivityRootView().setIsRootViewLoggingEnabled(isTerminalViewKeyLoggingEnabled); - ViewUtils.setIsViewUtilsLoggingEnabled(isTerminalViewKeyLoggingEnabled); + mActivity.getTermuxActivityRootView().setIsRootViewLoggingEnabled(isKeyLoggingEnabled); + ViewUtils.setIsViewUtilsLoggingEnabled(isKeyLoggingEnabled); } /** @@ -576,9 +576,10 @@ public void setSoftKeyboardState(boolean isStartup, boolean isReloadTermuxProper // in TerminalView layout to fix the issue. // If soft keyboard is disabled by user for Termux (check function docs for Termux behaviour info) - if (KeyboardUtils.shouldSoftKeyboardBeDisabled(mActivity, + boolean keyboardDisabledByUser = KeyboardUtils.shouldSoftKeyboardBeDisabled(mActivity, mActivity.getPreferences().isSoftKeyboardEnabled(), - mActivity.getPreferences().isSoftKeyboardEnabledOnlyIfNoHardware())) { + mActivity.getPreferences().isSoftKeyboardEnabledOnlyIfNoHardware()); + if (keyboardDisabledByUser) { Logger.logVerbose(LOG_TAG, "Maintaining disabled soft keyboard"); KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView()); mActivity.getTerminalView().requestFocus(); @@ -596,7 +597,8 @@ public void setSoftKeyboardState(boolean isStartup, boolean isReloadTermuxProper KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity); // If soft keyboard is to be hidden on startup - if (isStartup && mActivity.getProperties().shouldSoftKeyboardBeHiddenOnStartup()) { + boolean keyboardHidden = isStartup && mActivity.getProperties().shouldSoftKeyboardBeHiddenOnStartup(); + if (keyboardHidden) { Logger.logVerbose(LOG_TAG, "Hiding soft keyboard on startup"); // Required to keep keyboard hidden when Termux app is switched back from another app KeyboardUtils.setSoftKeyboardAlwaysHiddenFlags(mActivity); @@ -659,7 +661,8 @@ private Runnable getShowSoftKeyboardRunnable() { public void setTerminalCursorBlinkerState(boolean start) { if (start) { // If set/update the cursor blinking rate is successful, then enable cursor blinker - if (mActivity.getTerminalView().setTerminalCursorBlinkerRate(mActivity.getProperties().getTerminalCursorBlinkRate())) + boolean cursorBlinkerRateUpdate = mActivity.getTerminalView().setTerminalCursorBlinkerRate(mActivity.getProperties().getTerminalCursorBlinkRate()); + if (cursorBlinkerRateUpdate) mActivity.getTerminalView().setTerminalCursorBlinkerState(true, true); else Logger.logError(LOG_TAG,"Failed to start cursor blinker"); @@ -685,20 +688,17 @@ public void shareSessionTranscript() { } public void showUrlSelection() { - TerminalSession session = mActivity.getCurrentSession(); - if (session == null) return; + final CharSequence[] urls = getUrls(); + if (urls == null) return; - String text = ShellUtils.getTerminalSessionTranscriptText(session, true, true); + final AlertDialog dialog = getClickedUrlDialog(urls); - LinkedHashSet urlSet = TermuxUrlUtils.extractUrls(text); - if (urlSet.isEmpty()) { - new AlertDialog.Builder(mActivity).setMessage(R.string.title_select_url_none_found).show(); - return; - } + checkUrlLongPress(urls, dialog); - final CharSequence[] urls = urlSet.toArray(new CharSequence[0]); - Collections.reverse(Arrays.asList(urls)); // Latest first. + dialog.show(); + } + private AlertDialog getClickedUrlDialog(CharSequence[] urls) { // Click to copy url to clipboard: final AlertDialog dialog = new AlertDialog.Builder(mActivity).setItems(urls, (di, which) -> { String url = (String) urls[which]; @@ -706,19 +706,37 @@ public void showUrlSelection() { clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(url))); Toast.makeText(mActivity, R.string.msg_select_url_copied_to_clipboard, Toast.LENGTH_LONG).show(); }).setTitle(R.string.title_select_url_dialog).create(); + return dialog; + } + + private CharSequence[] getUrls() { + TerminalSession session = mActivity.getCurrentSession(); + if (session == null) return null; + + String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, true, true); + + LinkedHashSet urlSet = TermuxUrlUtils.extractUrls(transcriptText); + if (urlSet.isEmpty()) { + new AlertDialog.Builder(mActivity).setMessage(R.string.title_select_url_none_found).show(); + return null; + } + + final CharSequence[] urls = urlSet.toArray(new CharSequence[0]); + Collections.reverse(Arrays.asList(urls)); // Latest first. + return urls; + } + private void checkUrlLongPress(CharSequence[] urls, AlertDialog dialog) { // Long press to open URL: dialog.setOnShowListener(di -> { - ListView lv = dialog.getListView(); // this is a ListView with your "buds" in it - lv.setOnItemLongClickListener((parent, view, position, id) -> { + ListView dialogListView = dialog.getListView(); // this is a ListView with your "buds" in it + dialogListView.setOnItemLongClickListener((parent, view, position, id) -> { dialog.dismiss(); String url = (String) urls[position]; ShareUtils.openUrl(mActivity, url); return true; }); }); - - dialog.show(); } public void reportIssueFromTranscript() { @@ -743,45 +761,57 @@ private void reportIssueFromTranscript(String transcriptText, boolean addTermuxD public void run() { StringBuilder reportString = new StringBuilder(); - String title = TermuxConstants.TERMUX_APP_NAME + " Report Issue"; + addReportHeader(reportString, transcriptText); - reportString.append("## Transcript\n"); - reportString.append("\n").append(MarkdownUtils.getMarkdownCodeForString(transcriptText, true)); - reportString.append("\n##\n"); + addReportBody(reportString, addTermuxDebugInfo); - if (addTermuxDebugInfo) { - reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGES)); - } else { - reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, TermuxUtils.AppInfoMode.TERMUX_PACKAGE)); - } + ReportInfo reportInfo = updateReportInfo(reportString); - reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(mActivity)); + ReportActivity.startReportActivity(mActivity, reportInfo); + } + }.start(); + } - if (TermuxBootstrap.isAppPackageManagerAPT()) { - String termuxAptInfo = TermuxUtils.geAPTInfoMarkdownString(mActivity); - if (termuxAptInfo != null) - reportString.append("\n\n").append(termuxAptInfo); - } + private void addReportBody(StringBuilder reportString, boolean addTermuxDebugInfo) { + if (addTermuxDebugInfo) { + reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGES)); + } else { + reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, TermuxUtils.AppInfoMode.TERMUX_PACKAGE)); + } - if (addTermuxDebugInfo) { - String termuxDebugInfo = TermuxUtils.getTermuxDebugMarkdownString(mActivity); - if (termuxDebugInfo != null) - reportString.append("\n\n").append(termuxDebugInfo); - } + reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(mActivity)); - String userActionName = UserAction.REPORT_ISSUE_FROM_TRANSCRIPT.getName(); + if (TermuxBootstrap.isAppPackageManagerAPT()) { + String termuxAptInfo = TermuxUtils.geAPTInfoMarkdownString(mActivity); + if (termuxAptInfo != null) + reportString.append("\n\n").append(termuxAptInfo); + } - ReportInfo reportInfo = new ReportInfo(userActionName, - TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY_NAME, title); - reportInfo.setReportString(reportString.toString()); - reportInfo.setReportStringSuffix("\n\n" + TermuxUtils.getReportIssueMarkdownString(mActivity)); - reportInfo.setReportSaveFileLabelAndPath(userActionName, - Environment.getExternalStorageDirectory() + "/" + - FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true)); + if (addTermuxDebugInfo) { + String termuxDebugInfo = TermuxUtils.getTermuxDebugMarkdownString(mActivity); + if (termuxDebugInfo != null) + reportString.append("\n\n").append(termuxDebugInfo); + } + } - ReportActivity.startReportActivity(mActivity, reportInfo); - } - }.start(); + private void addReportHeader(StringBuilder reportString, String transcriptText) { + reportString.append("## Transcript\n"); + reportString.append("\n").append(MarkdownUtils.getMarkdownCodeForString(transcriptText, true)); + reportString.append("\n##\n"); + } + + private ReportInfo updateReportInfo(StringBuilder reportString) { + String userActionName = UserAction.REPORT_ISSUE_FROM_TRANSCRIPT.getName(); + String title = TermuxConstants.TERMUX_APP_NAME + " Report Issue"; + + ReportInfo reportInfo = new ReportInfo(userActionName, + TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY_NAME, title); + reportInfo.setReportString(reportString.toString()); + reportInfo.setReportStringSuffix("\n\n" + TermuxUtils.getReportIssueMarkdownString(mActivity)); + reportInfo.setReportSaveFileLabelAndPath(userActionName, + Environment.getExternalStorageDirectory() + "/" + + FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true)); + return reportInfo; } public void doPaste() { diff --git a/temp.txt b/temp.txt new file mode 100644 index 0000000000..e965047ad7 --- /dev/null +++ b/temp.txt @@ -0,0 +1 @@ +Hello diff --git a/terminal-emulator/src/main/java/com/termux/terminal/BufferStyler.java b/terminal-emulator/src/main/java/com/termux/terminal/BufferStyler.java new file mode 100644 index 0000000000..d9c5da7e6a --- /dev/null +++ b/terminal-emulator/src/main/java/com/termux/terminal/BufferStyler.java @@ -0,0 +1,45 @@ +package com.termux.terminal; + +public final class BufferStyler { + + TerminalBuffer mTerminalBuffer; + + public BufferStyler(TerminalBuffer terminalBuffer) { + mTerminalBuffer = terminalBuffer; + } + + /** Support for http://vt100.net/docs/vt510-rm/DECCARA and http://vt100.net/docs/vt510-rm/DECCARA */ + public void setOrClearEffect(int bits, boolean isSetOrClear, boolean isReverse, boolean isRectangular, int leftMargin, int rightMargin, int top, int left, + int bottom, int right) { + for (int row = top; row < bottom; row++) { + TerminalRow line = mTerminalBuffer.mLines[mTerminalBuffer.externalToInternalRow(row)]; + int startOfLine = (isRectangular || row == top) ? left : leftMargin; + int endOfLine = (isRectangular || row + 1 == bottom) ? right : rightMargin; + for (int col = startOfLine; col < endOfLine; col++) { + setStyle(bits, isSetOrClear, isReverse, line, col); + } + } + } + + private void setStyle(int bits, boolean isSetOrClear, boolean isReverse, TerminalRow line, int col) { + final long currentStyle = line.getStyle(col); + final int foreColor = TextStyle.decodeForeColor(currentStyle); + final int backColor = TextStyle.decodeBackColor(currentStyle); + int effect = getEffect(bits, isSetOrClear, isReverse, currentStyle); + line.mStyle[col] = TextStyle.encode(foreColor, backColor, effect); + } + + private int getEffect(int bits, boolean isSetOrClear, boolean isReverse, long currentStyle) { + int effect = TextStyle.decodeEffect(currentStyle); + if (isReverse) { + // Clear out the bits to reverse and add them back in reversed: + effect = (effect & ~bits) | (bits & ~effect); + } else if (isSetOrClear) { + effect |= bits; + } else { + effect &= ~bits; + } + return effect; + } + +} diff --git a/terminal-emulator/src/main/java/com/termux/terminal/Cursor.java b/terminal-emulator/src/main/java/com/termux/terminal/Cursor.java new file mode 100644 index 0000000000..1e465eeac8 --- /dev/null +++ b/terminal-emulator/src/main/java/com/termux/terminal/Cursor.java @@ -0,0 +1,40 @@ +package com.termux.terminal; + +public class Cursor { + private int row; + private int column; + + public Cursor(int row, int column) { + this.row = row; + this.column = column; + } + + public int getRow() { + return row; + } + + public int getColumn() { + return column; + } + + public void setCursor(Cursor cursor) { + this.row = cursor.getRow(); + this.column = cursor.getColumn(); + } + + public void setRow(int row) { + this.row = row; + } + + public void setColumn(int column) { + this.column = column; + } + + public void addToRow(int adder) { + this.row += adder; + } + + public void addToColumn(int adder) { + this.column += adder; + } +} diff --git a/terminal-emulator/src/main/java/com/termux/terminal/FastResize.java b/terminal-emulator/src/main/java/com/termux/terminal/FastResize.java new file mode 100644 index 0000000000..4b1f40552f --- /dev/null +++ b/terminal-emulator/src/main/java/com/termux/terminal/FastResize.java @@ -0,0 +1,58 @@ +package com.termux.terminal; + +public final class FastResize implements ResizeBuffer { + TerminalBuffer mTerminalBuffer; + + public FastResize(TerminalBuffer terminalBuffer) { + mTerminalBuffer = terminalBuffer; + } + + public void resize(int newColumns, int newRows, int newTotalRows, int[] cursor, long currentStyle, boolean isAltScreen) { + int shiftDownOfTopRow = calcShiftDownOfTopRow(newRows, cursor, currentStyle); + mTerminalBuffer.mScreenFirstRow += shiftDownOfTopRow; + mTerminalBuffer.mScreenFirstRow = (mTerminalBuffer.mScreenFirstRow < 0) ? (mTerminalBuffer.mScreenFirstRow + mTerminalBuffer.mTotalRows) : (mTerminalBuffer.mScreenFirstRow % mTerminalBuffer.mTotalRows); + mTerminalBuffer.mTotalRows = newTotalRows; + mTerminalBuffer.mActiveTranscriptRows = isAltScreen ? 0 : Math.max(0, mTerminalBuffer.mActiveTranscriptRows + shiftDownOfTopRow); + cursor[1] -= shiftDownOfTopRow; + mTerminalBuffer.mScreenRows = newRows; + } + + private int calcShiftDownOfTopRow(int newRows, int[] cursor, long currentStyle) { + int shiftDownOfTopRow = mTerminalBuffer.mScreenRows - newRows; + final boolean isShrinking = shiftDownOfTopRow > 0 && shiftDownOfTopRow < mTerminalBuffer.mScreenRows; + final boolean isExpanding = shiftDownOfTopRow < 0; + if (isShrinking) { + // Shrinking. Check if we can skip blank rows at bottom below cursor. + shiftDownOfTopRow = shrinkingRows(cursor, shiftDownOfTopRow); + } else if (isExpanding) { + // Negative shift down = expanding. Only move screen up if there is transcript to show: + shiftDownOfTopRow = expandingRows(currentStyle, shiftDownOfTopRow); + } + return shiftDownOfTopRow; + } + + private int shrinkingRows(int[] cursor, int shiftDownOfTopRow) { + for (int i = mTerminalBuffer.mScreenRows - 1; i > 0; i--) { + if (cursor[1] >= i) break; + int r = mTerminalBuffer.externalToInternalRow(i); + final boolean isLineEmpty = mTerminalBuffer.mLines[r] == null || mTerminalBuffer.mLines[r].isBlank(); + if (isLineEmpty) { + final boolean isShrinkingEnd = --shiftDownOfTopRow == 0; + if (isShrinkingEnd) break; + } + } + return shiftDownOfTopRow; + } + + private int expandingRows(long currentStyle, int shiftDownOfTopRow) { + int actualShift = Math.max(shiftDownOfTopRow, -mTerminalBuffer.mActiveTranscriptRows); + if (shiftDownOfTopRow != actualShift) { + // The new lines revealed by the resizing are not all from the transcript. Blank the below ones. + for (int i = 0; i < actualShift - shiftDownOfTopRow; i++) + mTerminalBuffer.allocateFullLineIfNecessary((mTerminalBuffer.mScreenFirstRow + mTerminalBuffer.mScreenRows + i) % mTerminalBuffer.mTotalRows).clear(currentStyle); + shiftDownOfTopRow = actualShift; + } + return shiftDownOfTopRow; + } + +} diff --git a/terminal-emulator/src/main/java/com/termux/terminal/ResizeBuffer.java b/terminal-emulator/src/main/java/com/termux/terminal/ResizeBuffer.java new file mode 100644 index 0000000000..9f8cd3d678 --- /dev/null +++ b/terminal-emulator/src/main/java/com/termux/terminal/ResizeBuffer.java @@ -0,0 +1,5 @@ +package com.termux.terminal; + +public interface ResizeBuffer { + void resize(int newColumns, int newRows, int newTotalRows, int[] cursor, long currentStyle, boolean isAltScreen) ; +} diff --git a/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java b/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java index df57bd3629..062728182c 100644 --- a/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java +++ b/terminal-emulator/src/main/java/com/termux/terminal/TerminalBuffer.java @@ -16,9 +16,9 @@ public final class TerminalBuffer { /** The number of rows and columns visible on the screen. */ int mScreenRows, mColumns; /** The number of rows kept in history. */ - private int mActiveTranscriptRows = 0; + int mActiveTranscriptRows = 0; /** The index in the circular buffer where the visible screen starts. */ - private int mScreenFirstRow = 0; + int mScreenFirstRow = 0; /** * Create a transcript screen. @@ -58,90 +58,11 @@ public String getSelectedText(int selX1, int selY1, int selX2, int selY2, boolea } public String getSelectedText(int selX1, int selY1, int selX2, int selY2, boolean joinBackLines, boolean joinFullLines) { - final StringBuilder builder = new StringBuilder(); - final int columns = mColumns; - - if (selY1 < -getActiveTranscriptRows()) selY1 = -getActiveTranscriptRows(); - if (selY2 >= mScreenRows) selY2 = mScreenRows - 1; - - for (int row = selY1; row <= selY2; row++) { - int x1 = (row == selY1) ? selX1 : 0; - int x2; - if (row == selY2) { - x2 = selX2 + 1; - if (x2 > columns) x2 = columns; - } else { - x2 = columns; - } - TerminalRow lineObject = mLines[externalToInternalRow(row)]; - int x1Index = lineObject.findStartOfColumn(x1); - int x2Index = (x2 < mColumns) ? lineObject.findStartOfColumn(x2) : lineObject.getSpaceUsed(); - if (x2Index == x1Index) { - // Selected the start of a wide character. - x2Index = lineObject.findStartOfColumn(x2 + 1); - } - char[] line = lineObject.mText; - int lastPrintingCharIndex = -1; - int i; - boolean rowLineWrap = getLineWrap(row); - if (rowLineWrap && x2 == columns) { - // If the line was wrapped, we shouldn't lose trailing space: - lastPrintingCharIndex = x2Index - 1; - } else { - for (i = x1Index; i < x2Index; ++i) { - char c = line[i]; - if (c != ' ') lastPrintingCharIndex = i; - } - } - - int len = lastPrintingCharIndex - x1Index + 1; - if (lastPrintingCharIndex != -1 && len > 0) - builder.append(line, x1Index, len); - - boolean lineFillsWidth = lastPrintingCharIndex == x2Index - 1; - if ((!joinBackLines || !rowLineWrap) && (!joinFullLines || !lineFillsWidth) - && row < selY2 && row < mScreenRows - 1) builder.append('\n'); - } - return builder.toString(); + return new TextFinder(this).getSelectedText(new Cursor(selY1, selX1), new Cursor(selY2, selX2), joinBackLines, joinFullLines); } public String getWordAtLocation(int x, int y) { - // Set y1 and y2 to the lines where the wrapped line starts and ends. - // I.e. if a line that is wrapped to 3 lines starts at line 4, and this - // is called with y=5, then y1 would be set to 4 and y2 would be set to 6. - int y1 = y; - int y2 = y; - while (y1 > 0 && !getSelectedText(0, y1 - 1, mColumns, y, true, true).contains("\n")) { - y1--; - } - while (y2 < mScreenRows && !getSelectedText(0, y, mColumns, y2 + 1, true, true).contains("\n")) { - y2++; - } - - // Get the text for the whole wrapped line - String text = getSelectedText(0, y1, mColumns, y2, true, true); - // The index of x in text - int textOffset = (y - y1) * mColumns + x; - - if (textOffset >= text.length()) { - // The click was to the right of the last word on the line, so - // there's no word to return - return ""; - } - - // Set x1 and x2 to the indices of the last space before x and the - // first space after x in text respectively - int x1 = text.lastIndexOf(' ', textOffset); - int x2 = text.indexOf(' ', textOffset); - if (x2 == -1) { - x2 = text.length(); - } - - if (x1 == x2) { - // The click was on a space, so there's no word to return - return ""; - } - return text.substring(x1 + 1, x2); + return new TextFinder(this).getWordAtLocation(new Cursor(y, x)); } public int getActiveTranscriptRows() { @@ -200,153 +121,14 @@ public void clearLineWrap(int row) { * @param newRows The number of rows the screen should have. * @param cursor An int[2] containing the (column, row) cursor location. */ - public void resize(int newColumns, int newRows, int newTotalRows, int[] cursor, long currentStyle, boolean altScreen) { + public void resize(int newColumns, int newRows, int newTotalRows, int[] cursor, long currentStyle, boolean isAltScreen) { // newRows > mTotalRows should not normally happen since mTotalRows is TRANSCRIPT_ROWS (10000): if (newColumns == mColumns && newRows <= mTotalRows) { // Fast resize where just the rows changed. - int shiftDownOfTopRow = mScreenRows - newRows; - if (shiftDownOfTopRow > 0 && shiftDownOfTopRow < mScreenRows) { - // Shrinking. Check if we can skip blank rows at bottom below cursor. - for (int i = mScreenRows - 1; i > 0; i--) { - if (cursor[1] >= i) break; - int r = externalToInternalRow(i); - if (mLines[r] == null || mLines[r].isBlank()) { - if (--shiftDownOfTopRow == 0) break; - } - } - } else if (shiftDownOfTopRow < 0) { - // Negative shift down = expanding. Only move screen up if there is transcript to show: - int actualShift = Math.max(shiftDownOfTopRow, -mActiveTranscriptRows); - if (shiftDownOfTopRow != actualShift) { - // The new lines revealed by the resizing are not all from the transcript. Blank the below ones. - for (int i = 0; i < actualShift - shiftDownOfTopRow; i++) - allocateFullLineIfNecessary((mScreenFirstRow + mScreenRows + i) % mTotalRows).clear(currentStyle); - shiftDownOfTopRow = actualShift; - } - } - mScreenFirstRow += shiftDownOfTopRow; - mScreenFirstRow = (mScreenFirstRow < 0) ? (mScreenFirstRow + mTotalRows) : (mScreenFirstRow % mTotalRows); - mTotalRows = newTotalRows; - mActiveTranscriptRows = altScreen ? 0 : Math.max(0, mActiveTranscriptRows + shiftDownOfTopRow); - cursor[1] -= shiftDownOfTopRow; - mScreenRows = newRows; + new FastResize(this).resize(newColumns, newRows, newTotalRows, cursor, currentStyle, isAltScreen); } else { // Copy away old state and update new: - TerminalRow[] oldLines = mLines; - mLines = new TerminalRow[newTotalRows]; - for (int i = 0; i < newTotalRows; i++) - mLines[i] = new TerminalRow(newColumns, currentStyle); - - final int oldActiveTranscriptRows = mActiveTranscriptRows; - final int oldScreenFirstRow = mScreenFirstRow; - final int oldScreenRows = mScreenRows; - final int oldTotalRows = mTotalRows; - mTotalRows = newTotalRows; - mScreenRows = newRows; - mActiveTranscriptRows = mScreenFirstRow = 0; - mColumns = newColumns; - - int newCursorRow = -1; - int newCursorColumn = -1; - int oldCursorRow = cursor[1]; - int oldCursorColumn = cursor[0]; - boolean newCursorPlaced = false; - - int currentOutputExternalRow = 0; - int currentOutputExternalColumn = 0; - - // Loop over every character in the initial state. - // Blank lines should be skipped only if at end of transcript (just as is done in the "fast" resize), so we - // keep track how many blank lines we have skipped if we later on find a non-blank line. - int skippedBlankLines = 0; - for (int externalOldRow = -oldActiveTranscriptRows; externalOldRow < oldScreenRows; externalOldRow++) { - // Do what externalToInternalRow() does but for the old state: - int internalOldRow = oldScreenFirstRow + externalOldRow; - internalOldRow = (internalOldRow < 0) ? (oldTotalRows + internalOldRow) : (internalOldRow % oldTotalRows); - - TerminalRow oldLine = oldLines[internalOldRow]; - boolean cursorAtThisRow = externalOldRow == oldCursorRow; - // The cursor may only be on a non-null line, which we should not skip: - if (oldLine == null || (!(!newCursorPlaced && cursorAtThisRow)) && oldLine.isBlank()) { - skippedBlankLines++; - continue; - } else if (skippedBlankLines > 0) { - // After skipping some blank lines we encounter a non-blank line. Insert the skipped blank lines. - for (int i = 0; i < skippedBlankLines; i++) { - if (currentOutputExternalRow == mScreenRows - 1) { - scrollDownOneLine(0, mScreenRows, currentStyle); - } else { - currentOutputExternalRow++; - } - currentOutputExternalColumn = 0; - } - skippedBlankLines = 0; - } - - int lastNonSpaceIndex = 0; - boolean justToCursor = false; - if (cursorAtThisRow || oldLine.mLineWrap) { - // Take the whole line, either because of cursor on it, or if line wrapping. - lastNonSpaceIndex = oldLine.getSpaceUsed(); - if (cursorAtThisRow) justToCursor = true; - } else { - for (int i = 0; i < oldLine.getSpaceUsed(); i++) - // NEWLY INTRODUCED BUG! Should not index oldLine.mStyle with char indices - if (oldLine.mText[i] != ' '/* || oldLine.mStyle[i] != currentStyle */) - lastNonSpaceIndex = i + 1; - } - - int currentOldCol = 0; - long styleAtCol = 0; - for (int i = 0; i < lastNonSpaceIndex; i++) { - // Note that looping over java character, not cells. - char c = oldLine.mText[i]; - int codePoint = (Character.isHighSurrogate(c)) ? Character.toCodePoint(c, oldLine.mText[++i]) : c; - int displayWidth = WcWidth.width(codePoint); - // Use the last style if this is a zero-width character: - if (displayWidth > 0) styleAtCol = oldLine.getStyle(currentOldCol); - - // Line wrap as necessary: - if (currentOutputExternalColumn + displayWidth > mColumns) { - setLineWrap(currentOutputExternalRow); - if (currentOutputExternalRow == mScreenRows - 1) { - if (newCursorPlaced) newCursorRow--; - scrollDownOneLine(0, mScreenRows, currentStyle); - } else { - currentOutputExternalRow++; - } - currentOutputExternalColumn = 0; - } - - int offsetDueToCombiningChar = ((displayWidth <= 0 && currentOutputExternalColumn > 0) ? 1 : 0); - int outputColumn = currentOutputExternalColumn - offsetDueToCombiningChar; - setChar(outputColumn, currentOutputExternalRow, codePoint, styleAtCol); - - if (displayWidth > 0) { - if (oldCursorRow == externalOldRow && oldCursorColumn == currentOldCol) { - newCursorColumn = currentOutputExternalColumn; - newCursorRow = currentOutputExternalRow; - newCursorPlaced = true; - } - currentOldCol += displayWidth; - currentOutputExternalColumn += displayWidth; - if (justToCursor && newCursorPlaced) break; - } - } - // Old row has been copied. Check if we need to insert newline if old line was not wrapping: - if (externalOldRow != (oldScreenRows - 1) && !oldLine.mLineWrap) { - if (currentOutputExternalRow == mScreenRows - 1) { - if (newCursorPlaced) newCursorRow--; - scrollDownOneLine(0, mScreenRows, currentStyle); - } else { - currentOutputExternalRow++; - } - currentOutputExternalColumn = 0; - } - } - - cursor[0] = newCursorColumn; - cursor[1] = newCursorRow; + new UpdateOldBuffer(this).resize(newColumns, newRows, newTotalRows, cursor, currentStyle, isAltScreen); } // Handle cursor scrolling off screen: @@ -421,9 +203,9 @@ public void blockCopy(int sx, int sy, int w, int h, int dx, int dy) { if (w == 0) return; if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows || dx < 0 || dx + w > mColumns || dy < 0 || dy + h > mScreenRows) throw new IllegalArgumentException(); - boolean copyingUp = sy > dy; + boolean isCopyingUp = sy > dy; for (int y = 0; y < h; y++) { - int y2 = copyingUp ? y : (h - (y + 1)); + int y2 = isCopyingUp ? y : (h - (y + 1)); TerminalRow sourceRow = allocateFullLineIfNecessary(externalToInternalRow(sy + y2)); allocateFullLineIfNecessary(externalToInternalRow(dy + y2)).copyInterval(sourceRow, sx, sx + w, dx); } @@ -455,35 +237,6 @@ public void setChar(int column, int row, int codePoint, long style) { allocateFullLineIfNecessary(row).setChar(column, codePoint, style); } - public long getStyleAt(int externalRow, int column) { - return allocateFullLineIfNecessary(externalToInternalRow(externalRow)).getStyle(column); - } - - /** Support for http://vt100.net/docs/vt510-rm/DECCARA and http://vt100.net/docs/vt510-rm/DECCARA */ - public void setOrClearEffect(int bits, boolean setOrClear, boolean reverse, boolean rectangular, int leftMargin, int rightMargin, int top, int left, - int bottom, int right) { - for (int y = top; y < bottom; y++) { - TerminalRow line = mLines[externalToInternalRow(y)]; - int startOfLine = (rectangular || y == top) ? left : leftMargin; - int endOfLine = (rectangular || y + 1 == bottom) ? right : rightMargin; - for (int x = startOfLine; x < endOfLine; x++) { - long currentStyle = line.getStyle(x); - int foreColor = TextStyle.decodeForeColor(currentStyle); - int backColor = TextStyle.decodeBackColor(currentStyle); - int effect = TextStyle.decodeEffect(currentStyle); - if (reverse) { - // Clear out the bits to reverse and add them back in reversed: - effect = (effect & ~bits) | (bits & ~effect); - } else if (setOrClear) { - effect |= bits; - } else { - effect &= ~bits; - } - line.mStyle[x] = TextStyle.encode(foreColor, backColor, effect); - } - } - } - public void clearTranscript() { if (mScreenFirstRow < mActiveTranscriptRows) { Arrays.fill(mLines, mTotalRows + mScreenFirstRow - mActiveTranscriptRows, mTotalRows, null); @@ -494,4 +247,16 @@ public void clearTranscript() { mActiveTranscriptRows = 0; } + public long getStyleAt(int externalRow, int column) { + return allocateFullLineIfNecessary(externalToInternalRow(externalRow)).getStyle(column); + } + + /** Support for http://vt100.net/docs/vt510-rm/DECCARA and http://vt100.net/docs/vt510-rm/DECCARA */ + public void setOrClearEffect(int bits, boolean isSetOrClear, boolean isReverse, boolean isRectangular, int leftMargin, int rightMargin, int top, int left, + int bottom, int right) { + new BufferStyler(this).setOrClearEffect(bits, isSetOrClear, isReverse, isRectangular, leftMargin, rightMargin, top, left, bottom, right); + } + + } + diff --git a/terminal-emulator/src/main/java/com/termux/terminal/TerminalColorScheme.java b/terminal-emulator/src/main/java/com/termux/terminal/TerminalColorScheme.java index e4fd4e9c77..2f77cf2d1e 100644 --- a/terminal-emulator/src/main/java/com/termux/terminal/TerminalColorScheme.java +++ b/terminal-emulator/src/main/java/com/termux/terminal/TerminalColorScheme.java @@ -74,29 +74,40 @@ public void updateWith(Properties props) { for (Map.Entry entries : props.entrySet()) { String key = (String) entries.getKey(); String value = (String) entries.getValue(); - int colorIndex; - if (key.equals("foreground")) { - colorIndex = TextStyle.COLOR_INDEX_FOREGROUND; - } else if (key.equals("background")) { - colorIndex = TextStyle.COLOR_INDEX_BACKGROUND; - } else if (key.equals("cursor")) { - colorIndex = TextStyle.COLOR_INDEX_CURSOR; - } else if (key.startsWith("color")) { - try { - colorIndex = Integer.parseInt(key.substring(5)); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Invalid property: '" + key + "'"); - } - } else { + try { + int colorIndex = getColorIndex(key); + setColorAtIndex(value, colorIndex); + } catch (IllegalArgumentException e) { + // Ignore. + } + } + } + + private int getColorIndex(String key) { + if (key.equals("foreground")) { + return TextStyle.COLOR_INDEX_FOREGROUND; + } else if (key.equals("background")) { + return TextStyle.COLOR_INDEX_BACKGROUND; + } else if (key.equals("cursor")) { + return TextStyle.COLOR_INDEX_CURSOR; + } else if (key.startsWith("color")) { + try { + return Integer.parseInt(key.substring(5)); + } catch (NumberFormatException e) { throw new IllegalArgumentException("Invalid property: '" + key + "'"); } + } else { + throw new IllegalArgumentException("Invalid property: '" + key + "'"); + } + } + private void setColorAtIndex(String value, int colorIndex) { + try { int colorValue = TerminalColors.parse(value); - if (colorValue == 0) - throw new IllegalArgumentException("Property '" + key + "' has invalid color: '" + value + "'"); - mDefaultColors[colorIndex] = colorValue; + } catch (IllegalArgumentException e) { + // Ignore. } } diff --git a/terminal-emulator/src/main/java/com/termux/terminal/TerminalColors.java b/terminal-emulator/src/main/java/com/termux/terminal/TerminalColors.java index 6d8cfdb9ec..2e124452f0 100644 --- a/terminal-emulator/src/main/java/com/termux/terminal/TerminalColors.java +++ b/terminal-emulator/src/main/java/com/termux/terminal/TerminalColors.java @@ -32,45 +32,60 @@ public void reset() { *

* Highest bit is set if successful, so return value is 0xFF${R}${G}${B}. Return 0 if failed. */ - static int parse(String c) { - try { - int skipInitial, skipBetween; - if (c.charAt(0) == '#') { - // #RGB, #RRGGBB, #RRRGGGBBB or #RRRRGGGGBBBB. Most significant bits. - skipInitial = 1; - skipBetween = 0; - } else if (c.startsWith("rgb:")) { - // rgb:// where , , := h | hh | hhh | hhhh. Scaled. - skipInitial = 4; - skipBetween = 1; - } else { - return 0; - } - int charsForColors = c.length() - skipInitial - 2 * skipBetween; - if (charsForColors % 3 != 0) return 0; // Unequal lengths. - int componentLength = charsForColors / 3; - double mult = 255 / (Math.pow(2, componentLength * 4) - 1); + static int parse(String colorString) { + int skipInitial, skipBetween; + if (colorString.charAt(0) == '#') { + // #RGB, #RRGGBB, #RRRGGGBBB or #RRRRGGGGBBBB. Most significant bits. + skipInitial = 1; + skipBetween = 0; + } else if (colorString.startsWith("rgb:")) { + // rgb:// where , , := h | hh | hhh | hhhh. Scaled. + skipInitial = 4; + skipBetween = 1; + } else { + throw new IllegalArgumentException("Wrong Prefix Format: '" + colorString + "'"); + } + + return parseRGB(colorString, skipInitial, skipBetween); + } + + private static int getComponentLength(String colorString, int skipInitial, int skipBetween) { + int charsForColors = colorString.length() - skipInitial - 2 * skipBetween; + if (charsForColors % 3 != 0) { + throw new IllegalArgumentException("Unequal Length: '" + colorString + "'"); + } + return charsForColors / 3; + } + private static int parseRGB(String colorString, int skipInitial, int skipBetween) { + final int componentLength = getComponentLength(colorString, skipInitial, skipBetween); + final double mult = 255 / (Math.pow(2, componentLength * 4) - 1); + try { int currentPosition = skipInitial; - String rString = c.substring(currentPosition, currentPosition + componentLength); + String rString = colorString.substring(currentPosition, currentPosition + componentLength); currentPosition += componentLength + skipBetween; - String gString = c.substring(currentPosition, currentPosition + componentLength); + String gString = colorString.substring(currentPosition, currentPosition + componentLength); currentPosition += componentLength + skipBetween; - String bString = c.substring(currentPosition, currentPosition + componentLength); + String bString = colorString.substring(currentPosition, currentPosition + componentLength); int r = (int) (Integer.parseInt(rString, 16) * mult); int g = (int) (Integer.parseInt(gString, 16) * mult); int b = (int) (Integer.parseInt(bString, 16) * mult); return 0xFF << 24 | r << 16 | g << 8 | b; - } catch (NumberFormatException | IndexOutOfBoundsException e) { - return 0; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("NumberFormatException: '" + colorString + "'"); + } catch (IndexOutOfBoundsException e) { + throw new IllegalArgumentException("IndexOutOfBoundsException: '" + colorString + "'"); } } /** Try parse a color from a text parameter and into a specified index. */ public void tryParseColor(int intoIndex, String textParameter) { - int c = parse(textParameter); - if (c != 0) mCurrentColors[intoIndex] = c; + try { + mCurrentColors[intoIndex] = parse(textParameter); + } catch (IllegalArgumentException e) { + // Ignore. + } } } diff --git a/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java b/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java index 975c1a5abb..52041d454a 100644 --- a/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java +++ b/terminal-emulator/src/main/java/com/termux/terminal/TerminalEmulator.java @@ -41,45 +41,6 @@ public final class TerminalEmulator { /** Used for invalid data - http://en.wikipedia.org/wiki/Replacement_character#Replacement_character */ public static final int UNICODE_REPLACEMENT_CHAR = 0xFFFD; - /** Escape processing: Not currently in an escape sequence. */ - private static final int ESC_NONE = 0; - /** Escape processing: Have seen an ESC character - proceed to {@link #doEsc(int)} */ - private static final int ESC = 1; - /** Escape processing: Have seen ESC POUND */ - private static final int ESC_POUND = 2; - /** Escape processing: Have seen ESC and a character-set-select ( char */ - private static final int ESC_SELECT_LEFT_PAREN = 3; - /** Escape processing: Have seen ESC and a character-set-select ) char */ - private static final int ESC_SELECT_RIGHT_PAREN = 4; - /** Escape processing: "ESC [" or CSI (Control Sequence Introducer). */ - private static final int ESC_CSI = 6; - /** Escape processing: ESC [ ? */ - private static final int ESC_CSI_QUESTIONMARK = 7; - /** Escape processing: ESC [ $ */ - private static final int ESC_CSI_DOLLAR = 8; - /** Escape processing: ESC % */ - private static final int ESC_PERCENT = 9; - /** Escape processing: ESC ] (AKA OSC - Operating System Controls) */ - private static final int ESC_OSC = 10; - /** Escape processing: ESC ] (AKA OSC - Operating System Controls) ESC */ - private static final int ESC_OSC_ESC = 11; - /** Escape processing: ESC [ > */ - private static final int ESC_CSI_BIGGERTHAN = 12; - /** Escape procession: "ESC P" or Device Control String (DCS) */ - private static final int ESC_P = 13; - /** Escape processing: CSI > */ - private static final int ESC_CSI_QUESTIONMARK_ARG_DOLLAR = 14; - /** Escape processing: CSI $ARGS ' ' */ - private static final int ESC_CSI_ARGS_SPACE = 15; - /** Escape processing: CSI $ARGS '*' */ - private static final int ESC_CSI_ARGS_ASTERIX = 16; - /** Escape processing: CSI " */ - private static final int ESC_CSI_DOUBLE_QUOTE = 17; - /** Escape processing: CSI ' */ - private static final int ESC_CSI_SINGLE_QUOTE = 18; - /** Escape processing: CSI ! */ - private static final int ESC_CSI_EXCLAMATION = 19; - /** The number of parameter arguments. This name comes from the ANSI standard for terminal escape codes. */ private static final int MAX_ESCAPE_PARAMETERS = 16; @@ -183,7 +144,7 @@ public final class TerminalEmulator { private boolean mContinueSequence; /** The current state of the escape sequence state machine. One of the ESC_* constants. */ - private int mEscapeState; + private EscapeState mEscapeState; private final SavedScreenState mSavedStateMain = new SavedScreenState(); private final SavedScreenState mSavedStateAlt = new SavedScreenState(); @@ -550,7 +511,7 @@ public void processCodePoint(int b) { case 0: // Null character (NUL, ^@). Do nothing. break; case 7: // Bell (BEL, ^G, \a). If in an OSC sequence, BEL may terminate a string; otherwise signal bell. - if (mEscapeState == ESC_OSC) + if (mEscapeState instanceof EscOscState) doOsc(b); else mSession.onBell(); @@ -593,18 +554,18 @@ public void processCodePoint(int b) { break; case 24: // CAN. case 26: // SUB. - if (mEscapeState != ESC_NONE) { + if (!(mEscapeState instanceof EscNoneState)) { // FIXME: What is this?? - mEscapeState = ESC_NONE; + mEscapeState = new EscNoneState(); emitCodePoint(127); } break; case 27: // ESC // Starts an escape sequence unless we're parsing a string - if (mEscapeState == ESC_P) { + if (mEscapeState instanceof EscPState) { // XXX: Ignore escape when reading device control sequence, since it may be part of string terminator. return; - } else if (mEscapeState != ESC_OSC) { + } else if (!(mEscapeState instanceof EscOscState)) { startEscapeSequence(); } else { doOsc(b); @@ -612,393 +573,14 @@ public void processCodePoint(int b) { break; default: mContinueSequence = false; - switch (mEscapeState) { - case ESC_NONE: - if (b >= 32) emitCodePoint(b); - break; - case ESC: - doEsc(b); - break; - case ESC_POUND: - doEscPound(b); - break; - case ESC_SELECT_LEFT_PAREN: // Designate G0 Character Set (ISO 2022, VT100). - mUseLineDrawingG0 = (b == '0'); - break; - case ESC_SELECT_RIGHT_PAREN: // Designate G1 Character Set (ISO 2022, VT100). - mUseLineDrawingG1 = (b == '0'); - break; - case ESC_CSI: - doCsi(b); - break; - case ESC_CSI_EXCLAMATION: - if (b == 'p') { // Soft terminal reset (DECSTR, http://vt100.net/docs/vt510-rm/DECSTR). - reset(); - } else { - unknownSequence(b); - } - break; - case ESC_CSI_QUESTIONMARK: - doCsiQuestionMark(b); - break; - case ESC_CSI_BIGGERTHAN: - doCsiBiggerThan(b); - break; - case ESC_CSI_DOLLAR: - boolean originMode = isDecsetInternalBitSet(DECSET_BIT_ORIGIN_MODE); - int effectiveTopMargin = originMode ? mTopMargin : 0; - int effectiveBottomMargin = originMode ? mBottomMargin : mRows; - int effectiveLeftMargin = originMode ? mLeftMargin : 0; - int effectiveRightMargin = originMode ? mRightMargin : mColumns; - switch (b) { - case 'v': // ${CSI}${SRC_TOP}${SRC_LEFT}${SRC_BOTTOM}${SRC_RIGHT}${SRC_PAGE}${DST_TOP}${DST_LEFT}${DST_PAGE}$v" - // Copy rectangular area (DECCRA - http://vt100.net/docs/vt510-rm/DECCRA): - // "If Pbs is greater than Pts, or Pls is greater than Prs, the terminal ignores DECCRA. - // The coordinates of the rectangular area are affected by the setting of origin mode (DECOM). - // DECCRA is not affected by the page margins. - // The copied text takes on the line attributes of the destination area. - // If the value of Pt, Pl, Pb, or Pr exceeds the width or height of the active page, then the value - // is treated as the width or height of that page. - // If the destination area is partially off the page, then DECCRA clips the off-page data. - // DECCRA does not change the active cursor position." - int topSource = Math.min(getArg(0, 1, true) - 1 + effectiveTopMargin, mRows); - int leftSource = Math.min(getArg(1, 1, true) - 1 + effectiveLeftMargin, mColumns); - // Inclusive, so do not subtract one: - int bottomSource = Math.min(Math.max(getArg(2, mRows, true) + effectiveTopMargin, topSource), mRows); - int rightSource = Math.min(Math.max(getArg(3, mColumns, true) + effectiveLeftMargin, leftSource), mColumns); - // int sourcePage = getArg(4, 1, true); - int destionationTop = Math.min(getArg(5, 1, true) - 1 + effectiveTopMargin, mRows); - int destinationLeft = Math.min(getArg(6, 1, true) - 1 + effectiveLeftMargin, mColumns); - // int destinationPage = getArg(7, 1, true); - int heightToCopy = Math.min(mRows - destionationTop, bottomSource - topSource); - int widthToCopy = Math.min(mColumns - destinationLeft, rightSource - leftSource); - mScreen.blockCopy(leftSource, topSource, widthToCopy, heightToCopy, destinationLeft, destionationTop); - break; - case '{': // ${CSI}${TOP}${LEFT}${BOTTOM}${RIGHT}${" - // Selective erase rectangular area (DECSERA - http://www.vt100.net/docs/vt510-rm/DECSERA). - case 'x': // ${CSI}${CHAR};${TOP}${LEFT}${BOTTOM}${RIGHT}$x" - // Fill rectangular area (DECFRA - http://www.vt100.net/docs/vt510-rm/DECFRA). - case 'z': // ${CSI}$${TOP}${LEFT}${BOTTOM}${RIGHT}$z" - // Erase rectangular area (DECERA - http://www.vt100.net/docs/vt510-rm/DECERA). - boolean erase = b != 'x'; - boolean selective = b == '{'; - // Only DECSERA keeps visual attributes, DECERA does not: - boolean keepVisualAttributes = erase && selective; - int argIndex = 0; - int fillChar = erase ? ' ' : getArg(argIndex++, -1, true); - // "Pch can be any value from 32 to 126 or from 160 to 255. If Pch is not in this range, then the - // terminal ignores the DECFRA command": - if ((fillChar >= 32 && fillChar <= 126) || (fillChar >= 160 && fillChar <= 255)) { - // "If the value of Pt, Pl, Pb, or Pr exceeds the width or height of the active page, the value - // is treated as the width or height of that page." - int top = Math.min(getArg(argIndex++, 1, true) + effectiveTopMargin, effectiveBottomMargin + 1); - int left = Math.min(getArg(argIndex++, 1, true) + effectiveLeftMargin, effectiveRightMargin + 1); - int bottom = Math.min(getArg(argIndex++, mRows, true) + effectiveTopMargin, effectiveBottomMargin); - int right = Math.min(getArg(argIndex, mColumns, true) + effectiveLeftMargin, effectiveRightMargin); - long style = getStyle(); - for (int row = top - 1; row < bottom; row++) - for (int col = left - 1; col < right; col++) - if (!selective || (TextStyle.decodeEffect(mScreen.getStyleAt(row, col)) & TextStyle.CHARACTER_ATTRIBUTE_PROTECTED) == 0) - mScreen.setChar(col, row, fillChar, keepVisualAttributes ? mScreen.getStyleAt(row, col) : style); - } - break; - case 'r': // "${CSI}${TOP}${LEFT}${BOTTOM}${RIGHT}${ATTRIBUTES}$r" - // Change attributes in rectangular area (DECCARA - http://vt100.net/docs/vt510-rm/DECCARA). - case 't': // "${CSI}${TOP}${LEFT}${BOTTOM}${RIGHT}${ATTRIBUTES}$t" - // Reverse attributes in rectangular area (DECRARA - http://www.vt100.net/docs/vt510-rm/DECRARA). - boolean reverse = b == 't'; - // FIXME: "coordinates of the rectangular area are affected by the setting of origin mode (DECOM)". - int top = Math.min(getArg(0, 1, true) - 1, effectiveBottomMargin) + effectiveTopMargin; - int left = Math.min(getArg(1, 1, true) - 1, effectiveRightMargin) + effectiveLeftMargin; - int bottom = Math.min(getArg(2, mRows, true) + 1, effectiveBottomMargin - 1) + effectiveTopMargin; - int right = Math.min(getArg(3, mColumns, true) + 1, effectiveRightMargin - 1) + effectiveLeftMargin; - if (mArgIndex >= 4) { - if (mArgIndex >= mArgs.length) mArgIndex = mArgs.length - 1; - for (int i = 4; i <= mArgIndex; i++) { - int bits = 0; - boolean setOrClear = true; // True if setting, false if clearing. - switch (getArg(i, 0, false)) { - case 0: // Attributes off (no bold, no underline, no blink, positive image). - bits = (TextStyle.CHARACTER_ATTRIBUTE_BOLD | TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE | TextStyle.CHARACTER_ATTRIBUTE_BLINK - | TextStyle.CHARACTER_ATTRIBUTE_INVERSE); - if (!reverse) setOrClear = false; - break; - case 1: // Bold. - bits = TextStyle.CHARACTER_ATTRIBUTE_BOLD; - break; - case 4: // Underline. - bits = TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE; - break; - case 5: // Blink. - bits = TextStyle.CHARACTER_ATTRIBUTE_BLINK; - break; - case 7: // Negative image. - bits = TextStyle.CHARACTER_ATTRIBUTE_INVERSE; - break; - case 22: // No bold. - bits = TextStyle.CHARACTER_ATTRIBUTE_BOLD; - setOrClear = false; - break; - case 24: // No underline. - bits = TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE; - setOrClear = false; - break; - case 25: // No blink. - bits = TextStyle.CHARACTER_ATTRIBUTE_BLINK; - setOrClear = false; - break; - case 27: // Positive image. - bits = TextStyle.CHARACTER_ATTRIBUTE_INVERSE; - setOrClear = false; - break; - } - if (reverse && !setOrClear) { - // Reverse attributes in rectangular area ignores non-(1,4,5,7) bits. - } else { - mScreen.setOrClearEffect(bits, setOrClear, reverse, isDecsetInternalBitSet(DECSET_BIT_RECTANGULAR_CHANGEATTRIBUTE), - effectiveLeftMargin, effectiveRightMargin, top, left, bottom, right); - } - } - } else { - // Do nothing. - } - break; - default: - unknownSequence(b); - } - break; - case ESC_CSI_DOUBLE_QUOTE: - if (b == 'q') { - // http://www.vt100.net/docs/vt510-rm/DECSCA - int arg = getArg0(0); - if (arg == 0 || arg == 2) { - // DECSED and DECSEL can erase characters. - mEffect &= ~TextStyle.CHARACTER_ATTRIBUTE_PROTECTED; - } else if (arg == 1) { - // DECSED and DECSEL cannot erase characters. - mEffect |= TextStyle.CHARACTER_ATTRIBUTE_PROTECTED; - } else { - unknownSequence(b); - } - } else { - unknownSequence(b); - } - break; - case ESC_CSI_SINGLE_QUOTE: - if (b == '}') { // Insert Ps Column(s) (default = 1) (DECIC), VT420 and up. - int columnsAfterCursor = mRightMargin - mCursorCol; - int columnsToInsert = Math.min(getArg0(1), columnsAfterCursor); - int columnsToMove = columnsAfterCursor - columnsToInsert; - mScreen.blockCopy(mCursorCol, 0, columnsToMove, mRows, mCursorCol + columnsToInsert, 0); - blockClear(mCursorCol, 0, columnsToInsert, mRows); - } else if (b == '~') { // Delete Ps Column(s) (default = 1) (DECDC), VT420 and up. - int columnsAfterCursor = mRightMargin - mCursorCol; - int columnsToDelete = Math.min(getArg0(1), columnsAfterCursor); - int columnsToMove = columnsAfterCursor - columnsToDelete; - mScreen.blockCopy(mCursorCol + columnsToDelete, 0, columnsToMove, mRows, mCursorCol, 0); - } else { - unknownSequence(b); - } - break; - case ESC_PERCENT: - break; - case ESC_OSC: - doOsc(b); - break; - case ESC_OSC_ESC: - doOscEsc(b); - break; - case ESC_P: - doDeviceControl(b); - break; - case ESC_CSI_QUESTIONMARK_ARG_DOLLAR: - if (b == 'p') { - // Request DEC private mode (DECRQM). - int mode = getArg0(0); - int value; - if (mode == 47 || mode == 1047 || mode == 1049) { - // This state is carried by mScreen pointer. - value = (mScreen == mAltBuffer) ? 1 : 2; - } else { - int internalBit = mapDecSetBitToInternalBit(mode); - if (internalBit != -1) { - value = isDecsetInternalBitSet(internalBit) ? 1 : 2; // 1=set, 2=reset. - } else { - Logger.logError(mClient, LOG_TAG, "Got DECRQM for unrecognized private DEC mode=" + mode); - value = 0; // 0=not recognized, 3=permanently set, 4=permanently reset - } - } - mSession.write(String.format(Locale.US, "\033[?%d;%d$y", mode, value)); - } else { - unknownSequence(b); - } - break; - case ESC_CSI_ARGS_SPACE: - int arg = getArg0(0); - switch (b) { - case 'q': // "${CSI}${STYLE} q" - set cursor style (http://www.vt100.net/docs/vt510-rm/DECSCUSR). - switch (arg) { - case 0: // Blinking block. - case 1: // Blinking block. - case 2: // Steady block. - mCursorStyle = TERMINAL_CURSOR_STYLE_BLOCK; - break; - case 3: // Blinking underline. - case 4: // Steady underline. - mCursorStyle = TERMINAL_CURSOR_STYLE_UNDERLINE; - break; - case 5: // Blinking bar (xterm addition). - case 6: // Steady bar (xterm addition). - mCursorStyle = TERMINAL_CURSOR_STYLE_BAR; - break; - } - break; - case 't': - case 'u': - // Set margin-bell volume - ignore. - break; - default: - unknownSequence(b); - } - break; - case ESC_CSI_ARGS_ASTERIX: - int attributeChangeExtent = getArg0(0); - if (b == 'x' && (attributeChangeExtent >= 0 && attributeChangeExtent <= 2)) { - // Select attribute change extent (DECSACE - http://www.vt100.net/docs/vt510-rm/DECSACE). - setDecsetinternalBit(DECSET_BIT_RECTANGULAR_CHANGEATTRIBUTE, attributeChangeExtent == 2); - } else { - unknownSequence(b); - } - break; - default: - unknownSequence(b); - break; - } - if (!mContinueSequence) mEscapeState = ESC_NONE; - break; - } - } - - /** When in {@link #ESC_P} ("device control") sequence. */ - private void doDeviceControl(int b) { - switch (b) { - case (byte) '\\': // End of ESC \ string Terminator - { - String dcs = mOSCOrDeviceControlArgs.toString(); - // DCS $ q P t ST. Request Status String (DECRQSS) - if (dcs.startsWith("$q")) { - if (dcs.equals("$q\"p")) { - // DECSCL, conformance level, http://www.vt100.net/docs/vt510-rm/DECSCL: - String csiString = "64;1\"p"; - mSession.write("\033P1$r" + csiString + "\033\\"); - } else { - finishSequenceAndLogError("Unrecognized DECRQSS string: '" + dcs + "'"); - } - } else if (dcs.startsWith("+q")) { - // Request Termcap/Terminfo String. The string following the "q" is a list of names encoded in - // hexadecimal (2 digits per character) separated by ; which correspond to termcap or terminfo key - // names. - // Two special features are also recognized, which are not key names: Co for termcap colors (or colors - // for terminfo colors), and TN for termcap name (or name for terminfo name). - // xterm responds with DCS 1 + r P t ST for valid requests, adding to P t an = , and the value of the - // corresponding string that xterm would send, or DCS 0 + r P t ST for invalid requests. The strings are - // encoded in hexadecimal (2 digits per character). - // Example: - // :kr=\EOC: ks=\E[?1h\E=: ku=\EOA: le=^H:mb=\E[5m:md=\E[1m:\ - // where - // kd=down-arrow key - // kl=left-arrow key - // kr=right-arrow key - // ku=up-arrow key - // #2=key_shome, "shifted home" - // #4=key_sleft, "shift arrow left" - // %i=key_sright, "shift arrow right" - // *7=key_send, "shifted end" - // k1=F1 function key - - // Example: Request for ku is "ESC P + q 6 b 7 5 ESC \", where 6b7d=ku in hexadecimal. - // Xterm response in normal cursor mode: - // "<27> P 1 + r 6 b 7 5 = 1 B 5 B 4 1" where 0x1B 0x5B 0x41 = 27 91 65 = ESC [ A - // Xterm response in application cursor mode: - // "<27> P 1 + r 6 b 7 5 = 1 B 5 B 4 1" where 0x1B 0x4F 0x41 = 27 91 65 = ESC 0 A - - // #4 is "shift arrow left": - // *** Device Control (DCS) for '#4'- 'ESC P + q 23 34 ESC \' - // Response: <27> P 1 + r 2 3 3 4 = 1 B 5 B 3 1 3 B 3 2 4 4 <27> \ - // where 0x1B 0x5B 0x31 0x3B 0x32 0x44 = ESC [ 1 ; 2 D - // which we find in: TermKeyListener.java: KEY_MAP.put(KEYMOD_SHIFT | KEYCODE_DPAD_LEFT, "\033[1;2D"); - - // See http://h30097.www3.hp.com/docs/base_doc/DOCUMENTATION/V40G_HTML/MAN/MAN4/0178____.HTM for what to - // respond, as well as http://www.freebsd.org/cgi/man.cgi?query=termcap&sektion=5#CAPABILITIES for - // the meaning of e.g. "ku", "kd", "kr", "kl" - - for (String part : dcs.substring(2).split(";")) { - if (part.length() % 2 == 0) { - StringBuilder transBuffer = new StringBuilder(); - char c; - for (int i = 0; i < part.length(); i += 2) { - try { - c = (char) Long.decode("0x" + part.charAt(i) + "" + part.charAt(i + 1)).longValue(); - } catch (NumberFormatException e) { - Logger.logStackTraceWithMessage(mClient, LOG_TAG, "Invalid device termcap/terminfo encoded name \"" + part + "\"", e); - continue; - } - transBuffer.append(c); - } - - String trans = transBuffer.toString(); - String responseValue; - switch (trans) { - case "Co": - case "colors": - responseValue = "256"; // Number of colors. - break; - case "TN": - case "name": - responseValue = "xterm"; - break; - default: - responseValue = KeyHandler.getCodeFromTermcap(trans, isDecsetInternalBitSet(DECSET_BIT_APPLICATION_CURSOR_KEYS), - isDecsetInternalBitSet(DECSET_BIT_APPLICATION_KEYPAD)); - break; - } - if (responseValue == null) { - switch (trans) { - case "%1": // Help key - ignore - case "&8": // Undo key - ignore. - break; - default: - Logger.logWarn(mClient, LOG_TAG, "Unhandled termcap/terminfo name: '" + trans + "'"); - } - // Respond with invalid request: - mSession.write("\033P0+r" + part + "\033\\"); - } else { - StringBuilder hexEncoded = new StringBuilder(); - for (int j = 0; j < responseValue.length(); j++) { - hexEncoded.append(String.format("%02X", (int) responseValue.charAt(j))); - } - mSession.write("\033P1+r" + part + "=" + hexEncoded + "\033\\"); - } - } else { - Logger.logError(mClient, LOG_TAG, "Invalid device termcap/terminfo name of odd length: " + part); - } - } - } else { - if (LOG_ESCAPE_SEQUENCES) - Logger.logError(mClient, LOG_TAG, "Unrecognized device control string: " + dcs); + if (mEscapeState != null) { + mEscapeState.processCodePoint(b); } - finishSequence(); - } - break; - default: - if (mOSCOrDeviceControlArgs.length() > MAX_OSC_STRING_LENGTH) { - // Too long. - mOSCOrDeviceControlArgs.setLength(0); - finishSequence(); - } else { - mOSCOrDeviceControlArgs.appendCodePoint(b); - continueSequence(mEscapeState); + else { + unknownSequence(b); } + if (!mContinueSequence) mEscapeState = new EscNoneState(); + break; } } @@ -1008,91 +590,6 @@ private int nextTabStop(int numTabs) { return mRightMargin - 1; } - /** Process byte while in the {@link #ESC_CSI_QUESTIONMARK} escape state. */ - private void doCsiQuestionMark(int b) { - switch (b) { - case 'J': // Selective erase in display (DECSED) - http://www.vt100.net/docs/vt510-rm/DECSED. - case 'K': // Selective erase in line (DECSEL) - http://vt100.net/docs/vt510-rm/DECSEL. - mAboutToAutoWrap = false; - int fillChar = ' '; - int startCol = -1; - int startRow = -1; - int endCol = -1; - int endRow = -1; - boolean justRow = (b == 'K'); - switch (getArg0(0)) { - case 0: // Erase from the active position to the end, inclusive (default). - startCol = mCursorCol; - startRow = mCursorRow; - endCol = mColumns; - endRow = justRow ? (mCursorRow + 1) : mRows; - break; - case 1: // Erase from start to the active position, inclusive. - startCol = 0; - startRow = justRow ? mCursorRow : 0; - endCol = mCursorCol + 1; - endRow = mCursorRow + 1; - break; - case 2: // Erase all of the display/line. - startCol = 0; - startRow = justRow ? mCursorRow : 0; - endCol = mColumns; - endRow = justRow ? (mCursorRow + 1) : mRows; - break; - default: - unknownSequence(b); - break; - } - long style = getStyle(); - for (int row = startRow; row < endRow; row++) { - for (int col = startCol; col < endCol; col++) { - if ((TextStyle.decodeEffect(mScreen.getStyleAt(row, col)) & TextStyle.CHARACTER_ATTRIBUTE_PROTECTED) == 0) - mScreen.setChar(col, row, fillChar, style); - } - } - break; - case 'h': - case 'l': - if (mArgIndex >= mArgs.length) mArgIndex = mArgs.length - 1; - for (int i = 0; i <= mArgIndex; i++) - doDecSetOrReset(b == 'h', mArgs[i]); - break; - case 'n': // Device Status Report (DSR, DEC-specific). - switch (getArg0(-1)) { - case 6: - // Extended Cursor Position (DECXCPR - http://www.vt100.net/docs/vt510-rm/DECXCPR). Page=1. - mSession.write(String.format(Locale.US, "\033[?%d;%d;1R", mCursorRow + 1, mCursorCol + 1)); - break; - default: - finishSequence(); - return; - } - break; - case 'r': - case 's': - if (mArgIndex >= mArgs.length) mArgIndex = mArgs.length - 1; - for (int i = 0; i <= mArgIndex; i++) { - int externalBit = mArgs[i]; - int internalBit = mapDecSetBitToInternalBit(externalBit); - if (internalBit == -1) { - Logger.logWarn(mClient, LOG_TAG, "Ignoring request to save/recall decset bit=" + externalBit); - } else { - if (b == 's') { - mSavedDecSetFlags |= internalBit; - } else { - doDecSetOrReset((mSavedDecSetFlags & internalBit) != 0, externalBit); - } - } - } - break; - case '$': - continueSequence(ESC_CSI_QUESTIONMARK_ARG_DOLLAR); - return; - default: - parseArg(b); - } - } - public void doDecSetOrReset(boolean setting, int externalBit) { int internalBit = mapDecSetBitToInternalBit(externalBit); if (internalBit != -1) { @@ -1193,88 +690,8 @@ public void doDecSetOrReset(boolean setting, int externalBit) { } } - private void doCsiBiggerThan(int b) { - switch (b) { - case 'c': // "${CSI}>c" or "${CSI}>c". Secondary Device Attributes (DA2). - // Originally this was used for the terminal to respond with "identification code, firmware version level, - // and hardware options" (http://vt100.net/docs/vt510-rm/DA2), with the first "41" meaning the VT420 - // terminal type. This is not used anymore, but the second version level field has been changed by xterm - // to mean it's release number ("patch numbers" listed at http://invisible-island.net/xterm/xterm.log.html), - // and some applications use it as a feature check: - // * tmux used to have a "xterm won't reach version 500 for a while so set that as the upper limit" check, - // and then check "xterm_version > 270" if rectangular area operations such as DECCRA could be used. - // * vim checks xterm version number >140 for "Request termcap/terminfo string" functionality >276 for SGR - // mouse report. - // The third number is a keyboard identifier not used nowadays. - mSession.write("\033[>41;320;0c"); - break; - case 'm': - // https://bugs.launchpad.net/gnome-terminal/+bug/96676/comments/25 - // Depending on the first number parameter, this can set one of the xterm resources - // modifyKeyboard, modifyCursorKeys, modifyFunctionKeys and modifyOtherKeys. - // http://invisible-island.net/xterm/manpage/xterm.html#RESOURCES - - // * modifyKeyboard (parameter=1): - // Normally xterm makes a special case regarding modifiers (shift, control, etc.) to handle special keyboard - // layouts (legacy and vt220). This is done to provide compatible keyboards for DEC VT220 and related - // terminals that implement user-defined keys (UDK). - // The bits of the resource value selectively enable modification of the given category when these keyboards - // are selected. The default is "0": - // (0) The legacy/vt220 keyboards interpret only the Control-modifier when constructing numbered - // function-keys. Other special keys are not modified. - // (1) allows modification of the numeric keypad - // (2) allows modification of the editing keypad - // (4) allows modification of function-keys, overrides use of Shift-modifier for UDK. - // (8) allows modification of other special keys - - // * modifyCursorKeys (parameter=2): - // Tells how to handle the special case where Control-, Shift-, Alt- or Meta-modifiers are used to add a - // parameter to the escape sequence returned by a cursor-key. The default is "2". - // - Set it to -1 to disable it. - // - Set it to 0 to use the old/obsolete behavior. - // - Set it to 1 to prefix modified sequences with CSI. - // - Set it to 2 to force the modifier to be the second parameter if it would otherwise be the first. - // - Set it to 3 to mark the sequence with a ">" to hint that it is private. - - // * modifyFunctionKeys (parameter=3): - // Tells how to handle the special case where Control-, Shift-, Alt- or Meta-modifiers are used to add a - // parameter to the escape sequence returned by a (numbered) function- - // key. The default is "2". The resource values are similar to modifyCursorKeys: - // Set it to -1 to permit the user to use shift- and control-modifiers to construct function-key strings - // using the normal encoding scheme. - // - Set it to 0 to use the old/obsolete behavior. - // - Set it to 1 to prefix modified sequences with CSI. - // - Set it to 2 to force the modifier to be the second parameter if it would otherwise be the first. - // - Set it to 3 to mark the sequence with a ">" to hint that it is private. - // If modifyFunctionKeys is zero, xterm uses Control- and Shift-modifiers to allow the user to construct - // numbered function-keys beyond the set provided by the keyboard: - // (Control) adds the value given by the ctrlFKeys resource. - // (Shift) adds twice the value given by the ctrlFKeys resource. - // (Control/Shift) adds three times the value given by the ctrlFKeys resource. - // - // As a special case, legacy (when oldFunctionKeys is true) or vt220 (when sunKeyboard is true) - // keyboards interpret only the Control-modifier when constructing numbered function-keys. - // This is done to provide compatible keyboards for DEC VT220 and related terminals that - // implement user-defined keys (UDK). - - // * modifyOtherKeys (parameter=4): - // Like modifyCursorKeys, tells xterm to construct an escape sequence for other keys (such as "2") when - // modified by Control-, Alt- or Meta-modifiers. This feature does not apply to function keys and - // well-defined keys such as ESC or the control keys. The default is "0". - // (0) disables this feature. - // (1) enables this feature for keys except for those with well-known behavior, e.g., Tab, Backarrow and - // some special control character cases, e.g., Control-Space to make a NUL. - // (2) enables this feature for keys including the exceptions listed. - Logger.logError(mClient, LOG_TAG, "(ignored) CSI > MODIFY RESOURCE: " + getArg0(-1) + " to " + getArg1(-1)); - break; - default: - parseArg(b); - break; - } - } - private void startEscapeSequence() { - mEscapeState = ESC; + mEscapeState = new EscState(); mArgIndex = 0; Arrays.fill(mArgs, -1); } @@ -1296,113 +713,11 @@ private void doLinefeed() { } } - private void continueSequence(int state) { + private void continueSequence(EscapeState state) { mEscapeState = state; mContinueSequence = true; } - private void doEscPound(int b) { - switch (b) { - case '8': // Esc # 8 - DEC screen alignment test - fill screen with E's. - mScreen.blockSet(0, 0, mColumns, mRows, 'E', getStyle()); - break; - default: - unknownSequence(b); - break; - } - } - - /** Encountering a character in the {@link #ESC} state. */ - private void doEsc(int b) { - switch (b) { - case '#': - continueSequence(ESC_POUND); - break; - case '(': - continueSequence(ESC_SELECT_LEFT_PAREN); - break; - case ')': - continueSequence(ESC_SELECT_RIGHT_PAREN); - break; - case '6': // Back index (http://www.vt100.net/docs/vt510-rm/DECBI). Move left, insert blank column if start. - if (mCursorCol > mLeftMargin) { - mCursorCol--; - } else { - int rows = mBottomMargin - mTopMargin; - mScreen.blockCopy(mLeftMargin, mTopMargin, mRightMargin - mLeftMargin - 1, rows, mLeftMargin + 1, mTopMargin); - mScreen.blockSet(mLeftMargin, mTopMargin, 1, rows, ' ', TextStyle.encode(mForeColor, mBackColor, 0)); - } - break; - case '7': // DECSC save cursor - http://www.vt100.net/docs/vt510-rm/DECSC - saveCursor(); - break; - case '8': // DECRC restore cursor - http://www.vt100.net/docs/vt510-rm/DECRC - restoreCursor(); - break; - case '9': // Forward Index (http://www.vt100.net/docs/vt510-rm/DECFI). Move right, insert blank column if end. - if (mCursorCol < mRightMargin - 1) { - mCursorCol++; - } else { - int rows = mBottomMargin - mTopMargin; - mScreen.blockCopy(mLeftMargin + 1, mTopMargin, mRightMargin - mLeftMargin - 1, rows, mLeftMargin, mTopMargin); - mScreen.blockSet(mRightMargin - 1, mTopMargin, 1, rows, ' ', TextStyle.encode(mForeColor, mBackColor, 0)); - } - break; - case 'c': // RIS - Reset to Initial State (http://vt100.net/docs/vt510-rm/RIS). - reset(); - mMainBuffer.clearTranscript(); - blockClear(0, 0, mColumns, mRows); - setCursorPosition(0, 0); - break; - case 'D': // INDEX - doLinefeed(); - break; - case 'E': // Next line (http://www.vt100.net/docs/vt510-rm/NEL). - setCursorCol(isDecsetInternalBitSet(DECSET_BIT_ORIGIN_MODE) ? mLeftMargin : 0); - doLinefeed(); - break; - case 'F': // Cursor to lower-left corner of screen - setCursorRowCol(0, mBottomMargin - 1); - break; - case 'H': // Tab set - mTabStop[mCursorCol] = true; - break; - case 'M': // "${ESC}M" - reverse index (RI). - // http://www.vt100.net/docs/vt100-ug/chapter3.html: "Move the active position to the same horizontal - // position on the preceding line. If the active position is at the top margin, a scroll down is performed". - if (mCursorRow <= mTopMargin) { - mScreen.blockCopy(0, mTopMargin, mColumns, mBottomMargin - (mTopMargin + 1), 0, mTopMargin + 1); - blockClear(0, mTopMargin, mColumns); - } else { - mCursorRow--; - } - break; - case 'N': // SS2, ignore. - case '0': // SS3, ignore. - break; - case 'P': // Device control string - mOSCOrDeviceControlArgs.setLength(0); - continueSequence(ESC_P); - break; - case '[': - continueSequence(ESC_CSI); - break; - case '=': // DECKPAM - setDecsetinternalBit(DECSET_BIT_APPLICATION_KEYPAD, true); - break; - case ']': // OSC - mOSCOrDeviceControlArgs.setLength(0); - continueSequence(ESC_OSC); - break; - case '>': // DECKPNM - setDecsetinternalBit(DECSET_BIT_APPLICATION_KEYPAD, false); - break; - default: - unknownSequence(b); - break; - } - } - /** DECSC save cursor - http://www.vt100.net/docs/vt510-rm/DECSC . See {@link #restoreCursor()}. */ private void saveCursor() { SavedScreenState state = (mScreen == mMainBuffer) ? mSavedStateMain : mSavedStateAlt; @@ -1431,327 +746,6 @@ private void restoreCursor() { mUseLineDrawingUsesG0 = state.mUseLineDrawingUsesG0; } - /** Following a CSI - Control Sequence Introducer, "\033[". {@link #ESC_CSI}. */ - private void doCsi(int b) { - switch (b) { - case '!': - continueSequence(ESC_CSI_EXCLAMATION); - break; - case '"': - continueSequence(ESC_CSI_DOUBLE_QUOTE); - break; - case '\'': - continueSequence(ESC_CSI_SINGLE_QUOTE); - break; - case '$': - continueSequence(ESC_CSI_DOLLAR); - break; - case '*': - continueSequence(ESC_CSI_ARGS_ASTERIX); - break; - case '@': { - // "CSI{n}@" - Insert ${n} space characters (ICH) - http://www.vt100.net/docs/vt510-rm/ICH. - mAboutToAutoWrap = false; - int columnsAfterCursor = mColumns - mCursorCol; - int spacesToInsert = Math.min(getArg0(1), columnsAfterCursor); - int charsToMove = columnsAfterCursor - spacesToInsert; - mScreen.blockCopy(mCursorCol, mCursorRow, charsToMove, 1, mCursorCol + spacesToInsert, mCursorRow); - blockClear(mCursorCol, mCursorRow, spacesToInsert); - } - break; - case 'A': // "CSI${n}A" - Cursor up (CUU) ${n} rows. - setCursorRow(Math.max(0, mCursorRow - getArg0(1))); - break; - case 'B': // "CSI${n}B" - Cursor down (CUD) ${n} rows. - setCursorRow(Math.min(mRows - 1, mCursorRow + getArg0(1))); - break; - case 'C': // "CSI${n}C" - Cursor forward (CUF). - case 'a': // "CSI${n}a" - Horizontal position relative (HPR). From ISO-6428/ECMA-48. - setCursorCol(Math.min(mRightMargin - 1, mCursorCol + getArg0(1))); - break; - case 'D': // "CSI${n}D" - Cursor backward (CUB) ${n} columns. - setCursorCol(Math.max(mLeftMargin, mCursorCol - getArg0(1))); - break; - case 'E': // "CSI{n}E - Cursor Next Line (CNL). From ISO-6428/ECMA-48. - setCursorPosition(0, mCursorRow + getArg0(1)); - break; - case 'F': // "CSI{n}F - Cursor Previous Line (CPL). From ISO-6428/ECMA-48. - setCursorPosition(0, mCursorRow - getArg0(1)); - break; - case 'G': // "CSI${n}G" - Cursor horizontal absolute (CHA) to column ${n}. - setCursorCol(Math.min(Math.max(1, getArg0(1)), mColumns) - 1); - break; - case 'H': // "${CSI}${ROW};${COLUMN}H" - Cursor position (CUP). - case 'f': // "${CSI}${ROW};${COLUMN}f" - Horizontal and Vertical Position (HVP). - setCursorPosition(getArg1(1) - 1, getArg0(1) - 1); - break; - case 'I': // Cursor Horizontal Forward Tabulation (CHT). Move the active position n tabs forward. - setCursorCol(nextTabStop(getArg0(1))); - break; - case 'J': // "${CSI}${0,1,2,3}J" - Erase in Display (ED) - // ED ignores the scrolling margins. - switch (getArg0(0)) { - case 0: // Erase from the active position to the end of the screen, inclusive (default). - blockClear(mCursorCol, mCursorRow, mColumns - mCursorCol); - blockClear(0, mCursorRow + 1, mColumns, mRows - (mCursorRow + 1)); - break; - case 1: // Erase from start of the screen to the active position, inclusive. - blockClear(0, 0, mColumns, mCursorRow); - blockClear(0, mCursorRow, mCursorCol + 1); - break; - case 2: // Erase all of the display - all lines are erased, changed to single-width, and the cursor does not - // move.. - blockClear(0, 0, mColumns, mRows); - break; - case 3: // Delete all lines saved in the scrollback buffer (xterm etc) - mMainBuffer.clearTranscript(); - break; - default: - unknownSequence(b); - return; - } - mAboutToAutoWrap = false; - break; - case 'K': // "CSI{n}K" - Erase in line (EL). - switch (getArg0(0)) { - case 0: // Erase from the cursor to the end of the line, inclusive (default) - blockClear(mCursorCol, mCursorRow, mColumns - mCursorCol); - break; - case 1: // Erase from the start of the screen to the cursor, inclusive. - blockClear(0, mCursorRow, mCursorCol + 1); - break; - case 2: // Erase all of the line. - blockClear(0, mCursorRow, mColumns); - break; - default: - unknownSequence(b); - return; - } - mAboutToAutoWrap = false; - break; - case 'L': // "${CSI}{N}L" - insert ${N} lines (IL). - { - int linesAfterCursor = mBottomMargin - mCursorRow; - int linesToInsert = Math.min(getArg0(1), linesAfterCursor); - int linesToMove = linesAfterCursor - linesToInsert; - mScreen.blockCopy(0, mCursorRow, mColumns, linesToMove, 0, mCursorRow + linesToInsert); - blockClear(0, mCursorRow, mColumns, linesToInsert); - } - break; - case 'M': // "${CSI}${N}M" - delete N lines (DL). - { - mAboutToAutoWrap = false; - int linesAfterCursor = mBottomMargin - mCursorRow; - int linesToDelete = Math.min(getArg0(1), linesAfterCursor); - int linesToMove = linesAfterCursor - linesToDelete; - mScreen.blockCopy(0, mCursorRow + linesToDelete, mColumns, linesToMove, 0, mCursorRow); - blockClear(0, mCursorRow + linesToMove, mColumns, linesToDelete); - } - break; - case 'P': // "${CSI}{N}P" - delete ${N} characters (DCH). - { - // http://www.vt100.net/docs/vt510-rm/DCH: "If ${N} is greater than the number of characters between the - // cursor and the right margin, then DCH only deletes the remaining characters. - // As characters are deleted, the remaining characters between the cursor and right margin move to the left. - // Character attributes move with the characters. The terminal adds blank spaces with no visual character - // attributes at the right margin. DCH has no effect outside the scrolling margins." - mAboutToAutoWrap = false; - int cellsAfterCursor = mColumns - mCursorCol; - int cellsToDelete = Math.min(getArg0(1), cellsAfterCursor); - int cellsToMove = cellsAfterCursor - cellsToDelete; - mScreen.blockCopy(mCursorCol + cellsToDelete, mCursorRow, cellsToMove, 1, mCursorCol, mCursorRow); - blockClear(mCursorCol + cellsToMove, mCursorRow, cellsToDelete); - } - break; - case 'S': { // "${CSI}${N}S" - scroll up ${N} lines (default = 1) (SU). - final int linesToScroll = getArg0(1); - for (int i = 0; i < linesToScroll; i++) - scrollDownOneLine(); - break; - } - case 'T': - if (mArgIndex == 0) { - // "${CSI}${N}T" - Scroll down N lines (default = 1) (SD). - // http://vt100.net/docs/vt510-rm/SD: "N is the number of lines to move the user window up in page - // memory. N new lines appear at the top of the display. N old lines disappear at the bottom of the - // display. You cannot pan past the top margin of the current page". - final int linesToScrollArg = getArg0(1); - final int linesBetweenTopAndBottomMargins = mBottomMargin - mTopMargin; - final int linesToScroll = Math.min(linesBetweenTopAndBottomMargins, linesToScrollArg); - mScreen.blockCopy(0, mTopMargin, mColumns, linesBetweenTopAndBottomMargins - linesToScroll, 0, mTopMargin + linesToScroll); - blockClear(0, mTopMargin, mColumns, linesToScroll); - } else { - // "${CSI}${func};${startx};${starty};${firstrow};${lastrow}T" - initiate highlight mouse tracking. - unimplementedSequence(b); - } - break; - case 'X': // "${CSI}${N}X" - Erase ${N:=1} character(s) (ECH). FIXME: Clears character attributes? - mAboutToAutoWrap = false; - mScreen.blockSet(mCursorCol, mCursorRow, Math.min(getArg0(1), mColumns - mCursorCol), 1, ' ', getStyle()); - break; - case 'Z': // Cursor Backward Tabulation (CBT). Move the active position n tabs backward. - int numberOfTabs = getArg0(1); - int newCol = mLeftMargin; - for (int i = mCursorCol - 1; i >= 0; i--) - if (mTabStop[i]) { - if (--numberOfTabs == 0) { - newCol = Math.max(i, mLeftMargin); - break; - } - } - mCursorCol = newCol; - break; - case '?': // Esc [ ? -- start of a private mode set - continueSequence(ESC_CSI_QUESTIONMARK); - break; - case '>': // "Esc [ >" -- - continueSequence(ESC_CSI_BIGGERTHAN); - break; - case '`': // Horizontal position absolute (HPA - http://www.vt100.net/docs/vt510-rm/HPA). - setCursorColRespectingOriginMode(getArg0(1) - 1); - break; - case 'b': // Repeat the preceding graphic character Ps times (REP). - if (mLastEmittedCodePoint == -1) break; - final int numRepeat = getArg0(1); - for (int i = 0; i < numRepeat; i++) emitCodePoint(mLastEmittedCodePoint); - break; - case 'c': // Primary Device Attributes (http://www.vt100.net/docs/vt510-rm/DA1) if argument is missing or zero. - // The important part that may still be used by some (tmux stores this value but does not currently use it) - // is the first response parameter identifying the terminal service class, where we send 64 for "vt420". - // This is followed by a list of attributes which is probably unused by applications. Send like xterm. - if (getArg0(0) == 0) mSession.write("\033[?64;1;2;6;9;15;18;21;22c"); - break; - case 'd': // ESC [ Pn d - Vert Position Absolute - setCursorRow(Math.min(Math.max(1, getArg0(1)), mRows) - 1); - break; - case 'e': // Vertical Position Relative (VPR). From ISO-6429 (ECMA-48). - setCursorPosition(mCursorCol, mCursorRow + getArg0(1)); - break; - // case 'f': "${CSI}${ROW};${COLUMN}f" - Horizontal and Vertical Position (HVP). Grouped with case 'H'. - case 'g': // Clear tab stop - switch (getArg0(0)) { - case 0: - mTabStop[mCursorCol] = false; - break; - case 3: - for (int i = 0; i < mColumns; i++) { - mTabStop[i] = false; - } - break; - default: - // Specified to have no effect. - break; - } - break; - case 'h': // Set Mode - doSetMode(true); - break; - case 'l': // Reset Mode - doSetMode(false); - break; - case 'm': // Esc [ Pn m - character attributes. (can have up to 16 numerical arguments) - selectGraphicRendition(); - break; - case 'n': // Esc [ Pn n - ECMA-48 Status Report Commands - // sendDeviceAttributes() - switch (getArg0(0)) { - case 5: // Device status report (DSR): - // Answer is ESC [ 0 n (Terminal OK). - byte[] dsr = {(byte) 27, (byte) '[', (byte) '0', (byte) 'n'}; - mSession.write(dsr, 0, dsr.length); - break; - case 6: // Cursor position report (CPR): - // Answer is ESC [ y ; x R, where x,y is - // the cursor location. - mSession.write(String.format(Locale.US, "\033[%d;%dR", mCursorRow + 1, mCursorCol + 1)); - break; - default: - break; - } - break; - case 'r': // "CSI${top};${bottom}r" - set top and bottom Margins (DECSTBM). - { - // https://vt100.net/docs/vt510-rm/DECSTBM.html - // The top margin defaults to 1, the bottom margin defaults to mRows. - // The escape sequence numbers top 1..23, but we number top 0..22. - // The escape sequence numbers bottom 2..24, and so do we (because we use a zero based numbering - // scheme, but we store the first line below the bottom-most scrolling line. - // As a result, we adjust the top line by -1, but we leave the bottom line alone. - // Also require that top + 2 <= bottom. - mTopMargin = Math.max(0, Math.min(getArg0(1) - 1, mRows - 2)); - mBottomMargin = Math.max(mTopMargin + 2, Math.min(getArg1(mRows), mRows)); - - // DECSTBM moves the cursor to column 1, line 1 of the page respecting origin mode. - setCursorPosition(0, 0); - } - break; - case 's': - if (isDecsetInternalBitSet(DECSET_BIT_LEFTRIGHT_MARGIN_MODE)) { - // Set left and right margins (DECSLRM - http://www.vt100.net/docs/vt510-rm/DECSLRM). - mLeftMargin = Math.min(getArg0(1) - 1, mColumns - 2); - mRightMargin = Math.max(mLeftMargin + 1, Math.min(getArg1(mColumns), mColumns)); - // DECSLRM moves the cursor to column 1, line 1 of the page. - setCursorPosition(0, 0); - } else { - // Save cursor (ANSI.SYS), available only when DECLRMM is disabled. - saveCursor(); - } - break; - case 't': // Window manipulation (from dtterm, as well as extensions) - switch (getArg0(0)) { - case 11: // Report xterm window state. If the xterm window is open (non-iconified), it returns CSI 1 t . - mSession.write("\033[1t"); - break; - case 13: // Report xterm window position. Result is CSI 3 ; x ; y t - mSession.write("\033[3;0;0t"); - break; - case 14: // Report xterm window in pixels. Result is CSI 4 ; height ; width t - // We just report characters time 12 here. - mSession.write(String.format(Locale.US, "\033[4;%d;%dt", mRows * 12, mColumns * 12)); - break; - case 18: // Report the size of the text area in characters. Result is CSI 8 ; height ; width t - mSession.write(String.format(Locale.US, "\033[8;%d;%dt", mRows, mColumns)); - break; - case 19: // Report the size of the screen in characters. Result is CSI 9 ; height ; width t - // We report the same size as the view, since it's the view really isn't resizable from the shell. - mSession.write(String.format(Locale.US, "\033[9;%d;%dt", mRows, mColumns)); - break; - case 20: // Report xterm windows icon label. Result is OSC L label ST. Disabled due to security concerns: - mSession.write("\033]LIconLabel\033\\"); - break; - case 21: // Report xterm windows title. Result is OSC l label ST. Disabled due to security concerns: - mSession.write("\033]l\033\\"); - break; - case 22: - // 22;0 -> Save xterm icon and window title on stack. - // 22;1 -> Save xterm icon title on stack. - // 22;2 -> Save xterm window title on stack. - mTitleStack.push(mTitle); - if (mTitleStack.size() > 20) { - // Limit size - mTitleStack.remove(0); - } - break; - case 23: // Like 22 above but restore from stack. - if (!mTitleStack.isEmpty()) setTitle(mTitleStack.pop()); - break; - default: - // Ignore window manipulation. - break; - } - break; - case 'u': // Restore cursor (ANSI.SYS). - restoreCursor(); - break; - case ' ': - continueSequence(ESC_CSI_ARGS_SPACE); - break; - default: - parseArg(b); - break; - } - } - /** Select Graphic Rendition (SGR) - see http://en.wikipedia.org/wiki/ANSI_escape_code#graphics. */ private void selectGraphicRendition() { if (mArgIndex >= mArgs.length) mArgIndex = mArgs.length - 1; @@ -1865,25 +859,10 @@ private void doOsc(int b) { doOscSetTextParameters("\007"); break; case 27: // Escape. - continueSequence(ESC_OSC_ESC); - break; - default: - collectOSCArgs(b); - break; - } - } - - private void doOscEsc(int b) { - switch (b) { - case '\\': - doOscSetTextParameters("\033\\"); + continueSequence(new EscOscEscState()); break; default: - // The ESC character was not followed by a \, so insert the ESC and - // the current character in arg buffer. - collectOSCArgs(27); collectOSCArgs(b); - continueSequence(ESC_OSC); break; } } @@ -2163,7 +1142,7 @@ private void logError(String errorType) { StringBuilder buf = new StringBuilder(); buf.append(errorType); buf.append(", escapeState="); - buf.append(mEscapeState); + buf.append(mEscapeState.getStateCode()); boolean firstArg = true; if (mArgIndex >= mArgs.length) mArgIndex = mArgs.length - 1; for (int i = 0; i <= mArgIndex; i++) { @@ -2189,7 +1168,7 @@ private void finishSequenceAndLogError(String error) { } private void finishSequence() { - mEscapeState = ESC_NONE; + mEscapeState = new EscNoneState(); } /** @@ -2382,7 +1361,7 @@ public void reset() { setCursorStyle(); mArgIndex = 0; mContinueSequence = false; - mEscapeState = ESC_NONE; + mEscapeState = new EscNoneState(); mInsertMode = false; mTopMargin = mLeftMargin = 0; mBottomMargin = mRows; @@ -2457,4 +1436,1220 @@ public String toString() { + "," + mLeftMargin + "}]"; } -} + + + + abstract class EscapeState { + public abstract void processCodePoint(int b); + public abstract int getStateCode(); + } + + /** Escape processing: Not currently in an escape sequence. */ + final class EscNoneState extends EscapeState { + EscNoneState() { + + } + + @Override + public void processCodePoint(int b) { + if (b >= 32) emitCodePoint(b); + } + + @Override + public int getStateCode() { + return 0; + } + } + + /** Escape processing: Have seen an ESC character */ + final class EscState extends EscapeState { + EscState() { + + } + + /** Encountering a character in the ESC state. */ + @Override + public void processCodePoint(int b) { + switch (b) { + case '#': + continueSequence(new EscPoundState()); + break; + case '(': + continueSequence(new EscSelectLeftParenState()); + break; + case ')': + continueSequence(new EscSelectRightParenState()); + break; + case '6': // Back index (http://www.vt100.net/docs/vt510-rm/DECBI). Move left, insert blank column if start. + if (mCursorCol > mLeftMargin) { + mCursorCol--; + } else { + int rows = mBottomMargin - mTopMargin; + mScreen.blockCopy(mLeftMargin, mTopMargin, mRightMargin - mLeftMargin - 1, rows, mLeftMargin + 1, mTopMargin); + mScreen.blockSet(mLeftMargin, mTopMargin, 1, rows, ' ', TextStyle.encode(mForeColor, mBackColor, 0)); + } + break; + case '7': // DECSC save cursor - http://www.vt100.net/docs/vt510-rm/DECSC + saveCursor(); + break; + case '8': // DECRC restore cursor - http://www.vt100.net/docs/vt510-rm/DECRC + restoreCursor(); + break; + case '9': // Forward Index (http://www.vt100.net/docs/vt510-rm/DECFI). Move right, insert blank column if end. + if (mCursorCol < mRightMargin - 1) { + mCursorCol++; + } else { + int rows = mBottomMargin - mTopMargin; + mScreen.blockCopy(mLeftMargin + 1, mTopMargin, mRightMargin - mLeftMargin - 1, rows, mLeftMargin, mTopMargin); + mScreen.blockSet(mRightMargin - 1, mTopMargin, 1, rows, ' ', TextStyle.encode(mForeColor, mBackColor, 0)); + } + break; + case 'c': // RIS - Reset to Initial State (http://vt100.net/docs/vt510-rm/RIS). + reset(); + mMainBuffer.clearTranscript(); + blockClear(0, 0, mColumns, mRows); + setCursorPosition(0, 0); + break; + case 'D': // INDEX + doLinefeed(); + break; + case 'E': // Next line (http://www.vt100.net/docs/vt510-rm/NEL). + setCursorCol(isDecsetInternalBitSet(DECSET_BIT_ORIGIN_MODE) ? mLeftMargin : 0); + doLinefeed(); + break; + case 'F': // Cursor to lower-left corner of screen + setCursorRowCol(0, mBottomMargin - 1); + break; + case 'H': // Tab set + mTabStop[mCursorCol] = true; + break; + case 'M': // "${ESC}M" - reverse index (RI). + // http://www.vt100.net/docs/vt100-ug/chapter3.html: "Move the active position to the same horizontal + // position on the preceding line. If the active position is at the top margin, a scroll down is performed". + if (mCursorRow <= mTopMargin) { + mScreen.blockCopy(0, mTopMargin, mColumns, mBottomMargin - (mTopMargin + 1), 0, mTopMargin + 1); + blockClear(0, mTopMargin, mColumns); + } else { + mCursorRow--; + } + break; + case 'N': // SS2, ignore. + case '0': // SS3, ignore. + break; + case 'P': // Device control string + mOSCOrDeviceControlArgs.setLength(0); + continueSequence(new EscPState()); + break; + case '[': + continueSequence(new EscCsiState()); + break; + case '=': // DECKPAM + setDecsetinternalBit(DECSET_BIT_APPLICATION_KEYPAD, true); + break; + case ']': // OSC + mOSCOrDeviceControlArgs.setLength(0); + continueSequence(new EscOscState()); + break; + case '>': // DECKPNM + setDecsetinternalBit(DECSET_BIT_APPLICATION_KEYPAD, false); + break; + default: + unknownSequence(b); + break; + } + } + + @Override + public int getStateCode() { + return 1; + } + } + + /** Escape processing: Have seen ESC POUND */ + final class EscPoundState extends EscapeState { + EscPoundState() { + + } + + @Override + public void processCodePoint(int b) { + if (b == '8') { // Esc # 8 - DEC screen alignment test - fill screen with E's. + mScreen.blockSet(0, 0, mColumns, mRows, 'E', getStyle()); + } else { + unknownSequence(b); + } + } + + @Override + public int getStateCode() { + return 2; + } + } + + /** Escape processing: Have seen ESC and a character-set-select ( char */ + final class EscSelectLeftParenState extends EscapeState { + EscSelectLeftParenState() { + + } + + @Override + public void processCodePoint(int b) { + mUseLineDrawingG0 = (b == '0'); + } + + @Override + public int getStateCode() { + return 3; + } + } + + /** Escape processing: Have seen ESC and a character-set-select ) char */ + final class EscSelectRightParenState extends EscapeState { + EscSelectRightParenState() { + + } + + @Override + public void processCodePoint(int b) { + mUseLineDrawingG1 = (b == '0'); + } + + @Override + public int getStateCode() { + return 4; + } + } + + /** Escape processing: Have seen ESC and a character-set-select ) char */ + final class EscCsiState extends EscapeState { + EscCsiState() { + + } + + /** Following a CSI - Control Sequence Introducer, "\033[". */ + @Override + public void processCodePoint(int b) { + switch (b) { + case '!': + continueSequence(new EscCsiExclamationState()); + break; + case '"': + continueSequence(new EscCsiDoubleQuoteState()); + break; + case '\'': + continueSequence(new EscCsiSingleQuoteState()); + break; + case '$': + continueSequence(new EscCsiDollarState()); + break; + case '*': + continueSequence(new EscCsiArgsAsterixState()); + break; + case '@': { + // "CSI{n}@" - Insert ${n} space characters (ICH) - http://www.vt100.net/docs/vt510-rm/ICH. + mAboutToAutoWrap = false; + int columnsAfterCursor = mColumns - mCursorCol; + int spacesToInsert = Math.min(getArg0(1), columnsAfterCursor); + int charsToMove = columnsAfterCursor - spacesToInsert; + mScreen.blockCopy(mCursorCol, mCursorRow, charsToMove, 1, mCursorCol + spacesToInsert, mCursorRow); + blockClear(mCursorCol, mCursorRow, spacesToInsert); + } + break; + case 'A': // "CSI${n}A" - Cursor up (CUU) ${n} rows. + setCursorRow(Math.max(0, mCursorRow - getArg0(1))); + break; + case 'B': // "CSI${n}B" - Cursor down (CUD) ${n} rows. + setCursorRow(Math.min(mRows - 1, mCursorRow + getArg0(1))); + break; + case 'C': // "CSI${n}C" - Cursor forward (CUF). + case 'a': // "CSI${n}a" - Horizontal position relative (HPR). From ISO-6428/ECMA-48. + setCursorCol(Math.min(mRightMargin - 1, mCursorCol + getArg0(1))); + break; + case 'D': // "CSI${n}D" - Cursor backward (CUB) ${n} columns. + setCursorCol(Math.max(mLeftMargin, mCursorCol - getArg0(1))); + break; + case 'E': // "CSI{n}E - Cursor Next Line (CNL). From ISO-6428/ECMA-48. + setCursorPosition(0, mCursorRow + getArg0(1)); + break; + case 'F': // "CSI{n}F - Cursor Previous Line (CPL). From ISO-6428/ECMA-48. + setCursorPosition(0, mCursorRow - getArg0(1)); + break; + case 'G': // "CSI${n}G" - Cursor horizontal absolute (CHA) to column ${n}. + setCursorCol(Math.min(Math.max(1, getArg0(1)), mColumns) - 1); + break; + case 'H': // "${CSI}${ROW};${COLUMN}H" - Cursor position (CUP). + case 'f': // "${CSI}${ROW};${COLUMN}f" - Horizontal and Vertical Position (HVP). + setCursorPosition(getArg1(1) - 1, getArg0(1) - 1); + break; + case 'I': // Cursor Horizontal Forward Tabulation (CHT). Move the active position n tabs forward. + setCursorCol(nextTabStop(getArg0(1))); + break; + case 'J': // "${CSI}${0,1,2,3}J" - Erase in Display (ED) + // ED ignores the scrolling margins. + switch (getArg0(0)) { + case 0: // Erase from the active position to the end of the screen, inclusive (default). + blockClear(mCursorCol, mCursorRow, mColumns - mCursorCol); + blockClear(0, mCursorRow + 1, mColumns, mRows - (mCursorRow + 1)); + break; + case 1: // Erase from start of the screen to the active position, inclusive. + blockClear(0, 0, mColumns, mCursorRow); + blockClear(0, mCursorRow, mCursorCol + 1); + break; + case 2: // Erase all of the display - all lines are erased, changed to single-width, and the cursor does not + // move.. + blockClear(0, 0, mColumns, mRows); + break; + case 3: // Delete all lines saved in the scrollback buffer (xterm etc) + mMainBuffer.clearTranscript(); + break; + default: + unknownSequence(b); + return; + } + mAboutToAutoWrap = false; + break; + case 'K': // "CSI{n}K" - Erase in line (EL). + switch (getArg0(0)) { + case 0: // Erase from the cursor to the end of the line, inclusive (default) + blockClear(mCursorCol, mCursorRow, mColumns - mCursorCol); + break; + case 1: // Erase from the start of the screen to the cursor, inclusive. + blockClear(0, mCursorRow, mCursorCol + 1); + break; + case 2: // Erase all of the line. + blockClear(0, mCursorRow, mColumns); + break; + default: + unknownSequence(b); + return; + } + mAboutToAutoWrap = false; + break; + case 'L': // "${CSI}{N}L" - insert ${N} lines (IL). + { + int linesAfterCursor = mBottomMargin - mCursorRow; + int linesToInsert = Math.min(getArg0(1), linesAfterCursor); + int linesToMove = linesAfterCursor - linesToInsert; + mScreen.blockCopy(0, mCursorRow, mColumns, linesToMove, 0, mCursorRow + linesToInsert); + blockClear(0, mCursorRow, mColumns, linesToInsert); + } + break; + case 'M': // "${CSI}${N}M" - delete N lines (DL). + { + mAboutToAutoWrap = false; + int linesAfterCursor = mBottomMargin - mCursorRow; + int linesToDelete = Math.min(getArg0(1), linesAfterCursor); + int linesToMove = linesAfterCursor - linesToDelete; + mScreen.blockCopy(0, mCursorRow + linesToDelete, mColumns, linesToMove, 0, mCursorRow); + blockClear(0, mCursorRow + linesToMove, mColumns, linesToDelete); + } + break; + case 'P': // "${CSI}{N}P" - delete ${N} characters (DCH). + { + // http://www.vt100.net/docs/vt510-rm/DCH: "If ${N} is greater than the number of characters between the + // cursor and the right margin, then DCH only deletes the remaining characters. + // As characters are deleted, the remaining characters between the cursor and right margin move to the left. + // Character attributes move with the characters. The terminal adds blank spaces with no visual character + // attributes at the right margin. DCH has no effect outside the scrolling margins." + mAboutToAutoWrap = false; + int cellsAfterCursor = mColumns - mCursorCol; + int cellsToDelete = Math.min(getArg0(1), cellsAfterCursor); + int cellsToMove = cellsAfterCursor - cellsToDelete; + mScreen.blockCopy(mCursorCol + cellsToDelete, mCursorRow, cellsToMove, 1, mCursorCol, mCursorRow); + blockClear(mCursorCol + cellsToMove, mCursorRow, cellsToDelete); + } + break; + case 'S': { // "${CSI}${N}S" - scroll up ${N} lines (default = 1) (SU). + final int linesToScroll = getArg0(1); + for (int i = 0; i < linesToScroll; i++) + scrollDownOneLine(); + break; + } + case 'T': + if (mArgIndex == 0) { + // "${CSI}${N}T" - Scroll down N lines (default = 1) (SD). + // http://vt100.net/docs/vt510-rm/SD: "N is the number of lines to move the user window up in page + // memory. N new lines appear at the top of the display. N old lines disappear at the bottom of the + // display. You cannot pan past the top margin of the current page". + final int linesToScrollArg = getArg0(1); + final int linesBetweenTopAndBottomMargins = mBottomMargin - mTopMargin; + final int linesToScroll = Math.min(linesBetweenTopAndBottomMargins, linesToScrollArg); + mScreen.blockCopy(0, mTopMargin, mColumns, linesBetweenTopAndBottomMargins - linesToScroll, 0, mTopMargin + linesToScroll); + blockClear(0, mTopMargin, mColumns, linesToScroll); + } else { + // "${CSI}${func};${startx};${starty};${firstrow};${lastrow}T" - initiate highlight mouse tracking. + unimplementedSequence(b); + } + break; + case 'X': // "${CSI}${N}X" - Erase ${N:=1} character(s) (ECH). FIXME: Clears character attributes? + mAboutToAutoWrap = false; + mScreen.blockSet(mCursorCol, mCursorRow, Math.min(getArg0(1), mColumns - mCursorCol), 1, ' ', getStyle()); + break; + case 'Z': // Cursor Backward Tabulation (CBT). Move the active position n tabs backward. + int numberOfTabs = getArg0(1); + int newCol = mLeftMargin; + for (int i = mCursorCol - 1; i >= 0; i--) + if (mTabStop[i]) { + if (--numberOfTabs == 0) { + newCol = Math.max(i, mLeftMargin); + break; + } + } + mCursorCol = newCol; + break; + case '?': // Esc [ ? -- start of a private mode set + continueSequence(new EscCsiQuestionMarkState()); + break; + case '>': // "Esc [ >" -- + continueSequence(new EscCsiBiggerThanState()); + break; + case '`': // Horizontal position absolute (HPA - http://www.vt100.net/docs/vt510-rm/HPA). + setCursorColRespectingOriginMode(getArg0(1) - 1); + break; + case 'b': // Repeat the preceding graphic character Ps times (REP). + if (mLastEmittedCodePoint == -1) break; + final int numRepeat = getArg0(1); + for (int i = 0; i < numRepeat; i++) emitCodePoint(mLastEmittedCodePoint); + break; + case 'c': // Primary Device Attributes (http://www.vt100.net/docs/vt510-rm/DA1) if argument is missing or zero. + // The important part that may still be used by some (tmux stores this value but does not currently use it) + // is the first response parameter identifying the terminal service class, where we send 64 for "vt420". + // This is followed by a list of attributes which is probably unused by applications. Send like xterm. + if (getArg0(0) == 0) mSession.write("\033[?64;1;2;6;9;15;18;21;22c"); + break; + case 'd': // ESC [ Pn d - Vert Position Absolute + setCursorRow(Math.min(Math.max(1, getArg0(1)), mRows) - 1); + break; + case 'e': // Vertical Position Relative (VPR). From ISO-6429 (ECMA-48). + setCursorPosition(mCursorCol, mCursorRow + getArg0(1)); + break; + // case 'f': "${CSI}${ROW};${COLUMN}f" - Horizontal and Vertical Position (HVP). Grouped with case 'H'. + case 'g': // Clear tab stop + switch (getArg0(0)) { + case 0: + mTabStop[mCursorCol] = false; + break; + case 3: + for (int i = 0; i < mColumns; i++) { + mTabStop[i] = false; + } + break; + default: + // Specified to have no effect. + break; + } + break; + case 'h': // Set Mode + doSetMode(true); + break; + case 'l': // Reset Mode + doSetMode(false); + break; + case 'm': // Esc [ Pn m - character attributes. (can have up to 16 numerical arguments) + selectGraphicRendition(); + break; + case 'n': // Esc [ Pn n - ECMA-48 Status Report Commands + // sendDeviceAttributes() + switch (getArg0(0)) { + case 5: // Device status report (DSR): + // Answer is ESC [ 0 n (Terminal OK). + byte[] dsr = {(byte) 27, (byte) '[', (byte) '0', (byte) 'n'}; + mSession.write(dsr, 0, dsr.length); + break; + case 6: // Cursor position report (CPR): + // Answer is ESC [ y ; x R, where x,y is + // the cursor location. + mSession.write(String.format(Locale.US, "\033[%d;%dR", mCursorRow + 1, mCursorCol + 1)); + break; + default: + break; + } + break; + case 'r': // "CSI${top};${bottom}r" - set top and bottom Margins (DECSTBM). + { + // https://vt100.net/docs/vt510-rm/DECSTBM.html + // The top margin defaults to 1, the bottom margin defaults to mRows. + // The escape sequence numbers top 1..23, but we number top 0..22. + // The escape sequence numbers bottom 2..24, and so do we (because we use a zero based numbering + // scheme, but we store the first line below the bottom-most scrolling line. + // As a result, we adjust the top line by -1, but we leave the bottom line alone. + // Also require that top + 2 <= bottom. + mTopMargin = Math.max(0, Math.min(getArg0(1) - 1, mRows - 2)); + mBottomMargin = Math.max(mTopMargin + 2, Math.min(getArg1(mRows), mRows)); + + // DECSTBM moves the cursor to column 1, line 1 of the page respecting origin mode. + setCursorPosition(0, 0); + } + break; + case 's': + if (isDecsetInternalBitSet(DECSET_BIT_LEFTRIGHT_MARGIN_MODE)) { + // Set left and right margins (DECSLRM - http://www.vt100.net/docs/vt510-rm/DECSLRM). + mLeftMargin = Math.min(getArg0(1) - 1, mColumns - 2); + mRightMargin = Math.max(mLeftMargin + 1, Math.min(getArg1(mColumns), mColumns)); + // DECSLRM moves the cursor to column 1, line 1 of the page. + setCursorPosition(0, 0); + } else { + // Save cursor (ANSI.SYS), available only when DECLRMM is disabled. + saveCursor(); + } + break; + case 't': // Window manipulation (from dtterm, as well as extensions) + switch (getArg0(0)) { + case 11: // Report xterm window state. If the xterm window is open (non-iconified), it returns CSI 1 t . + mSession.write("\033[1t"); + break; + case 13: // Report xterm window position. Result is CSI 3 ; x ; y t + mSession.write("\033[3;0;0t"); + break; + case 14: // Report xterm window in pixels. Result is CSI 4 ; height ; width t + // We just report characters time 12 here. + mSession.write(String.format(Locale.US, "\033[4;%d;%dt", mRows * 12, mColumns * 12)); + break; + case 18: // Report the size of the text area in characters. Result is CSI 8 ; height ; width t + mSession.write(String.format(Locale.US, "\033[8;%d;%dt", mRows, mColumns)); + break; + case 19: // Report the size of the screen in characters. Result is CSI 9 ; height ; width t + // We report the same size as the view, since it's the view really isn't resizable from the shell. + mSession.write(String.format(Locale.US, "\033[9;%d;%dt", mRows, mColumns)); + break; + case 20: // Report xterm windows icon label. Result is OSC L label ST. Disabled due to security concerns: + mSession.write("\033]LIconLabel\033\\"); + break; + case 21: // Report xterm windows title. Result is OSC l label ST. Disabled due to security concerns: + mSession.write("\033]l\033\\"); + break; + case 22: + // 22;0 -> Save xterm icon and window title on stack. + // 22;1 -> Save xterm icon title on stack. + // 22;2 -> Save xterm window title on stack. + mTitleStack.push(mTitle); + if (mTitleStack.size() > 20) { + // Limit size + mTitleStack.remove(0); + } + break; + case 23: // Like 22 above but restore from stack. + if (!mTitleStack.isEmpty()) setTitle(mTitleStack.pop()); + break; + default: + // Ignore window manipulation. + break; + } + break; + case 'u': // Restore cursor (ANSI.SYS). + restoreCursor(); + break; + case ' ': + continueSequence(new EscCsiArgSpaceState()); + break; + default: + parseArg(b); + break; + } + } + + @Override + public int getStateCode() { + return 6; + } + } + + /** Escape processing: ESC [ ? */ + final class EscCsiQuestionMarkState extends EscapeState { + EscCsiQuestionMarkState() { + + } + + /** Process byte while in the escape state. */ + @Override + public void processCodePoint(int b) { + switch (b) { + case 'J': // Selective erase in display (DECSED) - http://www.vt100.net/docs/vt510-rm/DECSED. + case 'K': // Selective erase in line (DECSEL) - http://vt100.net/docs/vt510-rm/DECSEL. + mAboutToAutoWrap = false; + int fillChar = ' '; + int startCol = -1; + int startRow = -1; + int endCol = -1; + int endRow = -1; + boolean justRow = (b == 'K'); + switch (getArg0(0)) { + case 0: // Erase from the active position to the end, inclusive (default). + startCol = mCursorCol; + startRow = mCursorRow; + endCol = mColumns; + endRow = justRow ? (mCursorRow + 1) : mRows; + break; + case 1: // Erase from start to the active position, inclusive. + startCol = 0; + startRow = justRow ? mCursorRow : 0; + endCol = mCursorCol + 1; + endRow = mCursorRow + 1; + break; + case 2: // Erase all of the display/line. + startCol = 0; + startRow = justRow ? mCursorRow : 0; + endCol = mColumns; + endRow = justRow ? (mCursorRow + 1) : mRows; + break; + default: + unknownSequence(b); + break; + } + long style = getStyle(); + for (int row = startRow; row < endRow; row++) { + for (int col = startCol; col < endCol; col++) { + if ((TextStyle.decodeEffect(mScreen.getStyleAt(row, col)) & TextStyle.CHARACTER_ATTRIBUTE_PROTECTED) == 0) + mScreen.setChar(col, row, fillChar, style); + } + } + break; + case 'h': + case 'l': + if (mArgIndex >= mArgs.length) mArgIndex = mArgs.length - 1; + for (int i = 0; i <= mArgIndex; i++) + doDecSetOrReset(b == 'h', mArgs[i]); + break; + case 'n': // Device Status Report (DSR, DEC-specific). + if (getArg0(-1) == 6) {// Extended Cursor Position (DECXCPR - http://www.vt100.net/docs/vt510-rm/DECXCPR). Page=1. + mSession.write(String.format(Locale.US, "\033[?%d;%d;1R", mCursorRow + 1, mCursorCol + 1)); + } else { + finishSequence(); + return; + } + break; + case 'r': + case 's': + if (mArgIndex >= mArgs.length) mArgIndex = mArgs.length - 1; + for (int i = 0; i <= mArgIndex; i++) { + int externalBit = mArgs[i]; + int internalBit = mapDecSetBitToInternalBit(externalBit); + if (internalBit == -1) { + Logger.logWarn(mClient, LOG_TAG, "Ignoring request to save/recall decset bit=" + externalBit); + } else { + if (b == 's') { + mSavedDecSetFlags |= internalBit; + } else { + doDecSetOrReset((mSavedDecSetFlags & internalBit) != 0, externalBit); + } + } + } + break; + case '$': + continueSequence(new EscCsiQuestionMarkArgDollarState()); + return; + default: + parseArg(b); + } + } + + @Override + public int getStateCode() { + return 7; + } + } + + /** Escape processing: ESC [ $ */ + final class EscCsiDollarState extends EscapeState { + EscCsiDollarState() { + + } + + @Override + public void processCodePoint(int b) { + boolean originMode = isDecsetInternalBitSet(DECSET_BIT_ORIGIN_MODE); + int effectiveTopMargin = originMode ? mTopMargin : 0; + int effectiveBottomMargin = originMode ? mBottomMargin : mRows; + int effectiveLeftMargin = originMode ? mLeftMargin : 0; + int effectiveRightMargin = originMode ? mRightMargin : mColumns; + switch (b) { + case 'v': // ${CSI}${SRC_TOP}${SRC_LEFT}${SRC_BOTTOM}${SRC_RIGHT}${SRC_PAGE}${DST_TOP}${DST_LEFT}${DST_PAGE}$v" + // Copy rectangular area (DECCRA - http://vt100.net/docs/vt510-rm/DECCRA): + // "If Pbs is greater than Pts, or Pls is greater than Prs, the terminal ignores DECCRA. + // The coordinates of the rectangular area are affected by the setting of origin mode (DECOM). + // DECCRA is not affected by the page margins. + // The copied text takes on the line attributes of the destination area. + // If the value of Pt, Pl, Pb, or Pr exceeds the width or height of the active page, then the value + // is treated as the width or height of that page. + // If the destination area is partially off the page, then DECCRA clips the off-page data. + // DECCRA does not change the active cursor position." + int topSource = Math.min(getArg(0, 1, true) - 1 + effectiveTopMargin, mRows); + int leftSource = Math.min(getArg(1, 1, true) - 1 + effectiveLeftMargin, mColumns); + // Inclusive, so do not subtract one: + int bottomSource = Math.min(Math.max(getArg(2, mRows, true) + effectiveTopMargin, topSource), mRows); + int rightSource = Math.min(Math.max(getArg(3, mColumns, true) + effectiveLeftMargin, leftSource), mColumns); + // int sourcePage = getArg(4, 1, true); + int destionationTop = Math.min(getArg(5, 1, true) - 1 + effectiveTopMargin, mRows); + int destinationLeft = Math.min(getArg(6, 1, true) - 1 + effectiveLeftMargin, mColumns); + // int destinationPage = getArg(7, 1, true); + int heightToCopy = Math.min(mRows - destionationTop, bottomSource - topSource); + int widthToCopy = Math.min(mColumns - destinationLeft, rightSource - leftSource); + mScreen.blockCopy(leftSource, topSource, widthToCopy, heightToCopy, destinationLeft, destionationTop); + break; + case '{': // ${CSI}${TOP}${LEFT}${BOTTOM}${RIGHT}${" + // Selective erase rectangular area (DECSERA - http://www.vt100.net/docs/vt510-rm/DECSERA). + case 'x': // ${CSI}${CHAR};${TOP}${LEFT}${BOTTOM}${RIGHT}$x" + // Fill rectangular area (DECFRA - http://www.vt100.net/docs/vt510-rm/DECFRA). + case 'z': // ${CSI}$${TOP}${LEFT}${BOTTOM}${RIGHT}$z" + // Erase rectangular area (DECERA - http://www.vt100.net/docs/vt510-rm/DECERA). + boolean erase = b != 'x'; + boolean selective = b == '{'; + // Only DECSERA keeps visual attributes, DECERA does not: + boolean keepVisualAttributes = erase && selective; + int argIndex = 0; + int fillChar = erase ? ' ' : getArg(argIndex++, -1, true); + // "Pch can be any value from 32 to 126 or from 160 to 255. If Pch is not in this range, then the + // terminal ignores the DECFRA command": + if ((fillChar >= 32 && fillChar <= 126) || (fillChar >= 160 && fillChar <= 255)) { + // "If the value of Pt, Pl, Pb, or Pr exceeds the width or height of the active page, the value + // is treated as the width or height of that page." + int top = Math.min(getArg(argIndex++, 1, true) + effectiveTopMargin, effectiveBottomMargin + 1); + int left = Math.min(getArg(argIndex++, 1, true) + effectiveLeftMargin, effectiveRightMargin + 1); + int bottom = Math.min(getArg(argIndex++, mRows, true) + effectiveTopMargin, effectiveBottomMargin); + int right = Math.min(getArg(argIndex, mColumns, true) + effectiveLeftMargin, effectiveRightMargin); + long style = getStyle(); + for (int row = top - 1; row < bottom; row++) + for (int col = left - 1; col < right; col++) + if (!selective || (TextStyle.decodeEffect(mScreen.getStyleAt(row, col)) & TextStyle.CHARACTER_ATTRIBUTE_PROTECTED) == 0) + mScreen.setChar(col, row, fillChar, keepVisualAttributes ? mScreen.getStyleAt(row, col) : style); + } + break; + case 'r': // "${CSI}${TOP}${LEFT}${BOTTOM}${RIGHT}${ATTRIBUTES}$r" + // Change attributes in rectangular area (DECCARA - http://vt100.net/docs/vt510-rm/DECCARA). + case 't': // "${CSI}${TOP}${LEFT}${BOTTOM}${RIGHT}${ATTRIBUTES}$t" + // Reverse attributes in rectangular area (DECRARA - http://www.vt100.net/docs/vt510-rm/DECRARA). + boolean reverse = b == 't'; + // FIXME: "coordinates of the rectangular area are affected by the setting of origin mode (DECOM)". + int top = Math.min(getArg(0, 1, true) - 1, effectiveBottomMargin) + effectiveTopMargin; + int left = Math.min(getArg(1, 1, true) - 1, effectiveRightMargin) + effectiveLeftMargin; + int bottom = Math.min(getArg(2, mRows, true) + 1, effectiveBottomMargin - 1) + effectiveTopMargin; + int right = Math.min(getArg(3, mColumns, true) + 1, effectiveRightMargin - 1) + effectiveLeftMargin; + if (mArgIndex >= 4) { + if (mArgIndex >= mArgs.length) mArgIndex = mArgs.length - 1; + for (int i = 4; i <= mArgIndex; i++) { + int bits = 0; + boolean setOrClear = true; // True if setting, false if clearing. + switch (getArg(i, 0, false)) { + case 0: // Attributes off (no bold, no underline, no blink, positive image). + bits = (TextStyle.CHARACTER_ATTRIBUTE_BOLD | TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE | TextStyle.CHARACTER_ATTRIBUTE_BLINK + | TextStyle.CHARACTER_ATTRIBUTE_INVERSE); + if (!reverse) setOrClear = false; + break; + case 1: // Bold. + bits = TextStyle.CHARACTER_ATTRIBUTE_BOLD; + break; + case 4: // Underline. + bits = TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE; + break; + case 5: // Blink. + bits = TextStyle.CHARACTER_ATTRIBUTE_BLINK; + break; + case 7: // Negative image. + bits = TextStyle.CHARACTER_ATTRIBUTE_INVERSE; + break; + case 22: // No bold. + bits = TextStyle.CHARACTER_ATTRIBUTE_BOLD; + setOrClear = false; + break; + case 24: // No underline. + bits = TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE; + setOrClear = false; + break; + case 25: // No blink. + bits = TextStyle.CHARACTER_ATTRIBUTE_BLINK; + setOrClear = false; + break; + case 27: // Positive image. + bits = TextStyle.CHARACTER_ATTRIBUTE_INVERSE; + setOrClear = false; + break; + } + if (reverse && !setOrClear) { + // Reverse attributes in rectangular area ignores non-(1,4,5,7) bits. + } else { + mScreen.setOrClearEffect(bits, setOrClear, reverse, isDecsetInternalBitSet(DECSET_BIT_RECTANGULAR_CHANGEATTRIBUTE), + effectiveLeftMargin, effectiveRightMargin, top, left, bottom, right); + } + } + } else { + // Do nothing. + } + break; + default: + unknownSequence(b); + } + } + + @Override + public int getStateCode() { + return 8; + } + } + + /** Escape processing: ESC % */ + final class EscPercentState extends EscapeState { + EscPercentState() { + + } + + @Override + public void processCodePoint(int b) { + + } + + @Override + public int getStateCode() { + return 9; + } + } + + /** Escape processing: ESC ] (AKA OSC - Operating System Controls) */ + final class EscOscState extends EscapeState { + EscOscState() { + + } + + @Override + public void processCodePoint(int b) { + doOsc(b); + } + + @Override + public int getStateCode() { + return 10; + } + } + + /** Escape processing: ESC ] (AKA OSC - Operating System Controls) ESC */ + final class EscOscEscState extends EscapeState { + EscOscEscState() { + + } + + @Override + public void processCodePoint(int b) { + if (b == '\\') { + doOscSetTextParameters("\033\\"); + } else {// The ESC character was not followed by a \, so insert the ESC and + // the current character in arg buffer. + collectOSCArgs(27); + collectOSCArgs(b); + continueSequence(new EscOscState()); + } + } + + @Override + public int getStateCode() { + return 11; + } + } + + /** Escape processing: ESC [ > */ + final class EscCsiBiggerThanState extends EscapeState { + EscCsiBiggerThanState() { + + } + + @Override + public void processCodePoint(int b) { + switch (b) { + case 'c': // "${CSI}>c" or "${CSI}>c". Secondary Device Attributes (DA2). + // Originally this was used for the terminal to respond with "identification code, firmware version level, + // and hardware options" (http://vt100.net/docs/vt510-rm/DA2), with the first "41" meaning the VT420 + // terminal type. This is not used anymore, but the second version level field has been changed by xterm + // to mean it's release number ("patch numbers" listed at http://invisible-island.net/xterm/xterm.log.html), + // and some applications use it as a feature check: + // * tmux used to have a "xterm won't reach version 500 for a while so set that as the upper limit" check, + // and then check "xterm_version > 270" if rectangular area operations such as DECCRA could be used. + // * vim checks xterm version number >140 for "Request termcap/terminfo string" functionality >276 for SGR + // mouse report. + // The third number is a keyboard identifier not used nowadays. + mSession.write("\033[>41;320;0c"); + break; + case 'm': + // https://bugs.launchpad.net/gnome-terminal/+bug/96676/comments/25 + // Depending on the first number parameter, this can set one of the xterm resources + // modifyKeyboard, modifyCursorKeys, modifyFunctionKeys and modifyOtherKeys. + // http://invisible-island.net/xterm/manpage/xterm.html#RESOURCES + + // * modifyKeyboard (parameter=1): + // Normally xterm makes a special case regarding modifiers (shift, control, etc.) to handle special keyboard + // layouts (legacy and vt220). This is done to provide compatible keyboards for DEC VT220 and related + // terminals that implement user-defined keys (UDK). + // The bits of the resource value selectively enable modification of the given category when these keyboards + // are selected. The default is "0": + // (0) The legacy/vt220 keyboards interpret only the Control-modifier when constructing numbered + // function-keys. Other special keys are not modified. + // (1) allows modification of the numeric keypad + // (2) allows modification of the editing keypad + // (4) allows modification of function-keys, overrides use of Shift-modifier for UDK. + // (8) allows modification of other special keys + + // * modifyCursorKeys (parameter=2): + // Tells how to handle the special case where Control-, Shift-, Alt- or Meta-modifiers are used to add a + // parameter to the escape sequence returned by a cursor-key. The default is "2". + // - Set it to -1 to disable it. + // - Set it to 0 to use the old/obsolete behavior. + // - Set it to 1 to prefix modified sequences with CSI. + // - Set it to 2 to force the modifier to be the second parameter if it would otherwise be the first. + // - Set it to 3 to mark the sequence with a ">" to hint that it is private. + + // * modifyFunctionKeys (parameter=3): + // Tells how to handle the special case where Control-, Shift-, Alt- or Meta-modifiers are used to add a + // parameter to the escape sequence returned by a (numbered) function- + // key. The default is "2". The resource values are similar to modifyCursorKeys: + // Set it to -1 to permit the user to use shift- and control-modifiers to construct function-key strings + // using the normal encoding scheme. + // - Set it to 0 to use the old/obsolete behavior. + // - Set it to 1 to prefix modified sequences with CSI. + // - Set it to 2 to force the modifier to be the second parameter if it would otherwise be the first. + // - Set it to 3 to mark the sequence with a ">" to hint that it is private. + // If modifyFunctionKeys is zero, xterm uses Control- and Shift-modifiers to allow the user to construct + // numbered function-keys beyond the set provided by the keyboard: + // (Control) adds the value given by the ctrlFKeys resource. + // (Shift) adds twice the value given by the ctrlFKeys resource. + // (Control/Shift) adds three times the value given by the ctrlFKeys resource. + // + // As a special case, legacy (when oldFunctionKeys is true) or vt220 (when sunKeyboard is true) + // keyboards interpret only the Control-modifier when constructing numbered function-keys. + // This is done to provide compatible keyboards for DEC VT220 and related terminals that + // implement user-defined keys (UDK). + + // * modifyOtherKeys (parameter=4): + // Like modifyCursorKeys, tells xterm to construct an escape sequence for other keys (such as "2") when + // modified by Control-, Alt- or Meta-modifiers. This feature does not apply to function keys and + // well-defined keys such as ESC or the control keys. The default is "0". + // (0) disables this feature. + // (1) enables this feature for keys except for those with well-known behavior, e.g., Tab, Backarrow and + // some special control character cases, e.g., Control-Space to make a NUL. + // (2) enables this feature for keys including the exceptions listed. + Logger.logError(mClient, LOG_TAG, "(ignored) CSI > MODIFY RESOURCE: " + getArg0(-1) + " to " + getArg1(-1)); + break; + default: + parseArg(b); + break; + } + } + + @Override + public int getStateCode() { + return 12; + } + } + + /** Escape procession: "ESC P" or Device Control String (DCS) */ + final class EscPState extends EscapeState { + EscPState() { + + } + + /** device control sequence. */ + @Override + public void processCodePoint(int b) { + if (b == (byte) '\\') { // End of ESC \ string Terminator + String dcs = mOSCOrDeviceControlArgs.toString(); + // DCS $ q P t ST. Request Status String (DECRQSS) + if (dcs.startsWith("$q")) { + if (dcs.equals("$q\"p")) { + // DECSCL, conformance level, http://www.vt100.net/docs/vt510-rm/DECSCL: + String csiString = "64;1\"p"; + mSession.write("\033P1$r" + csiString + "\033\\"); + } else { + finishSequenceAndLogError("Unrecognized DECRQSS string: '" + dcs + "'"); + } + } else if (dcs.startsWith("+q")) { + // Request Termcap/Terminfo String. The string following the "q" is a list of names encoded in + // hexadecimal (2 digits per character) separated by ; which correspond to termcap or terminfo key + // names. + // Two special features are also recognized, which are not key names: Co for termcap colors (or colors + // for terminfo colors), and TN for termcap name (or name for terminfo name). + // xterm responds with DCS 1 + r P t ST for valid requests, adding to P t an = , and the value of the + // corresponding string that xterm would send, or DCS 0 + r P t ST for invalid requests. The strings are + // encoded in hexadecimal (2 digits per character). + // Example: + // :kr=\EOC: ks=\E[?1h\E=: ku=\EOA: le=^H:mb=\E[5m:md=\E[1m:\ + // where + // kd=down-arrow key + // kl=left-arrow key + // kr=right-arrow key + // ku=up-arrow key + // #2=key_shome, "shifted home" + // #4=key_sleft, "shift arrow left" + // %i=key_sright, "shift arrow right" + // *7=key_send, "shifted end" + // k1=F1 function key + + // Example: Request for ku is "ESC P + q 6 b 7 5 ESC \", where 6b7d=ku in hexadecimal. + // Xterm response in normal cursor mode: + // "<27> P 1 + r 6 b 7 5 = 1 B 5 B 4 1" where 0x1B 0x5B 0x41 = 27 91 65 = ESC [ A + // Xterm response in application cursor mode: + // "<27> P 1 + r 6 b 7 5 = 1 B 5 B 4 1" where 0x1B 0x4F 0x41 = 27 91 65 = ESC 0 A + + // #4 is "shift arrow left": + // *** Device Control (DCS) for '#4'- 'ESC P + q 23 34 ESC \' + // Response: <27> P 1 + r 2 3 3 4 = 1 B 5 B 3 1 3 B 3 2 4 4 <27> \ + // where 0x1B 0x5B 0x31 0x3B 0x32 0x44 = ESC [ 1 ; 2 D + // which we find in: TermKeyListener.java: KEY_MAP.put(KEYMOD_SHIFT | KEYCODE_DPAD_LEFT, "\033[1;2D"); + + // See http://h30097.www3.hp.com/docs/base_doc/DOCUMENTATION/V40G_HTML/MAN/MAN4/0178____.HTM for what to + // respond, as well as http://www.freebsd.org/cgi/man.cgi?query=termcap&sektion=5#CAPABILITIES for + // the meaning of e.g. "ku", "kd", "kr", "kl" + + for (String part : dcs.substring(2).split(";")) { + if (part.length() % 2 == 0) { + StringBuilder transBuffer = new StringBuilder(); + char c; + for (int i = 0; i < part.length(); i += 2) { + try { + c = (char) Long.decode("0x" + part.charAt(i) + "" + part.charAt(i + 1)).longValue(); + } catch (NumberFormatException e) { + Logger.logStackTraceWithMessage(mClient, LOG_TAG, "Invalid device termcap/terminfo encoded name \"" + part + "\"", e); + continue; + } + transBuffer.append(c); + } + + String trans = transBuffer.toString(); + String responseValue; + switch (trans) { + case "Co": + case "colors": + responseValue = "256"; // Number of colors. + break; + case "TN": + case "name": + responseValue = "xterm"; + break; + default: + responseValue = KeyHandler.getCodeFromTermcap(trans, isDecsetInternalBitSet(DECSET_BIT_APPLICATION_CURSOR_KEYS), + isDecsetInternalBitSet(DECSET_BIT_APPLICATION_KEYPAD)); + break; + } + if (responseValue == null) { + switch (trans) { + case "%1": // Help key - ignore + case "&8": // Undo key - ignore. + break; + default: + Logger.logWarn(mClient, LOG_TAG, "Unhandled termcap/terminfo name: '" + trans + "'"); + } + // Respond with invalid request: + mSession.write("\033P0+r" + part + "\033\\"); + } else { + StringBuilder hexEncoded = new StringBuilder(); + for (int j = 0; j < responseValue.length(); j++) { + hexEncoded.append(String.format("%02X", (int) responseValue.charAt(j))); + } + mSession.write("\033P1+r" + part + "=" + hexEncoded + "\033\\"); + } + } else { + Logger.logError(mClient, LOG_TAG, "Invalid device termcap/terminfo name of odd length: " + part); + } + } + } else { + if (LOG_ESCAPE_SEQUENCES) + Logger.logError(mClient, LOG_TAG, "Unrecognized device control string: " + dcs); + } + finishSequence(); + } else { + if (mOSCOrDeviceControlArgs.length() > MAX_OSC_STRING_LENGTH) { + // Too long. + mOSCOrDeviceControlArgs.setLength(0); + finishSequence(); + } else { + mOSCOrDeviceControlArgs.appendCodePoint(b); + continueSequence(mEscapeState); + } + } + } + + @Override + public int getStateCode() { + return 13; + } + } + + /** Escape processing: CSI > */ + final class EscCsiQuestionMarkArgDollarState extends EscapeState { + EscCsiQuestionMarkArgDollarState() { + + } + + @Override + public void processCodePoint(int b) { + if (b == 'p') { + // Request DEC private mode (DECRQM). + int mode = getArg0(0); + int value; + if (mode == 47 || mode == 1047 || mode == 1049) { + // This state is carried by mScreen pointer. + value = (mScreen == mAltBuffer) ? 1 : 2; + } else { + int internalBit = mapDecSetBitToInternalBit(mode); + if (internalBit != -1) { + value = isDecsetInternalBitSet(internalBit) ? 1 : 2; // 1=set, 2=reset. + } else { + Logger.logError(mClient, LOG_TAG, "Got DECRQM for unrecognized private DEC mode=" + mode); + value = 0; // 0=not recognized, 3=permanently set, 4=permanently reset + } + } + mSession.write(String.format(Locale.US, "\033[?%d;%d$y", mode, value)); + } else { + unknownSequence(b); + } + } + + @Override + public int getStateCode() { + return 14; + } + } + + /** Escape processing: CSI $ARGS ' ' */ + final class EscCsiArgSpaceState extends EscapeState { + EscCsiArgSpaceState() { + + } + + @Override + public void processCodePoint(int b) { + int arg = getArg0(0); + switch (b) { + case 'q': // "${CSI}${STYLE} q" - set cursor style (http://www.vt100.net/docs/vt510-rm/DECSCUSR). + switch (arg) { + case 0: // Blinking block. + case 1: // Blinking block. + case 2: // Steady block. + mCursorStyle = TERMINAL_CURSOR_STYLE_BLOCK; + break; + case 3: // Blinking underline. + case 4: // Steady underline. + mCursorStyle = TERMINAL_CURSOR_STYLE_UNDERLINE; + break; + case 5: // Blinking bar (xterm addition). + case 6: // Steady bar (xterm addition). + mCursorStyle = TERMINAL_CURSOR_STYLE_BAR; + break; + } + break; + case 't': + case 'u': + // Set margin-bell volume - ignore. + break; + default: + unknownSequence(b); + } + } + + @Override + public int getStateCode() { + return 15; + } + } + + /** Escape processing: CSI $ARGS '*' */ + final class EscCsiArgsAsterixState extends EscapeState { + EscCsiArgsAsterixState() { + + } + + @Override + public void processCodePoint(int b) { + int attributeChangeExtent = getArg0(0); + if (b == 'x' && (attributeChangeExtent >= 0 && attributeChangeExtent <= 2)) { + // Select attribute change extent (DECSACE - http://www.vt100.net/docs/vt510-rm/DECSACE). + setDecsetinternalBit(DECSET_BIT_RECTANGULAR_CHANGEATTRIBUTE, attributeChangeExtent == 2); + } else { + unknownSequence(b); + } + } + + @Override + public int getStateCode() { + return 16; + } + } + + /** Escape processing: CSI " */ + final class EscCsiDoubleQuoteState extends EscapeState { + EscCsiDoubleQuoteState() { + + } + + @Override + public void processCodePoint(int b) { + if (b == 'q') { + // http://www.vt100.net/docs/vt510-rm/DECSCA + int arg = getArg0(0); + if (arg == 0 || arg == 2) { + // DECSED and DECSEL can erase characters. + mEffect &= ~TextStyle.CHARACTER_ATTRIBUTE_PROTECTED; + } else if (arg == 1) { + // DECSED and DECSEL cannot erase characters. + mEffect |= TextStyle.CHARACTER_ATTRIBUTE_PROTECTED; + } else { + unknownSequence(b); + } + } else { + unknownSequence(b); + } + } + + @Override + public int getStateCode() { + return 17; + } + } + + /** Escape processing: CSI ' */ + final class EscCsiSingleQuoteState extends EscapeState { + EscCsiSingleQuoteState() { + + } + + @Override + public void processCodePoint(int b) { + if (b == '}') { // Insert Ps Column(s) (default = 1) (DECIC), VT420 and up. + int columnsAfterCursor = mRightMargin - mCursorCol; + int columnsToInsert = Math.min(getArg0(1), columnsAfterCursor); + int columnsToMove = columnsAfterCursor - columnsToInsert; + mScreen.blockCopy(mCursorCol, 0, columnsToMove, mRows, mCursorCol + columnsToInsert, 0); + blockClear(mCursorCol, 0, columnsToInsert, mRows); + } else if (b == '~') { // Delete Ps Column(s) (default = 1) (DECDC), VT420 and up. + int columnsAfterCursor = mRightMargin - mCursorCol; + int columnsToDelete = Math.min(getArg0(1), columnsAfterCursor); + int columnsToMove = columnsAfterCursor - columnsToDelete; + mScreen.blockCopy(mCursorCol + columnsToDelete, 0, columnsToMove, mRows, mCursorCol, 0); + } else { + unknownSequence(b); + } + } + + @Override + public int getStateCode() { + return 18; + } + } + + /** Escape processing: CSI ! */ + final class EscCsiExclamationState extends EscapeState { + EscCsiExclamationState() { + + } + + @Override + public void processCodePoint(int b) { + if (b == 'p') { // Soft terminal reset (DECSTR, http://vt100.net/docs/vt510-rm/DECSTR). + reset(); + } else { + unknownSequence(b); + } + } + + @Override + public int getStateCode() { + return 19; + } + } +} + diff --git a/terminal-emulator/src/main/java/com/termux/terminal/TextFinder.java b/terminal-emulator/src/main/java/com/termux/terminal/TextFinder.java new file mode 100644 index 0000000000..1280d436c1 --- /dev/null +++ b/terminal-emulator/src/main/java/com/termux/terminal/TextFinder.java @@ -0,0 +1,136 @@ +package com.termux.terminal; + +public final class TextFinder { + + TerminalBuffer mTerminalBuffer; + + public TextFinder(TerminalBuffer terminalBuffer) { + mTerminalBuffer = terminalBuffer; + } + + public String getSelectedText(Cursor cursor1, Cursor cursor2, boolean joinBackLines, boolean joinFullLines) { + final StringBuilder builder = new StringBuilder(); + + if (cursor1.getRow() < -mTerminalBuffer.getActiveTranscriptRows()) cursor1.setRow(-mTerminalBuffer.getActiveTranscriptRows()); + if (cursor2.getRow() >= mTerminalBuffer.mScreenRows) cursor2.setRow(mTerminalBuffer.mScreenRows - 1); + + for (int row = cursor1.getRow(); row <= cursor2.getRow(); row++) { + final int x1 = (row == cursor1.getRow()) ? cursor1.getColumn() : 0; + final int x2 = getX2(cursor2, row); + + final TerminalRow lineObject = mTerminalBuffer.mLines[mTerminalBuffer.externalToInternalRow(row)]; + final int x1Index = lineObject.findStartOfColumn(x1); + final int x2Index = getX2Index(x2, lineObject, x1Index); + + final boolean rowLineWrap = mTerminalBuffer.getLineWrap(row); + + final char[] line = lineObject.mText; + final int lastPrintingCharIndex = getLastPrintingCharIndex(x2, x1Index, x2Index, line, rowLineWrap); + checkAndAppendLineText(builder, x1Index, line, lastPrintingCharIndex); + + final boolean lineFillsWidth = lastPrintingCharIndex == x2Index - 1; + checkAndAppendNewLine(builder, row, cursor2.getRow(), joinBackLines, joinFullLines, rowLineWrap, lineFillsWidth); + } + return builder.toString(); + } + + private int getX2(Cursor cursor, int row) { + if (row == cursor.getRow()) { + int x2 = cursor.getColumn() + 1; + if (x2 < mTerminalBuffer.mColumns) return x2; + } + return mTerminalBuffer.mColumns; + } + + private int getX2Index(int x2, TerminalRow lineObject, int x1Index) { + int x2Index = (x2 < mTerminalBuffer.mColumns) ? lineObject.findStartOfColumn(x2) : lineObject.getSpaceUsed(); + if (x2Index == x1Index) { + // Selected the start of a wide character. + x2Index = lineObject.findStartOfColumn(x2 + 1); + } + return x2Index; + } + + private int getLastPrintingCharIndex(int x2, int x1Index, int x2Index, char[] line, boolean rowLineWrap) { + if (rowLineWrap && x2 == mTerminalBuffer.mColumns){ + // If the line was wrapped, we shouldn't lose trailing space: + return x2Index - 1; + } + return findLastPrintingCharIndex(line, x1Index, x2Index); + } + + private int findLastPrintingCharIndex(char[] line, int x1Index, int x2Index) { + int lastPrintingCharIndex = -1; + for (int i = x1Index; i < x2Index; ++i) { + char c = line[i]; + if (c != ' ') lastPrintingCharIndex = i; + } + return lastPrintingCharIndex; + } + + private void checkAndAppendLineText(StringBuilder builder, int x1Index, char[] line, int lastPrintingCharIndex) { + final int len = lastPrintingCharIndex - x1Index + 1; + if (lastPrintingCharIndex != -1 && len > 0) builder.append(line, x1Index, len); + } + + private void checkAndAppendNewLine(StringBuilder builder, int row, int selY2, boolean joinBackLines, boolean joinFullLines, boolean rowLineWrap, boolean lineFillsWidth) { + boolean isBackLineRowNotWrapped = !(joinBackLines && rowLineWrap); + boolean isFullLineNotFillsWidth = !(joinFullLines && lineFillsWidth); + boolean isRowBeforeSelY2 = row < selY2 && row < mTerminalBuffer.mScreenRows - 1; + if (isBackLineRowNotWrapped && isFullLineNotFillsWidth && isRowBeforeSelY2) builder.append('\n'); + } + + public String getWordAtLocation(Cursor cursor) { + // Set y1 and y2 to the lines where the wrapped line starts and ends. + // I.e. if a line that is wrapped to 3 lines starts at line 4, and this + // is called with y=5, then y1 would be set to 4 and y2 would be set to 6. + int y1 = lineWrapStarts(cursor.getRow()); + int y2 = lineWrapEnds(cursor.getRow()); + + // Get the text for the whole wrapped line + String text = getSelectedText(new Cursor(y1, 0), new Cursor(y2, mTerminalBuffer.mColumns), true, true); + // The index of x in text + int textOffset = (cursor.getRow() - y1) * mTerminalBuffer.mColumns + cursor.getColumn(); + + if (textOffset >= text.length()) { + // The click was to the right of the last word on the line, so + // there's no word to return + return ""; + } + + return getWordAtOffset(text, textOffset); + } + + private String getWordAtOffset(String text, int textOffset) { + // Set x1 and x2 to the indices of the last space before x and the + // first space after x in text respectively + int x1 = text.lastIndexOf(' ', textOffset); + int x2 = text.indexOf(' ', textOffset); + if (x2 == -1) { + x2 = text.length(); + } + + if (x1 == x2) { + // The click was on a space, so there's no word to return + return ""; + } + return text.substring(x1 + 1, x2); + } + + private int lineWrapStarts(int y) { + int y1 = y; + while (y1 > 0 && !getSelectedText(new Cursor(0, y1 - 1), new Cursor(mTerminalBuffer.mColumns, y), true, true).contains("\n")) { + y1--; + } + return y1; + } + + private int lineWrapEnds(int y) { + int y2 = y; + while (y2 < mTerminalBuffer.mScreenRows && !getSelectedText(new Cursor(0, y), new Cursor(mTerminalBuffer.mColumns, y2 + 1), true, true).contains("\n")) { + y2++; + } + return y2; + } + +} diff --git a/terminal-emulator/src/main/java/com/termux/terminal/UpdateOldBuffer.java b/terminal-emulator/src/main/java/com/termux/terminal/UpdateOldBuffer.java new file mode 100644 index 0000000000..efa3e7d1ef --- /dev/null +++ b/terminal-emulator/src/main/java/com/termux/terminal/UpdateOldBuffer.java @@ -0,0 +1,147 @@ +package com.termux.terminal; + +public final class UpdateOldBuffer implements ResizeBuffer { + TerminalBuffer mTerminalBuffer; + + public UpdateOldBuffer(TerminalBuffer terminalBuffer) { + mTerminalBuffer = terminalBuffer; + } + + public void resize(int newColumns, int newRows, int newTotalRows, int[] cursor, long currentStyle, boolean isAltScreen) { + final TerminalRow[] oldLines = mTerminalBuffer.mLines; + + final int oldActiveTranscriptRows = mTerminalBuffer.mActiveTranscriptRows; + final int oldScreenFirstRow = mTerminalBuffer.mScreenFirstRow; + final int oldScreenRows = mTerminalBuffer.mScreenRows; + final int oldTotalRows = mTerminalBuffer.mTotalRows; + + final Cursor oldCursor = new Cursor(cursor[1], cursor[0]); + + mTerminalBuffer.mLines = new TerminalRow[newTotalRows]; + for (int i = 0; i < newTotalRows; i++) + mTerminalBuffer.mLines[i] = new TerminalRow(newColumns, currentStyle); + + mTerminalBuffer.mTotalRows = newTotalRows; + mTerminalBuffer.mScreenRows = newRows; + mTerminalBuffer.mActiveTranscriptRows = mTerminalBuffer.mScreenFirstRow = 0; + mTerminalBuffer.mColumns = newColumns; + + Cursor newCursor = new Cursor(-1, -1); + + Cursor currentOutputExternal = new Cursor(0, 0); + + // Loop over every character in the initial state. + // Blank lines should be skipped only if at end of transcript (just as is done in the "fast" resize), so we + // keep track how many blank lines we have skipped if we later on find a non-blank line. + int skippedBlankLines = 0; + for (int externalOldRow = -oldActiveTranscriptRows; externalOldRow < oldScreenRows; externalOldRow++) { + final TerminalRow oldLine = getOldTerminalRow(oldLines, oldScreenFirstRow, oldTotalRows, externalOldRow); + final boolean isCursorAtThisRow = externalOldRow == oldCursor.getRow(); + + // The cursor may only be on a non-null line, which we should not skip: + final boolean isOldCursorAtThisRow = (newCursor.getRow() == -1) && isCursorAtThisRow; + final boolean isCursorAtBlankLine = !isOldCursorAtThisRow && oldLine.isBlank(); + if (oldLine == null || isCursorAtBlankLine) { + skippedBlankLines++; + continue; + } + + if (skippedBlankLines > 0) { + // After skipping some blank lines we encounter a non-blank line. Insert the skipped blank lines. + insertSkippedBlankLines(currentStyle, currentOutputExternal, skippedBlankLines); + skippedBlankLines = 0; + } + + int lastNonSpaceIndex = 0; + boolean isJustToCursor = false; + if (isCursorAtThisRow || oldLine.mLineWrap) { + // Take the whole line, either because of cursor on it, or if line wrapping. + lastNonSpaceIndex = oldLine.getSpaceUsed(); + if (isCursorAtThisRow) isJustToCursor = true; + } else { + for (int i = 0; i < oldLine.getSpaceUsed(); i++) + // NEWLY INTRODUCED BUG! Should not index oldLine.mStyle with char indices + if (oldLine.mText[i] != ' '/* || oldLine.mStyle[i] != currentStyle */) + lastNonSpaceIndex = i + 1; + } + + copyLine(currentStyle, oldCursor, newCursor, currentOutputExternal, externalOldRow, oldLine, lastNonSpaceIndex, isJustToCursor); + + // Old row has been copied. Check if we need to insert newline if old line was not wrapping: + final boolean isOldRowCopied = externalOldRow != (oldScreenRows - 1); + final boolean isOldLineNotWrapping = !oldLine.mLineWrap; + if (isOldRowCopied && isOldLineNotWrapping) { + setNewCursorRow(currentStyle, newCursor, currentOutputExternal); + } + } + + cursor[0] = newCursor.getColumn(); + cursor[1] = newCursor.getRow(); + } + + private void copyLine(long currentStyle, Cursor oldCursor, Cursor newCursor, Cursor currentOutputExternal, int externalOldRow, TerminalRow oldLine, int lastNonSpaceIndex, boolean isJustToCursor) { + int currentOldCol = 0; + long styleAtCol = 0; + for (int i = 0; i < lastNonSpaceIndex; i++) { + // Note that looping over java character, not cells. + final char c = oldLine.mText[i]; + final int codePoint = (Character.isHighSurrogate(c)) ? Character.toCodePoint(c, oldLine.mText[++i]) : c; + final int displayWidth = WcWidth.width(codePoint); + // Use the last style if this is a zero-width character: + if (displayWidth > 0) styleAtCol = oldLine.getStyle(currentOldCol); + + // Line wrap as necessary: + lineWrap(currentStyle, newCursor, currentOutputExternal, displayWidth); + + final int offsetDueToCombiningChar = ((displayWidth <= 0 && currentOutputExternal.getColumn() > 0) ? 1 : 0); + final int outputColumn = currentOutputExternal.getColumn() - offsetDueToCombiningChar; + mTerminalBuffer.setChar(outputColumn, currentOutputExternal.getRow(), codePoint, styleAtCol); + + if (displayWidth > 0) { + if (oldCursor.getRow() == externalOldRow && oldCursor.getColumn() == currentOldCol) { + newCursor.setCursor(currentOutputExternal); + } + currentOldCol += displayWidth; + currentOutputExternal.addToColumn(displayWidth); + if (isJustToCursor && newCursor.getRow() != -1) break; + } + } + } + + private void lineWrap(long currentStyle, Cursor newCursor, Cursor currentOutputExternal, int displayWidth) { + final boolean isLineLong = currentOutputExternal.getColumn() + displayWidth > mTerminalBuffer.mColumns; + if (isLineLong) { + mTerminalBuffer.setLineWrap(currentOutputExternal.getRow()); + setNewCursorRow(currentStyle, newCursor, currentOutputExternal); + } + } + + private TerminalRow getOldTerminalRow(TerminalRow[] oldLines, int oldScreenFirstRow, int oldTotalRows, int externalOldRow) { + // Do what externalToInternalRow() does but for the old state: + int internalOldRow = oldScreenFirstRow + externalOldRow; + internalOldRow = (internalOldRow < 0) ? (oldTotalRows + internalOldRow) : (internalOldRow % oldTotalRows); + return oldLines[internalOldRow]; + } + + private void insertSkippedBlankLines(long currentStyle, Cursor currentOutputExternal, int skippedBlankLines) { + for (int i = 0; i < skippedBlankLines; i++) { + if (currentOutputExternal.getRow() == mTerminalBuffer.mScreenRows - 1) { + mTerminalBuffer.scrollDownOneLine(0, mTerminalBuffer.mScreenRows, currentStyle); + } else { + currentOutputExternal.addToRow(1); + } + currentOutputExternal.setColumn(0); + } + } + + private void setNewCursorRow(long currentStyle, Cursor newCursor, Cursor currentOutputExternal) { + if (currentOutputExternal.getRow() == mTerminalBuffer.mScreenRows - 1) { + if (newCursor.getRow() != -1) newCursor.addToRow(-1); + mTerminalBuffer.scrollDownOneLine(0, mTerminalBuffer.mScreenRows, currentStyle); + } else { + currentOutputExternal.addToRow(1); + } + currentOutputExternal.setColumn(0); + } + +} diff --git a/terminal-emulator/src/main/java/com/termux/terminal/WcWidth.java b/terminal-emulator/src/main/java/com/termux/terminal/WcWidth.java index 25660b30e8..14efdb8915 100644 --- a/terminal-emulator/src/main/java/com/termux/terminal/WcWidth.java +++ b/terminal-emulator/src/main/java/com/termux/terminal/WcWidth.java @@ -464,7 +464,7 @@ public final class WcWidth { }; - private static boolean intable(int[][] table, int c) { + private static boolean isInTable(int[][] table, int c) { // First quick check f|| Latin1 etc. characters. if (c < table[0][0]) return false; @@ -485,25 +485,38 @@ private static boolean intable(int[][] table, int c) { } /** Return the terminal display width of a code point: 0, 1 || 2. */ - public static int width(int ucs) { - if (ucs == 0 || - ucs == 0x034F || - (0x200B <= ucs && ucs <= 0x200F) || - ucs == 0x2028 || - ucs == 0x2029 || - (0x202A <= ucs && ucs <= 0x202E) || - (0x2060 <= ucs && ucs <= 0x2063)) { + public static int width(int codePoint) { + boolean isCombiningGraphemeJoiner = codePoint == 0x034F; + boolean isZeroWidth = 0x200B <= codePoint && codePoint <= 0x200D; + boolean isLeftToRightMark = codePoint == 0x200E; + boolean isRightToLeftMark = codePoint == 0x200F; + boolean isLineSeparator = codePoint == 0x2028; + boolean isParagraphSeparator = codePoint == 0x2029; + boolean isLeftToRightEmbedding = codePoint == 0x202A; + boolean isRightToLeftEmbedding = codePoint == 0x202B; + boolean isPopDirectionalFormatting = codePoint == 0x202C; + boolean isLeftToRightOverride = codePoint == 0x202d; + boolean isRightToLeftOverride = codePoint == 0x202E; + boolean isWordJoiner = codePoint == 0x2060; + boolean isFunctionApplication = codePoint == 0x2061; + boolean isInvisible = 0x2062 <= codePoint && codePoint <= 0x2064; + if (isCombiningGraphemeJoiner || + isZeroWidth || isLeftToRightMark || isRightToLeftMark || + isLineSeparator || isParagraphSeparator || + isLeftToRightEmbedding || isRightToLeftEmbedding || isPopDirectionalFormatting || + isLeftToRightOverride || isRightToLeftOverride || + isWordJoiner || isFunctionApplication || isInvisible) { return 0; } - // C0/C1 control characters + boolean isControlCharacters = (codePoint < 32) || (0x07F <= codePoint && codePoint < 0x0A0); // Termux change: Return 0 instead of -1. - if (ucs < 32 || (0x07F <= ucs && ucs < 0x0A0)) return 0; + if (isControlCharacters) return 0; // combining characters with zero width - if (intable(ZERO_WIDTH, ucs)) return 0; + if (isInTable(ZERO_WIDTH, codePoint)) return 0; - return intable(WIDE_EASTASIAN, ucs) ? 2 : 1; + return isInTable(WIDE_EASTASIAN, codePoint) ? 2 : 1; } /** The width at an index position in a java char array. */ diff --git a/terminal-emulator/src/test/java/com/termux/terminal/TerminalTest.java b/terminal-emulator/src/test/java/com/termux/terminal/TerminalTest.java index 390c0496ec..d7ca27c187 100644 --- a/terminal-emulator/src/test/java/com/termux/terminal/TerminalTest.java +++ b/terminal-emulator/src/test/java/com/termux/terminal/TerminalTest.java @@ -250,8 +250,18 @@ public void testParseColor() { assertEquals(0xFF0000FA, TerminalColors.parse("rgb:00/00/FA")); assertEquals(0xFF53186f, TerminalColors.parse("rgb:53/18/6f")); - assertEquals(0, TerminalColors.parse("invalid_0000FA")); - assertEquals(0, TerminalColors.parse("#3456")); + try { + TerminalColors.parse("invalid_0000FA"); + fail(); + } catch (IllegalArgumentException e) { + // pass + } + try { + TerminalColors.parse("#3456"); + fail(); + } catch (IllegalArgumentException e) { + // pass + } } /** The ncurses library still uses this. */ 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 307e422694..caa0180db1 100644 --- a/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java +++ b/terminal-view/src/main/java/com/termux/view/TerminalRenderer.java @@ -56,7 +56,7 @@ public TerminalRenderer(int textSize, Typeface typeface) { /** Render the terminal to a canvas with at a specified row scroll, and an optional rectangular selection. */ public final void render(TerminalEmulator mEmulator, Canvas canvas, int topRow, int selectionY1, int selectionY2, int selectionX1, int selectionX2) { - final boolean reverseVideo = mEmulator.isReverseVideo(); + final boolean isReverseVideo = mEmulator.isReverseVideo(); final int endRow = topRow + mEmulator.mRows; final int columns = mEmulator.mColumns; final int cursorCol = mEmulator.getCursorCol(); @@ -66,7 +66,7 @@ public final void render(TerminalEmulator mEmulator, Canvas canvas, int topRow, final int[] palette = mEmulator.mColors.mCurrentColors; final int cursorShape = mEmulator.getCursorStyle(); - if (reverseVideo) + if (isReverseVideo) canvas.drawColor(palette[TextStyle.COLOR_INDEX_FOREGROUND], PorterDuff.Mode.SRC); float heightOffset = mFontLineSpacingAndAscent; @@ -118,13 +118,10 @@ public final void render(TerminalEmulator mEmulator, Canvas canvas, int topRow, final int columnWidthSinceLastRun = column - lastRunStartColumn; final int charsSinceLastRun = currentCharIndex - lastRunStartIndex; int cursorColor = lastRunInsideCursor ? mEmulator.mColors.mCurrentColors[TextStyle.COLOR_INDEX_CURSOR] : 0; - boolean invertCursorTextColor = false; - if (lastRunInsideCursor && cursorShape == TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK) { - invertCursorTextColor = true; - } + boolean invertCursorTextColor = lastRunInsideCursor && cursorShape == TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK; drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun, measuredWidthForRun, - cursorColor, cursorShape, lastRunStyle, reverseVideo || invertCursorTextColor || lastRunInsideSelection); + cursorColor, cursorShape, lastRunStyle, isReverseVideo || invertCursorTextColor || lastRunInsideSelection); } measuredWidthForRun = 0.f; lastRunStyle = style; @@ -147,18 +144,15 @@ public final void render(TerminalEmulator mEmulator, Canvas canvas, int topRow, final int columnWidthSinceLastRun = columns - lastRunStartColumn; final int charsSinceLastRun = currentCharIndex - lastRunStartIndex; int cursorColor = lastRunInsideCursor ? mEmulator.mColors.mCurrentColors[TextStyle.COLOR_INDEX_CURSOR] : 0; - boolean invertCursorTextColor = false; - if (lastRunInsideCursor && cursorShape == TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK) { - invertCursorTextColor = true; - } + boolean invertCursorTextColor = lastRunInsideCursor && cursorShape == TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK; drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun, - measuredWidthForRun, cursorColor, cursorShape, lastRunStyle, reverseVideo || invertCursorTextColor || lastRunInsideSelection); + measuredWidthForRun, cursorColor, cursorShape, lastRunStyle, isReverseVideo || invertCursorTextColor || lastRunInsideSelection); } } private void drawTextRun(Canvas canvas, char[] text, int[] palette, float y, int startColumn, int runWidthColumns, int startCharIndex, int runWidthChars, float mes, int cursor, int cursorStyle, - long textStyle, boolean reverseVideo) { + long textStyle, boolean isReverseVideo) { int foreColor = TextStyle.decodeForeColor(textStyle); final int effect = TextStyle.decodeEffect(textStyle); int backColor = TextStyle.decodeBackColor(textStyle); @@ -179,7 +173,7 @@ private void drawTextRun(Canvas canvas, char[] text, int[] palette, float y, int } // Reverse video here if _one and only one_ of the reverse flags are set: - final boolean reverseVideoHere = reverseVideo ^ (effect & (TextStyle.CHARACTER_ATTRIBUTE_INVERSE)) != 0; + final boolean reverseVideoHere = isReverseVideo ^ (effect & (TextStyle.CHARACTER_ATTRIBUTE_INVERSE)) != 0; if (reverseVideoHere) { int tmp = foreColor; foreColor = backColor; 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 2b56d66ea6..160229eec7 100644 --- a/terminal-view/src/main/java/com/termux/view/TerminalView.java +++ b/terminal-view/src/main/java/com/termux/view/TerminalView.java @@ -61,6 +61,13 @@ public final class TerminalView extends View { public static final int TERMINAL_CURSOR_BLINK_RATE_MIN = 100; public static final int TERMINAL_CURSOR_BLINK_RATE_MAX = 2000; + public static final int CONTROL_CODE_POINT_ESC = '3'; + public static final int CONTROL_CODE_POINT_FS = '4'; + public static final int CONTROL_CODE_POINT_GS = '5'; + public static final int CONTROL_CODE_POINT_RS = '6'; + public static final int CONTROL_CODE_POINT_US = '7'; + public static final int CONTROL_CODE_POINT_DEL = '8'; + /** The top row of text to display. Ranges from -activeTranscriptRows to 0. */ int mTopRow; int[] mDefaultSelectors = new int[]{-1,-1,-1,-1}; @@ -94,7 +101,7 @@ public TerminalView(Context context, AttributeSet attributes) { // NO_UCD (unuse @Override public boolean onUp(MotionEvent event) { mScrollRemainder = 0.0f; - if (mEmulator != null && mEmulator.isMouseTrackingActive() && !event.isFromSource(InputDevice.SOURCE_MOUSE) && !isSelectingText() && !scrolledWithFinger) { + if (!isEmulatorNull() && mEmulator.isMouseTrackingActive() && !event.isFromSource(InputDevice.SOURCE_MOUSE) && !isSelectingText() && !scrolledWithFinger) { // Quick event processing when mouse tracking is active - do not wait for check of double tapping // for zooming. sendMouseEventCode(event, TerminalEmulator.MOUSE_LEFT_BUTTON, true); @@ -107,7 +114,7 @@ public boolean onUp(MotionEvent event) { @Override public boolean onSingleTapUp(MotionEvent event) { - if (mEmulator == null) return true; + if (isEmulatorNull()) return true; if (isSelectingText()) { stopTextSelectionMode(); @@ -120,7 +127,7 @@ public boolean onSingleTapUp(MotionEvent event) { @Override public boolean onScroll(MotionEvent e, float distanceX, float distanceY) { - if (mEmulator == null) return true; + if (isEmulatorNull()) return true; if (mEmulator.isMouseTrackingActive() && e.isFromSource(InputDevice.SOURCE_MOUSE)) { // If moving with mouse pointer while pressing button, report that instead of scroll. // This means that we never report moving with button press-events for touch input, @@ -139,7 +146,7 @@ public boolean onScroll(MotionEvent e, float distanceX, float distanceY) { @Override public boolean onScale(float focusX, float focusY, float scale) { - if (mEmulator == null || isSelectingText()) return true; + if (isEmulatorNull() || isSelectingText()) return true; mScaleFactor *= scale; mScaleFactor = mClient.onScale(mScaleFactor); return true; @@ -147,7 +154,7 @@ public boolean onScale(float focusX, float focusY, float scale) { @Override public boolean onFling(final MotionEvent e2, float velocityX, float velocityY) { - if (mEmulator == null) return true; + if (isEmulatorNull()) return true; // Do not start scrolling until last fling has been taken care of: if (!mScroller.isFinished()) return true; @@ -309,7 +316,7 @@ public boolean commitText(CharSequence text, int newCursorPosition) { } super.commitText(text, newCursorPosition); - if (mEmulator == null) return true; + if (isEmulatorNull()) return true; Editable content = getEditable(); sendTextToTerminal(content); @@ -389,21 +396,21 @@ void sendTextToTerminal(CharSequence text) { @Override protected int computeVerticalScrollRange() { - return mEmulator == null ? 1 : mEmulator.getScreen().getActiveRows(); + return isEmulatorNull() ? 1 : mEmulator.getScreen().getActiveRows(); } @Override protected int computeVerticalScrollExtent() { - return mEmulator == null ? 1 : mEmulator.mRows; + return isEmulatorNull() ? 1 : mEmulator.mRows; } @Override protected int computeVerticalScrollOffset() { - return mEmulator == null ? 1 : mEmulator.getScreen().getActiveRows() + mTopRow - mEmulator.mRows; + return isEmulatorNull() ? 1 : mEmulator.getScreen().getActiveRows() + mTopRow - mEmulator.mRows; } public void onScreenUpdated() { - if (mEmulator == null) return; + if (isEmulatorNull()) return; int rowsInHistory = mEmulator.getScreen().getActiveTranscriptRows(); if (mTopRow < -rowsInHistory) mTopRow = -rowsInHistory; @@ -525,7 +532,7 @@ void doScroll(MotionEvent event, int rowsDown) { /** Overriding {@link View#onGenericMotionEvent(MotionEvent)}. */ @Override public boolean onGenericMotionEvent(MotionEvent event) { - if (mEmulator != null && event.isFromSource(InputDevice.SOURCE_MOUSE) && event.getAction() == MotionEvent.ACTION_SCROLL) { + if (!isEmulatorNull() && event.isFromSource(InputDevice.SOURCE_MOUSE) && event.getAction() == MotionEvent.ACTION_SCROLL) { // Handle mouse wheel scrolling. boolean up = event.getAxisValue(MotionEvent.AXIS_VSCROLL) > 0.0f; doScroll(event, up ? -3 : 3); @@ -538,7 +545,7 @@ public boolean onGenericMotionEvent(MotionEvent event) { @Override @TargetApi(23) public boolean onTouchEvent(MotionEvent event) { - if (mEmulator == null) return true; + if (isEmulatorNull()) return true; final int action = event.getAction(); if (isSelectingText()) { @@ -699,7 +706,7 @@ public boolean onKeyPreIme(int keyCode, KeyEvent event) { public boolean onKeyDown(int keyCode, KeyEvent 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 (isEmulatorNull()) return true; if (isSelectingText()) { stopTextSelectionMode(); } @@ -780,7 +787,7 @@ public void inputCodePoint(int codePoint, boolean controlDownFromEvent, boolean if (mTermSession == null) return; // Ensure cursor is shown when a key is pressed down like long hold on (arrow) keys - if (mEmulator != null) + if (!isEmulatorNull()) mEmulator.setCursorBlinkState(true); final boolean controlDown = controlDownFromEvent || mClient.readControlKey(); @@ -795,19 +802,19 @@ public void inputCodePoint(int codePoint, boolean controlDownFromEvent, boolean codePoint = codePoint - 'A' + 1; } else if (codePoint == ' ' || codePoint == '2') { codePoint = 0; - } else if (codePoint == '[' || codePoint == '3') { + } else if (codePoint == '[' || codePoint == CONTROL_CODE_POINT_ESC) { codePoint = 27; // ^[ (Esc) - } else if (codePoint == '\\' || codePoint == '4') { + } else if (codePoint == '\\' || codePoint == CONTROL_CODE_POINT_FS) { codePoint = 28; - } else if (codePoint == ']' || codePoint == '5') { + } else if (codePoint == ']' || codePoint == CONTROL_CODE_POINT_GS) { codePoint = 29; - } else if (codePoint == '^' || codePoint == '6') { + } else if (codePoint == '^' || codePoint == CONTROL_CODE_POINT_RS) { codePoint = 30; // control-^ - } else if (codePoint == '_' || codePoint == '7' || codePoint == '/') { + } else if (codePoint == '_' || codePoint == CONTROL_CODE_POINT_US || codePoint == '/') { // "Ctrl-/ sends 0x1f which is equivalent of Ctrl-_ since the days of VT102" // - http://apple.stackexchange.com/questions/24261/how-do-i-send-c-that-is-control-slash-to-the-terminal codePoint = 31; - } else if (codePoint == '8') { + } else if (codePoint == CONTROL_CODE_POINT_DEL) { codePoint = 127; // DEL } } @@ -836,7 +843,7 @@ public void inputCodePoint(int codePoint, boolean controlDownFromEvent, boolean /** Input the specified keyCode if applicable and return if the input was consumed. */ public boolean handleKeyCode(int keyCode, int keyMod) { // Ensure cursor is shown when a key is pressed down like long hold on (arrow) keys - if (mEmulator != null) + if (!isEmulatorNull()) mEmulator.setCursorBlinkState(true); TerminalEmulator term = mTermSession.getEmulator(); @@ -860,7 +867,7 @@ public boolean onKeyUp(int keyCode, KeyEvent event) { // Do not return for KEYCODE_BACK and send it to the client since user may be trying // to exit the activity. - if (mEmulator == null && keyCode != KeyEvent.KEYCODE_BACK) return true; + if (isEmulatorNull() && keyCode != KeyEvent.KEYCODE_BACK) return true; if (mClient.onKeyUp(keyCode, event)) { invalidate(); @@ -892,7 +899,7 @@ public void updateSize() { int newColumns = Math.max(4, (int) (viewWidth / mRenderer.mFontWidth)); int newRows = Math.max(4, (viewHeight - mRenderer.mFontLineSpacingAndAscent) / mRenderer.mFontLineSpacing); - if (mEmulator == null || (newColumns != mEmulator.mColumns || newRows != mEmulator.mRows)) { + if (isEmulatorNull() || (newColumns != mEmulator.mColumns || newRows != mEmulator.mRows)) { mTermSession.updateSize(newColumns, newRows); mEmulator = mTermSession.getEmulator(); mClient.onEmulatorSet(); @@ -909,7 +916,7 @@ public void updateSize() { @Override protected void onDraw(Canvas canvas) { - if (mEmulator == null) { + if (isEmulatorNull()) { canvas.drawColor(0XFF000000); } else { // render the terminal view and highlight any selected text @@ -1072,7 +1079,7 @@ public synchronized void setTerminalCursorBlinkerState(boolean start, boolean st // Stop any existing cursor blinker callbacks stopTerminalCursorBlinker(); - if (mEmulator == null) return; + if (isEmulatorNull()) return; mEmulator.setCursorBlinkingEnabled(false); @@ -1128,7 +1135,7 @@ public void setEmulator(TerminalEmulator emulator) { public void run() { try { - if (mEmulator != null) { + if (!isEmulatorNull()) { // Toggle the blink state and then invalidate() the view so // that onDraw() is called, which then calls TerminalRenderer.render() // which checks with TerminalEmulator.shouldCursorBeVisible() to decide whether @@ -1280,4 +1287,7 @@ public void updateFloatingToolbarVisibility(MotionEvent event) { } } + private boolean isEmulatorNull() { + return mEmulator == null; + } } 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 index 2f64793a07..f621406924 100644 --- a/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionCursorController.java +++ b/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionCursorController.java @@ -3,7 +3,6 @@ import android.content.ClipboardManager; import android.content.Context; import android.graphics.Rect; -import android.text.TextUtils; import android.view.ActionMode; import android.view.Menu; import android.view.MenuItem; @@ -11,19 +10,14 @@ 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 TextSelectionCursorModel textSelectionCursorModel; 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; @@ -32,21 +26,24 @@ public class TextSelectionCursorController implements CursorController { 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()); + int mHandleHeight = Math.max(mStartHandle.getHandleHeight(), mEndHandle.getHandleHeight()); + + this.textSelectionCursorModel = new TextSelectionCursorModel(mHandleHeight); } @Override public void show(MotionEvent event) { setInitialTextSelectionPosition(event); - mStartHandle.positionAtCursor(mSelX1, mSelY1, true); - mEndHandle.positionAtCursor(mSelX2 + 1, mSelY2, true); + + setHandlerPosition(true); setActionModeCallBacks(); - mShowStartTime = System.currentTimeMillis(); - mIsSelectingText = true; + textSelectionCursorModel.setShorStartTime(System.currentTimeMillis()); + textSelectionCursorModel.setIsSelectingText(true); } @Override @@ -56,7 +53,7 @@ public boolean hide() { // 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) { + if (System.currentTimeMillis() - textSelectionCursorModel.getmShowStartTime() < 300) { return false; } @@ -68,8 +65,8 @@ public boolean hide() { mActionMode.finish(); } - mSelX1 = mSelY1 = mSelX2 = mSelY2 = -1; - mIsSelectingText = false; + textSelectionCursorModel.setSelectionPosition(-1); + textSelectionCursorModel.setIsSelectingText(false); return true; } @@ -78,41 +75,39 @@ public boolean hide() { public void render() { if (!isActive()) return; - mStartHandle.positionAtCursor(mSelX1, mSelY1, false); - mEndHandle.positionAtCursor(mSelX2 + 1, mSelY2, false); + setHandlerPosition(false); if (mActionMode != null) { mActionMode.invalidate(); } } + private void setHandlerPosition(boolean forceCheck) { + mStartHandle.positionAtCursor(textSelectionCursorModel.getmSelX1(), textSelectionCursorModel.getmSelY1(), forceCheck); + mEndHandle.positionAtCursor(textSelectionCursorModel.getmSelX2() + 1, textSelectionCursorModel.getmSelY2(), forceCheck); + } + public void setInitialTextSelectionPosition(MotionEvent event) { int[] columnAndRow = terminalView.getColumnAndRow(event, true); - mSelX1 = mSelX2 = columnAndRow[0]; - mSelY1 = mSelY2 = columnAndRow[1]; - - 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++; - } - } + textSelectionCursorModel.setSelectionPosition(columnAndRow); + + textSelectionCursorModel.setSelectionPositionBlank(terminalView.mEmulator); } public void setActionModeCallBacks() { final ActionMode.Callback callback = new ActionMode.Callback() { - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { + private void addStringOnMenu(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); + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + addStringOnMenu(menu); return true; } @@ -130,7 +125,8 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { switch (item.getItemId()) { case ACTION_COPY: - String selectedText = terminalView.mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2).trim(); + int[] selPosArr = textSelectionCursorModel.getSelPos(); + String selectedText = terminalView.mEmulator.getSelectedText(selPosArr[0], selPosArr[1], selPosArr[2], selPosArr[3]).trim(); terminalView.mTermSession.onCopyTextToClipboard(selectedText); terminalView.stopTextSelectionMode(); break; @@ -176,10 +172,11 @@ public void onDestroyActionMode(ActionMode mode) { @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()); + int[] selPosArr = textSelectionCursorModel.getSelPos(); + int x1 = Math.round(selPosArr[0] * terminalView.mRenderer.getFontWidth()); + int x2 = Math.round(selPosArr[2] * terminalView.mRenderer.getFontWidth()); + int y1 = Math.round((selPosArr[1] - 1 - terminalView.getTopRow()) * terminalView.mRenderer.getFontLineSpacing()); + int y2 = Math.round((selPosArr[3] + 1 - terminalView.getTopRow()) * terminalView.mRenderer.getFontLineSpacing()); if (x1 > x2) { int tmp = x1; @@ -188,8 +185,9 @@ public void onGetContentRect(ActionMode mode, View view, Rect outRect) { } int terminalBottom = terminalView.getBottom(); - int top = y1 + mHandleHeight; - int bottom = y2 + mHandleHeight; + int handleHeight = textSelectionCursorModel.getmHandleHeight(); + int top = y1 + handleHeight; + int bottom = y2 + handleHeight; if (top > terminalBottom) top = terminalBottom; if (bottom > terminalBottom) bottom = terminalBottom; @@ -200,128 +198,17 @@ public void onGetContentRect(ActionMode mode, View view, Rect outRect) { @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); - + textSelectionCursorModel.updatePosAtStartHandle(x, y, terminalView); } 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); + textSelectionCursorModel.updatePosAtEndHandle(x, y, terminalView); } 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; + textSelectionCursorModel.decrementYSelectionPos(decrement); } public boolean onTouchEvent(MotionEvent event) { @@ -340,7 +227,7 @@ public void onDetached() { @Override public boolean isActive() { - return mIsSelectingText; + return textSelectionCursorModel.getIsSelcetingText(); } public void getSelectors(int[] sel) { @@ -348,10 +235,11 @@ public void getSelectors(int[] sel) { return; } - sel[0] = mSelY1; - sel[1] = mSelY2; - sel[2] = mSelX1; - sel[3] = mSelX2; + int[] selPosArr = textSelectionCursorModel.getSelPos(); + sel[0] = selPosArr[1]; + sel[1] = selPosArr[3]; + sel[2] = selPosArr[0]; + sel[3] = selPosArr[2]; } public ActionMode getActionMode() { diff --git a/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionCursorModel.java b/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionCursorModel.java new file mode 100644 index 0000000000..a24aec3dfe --- /dev/null +++ b/terminal-view/src/main/java/com/termux/view/textselection/TextSelectionCursorModel.java @@ -0,0 +1,211 @@ +package com.termux.view.textselection; + +import android.text.TextUtils; +import android.view.ActionMode; + +import com.termux.terminal.TerminalBuffer; +import com.termux.terminal.TerminalEmulator; +import com.termux.terminal.WcWidth; +import com.termux.view.TerminalView; + +public class TextSelectionCursorModel { + private boolean mIsSelectingText = false; + private long mShowStartTime = System.currentTimeMillis(); + + private final int mHandleHeight; + private int mSelX1 = -1, mSelX2 = -1, mSelY1 = -1, mSelY2 = -1; + + TextSelectionCursorModel(int handleHeight) { + this.mHandleHeight = handleHeight; + } + + public int getmSelX1() { + return mSelX1; + } + + public int getmSelX2() { + return mSelX2; + } + + public int getmSelY1() { + return mSelY1; + } + + public int getmSelY2() { + return mSelY2; + } + + public int[] getSelPos() { + int[] selectionPosArray = {mSelX1, mSelY1, mSelX2, mSelY2}; + return selectionPosArray; + } + + public boolean getIsSelcetingText() { + return mIsSelectingText; + } + + public long getmShowStartTime() { + return mShowStartTime; + } + + public int getmHandleHeight() { + return mHandleHeight; + } + + public void setSelectionPosition(int[] columnAndRow) { + mSelX1 = mSelX2 = columnAndRow[0]; + mSelY1 = mSelY2 = columnAndRow[1]; + } + + public void setSelectionPosition(int pos) { + mSelX1 = mSelY1 = mSelX2 = mSelY2 = pos; + } + + public void setShorStartTime(long time) { + this.mShowStartTime = time; + } + + public void setIsSelectingText(boolean is) { + this.mIsSelectingText = is; + } + + public void setSelectionPositionBlank(TerminalEmulator mEmulator) { + TerminalBuffer screen = mEmulator.getScreen(); + String blankSpace = " "; + String blank = ""; + + if (!blankSpace.equals(screen.getSelectedText(mSelX1, mSelY1, mSelX1, mSelY1))) { + // Selecting something other than whitespace. Expand to word. + while (mSelX1 > 0 && !blank.equals(screen.getSelectedText(mSelX1 - 1, mSelY1, mSelX1 - 1, mSelY1))) { + mSelX1--; + } + while (mSelX2 < mEmulator.mColumns - 1 && !blank.equals(screen.getSelectedText(mSelX2 + 1, mSelY1, mSelX2 + 1, mSelY1))) { + mSelX2++; + } + } + } + + public void updatePosAtStartHandle(int x, int y, TerminalView terminalView) { + TerminalBuffer screen = terminalView.mEmulator.getScreen(); + final int scrollRows = screen.getActiveRows() - terminalView.mEmulator.mRows; + 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); + } + + public void updatePosAtEndHandle(int x, int y, TerminalView terminalView) { + TerminalBuffer screen = terminalView.mEmulator.getScreen(); + final int scrollRows = screen.getActiveRows() - terminalView.mEmulator.mRows; + 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); + } + + 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 decrementYSelectionPos(int decrement) { + mSelY1 -= decrement; + mSelY2 -= decrement; + } +} diff --git a/termux-shared/src/main/java/com/termux/shared/activity/media/AppCompatActivityUtils.java b/termux-shared/src/main/java/com/termux/shared/activity/media/AppCompatActivityUtils.java index 0666d58ee6..0f46220a65 100644 --- a/termux-shared/src/main/java/com/termux/shared/activity/media/AppCompatActivityUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/activity/media/AppCompatActivityUtils.java @@ -116,5 +116,4 @@ public static void setShowBackButtonInActionBar(@NonNull AppCompatActivity activ } } } - } diff --git a/termux-shared/src/main/java/com/termux/shared/android/AndroidUtils.java b/termux-shared/src/main/java/com/termux/shared/android/AndroidUtils.java index 766ea34ef9..722625f490 100644 --- a/termux-shared/src/main/java/com/termux/shared/android/AndroidUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/android/AndroidUtils.java @@ -101,15 +101,28 @@ public static String getDeviceInfoMarkdownString(@NonNull final Context context) StringBuilder markdownString = new StringBuilder(); markdownString.append("## Device Info"); + markdownString.append(getSoftwareInfoMarkdownString(context)); + markdownString.append(getHardwareInfoMarkdownString()); + + markdownString.append("\n##\n"); + + return markdownString.toString(); + } + + private static String getSoftwareInfoMarkdownString(@NonNull final Context context) { + Properties systemProperties = getSystemProperties(); + StringBuilder markdownString = new StringBuilder(); markdownString.append("\n\n### Software\n"); appendPropertyToMarkdown(markdownString,"OS_VERSION", getSystemPropertyWithAndroidAPI("os.version")); appendPropertyToMarkdown(markdownString, "SDK_INT", Build.VERSION.SDK_INT); + // If its a release version if ("REL".equals(Build.VERSION.CODENAME)) appendPropertyToMarkdown(markdownString, "RELEASE", Build.VERSION.RELEASE); else appendPropertyToMarkdown(markdownString, "CODENAME", Build.VERSION.CODENAME); + appendPropertyToMarkdown(markdownString, "ID", Build.ID); appendPropertyToMarkdown(markdownString, "DISPLAY", Build.DISPLAY); appendPropertyToMarkdown(markdownString, "INCREMENTAL", Build.VERSION.INCREMENTAL); @@ -124,6 +137,12 @@ public static String getDeviceInfoMarkdownString(@NonNull final Context context) if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) appendPropertyToMarkdown(markdownString, "MONITOR_PHANTOM_PROCS", FeatureFlagUtils.getFeatureFlagValueString(context, FeatureFlagUtils.SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS).getName()); + return markdownString.toString(); + } + + private static String getHardwareInfoMarkdownString() { + StringBuilder markdownString = new StringBuilder(); + markdownString.append("\n\n### Hardware\n"); appendPropertyToMarkdown(markdownString, "MANUFACTURER", Build.MANUFACTURER); appendPropertyToMarkdown(markdownString, "BRAND", Build.BRAND); @@ -134,13 +153,12 @@ public static String getDeviceInfoMarkdownString(@NonNull final Context context) appendPropertyToMarkdown(markdownString, "DEVICE", Build.DEVICE); appendPropertyToMarkdown(markdownString, "SUPPORTED_ABIS", Joiner.on(", ").skipNulls().join(Build.SUPPORTED_ABIS)); - markdownString.append("\n##\n"); - return markdownString.toString(); } + public static Properties getSystemProperties() { Properties systemProperties = new Properties(); @@ -198,7 +216,11 @@ public static String getSystemPropertyWithAndroidAPI(@NonNull String property) { public static void appendPropertyToMarkdownIfSet(StringBuilder markdownString, String label, Object value) { if (value == null) return; - if (value instanceof String && (((String) value).isEmpty()) || "REL".equals(value)) return; + boolean isString = value instanceof String; + boolean isEmpty = ((String) value).isEmpty(); + boolean isREL = "REL".equals(value); + + if (isString && (isEmpty || isREL)) return; markdownString.append("\n").append(getPropertyMarkdown(label, value)); } diff --git a/termux-shared/src/main/java/com/termux/shared/android/PermissionUtils.java b/termux-shared/src/main/java/com/termux/shared/android/PermissionUtils.java index ff19a1512a..dc30c19a2c 100644 --- a/termux-shared/src/main/java/com/termux/shared/android/PermissionUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/android/PermissionUtils.java @@ -73,7 +73,9 @@ public static boolean checkPermissions(@NonNull Context context, @NonNull String int result; for (String permission : permissions) { result = ContextCompat.checkSelfPermission(context, permission); - if (result != PackageManager.PERMISSION_GRANTED) { + boolean permissionGranted = (result == PackageManager.PERMISSION_GRANTED); + + if (!permissionGranted) { return false; } } @@ -128,14 +130,19 @@ public static boolean requestPermissions(@NonNull Context context, @NonNull Stri for (String permission : permissions) { int result = ContextCompat.checkSelfPermission(context, permission); + boolean permissionGranted = (result == PackageManager.PERMISSION_GRANTED); // If at least one permission not granted - if (result != PackageManager.PERMISSION_GRANTED) { + + if (!permissionGranted) { Logger.logInfo(LOG_TAG, "Requesting Permissions: " + Arrays.toString(permissions)); try { - if (context instanceof AppCompatActivity) + boolean isAppCompatActivity = context instanceof AppCompatActivity; + boolean isActivity = context instanceof Activity; + + if (isAppCompatActivity) ((AppCompatActivity) context).requestPermissions(permissions, requestCode); - else if (context instanceof Activity) + else if (isActivity) ((Activity) context).requestPermissions(permissions, requestCode); else { Error.logErrorAndShowToast(context, LOG_TAG, diff --git a/termux-shared/src/main/java/com/termux/shared/errors/Errno.java b/termux-shared/src/main/java/com/termux/shared/errors/Errno.java index 1ac9fd089b..130e34b215 100644 --- a/termux-shared/src/main/java/com/termux/shared/errors/Errno.java +++ b/termux-shared/src/main/java/com/termux/shared/errors/Errno.java @@ -5,6 +5,7 @@ import androidx.annotation.NonNull; import com.termux.shared.logger.Logger; +import com.termux.shared.errors.Error.ErrorBuilder; import java.util.Arrays; import java.util.Collections; @@ -70,7 +71,10 @@ public String getMessage() { * @param code The unique code of the {@link Errno}. */ public static Errno valueOf(String type, Integer code) { - if (type == null || type.isEmpty() || code == null) return null; + boolean isTypeInvaild = (type == null) || (type.isEmpty()); + boolean isCodeInvalid = (code == null); + + if (isTypeInvaild || isCodeInvalid) return null; return map.get(type + ":" + code); } @@ -100,13 +104,24 @@ public Error getError(Throwable throwable, Object... args) { public Error getError(List throwablesList, Object... args) { try { if (throwablesList == null) - return new Error(getType(), getCode(), String.format(getMessage(), args)); + return new ErrorBuilder().setType(getType()) + .setCode(getCode()) + .setMessage(String.format(getMessage(), args)) + .build(); else - return new Error(getType(), getCode(), String.format(getMessage(), args), throwablesList); + return new ErrorBuilder().setType(getType()) + .setCode(getCode()) + .setMessage(String.format(getMessage(), args)) + .setThrowableList(throwablesList) + .build(); } catch (Exception e) { Logger.logWarn(LOG_TAG, "Exception raised while calling String.format() for error message of errno " + this + " with args" + Arrays.toString(args) + "\n" + e.getMessage()); // Return unformatted message as a backup - return new Error(getType(), getCode(), getMessage() + ": " + Arrays.toString(args), throwablesList); + return new ErrorBuilder().setType(getType()) + .setCode(getCode()) + .setMessage(getMessage() + ": " + Arrays.toString(args)) + .setThrowableList(throwablesList) + .build(); } } diff --git a/termux-shared/src/main/java/com/termux/shared/errors/Error.java b/termux-shared/src/main/java/com/termux/shared/errors/Error.java index be00e34ff7..e0e9344670 100644 --- a/termux-shared/src/main/java/com/termux/shared/errors/Error.java +++ b/termux-shared/src/main/java/com/termux/shared/errors/Error.java @@ -23,7 +23,7 @@ public class Error implements Serializable { /** The error message. */ private String message; /** The error exceptions. */ - private List throwablesList = new ArrayList<>(); + private List throwableList = new ArrayList<>(); private static final String LOG_TAG = "Error"; @@ -32,8 +32,8 @@ public Error() { InitError(null, null, null, null); } - public Error(String type, Integer code, String message, List throwablesList) { - InitError(type, code, message, throwablesList); + public Error(String type, Integer code, String message, List throwableList) { + InitError(type, code, message, throwableList); } public Error(String type, Integer code, String message, Throwable throwable) { @@ -44,8 +44,8 @@ public Error(String type, Integer code, String message) { InitError(type, code, message, null); } - public Error(Integer code, String message, List throwablesList) { - InitError(null, code, message, throwablesList); + public Error(Integer code, String message, List throwableList) { + InitError(null, code, message, throwableList); } public Error(Integer code, String message, Throwable throwable) { @@ -60,8 +60,8 @@ public Error(String message, Throwable throwable) { InitError(null, null, message, Collections.singletonList(throwable)); } - public Error(String message, List throwablesList) { - InitError(null, null, message, throwablesList); + public Error(String message, List throwableList) { + InitError(null, null, message, throwableList); } public Error(String message) { @@ -82,7 +82,50 @@ private void InitError(String type, Integer code, String message, List throwableList = new ArrayList<>(); + + public ErrorBuilder setType(String type) { + if (type != null && !type.isEmpty()) + this.type = type; + else + this.type = Errno.TYPE; + return this; + } + + public ErrorBuilder setCode(Integer code) { + if (code != null && code > Errno.ERRNO_SUCCESS.getCode()) + this.code = code; + else + this.code = Errno.ERRNO_SUCCESS.getCode(); + return this; + } + + public ErrorBuilder setMessage(String message) { + this.message = message; + return this; + } + + public ErrorBuilder setThrowableList(Throwable throwable) { + return this.setThrowableList(Collections.singletonList(throwable)); + } + + public ErrorBuilder setThrowableList(List throwableList) { + if (throwableList != null) + this.throwableList = throwableList; + return this; + } + + public Error build() { + return new Error(type, code, message, throwableList); + } } public Error setLabel(String label) { @@ -117,8 +160,8 @@ public void appendMessage(String message) { this.message = this.message + message; } - public List getThrowablesList() { - return Collections.unmodifiableList(throwablesList); + public List getThrowableList() { + return Collections.unmodifiableList(throwableList); } @@ -147,7 +190,7 @@ public synchronized boolean setStateFailed(int code, String message, List throwablesList) { this.message = message; - this.throwablesList = throwablesList; + this.throwableList = throwablesList; if (type != null && !type.isEmpty()) this.type = type; @@ -209,7 +252,7 @@ public String getErrorLogString() { logString.append(getCodeString()); logString.append("\n").append(getTypeAndMessageLogString()); - if (throwablesList != null && throwablesList.size() > 0) + if (throwableList != null && throwableList.size() > 0) logString.append("\n").append(geStackTracesLogString()); return logString.toString(); @@ -271,8 +314,8 @@ public String getErrorMarkdownString() { markdownString.append(MarkdownUtils.getSingleLineMarkdownStringEntry("Error Code", getCode(), "-")); markdownString.append("\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry( - (Errno.TYPE.equals(getType()) ? "Error Message" : "Error Message (" + getType() + ")"), message, "-")); - if (throwablesList != null && throwablesList.size() > 0) + (Errno.TYPE.equals(getType()) ? "Error Message" : "Error Message (" + getType() + ")"), message, "-")); + if (throwableList != null && throwableList.size() > 0) markdownString.append("\n\n").append(geStackTracesMarkdownString()); return markdownString.toString(); @@ -288,11 +331,11 @@ public String getTypeAndMessageLogString() { } public String geStackTracesLogString() { - return Logger.getStackTracesString("StackTraces:", Logger.getStackTracesStringArray(throwablesList)); + return Logger.getStackTracesString("StackTraces:", Logger.getStackTracesStringArray(throwableList)); } public String geStackTracesMarkdownString() { - return Logger.getStackTracesMarkdownString("StackTraces", Logger.getStackTracesStringArray(throwablesList)); + return Logger.getStackTracesMarkdownString("StackTraces", Logger.getStackTracesStringArray(throwableList)); } } diff --git a/termux-shared/src/main/java/com/termux/shared/file/FileUtils.java b/termux-shared/src/main/java/com/termux/shared/file/FileUtils.java index f51de78283..95571e428b 100644 --- a/termux-shared/src/main/java/com/termux/shared/file/FileUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/file/FileUtils.java @@ -1883,7 +1883,7 @@ public static Error getShortFileUtilsError(final Error error) { Errno shortErrno = FileUtilsErrno.ERRNO_SHORT_MAPPING.get(Errno.valueOf(type, error.getCode())); if (shortErrno == null) return error; - List throwables = error.getThrowablesList(); + List throwables = error.getThrowableList(); if (throwables.isEmpty()) return shortErrno.getError(DataUtils.getDefaultIfNull(error.getLabel(), "file")); else diff --git a/termux-shared/src/main/java/com/termux/shared/file/filesystem/FileAttributes.java b/termux-shared/src/main/java/com/termux/shared/file/filesystem/FileAttributes.java index 4565acc3da..830db35d49 100644 --- a/termux-shared/src/main/java/com/termux/shared/file/filesystem/FileAttributes.java +++ b/termux-shared/src/main/java/com/termux/shared/file/filesystem/FileAttributes.java @@ -210,18 +210,17 @@ public boolean isBlock() { return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFBLK); } + /** Is not regular file, directory or symbolic link */ public boolean isOther() { - int type = st_mode & UnixConstants.S_IFMT; - return (type != UnixConstants.S_IFREG && - type != UnixConstants.S_IFDIR && - type != UnixConstants.S_IFLNK); + return (!isRegularFile() && + !isDirectory() && + !isSymbolicLink()); } - + /** Is character, block or fifo */ public boolean isDevice() { - int type = st_mode & UnixConstants.S_IFMT; - return (type == UnixConstants.S_IFCHR || - type == UnixConstants.S_IFBLK || - type == UnixConstants.S_IFIFO); + return (isCharacter() || + isBlock() || + isFifo()); } public long size() { @@ -265,25 +264,35 @@ public Set permissions() { int bits = (st_mode & UnixConstants.S_IAMB); HashSet perms = new HashSet<>(); - if ((bits & UnixConstants.S_IRUSR) > 0) + boolean canOwnerRead = ((bits & UnixConstants.S_IRUSR) > 0); + boolean canOwnerWrite = ((bits & UnixConstants.S_IWUSR) > 0); + boolean canOwnerExecute = ((bits & UnixConstants.S_IXUSR) > 0); + boolean canGroupRead = ((bits & UnixConstants.S_IRGRP) > 0); + boolean canGroupWrite = ((bits & UnixConstants.S_IWGRP) > 0); + boolean canGroupExecute = ((bits & UnixConstants.S_IXGRP) > 0); + boolean canOtherRead = ((bits & UnixConstants.S_IROTH) > 0); + boolean canOtherWrite = ((bits & UnixConstants.S_IWOTH) > 0); + boolean canOtherExecute = ((bits & UnixConstants.S_IXOTH) > 0); + + if (canOwnerRead) perms.add(FilePermission.OWNER_READ); - if ((bits & UnixConstants.S_IWUSR) > 0) + if (canOwnerWrite) perms.add(FilePermission.OWNER_WRITE); - if ((bits & UnixConstants.S_IXUSR) > 0) + if (canOwnerExecute) perms.add(FilePermission.OWNER_EXECUTE); - if ((bits & UnixConstants.S_IRGRP) > 0) + if (canGroupRead) perms.add(FilePermission.GROUP_READ); - if ((bits & UnixConstants.S_IWGRP) > 0) + if (canGroupWrite) perms.add(FilePermission.GROUP_WRITE); - if ((bits & UnixConstants.S_IXGRP) > 0) + if (canGroupExecute) perms.add(FilePermission.GROUP_EXECUTE); - if ((bits & UnixConstants.S_IROTH) > 0) + if (canOtherRead) perms.add(FilePermission.OTHERS_READ); - if ((bits & UnixConstants.S_IWOTH) > 0) + if (canOtherWrite) perms.add(FilePermission.OTHERS_WRITE); - if ((bits & UnixConstants.S_IXOTH) > 0) + if (canOtherExecute) perms.add(FilePermission.OTHERS_EXECUTE); return perms; diff --git a/termux-shared/src/main/java/com/termux/shared/logger/Logger.java b/termux-shared/src/main/java/com/termux/shared/logger/Logger.java index e5075b1f91..0a245508f0 100644 --- a/termux-shared/src/main/java/com/termux/shared/logger/Logger.java +++ b/termux-shared/src/main/java/com/termux/shared/logger/Logger.java @@ -54,16 +54,33 @@ public class Logger { public static void logMessage(int logPriority, String tag, String message) { - if (logPriority == Log.ERROR && CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL) - Log.e(getFullTag(tag), message); - else if (logPriority == Log.WARN && CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL) - Log.w(getFullTag(tag), message); - else if (logPriority == Log.INFO && CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL) - Log.i(getFullTag(tag), message); - else if (logPriority == Log.DEBUG && CURRENT_LOG_LEVEL >= LOG_LEVEL_DEBUG) - Log.d(getFullTag(tag), message); - else if (logPriority == Log.VERBOSE && CURRENT_LOG_LEVEL >= LOG_LEVEL_VERBOSE) - Log.v(getFullTag(tag), message); + boolean higherThanNormal = (CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL); + boolean higherThanDebug = (CURRENT_LOG_LEVEL >= LOG_LEVEL_DEBUG); + boolean higherThanVerbose = (CURRENT_LOG_LEVEL >= LOG_LEVEL_VERBOSE); + + switch (logPriority) { + case Log.ERROR: + if (higherThanNormal) + Log.e(getFullTag(tag), message); break; + + case Log.WARN: + if (higherThanNormal) + Log.w(getFullTag(tag), message); break; + + case Log.INFO: + if (higherThanNormal) + Log.i(getFullTag(tag), message); break; + + case Log.DEBUG: + if (higherThanDebug) + Log.d(getFullTag(tag), message); break; + + case Log.VERBOSE: + if (higherThanVerbose) + Log.v(getFullTag(tag), message); break; + + default: break; + } } public static void logExtendedMessage(int logLevel, String tag, String message) { diff --git a/termux-shared/src/main/java/com/termux/shared/models/ReportInfo.java b/termux-shared/src/main/java/com/termux/shared/models/ReportInfo.java index 0f70fb5623..1372c22171 100644 --- a/termux-shared/src/main/java/com/termux/shared/models/ReportInfo.java +++ b/termux-shared/src/main/java/com/termux/shared/models/ReportInfo.java @@ -1,9 +1,14 @@ package com.termux.shared.models; +import android.util.Pair; + import com.termux.shared.markdown.MarkdownUtils; import com.termux.shared.android.AndroidUtils; +import com.termux.shared.net.socket.local.LocalClientSocket; import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; /** * An object that stored info for {@link com.termux.shared.activities.ReportActivity}. @@ -71,6 +76,20 @@ public void setReportSaveFilePath(String reportSaveFilePath) { this.reportSaveFilePath = reportSaveFilePath; } + /** + * Get log variables {@link List < Pair >} for {@link ReportInfo}. + * + * @return Returns the log variables in list {@link List< Pair >}. + */ + private static List> getLogVariableList(final ReportInfo reportInfo) { + List> logVariableList = new ArrayList>() {{ + add(Pair.create("User Action", reportInfo.userAction)); + add(Pair.create("Sender", reportInfo.sender)); + add(Pair.create("Report Timestamp", reportInfo.reportTimestamp)); + }}; + return logVariableList; + } + /** * Get a markdown {@link String} for {@link ReportInfo}. * @@ -84,9 +103,13 @@ public static String getReportInfoMarkdownString(final ReportInfo reportInfo) { if (reportInfo.addReportInfoHeaderToMarkdown) { markdownString.append("## Report Info\n\n"); - markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("User Action", reportInfo.userAction, "-")); - markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Sender", reportInfo.sender, "-")); - markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Report Timestamp", reportInfo.reportTimestamp, "-")); + + for (Pair logVar: getLogVariableList(reportInfo)) { + String label = logVar.first; + Object object = logVar.second; + markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry(label, object, "-")); + } + markdownString.append("\n##\n\n"); } diff --git a/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalClientSocket.java b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalClientSocket.java index 75a7e6a8c3..63b3f82446 100644 --- a/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalClientSocket.java +++ b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalClientSocket.java @@ -1,5 +1,7 @@ package com.termux.shared.net.socket.local; +import android.util.Pair; + import androidx.annotation.NonNull; import com.termux.shared.data.DataUtils; @@ -15,6 +17,8 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; +import java.util.ArrayList; +import java.util.List; /** The client socket for {@link LocalSocketManager}. */ public class LocalClientSocket implements Closeable { @@ -359,7 +363,18 @@ public InputStreamReader getInputStreamReader() { return new InputStreamReader(getInputStream()); } - + /** + * Get log variables {@link List < Pair >} for {@link LocalClientSocket}. + * + * @return Returns the log variables in list {@link List>}. + */ + private List> getLogVariableList() { + List> logVariableList = new ArrayList>() {{ + add(Pair.create("FD", mFD)); + add(Pair.create("Creation Time", mCreationTime)); + }}; + return logVariableList; + } /** Get a log {@link String} for the {@link LocalClientSocket}. */ @NonNull @@ -367,8 +382,13 @@ public String getLogString() { StringBuilder logString = new StringBuilder(); logString.append("Client Socket:"); - logString.append("\n").append(Logger.getSingleLineLogStringEntry("FD", mFD, "-")); - logString.append("\n").append(Logger.getSingleLineLogStringEntry("Creation Time", mCreationTime, "-")); + + for (Pair logVar: getLogVariableList()) { + String label = logVar.first; + Object object = logVar.second; + logString.append("\n").append(Logger.getSingleLineLogStringEntry(label, object, "-")); + } + logString.append("\n\n\n"); logString.append(mPeerCred.getLogString()); @@ -382,8 +402,13 @@ public String getMarkdownString() { StringBuilder markdownString = new StringBuilder(); markdownString.append("## ").append("Client Socket"); - markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("FD", mFD, "-")); - markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Creation Time", mCreationTime, "-")); + + for (Pair logVar: getLogVariableList()) { + String label = logVar.first; + Object object = logVar.second; + markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry(label, object, "-")); + } + markdownString.append("\n\n\n"); markdownString.append(mPeerCred.getMarkdownString()); diff --git a/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketRunConfig.java b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketRunConfig.java index 53ef895720..5ff7eb8d91 100644 --- a/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketRunConfig.java +++ b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketRunConfig.java @@ -1,5 +1,7 @@ package com.termux.shared.net.socket.local; +import android.util.Pair; + import androidx.annotation.NonNull; import com.termux.shared.file.FileUtils; @@ -8,6 +10,8 @@ import java.io.Serializable; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; /** @@ -207,20 +211,37 @@ public static String getRunConfigLogString(final LocalSocketRunConfig config) { return config.getLogString(); } + /** + * Get log variables {@link List>} for {@link LocalSocketRunConfig}. + * + * @return Returns the log variables in list {@link List>}. + */ + private List> getLogVariableList() { + List> logVariableList = new ArrayList>() {{ + add(Pair.create("Path", mPath)); + add(Pair.create("AbstractNamespaceSocket", mAbstractNamespaceSocket)); + add(Pair.create("LocalSocketManagerClient", mLocalSocketManagerClient.getClass().getName())); + add(Pair.create("FD", mFD)); + add(Pair.create("ReceiveTimeout", getReceiveTimeout())); + add(Pair.create("SendTimeout", getSendTimeout())); + add(Pair.create("Deadline", getDeadline())); + add(Pair.create("Backlog", getBacklog())); + }}; + return logVariableList; + } + /** Get a log {@link String} for the {@link LocalSocketRunConfig}. */ @NonNull public String getLogString() { StringBuilder logString = new StringBuilder(); logString.append(mTitle).append(" Socket Server Run Config:"); - logString.append("\n").append(Logger.getSingleLineLogStringEntry("Path", mPath, "-")); - logString.append("\n").append(Logger.getSingleLineLogStringEntry("AbstractNamespaceSocket", mAbstractNamespaceSocket, "-")); - logString.append("\n").append(Logger.getSingleLineLogStringEntry("LocalSocketManagerClient", mLocalSocketManagerClient.getClass().getName(), "-")); - logString.append("\n").append(Logger.getSingleLineLogStringEntry("FD", mFD, "-")); - logString.append("\n").append(Logger.getSingleLineLogStringEntry("ReceiveTimeout", getReceiveTimeout(), "-")); - logString.append("\n").append(Logger.getSingleLineLogStringEntry("SendTimeout", getSendTimeout(), "-")); - logString.append("\n").append(Logger.getSingleLineLogStringEntry("Deadline", getDeadline(), "-")); - logString.append("\n").append(Logger.getSingleLineLogStringEntry("Backlog", getBacklog(), "-")); + + for(Pair logVar: getLogVariableList()) { + String label = logVar.first; + Object object = logVar.second; + logString.append("\n").append(Logger.getSingleLineLogStringEntry(label, object, "-")); + } return logString.toString(); } @@ -242,14 +263,12 @@ public String getMarkdownString() { StringBuilder markdownString = new StringBuilder(); markdownString.append("## ").append(mTitle).append(" Socket Server Run Config"); - markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Path", mPath, "-")); - markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("AbstractNamespaceSocket", mAbstractNamespaceSocket, "-")); - markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("LocalSocketManagerClient", mLocalSocketManagerClient.getClass().getName(), "-")); - markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("FD", mFD, "-")); - markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("ReceiveTimeout", getReceiveTimeout(), "-")); - markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("SendTimeout", getSendTimeout(), "-")); - markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Deadline", getDeadline(), "-")); - markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Backlog", getBacklog(), "-")); + + for(Pair logVar: getLogVariableList()) { + String label = logVar.first; + Object object = logVar.second; + markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry(label, object, "-")); + } return markdownString.toString(); } diff --git a/termux-shared/src/main/java/com/termux/shared/net/socket/local/PeerCred.java b/termux-shared/src/main/java/com/termux/shared/net/socket/local/PeerCred.java index 6674eee0f7..74fce0da9f 100644 --- a/termux-shared/src/main/java/com/termux/shared/net/socket/local/PeerCred.java +++ b/termux-shared/src/main/java/com/termux/shared/net/socket/local/PeerCred.java @@ -1,6 +1,7 @@ package com.termux.shared.net.socket.local; import android.content.Context; +import android.util.Pair; import androidx.annotation.Keep; import androidx.annotation.NonNull; @@ -10,6 +11,9 @@ import com.termux.shared.logger.Logger; import com.termux.shared.markdown.MarkdownUtils; +import java.util.ArrayList; +import java.util.List; + /** The {@link PeerCred} of the {@link LocalClientSocket} containing info of client/peer. */ @Keep public class PeerCred { @@ -76,18 +80,39 @@ public static String getPeerCredLogString(final PeerCred peerCred) { return peerCred.getLogString(); } + /** + * Get log variables {@link List < Pair >} for {@link PeerCred}. + * + * @return Returns the log variables in list {@link List< Pair >}. + */ + private List> getLogVariableList() { + List> variableList = new ArrayList>() {{ + add(Pair.create("Process", getProcessString())); + add(Pair.create("User", getUserString())); + add(Pair.create("Group", getGroupString())); + add(Pair.create("Cmdline", cmdline)); + }}; + return variableList; + } + /** Get a log {@link String} for the {@link PeerCred}. */ @NonNull public String getLogString() { StringBuilder logString = new StringBuilder(); logString.append("Peer Cred:"); - logString.append("\n").append(Logger.getSingleLineLogStringEntry("Process", getProcessString(), "-")); - logString.append("\n").append(Logger.getSingleLineLogStringEntry("User", getUserString(), "-")); - logString.append("\n").append(Logger.getSingleLineLogStringEntry("Group", getGroupString(), "-")); - if (cmdline != null) - logString.append("\n").append(Logger.getMultiLineLogStringEntry("Cmdline", cmdline, "-")); + for (Pair logVar: getLogVariableList()) { + String label = logVar.first; + String object = logVar.second; + switch(label) { + case "Cmdline": + if (label != null) logString.append("\n").append(Logger.getMultiLineLogStringEntry(label, object, "-")); + break; + default: + logString.append("\n").append(Logger.getSingleLineLogStringEntry(label, object, "-")); + } + } return logString.toString(); } @@ -109,12 +134,18 @@ public String getMarkdownString() { StringBuilder markdownString = new StringBuilder(); markdownString.append("## ").append("Peer Cred"); - markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Process", getProcessString(), "-")); - markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("User", getUserString(), "-")); - markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Group", getGroupString(), "-")); - if (cmdline != null) - markdownString.append("\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Cmdline", cmdline, "-")); + for (Pair logVar: getLogVariableList()) { + String label = logVar.first; + String object = logVar.second; + switch(label) { + case "Cmdline": + if (label != null) markdownString.append("\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry(label, object, "-")); + break; + default: + markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry(label, object, "-")); + } + } return markdownString.toString(); } diff --git a/termux-shared/src/main/java/com/termux/shared/reflection/InvokeResult.java b/termux-shared/src/main/java/com/termux/shared/reflection/InvokeResult.java new file mode 100644 index 0000000000..847a2bbab7 --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/reflection/InvokeResult.java @@ -0,0 +1,12 @@ +package com.termux.shared.reflection; + +/** Class that represents result of invoking of something */ +public abstract class InvokeResult { + public boolean success; + public Object value; + + InvokeResult(boolean success, Object value) { + this.value = success; + this.value = value; + } +} diff --git a/termux-shared/src/main/java/com/termux/shared/reflection/ReflectionUtils.java b/termux-shared/src/main/java/com/termux/shared/reflection/ReflectionUtils.java index fadcb7d986..b00237808d 100644 --- a/termux-shared/src/main/java/com/termux/shared/reflection/ReflectionUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/reflection/ReflectionUtils.java @@ -26,7 +26,9 @@ public class ReflectionUtils { * https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces */ public static void bypassHiddenAPIReflectionRestrictions() { - if (!HIDDEN_API_REFLECTION_RESTRICTIONS_BYPASSED && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + boolean isBuildVersionOverP = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P; + + if (!HIDDEN_API_REFLECTION_RESTRICTIONS_BYPASSED && isBuildVersionOverP) { Logger.logDebug(LOG_TAG, "Bypassing android hidden api reflection restrictions"); HiddenApiBypass.addHiddenApiExemptions(""); HIDDEN_API_REFLECTION_RESTRICTIONS_BYPASSED = true; @@ -38,10 +40,6 @@ public static boolean areHiddenAPIReflectionRestrictionsBypassed() { return HIDDEN_API_REFLECTION_RESTRICTIONS_BYPASSED; } - - - - /** * Get a {@link Field} for the specified class. * @@ -61,16 +59,10 @@ public static Field getDeclaredField(@NonNull Class clazz, @NonNull String fi } } - - /** Class that represents result of invoking a field. */ - public static class FieldInvokeResult { - public boolean success; - public Object value; - + public static class FieldInvokeResult extends InvokeResult { FieldInvokeResult(boolean success, Object value) { - this.value = success; - this.value = value; + super(success, value); } } @@ -97,10 +89,6 @@ public static FieldInvokeResult invokeField(@NonNull Class claz } } - - - - /** * Wrapper for {@link #getDeclaredMethod(Class, String, Class[])} without parameters. */ @@ -129,8 +117,6 @@ public static Method getDeclaredMethod(@NonNull Class clazz, @NonNull String } } - - /** * Wrapper for {@link #invokeVoidMethod(Method, Object, Object...)} without arguments. */ @@ -158,16 +144,10 @@ public static boolean invokeVoidMethod(@NonNull Method method, Object obj, Objec } } - - /** Class that represents result of invoking a method that has a non-void return type. */ - public static class MethodInvokeResult { - public boolean success; - public Object value; - + public static class MethodInvokeResult extends InvokeResult{ MethodInvokeResult(boolean success, Object value) { - this.value = success; - this.value = value; + super(success, value); } } @@ -201,8 +181,6 @@ public static MethodInvokeResult invokeMethod(@NonNull Method method, Object obj } } - - /** * Wrapper for {@link #getConstructor(String, Class[])} without parameters. */ @@ -244,8 +222,6 @@ public static Constructor getConstructor(@NonNull Class clazz, Class... } } - - /** * Wrapper for {@link #invokeConstructor(Constructor, Object...)} without arguments. */ diff --git a/termux-shared/src/main/java/com/termux/shared/settings/preferences/SharedPreferenceUtils.java b/termux-shared/src/main/java/com/termux/shared/settings/preferences/SharedPreferenceUtils.java index 8f7a420ae8..619ff647d5 100644 --- a/termux-shared/src/main/java/com/termux/shared/settings/preferences/SharedPreferenceUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/settings/preferences/SharedPreferenceUtils.java @@ -12,6 +12,10 @@ public class SharedPreferenceUtils { private static final String LOG_TAG = "SharedPreferenceUtils"; + private static boolean isSharedPreferenceNull(SharedPreferences sharedPreferences) { + return sharedPreferences == null; + } + /** * Get {@link SharedPreferences} instance of the preferences file 'name' with the operating mode * {@link Context#MODE_PRIVATE}. This file will be created in the app package's default @@ -52,7 +56,7 @@ public static SharedPreferences getPrivateAndMultiProcessSharedPreferences(Conte * default if failed to read a valid value, like in case of an exception. */ public static boolean getBoolean(SharedPreferences sharedPreferences, String key, boolean def) { - if (sharedPreferences == null) { + if (isSharedPreferenceNull(sharedPreferences)) { Logger.logError(LOG_TAG, "Error getting boolean value for the \"" + key + "\" key from null shared preferences. Returning default value \"" + def + "\"."); return def; } @@ -78,7 +82,7 @@ public static boolean getBoolean(SharedPreferences sharedPreferences, String key */ @SuppressLint("ApplySharedPref") public static void setBoolean(SharedPreferences sharedPreferences, String key, boolean value, boolean commitToFile) { - if (sharedPreferences == null) { + if (isSharedPreferenceNull(sharedPreferences)) { Logger.logError(LOG_TAG, "Ignoring setting boolean value \"" + value + "\" for the \"" + key + "\" key into null shared preferences."); return; } @@ -102,7 +106,7 @@ public static void setBoolean(SharedPreferences sharedPreferences, String key, b * default if failed to read a valid value, like in case of an exception. */ public static float getFloat(SharedPreferences sharedPreferences, String key, float def) { - if (sharedPreferences == null) { + if (isSharedPreferenceNull(sharedPreferences)) { Logger.logError(LOG_TAG, "Error getting float value for the \"" + key + "\" key from null shared preferences. Returning default value \"" + def + "\"."); return def; } @@ -128,7 +132,7 @@ public static float getFloat(SharedPreferences sharedPreferences, String key, fl */ @SuppressLint("ApplySharedPref") public static void setFloat(SharedPreferences sharedPreferences, String key, float value, boolean commitToFile) { - if (sharedPreferences == null) { + if (isSharedPreferenceNull(sharedPreferences)) { Logger.logError(LOG_TAG, "Ignoring setting float value \"" + value + "\" for the \"" + key + "\" key into null shared preferences."); return; } @@ -151,7 +155,7 @@ public static void setFloat(SharedPreferences sharedPreferences, String key, flo * default if failed to read a valid value, like in case of an exception. */ public static int getInt(SharedPreferences sharedPreferences, String key, int def) { - if (sharedPreferences == null) { + if (isSharedPreferenceNull(sharedPreferences)) { Logger.logError(LOG_TAG, "Error getting int value for the \"" + key + "\" key from null shared preferences. Returning default value \"" + def + "\"."); return def; } @@ -177,7 +181,7 @@ public static int getInt(SharedPreferences sharedPreferences, String key, int de */ @SuppressLint("ApplySharedPref") public static void setInt(SharedPreferences sharedPreferences, String key, int value, boolean commitToFile) { - if (sharedPreferences == null) { + if (isSharedPreferenceNull(sharedPreferences)) { Logger.logError(LOG_TAG, "Ignoring setting int value \"" + value + "\" for the \"" + key + "\" key into null shared preferences."); return; } @@ -200,7 +204,7 @@ public static void setInt(SharedPreferences sharedPreferences, String key, int v * default if failed to read a valid value, like in case of an exception. */ public static long getLong(SharedPreferences sharedPreferences, String key, long def) { - if (sharedPreferences == null) { + if (isSharedPreferenceNull(sharedPreferences)) { Logger.logError(LOG_TAG, "Error getting long value for the \"" + key + "\" key from null shared preferences. Returning default value \"" + def + "\"."); return def; } @@ -226,7 +230,7 @@ public static long getLong(SharedPreferences sharedPreferences, String key, long */ @SuppressLint("ApplySharedPref") public static void setLong(SharedPreferences sharedPreferences, String key, long value, boolean commitToFile) { - if (sharedPreferences == null) { + if (isSharedPreferenceNull(sharedPreferences)) { Logger.logError(LOG_TAG, "Ignoring setting long value \"" + value + "\" for the \"" + key + "\" key into null shared preferences."); return; } @@ -250,7 +254,7 @@ public static void setLong(SharedPreferences sharedPreferences, String key, long * default if failed to read a valid value, like in case of an exception. */ public static String getString(SharedPreferences sharedPreferences, String key, String def, boolean defIfEmpty) { - if (sharedPreferences == null) { + if (isSharedPreferenceNull(sharedPreferences)) { Logger.logError(LOG_TAG, "Error getting String value for the \"" + key + "\" key from null shared preferences. Returning default value \"" + def + "\"."); return def; } @@ -280,7 +284,7 @@ public static String getString(SharedPreferences sharedPreferences, String key, */ @SuppressLint("ApplySharedPref") public static void setString(SharedPreferences sharedPreferences, String key, String value, boolean commitToFile) { - if (sharedPreferences == null) { + if (isSharedPreferenceNull(sharedPreferences)) { Logger.logError(LOG_TAG, "Ignoring setting String value \"" + value + "\" for the \"" + key + "\" key into null shared preferences."); return; } @@ -303,7 +307,7 @@ public static void setString(SharedPreferences sharedPreferences, String key, St * default if failed to read a valid value, like in case of an exception. */ public static Set getStringSet(SharedPreferences sharedPreferences, String key, Set def) { - if (sharedPreferences == null) { + if (isSharedPreferenceNull(sharedPreferences)) { Logger.logError(LOG_TAG, "Error getting Set value for the \"" + key + "\" key from null shared preferences. Returning default value \"" + def + "\"."); return def; } @@ -329,7 +333,7 @@ public static Set getStringSet(SharedPreferences sharedPreferences, Stri */ @SuppressLint("ApplySharedPref") public static void setStringSet(SharedPreferences sharedPreferences, String key, Set value, boolean commitToFile) { - if (sharedPreferences == null) { + if (isSharedPreferenceNull(sharedPreferences)) { Logger.logError(LOG_TAG, "Ignoring setting Set value \"" + value + "\" for the \"" + key + "\" key into null shared preferences."); return; } @@ -353,7 +357,7 @@ public static void setStringSet(SharedPreferences sharedPreferences, String key, * like in case of an exception. */ public static int getIntStoredAsString(SharedPreferences sharedPreferences, String key, int def) { - if (sharedPreferences == null) { + if (isSharedPreferenceNull(sharedPreferences)) { Logger.logError(LOG_TAG, "Error getting int value for the \"" + key + "\" key from null shared preferences. Returning default value \"" + def + "\"."); return def; } @@ -386,7 +390,7 @@ public static int getIntStoredAsString(SharedPreferences sharedPreferences, Stri */ @SuppressLint("ApplySharedPref") public static void setIntStoredAsString(SharedPreferences sharedPreferences, String key, int value, boolean commitToFile) { - if (sharedPreferences == null) { + if (isSharedPreferenceNull(sharedPreferences)) { Logger.logError(LOG_TAG, "Ignoring setting int value \"" + value + "\" for the \"" + key + "\" key into null shared preferences."); return; } diff --git a/termux-shared/src/main/java/com/termux/shared/shell/ShellUtils.java b/termux-shared/src/main/java/com/termux/shared/shell/ShellUtils.java index 302528b794..6d261a06c9 100644 --- a/termux-shared/src/main/java/com/termux/shared/shell/ShellUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/shell/ShellUtils.java @@ -10,6 +10,7 @@ public class ShellUtils { public static int getPid(Process p) { + int invalid = -1; try { Field f = p.getClass().getDeclaredField("pid"); f.setAccessible(true); @@ -19,7 +20,7 @@ public static int getPid(Process p) { f.setAccessible(false); } } catch (Throwable e) { - return -1; + return invalid; } } diff --git a/termux-shared/src/main/java/com/termux/shared/shell/command/result/ResultConfig.java b/termux-shared/src/main/java/com/termux/shared/shell/command/result/ResultConfig.java index e3ab5cafc9..d693ea237e 100644 --- a/termux-shared/src/main/java/com/termux/shared/shell/command/result/ResultConfig.java +++ b/termux-shared/src/main/java/com/termux/shared/shell/command/result/ResultConfig.java @@ -1,13 +1,17 @@ package com.termux.shared.shell.command.result; import android.app.PendingIntent; +import android.util.Pair; import androidx.annotation.NonNull; import com.termux.shared.logger.Logger; import com.termux.shared.markdown.MarkdownUtils; +import com.termux.shared.models.ReportInfo; +import java.util.ArrayList; import java.util.Formatter; +import java.util.List; public class ResultConfig { @@ -91,6 +95,25 @@ public static String getResultConfigLogString(final ResultConfig resultConfig, b return logString.toString(); } + + /** + * Get log variables {@link List < Pair >} for {@link ResultConfig}. + * + * @return Returns the result pending intent variables in list {@link List< Pair >}. + */ + private List> getResultPendingIntentVariableList() { + List> variableList = new ArrayList>() {{ + add(Pair.create("Result Bundle Key", resultBundleKey)); + add(Pair.create("Result Stdout Key", resultStdoutKey)); + add(Pair.create("Result Stderr Key", resultStderrKey)); + add(Pair.create("Result Exit Code Key", resultExitCodeKey)); + add(Pair.create("Result Err Code Key", resultErrCodeKey)); + add(Pair.create("Result Error Key", resultErrmsgKey)); + add(Pair.create("Result Stdout Original Length Key", resultStdoutOriginalLengthKey)); + add(Pair.create("Result Stderr Original Length Key", resultStderrOriginalLengthKey)); + }}; + return variableList; + } public String getResultPendingIntentVariablesLogString(boolean ignoreNull) { if (resultPendingIntent == null) return "Result PendingIntent Creator: -"; @@ -99,42 +122,51 @@ public String getResultPendingIntentVariablesLogString(boolean ignoreNull) { resultPendingIntentVariablesString.append("Result PendingIntent Creator: `").append(resultPendingIntent.getCreatorPackage()).append("`"); - if (!ignoreNull || resultBundleKey != null) - resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Bundle Key", resultBundleKey, "-")); - if (!ignoreNull || resultStdoutKey != null) - resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Stdout Key", resultStdoutKey, "-")); - if (!ignoreNull || resultStderrKey != null) - resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Stderr Key", resultStderrKey, "-")); - if (!ignoreNull || resultExitCodeKey != null) - resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Exit Code Key", resultExitCodeKey, "-")); - if (!ignoreNull || resultErrCodeKey != null) - resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Err Code Key", resultErrCodeKey, "-")); - if (!ignoreNull || resultErrmsgKey != null) - resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Error Key", resultErrmsgKey, "-")); - if (!ignoreNull || resultStdoutOriginalLengthKey != null) - resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Stdout Original Length Key", resultStdoutOriginalLengthKey, "-")); - if (!ignoreNull || resultStderrOriginalLengthKey != null) - resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Stderr Original Length Key", resultStderrOriginalLengthKey, "-")); + for (Pair logVar: getResultPendingIntentVariableList()) { + if (!ignoreNull || logVar.second != null) { + resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry(logVar.first, logVar.second, "-")); + } + } return resultPendingIntentVariablesString.toString(); } + /** + * Get log variables {@link List < Pair >} for {@link ResultConfig}. + * + * @return Returns the result directory variables in list {@link List< Pair >}. + */ + private static List> getResultDirectoryVariableList(final ResultConfig resultConfig) { + List> variableList = new ArrayList>() {{ + add(Pair.create("Result Directory Path", resultConfig.resultDirectoryPath)); + add(Pair.create("Result Single File", resultConfig.resultSingleFile)); + add(Pair.create("Result File Basename", resultConfig.resultFileBasename)); + add(Pair.create("Result File Output Format", resultConfig.resultFileOutputFormat)); + add(Pair.create("Result File Error Format", resultConfig.resultFileErrorFormat)); + add(Pair.create("Result Files Suffix", resultConfig.resultFilesSuffix)); + }}; + return variableList; + } + public String getResultDirectoryVariablesLogString(boolean ignoreNull) { if (resultDirectoryPath == null) return "Result Directory Path: -"; StringBuilder resultDirectoryVariablesString = new StringBuilder(); - resultDirectoryVariablesString.append(Logger.getSingleLineLogStringEntry("Result Directory Path", resultDirectoryPath, "-")); - - resultDirectoryVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Single File", resultSingleFile, "-")); - if (!ignoreNull || resultFileBasename != null) - resultDirectoryVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result File Basename", resultFileBasename, "-")); - if (!ignoreNull || resultFileOutputFormat != null) - resultDirectoryVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result File Output Format", resultFileOutputFormat, "-")); - if (!ignoreNull || resultFileErrorFormat != null) - resultDirectoryVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result File Error Format", resultFileErrorFormat, "-")); - if (!ignoreNull || resultFilesSuffix != null) - resultDirectoryVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Files Suffix", resultFilesSuffix, "-")); + for (Pair logVar: getResultDirectoryVariableList(this)) { + switch(logVar.first) { + case "Result Directory Path": + resultDirectoryVariablesString.append(Logger.getSingleLineLogStringEntry(logVar.first, logVar.second, "-")); + break; + case "Result Single File": + resultDirectoryVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry(logVar.first, logVar.second, "-")); + break; + default: + if (!ignoreNull || logVar.second != null) { + resultDirectoryVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry(logVar.first, logVar.second, "-")); + } + } + } return resultDirectoryVariablesString.toString(); } @@ -156,12 +188,9 @@ public static String getResultConfigMarkdownString(final ResultConfig resultConf markdownString.append("**Result PendingIntent Creator:** - "); if (resultConfig.resultDirectoryPath != null) { - markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result Directory Path", resultConfig.resultDirectoryPath, "-")); - markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result Single File", resultConfig.resultSingleFile, "-")); - markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result File Basename", resultConfig.resultFileBasename, "-")); - markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result File Output Format", resultConfig.resultFileOutputFormat, "-")); - markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result File Error Format", resultConfig.resultFileErrorFormat, "-")); - markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result Files Suffix", resultConfig.resultFilesSuffix, "-")); + for (Pair logVar: getResultDirectoryVariableList(resultConfig)) { + markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry(logVar.first, logVar.second, "-")); + } } return markdownString.toString(); diff --git a/termux-shared/src/main/java/com/termux/shared/shell/command/result/ResultData.java b/termux-shared/src/main/java/com/termux/shared/shell/command/result/ResultData.java index 3e42532089..44328e8920 100644 --- a/termux-shared/src/main/java/com/termux/shared/shell/command/result/ResultData.java +++ b/termux-shared/src/main/java/com/termux/shared/shell/command/result/ResultData.java @@ -15,6 +15,11 @@ public class ResultData implements Serializable { + private enum StringType { + Log, Markdown, Minimal + } + + /** The stdout of command. */ public final StringBuilder stdout = new StringBuilder(); /** The stderr of command. */ @@ -172,21 +177,34 @@ public String getExitCodeLogString() { return Logger.getSingleLineLogStringEntry("Exit Code", exitCode, "-"); } - public static String getErrorsListLogString(final ResultData resultData) { - if (resultData == null) return "null"; - - StringBuilder logString = new StringBuilder(); - - if (resultData.errorsList != null) { - for (Error error : resultData.errorsList) { - if (error.isStateFailed()) { - if (!logString.toString().isEmpty()) - logString.append("\n"); - logString.append(Error.getErrorLogString(error)); + private static String getErrorsListString(final ResultData resultData, StringType type) { + StringBuilder result = new StringBuilder(); + if (resultData.errorsList == null) return result.toString(); + + for (Error error : resultData.errorsList) { + if (error.isStateFailed()) { + if (!result.toString().isEmpty()) + result.append("\n"); + switch (type) { + case Log: + result.append(Error.getErrorLogString(error)); break; + case Markdown: + result.append(Error.getErrorMarkdownString(error)); break; + case Minimal: + result.append(Error.getMinimalErrorString(error)); break; + default: break; } } } + return result.toString(); + } + + public static String getErrorsListLogString(final ResultData resultData) { + if (resultData == null) return "null"; + + StringBuilder logString = new StringBuilder(); + logString.append(getErrorsListString(resultData, StringType.Log)); return logString.toString(); } @@ -223,17 +241,7 @@ public static String getErrorsListMarkdownString(final ResultData resultData) { if (resultData == null) return "null"; StringBuilder markdownString = new StringBuilder(); - - if (resultData.errorsList != null) { - for (Error error : resultData.errorsList) { - if (error.isStateFailed()) { - if (!markdownString.toString().isEmpty()) - markdownString.append("\n"); - markdownString.append(Error.getErrorMarkdownString(error)); - } - } - } - + markdownString.append(getErrorsListString(resultData, StringType.Markdown)); return markdownString.toString(); } @@ -241,17 +249,7 @@ public static String getErrorsListMinimalString(final ResultData resultData) { if (resultData == null) return "null"; StringBuilder minimalString = new StringBuilder(); - - if (resultData.errorsList != null) { - for (Error error : resultData.errorsList) { - if (error.isStateFailed()) { - if (!minimalString.toString().isEmpty()) - minimalString.append("\n"); - minimalString.append(Error.getMinimalErrorString(error)); - } - } - } - + minimalString.append(getErrorsListString(resultData, StringType.Minimal)); return minimalString.toString(); } diff --git a/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java b/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java index 3a33eb52ea..30741cefb5 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java +++ b/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java @@ -668,6 +668,29 @@ public final class TermuxConstants { /** Termux and plugin apps directory */ public static final File TERMUX_APPS_DIR = new File(TERMUX_APPS_DIR_PATH); + public static StringBuilder buildScript(String filesDir) { + StringBuilder statScript = new StringBuilder(); + statScript + .append("echo 'ls info:'\n") + .append("/system/bin/ls -lhdZ") + .append(" '/data/data'") + .append(" '/data/user/0'") + .append(" '" + TermuxConstants.TERMUX_INTERNAL_PRIVATE_APP_DATA_DIR_PATH + "'") + .append(" '/data/user/0/" + TermuxConstants.TERMUX_PACKAGE_NAME + "'") + .append(" '" + TermuxConstants.TERMUX_FILES_DIR_PATH + "'") + .append(" '" + filesDir + "'") + .append(" '/data/user/0/" + TermuxConstants.TERMUX_PACKAGE_NAME + "/files'") + .append(" '/data/user/" + TermuxConstants.TERMUX_PACKAGE_NAME + "/files'") + .append(" '" + TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH + "'") + .append(" '" + TermuxConstants.TERMUX_PREFIX_DIR_PATH + "'") + .append(" '" + TermuxConstants.TERMUX_HOME_DIR_PATH + "'") + .append(" '" + TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/login'") + .append(" 2>&1") + .append("\necho; echo 'mount info:'\n") + .append("/system/bin/grep -E '( /data )|( /data/data )|( /data/user/[0-9]+ )' /proc/self/mountinfo 2>&1 | /system/bin/grep -v '/data_mirror' 2>&1"); + return statScript; + } + /* diff --git a/termux-shared/src/main/java/com/termux/shared/termux/crash/TermuxCrashUtils.java b/termux-shared/src/main/java/com/termux/shared/termux/crash/TermuxCrashUtils.java index 5d551a9715..9e3a3a5522 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/crash/TermuxCrashUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/termux/crash/TermuxCrashUtils.java @@ -36,7 +36,7 @@ public class TermuxCrashUtils implements CrashHandler.CrashHandlerClient { public enum TYPE { UNCAUGHT_EXCEPTION, - CAUGHT_EXCEPTION; + CAUGHT_EXCEPTION } private final TYPE mType; @@ -101,7 +101,9 @@ public void onPostLogCrash(final Context currentPackageContext, Thread thread, T } // If an uncaught exception, then do not notify since the termux app itself would be crashing - if (TYPE.UNCAUGHT_EXCEPTION.equals(mType) && TermuxConstants.TERMUX_PACKAGE_NAME.equals(currentPackageName)) + boolean isUncaughtException = TYPE.UNCAUGHT_EXCEPTION.equals(mType); + boolean isTermuxPackageNameSame = TermuxConstants.TERMUX_PACKAGE_NAME.equals(currentPackageName); + if (isUncaughtException && isTermuxPackageNameSame) return; String message = TERMUX_APP.TERMUX_ACTIVITY_NAME + " that \"" + currentPackageName + "\" app crashed"; @@ -199,27 +201,6 @@ private static synchronized void notifyAppCrashFromCrashLogFileInner(final Conte sendCrashReportNotification(context, logTag, null, null, reportString, false, false, null, false); } - - - - /** - * Send a crash report notification for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID} - * and {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}. - * - * @param currentPackageContext The {@link Context} of current package. - * @param logTag The log tag to use for logging. - * @param title The title for the crash report and notification. - * @param message The message for the crash report. - * @param throwable The {@link Throwable} for the crash report. - */ - public static void sendCrashReportNotification(final Context currentPackageContext, String logTag, - CharSequence title, String message, Throwable throwable) { - sendCrashReportNotification(currentPackageContext, logTag, - title, message, - MarkdownUtils.getMarkdownCodeForString(Logger.getMessageAndStackTraceString(message, throwable), true), - false, false, true); - } - /** * Send a crash report notification for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID} * and {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}. @@ -229,55 +210,11 @@ public static void sendCrashReportNotification(final Context currentPackageConte * @param title The title for the crash report and notification. * @param notificationTextString The text of the notification. * @param message The message for the crash report. - */ - public static void sendCrashReportNotification(final Context currentPackageContext, String logTag, - CharSequence title, String notificationTextString, - String message) { - sendCrashReportNotification(currentPackageContext, logTag, - title, notificationTextString, message, - false, false, true); - } - - /** - * Send a crash report notification for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID} - * and {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}. - * - * @param currentPackageContext The {@link Context} of current package. - * @param logTag The log tag to use for logging. - * @param title The title for the crash report and notification. - * @param notificationTextString The text of the notification. - * @param message The message for the crash report. - * @param forceNotification If set to {@code true}, then a notification will be shown + * @param isForceNotificationOn If set to {@code true}, then a notification will be shown * regardless of if pending intent is {@code null} or * {@link TermuxPreferenceConstants.TERMUX_APP#KEY_CRASH_REPORT_NOTIFICATIONS_ENABLED} * is {@code false}. - * @param showToast If set to {@code true}, then a toast will be shown for {@code notificationTextString}. - * @param addDeviceInfo If set to {@code true}, then device info should be appended to the message. - */ - public static void sendCrashReportNotification(final Context currentPackageContext, String logTag, - CharSequence title, String notificationTextString, - String message, boolean forceNotification, - boolean showToast, - boolean addDeviceInfo) { - sendCrashReportNotification(currentPackageContext, logTag, - title, notificationTextString, "## " + title + "\n\n" + message + "\n\n", - forceNotification, showToast, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGE, addDeviceInfo); - } - - /** - * Send a crash report notification for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID} - * and {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}. - * - * @param currentPackageContext The {@link Context} of current package. - * @param logTag The log tag to use for logging. - * @param title The title for the crash report and notification. - * @param notificationTextString The text of the notification. - * @param message The message for the crash report. - * @param forceNotification If set to {@code true}, then a notification will be shown - * regardless of if pending intent is {@code null} or - * {@link TermuxPreferenceConstants.TERMUX_APP#KEY_CRASH_REPORT_NOTIFICATIONS_ENABLED} - * is {@code false}. - * @param showToast If set to {@code true}, then a toast will be shown for {@code notificationTextString}. + * @param isShowToast If set to {@code true}, then a toast will be shown for {@code notificationTextString}. * @param appInfoMode The {@link TermuxUtils.AppInfoMode} to use to add app info to the message. * Set to {@code null} if app info should not be appended to the message. * @param addDeviceInfo If set to {@code true}, then device info should be appended to the message. @@ -285,8 +222,8 @@ public static void sendCrashReportNotification(final Context currentPackageConte public static void sendCrashReportNotification(final Context currentPackageContext, String logTag, CharSequence title, String notificationTextString, - String message, boolean forceNotification, - boolean showToast, + String message, boolean isForceNotificationOn, + boolean isShowToast, TermuxUtils.AppInfoMode appInfoMode, boolean addDeviceInfo) { // Note: Do not change currentPackageContext or termuxPackageContext passed to functions or things will break @@ -304,17 +241,19 @@ public static void sendCrashReportNotification(final Context currentPackageConte if (preferences == null) return; // If user has disabled notifications for crashes - if (!preferences.areCrashReportNotificationsEnabled(true) && !forceNotification) + boolean isNotificationEnable = preferences.areCrashReportNotificationsEnabled(true); + if (!isNotificationEnable && !isForceNotificationOn) return; logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG); - if (showToast) + if (isShowToast) Logger.showToast(currentPackageContext, notificationTextString, true); // Send a notification to show the crash log which when clicked will open the {@link ReportActivity} // to show the details of the crash - if (title == null || title.toString().isEmpty()) + boolean isTitleNull = title == null; + if (isTitleNull || title.toString().isEmpty()) title = TermuxConstants.TERMUX_APP_NAME + " Crash Report"; Logger.logDebug(logTag, "Sending \"" + title + "\" notification."); @@ -329,13 +268,7 @@ public static void sendCrashReportNotification(final Context currentPackageConte String userActionName = UserAction.CRASH_REPORT.getName(); - ReportInfo reportInfo = new ReportInfo(userActionName, logTag, title.toString()); - reportInfo.setReportString(reportString.toString()); - reportInfo.setReportStringSuffix("\n\n" + TermuxUtils.getReportIssueMarkdownString(currentPackageContext)); - reportInfo.setAddReportInfoHeaderToMarkdown(true); - reportInfo.setReportSaveFileLabelAndPath(userActionName, - Environment.getExternalStorageDirectory() + "/" + - FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true)); + ReportInfo reportInfo = makeNewReportInfo(userActionName, logTag, title.toString(), reportString.toString(), currentPackageContext); ReportActivity.NewInstanceResult result = ReportActivity.newInstance(termuxPackageContext, reportInfo); if (result.contentIntent == null) return; @@ -368,6 +301,18 @@ public static void sendCrashReportNotification(final Context currentPackageConte notificationManager.notify(nextNotificationId, builder.build()); } + private static ReportInfo makeNewReportInfo(String userActionName, String logTag, String title, String reportString, Context currentPackageContext) { + ReportInfo reportInfo = new ReportInfo(userActionName, logTag, title); + reportInfo.setReportString(reportString); + reportInfo.setReportStringSuffix("\n\n" + TermuxUtils.getReportIssueMarkdownString(currentPackageContext)); + reportInfo.setAddReportInfoHeaderToMarkdown(true); + reportInfo.setReportSaveFileLabelAndPath(userActionName, + Environment.getExternalStorageDirectory() + "/" + + FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true)); + + return reportInfo; + } + /** * Get {@link Notification.Builder} for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID} * and {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}. diff --git a/termux-shared/src/main/java/com/termux/shared/termux/extrakeys/ExtraKeysInfo.java b/termux-shared/src/main/java/com/termux/shared/termux/extrakeys/ExtraKeysInfo.java index 9140c13edd..05aaecff23 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/extrakeys/ExtraKeysInfo.java +++ b/termux-shared/src/main/java/com/termux/shared/termux/extrakeys/ExtraKeysInfo.java @@ -151,18 +151,18 @@ private ExtraKeyButton[][] initExtraKeysInfo(@NonNull String propertiesInfo, for (int j = 0; j < matrix[i].length; j++) { Object key = matrix[i][j]; - JSONObject jobject = normalizeKeyConfig(key); - + JSONObject jsonObject = normalizeKeyConfig(key); + boolean hasKeyPopup = jsonObject.has(ExtraKeyButton.KEY_POPUP); ExtraKeyButton button; - if (!jobject.has(ExtraKeyButton.KEY_POPUP)) { + if (!hasKeyPopup) { // no popup - button = new ExtraKeyButton(jobject, extraKeyDisplayMap, extraKeyAliasMap); + button = new ExtraKeyButton(jsonObject, extraKeyDisplayMap, extraKeyAliasMap); } else { // a popup - JSONObject popupJobject = normalizeKeyConfig(jobject.get(ExtraKeyButton.KEY_POPUP)); - ExtraKeyButton popup = new ExtraKeyButton(popupJobject, extraKeyDisplayMap, extraKeyAliasMap); - button = new ExtraKeyButton(jobject, popup, extraKeyDisplayMap, extraKeyAliasMap); + JSONObject popupJSONObject = normalizeKeyConfig(jsonObject.get(ExtraKeyButton.KEY_POPUP)); + ExtraKeyButton popup = new ExtraKeyButton(popupJSONObject, extraKeyDisplayMap, extraKeyAliasMap); + button = new ExtraKeyButton(jsonObject, popup, extraKeyDisplayMap, extraKeyAliasMap); } buttons[i][j] = button; @@ -177,16 +177,16 @@ private ExtraKeyButton[][] initExtraKeysInfo(@NonNull String propertiesInfo, * {@link ExtraKeyButton#ExtraKeyButton(JSONObject, ExtraKeyButton, ExtraKeysConstants.ExtraKeyDisplayMap, ExtraKeysConstants.ExtraKeyDisplayMap)}. */ private static JSONObject normalizeKeyConfig(Object key) throws JSONException { - JSONObject jobject; + JSONObject jsonObject; if (key instanceof String) { - jobject = new JSONObject(); - jobject.put(ExtraKeyButton.KEY_KEY_NAME, key); + jsonObject = new JSONObject(); + jsonObject.put(ExtraKeyButton.KEY_KEY_NAME, key); } else if (key instanceof JSONObject) { - jobject = (JSONObject) key; + jsonObject = (JSONObject) key; } else { throw new JSONException("An key in the extra-key matrix must be a string or an object"); } - return jobject; + return jsonObject; } public ExtraKeyButton[][] getMatrix() { diff --git a/termux-shared/src/main/java/com/termux/shared/termux/file/TermuxFileUtils.java b/termux-shared/src/main/java/com/termux/shared/termux/file/TermuxFileUtils.java index 68c547dc25..d143a647e9 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/file/TermuxFileUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/termux/file/TermuxFileUtils.java @@ -26,6 +26,10 @@ public class TermuxFileUtils { private static final String LOG_TAG = "TermuxFileUtils"; + private static boolean isStringHasValue(String str) { + return str != null && !str.isEmpty(); + } + /** * Replace "$PREFIX/" or "~/" prefix with termux absolute paths. * @@ -50,7 +54,7 @@ public static List getExpandedTermuxPaths(List paths) { * @return Returns the {@code expand path}. */ public static String getExpandedTermuxPath(String path) { - if (path != null && !path.isEmpty()) { + if (isStringHasValue(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); @@ -84,7 +88,7 @@ public static List getUnExpandedTermuxPaths(List paths) { * @return Returns the {@code unexpand path}. */ public static String getUnExpandedTermuxPath(String path) { - if (path != null && !path.isEmpty()) { + if (isStringHasValue(path)) { path = path.replaceAll("^" + Pattern.quote(TermuxConstants.TERMUX_PREFIX_DIR_PATH) + "/", "\\$PREFIX/"); path = path.replaceAll("^" + Pattern.quote(TermuxConstants.TERMUX_HOME_DIR_PATH) + "/", "~/"); } @@ -123,7 +127,7 @@ public static String getCanonicalPath(String path, final String prefixForNonAbso * @return Returns the allowed path if it {@code path} is under it, otherwise {@link TermuxConstants#TERMUX_FILES_DIR_PATH}. */ public static String getMatchedAllowedTermuxWorkingDirectoryParentPathForPath(String path) { - if (path == null || path.isEmpty()) return TermuxConstants.TERMUX_FILES_DIR_PATH; + if (!isStringHasValue(path)) return TermuxConstants.TERMUX_FILES_DIR_PATH; if (path.startsWith(TermuxConstants.TERMUX_STORAGE_HOME_DIR_PATH + "/")) { return TermuxConstants.TERMUX_STORAGE_HOME_DIR_PATH; @@ -324,7 +328,6 @@ public static Error isAppsTermuxAppDirectoryAccessible(boolean createDirectoryIf FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS, setMissingPermissions, true, false, false); } - /** * Get a markdown {@link String} for stat output for various Termux app files paths. * @@ -339,27 +342,22 @@ public static String getTermuxFilesStatMarkdownString(@NonNull final Context con String filesDir = termuxPackageContext.getFilesDir().getAbsolutePath(); // Build script - StringBuilder statScript = new StringBuilder(); - statScript - .append("echo 'ls info:'\n") - .append("/system/bin/ls -lhdZ") - .append(" '/data/data'") - .append(" '/data/user/0'") - .append(" '" + TermuxConstants.TERMUX_INTERNAL_PRIVATE_APP_DATA_DIR_PATH + "'") - .append(" '/data/user/0/" + TermuxConstants.TERMUX_PACKAGE_NAME + "'") - .append(" '" + TermuxConstants.TERMUX_FILES_DIR_PATH + "'") - .append(" '" + filesDir + "'") - .append(" '/data/user/0/" + TermuxConstants.TERMUX_PACKAGE_NAME + "/files'") - .append(" '/data/user/" + TermuxConstants.TERMUX_PACKAGE_NAME + "/files'") - .append(" '" + TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH + "'") - .append(" '" + TermuxConstants.TERMUX_PREFIX_DIR_PATH + "'") - .append(" '" + TermuxConstants.TERMUX_HOME_DIR_PATH + "'") - .append(" '" + TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/login'") - .append(" 2>&1") - .append("\necho; echo 'mount info:'\n") - .append("/system/bin/grep -E '( /data )|( /data/data )|( /data/user/[0-9]+ )' /proc/self/mountinfo 2>&1 | /system/bin/grep -v '/data_mirror' 2>&1"); + StringBuilder statScript = TermuxConstants.buildScript(filesDir); // Run script + ExecutionCommand executionCommand = runningScript(statScript, context); + if(executionCommand == null) return null; + + // Build script output + StringBuilder statOutput = buildOutputScript(statScript, executionCommand); + + // Build markdown output + StringBuilder markdownString = buildOutputMarkdown(filesDir ,statOutput); + + return markdownString.toString(); + } + + private static ExecutionCommand runningScript(StringBuilder statScript, Context context) { ExecutionCommand executionCommand = new ExecutionCommand(1, "/system/bin/sh", null, statScript.toString() + "\n", "/", ExecutionCommand.Runner.APP_SHELL.getName(), true); executionCommand.commandLabel = TermuxConstants.TERMUX_APP_NAME + " Files Stat Command"; @@ -370,7 +368,10 @@ public static String getTermuxFilesStatMarkdownString(@NonNull final Context con return null; } - // Build script output + return executionCommand; + } + + private static StringBuilder buildOutputScript(StringBuilder statScript, ExecutionCommand executionCommand) { StringBuilder statOutput = new StringBuilder(); statOutput.append("$ ").append(statScript.toString()); statOutput.append("\n\n").append(executionCommand.resultData.stdout.toString()); @@ -383,7 +384,10 @@ public static String getTermuxFilesStatMarkdownString(@NonNull final Context con statOutput.append("\n").append("exit code: ").append(executionCommand.resultData.exitCode.toString()); } - // Build markdown output + return statOutput; + } + + private static StringBuilder buildOutputMarkdown(String filesDir, StringBuilder statOutput) { StringBuilder markdownString = new StringBuilder(); markdownString.append("## ").append(TermuxConstants.TERMUX_APP_NAME).append(" Files Info\n\n"); AndroidUtils.appendPropertyToMarkdown(markdownString,"TERMUX_REQUIRED_FILES_DIR_PATH ($PREFIX)", TermuxConstants.TERMUX_FILES_DIR_PATH); @@ -391,7 +395,7 @@ public static String getTermuxFilesStatMarkdownString(@NonNull final Context con markdownString.append("\n\n").append(MarkdownUtils.getMarkdownCodeForString(statOutput.toString(), true)); markdownString.append("\n##\n"); - return markdownString.toString(); + return markdownString; } } diff --git a/termux-shared/src/main/java/com/termux/shared/termux/interact/TextInputDialogUtils.java b/termux-shared/src/main/java/com/termux/shared/termux/interact/TextInputDialogUtils.java index d644af7260..21cf7ae767 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/interact/TextInputDialogUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/termux/interact/TextInputDialogUtils.java @@ -12,6 +12,9 @@ public final class TextInputDialogUtils { + public static final int PADDING_PER_DIP_AT_TOPANDSIDES = 16; + public static final int PADDING_PER_DIP_AT_BOTTOM = 24; + public interface TextSetListener { void onTextSet(String text); } @@ -38,8 +41,8 @@ public static void textInput(Activity activity, int titleText, String initialTex 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); + int paddingTopAndSides = Math.round(PADDING_PER_DIP_AT_TOPANDSIDES * dipInPixels); + int paddingBottom = Math.round(PADDING_PER_DIP_AT_BOTTOM * dipInPixels); LinearLayout layout = new LinearLayout(activity); layout.setOrientation(LinearLayout.VERTICAL); diff --git a/termux-shared/src/main/java/com/termux/shared/termux/notification/TermuxNotificationUtils.java b/termux-shared/src/main/java/com/termux/shared/termux/notification/TermuxNotificationUtils.java index eb0b50626d..4dca3dfe94 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/notification/TermuxNotificationUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/termux/notification/TermuxNotificationUtils.java @@ -25,11 +25,21 @@ public synchronized static int getNextNotificationId(final Context context) { int lastNotificationId = preferences.getLastNotificationId(); int nextNotificationId = lastNotificationId + 1; - while(nextNotificationId == TermuxConstants.TERMUX_APP_NOTIFICATION_ID || nextNotificationId == TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_ID) { + + boolean isAppNotificationIdSame = nextNotificationId == TermuxConstants.TERMUX_APP_NOTIFICATION_ID; + boolean isCommandNotificationIdSame = nextNotificationId == TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_ID; + + while( isAppNotificationIdSame || isCommandNotificationIdSame) { nextNotificationId++; + + isAppNotificationIdSame = nextNotificationId == TermuxConstants.TERMUX_APP_NOTIFICATION_ID; + isCommandNotificationIdSame = nextNotificationId == TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_ID; } - if (nextNotificationId == Integer.MAX_VALUE || nextNotificationId < 0) + boolean isNotificationIdMaxValue = nextNotificationId == Integer.MAX_VALUE; + boolean isNotificationIdUnderZero = nextNotificationId < 0; + + if (isNotificationIdMaxValue || isNotificationIdUnderZero) nextNotificationId = TermuxPreferenceConstants.TERMUX_APP.DEFAULT_VALUE_KEY_LAST_NOTIFICATION_ID; preferences.setLastNotificationId(nextNotificationId); diff --git a/termux-shared/src/main/java/com/termux/shared/termux/plugins/TermuxPluginUtils.java b/termux-shared/src/main/java/com/termux/shared/termux/plugins/TermuxPluginUtils.java index ed2d9a2016..c4fe31e140 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/plugins/TermuxPluginUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/termux/plugins/TermuxPluginUtils.java @@ -207,27 +207,6 @@ public static void setPluginResultDirectoryVariables(ExecutionCommand executionC resultConfig.resultFileBasename = ShellUtils.getExecutableBasename(executionCommand.executable) + "-" + AndroidUtils.getCurrentMilliSecondLocalTimeStamp() + ".log"; } - - - - /** - * Send a plugin error report notification for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID} - * and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}. - * - * @param currentPackageContext The {@link Context} of current package. - * @param logTag The log tag to use for logging. - * @param title The title for the error report and notification. - * @param message The message for the error report. - * @param throwable The {@link Throwable} for the error report. - */ - public static void sendPluginCommandErrorNotification(final Context currentPackageContext, String logTag, - CharSequence title, String message, Throwable throwable) { - sendPluginCommandErrorNotification(currentPackageContext, logTag, - title, message, - MarkdownUtils.getMarkdownCodeForString(Logger.getMessageAndStackTraceString(message, throwable), true), - false, false, true); - } - /** * Send a plugin error report notification for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID} * and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}. @@ -241,35 +220,10 @@ public static void sendPluginCommandErrorNotification(final Context currentPacka public static void sendPluginCommandErrorNotification(final Context currentPackageContext, String logTag, CharSequence title, String notificationTextString, String message) { - sendPluginCommandErrorNotification(currentPackageContext, logTag, - title, notificationTextString, message, - false, false, true); - } - /** - * Send a plugin error report notification for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID} - * and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}. - * - * @param currentPackageContext The {@link Context} of current package. - * @param logTag The log tag to use for logging. - * @param title The title for the error report and notification. - * @param notificationTextString The text of the notification. - * @param message The message for the error report. - * @param forceNotification If set to {@code true}, then a notification will be shown - * regardless of if pending intent is {@code null} or - * {@link TermuxPreferenceConstants.TERMUX_APP#KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED} - * is {@code false}. - * @param showToast If set to {@code true}, then a toast will be shown for {@code notificationTextString}. - * @param addDeviceInfo If set to {@code true}, then device info should be appended to the message. - */ - public static void sendPluginCommandErrorNotification(final Context currentPackageContext, String logTag, - CharSequence title, String notificationTextString, - String message, boolean forceNotification, - boolean showToast, - boolean addDeviceInfo) { sendPluginCommandErrorNotification(currentPackageContext, logTag, title, notificationTextString, "## " + title + "\n\n" + message + "\n\n", - forceNotification, showToast, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGE, addDeviceInfo, null); + false, false, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGE, true, null); } /** diff --git a/termux-shared/src/main/java/com/termux/shared/termux/shell/am/TermuxAmSocketServer.java b/termux-shared/src/main/java/com/termux/shared/termux/shell/am/TermuxAmSocketServer.java index 32116b3083..c72857a694 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/shell/am/TermuxAmSocketServer.java +++ b/termux-shared/src/main/java/com/termux/shared/termux/shell/am/TermuxAmSocketServer.java @@ -14,6 +14,7 @@ import com.termux.shared.net.socket.local.LocalSocketRunConfig; import com.termux.shared.shell.am.AmSocketServer; import com.termux.shared.termux.TermuxConstants; +import com.termux.shared.termux.TermuxUtils; import com.termux.shared.termux.crash.TermuxCrashUtils; import com.termux.shared.termux.plugins.TermuxPluginUtils; import com.termux.shared.termux.settings.properties.TermuxAppSharedProperties; @@ -153,9 +154,13 @@ public static synchronized LocalSocketManager getTermuxAmSocketServer() { public static synchronized void showErrorNotification(@NonNull Context context, @NonNull Error error, @NonNull LocalSocketRunConfig localSocketRunConfig, @Nullable LocalClientSocket clientSocket) { - TermuxPluginUtils.sendPluginCommandErrorNotification(context, LOG_TAG, - localSocketRunConfig.getTitle() + " Socket Server Error", error.getMinimalErrorString(), - LocalSocketManager.getErrorMarkdownString(error, localSocketRunConfig, clientSocket)); + + CharSequence title = localSocketRunConfig.getTitle() + " Socket Server Error"; + String notificationTextString = error.getMinimalErrorString(); + String message = "## " + title + "\n\n" + LocalSocketManager.getErrorMarkdownString(error, localSocketRunConfig, clientSocket) + "\n\n"; + + TermuxPluginUtils.sendPluginCommandErrorNotification(context, LOG_TAG, title, notificationTextString, message, + false, false, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGE, true, null); } diff --git a/termux-shared/src/main/java/com/termux/shared/theme/ThemeUtils.java b/termux-shared/src/main/java/com/termux/shared/theme/ThemeUtils.java index 2b652026fb..90a7508b6e 100644 --- a/termux-shared/src/main/java/com/termux/shared/theme/ThemeUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/theme/ThemeUtils.java @@ -27,43 +27,37 @@ public static boolean isNightModeEnabled(Context context) { /** Will return true if mode is set to {@link NightMode#TRUE}, otherwise will return true if * mode is set to {@link NightMode#SYSTEM} and night mode is enabled by system. */ public static boolean shouldEnableDarkTheme(Context context, String name) { - if (NightMode.TRUE.getName().equals(name)) + boolean isNightMode = (NightMode.TRUE.getName().equals(name)); +// boolean isDarkMode = (NightMode.FALSE.getName().equals(name)); + boolean isSystemMode = (NightMode.SYSTEM.getName().equals(name)); + + if(isNightMode) return true; - else if (NightMode.FALSE.getName().equals(name)) - return false; - else if (NightMode.SYSTEM.getName().equals(name)) { + else if(isSystemMode) return isNightModeEnabled(context); - } else { + else { return false; } } - /** Get {@link #ATTR_TEXT_COLOR_PRIMARY} value being used by current theme. */ public static int getTextColorPrimary(Context context) { - return getSystemAttrColor(context, ATTR_TEXT_COLOR_PRIMARY); + return getSystemAttrColor(context, ATTR_TEXT_COLOR_PRIMARY, 0); } /** Get {@link #ATTR_TEXT_COLOR_SECONDARY} value being used by current theme. */ public static int getTextColorSecondary(Context context) { - return getSystemAttrColor(context, ATTR_TEXT_COLOR_SECONDARY); + return getSystemAttrColor(context, ATTR_TEXT_COLOR_SECONDARY, 0); } /** Get {@link #ATTR_TEXT_COLOR} value being used by current theme. */ public static int getTextColor(Context context) { - return getSystemAttrColor(context, ATTR_TEXT_COLOR); + return getSystemAttrColor(context, ATTR_TEXT_COLOR, 0); } /** Get {@link #ATTR_TEXT_COLOR_LINK} value being used by current theme. */ public static int getTextColorLink(Context context) { - return getSystemAttrColor(context, ATTR_TEXT_COLOR_LINK); - } - - - - /** Wrapper for {@link #getSystemAttrColor(Context, int, int)} with {@code def} value {@code 0}. */ - public static int getSystemAttrColor(Context context, int attr) { - return getSystemAttrColor(context, attr, 0); + return getSystemAttrColor(context, ATTR_TEXT_COLOR_LINK, 0); } /** diff --git a/termux-shared/src/main/java/com/termux/shared/view/ViewUtils.java b/termux-shared/src/main/java/com/termux/shared/view/ViewUtils.java index e0d87345c4..b5f27464e2 100644 --- a/termux-shared/src/main/java/com/termux/shared/view/ViewUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/view/ViewUtils.java @@ -110,23 +110,24 @@ public static Rect[] getWindowAndViewRects(View view, int statusBarHeight) { ", displayOrientation=" + displayOrientation); } + boolean isStatusBarHeightTop = (statusBarHeight == windowRect.top); if (isInMultiWindowMode) { - if (displayOrientation == Configuration.ORIENTATION_PORTRAIT) { - // The windowRect.top of the window at the of split screen mode should start right - // below the status bar - if (statusBarHeight != windowRect.top) { - if (view_utils_logging_enabled) - Logger.logVerbose(LOG_TAG, "Window top does not equal statusBarHeight " + statusBarHeight + " in multi-window portrait mode. Window is possibly bottom app in split screen mode. Adding windowRect.top " + windowRect.top + " to viewTop."); - viewTop += windowRect.top; - } else { - if (view_utils_logging_enabled) - Logger.logVerbose(LOG_TAG, "windowRect.top equals statusBarHeight " + statusBarHeight + " in multi-window portrait mode. Window is possibly top app in split screen mode."); - } - - } else if (displayOrientation == Configuration.ORIENTATION_LANDSCAPE) { - // If window is on the right in landscape mode of split screen, the viewLeft actually - // starts at windowRect.left instead of 0 returned by getLocationInWindow - viewLeft += windowRect.left; + switch (displayOrientation) { + case Configuration.ORIENTATION_PORTRAIT: + // The windowRect.top of the window at the of split screen mode should start right + // below the status bar + if (!isStatusBarHeightTop) { + if (view_utils_logging_enabled) + Logger.logVerbose(LOG_TAG, "Window top does not equal statusBarHeight " + statusBarHeight + " in multi-window portrait mode. Window is possibly bottom app in split screen mode. Adding windowRect.top " + windowRect.top + " to viewTop."); + viewTop += windowRect.top; + } else { + if (view_utils_logging_enabled) + Logger.logVerbose(LOG_TAG, "windowRect.top equals statusBarHeight " + statusBarHeight + " in multi-window portrait mode. Window is possibly top app in split screen mode."); + } + break; + case Configuration.ORIENTATION_LANDSCAPE: + viewLeft += windowRect.left; + break; } } @@ -152,9 +153,14 @@ public static Rect[] getWindowAndViewRects(View view, int statusBarHeight) { */ public static boolean isRectAbove(@NonNull Rect r1, @NonNull Rect r2) { // check for empty first - return r1.left < r1.right && r1.top < r1.bottom - // now check if above - && r1.left <= r2.left && r1.bottom >= r2.bottom; + + // now check if above + boolean isR1RightAboveLeft = (r1.left < r1.right); + boolean isR1BottomAboveTop = (r1.top < r1.bottom); + boolean isR2LeftAboveR1Left = (r1.left <= r2.left); + boolean isR1BottomAboveR2Bottom = (r1.bottom >= r2.bottom); + + return isR1RightAboveLeft && isR1BottomAboveTop && isR2LeftAboveR1Left && isR1BottomAboveR2Bottom; } /**