Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bde9d01f76 | ||
|
|
5a511a2ba3 | ||
|
|
5c50964b1f | ||
|
|
dea8c9879e | ||
|
|
2034121798 | ||
|
|
23a900c433 | ||
|
|
93a7525d9b | ||
|
|
5670128236 | ||
|
|
dfd32435af | ||
|
|
49265160f8 | ||
|
|
70e1accafe | ||
|
|
1c7f9166f2 | ||
|
|
553913cde1 | ||
|
|
6bca378cec | ||
|
|
12f910c32d | ||
|
|
94c5f3674a | ||
|
|
28b9f93d13 | ||
|
|
69bebb5916 | ||
|
|
321350256e | ||
|
|
e5a9b99afe | ||
|
|
00f805f7ec | ||
|
|
d3c34ad1f5 | ||
|
|
59877a08d1 | ||
|
|
9c92251595 | ||
|
|
e408fdcc08 | ||
|
|
53c1a49b5b | ||
|
|
2aafcf8435 | ||
|
|
1c1af34374 | ||
|
|
52f18a73fb | ||
|
|
28f81f2cc7 | ||
|
|
4494bc66e4 | ||
|
|
679e0de044 | ||
|
|
80b495e50b | ||
|
|
69e5deedc7 | ||
|
|
7f36d7bbd0 | ||
|
|
b7b12ebe84 | ||
|
|
f77c88633e | ||
|
|
5f2ccca423 | ||
|
|
f0f6927273 | ||
|
|
0fb18c0c8b | ||
|
|
4dfed3320e | ||
|
|
7ac62c9840 | ||
|
|
fd80cdaf23 | ||
|
|
19c690d02b | ||
|
|
e119d34bca | ||
|
|
f545ebf0bd | ||
|
|
0b4bbaf23d | ||
|
|
e7dd0eeebe | ||
|
|
7ef9255437 | ||
|
|
7225e2b379 | ||
|
|
1ad038ece5 | ||
|
|
cb8b0225ca | ||
|
|
7620800cd5 | ||
|
|
6837db0015 | ||
|
|
e08e3b536e |
26
.github/workflows/publish_libraries.yml
vendored
26
.github/workflows/publish_libraries.yml
vendored
@@ -1,26 +0,0 @@
|
|||||||
name: Publish library packages
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
paths:
|
|
||||||
- 'terminal-emulator/build.gradle'
|
|
||||||
- 'terminal-view/build.gradle'
|
|
||||||
- 'termux-shared/build.gradle'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Clone repository
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Perform release build
|
|
||||||
run: |
|
|
||||||
./gradlew assembleRelease
|
|
||||||
- name: Publish libraries on Github Packages
|
|
||||||
env:
|
|
||||||
GH_USERNAME: xeffyr
|
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
|
||||||
run: |
|
|
||||||
./gradlew publish
|
|
||||||
@@ -2,6 +2,5 @@ The `termux/termux-app` repository is released under [GPLv3 only](https://www.gn
|
|||||||
|
|
||||||
### Exceptions
|
### Exceptions
|
||||||
|
|
||||||
- [Terminal Emulator for Android](https://github.com/jackpal/Android-Terminal-Emulator) code is used which is released under [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). Check [terminal-view](terminal-view) and [terminal-emulator](terminal-emulator) modules.
|
- [Terminal Emulator for Android](https://github.com/jackpal/Android-Terminal-Emulator) code is used which is released under [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). Check [`terminal-view`](terminal-view) and [`terminal-emulator`](terminal-emulator) libraries.
|
||||||
- [libcore/ojluni](https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/) code is used which is released under [GPLv2 only with "Classpath" exception](https://openjdk.java.net/legal/gplv2+ce.html). Check `com.termux.shared.file` package under [termux-shared](termux-shared) module.
|
- Check [`termux-shared/LICENSE.md`](termux-shared/LICENSE.md) for `termux-shared` library related exceptions.
|
||||||
- [libsuperuser ](https://github.com/Chainfire/libsuperuser) code is used which is released under [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). Check `com.termux.shared.shell.StreamGobbler` class under [termux-shared](termux-shared) module.
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "androidx.annotation:annotation:1.2.0"
|
implementation "androidx.annotation:annotation:1.2.0"
|
||||||
implementation "androidx.core:core:1.5.0-rc01"
|
implementation "androidx.core:core:1.6.0-rc01"
|
||||||
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
|
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
|
||||||
implementation "androidx.preference:preference:1.1.1"
|
implementation "androidx.preference:preference:1.1.1"
|
||||||
implementation "androidx.viewpager:viewpager:1.0.0"
|
implementation "androidx.viewpager:viewpager:1.0.0"
|
||||||
@@ -26,8 +26,8 @@ android {
|
|||||||
applicationId "com.termux"
|
applicationId "com.termux"
|
||||||
minSdkVersion project.properties.minSdkVersion.toInteger()
|
minSdkVersion project.properties.minSdkVersion.toInteger()
|
||||||
targetSdkVersion project.properties.targetSdkVersion.toInteger()
|
targetSdkVersion project.properties.targetSdkVersion.toInteger()
|
||||||
versionCode 113
|
versionCode 115
|
||||||
versionName "0.113"
|
versionName "0.115"
|
||||||
|
|
||||||
manifestPlaceholders.TERMUX_PACKAGE_NAME = "com.termux"
|
manifestPlaceholders.TERMUX_PACKAGE_NAME = "com.termux"
|
||||||
manifestPlaceholders.TERMUX_APP_NAME = "Termux"
|
manifestPlaceholders.TERMUX_APP_NAME = "Termux"
|
||||||
@@ -155,11 +155,11 @@ clean {
|
|||||||
|
|
||||||
task downloadBootstraps() {
|
task downloadBootstraps() {
|
||||||
doLast {
|
doLast {
|
||||||
def version = "2021.05.16-r1"
|
def version = "2021.06.30-r1"
|
||||||
downloadBootstrap("aarch64", "6e340d8ab11d1225b89ee920e0884cbbd944d37765d81c5b06ef34579564fd9a", version)
|
downloadBootstrap("aarch64", "ce56ce9a4e8845bd1d35cc2695bbdd636c72625ee10ce21c9b98ab38ebbee5ab", version)
|
||||||
downloadBootstrap("arm", "3f02bc2b5bd45c2ec5170527e39ee0413246698f11be4799c7bde6d364cfd780", version)
|
downloadBootstrap("arm", "537e81951c7d3d3f3def9ce6778e1032457488e21edb2c037a1e0e680c39e747", version)
|
||||||
downloadBootstrap("i686", "36a3733fb2d8531d7f8abd989b711919872b9e8a79d7eb2e8b00bef467199187", version)
|
downloadBootstrap("i686", "3c2ca858c0225671c00c44ac182e31819ffa93ec624e95e02824e7d6d30ca1b4", version)
|
||||||
downloadBootstrap("x86_64", "3885376cc514220c0803e38f70b25f837854029fff2b7fda7a81452623cd9074", version)
|
downloadBootstrap("x86_64", "93c50d36b45bca42bb014395e8e184e5b540adcad5d4e215f7e64ebf0d655d2b", version)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,7 @@
|
|||||||
android:theme="@style/Theme.AppCompat.Light.DarkActionBar" />
|
android:theme="@style/Theme.AppCompat.Light.DarkActionBar" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".app.activities.ReportActivity"
|
android:name=".shared.activities.ReportActivity"
|
||||||
android:theme="@style/Theme.AppCompat.TermuxReportActivity"
|
android:theme="@style/Theme.AppCompat.TermuxReportActivity"
|
||||||
android:documentLaunchMode="intoExisting"
|
android:documentLaunchMode="intoExisting"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ import android.os.Build;
|
|||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
|
import com.termux.shared.data.DataUtils;
|
||||||
|
import com.termux.shared.data.IntentUtils;
|
||||||
|
import com.termux.shared.file.TermuxFileUtils;
|
||||||
|
import com.termux.shared.models.errors.Errno;
|
||||||
|
import com.termux.shared.models.errors.Error;
|
||||||
import com.termux.shared.termux.TermuxConstants;
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE;
|
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE;
|
||||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||||
@@ -17,7 +22,6 @@ import com.termux.shared.file.FileUtils;
|
|||||||
import com.termux.shared.logger.Logger;
|
import com.termux.shared.logger.Logger;
|
||||||
import com.termux.shared.notification.NotificationUtils;
|
import com.termux.shared.notification.NotificationUtils;
|
||||||
import com.termux.app.utils.PluginUtils;
|
import com.termux.app.utils.PluginUtils;
|
||||||
import com.termux.shared.data.DataUtils;
|
|
||||||
import com.termux.shared.models.ExecutionCommand;
|
import com.termux.shared.models.ExecutionCommand;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,29 +64,57 @@ public class RunCommandService extends Service {
|
|||||||
ExecutionCommand executionCommand = new ExecutionCommand();
|
ExecutionCommand executionCommand = new ExecutionCommand();
|
||||||
executionCommand.pluginAPIHelp = this.getString(R.string.error_run_command_service_api_help, RUN_COMMAND_SERVICE.RUN_COMMAND_API_HELP_URL);
|
executionCommand.pluginAPIHelp = this.getString(R.string.error_run_command_service_api_help, RUN_COMMAND_SERVICE.RUN_COMMAND_API_HELP_URL);
|
||||||
|
|
||||||
|
Error error;
|
||||||
String errmsg;
|
String errmsg;
|
||||||
|
|
||||||
// If invalid action passed, then just return
|
// If invalid action passed, then just return
|
||||||
if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) {
|
if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) {
|
||||||
errmsg = this.getString(R.string.error_run_command_service_invalid_intent_action, intent.getAction());
|
errmsg = this.getString(R.string.error_run_command_service_invalid_intent_action, intent.getAction());
|
||||||
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||||
return Service.START_NOT_STICKY;
|
return Service.START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
|
|
||||||
executionCommand.executable = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH);
|
executionCommand.executable = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH, null);
|
||||||
executionCommand.arguments = intent.getStringArrayExtra(RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS);
|
|
||||||
executionCommand.stdin = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_STDIN);
|
executionCommand.arguments = IntentUtils.getStringArrayExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS, null);
|
||||||
executionCommand.workingDirectory = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_WORKDIR);
|
|
||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
executionCommand.stdin = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_STDIN, null);
|
||||||
|
executionCommand.workingDirectory = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_WORKDIR, null);
|
||||||
executionCommand.inBackground = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false);
|
executionCommand.inBackground = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false);
|
||||||
executionCommand.sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION);
|
executionCommand.sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION);
|
||||||
executionCommand.commandLabel = DataUtils.getDefaultIfNull(intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_LABEL), "RUN_COMMAND Execution Intent Command");
|
executionCommand.commandLabel = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_LABEL, "RUN_COMMAND Execution Intent Command");
|
||||||
executionCommand.commandDescription = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION);
|
executionCommand.commandDescription = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION, null);
|
||||||
executionCommand.commandHelp = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_HELP);
|
executionCommand.commandHelp = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_HELP, null);
|
||||||
executionCommand.isPluginExecutionCommand = true;
|
executionCommand.isPluginExecutionCommand = true;
|
||||||
executionCommand.pluginPendingIntent = intent.getParcelableExtra(RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT);
|
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
|
// 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
|
// We enable force notifications if "allow-external-apps" policy is violated so that the
|
||||||
@@ -91,7 +123,7 @@ public class RunCommandService extends Service {
|
|||||||
// also sent, then its creator is also logged and shown.
|
// also sent, then its creator is also logged and shown.
|
||||||
errmsg = PluginUtils.checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(this);
|
errmsg = PluginUtils.checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(this);
|
||||||
if (errmsg != null) {
|
if (errmsg != null) {
|
||||||
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true);
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true);
|
||||||
return Service.START_NOT_STICKY;
|
return Service.START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
@@ -101,22 +133,22 @@ public class RunCommandService extends Service {
|
|||||||
// If executable is null or empty, then exit here instead of getting canonical path which would expand to "/"
|
// 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()) {
|
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);
|
errmsg = this.getString(R.string.error_run_command_service_mandatory_extra_missing, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH);
|
||||||
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||||
return Service.START_NOT_STICKY;
|
return Service.START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get canonical path of executable
|
// Get canonical path of executable
|
||||||
executionCommand.executable = FileUtils.getCanonicalPath(executionCommand.executable, null, true);
|
executionCommand.executable = TermuxFileUtils.getCanonicalPath(executionCommand.executable, null, true);
|
||||||
|
|
||||||
// If executable is not a regular file, or is not readable or executable, then just return
|
// 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
|
// Setting of missing read and execute permissions is not done
|
||||||
errmsg = FileUtils.validateRegularFileExistenceAndPermissions(this, "executable", executionCommand.executable, null,
|
error = FileUtils.validateRegularFileExistenceAndPermissions("executable", executionCommand.executable, null,
|
||||||
PluginUtils.PLUGIN_EXECUTABLE_FILE_PERMISSIONS, true, true,
|
FileUtils.APP_EXECUTABLE_FILE_PERMISSIONS, true, true,
|
||||||
false);
|
false);
|
||||||
if (errmsg != null) {
|
if (error != null) {
|
||||||
errmsg += "\n" + this.getString(R.string.msg_executable_absolute_path, executionCommand.executable);
|
error.appendMessage("\n" + this.getString(R.string.msg_executable_absolute_path, executionCommand.executable));
|
||||||
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
executionCommand.setStateFailed(error);
|
||||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||||
return Service.START_NOT_STICKY;
|
return Service.START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
@@ -126,19 +158,19 @@ public class RunCommandService extends Service {
|
|||||||
// If workingDirectory is not null or empty
|
// If workingDirectory is not null or empty
|
||||||
if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) {
|
if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) {
|
||||||
// Get canonical path of workingDirectory
|
// Get canonical path of workingDirectory
|
||||||
executionCommand.workingDirectory = FileUtils.getCanonicalPath(executionCommand.workingDirectory, null, true);
|
executionCommand.workingDirectory = TermuxFileUtils.getCanonicalPath(executionCommand.workingDirectory, null, true);
|
||||||
|
|
||||||
// If workingDirectory is not a directory, or is not readable or writable, then just return
|
// 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
|
// Creation of missing directory and setting of read, write and execute permissions are only done if workingDirectory is
|
||||||
// under {@link TermuxConstants#TERMUX_FILES_DIR_PATH}
|
// 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
|
// We try to set execute permissions, but ignore if they are missing, since only read and write permissions are required
|
||||||
// for working directories.
|
// for working directories.
|
||||||
errmsg = FileUtils.validateDirectoryFileExistenceAndPermissions(this, "working", executionCommand.workingDirectory, TermuxConstants.TERMUX_FILES_DIR_PATH, true,
|
error = TermuxFileUtils.validateDirectoryFileExistenceAndPermissions("working", executionCommand.workingDirectory,
|
||||||
PluginUtils.PLUGIN_WORKING_DIRECTORY_PERMISSIONS, true, true,
|
true, true, true,
|
||||||
true, true);
|
false, true);
|
||||||
if (errmsg != null) {
|
if (error != null) {
|
||||||
errmsg += "\n" + this.getString(R.string.msg_working_directory_absolute_path, executionCommand.workingDirectory);
|
error.appendMessage("\n" + this.getString(R.string.msg_working_directory_absolute_path, executionCommand.workingDirectory));
|
||||||
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
executionCommand.setStateFailed(error);
|
||||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||||
return Service.START_NOT_STICKY;
|
return Service.START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
@@ -146,7 +178,7 @@ public class RunCommandService extends Service {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(FileUtils.getExpandedTermuxPath(executionCommand.executable)).build();
|
executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(TermuxFileUtils.getExpandedTermuxPath(executionCommand.executable)).build();
|
||||||
|
|
||||||
Logger.logVerbose(LOG_TAG, executionCommand.toString());
|
Logger.logVerbose(LOG_TAG, executionCommand.toString());
|
||||||
|
|
||||||
@@ -162,7 +194,15 @@ public class RunCommandService extends Service {
|
|||||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, executionCommand.commandDescription);
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, executionCommand.commandDescription);
|
||||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_HELP, executionCommand.commandHelp);
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_HELP, executionCommand.commandHelp);
|
||||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP, executionCommand.pluginAPIHelp);
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP, executionCommand.pluginAPIHelp);
|
||||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT, executionCommand.pluginPendingIntent);
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT, executionCommand.resultConfig.resultPendingIntent);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_DIRECTORY, executionCommand.resultConfig.resultDirectoryPath);
|
||||||
|
if (executionCommand.resultConfig.resultDirectoryPath != null) {
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_SINGLE_FILE, executionCommand.resultConfig.resultSingleFile);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_BASENAME, executionCommand.resultConfig.resultFileBasename);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, executionCommand.resultConfig.resultFileOutputFormat);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, executionCommand.resultConfig.resultFileErrorFormat);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILES_SUFFIX, executionCommand.resultConfig.resultFilesSuffix);
|
||||||
|
}
|
||||||
|
|
||||||
// Start TERMUX_SERVICE and pass it execution intent
|
// Start TERMUX_SERVICE and pass it execution intent
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import android.content.Intent;
|
|||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
|
import android.graphics.Color;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@@ -26,10 +27,13 @@ import android.view.ViewGroup;
|
|||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.view.autofill.AutofillManager;
|
import android.view.autofill.AutofillManager;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
|
import android.widget.ImageButton;
|
||||||
import android.widget.ListView;
|
import android.widget.ListView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
|
import com.termux.app.terminal.TermuxActivityRootView;
|
||||||
|
import com.termux.shared.packages.PermissionUtils;
|
||||||
import com.termux.shared.termux.TermuxConstants;
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
|
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
|
||||||
import com.termux.app.activities.HelpActivity;
|
import com.termux.app.activities.HelpActivity;
|
||||||
@@ -41,7 +45,7 @@ import com.termux.app.terminal.TermuxTerminalSessionClient;
|
|||||||
import com.termux.app.terminal.TermuxTerminalViewClient;
|
import com.termux.app.terminal.TermuxTerminalViewClient;
|
||||||
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
|
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
|
||||||
import com.termux.app.settings.properties.TermuxAppSharedProperties;
|
import com.termux.app.settings.properties.TermuxAppSharedProperties;
|
||||||
import com.termux.shared.interact.DialogUtils;
|
import com.termux.shared.interact.TextInputDialogUtils;
|
||||||
import com.termux.shared.logger.Logger;
|
import com.termux.shared.logger.Logger;
|
||||||
import com.termux.shared.termux.TermuxUtils;
|
import com.termux.shared.termux.TermuxUtils;
|
||||||
import com.termux.terminal.TerminalSession;
|
import com.termux.terminal.TerminalSession;
|
||||||
@@ -68,7 +72,6 @@ import androidx.viewpager.widget.ViewPager;
|
|||||||
*/
|
*/
|
||||||
public final class TermuxActivity extends Activity implements ServiceConnection {
|
public final class TermuxActivity extends Activity implements ServiceConnection {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The connection to the {@link TermuxService}. Requested in {@link #onCreate(Bundle)} with a call to
|
* The connection to the {@link TermuxService}. Requested in {@link #onCreate(Bundle)} with a call to
|
||||||
* {@link #bindService(Intent, ServiceConnection, int)}, and obtained and stored in
|
* {@link #bindService(Intent, ServiceConnection, int)}, and obtained and stored in
|
||||||
@@ -77,7 +80,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
TermuxService mTermuxService;
|
TermuxService mTermuxService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main view of the activity showing the terminal. Initialized in onCreate().
|
* The {@link TerminalView} shown in {@link TermuxActivity} that displays the terminal.
|
||||||
*/
|
*/
|
||||||
TerminalView mTerminalView;
|
TerminalView mTerminalView;
|
||||||
|
|
||||||
@@ -103,6 +106,16 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
*/
|
*/
|
||||||
private TermuxAppSharedProperties mProperties;
|
private TermuxAppSharedProperties mProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The root view of the {@link TermuxActivity}.
|
||||||
|
*/
|
||||||
|
TermuxActivityRootView mTermuxActivityRootView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The space at the bottom of {@link @mTermuxActivityRootView} of the {@link TermuxActivity}.
|
||||||
|
*/
|
||||||
|
View mTermuxActivityBottomSpaceView;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The terminal extra keys view.
|
* The terminal extra keys view.
|
||||||
*/
|
*/
|
||||||
@@ -129,6 +142,11 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
*/
|
*/
|
||||||
private boolean mIsVisible;
|
private boolean mIsVisible;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If onResume() was called after onCreate().
|
||||||
|
*/
|
||||||
|
private boolean isOnResumeAfterOnCreate = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link TermuxActivity} is in an invalid state and must not be run.
|
* The {@link TermuxActivity} is in an invalid state and must not be run.
|
||||||
*/
|
*/
|
||||||
@@ -150,8 +168,6 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
private static final int CONTEXT_MENU_SETTINGS_ID = 8;
|
private static final int CONTEXT_MENU_SETTINGS_ID = 8;
|
||||||
private static final int CONTEXT_MENU_REPORT_ID = 9;
|
private static final int CONTEXT_MENU_REPORT_ID = 9;
|
||||||
|
|
||||||
private static final int REQUESTCODE_PERMISSION_STORAGE = 1234;
|
|
||||||
|
|
||||||
private static final String ARG_TERMINAL_TOOLBAR_TEXT_INPUT = "terminal_toolbar_text_input";
|
private static final String ARG_TERMINAL_TOOLBAR_TEXT_INPUT = "terminal_toolbar_text_input";
|
||||||
|
|
||||||
private static final String LOG_TAG = "TermuxActivity";
|
private static final String LOG_TAG = "TermuxActivity";
|
||||||
@@ -160,10 +176,11 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
|
||||||
Logger.logDebug(LOG_TAG, "onCreate");
|
Logger.logDebug(LOG_TAG, "onCreate");
|
||||||
|
isOnResumeAfterOnCreate = true;
|
||||||
|
|
||||||
// Check if a crash happened on last run of the app and show a
|
// Check if a crash happened on last run of the app and show a
|
||||||
// notification with the crash details if it did
|
// notification with the crash details if it did
|
||||||
CrashUtils.notifyCrash(this, LOG_TAG);
|
CrashUtils.notifyAppCrashOnLastRun(this, LOG_TAG);
|
||||||
|
|
||||||
// Load termux shared properties
|
// Load termux shared properties
|
||||||
mProperties = new TermuxAppSharedProperties(this);
|
mProperties = new TermuxAppSharedProperties(this);
|
||||||
@@ -183,6 +200,11 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mTermuxActivityRootView = findViewById(R.id.activity_termux_root_view);
|
||||||
|
mTermuxActivityRootView.setActivity(this);
|
||||||
|
mTermuxActivityBottomSpaceView = findViewById(R.id.activity_termux_bottom_space_view);
|
||||||
|
mTermuxActivityRootView.setOnApplyWindowInsetsListener(new TermuxActivityRootView.WindowInsetsListener());
|
||||||
|
|
||||||
View content = findViewById(android.R.id.content);
|
View content = findViewById(android.R.id.content);
|
||||||
content.setOnApplyWindowInsetsListener((v, insets) -> {
|
content.setOnApplyWindowInsetsListener((v, insets) -> {
|
||||||
mNavBarHeight = insets.getSystemWindowInsetBottom();
|
mNavBarHeight = insets.getSystemWindowInsetBottom();
|
||||||
@@ -199,6 +221,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
|
|
||||||
setTerminalToolbarView(savedInstanceState);
|
setTerminalToolbarView(savedInstanceState);
|
||||||
|
|
||||||
|
setSettingsButtonView();
|
||||||
|
|
||||||
setNewSessionButtonView();
|
setNewSessionButtonView();
|
||||||
|
|
||||||
setToggleKeyboardView();
|
setToggleKeyboardView();
|
||||||
@@ -235,6 +259,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
if (mTermuxTerminalViewClient != null)
|
if (mTermuxTerminalViewClient != null)
|
||||||
mTermuxTerminalViewClient.onStart();
|
mTermuxTerminalViewClient.onStart();
|
||||||
|
|
||||||
|
if (!mProperties.isTerminalMarginAdjustmentDisabled())
|
||||||
|
addTermuxActivityRootViewGlobalLayoutListener();
|
||||||
|
|
||||||
registerTermuxActivityBroadcastReceiver();
|
registerTermuxActivityBroadcastReceiver();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,6 +278,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
|
|
||||||
if (mTermuxTerminalViewClient != null)
|
if (mTermuxTerminalViewClient != null)
|
||||||
mTermuxTerminalViewClient.onResume();
|
mTermuxTerminalViewClient.onResume();
|
||||||
|
|
||||||
|
isOnResumeAfterOnCreate = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -269,6 +298,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
if (mTermuxTerminalViewClient != null)
|
if (mTermuxTerminalViewClient != null)
|
||||||
mTermuxTerminalViewClient.onStop();
|
mTermuxTerminalViewClient.onStop();
|
||||||
|
|
||||||
|
removeTermuxActivityRootViewGlobalLayoutListener();
|
||||||
|
|
||||||
unregisterTermuxActivityBroadcastReceiever();
|
unregisterTermuxActivityBroadcastReceiever();
|
||||||
getDrawer().closeDrawers();
|
getDrawer().closeDrawers();
|
||||||
}
|
}
|
||||||
@@ -377,11 +408,23 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
if (mProperties.isUsingBlackUI()) {
|
if (mProperties.isUsingBlackUI()) {
|
||||||
findViewById(R.id.left_drawer).setBackgroundColor(ContextCompat.getColor(this,
|
findViewById(R.id.left_drawer).setBackgroundColor(ContextCompat.getColor(this,
|
||||||
android.R.color.background_dark));
|
android.R.color.background_dark));
|
||||||
|
((ImageButton) findViewById(R.id.settings_button)).setColorFilter(Color.WHITE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void addTermuxActivityRootViewGlobalLayoutListener() {
|
||||||
|
getTermuxActivityRootView().getViewTreeObserver().addOnGlobalLayoutListener(getTermuxActivityRootView());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeTermuxActivityRootViewGlobalLayoutListener() {
|
||||||
|
if (getTermuxActivityRootView() != null)
|
||||||
|
getTermuxActivityRootView().getViewTreeObserver().removeOnGlobalLayoutListener(getTermuxActivityRootView());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void setTermuxTerminalViewAndClients() {
|
private void setTermuxTerminalViewAndClients() {
|
||||||
// Set termux terminal view and session clients
|
// Set termux terminal view and session clients
|
||||||
mTermuxTerminalSessionClient = new TermuxTerminalSessionClient(this);
|
mTermuxTerminalSessionClient = new TermuxTerminalSessionClient(this);
|
||||||
@@ -430,8 +473,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
if (terminalToolbarViewPager == null) return;
|
if (terminalToolbarViewPager == null) return;
|
||||||
ViewGroup.LayoutParams layoutParams = terminalToolbarViewPager.getLayoutParams();
|
ViewGroup.LayoutParams layoutParams = terminalToolbarViewPager.getLayoutParams();
|
||||||
layoutParams.height = (int) Math.round(mTerminalToolbarDefaultHeight *
|
layoutParams.height = (int) Math.round(mTerminalToolbarDefaultHeight *
|
||||||
(mProperties.getExtraKeysInfo() == null ? 0 : mProperties.getExtraKeysInfo().getMatrix().length) *
|
(mProperties.getExtraKeysInfo() == null ? 0 : mProperties.getExtraKeysInfo().getMatrix().length) *
|
||||||
mProperties.getTerminalToolbarHeightScaleFactor());
|
mProperties.getTerminalToolbarHeightScaleFactor());
|
||||||
terminalToolbarViewPager.setLayoutParams(layoutParams);
|
terminalToolbarViewPager.setLayoutParams(layoutParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,11 +503,18 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private void setSettingsButtonView() {
|
||||||
|
ImageButton settingsButton = findViewById(R.id.settings_button);
|
||||||
|
settingsButton.setOnClickListener(v -> {
|
||||||
|
startActivity(new Intent(this, SettingsActivity.class));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void setNewSessionButtonView() {
|
private void setNewSessionButtonView() {
|
||||||
View newSessionButton = findViewById(R.id.new_session_button);
|
View newSessionButton = findViewById(R.id.new_session_button);
|
||||||
newSessionButton.setOnClickListener(v -> mTermuxTerminalSessionClient.addNewSession(false, null));
|
newSessionButton.setOnClickListener(v -> mTermuxTerminalSessionClient.addNewSession(false, null));
|
||||||
newSessionButton.setOnLongClickListener(v -> {
|
newSessionButton.setOnLongClickListener(v -> {
|
||||||
DialogUtils.textInput(TermuxActivity.this, R.string.title_create_named_session, null,
|
TextInputDialogUtils.textInput(TermuxActivity.this, R.string.title_create_named_session, null,
|
||||||
R.string.action_create_named_session_confirm, text -> mTermuxTerminalSessionClient.addNewSession(false, text),
|
R.string.action_create_named_session_confirm, text -> mTermuxTerminalSessionClient.addNewSession(false, text),
|
||||||
R.string.action_new_session_failsafe, text -> mTermuxTerminalSessionClient.addNewSession(true, text),
|
R.string.action_new_session_failsafe, text -> mTermuxTerminalSessionClient.addNewSession(true, text),
|
||||||
-1, null, null);
|
-1, null, null);
|
||||||
@@ -563,7 +613,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
requestAutoFill();
|
requestAutoFill();
|
||||||
return true;
|
return true;
|
||||||
case CONTEXT_MENU_RESET_TERMINAL_ID:
|
case CONTEXT_MENU_RESET_TERMINAL_ID:
|
||||||
resetSession(session);
|
onResetTerminalSession(session);
|
||||||
return true;
|
return true;
|
||||||
case CONTEXT_MENU_KILL_PROCESS_ID:
|
case CONTEXT_MENU_KILL_PROCESS_ID:
|
||||||
showKillSessionDialog(session);
|
showKillSessionDialog(session);
|
||||||
@@ -602,10 +652,13 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
b.show();
|
b.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resetSession(TerminalSession session) {
|
private void onResetTerminalSession(TerminalSession session) {
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
session.reset();
|
session.reset();
|
||||||
showToast(getResources().getString(R.string.msg_terminal_reset), true);
|
showToast(getResources().getString(R.string.msg_terminal_reset), true);
|
||||||
|
|
||||||
|
if (mTermuxTerminalSessionClient != null)
|
||||||
|
mTermuxTerminalSessionClient.onResetTerminalSession();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -646,22 +699,22 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
* For processes to access shared internal storage (/sdcard) we need this permission.
|
* For processes to access shared internal storage (/sdcard) we need this permission.
|
||||||
*/
|
*/
|
||||||
public boolean ensureStoragePermissionGranted() {
|
public boolean ensureStoragePermissionGranted() {
|
||||||
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
|
if (PermissionUtils.checkPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
Logger.logDebug(LOG_TAG, "Storage permission not granted, requesting permission.");
|
Logger.logInfo(LOG_TAG, "Storage permission not granted, requesting permission.");
|
||||||
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUESTCODE_PERMISSION_STORAGE);
|
PermissionUtils.requestPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE, PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||||
if (requestCode == REQUESTCODE_PERMISSION_STORAGE && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
if (requestCode == PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
Logger.logDebug(LOG_TAG, "Storage permission granted by user on request.");
|
Logger.logInfo(LOG_TAG, "Storage permission granted by user on request.");
|
||||||
TermuxInstaller.setupStorageSymlinks(this);
|
TermuxInstaller.setupStorageSymlinks(this);
|
||||||
} else {
|
} else {
|
||||||
Logger.logDebug(LOG_TAG, "Storage permission denied by user on request.");
|
Logger.logInfo(LOG_TAG, "Storage permission denied by user on request.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -671,6 +724,14 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
return mNavBarHeight;
|
return mNavBarHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TermuxActivityRootView getTermuxActivityRootView() {
|
||||||
|
return mTermuxActivityRootView;
|
||||||
|
}
|
||||||
|
|
||||||
|
public View getTermuxActivityBottomSpaceView() {
|
||||||
|
return mTermuxActivityBottomSpaceView;
|
||||||
|
}
|
||||||
|
|
||||||
public ExtraKeysView getExtraKeysView() {
|
public ExtraKeysView getExtraKeysView() {
|
||||||
return mExtraKeysView;
|
return mExtraKeysView;
|
||||||
}
|
}
|
||||||
@@ -691,6 +752,10 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
return mIsVisible;
|
return mIsVisible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isOnResumeAfterOnCreate() {
|
||||||
|
return isOnResumeAfterOnCreate;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public TermuxService getTermuxService() {
|
public TermuxService getTermuxService() {
|
||||||
@@ -797,6 +862,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
if (mTermuxTerminalViewClient != null)
|
if (mTermuxTerminalViewClient != null)
|
||||||
mTermuxTerminalViewClient.onReload();
|
mTermuxTerminalViewClient.onReload();
|
||||||
|
|
||||||
|
if (mTermuxService != null)
|
||||||
|
mTermuxService.setTerminalTranscriptRows();
|
||||||
|
|
||||||
// To change the activity and drawer theme, activity needs to be recreated.
|
// To change the activity and drawer theme, activity needs to be recreated.
|
||||||
// But this will destroy the activity, and will call the onCreate() again.
|
// But this will destroy the activity, and will call the onCreate() again.
|
||||||
// We need to investigate if enabling this is wise, since all stored variables and
|
// We need to investigate if enabling this is wise, since all stored variables and
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package com.termux.app;
|
|||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
|
|
||||||
import com.termux.shared.crash.CrashHandler;
|
import com.termux.shared.crash.TermuxCrashUtils;
|
||||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||||
import com.termux.shared.logger.Logger;
|
import com.termux.shared.logger.Logger;
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ public class TermuxApplication extends Application {
|
|||||||
super.onCreate();
|
super.onCreate();
|
||||||
|
|
||||||
// Set crash handler for the app
|
// Set crash handler for the app
|
||||||
CrashHandler.setCrashHandler(this);
|
TermuxCrashUtils.setCrashHandler(this);
|
||||||
|
|
||||||
// Set log level for the app
|
// Set log level for the app
|
||||||
setLogLevel();
|
setLogLevel();
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ import android.util.Pair;
|
|||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
|
import com.termux.app.utils.CrashUtils;
|
||||||
import com.termux.shared.file.FileUtils;
|
import com.termux.shared.file.FileUtils;
|
||||||
import com.termux.shared.interact.DialogUtils;
|
import com.termux.shared.interact.MessageDialogUtils;
|
||||||
import com.termux.shared.logger.Logger;
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.models.errors.Error;
|
||||||
import com.termux.shared.termux.TermuxConstants;
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
@@ -58,7 +60,7 @@ final class TermuxInstaller {
|
|||||||
if (!isPrimaryUser) {
|
if (!isPrimaryUser) {
|
||||||
String bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message, TermuxConstants.TERMUX_PREFIX_DIR_PATH);
|
String bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message, TermuxConstants.TERMUX_PREFIX_DIR_PATH);
|
||||||
Logger.logError(LOG_TAG, bootstrapErrorMessage);
|
Logger.logError(LOG_TAG, bootstrapErrorMessage);
|
||||||
DialogUtils.exitAppWithErrorMessage(activity,
|
MessageDialogUtils.exitAppWithErrorMessage(activity,
|
||||||
activity.getString(R.string.bootstrap_error_title),
|
activity.getString(R.string.bootstrap_error_title),
|
||||||
bootstrapErrorMessage);
|
bootstrapErrorMessage);
|
||||||
return;
|
return;
|
||||||
@@ -69,14 +71,14 @@ final class TermuxInstaller {
|
|||||||
|
|
||||||
// If prefix directory exists, even if its a symlink to a valid directory and symlink is not broken/dangling
|
// If prefix directory exists, even if its a symlink to a valid directory and symlink is not broken/dangling
|
||||||
if (FileUtils.directoryFileExists(PREFIX_FILE_PATH, true)) {
|
if (FileUtils.directoryFileExists(PREFIX_FILE_PATH, true)) {
|
||||||
File[] PREFIX_FILE_LIST = PREFIX_FILE.listFiles();
|
File[] PREFIX_FILE_LIST = PREFIX_FILE.listFiles();
|
||||||
// If prefix directory is empty or only contains the tmp directory
|
// 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()))) {
|
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()))) {
|
||||||
Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" exists but is empty or only contains the tmp directory.");
|
Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" exists but is empty or only contains the tmp directory.");
|
||||||
} else {
|
} else {
|
||||||
whenDone.run();
|
whenDone.run();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (FileUtils.fileExists(PREFIX_FILE_PATH, false)) {
|
} else if (FileUtils.fileExists(PREFIX_FILE_PATH, false)) {
|
||||||
Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" does not exist but another file exists at its destination.");
|
Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" does not exist but another file exists at its destination.");
|
||||||
}
|
}
|
||||||
@@ -88,21 +90,23 @@ final class TermuxInstaller {
|
|||||||
try {
|
try {
|
||||||
Logger.logInfo(LOG_TAG, "Installing " + TermuxConstants.TERMUX_APP_NAME + " bootstrap packages.");
|
Logger.logInfo(LOG_TAG, "Installing " + TermuxConstants.TERMUX_APP_NAME + " bootstrap packages.");
|
||||||
|
|
||||||
String errmsg;
|
Error error;
|
||||||
|
|
||||||
final String STAGING_PREFIX_PATH = TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH;
|
final String STAGING_PREFIX_PATH = TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH;
|
||||||
final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH);
|
final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH);
|
||||||
|
|
||||||
// Delete prefix staging directory or any file at its destination
|
// Delete prefix staging directory or any file at its destination
|
||||||
errmsg = FileUtils.deleteFile(activity, "prefix staging directory", STAGING_PREFIX_PATH, true);
|
error = FileUtils.deleteFile("prefix staging directory", STAGING_PREFIX_PATH, true);
|
||||||
if (errmsg != null) {
|
if (error != null) {
|
||||||
throw new RuntimeException(errmsg);
|
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete prefix directory or any file at its destination
|
// Delete prefix directory or any file at its destination
|
||||||
errmsg = FileUtils.deleteFile(activity, "prefix directory", PREFIX_FILE_PATH, true);
|
error = FileUtils.deleteFile("prefix directory", PREFIX_FILE_PATH, true);
|
||||||
if (errmsg != null) {
|
if (error != null) {
|
||||||
throw new RuntimeException(errmsg);
|
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.logInfo(LOG_TAG, "Extracting bootstrap zip to prefix staging directory \"" + STAGING_PREFIX_PATH + "\".");
|
Logger.logInfo(LOG_TAG, "Extracting bootstrap zip to prefix staging directory \"" + STAGING_PREFIX_PATH + "\".");
|
||||||
@@ -125,14 +129,22 @@ final class TermuxInstaller {
|
|||||||
String newPath = STAGING_PREFIX_PATH + "/" + parts[1];
|
String newPath = STAGING_PREFIX_PATH + "/" + parts[1];
|
||||||
symlinks.add(Pair.create(oldPath, newPath));
|
symlinks.add(Pair.create(oldPath, newPath));
|
||||||
|
|
||||||
ensureDirectoryExists(activity, new File(newPath).getParentFile());
|
error = ensureDirectoryExists(new File(newPath).getParentFile());
|
||||||
|
if (error != null) {
|
||||||
|
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
String zipEntryName = zipEntry.getName();
|
String zipEntryName = zipEntry.getName();
|
||||||
File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName);
|
File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName);
|
||||||
boolean isDirectory = zipEntry.isDirectory();
|
boolean isDirectory = zipEntry.isDirectory();
|
||||||
|
|
||||||
ensureDirectoryExists(activity, isDirectory ? targetFile : targetFile.getParentFile());
|
error = ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile());
|
||||||
|
if (error != null) {
|
||||||
|
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isDirectory) {
|
if (!isDirectory) {
|
||||||
try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
|
try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
|
||||||
@@ -163,22 +175,10 @@ final class TermuxInstaller {
|
|||||||
|
|
||||||
Logger.logInfo(LOG_TAG, "Bootstrap packages installed successfully.");
|
Logger.logInfo(LOG_TAG, "Bootstrap packages installed successfully.");
|
||||||
activity.runOnUiThread(whenDone);
|
activity.runOnUiThread(whenDone);
|
||||||
|
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
Logger.logStackTraceWithMessage(LOG_TAG, "Bootstrap error", e);
|
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)));
|
||||||
activity.runOnUiThread(() -> {
|
|
||||||
try {
|
|
||||||
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
|
|
||||||
.setNegativeButton(R.string.bootstrap_error_abort, (dialog, which) -> {
|
|
||||||
dialog.dismiss();
|
|
||||||
activity.finish();
|
|
||||||
}).setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
|
|
||||||
dialog.dismiss();
|
|
||||||
TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone);
|
|
||||||
}).show();
|
|
||||||
} catch (WindowManager.BadTokenException e1) {
|
|
||||||
// Activity already dismissed - ignore.
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} finally {
|
} finally {
|
||||||
activity.runOnUiThread(() -> {
|
activity.runOnUiThread(() -> {
|
||||||
try {
|
try {
|
||||||
@@ -192,6 +192,30 @@ final class TermuxInstaller {
|
|||||||
}.start();
|
}.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void showBootstrapErrorDialog(Activity activity, String PREFIX_FILE_PATH, Runnable whenDone, String message) {
|
||||||
|
Logger.logErrorExtended(LOG_TAG, "Bootstrap Error:\n" + message);
|
||||||
|
|
||||||
|
// Send a notification with the exception so that the user knows why bootstrap setup failed
|
||||||
|
CrashUtils.sendCrashReportNotification(activity, LOG_TAG, "## Bootstrap Error\n\n" + message, true);
|
||||||
|
|
||||||
|
activity.runOnUiThread(() -> {
|
||||||
|
try {
|
||||||
|
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
|
||||||
|
.setNegativeButton(R.string.bootstrap_error_abort, (dialog, which) -> {
|
||||||
|
dialog.dismiss();
|
||||||
|
activity.finish();
|
||||||
|
})
|
||||||
|
.setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
|
||||||
|
dialog.dismiss();
|
||||||
|
FileUtils.deleteFile("prefix directory", PREFIX_FILE_PATH, true);
|
||||||
|
TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone);
|
||||||
|
}).show();
|
||||||
|
} catch (WindowManager.BadTokenException e1) {
|
||||||
|
// Activity already dismissed - ignore.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static void setupStorageSymlinks(final Context context) {
|
static void setupStorageSymlinks(final Context context) {
|
||||||
final String LOG_TAG = "termux-storage";
|
final String LOG_TAG = "termux-storage";
|
||||||
|
|
||||||
@@ -200,12 +224,14 @@ final class TermuxInstaller {
|
|||||||
new Thread() {
|
new Thread() {
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
String errmsg;
|
Error error;
|
||||||
File storageDir = TermuxConstants.TERMUX_STORAGE_HOME_DIR;
|
File storageDir = TermuxConstants.TERMUX_STORAGE_HOME_DIR;
|
||||||
|
|
||||||
errmsg = FileUtils.clearDirectory(context, "~/storage", storageDir.getAbsolutePath());
|
error = FileUtils.clearDirectory("~/storage", storageDir.getAbsolutePath());
|
||||||
if (errmsg != null) {
|
if (error != null) {
|
||||||
Logger.logErrorAndShowToast(context, LOG_TAG, errmsg);
|
Logger.logErrorAndShowToast(context, LOG_TAG, error.getMessage());
|
||||||
|
Logger.logErrorExtended(LOG_TAG, "Setup Storage Error\n" + error.toString());
|
||||||
|
CrashUtils.sendCrashReportNotification(context, LOG_TAG, "## Setup Storage Error\n\n" + Error.getErrorMarkdownString(error), true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,19 +268,16 @@ final class TermuxInstaller {
|
|||||||
|
|
||||||
Logger.logInfo(LOG_TAG, "Storage symlinks created successfully.");
|
Logger.logInfo(LOG_TAG, "Storage symlinks created successfully.");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Logger.logStackTraceWithMessage(LOG_TAG, "Error setting up link", e);
|
Logger.logErrorAndShowToast(context, LOG_TAG, e.getMessage());
|
||||||
|
Logger.logStackTraceWithMessage(LOG_TAG, "Setup Storage Error: Error setting up link", e);
|
||||||
|
CrashUtils.sendCrashReportNotification(context, LOG_TAG, "## Setup Storage Error\n\n" + Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)), true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.start();
|
}.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ensureDirectoryExists(Context context, File directory) {
|
private static Error ensureDirectoryExists(File directory) {
|
||||||
String errmsg;
|
return FileUtils.createDirectoryFile(directory.getAbsolutePath());
|
||||||
|
|
||||||
errmsg = FileUtils.createDirectoryFile(context, directory.getAbsolutePath());
|
|
||||||
if (errmsg != null) {
|
|
||||||
throw new RuntimeException(errmsg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] loadZipBytes() {
|
public static byte[] loadZipBytes() {
|
||||||
|
|||||||
@@ -20,8 +20,14 @@ import android.provider.Settings;
|
|||||||
import android.widget.ArrayAdapter;
|
import android.widget.ArrayAdapter;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
|
import com.termux.app.settings.properties.TermuxAppSharedProperties;
|
||||||
import com.termux.app.terminal.TermuxTerminalSessionClient;
|
import com.termux.app.terminal.TermuxTerminalSessionClient;
|
||||||
import com.termux.app.utils.PluginUtils;
|
import com.termux.app.utils.PluginUtils;
|
||||||
|
import com.termux.shared.data.IntentUtils;
|
||||||
|
import com.termux.shared.models.errors.Errno;
|
||||||
|
import com.termux.shared.shell.ShellUtils;
|
||||||
|
import com.termux.shared.shell.TermuxShellEnvironmentClient;
|
||||||
|
import com.termux.shared.shell.TermuxShellUtils;
|
||||||
import com.termux.shared.termux.TermuxConstants;
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
|
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
|
||||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||||
@@ -31,7 +37,6 @@ import com.termux.shared.terminal.TermuxTerminalSessionClientBase;
|
|||||||
import com.termux.shared.logger.Logger;
|
import com.termux.shared.logger.Logger;
|
||||||
import com.termux.shared.notification.NotificationUtils;
|
import com.termux.shared.notification.NotificationUtils;
|
||||||
import com.termux.shared.packages.PermissionUtils;
|
import com.termux.shared.packages.PermissionUtils;
|
||||||
import com.termux.shared.shell.ShellUtils;
|
|
||||||
import com.termux.shared.data.DataUtils;
|
import com.termux.shared.data.DataUtils;
|
||||||
import com.termux.shared.models.ExecutionCommand;
|
import com.termux.shared.models.ExecutionCommand;
|
||||||
import com.termux.shared.shell.TermuxTask;
|
import com.termux.shared.shell.TermuxTask;
|
||||||
@@ -106,6 +111,8 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||||||
/** If the user has executed the {@link TERMUX_SERVICE#ACTION_STOP_SERVICE} intent. */
|
/** If the user has executed the {@link TERMUX_SERVICE#ACTION_STOP_SERVICE} intent. */
|
||||||
boolean mWantsToStop = false;
|
boolean mWantsToStop = false;
|
||||||
|
|
||||||
|
public Integer mTerminalTranscriptRows;
|
||||||
|
|
||||||
private static final String LOG_TAG = "TermuxService";
|
private static final String LOG_TAG = "TermuxService";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -157,7 +164,7 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
Logger.logVerbose(LOG_TAG, "onDestroy");
|
Logger.logVerbose(LOG_TAG, "onDestroy");
|
||||||
|
|
||||||
ShellUtils.clearTermuxTMPDIR(this, true);
|
TermuxShellUtils.clearTermuxTMPDIR(true);
|
||||||
|
|
||||||
actionReleaseWakeLock(false);
|
actionReleaseWakeLock(false);
|
||||||
if (!mWantsToStop)
|
if (!mWantsToStop)
|
||||||
@@ -251,22 +258,22 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||||||
List<TermuxSession> termuxSessions = new ArrayList<>(mTermuxSessions);
|
List<TermuxSession> termuxSessions = new ArrayList<>(mTermuxSessions);
|
||||||
for (int i = 0; i < termuxSessions.size(); i++) {
|
for (int i = 0; i < termuxSessions.size(); i++) {
|
||||||
ExecutionCommand executionCommand = termuxSessions.get(i).getExecutionCommand();
|
ExecutionCommand executionCommand = termuxSessions.get(i).getExecutionCommand();
|
||||||
processResult = mWantsToStop || (executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null);
|
processResult = mWantsToStop || executionCommand.isPluginExecutionCommandWithPendingResult();
|
||||||
termuxSessions.get(i).killIfExecuting(this, processResult);
|
termuxSessions.get(i).killIfExecuting(this, processResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<TermuxTask> termuxTasks = new ArrayList<>(mTermuxTasks);
|
List<TermuxTask> termuxTasks = new ArrayList<>(mTermuxTasks);
|
||||||
for (int i = 0; i < termuxTasks.size(); i++) {
|
for (int i = 0; i < termuxTasks.size(); i++) {
|
||||||
ExecutionCommand executionCommand = termuxTasks.get(i).getExecutionCommand();
|
ExecutionCommand executionCommand = termuxTasks.get(i).getExecutionCommand();
|
||||||
if (executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null)
|
if (executionCommand.isPluginExecutionCommandWithPendingResult())
|
||||||
termuxTasks.get(i).killIfExecuting(this, true);
|
termuxTasks.get(i).killIfExecuting(this, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ExecutionCommand> pendingPluginExecutionCommands = new ArrayList<>(mPendingPluginExecutionCommands);
|
List<ExecutionCommand> pendingPluginExecutionCommands = new ArrayList<>(mPendingPluginExecutionCommands);
|
||||||
for (int i = 0; i < pendingPluginExecutionCommands.size(); i++) {
|
for (int i = 0; i < pendingPluginExecutionCommands.size(); i++) {
|
||||||
ExecutionCommand executionCommand = pendingPluginExecutionCommands.get(i);
|
ExecutionCommand executionCommand = pendingPluginExecutionCommands.get(i);
|
||||||
if (!executionCommand.shouldNotProcessResults() && executionCommand.pluginPendingIntent != null) {
|
if (!executionCommand.shouldNotProcessResults() && executionCommand.isPluginExecutionCommandWithPendingResult()) {
|
||||||
if (executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_CANCELED, this.getString(com.termux.shared.R.string.error_execution_cancelled), null)) {
|
if (executionCommand.setStateFailed(Errno.ERRNO_CANCELLED.getCode(), this.getString(com.termux.shared.R.string.error_execution_cancelled))) {
|
||||||
PluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
|
PluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -354,20 +361,28 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||||||
|
|
||||||
if (executionCommand.executableUri != null) {
|
if (executionCommand.executableUri != null) {
|
||||||
executionCommand.executable = executionCommand.executableUri.getPath();
|
executionCommand.executable = executionCommand.executableUri.getPath();
|
||||||
executionCommand.arguments = intent.getStringArrayExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS);
|
executionCommand.arguments = IntentUtils.getStringArrayExtraIfSet(intent, TERMUX_SERVICE.EXTRA_ARGUMENTS, null);
|
||||||
if (executionCommand.inBackground)
|
if (executionCommand.inBackground)
|
||||||
executionCommand.stdin = intent.getStringExtra(TERMUX_SERVICE.EXTRA_STDIN);
|
executionCommand.stdin = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_STDIN, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
executionCommand.workingDirectory = intent.getStringExtra(TERMUX_SERVICE.EXTRA_WORKDIR);
|
executionCommand.workingDirectory = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_WORKDIR, null);
|
||||||
executionCommand.isFailsafe = intent.getBooleanExtra(TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION, false);
|
executionCommand.isFailsafe = intent.getBooleanExtra(TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION, false);
|
||||||
executionCommand.sessionAction = intent.getStringExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION);
|
executionCommand.sessionAction = intent.getStringExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION);
|
||||||
executionCommand.commandLabel = DataUtils.getDefaultIfNull(intent.getStringExtra(TERMUX_SERVICE.EXTRA_COMMAND_LABEL), "Execution Intent Command");
|
executionCommand.commandLabel = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_LABEL, "Execution Intent Command");
|
||||||
executionCommand.commandDescription = intent.getStringExtra(TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION);
|
executionCommand.commandDescription = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, null);
|
||||||
executionCommand.commandHelp = intent.getStringExtra(TERMUX_SERVICE.EXTRA_COMMAND_HELP);
|
executionCommand.commandHelp = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_HELP, null);
|
||||||
executionCommand.pluginAPIHelp = intent.getStringExtra(TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP);
|
executionCommand.pluginAPIHelp = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP, null);
|
||||||
executionCommand.isPluginExecutionCommand = true;
|
executionCommand.isPluginExecutionCommand = true;
|
||||||
executionCommand.pluginPendingIntent = intent.getParcelableExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT);
|
executionCommand.resultConfig.resultPendingIntent = intent.getParcelableExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT);
|
||||||
|
executionCommand.resultConfig.resultDirectoryPath = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_DIRECTORY, null);
|
||||||
|
if (executionCommand.resultConfig.resultDirectoryPath != null) {
|
||||||
|
executionCommand.resultConfig.resultSingleFile = intent.getBooleanExtra(TERMUX_SERVICE.EXTRA_RESULT_SINGLE_FILE, false);
|
||||||
|
executionCommand.resultConfig.resultFileBasename = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILE_BASENAME, null);
|
||||||
|
executionCommand.resultConfig.resultFileOutputFormat = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, null);
|
||||||
|
executionCommand.resultConfig.resultFileErrorFormat = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, null);
|
||||||
|
executionCommand.resultConfig.resultFilesSuffix = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILES_SUFFIX, null);
|
||||||
|
}
|
||||||
|
|
||||||
// Add the execution command to pending plugin execution commands list
|
// Add the execution command to pending plugin execution commands list
|
||||||
mPendingPluginExecutionCommands.add(executionCommand);
|
mPendingPluginExecutionCommands.add(executionCommand);
|
||||||
@@ -413,9 +428,14 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||||||
if (Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE)
|
if (Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE)
|
||||||
Logger.logVerbose(LOG_TAG, executionCommand.toString());
|
Logger.logVerbose(LOG_TAG, executionCommand.toString());
|
||||||
|
|
||||||
TermuxTask newTermuxTask = TermuxTask.execute(this, executionCommand, this, false);
|
TermuxTask newTermuxTask = TermuxTask.execute(this, executionCommand, this, new TermuxShellEnvironmentClient(), false);
|
||||||
if (newTermuxTask == null) {
|
if (newTermuxTask == null) {
|
||||||
Logger.logError(LOG_TAG, "Failed to execute new TermuxTask command for:\n" + executionCommand.getCommandIdAndLabelLogString());
|
Logger.logError(LOG_TAG, "Failed to execute new TermuxTask command for:\n" + executionCommand.getCommandIdAndLabelLogString());
|
||||||
|
// If the execution command was started for a plugin, then process the error
|
||||||
|
if (executionCommand.isPluginExecutionCommand)
|
||||||
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||||
|
else
|
||||||
|
Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,9 +523,15 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||||||
// If the execution command was started for a plugin, only then will the stdout be set
|
// If the execution command was started for a plugin, only then will the stdout be set
|
||||||
// Otherwise if command was manually started by the user like by adding a new terminal session,
|
// Otherwise if command was manually started by the user like by adding a new terminal session,
|
||||||
// then no need to set stdout
|
// then no need to set stdout
|
||||||
TermuxSession newTermuxSession = TermuxSession.execute(this, executionCommand, getTermuxTerminalSessionClient(), this, sessionName, executionCommand.isPluginExecutionCommand);
|
executionCommand.terminalTranscriptRows = getTerminalTranscriptRows();
|
||||||
|
TermuxSession newTermuxSession = TermuxSession.execute(this, executionCommand, getTermuxTerminalSessionClient(), this, new TermuxShellEnvironmentClient(), sessionName, executionCommand.isPluginExecutionCommand);
|
||||||
if (newTermuxSession == null) {
|
if (newTermuxSession == null) {
|
||||||
Logger.logError(LOG_TAG, "Failed to execute new TermuxSession command for:\n" + executionCommand.getCommandIdAndLabelLogString());
|
Logger.logError(LOG_TAG, "Failed to execute new TermuxSession command for:\n" + executionCommand.getCommandIdAndLabelLogString());
|
||||||
|
// If the execution command was started for a plugin, then process the error
|
||||||
|
if (executionCommand.isPluginExecutionCommand)
|
||||||
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||||
|
else
|
||||||
|
Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -560,6 +586,19 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||||||
updateNotification();
|
updateNotification();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get the terminal transcript rows to be used for new {@link TermuxSession}. */
|
||||||
|
public Integer getTerminalTranscriptRows() {
|
||||||
|
if (mTerminalTranscriptRows == null)
|
||||||
|
setTerminalTranscriptRows();
|
||||||
|
return mTerminalTranscriptRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTerminalTranscriptRows() {
|
||||||
|
// TermuxService only uses this termux property currently, so no need to load them all into
|
||||||
|
// an internal values map like TermuxActivity does
|
||||||
|
mTerminalTranscriptRows = TermuxAppSharedProperties.getTerminalTranscriptRows(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -601,8 +640,13 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
|||||||
// For android >= 10, apps require Display over other apps permission to start foreground activities
|
// For android >= 10, apps require Display over other apps permission to start foreground activities
|
||||||
// from background (services). If it is not granted, then TermuxSessions that are started will
|
// from background (services). If it is not granted, then TermuxSessions that are started will
|
||||||
// show in Termux notification but will not run until user manually clicks the notification.
|
// show in Termux notification but will not run until user manually clicks the notification.
|
||||||
if (PermissionUtils.validateDisplayOverOtherAppsPermissionForPostAndroid10(this)) {
|
if (PermissionUtils.validateDisplayOverOtherAppsPermissionForPostAndroid10(this, true)) {
|
||||||
TermuxActivity.startTermuxActivity(this);
|
TermuxActivity.startTermuxActivity(this);
|
||||||
|
} else {
|
||||||
|
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(this);
|
||||||
|
if (preferences == null) return;
|
||||||
|
if (preferences.arePluginErrorNotificationsEnabled())
|
||||||
|
Logger.showToast(this, this.getString(R.string.error_display_over_other_apps_permission_not_granted), true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import android.webkit.WebViewClient;
|
|||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.RelativeLayout;
|
import android.widget.RelativeLayout;
|
||||||
|
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
|
||||||
/** Basic embedded browser for viewing help pages. */
|
/** Basic embedded browser for viewing help pages. */
|
||||||
public final class HelpActivity extends Activity {
|
public final class HelpActivity extends Activity {
|
||||||
|
|
||||||
@@ -39,7 +41,7 @@ public final class HelpActivity extends Activity {
|
|||||||
mWebView.setWebViewClient(new WebViewClient() {
|
mWebView.setWebViewClient(new WebViewClient() {
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||||
if (url.startsWith("https://wiki.termux.com")) {
|
if (url.equals(TermuxConstants.TERMUX_WIKI_URL) || url.startsWith(TermuxConstants.TERMUX_WIKI_URL + "/")) {
|
||||||
// Inline help.
|
// Inline help.
|
||||||
setContentView(progressLayout);
|
setContentView(progressLayout);
|
||||||
return false;
|
return false;
|
||||||
@@ -60,7 +62,7 @@ public final class HelpActivity extends Activity {
|
|||||||
setContentView(mWebView);
|
setContentView(mWebView);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
mWebView.loadUrl("https://wiki.termux.com/wiki/Main_Page");
|
mWebView.loadUrl(TermuxConstants.TERMUX_WIKI_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ import androidx.preference.Preference;
|
|||||||
import androidx.preference.PreferenceFragmentCompat;
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.app.models.ReportInfo;
|
import com.termux.shared.activities.ReportActivity;
|
||||||
|
import com.termux.shared.models.ReportInfo;
|
||||||
import com.termux.app.models.UserAction;
|
import com.termux.app.models.UserAction;
|
||||||
import com.termux.shared.interact.ShareUtils;
|
import com.termux.shared.interact.ShareUtils;
|
||||||
import com.termux.shared.packages.PackageUtils;
|
import com.termux.shared.packages.PackageUtils;
|
||||||
import com.termux.shared.settings.preferences.TermuxTaskerAppSharedPreferences;
|
import com.termux.shared.settings.preferences.TermuxTaskerAppSharedPreferences;
|
||||||
|
import com.termux.shared.termux.AndroidUtils;
|
||||||
import com.termux.shared.termux.TermuxConstants;
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
import com.termux.shared.termux.TermuxUtils;
|
import com.termux.shared.termux.TermuxUtils;
|
||||||
|
|
||||||
@@ -81,10 +83,10 @@ public class SettingsActivity extends AppCompatActivity {
|
|||||||
if (termuxPluginAppsInfo != null)
|
if (termuxPluginAppsInfo != null)
|
||||||
aboutString.append("\n\n").append(termuxPluginAppsInfo);
|
aboutString.append("\n\n").append(termuxPluginAppsInfo);
|
||||||
|
|
||||||
aboutString.append("\n\n").append(TermuxUtils.getDeviceInfoMarkdownString(context));
|
aboutString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context));
|
||||||
aboutString.append("\n\n").append(TermuxUtils.getImportantLinksMarkdownString(context));
|
aboutString.append("\n\n").append(TermuxUtils.getImportantLinksMarkdownString(context));
|
||||||
|
|
||||||
ReportActivity.startReportActivity(context, new ReportInfo(UserAction.ABOUT, TermuxConstants.TERMUX_APP.TERMUX_SETTINGS_ACTIVITY_NAME, title, null, aboutString.toString(), null, false));
|
ReportActivity.startReportActivity(context, new ReportInfo(UserAction.ABOUT.getName(), TermuxConstants.TERMUX_APP.TERMUX_SETTINGS_ACTIVITY_NAME, title, null, aboutString.toString(), null, false));
|
||||||
}
|
}
|
||||||
}.start();
|
}.start();
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import android.content.Context;
|
|||||||
import com.termux.app.terminal.io.KeyboardShortcut;
|
import com.termux.app.terminal.io.KeyboardShortcut;
|
||||||
import com.termux.app.terminal.io.extrakeys.ExtraKeysInfo;
|
import com.termux.app.terminal.io.extrakeys.ExtraKeysInfo;
|
||||||
import com.termux.shared.logger.Logger;
|
import com.termux.shared.logger.Logger;
|
||||||
import com.termux.shared.settings.properties.SharedPropertiesParser;
|
|
||||||
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||||
import com.termux.shared.settings.properties.TermuxSharedProperties;
|
import com.termux.shared.settings.properties.TermuxSharedProperties;
|
||||||
|
|
||||||
@@ -17,7 +16,7 @@ import java.util.Map;
|
|||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
public class TermuxAppSharedProperties extends TermuxSharedProperties implements SharedPropertiesParser {
|
public class TermuxAppSharedProperties extends TermuxSharedProperties {
|
||||||
|
|
||||||
private ExtraKeysInfo mExtraKeysInfo;
|
private ExtraKeysInfo mExtraKeysInfo;
|
||||||
private List<KeyboardShortcut> mSessionShortcuts = new ArrayList<>();
|
private List<KeyboardShortcut> mSessionShortcuts = new ArrayList<>();
|
||||||
@@ -96,4 +95,13 @@ public class TermuxAppSharedProperties extends TermuxSharedProperties implements
|
|||||||
return mExtraKeysInfo;
|
return mExtraKeysInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the {@link TermuxPropertyConstants#KEY_TERMINAL_TRANSCRIPT_ROWS} value from termux properties file on disk.
|
||||||
|
*/
|
||||||
|
public static int getTerminalTranscriptRows(Context context) {
|
||||||
|
return (int) TermuxSharedProperties.getInternalPropertyValue(context, TermuxPropertyConstants.KEY_TERMINAL_TRANSCRIPT_ROWS);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,284 @@
|
|||||||
|
package com.termux.app.terminal;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.inputmethodservice.InputMethodService;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.ViewTreeObserver;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.inputmethod.EditorInfo;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
|
|
||||||
|
import com.termux.app.TermuxActivity;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.view.ViewUtils;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link TermuxActivity} relies on {@link android.view.WindowManager.LayoutParams#SOFT_INPUT_ADJUST_RESIZE)}
|
||||||
|
* set by {@link TermuxTerminalViewClient#setSoftKeyboardState(boolean, boolean)} to automatically
|
||||||
|
* resize the view and push the terminal up when soft keyboard is opened. However, this does not
|
||||||
|
* always work properly. When `enforce-char-based-input=true` is set in `termux.properties`
|
||||||
|
* and {@link com.termux.view.TerminalView#onCreateInputConnection(EditorInfo)} sets the inputType
|
||||||
|
* to `InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS`
|
||||||
|
* instead of the default `InputType.TYPE_NULL` for termux, some keyboards may still show suggestions.
|
||||||
|
* Gboard does too, but only when text is copied and clipboard suggestions **and** number keys row
|
||||||
|
* toggles are enabled in its settings. When number keys row toggle is not enabled, Gboard will still
|
||||||
|
* show the row but will switch it with suggestions if needed. If its enabled, then number keys row
|
||||||
|
* is always shown and suggestions are shown in an additional row on top of it. This additional row is likely
|
||||||
|
* part of the candidates view returned by the keyboard app in {@link InputMethodService#onCreateCandidatesView()}.
|
||||||
|
*
|
||||||
|
* With the above configuration, the additional clipboard suggestions row partially covers the
|
||||||
|
* extra keys/terminal. Reopening the keyboard/activity does not fix the issue. This is either a bug
|
||||||
|
* in the Android OS where it does not consider the candidate's view height in its calculation to push
|
||||||
|
* up the view or because Gboard does not include the candidate's view height in the height reported
|
||||||
|
* to android that should be used, hence causing an overlap.
|
||||||
|
*
|
||||||
|
* Gboard logs the following entry to `logcat` when its opened with or without the suggestions bar showing:
|
||||||
|
* I/KeyboardViewUtil: KeyboardViewUtil.calculateMaxKeyboardBodyHeight():62 leave 500 height for app when screen height:2392, header height:176 and isFullscreenMode:false, so the max keyboard body height is:1716
|
||||||
|
* where `keyboard_height = screen_height - height_for_app - header_height` (62 is a hardcoded value in Gboard source code and may be a version number)
|
||||||
|
* So this may in fact be due to Gboard but https://stackoverflow.com/questions/57567272 suggests
|
||||||
|
* otherwise. Another similar report https://stackoverflow.com/questions/66761661.
|
||||||
|
* Also check https://github.com/termux/termux-app/issues/1539.
|
||||||
|
*
|
||||||
|
* This overlap may happen even without `enforce-char-based-input=true` for keyboards with extended layouts
|
||||||
|
* like number row, etc.
|
||||||
|
*
|
||||||
|
* To fix these issues, `activity_termux.xml` has the constant 1sp transparent
|
||||||
|
* `activity_termux_bottom_space_view` View at the bottom. This will appear as a line matching the
|
||||||
|
* activity theme. When {@link TermuxActivity} {@link ViewTreeObserver.OnGlobalLayoutListener} is
|
||||||
|
* called when any of the sub view layouts change, like keyboard opening/closing keyboard,
|
||||||
|
* extra keys/input view switched, etc, we check if the bottom space view is visible or not.
|
||||||
|
* If its not, then we add a margin to the bottom of the root view, so that the keyboard does not
|
||||||
|
* overlap the extra keys/terminal, since the margin will push up the view. By default the margin
|
||||||
|
* added is equal to the height of the hidden part of extra keys/terminal. For Gboard's case, the
|
||||||
|
* hidden part equals the `header_height`. The updates to margins may cause a jitter in some cases
|
||||||
|
* when the view is redrawn if the margin is incorrect, but logic has been implemented to avoid that.
|
||||||
|
*/
|
||||||
|
public class TermuxActivityRootView extends LinearLayout implements ViewTreeObserver.OnGlobalLayoutListener {
|
||||||
|
|
||||||
|
public TermuxActivity mActivity;
|
||||||
|
public Integer marginBottom;
|
||||||
|
public Integer lastMarginBottom;
|
||||||
|
public long lastMarginBottomTime;
|
||||||
|
public long lastMarginBottomExtraTime;
|
||||||
|
|
||||||
|
/** Log root view events. */
|
||||||
|
private boolean ROOT_VIEW_LOGGING_ENABLED = false;
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "TermuxActivityRootView";
|
||||||
|
|
||||||
|
private static int mStatusBarHeight;
|
||||||
|
|
||||||
|
public TermuxActivityRootView(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TermuxActivityRootView(Context context, @Nullable AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TermuxActivityRootView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActivity(TermuxActivity activity) {
|
||||||
|
mActivity = activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether root view logging is enabled or not.
|
||||||
|
*
|
||||||
|
* @param value The boolean value that defines the state.
|
||||||
|
*/
|
||||||
|
public void setIsRootViewLoggingEnabled(boolean value) {
|
||||||
|
ROOT_VIEW_LOGGING_ENABLED = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||||
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||||
|
|
||||||
|
if (marginBottom != null) {
|
||||||
|
if (ROOT_VIEW_LOGGING_ENABLED)
|
||||||
|
Logger.logVerbose(LOG_TAG, "onMeasure: Setting bottom margin to " + marginBottom);
|
||||||
|
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) getLayoutParams();
|
||||||
|
params.setMargins(0, 0, 0, marginBottom);
|
||||||
|
setLayoutParams(params);
|
||||||
|
marginBottom = null;
|
||||||
|
requestLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
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 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;
|
||||||
|
boolean isVisibleBecauseExtraMargin = ((bottomSpaceViewRect.bottom - windowAvailableRect.bottom) < 0);
|
||||||
|
|
||||||
|
if (root_view_logging_enabled) {
|
||||||
|
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 +
|
||||||
|
", isVisible " + windowAvailableRect.contains(bottomSpaceViewRect) + ", isRectAbove " + ViewUtils.isRectAbove(windowAvailableRect, bottomSpaceViewRect) +
|
||||||
|
", isVisibleBecauseMargin " + isVisibleBecauseMargin + ", isVisibleBecauseExtraMargin " + isVisibleBecauseExtraMargin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the bottomSpaceViewRect is visible, then remove the margin if needed
|
||||||
|
if (isVisible) {
|
||||||
|
// If visible because of margin, i.e the bottom of bottomSpaceViewRect equals that of windowAvailableRect
|
||||||
|
// and a margin has been added
|
||||||
|
// Necessary so that we don't get stuck in an infinite loop since setting margin
|
||||||
|
// will call OnGlobalLayoutListener again and next time bottom space view
|
||||||
|
// will be visible and margin will be set to 0, which again will call
|
||||||
|
// OnGlobalLayoutListener...
|
||||||
|
// Calling addTermuxActivityRootViewGlobalLayoutListener with a delay fails to
|
||||||
|
// 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ELse find the part of the extra keys/terminal that is hidden and add a margin accordingly
|
||||||
|
else {
|
||||||
|
int pxHidden = bottomSpaceViewRect.bottom - windowAvailableRect.bottom;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pxHidden < 0) {
|
||||||
|
if (root_view_logging_enabled)
|
||||||
|
Logger.logVerbose(LOG_TAG, "Force setting margin to 0 since new margin is negative");
|
||||||
|
pxHidden = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
} else {
|
||||||
|
if (root_view_logging_enabled)
|
||||||
|
Logger.logVerbose(LOG_TAG, "Bottom margin already equals " + pxHidden);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class WindowInsetsListener implements View.OnApplyWindowInsetsListener {
|
||||||
|
@Override
|
||||||
|
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
|
||||||
|
mStatusBarHeight = WindowInsetsCompat.toWindowInsetsCompat(insets).getInsets(WindowInsetsCompat.Type.statusBars()).top;
|
||||||
|
// Let view window handle insets however it wants
|
||||||
|
return v.onApplyWindowInsets(insets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ import android.widget.ListView;
|
|||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.shared.shell.TermuxSession;
|
import com.termux.shared.shell.TermuxSession;
|
||||||
import com.termux.shared.interact.DialogUtils;
|
import com.termux.shared.interact.TextInputDialogUtils;
|
||||||
import com.termux.app.TermuxActivity;
|
import com.termux.app.TermuxActivity;
|
||||||
import com.termux.shared.terminal.TermuxTerminalSessionClientBase;
|
import com.termux.shared.terminal.TermuxTerminalSessionClientBase;
|
||||||
import com.termux.shared.termux.TermuxConstants;
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
@@ -205,6 +205,22 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
|||||||
mActivity.getTerminalView().setTerminalCursorBlinkerState(enabled, false);
|
mActivity.getTerminalView().setTerminalCursorBlinkerState(enabled, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be called when mActivity.onResetTerminalSession() is called
|
||||||
|
*/
|
||||||
|
public void onResetTerminalSession() {
|
||||||
|
// Ensure blinker starts again after reset if cursor blinking was disabled before reset like
|
||||||
|
// with "tput civis" which would have called onTerminalCursorStateChange()
|
||||||
|
mActivity.getTerminalView().setTerminalCursorBlinkerState(true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer getTerminalCursorStyle() {
|
||||||
|
return mActivity.getProperties().getTerminalCursorStyle();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/** Initialize and get mBellSoundPool */
|
/** Initialize and get mBellSoundPool */
|
||||||
@@ -248,8 +264,10 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
|||||||
void notifyOfSessionChange() {
|
void notifyOfSessionChange() {
|
||||||
if (!mActivity.isVisible()) return;
|
if (!mActivity.isVisible()) return;
|
||||||
|
|
||||||
TerminalSession session = mActivity.getCurrentSession();
|
if (!mActivity.getProperties().areTerminalSessionChangeToastsDisabled()) {
|
||||||
mActivity.showToast(toToastTitle(session), false);
|
TerminalSession session = mActivity.getCurrentSession();
|
||||||
|
mActivity.showToast(toToastTitle(session), false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void switchToSession(boolean forward) {
|
public void switchToSession(boolean forward) {
|
||||||
@@ -283,7 +301,7 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
|||||||
public void renameSession(final TerminalSession sessionToRename) {
|
public void renameSession(final TerminalSession sessionToRename) {
|
||||||
if (sessionToRename == null) return;
|
if (sessionToRename == null) return;
|
||||||
|
|
||||||
DialogUtils.textInput(mActivity, R.string.title_rename_session, sessionToRename.mSessionName, R.string.action_rename_session_confirm, text -> {
|
TextInputDialogUtils.textInput(mActivity, R.string.title_rename_session, sessionToRename.mSessionName, R.string.action_rename_session_confirm, text -> {
|
||||||
sessionToRename.mSessionName = text;
|
sessionToRename.mSessionName = text;
|
||||||
termuxSessionListNotifyUpdated();
|
termuxSessionListNotifyUpdated();
|
||||||
}, -1, null, -1, null, null);
|
}, -1, null, -1, null, null);
|
||||||
|
|||||||
@@ -15,16 +15,19 @@ import android.view.InputDevice;
|
|||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.widget.EditText;
|
||||||
import android.widget.ListView;
|
import android.widget.ListView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.app.TermuxActivity;
|
import com.termux.app.TermuxActivity;
|
||||||
|
import com.termux.shared.data.UrlUtils;
|
||||||
import com.termux.shared.shell.ShellUtils;
|
import com.termux.shared.shell.ShellUtils;
|
||||||
import com.termux.shared.terminal.TermuxTerminalViewClientBase;
|
import com.termux.shared.terminal.TermuxTerminalViewClientBase;
|
||||||
|
import com.termux.shared.termux.AndroidUtils;
|
||||||
import com.termux.shared.termux.TermuxConstants;
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
import com.termux.app.activities.ReportActivity;
|
import com.termux.shared.activities.ReportActivity;
|
||||||
import com.termux.app.models.ReportInfo;
|
import com.termux.shared.models.ReportInfo;
|
||||||
import com.termux.app.models.UserAction;
|
import com.termux.app.models.UserAction;
|
||||||
import com.termux.app.terminal.io.KeyboardShortcut;
|
import com.termux.app.terminal.io.KeyboardShortcut;
|
||||||
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
|
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
|
||||||
@@ -34,6 +37,7 @@ import com.termux.shared.logger.Logger;
|
|||||||
import com.termux.shared.markdown.MarkdownUtils;
|
import com.termux.shared.markdown.MarkdownUtils;
|
||||||
import com.termux.shared.termux.TermuxUtils;
|
import com.termux.shared.termux.TermuxUtils;
|
||||||
import com.termux.shared.view.KeyboardUtils;
|
import com.termux.shared.view.KeyboardUtils;
|
||||||
|
import com.termux.shared.view.ViewUtils;
|
||||||
import com.termux.terminal.KeyHandler;
|
import com.termux.terminal.KeyHandler;
|
||||||
import com.termux.terminal.TerminalEmulator;
|
import com.termux.terminal.TerminalEmulator;
|
||||||
import com.termux.terminal.TerminalSession;
|
import com.termux.terminal.TerminalSession;
|
||||||
@@ -56,6 +60,11 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
|
|
||||||
private Runnable mShowSoftKeyboardRunnable;
|
private Runnable mShowSoftKeyboardRunnable;
|
||||||
|
|
||||||
|
private boolean mShowSoftKeyboardIgnoreOnce;
|
||||||
|
private boolean mShowSoftKeyboardWithDelayOnce;
|
||||||
|
|
||||||
|
private boolean mTerminalCursorBlinkerStateAlreadySet;
|
||||||
|
|
||||||
private static final String LOG_TAG = "TermuxTerminalViewClient";
|
private static final String LOG_TAG = "TermuxTerminalViewClient";
|
||||||
|
|
||||||
public TermuxTerminalViewClient(TermuxActivity activity, TermuxTerminalSessionClient termuxTerminalSessionClient) {
|
public TermuxTerminalViewClient(TermuxActivity activity, TermuxTerminalSessionClient termuxTerminalSessionClient) {
|
||||||
@@ -75,10 +84,14 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
* Should be called when mActivity.onStart() is called
|
* Should be called when mActivity.onStart() is called
|
||||||
*/
|
*/
|
||||||
public void onStart() {
|
public void onStart() {
|
||||||
|
|
||||||
// Set {@link TerminalView#TERMINAL_VIEW_KEY_LOGGING_ENABLED} value
|
// Set {@link TerminalView#TERMINAL_VIEW_KEY_LOGGING_ENABLED} value
|
||||||
// Also required if user changed the preference from {@link TermuxSettings} activity and returns
|
// Also required if user changed the preference from {@link TermuxSettings} activity and returns
|
||||||
mActivity.getTerminalView().setIsTerminalViewKeyLoggingEnabled(mActivity.getPreferences().isTerminalViewKeyLoggingEnabled());
|
boolean isTerminalViewKeyLoggingEnabled = mActivity.getPreferences().isTerminalViewKeyLoggingEnabled();
|
||||||
|
mActivity.getTerminalView().setIsTerminalViewKeyLoggingEnabled(isTerminalViewKeyLoggingEnabled);
|
||||||
|
|
||||||
|
// Piggyback on the terminal view key logging toggle for now, should add a separate toggle in future
|
||||||
|
mActivity.getTermuxActivityRootView().setIsRootViewLoggingEnabled(isTerminalViewKeyLoggingEnabled);
|
||||||
|
ViewUtils.setIsViewUtilsLoggingEnabled(isTerminalViewKeyLoggingEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -88,8 +101,16 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
// Show the soft keyboard if required
|
// Show the soft keyboard if required
|
||||||
setSoftKeyboardState(true, false);
|
setSoftKeyboardState(true, false);
|
||||||
|
|
||||||
// Start terminal cursor blinking if enabled
|
mTerminalCursorBlinkerStateAlreadySet = false;
|
||||||
setTerminalCursorBlinkerState(true);
|
|
||||||
|
if (mActivity.getTerminalView().mEmulator != null) {
|
||||||
|
// Start terminal cursor blinking if enabled
|
||||||
|
// If emulator is already set, then start blinker now, otherwise wait for onEmulatorSet()
|
||||||
|
// event to start it. This is needed since onEmulatorSet() may not be called after
|
||||||
|
// TermuxActivity is started after device display timeout with double tap and not power button.
|
||||||
|
setTerminalCursorBlinkerState(true);
|
||||||
|
mTerminalCursorBlinkerStateAlreadySet = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -111,6 +132,23 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
setTerminalCursorBlinkerState(true);
|
setTerminalCursorBlinkerState(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be called when {@link com.termux.view.TerminalView#mEmulator}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onEmulatorSet() {
|
||||||
|
if (!mTerminalCursorBlinkerStateAlreadySet) {
|
||||||
|
// Start terminal cursor blinking if enabled
|
||||||
|
// We need to wait for the first session to be attached that's set in
|
||||||
|
// TermuxActivity.onServiceConnected() and then the multiple calls to TerminalView.updateSize()
|
||||||
|
// where the final one eventually sets the mEmulator when width/height is not 0. Otherwise
|
||||||
|
// blinker will not start again if TermuxActivity is started again after exiting it with
|
||||||
|
// double back press. Check TerminalView.setTerminalCursorBlinkerState().
|
||||||
|
setTerminalCursorBlinkerState(true);
|
||||||
|
mTerminalCursorBlinkerStateAlreadySet = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -211,6 +249,13 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onKeyUp(int keyCode, KeyEvent e) {
|
public boolean onKeyUp(int keyCode, KeyEvent e) {
|
||||||
|
// If emulator is not set, like if bootstrap installation failed and user dismissed the error
|
||||||
|
// dialog, then just exit the activity, otherwise they will be stuck in a broken state.
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_BACK && mActivity.getTerminalView().mEmulator == null) {
|
||||||
|
mActivity.finishActivityIfNotFinishing();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return handleVirtualKeys(keyCode, e, false);
|
return handleVirtualKeys(keyCode, e, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,10 +454,19 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
mActivity.getPreferences().setSoftKeyboardEnabled(false);
|
mActivity.getPreferences().setSoftKeyboardEnabled(false);
|
||||||
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
|
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||||
} else {
|
} else {
|
||||||
|
// Show with a delay, otherwise pressing keyboard toggle won't show the keyboard after
|
||||||
|
// switching back from another app if keyboard was previously disabled by user.
|
||||||
|
// Also request focus, since it wouldn't have been requested at startup by
|
||||||
|
// setSoftKeyboardState if keyboard was disabled. #2112
|
||||||
Logger.logVerbose(LOG_TAG, "Enabling soft keyboard on toggle");
|
Logger.logVerbose(LOG_TAG, "Enabling soft keyboard on toggle");
|
||||||
mActivity.getPreferences().setSoftKeyboardEnabled(true);
|
mActivity.getPreferences().setSoftKeyboardEnabled(true);
|
||||||
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
|
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
|
||||||
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
|
if(mShowSoftKeyboardWithDelayOnce) {
|
||||||
|
mShowSoftKeyboardWithDelayOnce = false;
|
||||||
|
mActivity.getTerminalView().postDelayed(getShowSoftKeyboardRunnable(), 500);
|
||||||
|
mActivity.getTerminalView().requestFocus();
|
||||||
|
} else
|
||||||
|
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If soft keyboard toggle behaviour is show/hide
|
// If soft keyboard toggle behaviour is show/hide
|
||||||
@@ -430,15 +484,23 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setSoftKeyboardState(boolean isStartup, boolean isReloadTermuxProperties) {
|
public void setSoftKeyboardState(boolean isStartup, boolean isReloadTermuxProperties) {
|
||||||
|
boolean noRequestFocus = false;
|
||||||
|
|
||||||
// If soft keyboard is disabled by user for Termux (check function docs for Termux behaviour info)
|
// If soft keyboard is disabled by user for Termux (check function docs for Termux behaviour info)
|
||||||
if (KeyboardUtils.shouldSoftKeyboardBeDisabled(mActivity,
|
if (KeyboardUtils.shouldSoftKeyboardBeDisabled(mActivity,
|
||||||
mActivity.getPreferences().isSoftKeyboardEnabled(),
|
mActivity.getPreferences().isSoftKeyboardEnabled(),
|
||||||
mActivity.getPreferences().isSoftKeyboardEnabledOnlyIfNoHardware())) {
|
mActivity.getPreferences().isSoftKeyboardEnabledOnlyIfNoHardware())) {
|
||||||
Logger.logVerbose(LOG_TAG, "Maintaining disabled soft keyboard");
|
Logger.logVerbose(LOG_TAG, "Maintaining disabled soft keyboard");
|
||||||
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
|
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||||
|
noRequestFocus = true;
|
||||||
|
// Delay is only required if onCreate() is called like when Termux app is exited with
|
||||||
|
// double back press, not when Termux app is switched back from another app and keyboard
|
||||||
|
// toggle is pressed to enable keyboard
|
||||||
|
if (isStartup && mActivity.isOnResumeAfterOnCreate())
|
||||||
|
mShowSoftKeyboardWithDelayOnce = true;
|
||||||
} else {
|
} else {
|
||||||
// Set flag to automatically push up TerminalView when keyboard is opened instead of showing over it
|
// Set flag to automatically push up TerminalView when keyboard is opened instead of showing over it
|
||||||
KeyboardUtils.setResizeTerminalViewForSoftKeyboardFlags(mActivity);
|
KeyboardUtils.setSoftInputModeAdjustResize(mActivity);
|
||||||
|
|
||||||
// Clear any previous flags to disable soft keyboard in case setting updated
|
// Clear any previous flags to disable soft keyboard in case setting updated
|
||||||
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
|
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
|
||||||
@@ -449,30 +511,55 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
KeyboardUtils.hideSoftKeyboard(mActivity, mActivity.getTerminalView());
|
KeyboardUtils.hideSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||||
// Required to keep keyboard hidden when Termux app is switched back from another app
|
// Required to keep keyboard hidden when Termux app is switched back from another app
|
||||||
KeyboardUtils.setSoftKeyboardAlwaysHiddenFlags(mActivity);
|
KeyboardUtils.setSoftKeyboardAlwaysHiddenFlags(mActivity);
|
||||||
} else {
|
noRequestFocus = true;
|
||||||
// Do not force show soft keyboard if termux-reload-settings command was run with hardware keyboard
|
// Required to keep keyboard hidden on app startup
|
||||||
if (isReloadTermuxProperties)
|
mShowSoftKeyboardIgnoreOnce = true;
|
||||||
return;
|
|
||||||
|
|
||||||
if (mShowSoftKeyboardRunnable == null) {
|
|
||||||
mShowSoftKeyboardRunnable = () -> {
|
|
||||||
Logger.logVerbose(LOG_TAG, "Showing soft keyboard on focus change");
|
|
||||||
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
mActivity.getTerminalView().setOnFocusChangeListener(new View.OnFocusChangeListener() {
|
|
||||||
@Override
|
|
||||||
public void onFocusChange(View view, boolean hasFocus) {
|
|
||||||
// Force show soft keyboard if TerminalView has focus and close it if it doesn't
|
|
||||||
KeyboardUtils.setSoftKeyboardVisibility(mShowSoftKeyboardRunnable, mActivity, mActivity.getTerminalView(), hasFocus);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Request focus for TerminalView
|
|
||||||
mActivity.getTerminalView().requestFocus();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mActivity.getTerminalView().setOnFocusChangeListener(new View.OnFocusChangeListener() {
|
||||||
|
@Override
|
||||||
|
public void onFocusChange(View view, boolean hasFocus) {
|
||||||
|
// Force show soft keyboard if TerminalView or toolbar text input view has
|
||||||
|
// focus and close it if they don't
|
||||||
|
boolean textInputViewHasFocus = false;
|
||||||
|
final EditText textInputView = mActivity.findViewById(R.id.terminal_toolbar_text_input);
|
||||||
|
if (textInputView != null) textInputViewHasFocus = textInputView.hasFocus();
|
||||||
|
|
||||||
|
if (hasFocus || textInputViewHasFocus) {
|
||||||
|
if (mShowSoftKeyboardIgnoreOnce) {
|
||||||
|
mShowSoftKeyboardIgnoreOnce = false; return;
|
||||||
|
}
|
||||||
|
Logger.logVerbose(LOG_TAG, "Showing soft keyboard on focus change");
|
||||||
|
} else {
|
||||||
|
Logger.logVerbose(LOG_TAG, "Hiding soft keyboard on focus change");
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyboardUtils.setSoftKeyboardVisibility(getShowSoftKeyboardRunnable(), mActivity, mActivity.getTerminalView(), hasFocus || textInputViewHasFocus);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Do not force show soft keyboard if termux-reload-settings command was run with hardware keyboard
|
||||||
|
// or soft keyboard is to be hidden or is disabled
|
||||||
|
if (!isReloadTermuxProperties && !noRequestFocus) {
|
||||||
|
// Request focus for TerminalView
|
||||||
|
// Also show the keyboard, since onFocusChange will not be called if TerminalView already
|
||||||
|
// had focus on startup to show the keyboard, like when opening url with context menu
|
||||||
|
// "Select URL" long press and returning to Termux app with back button. This
|
||||||
|
// will also show keyboard even if it was closed before opening url. #2111
|
||||||
|
Logger.logVerbose(LOG_TAG, "Requesting TerminalView focus and showing soft keyboard");
|
||||||
|
mActivity.getTerminalView().requestFocus();
|
||||||
|
mActivity.getTerminalView().postDelayed(getShowSoftKeyboardRunnable(), 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Runnable getShowSoftKeyboardRunnable() {
|
||||||
|
if (mShowSoftKeyboardRunnable == null) {
|
||||||
|
mShowSoftKeyboardRunnable = () -> {
|
||||||
|
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return mShowSoftKeyboardRunnable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -518,7 +605,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
|
|
||||||
String text = ShellUtils.getTerminalSessionTranscriptText(session, true, true);
|
String text = ShellUtils.getTerminalSessionTranscriptText(session, true, true);
|
||||||
|
|
||||||
LinkedHashSet<CharSequence> urlSet = DataUtils.extractUrls(text);
|
LinkedHashSet<CharSequence> urlSet = UrlUtils.extractUrls(text);
|
||||||
if (urlSet.isEmpty()) {
|
if (urlSet.isEmpty()) {
|
||||||
new AlertDialog.Builder(mActivity).setMessage(R.string.title_select_url_none_found).show();
|
new AlertDialog.Builder(mActivity).setMessage(R.string.title_select_url_none_found).show();
|
||||||
return;
|
return;
|
||||||
@@ -578,13 +665,13 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
reportString.append("\n").append(MarkdownUtils.getMarkdownCodeForString(transcriptTextTruncated, true));
|
reportString.append("\n").append(MarkdownUtils.getMarkdownCodeForString(transcriptTextTruncated, true));
|
||||||
|
|
||||||
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, true));
|
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, true));
|
||||||
reportString.append("\n\n").append(TermuxUtils.getDeviceInfoMarkdownString(mActivity));
|
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(mActivity));
|
||||||
|
|
||||||
String termuxAptInfo = TermuxUtils.geAPTInfoMarkdownString(mActivity);
|
String termuxAptInfo = TermuxUtils.geAPTInfoMarkdownString(mActivity);
|
||||||
if (termuxAptInfo != null)
|
if (termuxAptInfo != null)
|
||||||
reportString.append("\n\n").append(termuxAptInfo);
|
reportString.append("\n\n").append(termuxAptInfo);
|
||||||
|
|
||||||
ReportActivity.startReportActivity(mActivity, new ReportInfo(UserAction.REPORT_ISSUE_FROM_TRANSCRIPT, TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY_NAME, title, null, reportString.toString(), "\n\n" + TermuxUtils.getReportIssueMarkdownString(mActivity), false));
|
ReportActivity.startReportActivity(mActivity, new ReportInfo(UserAction.REPORT_ISSUE_FROM_TRANSCRIPT.getName(), TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY_NAME, title, null, reportString.toString(), "\n\n" + TermuxUtils.getReportIssueMarkdownString(mActivity), false));
|
||||||
}
|
}
|
||||||
}.start();
|
}.start();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ import android.content.Intent;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.app.activities.ReportActivity;
|
import com.termux.shared.activities.ReportActivity;
|
||||||
|
import com.termux.shared.models.errors.Error;
|
||||||
import com.termux.shared.notification.NotificationUtils;
|
import com.termux.shared.notification.NotificationUtils;
|
||||||
import com.termux.shared.file.FileUtils;
|
import com.termux.shared.file.FileUtils;
|
||||||
import com.termux.app.models.ReportInfo;
|
import com.termux.shared.models.ReportInfo;
|
||||||
import com.termux.app.models.UserAction;
|
import com.termux.app.models.UserAction;
|
||||||
|
import com.termux.shared.notification.TermuxNotificationUtils;
|
||||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||||
import com.termux.shared.settings.preferences.TermuxPreferenceConstants;
|
import com.termux.shared.settings.preferences.TermuxPreferenceConstants;
|
||||||
import com.termux.shared.data.DataUtils;
|
import com.termux.shared.data.DataUtils;
|
||||||
@@ -29,8 +31,8 @@ public class CrashUtils {
|
|||||||
private static final String LOG_TAG = "CrashUtils";
|
private static final String LOG_TAG = "CrashUtils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notify the user of a previous app crash by reading the crash info from the crash log file at
|
* Notify the user of an app crash at last run by reading the crash info from the crash log file
|
||||||
* {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}. The crash log file would have been
|
* at {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}. The crash log file would have been
|
||||||
* created by {@link com.termux.shared.crash.CrashHandler}.
|
* created by {@link com.termux.shared.crash.CrashHandler}.
|
||||||
*
|
*
|
||||||
* If the crash log file exists and is not empty and
|
* If the crash log file exists and is not empty and
|
||||||
@@ -43,10 +45,9 @@ public class CrashUtils {
|
|||||||
* @param context The {@link Context} for operations.
|
* @param context The {@link Context} for operations.
|
||||||
* @param logTagParam The log tag to use for logging.
|
* @param logTagParam The log tag to use for logging.
|
||||||
*/
|
*/
|
||||||
public static void notifyCrash(final Context context, final String logTagParam) {
|
public static void notifyAppCrashOnLastRun(final Context context, final String logTagParam) {
|
||||||
if (context == null) return;
|
if (context == null) return;
|
||||||
|
|
||||||
|
|
||||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
||||||
if (preferences == null) return;
|
if (preferences == null) return;
|
||||||
|
|
||||||
@@ -62,52 +63,81 @@ public class CrashUtils {
|
|||||||
if (!FileUtils.regularFileExists(TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, false))
|
if (!FileUtils.regularFileExists(TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, false))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
String errmsg;
|
Error error;
|
||||||
StringBuilder reportStringBuilder = new StringBuilder();
|
StringBuilder reportStringBuilder = new StringBuilder();
|
||||||
|
|
||||||
// Read report string from crash log file
|
// Read report string from crash log file
|
||||||
errmsg = FileUtils.readStringFromFile(context, "crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, Charset.defaultCharset(), reportStringBuilder, false);
|
error = FileUtils.readStringFromFile("crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, Charset.defaultCharset(), reportStringBuilder, false);
|
||||||
if (errmsg != null) {
|
if (error != null) {
|
||||||
Logger.logError(logTag, errmsg);
|
Logger.logErrorExtended(logTag, error.toString());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move crash log file to backup location if it exists
|
// Move crash log file to backup location if it exists
|
||||||
FileUtils.moveRegularFile(context, "crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, TermuxConstants.TERMUX_CRASH_LOG_BACKUP_FILE_PATH, true);
|
error = FileUtils.moveRegularFile("crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, TermuxConstants.TERMUX_CRASH_LOG_BACKUP_FILE_PATH, true);
|
||||||
if (errmsg != null) {
|
if (error != null) {
|
||||||
Logger.logError(logTag, errmsg);
|
Logger.logErrorExtended(logTag, error.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
String reportString = reportStringBuilder.toString();
|
String reportString = reportStringBuilder.toString();
|
||||||
|
|
||||||
if (reportString == null || reportString.isEmpty())
|
if (reportString.isEmpty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Send a notification to show the crash log which when clicked will open the {@link ReportActivity}
|
Logger.logDebug(logTag, "A crash log file found at \"" + TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH + "\".");
|
||||||
// to show the details of the crash
|
|
||||||
String title = TermuxConstants.TERMUX_APP_NAME + " Crash Report";
|
|
||||||
|
|
||||||
Logger.logDebug(logTag, "The crash log file at \"" + TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH + "\" found. Sending \"" + title + "\" notification.");
|
sendCrashReportNotification(context, logTag, reportString, false);
|
||||||
|
|
||||||
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.CRASH_REPORT, logTag, title, null, reportString, "\n\n" + TermuxUtils.getReportIssueMarkdownString(context), true));
|
|
||||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
|
||||||
|
|
||||||
// Setup the notification channel if not already set up
|
|
||||||
setupCrashReportsNotificationChannel(context);
|
|
||||||
|
|
||||||
// Build the notification
|
|
||||||
Notification.Builder builder = getCrashReportsNotificationBuilder(context, title, null, null, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
|
|
||||||
if (builder == null) return;
|
|
||||||
|
|
||||||
// Send the notification
|
|
||||||
int nextNotificationId = NotificationUtils.getNextNotificationId(context);
|
|
||||||
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
|
|
||||||
if (notificationManager != null)
|
|
||||||
notificationManager.notify(nextNotificationId, builder.build());
|
|
||||||
}
|
}
|
||||||
}.start();
|
}.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a crash report notification for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID}
|
||||||
|
* and {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} for operations.
|
||||||
|
* @param logTag The log tag to use for logging.
|
||||||
|
* @param reportString The text 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}.
|
||||||
|
*/
|
||||||
|
public static void sendCrashReportNotification(final Context context, String logTag, String reportString, boolean forceNotification) {
|
||||||
|
if (context == null) return;
|
||||||
|
|
||||||
|
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
||||||
|
if (preferences == null) return;
|
||||||
|
|
||||||
|
// If user has disabled notifications for crashes
|
||||||
|
if (!preferences.areCrashReportNotificationsEnabled() && !forceNotification)
|
||||||
|
return;
|
||||||
|
|
||||||
|
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||||
|
|
||||||
|
// Send a notification to show the crash log which when clicked will open the {@link ReportActivity}
|
||||||
|
// to show the details of the crash
|
||||||
|
String title = TermuxConstants.TERMUX_APP_NAME + " Crash Report";
|
||||||
|
|
||||||
|
Logger.logDebug(logTag, "Sending \"" + title + "\" notification.");
|
||||||
|
|
||||||
|
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.CRASH_REPORT.getName(), logTag, title, null, reportString, "\n\n" + TermuxUtils.getReportIssueMarkdownString(context), true));
|
||||||
|
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
|
||||||
|
// Setup the notification channel if not already set up
|
||||||
|
setupCrashReportsNotificationChannel(context);
|
||||||
|
|
||||||
|
// Build the notification
|
||||||
|
Notification.Builder builder = getCrashReportsNotificationBuilder(context, title, null, null, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
|
||||||
|
if (builder == null) return;
|
||||||
|
|
||||||
|
// Send the notification
|
||||||
|
int nextNotificationId = TermuxNotificationUtils.getNextNotificationId(context);
|
||||||
|
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
|
||||||
|
if (notificationManager != null)
|
||||||
|
notificationManager.notify(nextNotificationId, builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get {@link Notification.Builder} for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID}
|
* Get {@link Notification.Builder} for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID}
|
||||||
* and {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}.
|
* and {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}.
|
||||||
|
|||||||
@@ -1,26 +1,33 @@
|
|||||||
package com.termux.app.utils;
|
package com.termux.app.utils;
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.Notification;
|
import android.app.Notification;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
|
import com.termux.shared.activities.ReportActivity;
|
||||||
|
import com.termux.shared.file.TermuxFileUtils;
|
||||||
|
import com.termux.shared.models.ResultConfig;
|
||||||
|
import com.termux.shared.models.ResultData;
|
||||||
|
import com.termux.shared.models.errors.Errno;
|
||||||
|
import com.termux.shared.models.errors.Error;
|
||||||
import com.termux.shared.notification.NotificationUtils;
|
import com.termux.shared.notification.NotificationUtils;
|
||||||
|
import com.termux.shared.notification.TermuxNotificationUtils;
|
||||||
|
import com.termux.shared.shell.ResultSender;
|
||||||
|
import com.termux.shared.shell.ShellUtils;
|
||||||
|
import com.termux.shared.termux.AndroidUtils;
|
||||||
import com.termux.shared.termux.TermuxConstants;
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||||
import com.termux.app.activities.ReportActivity;
|
|
||||||
import com.termux.shared.logger.Logger;
|
import com.termux.shared.logger.Logger;
|
||||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||||
import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_APP;
|
import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_APP;
|
||||||
import com.termux.shared.settings.properties.SharedProperties;
|
import com.termux.shared.settings.properties.SharedProperties;
|
||||||
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||||
import com.termux.app.models.ReportInfo;
|
import com.termux.shared.models.ReportInfo;
|
||||||
import com.termux.shared.models.ExecutionCommand;
|
import com.termux.shared.models.ExecutionCommand;
|
||||||
import com.termux.app.models.UserAction;
|
import com.termux.app.models.UserAction;
|
||||||
import com.termux.shared.data.DataUtils;
|
import com.termux.shared.data.DataUtils;
|
||||||
@@ -29,12 +36,6 @@ import com.termux.shared.termux.TermuxUtils;
|
|||||||
|
|
||||||
public class PluginUtils {
|
public class PluginUtils {
|
||||||
|
|
||||||
/** Required file permissions for the executable file of execute intent. Executable file must have read and execute permissions */
|
|
||||||
public static final String PLUGIN_EXECUTABLE_FILE_PERMISSIONS = "r-x"; // Default: "r-x"
|
|
||||||
/** Required file permissions for the working directory of execute intent. Working directory must have read and write permissions.
|
|
||||||
* Execute permissions should be attempted to be set, but ignored if they are missing */
|
|
||||||
public static final String PLUGIN_WORKING_DIRECTORY_PERMISSIONS = "rwx"; // Default: "rwx"
|
|
||||||
|
|
||||||
private static final String LOG_TAG = "PluginUtils";
|
private static final String LOG_TAG = "PluginUtils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,8 +44,8 @@ public class PluginUtils {
|
|||||||
* The ExecutionCommand currentState must be greater or equal to
|
* The ExecutionCommand currentState must be greater or equal to
|
||||||
* {@link ExecutionCommand.ExecutionState#EXECUTED}.
|
* {@link ExecutionCommand.ExecutionState#EXECUTED}.
|
||||||
* If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and
|
* If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and
|
||||||
* {@link ExecutionCommand#pluginPendingIntent} is not {@code null}, then the result of commands
|
* {@link ResultConfig#resultPendingIntent} or {@link ResultConfig#resultDirectoryPath}
|
||||||
* are sent back to the {@link PendingIntent} creator.
|
* is not {@code null}, then the result of commands are sent back to the command caller.
|
||||||
*
|
*
|
||||||
* @param context The {@link Context} that will be used to send result intent to the {@link PendingIntent} creator.
|
* @param context The {@link Context} that will be used to send result intent to the {@link PendingIntent} creator.
|
||||||
* @param logTag The log tag to use for logging.
|
* @param logTag The log tag to use for logging.
|
||||||
@@ -54,31 +55,42 @@ public class PluginUtils {
|
|||||||
if (executionCommand == null) return;
|
if (executionCommand == null) return;
|
||||||
|
|
||||||
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||||
|
Error error = null;
|
||||||
|
ResultData resultData = executionCommand.resultData;
|
||||||
|
|
||||||
if (!executionCommand.hasExecuted()) {
|
if (!executionCommand.hasExecuted()) {
|
||||||
Logger.logWarn(logTag, "Ignoring call to processPluginExecutionCommandResult() since the execution command state is not higher than the ExecutionState.EXECUTED");
|
Logger.logWarn(logTag, executionCommand.getCommandIdAndLabelLogString() + ": Ignoring call to processPluginExecutionCommandResult() since the execution command state is not higher than the ExecutionState.EXECUTED");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.logDebug(LOG_TAG, executionCommand.toString());
|
boolean isPluginExecutionCommandWithPendingResult = executionCommand.isPluginExecutionCommandWithPendingResult();
|
||||||
|
|
||||||
boolean result = true;
|
// Log the output. ResultData should not be logged if pending result since ResultSender will do it
|
||||||
|
Logger.logDebugExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, !isPluginExecutionCommandWithPendingResult));
|
||||||
|
|
||||||
// If isPluginExecutionCommand is true and pluginPendingIntent is not null, then
|
// If execution command was started by a plugin which expects the result back
|
||||||
// send pluginPendingIntent to its creator with the result
|
if (isPluginExecutionCommandWithPendingResult) {
|
||||||
if (executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null) {
|
// Set variables which will be used by sendCommandResultData to send back the result
|
||||||
String errmsg = executionCommand.errmsg;
|
if (executionCommand.resultConfig.resultPendingIntent != null)
|
||||||
|
setPluginResultPendingIntentVariables(executionCommand);
|
||||||
|
if (executionCommand.resultConfig.resultDirectoryPath != null)
|
||||||
|
setPluginResultDirectoryVariables(executionCommand);
|
||||||
|
|
||||||
//Combine errmsg and stacktraces
|
// Send result to caller
|
||||||
if (executionCommand.isStateFailed()) {
|
error = ResultSender.sendCommandResultData(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.resultConfig, executionCommand.resultData);
|
||||||
errmsg = Logger.getMessageAndStackTracesString(executionCommand.errmsg, executionCommand.throwableList);
|
if (error != null) {
|
||||||
|
// error will be added to existing Errors
|
||||||
|
resultData.setStateFailed(error);
|
||||||
|
Logger.logDebugExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, true));
|
||||||
|
|
||||||
|
// Flash and send notification for the error
|
||||||
|
Logger.showToast(context, ResultData.getErrorsListMinimalString(resultData), true);
|
||||||
|
sendPluginCommandErrorNotification(context, logTag, executionCommand, ResultData.getErrorsListMinimalString(resultData));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send pluginPendingIntent to its creator
|
|
||||||
result = sendPluginExecutionCommandResultPendingIntent(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.stdout, executionCommand.stderr, executionCommand.exitCode, executionCommand.errCode, errmsg, executionCommand.pluginPendingIntent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!executionCommand.isStateFailed() && result)
|
if (!executionCommand.isStateFailed() && error == null)
|
||||||
executionCommand.setState(ExecutionCommand.ExecutionState.SUCCESS);
|
executionCommand.setState(ExecutionCommand.ExecutionState.SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,14 +98,13 @@ public class PluginUtils {
|
|||||||
* Process {@link ExecutionCommand} error.
|
* Process {@link ExecutionCommand} error.
|
||||||
*
|
*
|
||||||
* The ExecutionCommand currentState must be equal to {@link ExecutionCommand.ExecutionState#FAILED}.
|
* The ExecutionCommand currentState must be equal to {@link ExecutionCommand.ExecutionState#FAILED}.
|
||||||
* The {@link ExecutionCommand#errCode} must have been set to a value greater than
|
* The {@link ResultData#getErrCode()} must have been set to a value greater than
|
||||||
* {@link ExecutionCommand#RESULT_CODE_OK}.
|
* {@link Errno#ERRNO_SUCCESS}.
|
||||||
* The {@link ExecutionCommand#errmsg} and any {@link ExecutionCommand#throwableList} must also
|
* The {@link ResultData#errorsList} must also be set with appropriate error info.
|
||||||
* be set with appropriate error info.
|
|
||||||
*
|
*
|
||||||
* If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and
|
* If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and
|
||||||
* {@link ExecutionCommand#pluginPendingIntent} is not {@code null}, then the errors of commands
|
* {@link ResultConfig#resultPendingIntent} or {@link ResultConfig#resultDirectoryPath}
|
||||||
* are sent back to the {@link PendingIntent} creator.
|
* is not {@code null}, then the errors of commands are sent back to the command caller.
|
||||||
*
|
*
|
||||||
* Otherwise if the {@link TERMUX_APP#KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED} is
|
* Otherwise if the {@link TERMUX_APP#KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED} is
|
||||||
* enabled, then a flash and a notification will be shown for the error as well
|
* enabled, then a flash and a notification will be shown for the error as well
|
||||||
@@ -112,44 +123,93 @@ public class PluginUtils {
|
|||||||
if (context == null || executionCommand == null) return;
|
if (context == null || executionCommand == null) return;
|
||||||
|
|
||||||
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||||
|
Error error;
|
||||||
|
ResultData resultData = executionCommand.resultData;
|
||||||
|
|
||||||
if (!executionCommand.isStateFailed()) {
|
if (!executionCommand.isStateFailed()) {
|
||||||
Logger.logWarn(logTag, "Ignoring call to processPluginExecutionCommandError() since the execution command is not in ExecutionState.FAILED");
|
Logger.logWarn(logTag, executionCommand.getCommandIdAndLabelLogString() + ": Ignoring call to processPluginExecutionCommandError() since the execution command is not in ExecutionState.FAILED");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log the error and any exception
|
boolean isPluginExecutionCommandWithPendingResult = executionCommand.isPluginExecutionCommandWithPendingResult();
|
||||||
Logger.logStackTracesWithMessage(logTag, "(" + executionCommand.errCode + ") " + executionCommand.errmsg, executionCommand.throwableList);
|
|
||||||
|
|
||||||
|
// Log the error and any exception. ResultData should not be logged if pending result since ResultSender will do it
|
||||||
|
Logger.logErrorExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, !isPluginExecutionCommandWithPendingResult));
|
||||||
|
|
||||||
// If isPluginExecutionCommand is true and pluginPendingIntent is not null, then
|
// If execution command was started by a plugin which expects the result back
|
||||||
// send pluginPendingIntent to its creator with the errors
|
if (isPluginExecutionCommandWithPendingResult) {
|
||||||
if (executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null) {
|
// Set variables which will be used by sendCommandResultData to send back the result
|
||||||
String errmsg = executionCommand.errmsg;
|
if (executionCommand.resultConfig.resultPendingIntent != null)
|
||||||
|
setPluginResultPendingIntentVariables(executionCommand);
|
||||||
|
if (executionCommand.resultConfig.resultDirectoryPath != null)
|
||||||
|
setPluginResultDirectoryVariables(executionCommand);
|
||||||
|
|
||||||
//Combine errmsg and stacktraces
|
// Send result to caller
|
||||||
if (executionCommand.isStateFailed()) {
|
error = ResultSender.sendCommandResultData(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.resultConfig, executionCommand.resultData);
|
||||||
errmsg = Logger.getMessageAndStackTracesString(executionCommand.errmsg, executionCommand.throwableList);
|
if (error != null) {
|
||||||
|
// error will be added to existing Errors
|
||||||
|
resultData.setStateFailed(error);
|
||||||
|
Logger.logErrorExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, true));
|
||||||
|
forceNotification = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendPluginExecutionCommandResultPendingIntent(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.stdout, executionCommand.stderr, executionCommand.exitCode, executionCommand.errCode, errmsg, executionCommand.pluginPendingIntent);
|
|
||||||
|
|
||||||
// No need to show notifications if a pending intent was sent, let the caller handle the result himself
|
// No need to show notifications if a pending intent was sent, let the caller handle the result himself
|
||||||
if (!forceNotification) return;
|
if (!forceNotification) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
||||||
if (preferences == null) return;
|
if (preferences == null) return;
|
||||||
|
|
||||||
// If user has disabled notifications for plugin, then just return
|
// If user has disabled notifications for plugin commands, then just return
|
||||||
if (!preferences.arePluginErrorNotificationsEnabled() && !forceNotification)
|
if (!preferences.arePluginErrorNotificationsEnabled() && !forceNotification)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Flash the errmsg
|
// Flash and send notification for the error
|
||||||
Logger.showToast(context, executionCommand.errmsg, true);
|
Logger.showToast(context, ResultData.getErrorsListMinimalString(resultData), true);
|
||||||
|
sendPluginCommandErrorNotification(context, logTag, executionCommand, ResultData.getErrorsListMinimalString(resultData));
|
||||||
|
|
||||||
// Send a notification to show the errmsg which when clicked will open the {@link ReportActivity}
|
}
|
||||||
|
|
||||||
|
/** Set variables which will be used by {@link ResultSender#sendCommandResultData(Context, String, String, ResultConfig, ResultData)}
|
||||||
|
* to send back the result via {@link ResultConfig#resultPendingIntent}. */
|
||||||
|
public static void setPluginResultPendingIntentVariables(ExecutionCommand executionCommand) {
|
||||||
|
ResultConfig resultConfig = executionCommand.resultConfig;
|
||||||
|
|
||||||
|
resultConfig.resultBundleKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE;
|
||||||
|
resultConfig.resultStdoutKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT;
|
||||||
|
resultConfig.resultStdoutOriginalLengthKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH;
|
||||||
|
resultConfig.resultStderrKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR;
|
||||||
|
resultConfig.resultStderrOriginalLengthKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH;
|
||||||
|
resultConfig.resultExitCodeKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE;
|
||||||
|
resultConfig.resultErrCodeKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERR;
|
||||||
|
resultConfig.resultErrmsgKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set variables which will be used by {@link ResultSender#sendCommandResultData(Context, String, String, ResultConfig, ResultData)}
|
||||||
|
* to send back the result by writing it to files in {@link ResultConfig#resultDirectoryPath}. */
|
||||||
|
public static void setPluginResultDirectoryVariables(ExecutionCommand executionCommand) {
|
||||||
|
ResultConfig resultConfig = executionCommand.resultConfig;
|
||||||
|
|
||||||
|
resultConfig.resultDirectoryPath = TermuxFileUtils.getCanonicalPath(resultConfig.resultDirectoryPath, null, true);
|
||||||
|
resultConfig.resultDirectoryAllowedParentPath = TermuxFileUtils.getMatchedAllowedTermuxWorkingDirectoryParentPathForPath(resultConfig.resultDirectoryPath);
|
||||||
|
|
||||||
|
// Set default resultFileBasename if resultSingleFile is true to `<executable_basename>-<timestamp>.log`
|
||||||
|
if (resultConfig.resultSingleFile && resultConfig.resultFileBasename == null)
|
||||||
|
resultConfig.resultFileBasename = ShellUtils.getExecutableBasename(executionCommand.executable) + "-" + AndroidUtils.getCurrentMilliSecondLocalTimeStamp() + ".log";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an error notification for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
|
||||||
|
* and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} for operations.
|
||||||
|
* @param executionCommand The {@link ExecutionCommand} that failed.
|
||||||
|
* @param notificationTextString The text of the notification.
|
||||||
|
*/
|
||||||
|
public static void sendPluginCommandErrorNotification(Context context, String logTag, ExecutionCommand executionCommand, String notificationTextString) {
|
||||||
|
// Send a notification to show the error which when clicked will open the ReportActivity
|
||||||
// to show the details of the error
|
// to show the details of the error
|
||||||
String title = TermuxConstants.TERMUX_APP_NAME + " Plugin Execution Command Error";
|
String title = TermuxConstants.TERMUX_APP_NAME + " Plugin Execution Command Error";
|
||||||
|
|
||||||
@@ -157,114 +217,29 @@ public class PluginUtils {
|
|||||||
|
|
||||||
reportString.append(ExecutionCommand.getExecutionCommandMarkdownString(executionCommand));
|
reportString.append(ExecutionCommand.getExecutionCommandMarkdownString(executionCommand));
|
||||||
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, true));
|
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, true));
|
||||||
reportString.append("\n\n").append(TermuxUtils.getDeviceInfoMarkdownString(context));
|
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context));
|
||||||
|
|
||||||
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.PLUGIN_EXECUTION_COMMAND, logTag, title, null, reportString.toString(), null,true));
|
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.PLUGIN_EXECUTION_COMMAND.getName(), logTag, title, null, reportString.toString(), null,true));
|
||||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
|
||||||
// Setup the notification channel if not already set up
|
// Setup the notification channel if not already set up
|
||||||
setupPluginCommandErrorsNotificationChannel(context);
|
setupPluginCommandErrorsNotificationChannel(context);
|
||||||
|
|
||||||
// Use markdown in notification
|
// Use markdown in notification
|
||||||
CharSequence notificationText = MarkdownUtils.getSpannedMarkdownText(context, executionCommand.errmsg);
|
CharSequence notificationTextCharSequence = MarkdownUtils.getSpannedMarkdownText(context, notificationTextString);
|
||||||
//CharSequence notificationText = executionCommand.errmsg;
|
//CharSequence notificationTextCharSequence = notificationTextString;
|
||||||
|
|
||||||
// Build the notification
|
// Build the notification
|
||||||
Notification.Builder builder = getPluginCommandErrorsNotificationBuilder(context, title, notificationText, notificationText, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
|
Notification.Builder builder = getPluginCommandErrorsNotificationBuilder(context, title, notificationTextCharSequence, notificationTextCharSequence, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
|
||||||
if (builder == null) return;
|
if (builder == null) return;
|
||||||
|
|
||||||
// Send the notification
|
// Send the notification
|
||||||
int nextNotificationId = NotificationUtils.getNextNotificationId(context);
|
int nextNotificationId = TermuxNotificationUtils.getNextNotificationId(context);
|
||||||
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
|
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
|
||||||
if (notificationManager != null)
|
if (notificationManager != null)
|
||||||
notificationManager.notify(nextNotificationId, builder.build());
|
notificationManager.notify(nextNotificationId, builder.build());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Send {@link ExecutionCommand} result {@link PendingIntent} in the
|
|
||||||
* {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE} bundle.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param context The {@link Context} that will be used to send result intent to the {@link PendingIntent} creator.
|
|
||||||
* @param logTag The log tag to use for logging.
|
|
||||||
* @param label The label of {@link ExecutionCommand}.
|
|
||||||
* @param stdout The stdout of {@link ExecutionCommand}.
|
|
||||||
* @param stderr The stderr of {@link ExecutionCommand}.
|
|
||||||
* @param exitCode The exitCode of {@link ExecutionCommand}.
|
|
||||||
* @param errCode The errCode of {@link ExecutionCommand}.
|
|
||||||
* @param errmsg The errmsg of {@link ExecutionCommand}.
|
|
||||||
* @param pluginPendingIntent The pluginPendingIntent of {@link ExecutionCommand}.
|
|
||||||
* @return Returns {@code true} if pluginPendingIntent was successfully send, otherwise [@code false}.
|
|
||||||
*/
|
|
||||||
public static boolean sendPluginExecutionCommandResultPendingIntent(Context context, String logTag, String label, String stdout, String stderr, Integer exitCode, Integer errCode, String errmsg, PendingIntent pluginPendingIntent) {
|
|
||||||
if (context == null || pluginPendingIntent == null) return false;
|
|
||||||
|
|
||||||
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
|
||||||
|
|
||||||
Logger.logDebug(logTag, "Sending execution result for Execution Command \"" + label + "\" to " + pluginPendingIntent.getCreatorPackage());
|
|
||||||
|
|
||||||
String truncatedStdout = null;
|
|
||||||
String truncatedStderr = null;
|
|
||||||
|
|
||||||
String stdoutOriginalLength = (stdout == null) ? null: String.valueOf(stdout.length());
|
|
||||||
String stderrOriginalLength = (stderr == null) ? null: String.valueOf(stderr.length());
|
|
||||||
|
|
||||||
// Truncate stdout and stdout to max TRANSACTION_SIZE_LIMIT_IN_BYTES
|
|
||||||
if (stderr == null || stderr.isEmpty()) {
|
|
||||||
truncatedStdout = DataUtils.getTruncatedCommandOutput(stdout, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false);
|
|
||||||
} else if (stdout == null || stdout.isEmpty()) {
|
|
||||||
truncatedStderr = DataUtils.getTruncatedCommandOutput(stderr, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false);
|
|
||||||
} else {
|
|
||||||
truncatedStdout = DataUtils.getTruncatedCommandOutput(stdout, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 2, false, false, false);
|
|
||||||
truncatedStderr = DataUtils.getTruncatedCommandOutput(stderr, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 2, false, false, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (truncatedStdout != null && truncatedStdout.length() < stdout.length()) {
|
|
||||||
Logger.logWarn(logTag, "Execution Result for Execution Command \"" + label + "\" stdout length truncated from " + stdoutOriginalLength + " to " + truncatedStdout.length());
|
|
||||||
stdout = truncatedStdout;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (truncatedStderr != null && truncatedStderr.length() < stderr.length()) {
|
|
||||||
Logger.logWarn(logTag, "Execution Result for Execution Command \"" + label + "\" stderr length truncated from " + stderrOriginalLength + " to " + truncatedStderr.length());
|
|
||||||
stderr = truncatedStderr;
|
|
||||||
}
|
|
||||||
|
|
||||||
String errmsgOriginalLength = (errmsg == null) ? null: String.valueOf(errmsg.length());
|
|
||||||
|
|
||||||
// Truncate errmsg to max TRANSACTION_SIZE_LIMIT_IN_BYTES / 4
|
|
||||||
// trim from end to preserve start of stacktraces
|
|
||||||
String truncatedErrmsg = DataUtils.getTruncatedCommandOutput(errmsg, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 4, true, false, false);
|
|
||||||
if (truncatedErrmsg != null && truncatedErrmsg.length() < errmsg.length()) {
|
|
||||||
Logger.logWarn(logTag, "Execution Result for Execution Command \"" + label + "\" errmsg length truncated from " + errmsgOriginalLength + " to " + truncatedErrmsg.length());
|
|
||||||
errmsg = truncatedErrmsg;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
final Bundle resultBundle = new Bundle();
|
|
||||||
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT, stdout);
|
|
||||||
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH, stdoutOriginalLength);
|
|
||||||
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR, stderr);
|
|
||||||
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH, stderrOriginalLength);
|
|
||||||
if (exitCode != null) resultBundle.putInt(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE, exitCode);
|
|
||||||
if (errCode != null) resultBundle.putInt(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERR, errCode);
|
|
||||||
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG, errmsg);
|
|
||||||
|
|
||||||
Intent resultIntent = new Intent();
|
|
||||||
resultIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE, resultBundle);
|
|
||||||
|
|
||||||
try {
|
|
||||||
pluginPendingIntent.send(context, Activity.RESULT_OK, resultIntent);
|
|
||||||
} catch (PendingIntent.CanceledException e) {
|
|
||||||
// The caller doesn't want the result? That's fine, just ignore
|
|
||||||
Logger.logDebug(logTag, "The Execution Command \"" + label + "\" creator " + pluginPendingIntent.getCreatorPackage() + " does not want the results anymore");
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get {@link Notification.Builder} for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
|
* Get {@link Notification.Builder} for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
|
||||||
* and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
|
* and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
|
||||||
@@ -318,7 +293,7 @@ public class PluginUtils {
|
|||||||
* Check if {@link TermuxConstants#PROP_ALLOW_EXTERNAL_APPS} property is not set to "true".
|
* Check if {@link TermuxConstants#PROP_ALLOW_EXTERNAL_APPS} property is not set to "true".
|
||||||
*
|
*
|
||||||
* @param context The {@link Context} to get error string.
|
* @param context The {@link Context} to get error string.
|
||||||
* @return Returns the {@code errmsg} if policy is violated, otherwise {@code null}.
|
* @return Returns the {@code error} if policy is violated, otherwise {@code null}.
|
||||||
*/
|
*/
|
||||||
public static String checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(final Context context) {
|
public static String checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(final Context context) {
|
||||||
String errmsg = null;
|
String errmsg = null;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import android.provider.OpenableColumns;
|
|||||||
import android.util.Patterns;
|
import android.util.Patterns;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.shared.interact.DialogUtils;
|
import com.termux.shared.interact.TextInputDialogUtils;
|
||||||
import com.termux.shared.termux.TermuxConstants;
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||||
import com.termux.app.TermuxService;
|
import com.termux.app.TermuxService;
|
||||||
@@ -118,7 +118,7 @@ public class TermuxFileReceiverActivity extends Activity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void promptNameAndSave(final InputStream in, final String attachmentFileName) {
|
void promptNameAndSave(final InputStream in, final String attachmentFileName) {
|
||||||
DialogUtils.textInput(this, R.string.title_file_received, attachmentFileName, R.string.action_file_received_edit, text -> {
|
TextInputDialogUtils.textInput(this, R.string.title_file_received, attachmentFileName, R.string.action_file_received_edit, text -> {
|
||||||
File outFile = saveStreamWithName(in, text);
|
File outFile = saveStreamWithName(in, text);
|
||||||
if (outFile == null) return;
|
if (outFile == null) return;
|
||||||
|
|
||||||
|
|||||||
@@ -1,80 +1,109 @@
|
|||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<com.termux.app.terminal.TermuxActivityRootView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/activity_termux_root_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:fitsSystemWindows="true">
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
<androidx.drawerlayout.widget.DrawerLayout
|
<RelativeLayout
|
||||||
android:id="@+id/drawer_layout"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_alignParentTop="true"
|
android:layout_height="0dp"
|
||||||
android:layout_above="@+id/terminal_toolbar_view_pager"
|
android:layout_weight="1"
|
||||||
android:layout_height="match_parent">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<com.termux.view.TerminalView
|
<androidx.drawerlayout.widget.DrawerLayout
|
||||||
android:id="@+id/terminal_view"
|
android:id="@+id/drawer_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_alignParentTop="true"
|
||||||
android:layout_marginRight="3dp"
|
android:layout_above="@+id/terminal_toolbar_view_pager"
|
||||||
android:layout_marginLeft="3dp"
|
android:layout_height="match_parent">
|
||||||
android:focusableInTouchMode="true"
|
|
||||||
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
|
|
||||||
android:scrollbars="vertical"
|
|
||||||
android:importantForAutofill="no"
|
|
||||||
android:autofillHints="password" />
|
|
||||||
|
|
||||||
<LinearLayout
|
<com.termux.view.TerminalView
|
||||||
android:id="@+id/left_drawer"
|
android:id="@+id/terminal_view"
|
||||||
android:layout_width="240dp"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_gravity="start"
|
|
||||||
android:background="@android:color/white"
|
|
||||||
android:choiceMode="singleChoice"
|
|
||||||
android:divider="@android:color/transparent"
|
|
||||||
android:dividerHeight="0dp"
|
|
||||||
android:descendantFocusability="blocksDescendants"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<ListView
|
|
||||||
android:id="@+id/terminal_sessions_list"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="top"
|
android:layout_marginRight="3dp"
|
||||||
android:layout_weight="1"
|
android:layout_marginLeft="3dp"
|
||||||
android:choiceMode="singleChoice"
|
android:focusableInTouchMode="true"
|
||||||
android:longClickable="true" />
|
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
android:importantForAutofill="no"
|
||||||
|
android:autofillHints="password" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
style="?android:attr/buttonBarStyle"
|
android:id="@+id/left_drawer"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="240dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:orientation="horizontal">
|
android:layout_gravity="start"
|
||||||
|
android:background="@android:color/white"
|
||||||
|
android:choiceMode="singleChoice"
|
||||||
|
android:divider="@android:color/transparent"
|
||||||
|
android:dividerHeight="0dp"
|
||||||
|
android:descendantFocusability="blocksDescendants"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
<Button
|
<LinearLayout
|
||||||
android:id="@+id/toggle_keyboard_button"
|
|
||||||
style="?android:attr/buttonBarButtonStyle"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:orientation="horizontal">
|
||||||
android:text="@string/action_toggle_soft_keyboard" />
|
<ImageButton
|
||||||
|
android:id="@+id/settings_button"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:src="@drawable/ic_settings"
|
||||||
|
android:background="@null"
|
||||||
|
android:contentDescription="@string/action_open_settings" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<Button
|
<ListView
|
||||||
android:id="@+id/new_session_button"
|
android:id="@+id/terminal_sessions_list"
|
||||||
style="?android:attr/buttonBarButtonStyle"
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_gravity="top"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:choiceMode="singleChoice"
|
||||||
|
android:longClickable="true" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
style="?android:attr/buttonBarStyle"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:orientation="horizontal">
|
||||||
android:text="@string/action_new_session" />
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/toggle_keyboard_button"
|
||||||
|
style="?android:attr/buttonBarButtonStyle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/action_toggle_soft_keyboard" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/new_session_button"
|
||||||
|
style="?android:attr/buttonBarButtonStyle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/action_new_session" />
|
||||||
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</androidx.drawerlayout.widget.DrawerLayout>
|
</androidx.drawerlayout.widget.DrawerLayout>
|
||||||
|
|
||||||
<androidx.viewpager.widget.ViewPager
|
<androidx.viewpager.widget.ViewPager
|
||||||
android:id="@+id/terminal_toolbar_view_pager"
|
android:id="@+id/terminal_toolbar_view_pager"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="37.5dp"
|
||||||
|
android:background="@android:drawable/screen_background_dark_transparent"
|
||||||
|
android:layout_alignParentBottom="true" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/activity_termux_bottom_space_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="37.5dp"
|
android:layout_height="1dp"
|
||||||
android:background="@android:drawable/screen_background_dark_transparent"
|
android:background="@android:color/transparent" />
|
||||||
android:layout_alignParentBottom="true" />
|
|
||||||
</RelativeLayout>
|
</com.termux.app.terminal.TermuxActivityRootView>
|
||||||
|
|||||||
@@ -91,6 +91,11 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- TermuxService -->
|
||||||
|
<string name="error_display_over_other_apps_permission_not_granted">&TERMUX_APP_NAME; requires \"Display over other apps\" permission to start terminal sessions from background on Android >= 10. Grants it from Settings -> Apps -> &TERMUX_APP_NAME; -> Advanced</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Termux RunCommandService -->
|
<!-- Termux RunCommandService -->
|
||||||
<string name="error_run_command_service_invalid_intent_action">Invalid intent action to RunCommandService: `%1$s`</string>
|
<string name="error_run_command_service_invalid_intent_action">Invalid intent action to RunCommandService: `%1$s`</string>
|
||||||
<string name="error_run_command_service_mandatory_extra_missing">Mandatory extra missing to RunCommandService: \"%1$s\"</string>
|
<string name="error_run_command_service_mandatory_extra_missing">Mandatory extra missing to RunCommandService: \"%1$s\"</string>
|
||||||
@@ -105,15 +110,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Termux Report And ShareUtils -->
|
|
||||||
<string name="action_copy">Copy</string>
|
|
||||||
<string name="action_share">Share</string>
|
|
||||||
|
|
||||||
<string name="title_share_with">Share With</string>
|
|
||||||
<string name="title_report_text">Report Text</string>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Termux File Receiver -->
|
<!-- Termux File Receiver -->
|
||||||
<string name="title_file_received">Save file in ~/downloads/</string>
|
<string name="title_file_received">Save file in ~/downloads/</string>
|
||||||
<string name="action_file_received_edit">Edit</string>
|
<string name="action_file_received_edit">Edit</string>
|
||||||
|
|||||||
@@ -44,15 +44,6 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
<style name="Theme.AppCompat.TermuxReportActivity" parent="Theme.AppCompat.Light.NoActionBar">
|
|
||||||
<item name="colorPrimaryDark">#FF0000</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="Toolbar.Title" parent="TextAppearance.Widget.AppCompat.Toolbar.Title">
|
|
||||||
<item name="android:textSize">14sp</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
||||||
<style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert">
|
<style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert">
|
||||||
<!-- Seen in buttons on alert dialog: -->
|
<!-- Seen in buttons on alert dialog: -->
|
||||||
<item name="android:colorAccent">#212121</item>
|
<item name="android:colorAccent">#212121</item>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.termux.app;
|
package com.termux.app;
|
||||||
|
|
||||||
import com.termux.shared.data.DataUtils;
|
import com.termux.shared.data.UrlUtils;
|
||||||
|
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -13,7 +13,7 @@ public class TermuxActivityTest {
|
|||||||
private void assertUrlsAre(String text, String... urls) {
|
private void assertUrlsAre(String text, String... urls) {
|
||||||
LinkedHashSet<String> expected = new LinkedHashSet<>();
|
LinkedHashSet<String> expected = new LinkedHashSet<>();
|
||||||
Collections.addAll(expected, urls);
|
Collections.addAll(expected, urls);
|
||||||
Assert.assertEquals(expected, DataUtils.extractUrls(text));
|
Assert.assertEquals(expected, UrlUtils.extractUrls(text));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ buildscript {
|
|||||||
google()
|
google()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:4.1.3'
|
classpath 'com.android.tools.build:gradle:4.2.1'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ allprojects {
|
|||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
maven { url "https://jitpack.io" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,6 @@
|
|||||||
org.gradle.jvmargs=-Xmx2048M
|
org.gradle.jvmargs=-Xmx2048M
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
|
|
||||||
termuxVersion=0.111
|
|
||||||
termuxVersionCode=111
|
|
||||||
|
|
||||||
minSdkVersion=24
|
minSdkVersion=24
|
||||||
targetSdkVersion=28
|
targetSdkVersion=28
|
||||||
ndkVersion=22.1.7171670
|
ndkVersion=22.1.7171670
|
||||||
|
|||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-all.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -58,25 +58,16 @@ task sourceJar(type: Jar) {
|
|||||||
classifier "sources"
|
classifier "sources"
|
||||||
}
|
}
|
||||||
|
|
||||||
publishing {
|
afterEvaluate {
|
||||||
publications {
|
publishing {
|
||||||
bar(MavenPublication) {
|
publications {
|
||||||
groupId 'com.termux'
|
// Creates a Maven publication called "release".
|
||||||
artifactId 'terminal-emulator'
|
release(MavenPublication) {
|
||||||
version "0.113"
|
from components.release
|
||||||
artifact(sourceJar)
|
groupId = 'com.termux'
|
||||||
artifact("$buildDir/outputs/aar/terminal-emulator-release.aar")
|
artifactId = 'terminal-emulator'
|
||||||
}
|
version = '0.115'
|
||||||
}
|
artifact(sourceJar)
|
||||||
|
|
||||||
repositories {
|
|
||||||
maven {
|
|
||||||
name = "GitHubPackages"
|
|
||||||
url = uri("https://maven.pkg.github.com/termux/termux-app")
|
|
||||||
|
|
||||||
credentials {
|
|
||||||
username = System.getenv("GH_USERNAME")
|
|
||||||
password = System.getenv("GH_TOKEN")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,10 +38,6 @@ public final class TerminalEmulator {
|
|||||||
public static final int MOUSE_WHEELUP_BUTTON = 64;
|
public static final int MOUSE_WHEELUP_BUTTON = 64;
|
||||||
public static final int MOUSE_WHEELDOWN_BUTTON = 65;
|
public static final int MOUSE_WHEELDOWN_BUTTON = 65;
|
||||||
|
|
||||||
public static final int CURSOR_STYLE_BLOCK = 0;
|
|
||||||
public static final int CURSOR_STYLE_UNDERLINE = 1;
|
|
||||||
public static final int CURSOR_STYLE_BAR = 2;
|
|
||||||
|
|
||||||
/** Used for invalid data - http://en.wikipedia.org/wiki/Replacement_character#Replacement_character */
|
/** Used for invalid data - http://en.wikipedia.org/wiki/Replacement_character#Replacement_character */
|
||||||
public static final int UNICODE_REPLACEMENT_CHAR = 0xFFFD;
|
public static final int UNICODE_REPLACEMENT_CHAR = 0xFFFD;
|
||||||
|
|
||||||
@@ -126,17 +122,35 @@ public final class TerminalEmulator {
|
|||||||
/** Not really DECSET bit... - http://www.vt100.net/docs/vt510-rm/DECSACE */
|
/** Not really DECSET bit... - http://www.vt100.net/docs/vt510-rm/DECSACE */
|
||||||
private static final int DECSET_BIT_RECTANGULAR_CHANGEATTRIBUTE = 1 << 12;
|
private static final int DECSET_BIT_RECTANGULAR_CHANGEATTRIBUTE = 1 << 12;
|
||||||
|
|
||||||
|
|
||||||
private String mTitle;
|
private String mTitle;
|
||||||
private final Stack<String> mTitleStack = new Stack<>();
|
private final Stack<String> mTitleStack = new Stack<>();
|
||||||
|
|
||||||
|
|
||||||
/** The cursor position. Between (0,0) and (mRows-1, mColumns-1). */
|
/** The cursor position. Between (0,0) and (mRows-1, mColumns-1). */
|
||||||
private int mCursorRow, mCursorCol;
|
private int mCursorRow, mCursorCol;
|
||||||
|
|
||||||
private int mCursorStyle = CURSOR_STYLE_BLOCK;
|
|
||||||
|
|
||||||
/** The number of character rows and columns in the terminal screen. */
|
/** The number of character rows and columns in the terminal screen. */
|
||||||
public int mRows, mColumns;
|
public int mRows, mColumns;
|
||||||
|
|
||||||
|
/** The number of terminal transcript rows that can be scrolled back to. */
|
||||||
|
public static final int TERMINAL_TRANSCRIPT_ROWS_MIN = 100;
|
||||||
|
public static final int TERMINAL_TRANSCRIPT_ROWS_MAX = 50000;
|
||||||
|
public static final int DEFAULT_TERMINAL_TRANSCRIPT_ROWS = 2000;
|
||||||
|
|
||||||
|
|
||||||
|
/* The supported terminal cursor styles. */
|
||||||
|
|
||||||
|
public static final int TERMINAL_CURSOR_STYLE_BLOCK = 0;
|
||||||
|
public static final int TERMINAL_CURSOR_STYLE_UNDERLINE = 1;
|
||||||
|
public static final int TERMINAL_CURSOR_STYLE_BAR = 2;
|
||||||
|
public static final int DEFAULT_TERMINAL_CURSOR_STYLE = TERMINAL_CURSOR_STYLE_BLOCK;
|
||||||
|
public static final Integer[] TERMINAL_CURSOR_STYLES_LIST = new Integer[]{TERMINAL_CURSOR_STYLE_BLOCK, TERMINAL_CURSOR_STYLE_UNDERLINE, TERMINAL_CURSOR_STYLE_BAR};
|
||||||
|
|
||||||
|
/** The terminal cursor styles. */
|
||||||
|
private int mCursorStyle = DEFAULT_TERMINAL_CURSOR_STYLE;
|
||||||
|
|
||||||
|
|
||||||
/** The normal screen buffer. Stores the characters that appear on the screen of the emulated terminal. */
|
/** The normal screen buffer. Stores the characters that appear on the screen of the emulated terminal. */
|
||||||
private final TerminalBuffer mMainBuffer;
|
private final TerminalBuffer mMainBuffer;
|
||||||
/**
|
/**
|
||||||
@@ -294,9 +308,9 @@ public final class TerminalEmulator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public TerminalEmulator(TerminalOutput session, int columns, int rows, int transcriptRows, TerminalSessionClient client) {
|
public TerminalEmulator(TerminalOutput session, int columns, int rows, Integer transcriptRows, TerminalSessionClient client) {
|
||||||
mSession = session;
|
mSession = session;
|
||||||
mScreen = mMainBuffer = new TerminalBuffer(columns, transcriptRows, rows);
|
mScreen = mMainBuffer = new TerminalBuffer(columns, getTerminalTranscriptRows(transcriptRows), rows);
|
||||||
mAltBuffer = new TerminalBuffer(columns, rows, rows);
|
mAltBuffer = new TerminalBuffer(columns, rows, rows);
|
||||||
mClient = client;
|
mClient = client;
|
||||||
mRows = rows;
|
mRows = rows;
|
||||||
@@ -307,6 +321,8 @@ public final class TerminalEmulator {
|
|||||||
|
|
||||||
public void updateTerminalSessionClient(TerminalSessionClient client) {
|
public void updateTerminalSessionClient(TerminalSessionClient client) {
|
||||||
mClient = client;
|
mClient = client;
|
||||||
|
setCursorStyle();
|
||||||
|
setCursorBlinkState(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public TerminalBuffer getScreen() {
|
public TerminalBuffer getScreen() {
|
||||||
@@ -317,6 +333,13 @@ public final class TerminalEmulator {
|
|||||||
return mScreen == mAltBuffer;
|
return mScreen == mAltBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int getTerminalTranscriptRows(Integer transcriptRows) {
|
||||||
|
if (transcriptRows == null || transcriptRows < TERMINAL_TRANSCRIPT_ROWS_MIN || transcriptRows > TERMINAL_TRANSCRIPT_ROWS_MAX)
|
||||||
|
return DEFAULT_TERMINAL_TRANSCRIPT_ROWS;
|
||||||
|
else
|
||||||
|
return transcriptRows;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mouseButton one of the MOUSE_* constants of this class.
|
* @param mouseButton one of the MOUSE_* constants of this class.
|
||||||
*/
|
*/
|
||||||
@@ -384,11 +407,24 @@ public final class TerminalEmulator {
|
|||||||
return mCursorCol;
|
return mCursorCol;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link #CURSOR_STYLE_BAR}, {@link #CURSOR_STYLE_BLOCK} or {@link #CURSOR_STYLE_UNDERLINE} */
|
/** Get the terminal cursor style. It will be one of {@link #TERMINAL_CURSOR_STYLES_LIST} */
|
||||||
public int getCursorStyle() {
|
public int getCursorStyle() {
|
||||||
return mCursorStyle;
|
return mCursorStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Set the terminal cursor style. */
|
||||||
|
public void setCursorStyle() {
|
||||||
|
Integer cursorStyle = null;
|
||||||
|
|
||||||
|
if (mClient != null)
|
||||||
|
cursorStyle = mClient.getTerminalCursorStyle();
|
||||||
|
|
||||||
|
if (cursorStyle == null || !Arrays.asList(TERMINAL_CURSOR_STYLES_LIST).contains(cursorStyle))
|
||||||
|
mCursorStyle = DEFAULT_TERMINAL_CURSOR_STYLE;
|
||||||
|
else
|
||||||
|
mCursorStyle = cursorStyle;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isReverseVideo() {
|
public boolean isReverseVideo() {
|
||||||
return isDecsetInternalBitSet(DECSET_BIT_REVERSE_VIDEO);
|
return isDecsetInternalBitSet(DECSET_BIT_REVERSE_VIDEO);
|
||||||
}
|
}
|
||||||
@@ -806,15 +842,15 @@ public final class TerminalEmulator {
|
|||||||
case 0: // Blinking block.
|
case 0: // Blinking block.
|
||||||
case 1: // Blinking block.
|
case 1: // Blinking block.
|
||||||
case 2: // Steady block.
|
case 2: // Steady block.
|
||||||
mCursorStyle = CURSOR_STYLE_BLOCK;
|
mCursorStyle = TERMINAL_CURSOR_STYLE_BLOCK;
|
||||||
break;
|
break;
|
||||||
case 3: // Blinking underline.
|
case 3: // Blinking underline.
|
||||||
case 4: // Steady underline.
|
case 4: // Steady underline.
|
||||||
mCursorStyle = CURSOR_STYLE_UNDERLINE;
|
mCursorStyle = TERMINAL_CURSOR_STYLE_UNDERLINE;
|
||||||
break;
|
break;
|
||||||
case 5: // Blinking bar (xterm addition).
|
case 5: // Blinking bar (xterm addition).
|
||||||
case 6: // Steady bar (xterm addition).
|
case 6: // Steady bar (xterm addition).
|
||||||
mCursorStyle = CURSOR_STYLE_BAR;
|
mCursorStyle = TERMINAL_CURSOR_STYLE_BAR;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -2330,7 +2366,7 @@ public final class TerminalEmulator {
|
|||||||
|
|
||||||
/** Reset terminal state so user can interact with it regardless of present state. */
|
/** Reset terminal state so user can interact with it regardless of present state. */
|
||||||
public void reset() {
|
public void reset() {
|
||||||
mCursorStyle = CURSOR_STYLE_BLOCK;
|
setCursorStyle();
|
||||||
mArgIndex = 0;
|
mArgIndex = 0;
|
||||||
mContinueSequence = false;
|
mContinueSequence = false;
|
||||||
mEscapeState = ESC_NONE;
|
mEscapeState = ESC_NONE;
|
||||||
|
|||||||
@@ -74,14 +74,17 @@ public final class TerminalSession extends TerminalOutput {
|
|||||||
private final String mCwd;
|
private final String mCwd;
|
||||||
private final String[] mArgs;
|
private final String[] mArgs;
|
||||||
private final String[] mEnv;
|
private final String[] mEnv;
|
||||||
|
private final Integer mTranscriptRows;
|
||||||
|
|
||||||
|
|
||||||
private static final String LOG_TAG = "TerminalSession";
|
private static final String LOG_TAG = "TerminalSession";
|
||||||
|
|
||||||
public TerminalSession(String shellPath, String cwd, String[] args, String[] env, TerminalSessionClient client) {
|
public TerminalSession(String shellPath, String cwd, String[] args, String[] env, Integer transcriptRows, TerminalSessionClient client) {
|
||||||
this.mShellPath = shellPath;
|
this.mShellPath = shellPath;
|
||||||
this.mCwd = cwd;
|
this.mCwd = cwd;
|
||||||
this.mArgs = args;
|
this.mArgs = args;
|
||||||
this.mEnv = env;
|
this.mEnv = env;
|
||||||
|
this.mTranscriptRows = transcriptRows;
|
||||||
this.mClient = client;
|
this.mClient = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +121,7 @@ public final class TerminalSession extends TerminalOutput {
|
|||||||
* @param rows The number of rows in the terminal window.
|
* @param rows The number of rows in the terminal window.
|
||||||
*/
|
*/
|
||||||
public void initializeEmulator(int columns, int rows) {
|
public void initializeEmulator(int columns, int rows) {
|
||||||
mEmulator = new TerminalEmulator(this, columns, rows, /* transcript= */2000, mClient);
|
mEmulator = new TerminalEmulator(this, columns, rows, mTranscriptRows, mClient);
|
||||||
|
|
||||||
int[] processId = new int[1];
|
int[] processId = new int[1];
|
||||||
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns);
|
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns);
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ public interface TerminalSessionClient {
|
|||||||
void onTerminalCursorStateChange(boolean state);
|
void onTerminalCursorStateChange(boolean state);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Integer getTerminalCursorStyle();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void logError(String tag, String message);
|
void logError(String tag, String message);
|
||||||
|
|
||||||
void logWarn(String tag, String message);
|
void logWarn(String tag, String message);
|
||||||
|
|||||||
@@ -103,23 +103,23 @@ public class TerminalTest extends TerminalTestCase {
|
|||||||
/** Test the cursor shape changes using DECSCUSR. */
|
/** Test the cursor shape changes using DECSCUSR. */
|
||||||
public void testSetCursorStyle() throws Exception {
|
public void testSetCursorStyle() throws Exception {
|
||||||
withTerminalSized(5, 5);
|
withTerminalSized(5, 5);
|
||||||
assertEquals(TerminalEmulator.CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
|
assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
|
||||||
enterString("\033[3 q");
|
enterString("\033[3 q");
|
||||||
assertEquals(TerminalEmulator.CURSOR_STYLE_UNDERLINE, mTerminal.getCursorStyle());
|
assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_UNDERLINE, mTerminal.getCursorStyle());
|
||||||
enterString("\033[5 q");
|
enterString("\033[5 q");
|
||||||
assertEquals(TerminalEmulator.CURSOR_STYLE_BAR, mTerminal.getCursorStyle());
|
assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_BAR, mTerminal.getCursorStyle());
|
||||||
enterString("\033[0 q");
|
enterString("\033[0 q");
|
||||||
assertEquals(TerminalEmulator.CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
|
assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
|
||||||
enterString("\033[6 q");
|
enterString("\033[6 q");
|
||||||
assertEquals(TerminalEmulator.CURSOR_STYLE_BAR, mTerminal.getCursorStyle());
|
assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_BAR, mTerminal.getCursorStyle());
|
||||||
enterString("\033[4 q");
|
enterString("\033[4 q");
|
||||||
assertEquals(TerminalEmulator.CURSOR_STYLE_UNDERLINE, mTerminal.getCursorStyle());
|
assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_UNDERLINE, mTerminal.getCursorStyle());
|
||||||
enterString("\033[1 q");
|
enterString("\033[1 q");
|
||||||
assertEquals(TerminalEmulator.CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
|
assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
|
||||||
enterString("\033[4 q");
|
enterString("\033[4 q");
|
||||||
assertEquals(TerminalEmulator.CURSOR_STYLE_UNDERLINE, mTerminal.getCursorStyle());
|
assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_UNDERLINE, mTerminal.getCursorStyle());
|
||||||
enterString("\033[2 q");
|
enterString("\033[2 q");
|
||||||
assertEquals(TerminalEmulator.CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
|
assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testPaste() {
|
public void testPaste() {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ android {
|
|||||||
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "androidx.annotation:annotation:1.1.0"
|
implementation "androidx.annotation:annotation:1.2.0"
|
||||||
api project(":terminal-emulator")
|
api project(":terminal-emulator")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,25 +37,16 @@ task sourceJar(type: Jar) {
|
|||||||
classifier "sources"
|
classifier "sources"
|
||||||
}
|
}
|
||||||
|
|
||||||
publishing {
|
afterEvaluate {
|
||||||
publications {
|
publishing {
|
||||||
bar(MavenPublication) {
|
publications {
|
||||||
groupId 'com.termux'
|
// Creates a Maven publication called "release".
|
||||||
artifactId 'terminal-view'
|
release(MavenPublication) {
|
||||||
version "0.113"
|
from components.release
|
||||||
artifact(sourceJar)
|
groupId = 'com.termux'
|
||||||
artifact("$buildDir/outputs/aar/terminal-view-release.aar")
|
artifactId = 'terminal-view'
|
||||||
}
|
version = '0.115'
|
||||||
}
|
artifact(sourceJar)
|
||||||
|
|
||||||
repositories {
|
|
||||||
maven {
|
|
||||||
name = "GitHubPackages"
|
|
||||||
url = uri("https://maven.pkg.github.com/termux/termux-app")
|
|
||||||
|
|
||||||
credentials {
|
|
||||||
username = System.getenv("GH_USERNAME")
|
|
||||||
password = System.getenv("GH_TOKEN")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,8 +200,8 @@ public final class TerminalRenderer {
|
|||||||
if (cursor != 0) {
|
if (cursor != 0) {
|
||||||
mTextPaint.setColor(cursor);
|
mTextPaint.setColor(cursor);
|
||||||
float cursorHeight = mFontLineSpacingAndAscent - mFontAscent;
|
float cursorHeight = mFontLineSpacingAndAscent - mFontAscent;
|
||||||
if (cursorStyle == TerminalEmulator.CURSOR_STYLE_UNDERLINE) cursorHeight /= 4.;
|
if (cursorStyle == TerminalEmulator.TERMINAL_CURSOR_STYLE_UNDERLINE) cursorHeight /= 4.;
|
||||||
else if (cursorStyle == TerminalEmulator.CURSOR_STYLE_BAR) right -= ((right - left) * 3) / 4.;
|
else if (cursorStyle == TerminalEmulator.TERMINAL_CURSOR_STYLE_BAR) right -= ((right - left) * 3) / 4.;
|
||||||
canvas.drawRect(left, y - cursorHeight, right, y, mTextPaint);
|
canvas.drawRect(left, y - cursorHeight, right, y, mTextPaint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -720,7 +720,10 @@ public final class TerminalView extends View {
|
|||||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||||
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED)
|
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED)
|
||||||
mClient.logInfo(LOG_TAG, "onKeyUp(keyCode=" + keyCode + ", event=" + event + ")");
|
mClient.logInfo(LOG_TAG, "onKeyUp(keyCode=" + keyCode + ", event=" + event + ")");
|
||||||
if (mEmulator == null) return true;
|
|
||||||
|
// 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 (mClient.onKeyUp(keyCode, event)) {
|
if (mClient.onKeyUp(keyCode, event)) {
|
||||||
invalidate();
|
invalidate();
|
||||||
@@ -755,6 +758,11 @@ public final class TerminalView extends View {
|
|||||||
if (mEmulator == null || (newColumns != mEmulator.mColumns || newRows != mEmulator.mRows)) {
|
if (mEmulator == null || (newColumns != mEmulator.mColumns || newRows != mEmulator.mRows)) {
|
||||||
mTermSession.updateSize(newColumns, newRows);
|
mTermSession.updateSize(newColumns, newRows);
|
||||||
mEmulator = mTermSession.getEmulator();
|
mEmulator = mTermSession.getEmulator();
|
||||||
|
mClient.onEmulatorSet();
|
||||||
|
|
||||||
|
// Update mTerminalCursorBlinkerRunnable inner class mEmulator on session change
|
||||||
|
if (mTerminalCursorBlinkerRunnable != null)
|
||||||
|
mTerminalCursorBlinkerRunnable.setEmulator(mEmulator);
|
||||||
|
|
||||||
mTopRow = 0;
|
mTopRow = 0;
|
||||||
scrollTo(0, 0);
|
scrollTo(0, 0);
|
||||||
@@ -880,7 +888,16 @@ public final class TerminalView extends View {
|
|||||||
* {@link #TERMINAL_CURSOR_BLINK_RATE_MIN} and {@link #TERMINAL_CURSOR_BLINK_RATE_MAX}.
|
* {@link #TERMINAL_CURSOR_BLINK_RATE_MIN} and {@link #TERMINAL_CURSOR_BLINK_RATE_MAX}.
|
||||||
*
|
*
|
||||||
* This should be called when the view holding this activity is resumed or stopped so that
|
* This should be called when the view holding this activity is resumed or stopped so that
|
||||||
* cursor blinker does not run when activity is not visible.
|
* cursor blinker does not run when activity is not visible. If you call this on onResume()
|
||||||
|
* to start cursor blinking, then ensure that {@link #mEmulator} is set, otherwise wait for the
|
||||||
|
* {@link TerminalViewClient#onEmulatorSet()} event after calling {@link #attachSession(TerminalSession)}
|
||||||
|
* for the first session added in the activity since blinking will not start if {@link #mEmulator}
|
||||||
|
* is not set, like if activity is started again after exiting it with double back press. Do not
|
||||||
|
* call this directly after {@link #attachSession(TerminalSession)} since {@link #updateSize()}
|
||||||
|
* may return without setting {@link #mEmulator} since width/height may be 0. Its called again in
|
||||||
|
* {@link #onSizeChanged(int, int, int, int)}. Calling on onResume() if emulator is already set
|
||||||
|
* is necessary, since onEmulatorSet() may not be called after activity is started after device
|
||||||
|
* display timeout with double tap and not power button.
|
||||||
*
|
*
|
||||||
* It should also be called on the
|
* It should also be called on the
|
||||||
* {@link com.termux.terminal.TerminalSessionClient#onTerminalCursorStateChange(boolean)}
|
* {@link com.termux.terminal.TerminalSessionClient#onTerminalCursorStateChange(boolean)}
|
||||||
@@ -888,6 +905,10 @@ public final class TerminalView extends View {
|
|||||||
* to be shown. It should also be checked if activity is visible if blinker is to be started
|
* to be shown. It should also be checked if activity is visible if blinker is to be started
|
||||||
* before calling this.
|
* before calling this.
|
||||||
*
|
*
|
||||||
|
* It should also be called after terminal is reset with {@link TerminalSession#reset()} in case
|
||||||
|
* cursor blinker was disabled before reset due to call to
|
||||||
|
* {@link com.termux.terminal.TerminalSessionClient#onTerminalCursorStateChange(boolean)}.
|
||||||
|
*
|
||||||
* How cursor blinker starting works is by registering a {@link Runnable} with the looper of
|
* How cursor blinker starting works is by registering a {@link Runnable} with the looper of
|
||||||
* the main thread of the app which when run, toggles the cursor blinking state and re-registers
|
* the main thread of the app which when run, toggles the cursor blinking state and re-registers
|
||||||
* itself to be called with the delay set by {@link #mTerminalCursorBlinkerRate}. When cursor
|
* itself to be called with the delay set by {@link #mTerminalCursorBlinkerRate}. When cursor
|
||||||
@@ -953,7 +974,7 @@ public final class TerminalView extends View {
|
|||||||
|
|
||||||
private class TerminalCursorBlinkerRunnable implements Runnable {
|
private class TerminalCursorBlinkerRunnable implements Runnable {
|
||||||
|
|
||||||
private final TerminalEmulator mEmulator;
|
private TerminalEmulator mEmulator;
|
||||||
private final int mBlinkRate;
|
private final int mBlinkRate;
|
||||||
|
|
||||||
// Initialize with false so that initial blink state is visible after toggling
|
// Initialize with false so that initial blink state is visible after toggling
|
||||||
@@ -964,6 +985,10 @@ public final class TerminalView extends View {
|
|||||||
mBlinkRate = blinkRate;
|
mBlinkRate = blinkRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setEmulator(TerminalEmulator emulator) {
|
||||||
|
mEmulator = emulator;
|
||||||
|
}
|
||||||
|
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
if (mEmulator != null) {
|
if (mEmulator != null) {
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ public interface TerminalViewClient {
|
|||||||
boolean onCodePoint(int codePoint, boolean ctrlDown, TerminalSession session);
|
boolean onCodePoint(int codePoint, boolean ctrlDown, TerminalSession session);
|
||||||
|
|
||||||
|
|
||||||
|
void onEmulatorSet();
|
||||||
|
|
||||||
|
|
||||||
void logError(String tag, String message);
|
void logError(String tag, String message);
|
||||||
|
|
||||||
|
|||||||
65
termux-shared/LICENSE.md
Normal file
65
termux-shared/LICENSE.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
The `termux-shared` library is released under [GPLv3 only](https://www.gnu.org/licenses/gpl-3.0.html) license.
|
||||||
|
|
||||||
|
### Exceptions
|
||||||
|
|
||||||
|
#### [MIT License](https://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
|
- [`src/main/java/com/termux/shared/termux/TermuxConstants.java`](src/main/java/com/termux/shared/termux/TermuxConstants.java).
|
||||||
|
- [`src/main/java/com/termux/shared/settings/properties/TermuxPropertyConstants.java`](src/main/java/com/termux/shared/settings/properties/TermuxPropertyConstants.java).
|
||||||
|
|
||||||
|
- [`src/main/java/com/termux/shared/activities/ReportActivity.java`](src/main/java/com/termux/shared/activities/ReportActivity.java).
|
||||||
|
|
||||||
|
- [`src/main/java/com/termux/shared/crash/CrashHandler.java`](src/main/java/com/termux/shared/crash/CrashHandler.java).
|
||||||
|
|
||||||
|
- [`src/main/java/com/termux/shared/data/DataUtils.java`](src/main/java/com/termux/shared/data/DataUtils.java).
|
||||||
|
- [`src/main/java/com/termux/shared/data/IntentUtils.java`](src/main/java/com/termux/shared/data/IntentUtils.java).
|
||||||
|
|
||||||
|
- [`src/main/java/com/termux/shared/file/filesystem/FileType.java`](src/main/java/com/termux/shared/file/filesystem/FileType.java).
|
||||||
|
- [`src/main/java/com/termux/shared/file/filesystem/FileTypes.java`](src/main/java/com/termux/shared/file/filesystem/FileTypes.java).
|
||||||
|
- [`src/main/java/com/termux/shared/file/filesystem/NativeDispatcher.java`](src/main/java/com/termux/shared/file/filesystem/NativeDispatcher.java).
|
||||||
|
- [`src/main/java/com/termux/shared/file/tests/FileUtilsTests.java`](src/main/java/com/termux/shared/file/tests/FileUtilsTests.java).
|
||||||
|
- [`src/main/java/com/termux/shared/file/FileUtils.java`](src/main/java/com/termux/shared/file/FileUtils.java).
|
||||||
|
|
||||||
|
- [`src/main/java/com/termux/shared/interact/ShareUtils.java`](src/main/java/com/termux/shared/interact/ShareUtils.java).
|
||||||
|
- [`src/main/java/com/termux/shared/interact/MessageDialogUtils.java`](src/main/java/com/termux/shared/interact/MessageDialogUtils.java).
|
||||||
|
|
||||||
|
- [`src/main/java/com/termux/shared/logger/Logger.java`](src/main/java/com/termux/shared/logger/Logger.java).
|
||||||
|
|
||||||
|
- [`src/main/java/com/termux/shared/markdown/MarkdownUtils.java`](src/main/java/com/termux/shared/markdown/MarkdownUtils.java).
|
||||||
|
|
||||||
|
- [`src/main/java/com/termux/shared/models/*`](src/main/java/com/termux/shared/models).
|
||||||
|
|
||||||
|
- [`src/main/java/com/termux/shared/notification/NotificationUtils.java`](src/main/java/com/termux/shared/notification/NotificationUtils.java).
|
||||||
|
|
||||||
|
- [`src/main/java/com/termux/shared/settings/preferences/SharedPreferenceUtils.java`](src/main/java/com/termux/shared/settings/preferences/SharedPreferenceUtils.java).
|
||||||
|
|
||||||
|
- [`src/main/java/com/termux/shared/settings/properties/SharedPropertiesParser.java`](src/main/java/com/termux/shared/settings/properties/SharedPropertiesParser.java).
|
||||||
|
- [`src/main/java/com/termux/shared/settings/properties/SharedProperties.java`](src/main/java/com/termux/shared/settings/properties/SharedProperties.java).
|
||||||
|
|
||||||
|
- [`src/main/java/com/termux/shared/shell/ResultSender.java`](src/main/java/com/termux/shared/shell/ResultSender.java).
|
||||||
|
- [`src/main/java/com/termux/shared/shell/ShellEnvironmentClient.java`](src/main/java/com/termux/shared/shell/ShellEnvironmentClient.java).
|
||||||
|
- [`src/main/java/com/termux/shared/shell/ShellUtils.java`](src/main/java/com/termux/shared/shell/ShellUtils.java).
|
||||||
|
- [`src/main/java/com/termux/shared/shell/TermuxTask.java`](src/main/java/com/termux/shared/shell/TermuxTask.java).
|
||||||
|
|
||||||
|
- [`src/main/java/com/termux/shared/termux/AndroidUtils.java`](src/main/java/com/termux/shared/termux/AndroidUtils.java).
|
||||||
|
|
||||||
|
- [`src/main/java/com/termux/shared/view/KeyboardUtils.java`](src/main/java/com/termux/shared/view/KeyboardUtils.java).
|
||||||
|
- [`src/main/java/com/termux/shared/view/ViewUtils.java`](src/main/java/com/termux/shared/view/ViewUtils.java).
|
||||||
|
|
||||||
|
- [`src/main/res/drawable/*`](src/main/res/drawable).
|
||||||
|
- [`src/main/res/layout/*`](src/main/res/layout).
|
||||||
|
- [`src/main/res/menu/*`](src/main/res/menu).
|
||||||
|
- [`src/main/res/values/*`](src/main/res/values).
|
||||||
|
##
|
||||||
|
|
||||||
|
|
||||||
|
#### [GPLv2 only with "Classpath" exception](https://openjdk.java.net/legal/gplv2+ce.html)
|
||||||
|
|
||||||
|
- [`src/main/java/com/termux/shared/file/filesystem/*`](src/main/java/com/termux/shared/file/filesystem) files that use code from [libcore/ojluni](https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/).
|
||||||
|
##
|
||||||
|
|
||||||
|
|
||||||
|
#### [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
||||||
|
|
||||||
|
- [`src/main/java/com/termux/shared/shell/StreamGobbler.java`](src/main/java/com/termux/shared/shell/StreamGobbler.java) uses code from [libsuperuser ](https://github.com/Chainfire/libsuperuser).
|
||||||
|
##
|
||||||
@@ -5,9 +5,10 @@ android {
|
|||||||
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
implementation 'androidx.appcompat:appcompat:1.3.0'
|
||||||
implementation "androidx.annotation:annotation:1.2.0"
|
implementation "androidx.annotation:annotation:1.2.0"
|
||||||
implementation "androidx.core:core:1.5.0-rc01"
|
implementation "androidx.core:core:1.6.0-rc01"
|
||||||
|
implementation "androidx.window:window:1.0.0-alpha08"
|
||||||
implementation "com.google.guava:guava:24.1-jre"
|
implementation "com.google.guava:guava:24.1-jre"
|
||||||
implementation "io.noties.markwon:core:$markwonVersion"
|
implementation "io.noties.markwon:core:$markwonVersion"
|
||||||
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
|
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
|
||||||
@@ -52,25 +53,16 @@ task sourceJar(type: Jar) {
|
|||||||
classifier "sources"
|
classifier "sources"
|
||||||
}
|
}
|
||||||
|
|
||||||
publishing {
|
afterEvaluate {
|
||||||
publications {
|
publishing {
|
||||||
bar(MavenPublication) {
|
publications {
|
||||||
groupId 'com.termux'
|
// Creates a Maven publication called "release".
|
||||||
artifactId 'termux-shared'
|
release(MavenPublication) {
|
||||||
version "0.113"
|
from components.release
|
||||||
artifact(sourceJar)
|
groupId = 'com.termux'
|
||||||
artifact("$buildDir/outputs/aar/termux-shared-release.aar")
|
artifactId = 'termux-shared'
|
||||||
}
|
version = '0.115'
|
||||||
}
|
artifact(sourceJar)
|
||||||
|
|
||||||
repositories {
|
|
||||||
maven {
|
|
||||||
name = "GitHubPackages"
|
|
||||||
url = uri("https://maven.pkg.github.com/termux/termux-app")
|
|
||||||
|
|
||||||
credentials {
|
|
||||||
username = System.getenv("GH_USERNAME")
|
|
||||||
password = System.getenv("GH_TOKEN")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.termux.app.activities;
|
package com.termux.shared.activities;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
@@ -14,11 +14,11 @@ import android.view.Menu;
|
|||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.shared.R;
|
||||||
import com.termux.shared.termux.TermuxConstants;
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
import com.termux.shared.markdown.MarkdownUtils;
|
import com.termux.shared.markdown.MarkdownUtils;
|
||||||
import com.termux.shared.interact.ShareUtils;
|
import com.termux.shared.interact.ShareUtils;
|
||||||
import com.termux.app.models.ReportInfo;
|
import com.termux.shared.models.ReportInfo;
|
||||||
|
|
||||||
import org.commonmark.node.FencedCodeBlock;
|
import org.commonmark.node.FencedCodeBlock;
|
||||||
|
|
||||||
@@ -7,8 +7,8 @@ import androidx.annotation.NonNull;
|
|||||||
import com.termux.shared.file.FileUtils;
|
import com.termux.shared.file.FileUtils;
|
||||||
import com.termux.shared.logger.Logger;
|
import com.termux.shared.logger.Logger;
|
||||||
import com.termux.shared.markdown.MarkdownUtils;
|
import com.termux.shared.markdown.MarkdownUtils;
|
||||||
import com.termux.shared.termux.TermuxConstants;
|
import com.termux.shared.models.errors.Error;
|
||||||
import com.termux.shared.termux.TermuxUtils;
|
import com.termux.shared.termux.AndroidUtils;
|
||||||
|
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
|
|
||||||
@@ -17,58 +17,85 @@ import java.nio.charset.Charset;
|
|||||||
*/
|
*/
|
||||||
public class CrashHandler implements Thread.UncaughtExceptionHandler {
|
public class CrashHandler implements Thread.UncaughtExceptionHandler {
|
||||||
|
|
||||||
private final Context context;
|
private final Context mContext;
|
||||||
|
private final CrashHandlerClient mCrashHandlerClient;
|
||||||
private final Thread.UncaughtExceptionHandler defaultUEH;
|
private final Thread.UncaughtExceptionHandler defaultUEH;
|
||||||
|
|
||||||
private static final String LOG_TAG = "CrashUtils";
|
private static final String LOG_TAG = "CrashUtils";
|
||||||
|
|
||||||
private CrashHandler(final Context context) {
|
private CrashHandler(@NonNull final Context context, @NonNull final CrashHandlerClient crashHandlerClient) {
|
||||||
this.context = context;
|
this.mContext = context;
|
||||||
|
this.mCrashHandlerClient = crashHandlerClient;
|
||||||
this.defaultUEH = Thread.getDefaultUncaughtExceptionHandler();
|
this.defaultUEH = Thread.getDefaultUncaughtExceptionHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
|
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
|
||||||
logCrash(context,thread, throwable);
|
logCrash(mContext, mCrashHandlerClient, thread, throwable);
|
||||||
defaultUEH.uncaughtException(thread, throwable);
|
defaultUEH.uncaughtException(thread, throwable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set default uncaught crash handler of current thread to {@link CrashHandler}.
|
* Set default uncaught crash handler of current thread to {@link CrashHandler}.
|
||||||
*/
|
*/
|
||||||
public static void setCrashHandler(final Context context) {
|
public static void setCrashHandler(@NonNull final Context context, @NonNull final CrashHandlerClient crashHandlerClient) {
|
||||||
if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof CrashHandler)) {
|
if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof CrashHandler)) {
|
||||||
Thread.setDefaultUncaughtExceptionHandler(new CrashHandler(context));
|
Thread.setDefaultUncaughtExceptionHandler(new CrashHandler(context, crashHandlerClient));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log a crash in the crash log file at
|
* Log a crash in the crash log file at {@code crashlogFilePath}.
|
||||||
* {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}.
|
|
||||||
*
|
*
|
||||||
* @param context The {@link Context} for operations.
|
* @param context The {@link Context} for operations.
|
||||||
|
* @param crashHandlerClient The {@link CrashHandlerClient} implementation.
|
||||||
* @param thread The {@link Thread} in which the crash happened.
|
* @param thread The {@link Thread} in which the crash happened.
|
||||||
* @param throwable The {@link Throwable} thrown for the crash.
|
* @param throwable The {@link Throwable} thrown for the crash.
|
||||||
*/
|
*/
|
||||||
public static void logCrash(final Context context, final Thread thread, final Throwable throwable) {
|
public static void logCrash(@NonNull final Context context, @NonNull final CrashHandlerClient crashHandlerClient, final Thread thread, final Throwable throwable) {
|
||||||
|
|
||||||
StringBuilder reportString = new StringBuilder();
|
StringBuilder reportString = new StringBuilder();
|
||||||
|
|
||||||
reportString.append("## Crash Details\n");
|
reportString.append("## Crash Details\n");
|
||||||
reportString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Crash Thread", thread.toString(), "-"));
|
reportString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Crash Thread", thread.toString(), "-"));
|
||||||
reportString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Crash Timestamp", TermuxUtils.getCurrentTimeStamp(), "-"));
|
reportString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Crash Timestamp", AndroidUtils.getCurrentTimeStamp(), "-"));
|
||||||
|
reportString.append("\n\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Crash Message", throwable.getMessage(), "-"));
|
||||||
|
reportString.append("\n\n").append(Logger.getStackTracesMarkdownString("Stacktrace", Logger.getStackTracesStringArray(throwable)));
|
||||||
|
|
||||||
reportString.append("\n\n").append(Logger.getStackTracesMarkdownString("Stacktrace", Logger.getStackTraceStringArray(throwable)));
|
String appInfoMarkdownString = crashHandlerClient.getAppInfoMarkdownString(context);
|
||||||
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, true));
|
if (appInfoMarkdownString != null && !appInfoMarkdownString.isEmpty())
|
||||||
reportString.append("\n\n").append(TermuxUtils.getDeviceInfoMarkdownString(context));
|
reportString.append("\n\n").append(appInfoMarkdownString);
|
||||||
|
|
||||||
|
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context));
|
||||||
|
|
||||||
// Log report string to logcat
|
// Log report string to logcat
|
||||||
Logger.logError(reportString.toString());
|
Logger.logError(reportString.toString());
|
||||||
|
|
||||||
// Write report string to crash log file
|
// Write report string to crash log file
|
||||||
String errmsg = FileUtils.writeStringToFile(context, "crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, Charset.defaultCharset(), reportString.toString(), false);
|
Error error = FileUtils.writeStringToFile("crash log", crashHandlerClient.getCrashLogFilePath(context),
|
||||||
if (errmsg != null) {
|
Charset.defaultCharset(), reportString.toString(), false);
|
||||||
Logger.logError(LOG_TAG, errmsg);
|
if (error != null) {
|
||||||
|
Logger.logErrorExtended(LOG_TAG, error.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface CrashHandlerClient {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get crash log file path.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} passed to {@link CrashHandler#CrashHandler(Context, CrashHandlerClient)}.
|
||||||
|
* @return Should return the crash log file path.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
String getCrashLogFilePath(Context context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get app info markdown string to add to crash log.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} passed to {@link CrashHandler#CrashHandler(Context, CrashHandlerClient)}.
|
||||||
|
* @return Should return app info markdown string.
|
||||||
|
*/
|
||||||
|
String getAppInfoMarkdownString(Context context);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.termux.shared.crash;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
import com.termux.shared.termux.TermuxUtils;
|
||||||
|
|
||||||
|
public class TermuxCrashUtils implements CrashHandler.CrashHandlerClient {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set default uncaught crash handler of current thread to {@link CrashHandler} for Termux app
|
||||||
|
* and its plugin to log crashes at {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}.
|
||||||
|
*/
|
||||||
|
public static void setCrashHandler(@NonNull final Context context) {
|
||||||
|
CrashHandler.setCrashHandler(context, new TermuxCrashUtils());
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String getCrashLogFilePath(Context context) {
|
||||||
|
return TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAppInfoMarkdownString(Context context) {
|
||||||
|
return TermuxUtils.getAppInfoMarkdownString(context, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ package com.termux.shared.data;
|
|||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@@ -23,7 +25,7 @@ public class DataUtils {
|
|||||||
if (maxLength < 0 || text.length() < maxLength) return text;
|
if (maxLength < 0 || text.length() < maxLength) return text;
|
||||||
|
|
||||||
if (fromEnd) {
|
if (fromEnd) {
|
||||||
text = text.substring(0, Math.min(text.length(), maxLength));
|
text = text.substring(0, maxLength);
|
||||||
} else {
|
} else {
|
||||||
int cutOffIndex = text.length() - maxLength;
|
int cutOffIndex = text.length() - maxLength;
|
||||||
|
|
||||||
@@ -42,6 +44,21 @@ public class DataUtils {
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace a sub string in each item of a {@link String[]}.
|
||||||
|
*
|
||||||
|
* @param array The {@link String[]} to replace in.
|
||||||
|
* @param find The sub string to replace.
|
||||||
|
* @param replace The sub string to replace with.
|
||||||
|
*/
|
||||||
|
public static void replaceSubStringsInStringArrayItems(String[] array, String find, String replace) {
|
||||||
|
if(array == null || array.length == 0) return;
|
||||||
|
|
||||||
|
for (int i = 0; i < array.length; i++) {
|
||||||
|
array[i] = array[i].replace(find, replace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the {@code float} from a {@link String}.
|
* Get the {@code float} from a {@link String}.
|
||||||
*
|
*
|
||||||
@@ -139,97 +156,13 @@ public class DataUtils {
|
|||||||
* @param def The default {@link Object}.
|
* @param def The default {@link Object}.
|
||||||
* @return Returns {@code object} if it is not {@code null}, otherwise returns {@code def}.
|
* @return Returns {@code object} if it is not {@code null}, otherwise returns {@code def}.
|
||||||
*/
|
*/
|
||||||
public static <T> T getDefaultIfNull(@androidx.annotation.Nullable T object, @androidx.annotation.Nullable T def) {
|
public static <T> T getDefaultIfNull(@Nullable T object, @Nullable T def) {
|
||||||
return (object == null) ? def : object;
|
return (object == null) ? def : object;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Check if a string is null or empty. */
|
||||||
|
public static boolean isNullOrEmpty(String string) {
|
||||||
public static LinkedHashSet<CharSequence> extractUrls(String text) {
|
return string == null || string.isEmpty();
|
||||||
|
|
||||||
StringBuilder regex_sb = new StringBuilder();
|
|
||||||
|
|
||||||
regex_sb.append("("); // Begin first matching group.
|
|
||||||
regex_sb.append("(?:"); // Begin scheme group.
|
|
||||||
regex_sb.append("dav|"); // The DAV proto.
|
|
||||||
regex_sb.append("dict|"); // The DICT proto.
|
|
||||||
regex_sb.append("dns|"); // The DNS proto.
|
|
||||||
regex_sb.append("file|"); // File path.
|
|
||||||
regex_sb.append("finger|"); // The Finger proto.
|
|
||||||
regex_sb.append("ftp(?:s?)|"); // The FTP proto.
|
|
||||||
regex_sb.append("git|"); // The Git proto.
|
|
||||||
regex_sb.append("gopher|"); // The Gopher proto.
|
|
||||||
regex_sb.append("http(?:s?)|"); // The HTTP proto.
|
|
||||||
regex_sb.append("imap(?:s?)|"); // The IMAP proto.
|
|
||||||
regex_sb.append("irc(?:[6s]?)|"); // The IRC proto.
|
|
||||||
regex_sb.append("ip[fn]s|"); // The IPFS proto.
|
|
||||||
regex_sb.append("ldap(?:s?)|"); // The LDAP proto.
|
|
||||||
regex_sb.append("pop3(?:s?)|"); // The POP3 proto.
|
|
||||||
regex_sb.append("redis(?:s?)|"); // The Redis proto.
|
|
||||||
regex_sb.append("rsync|"); // The Rsync proto.
|
|
||||||
regex_sb.append("rtsp(?:[su]?)|"); // The RTSP proto.
|
|
||||||
regex_sb.append("sftp|"); // The SFTP proto.
|
|
||||||
regex_sb.append("smb(?:s?)|"); // The SAMBA proto.
|
|
||||||
regex_sb.append("smtp(?:s?)|"); // The SMTP proto.
|
|
||||||
regex_sb.append("svn(?:(?:\\+ssh)?)|"); // The Subversion proto.
|
|
||||||
regex_sb.append("tcp|"); // The TCP proto.
|
|
||||||
regex_sb.append("telnet|"); // The Telnet proto.
|
|
||||||
regex_sb.append("tftp|"); // The TFTP proto.
|
|
||||||
regex_sb.append("udp|"); // The UDP proto.
|
|
||||||
regex_sb.append("vnc|"); // The VNC proto.
|
|
||||||
regex_sb.append("ws(?:s?)"); // The Websocket proto.
|
|
||||||
regex_sb.append(")://"); // End scheme group.
|
|
||||||
regex_sb.append(")"); // End first matching group.
|
|
||||||
|
|
||||||
|
|
||||||
// Begin second matching group.
|
|
||||||
regex_sb.append("(");
|
|
||||||
|
|
||||||
// User name and/or password in format 'user:pass@'.
|
|
||||||
regex_sb.append("(?:\\S+(?::\\S*)?@)?");
|
|
||||||
|
|
||||||
// Begin host group.
|
|
||||||
regex_sb.append("(?:");
|
|
||||||
|
|
||||||
// IP address (from http://www.regular-expressions.info/examples.html).
|
|
||||||
regex_sb.append("(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|");
|
|
||||||
|
|
||||||
// Host name or domain.
|
|
||||||
regex_sb.append("(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)(?:(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))?|");
|
|
||||||
|
|
||||||
// Just path. Used in case of 'file://' scheme.
|
|
||||||
regex_sb.append("/(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)");
|
|
||||||
|
|
||||||
// End host group.
|
|
||||||
regex_sb.append(")");
|
|
||||||
|
|
||||||
// Port number.
|
|
||||||
regex_sb.append("(?::\\d{1,5})?");
|
|
||||||
|
|
||||||
// Resource path with optional query string.
|
|
||||||
regex_sb.append("(?:/[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?");
|
|
||||||
|
|
||||||
// Fragment.
|
|
||||||
regex_sb.append("(?:#[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?");
|
|
||||||
|
|
||||||
// End second matching group.
|
|
||||||
regex_sb.append(")");
|
|
||||||
|
|
||||||
final Pattern urlPattern = Pattern.compile(
|
|
||||||
regex_sb.toString(),
|
|
||||||
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
|
|
||||||
|
|
||||||
LinkedHashSet<CharSequence> urlSet = new LinkedHashSet<>();
|
|
||||||
Matcher matcher = urlPattern.matcher(text);
|
|
||||||
|
|
||||||
while (matcher.find()) {
|
|
||||||
int matchStart = matcher.start(1);
|
|
||||||
int matchEnd = matcher.end();
|
|
||||||
String url = text.substring(matchStart, matchEnd);
|
|
||||||
urlSet.add(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
return urlSet;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package com.termux.shared.data;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class IntentUtils {
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "IntentUtils";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a {@link String} extra from an {@link Intent} if its not {@code null} or empty.
|
||||||
|
*
|
||||||
|
* @param intent The {@link Intent} to get the extra from.
|
||||||
|
* @param key The {@link String} key name.
|
||||||
|
* @param def The default value if extra is not set.
|
||||||
|
* @param throwExceptionIfNotSet If set to {@code true}, then an exception will be thrown if extra
|
||||||
|
* is not set.
|
||||||
|
* @return Returns the {@link String} extra if set, otherwise {@code null}.
|
||||||
|
*/
|
||||||
|
public static String getStringExtraIfSet(@NonNull Intent intent, String key, String def, boolean throwExceptionIfNotSet) throws Exception {
|
||||||
|
String value = getStringExtraIfSet(intent, key, def);
|
||||||
|
if (value == null && throwExceptionIfNotSet)
|
||||||
|
throw new Exception("The \"" + key + "\" key string value is null or empty");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a {@link String} extra from an {@link Intent} if its not {@code null} or empty.
|
||||||
|
*
|
||||||
|
* @param intent The {@link Intent} to get the extra from.
|
||||||
|
* @param key The {@link String} key name.
|
||||||
|
* @param def The default value if extra is not set.
|
||||||
|
* @return Returns the {@link String} extra if set, otherwise {@code null}.
|
||||||
|
*/
|
||||||
|
public static String getStringExtraIfSet(@NonNull Intent intent, String key, String def) {
|
||||||
|
String value = intent.getStringExtra(key);
|
||||||
|
if (value == null || value.isEmpty()) {
|
||||||
|
if (def != null && !def.isEmpty())
|
||||||
|
return def;
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a {@link String[]} extra from an {@link Intent} if its not {@code null} or empty.
|
||||||
|
*
|
||||||
|
* @param intent The {@link Intent} to get the extra from.
|
||||||
|
* @param key The {@link String} key name.
|
||||||
|
* @param def The default value if extra is not set.
|
||||||
|
* @param throwExceptionIfNotSet If set to {@code true}, then an exception will be thrown if extra
|
||||||
|
* is not set.
|
||||||
|
* @return Returns the {@link String[]} extra if set, otherwise {@code null}.
|
||||||
|
*/
|
||||||
|
public static String[] getStringArrayExtraIfSet(@NonNull Intent intent, String key, String[] def, boolean throwExceptionIfNotSet) throws Exception {
|
||||||
|
String[] value = getStringArrayExtraIfSet(intent, key, def);
|
||||||
|
if (value == null && throwExceptionIfNotSet)
|
||||||
|
throw new Exception("The \"" + key + "\" key string array is null or empty");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a {@link String[]} extra from an {@link Intent} if its not {@code null} or empty.
|
||||||
|
*
|
||||||
|
* @param intent The {@link Intent} to get the extra from.
|
||||||
|
* @param key The {@link String} key name.
|
||||||
|
* @param def The default value if extra is not set.
|
||||||
|
* @return Returns the {@link String[]} extra if set, otherwise {@code null}.
|
||||||
|
*/
|
||||||
|
public static String[] getStringArrayExtraIfSet(Intent intent, String key, String[] def) {
|
||||||
|
String[] value = intent.getStringArrayExtra(key);
|
||||||
|
if (value == null || value.length == 0) {
|
||||||
|
if (def != null && def.length != 0)
|
||||||
|
return def;
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getIntentString(Intent intent) {
|
||||||
|
if (intent == null) return null;
|
||||||
|
|
||||||
|
return intent.toString() + "\n" + getBundleString(intent.getExtras());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getBundleString(Bundle bundle) {
|
||||||
|
if (bundle == null || bundle.size() == 0) return "Bundle[]";
|
||||||
|
|
||||||
|
StringBuilder bundleString = new StringBuilder("Bundle[\n");
|
||||||
|
boolean first = true;
|
||||||
|
for (String key : bundle.keySet()) {
|
||||||
|
if (!first)
|
||||||
|
bundleString.append("\n");
|
||||||
|
|
||||||
|
bundleString.append(key).append(": `");
|
||||||
|
|
||||||
|
Object value = bundle.get(key);
|
||||||
|
if (value instanceof int[]) {
|
||||||
|
bundleString.append(Arrays.toString((int[]) value));
|
||||||
|
} else if (value instanceof byte[]) {
|
||||||
|
bundleString.append(Arrays.toString((byte[]) value));
|
||||||
|
} else if (value instanceof boolean[]) {
|
||||||
|
bundleString.append(Arrays.toString((boolean[]) value));
|
||||||
|
} else if (value instanceof short[]) {
|
||||||
|
bundleString.append(Arrays.toString((short[]) value));
|
||||||
|
} else if (value instanceof long[]) {
|
||||||
|
bundleString.append(Arrays.toString((long[]) value));
|
||||||
|
} else if (value instanceof float[]) {
|
||||||
|
bundleString.append(Arrays.toString((float[]) value));
|
||||||
|
} else if (value instanceof double[]) {
|
||||||
|
bundleString.append(Arrays.toString((double[]) value));
|
||||||
|
} else if (value instanceof String[]) {
|
||||||
|
bundleString.append(Arrays.toString((String[]) value));
|
||||||
|
} else if (value instanceof CharSequence[]) {
|
||||||
|
bundleString.append(Arrays.toString((CharSequence[]) value));
|
||||||
|
} else if (value instanceof Parcelable[]) {
|
||||||
|
bundleString.append(Arrays.toString((Parcelable[]) value));
|
||||||
|
} else if (value instanceof Bundle) {
|
||||||
|
bundleString.append(getBundleString((Bundle) value));
|
||||||
|
} else {
|
||||||
|
bundleString.append(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
bundleString.append("`");
|
||||||
|
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bundleString.append("\n]");
|
||||||
|
return bundleString.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
103
termux-shared/src/main/java/com/termux/shared/data/UrlUtils.java
Normal file
103
termux-shared/src/main/java/com/termux/shared/data/UrlUtils.java
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package com.termux.shared.data;
|
||||||
|
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class UrlUtils {
|
||||||
|
|
||||||
|
public static Pattern URL_MATCH_REGEX;
|
||||||
|
|
||||||
|
public static Pattern getUrlMatchRegex() {
|
||||||
|
if (URL_MATCH_REGEX != null) return URL_MATCH_REGEX;
|
||||||
|
|
||||||
|
StringBuilder regex_sb = new StringBuilder();
|
||||||
|
|
||||||
|
regex_sb.append("("); // Begin first matching group.
|
||||||
|
regex_sb.append("(?:"); // Begin scheme group.
|
||||||
|
regex_sb.append("dav|"); // The DAV proto.
|
||||||
|
regex_sb.append("dict|"); // The DICT proto.
|
||||||
|
regex_sb.append("dns|"); // The DNS proto.
|
||||||
|
regex_sb.append("file|"); // File path.
|
||||||
|
regex_sb.append("finger|"); // The Finger proto.
|
||||||
|
regex_sb.append("ftp(?:s?)|"); // The FTP proto.
|
||||||
|
regex_sb.append("git|"); // The Git proto.
|
||||||
|
regex_sb.append("gopher|"); // The Gopher proto.
|
||||||
|
regex_sb.append("http(?:s?)|"); // The HTTP proto.
|
||||||
|
regex_sb.append("imap(?:s?)|"); // The IMAP proto.
|
||||||
|
regex_sb.append("irc(?:[6s]?)|"); // The IRC proto.
|
||||||
|
regex_sb.append("ip[fn]s|"); // The IPFS proto.
|
||||||
|
regex_sb.append("ldap(?:s?)|"); // The LDAP proto.
|
||||||
|
regex_sb.append("pop3(?:s?)|"); // The POP3 proto.
|
||||||
|
regex_sb.append("redis(?:s?)|"); // The Redis proto.
|
||||||
|
regex_sb.append("rsync|"); // The Rsync proto.
|
||||||
|
regex_sb.append("rtsp(?:[su]?)|"); // The RTSP proto.
|
||||||
|
regex_sb.append("sftp|"); // The SFTP proto.
|
||||||
|
regex_sb.append("smb(?:s?)|"); // The SAMBA proto.
|
||||||
|
regex_sb.append("smtp(?:s?)|"); // The SMTP proto.
|
||||||
|
regex_sb.append("svn(?:(?:\\+ssh)?)|"); // The Subversion proto.
|
||||||
|
regex_sb.append("tcp|"); // The TCP proto.
|
||||||
|
regex_sb.append("telnet|"); // The Telnet proto.
|
||||||
|
regex_sb.append("tftp|"); // The TFTP proto.
|
||||||
|
regex_sb.append("udp|"); // The UDP proto.
|
||||||
|
regex_sb.append("vnc|"); // The VNC proto.
|
||||||
|
regex_sb.append("ws(?:s?)"); // The Websocket proto.
|
||||||
|
regex_sb.append(")://"); // End scheme group.
|
||||||
|
regex_sb.append(")"); // End first matching group.
|
||||||
|
|
||||||
|
|
||||||
|
// Begin second matching group.
|
||||||
|
regex_sb.append("(");
|
||||||
|
|
||||||
|
// User name and/or password in format 'user:pass@'.
|
||||||
|
regex_sb.append("(?:\\S+(?::\\S*)?@)?");
|
||||||
|
|
||||||
|
// Begin host group.
|
||||||
|
regex_sb.append("(?:");
|
||||||
|
|
||||||
|
// IP address (from http://www.regular-expressions.info/examples.html).
|
||||||
|
regex_sb.append("(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|");
|
||||||
|
|
||||||
|
// Host name or domain.
|
||||||
|
regex_sb.append("(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)(?:(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))?|");
|
||||||
|
|
||||||
|
// Just path. Used in case of 'file://' scheme.
|
||||||
|
regex_sb.append("/(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)");
|
||||||
|
|
||||||
|
// End host group.
|
||||||
|
regex_sb.append(")");
|
||||||
|
|
||||||
|
// Port number.
|
||||||
|
regex_sb.append("(?::\\d{1,5})?");
|
||||||
|
|
||||||
|
// Resource path with optional query string.
|
||||||
|
regex_sb.append("(?:/[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?");
|
||||||
|
|
||||||
|
// Fragment.
|
||||||
|
regex_sb.append("(?:#[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?");
|
||||||
|
|
||||||
|
// End second matching group.
|
||||||
|
regex_sb.append(")");
|
||||||
|
|
||||||
|
URL_MATCH_REGEX = Pattern.compile(
|
||||||
|
regex_sb.toString(),
|
||||||
|
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
|
||||||
|
|
||||||
|
return URL_MATCH_REGEX;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LinkedHashSet<CharSequence> extractUrls(String text) {
|
||||||
|
LinkedHashSet<CharSequence> urlSet = new LinkedHashSet<>();
|
||||||
|
Matcher matcher = getUrlMatchRegex().matcher(text);
|
||||||
|
|
||||||
|
while (matcher.find()) {
|
||||||
|
int matchStart = matcher.start(1);
|
||||||
|
int matchEnd = matcher.end();
|
||||||
|
String url = text.substring(matchStart, matchEnd);
|
||||||
|
urlSet.add(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
return urlSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,123 @@
|
|||||||
|
package com.termux.shared.file;
|
||||||
|
|
||||||
|
import android.os.Environment;
|
||||||
|
|
||||||
|
import com.termux.shared.models.errors.Error;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class TermuxFileUtils {
|
||||||
|
/**
|
||||||
|
* Replace "$PREFIX/" or "~/" prefix with termux absolute paths.
|
||||||
|
*
|
||||||
|
* @param path The {@code path} to expand.
|
||||||
|
* @return Returns the {@code expand path}.
|
||||||
|
*/
|
||||||
|
public static String getExpandedTermuxPath(String path) {
|
||||||
|
if (path != null && !path.isEmpty()) {
|
||||||
|
path = path.replaceAll("^\\$PREFIX$", TermuxConstants.TERMUX_PREFIX_DIR_PATH);
|
||||||
|
path = path.replaceAll("^\\$PREFIX/", TermuxConstants.TERMUX_PREFIX_DIR_PATH + "/");
|
||||||
|
path = path.replaceAll("^~/$", TermuxConstants.TERMUX_HOME_DIR_PATH);
|
||||||
|
path = path.replaceAll("^~/", TermuxConstants.TERMUX_HOME_DIR_PATH + "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace termux absolute paths with "$PREFIX/" or "~/" prefix.
|
||||||
|
*
|
||||||
|
* @param path The {@code path} to unexpand.
|
||||||
|
* @return Returns the {@code unexpand path}.
|
||||||
|
*/
|
||||||
|
public static String getUnExpandedTermuxPath(String path) {
|
||||||
|
if (path != null && !path.isEmpty()) {
|
||||||
|
path = path.replaceAll("^" + Pattern.quote(TermuxConstants.TERMUX_PREFIX_DIR_PATH) + "/", "\\$PREFIX/");
|
||||||
|
path = path.replaceAll("^" + Pattern.quote(TermuxConstants.TERMUX_HOME_DIR_PATH) + "/", "~/");
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get canonical path.
|
||||||
|
*
|
||||||
|
* @param path The {@code path} to convert.
|
||||||
|
* @param prefixForNonAbsolutePath Optional prefix path to prefix before non-absolute paths. This
|
||||||
|
* can be set to {@code null} if non-absolute paths should
|
||||||
|
* be prefixed with "/". The call to {@link File#getCanonicalPath()}
|
||||||
|
* will automatically do this anyways.
|
||||||
|
* @param expandPath The {@code boolean} that decides if input path is first attempted to be expanded by calling
|
||||||
|
* {@link TermuxFileUtils#getExpandedTermuxPath(String)} before its passed to
|
||||||
|
* {@link FileUtils#getCanonicalPath(String, String)}.
|
||||||
|
|
||||||
|
* @return Returns the {@code canonical path}.
|
||||||
|
*/
|
||||||
|
public static String getCanonicalPath(String path, final String prefixForNonAbsolutePath, final boolean expandPath) {
|
||||||
|
if (path == null) path = "";
|
||||||
|
|
||||||
|
if (expandPath)
|
||||||
|
path = getExpandedTermuxPath(path);
|
||||||
|
|
||||||
|
return FileUtils.getCanonicalPath(path, prefixForNonAbsolutePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if {@code path} is under the allowed termux working directory paths. If it is, then
|
||||||
|
* allowed parent path is returned.
|
||||||
|
*
|
||||||
|
* @param path The {@code path} to check.
|
||||||
|
* @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 (path.startsWith(TermuxConstants.TERMUX_STORAGE_HOME_DIR_PATH + "/")) {
|
||||||
|
return TermuxConstants.TERMUX_STORAGE_HOME_DIR_PATH;
|
||||||
|
} if (path.startsWith(Environment.getExternalStorageDirectory().getAbsolutePath() + "/")) {
|
||||||
|
return Environment.getExternalStorageDirectory().getAbsolutePath();
|
||||||
|
} else if (path.startsWith("/sdcard/")) {
|
||||||
|
return "/sdcard";
|
||||||
|
} else {
|
||||||
|
return TermuxConstants.TERMUX_FILES_DIR_PATH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the existence and permissions of directory file at path as a working directory for
|
||||||
|
* termux app.
|
||||||
|
*
|
||||||
|
* The creation of missing directory and setting of missing permissions will only be done if
|
||||||
|
* {@code path} is under paths returned by {@link #getMatchedAllowedTermuxWorkingDirectoryParentPathForPath(String)}.
|
||||||
|
*
|
||||||
|
* The permissions set to directory will be {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS}.
|
||||||
|
*
|
||||||
|
* @param label The optional label for the directory file. This can optionally be {@code null}.
|
||||||
|
* @param filePath The {@code path} for file to validate or create. Symlinks will not be followed.
|
||||||
|
* @param createDirectoryIfMissing The {@code boolean} that decides if directory file
|
||||||
|
* should be created if its missing.
|
||||||
|
* @param setPermissions The {@code boolean} that decides if permissions are to be
|
||||||
|
* automatically set defined by {@code permissionsToCheck}.
|
||||||
|
* @param setMissingPermissionsOnly The {@code boolean} that decides if only missing permissions
|
||||||
|
* are to be set or if they should be overridden.
|
||||||
|
* @param ignoreErrorsIfPathIsInParentDirPath The {@code boolean} that decides if existence
|
||||||
|
* and permission errors are to be ignored if path is
|
||||||
|
* in {@code parentDirPath}.
|
||||||
|
* @param ignoreIfNotExecutable The {@code boolean} that decides if missing executable permission
|
||||||
|
* error is to be ignored. This allows making an attempt to set
|
||||||
|
* executable permissions, but ignoring if it fails.
|
||||||
|
* @return Returns the {@code error} if path is not a directory file, failed to create it,
|
||||||
|
* or validating permissions failed, otherwise {@code null}.
|
||||||
|
*/
|
||||||
|
public static Error validateDirectoryFileExistenceAndPermissions(String label, final String filePath, final boolean createDirectoryIfMissing,
|
||||||
|
final boolean setPermissions, final boolean setMissingPermissionsOnly,
|
||||||
|
final boolean ignoreErrorsIfPathIsInParentDirPath, final boolean ignoreIfNotExecutable) {
|
||||||
|
return FileUtils.validateDirectoryFileExistenceAndPermissions(label, filePath,
|
||||||
|
TermuxFileUtils.getMatchedAllowedTermuxWorkingDirectoryParentPathForPath(filePath), createDirectoryIfMissing,
|
||||||
|
FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS, setPermissions, setMissingPermissionsOnly,
|
||||||
|
ignoreErrorsIfPathIsInParentDirPath, ignoreIfNotExecutable);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -90,7 +90,7 @@ public class FileTypes {
|
|||||||
return getFileType(fileAttributes);
|
return getFileType(fileAttributes);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// If not a ENOENT (No such file or directory) exception
|
// If not a ENOENT (No such file or directory) exception
|
||||||
if (!e.getMessage().contains("ENOENT"))
|
if (e.getMessage() != null && !e.getMessage().contains("ENOENT"))
|
||||||
Logger.logError("Failed to get file type for file at path \"" + filePath + "\": " + e.getMessage());
|
Logger.logError("Failed to get file type for file at path \"" + filePath + "\": " + e.getMessage());
|
||||||
return FileType.NO_EXIST;
|
return FileType.NO_EXIST;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.annotation.NonNull;
|
|||||||
|
|
||||||
import com.termux.shared.file.FileUtils;
|
import com.termux.shared.file.FileUtils;
|
||||||
import com.termux.shared.logger.Logger;
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.models.errors.Error;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
@@ -31,18 +32,19 @@ public class FileUtilsTests {
|
|||||||
Logger.logInfo(LOG_TAG, "Running tests");
|
Logger.logInfo(LOG_TAG, "Running tests");
|
||||||
Logger.logInfo(LOG_TAG, "testRootDirectoryPath: \"" + testRootDirectoryPath + "\"");
|
Logger.logInfo(LOG_TAG, "testRootDirectoryPath: \"" + testRootDirectoryPath + "\"");
|
||||||
|
|
||||||
String fileUtilsTestsDirectoryCanonicalPath = FileUtils.getCanonicalPath(testRootDirectoryPath, null, false);
|
String fileUtilsTestsDirectoryCanonicalPath = FileUtils.getCanonicalPath(testRootDirectoryPath, null);
|
||||||
assertEqual("FileUtilsTests directory path is not a canonical path", testRootDirectoryPath, fileUtilsTestsDirectoryCanonicalPath);
|
assertEqual("FileUtilsTests directory path is not a canonical path", testRootDirectoryPath, fileUtilsTestsDirectoryCanonicalPath);
|
||||||
|
|
||||||
runTestsInner(context, testRootDirectoryPath);
|
runTestsInner(testRootDirectoryPath);
|
||||||
Logger.logInfo(LOG_TAG, "All tests successful");
|
Logger.logInfo(LOG_TAG, "All tests successful");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Logger.logErrorAndShowToast(context, LOG_TAG, e.getMessage());
|
Logger.logErrorExtended(LOG_TAG, e.getMessage());
|
||||||
|
Logger.showToast(context, e.getMessage() != null ? e.getMessage().replaceAll("(?s)\nFull Error:\n.*", "") : null, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void runTestsInner(@NonNull final Context context, @NonNull final String testRootDirectoryPath) throws Exception {
|
private static void runTestsInner(@NonNull final String testRootDirectoryPath) throws Exception {
|
||||||
String errmsg;
|
Error error;
|
||||||
String label;
|
String label;
|
||||||
String path;
|
String path;
|
||||||
|
|
||||||
@@ -101,20 +103,20 @@ public class FileUtilsTests {
|
|||||||
|
|
||||||
// Create or clear test root directory file
|
// Create or clear test root directory file
|
||||||
label = "testRootDirectoryPath";
|
label = "testRootDirectoryPath";
|
||||||
errmsg = FileUtils.clearDirectory(context, label, testRootDirectoryPath);
|
error = FileUtils.clearDirectory(label, testRootDirectoryPath);
|
||||||
assertEqual("Failed to create " + label + " directory file", null, errmsg);
|
assertEqual("Failed to create " + label + " directory file", null, error);
|
||||||
|
|
||||||
if (!FileUtils.directoryFileExists(testRootDirectoryPath, false))
|
if (!FileUtils.directoryFileExists(testRootDirectoryPath, false))
|
||||||
throwException("The " + label + " directory file does not exist as expected after creation");
|
throwException("The " + label + " directory file does not exist as expected after creation");
|
||||||
|
|
||||||
|
|
||||||
// Create dir1 directory file
|
// Create dir1 directory file
|
||||||
errmsg = FileUtils.createDirectoryFile(context, dir1_label, dir1_path);
|
error = FileUtils.createDirectoryFile(dir1_label, dir1_path);
|
||||||
assertEqual("Failed to create " + dir1_label + " directory file", null, errmsg);
|
assertEqual("Failed to create " + dir1_label + " directory file", null, error);
|
||||||
|
|
||||||
// Create dir2 directory file
|
// Create dir2 directory file
|
||||||
errmsg = FileUtils.createDirectoryFile(context, dir2_label, dir2_path);
|
error = FileUtils.createDirectoryFile(dir2_label, dir2_path);
|
||||||
assertEqual("Failed to create " + dir2_label + " directory file", null, errmsg);
|
assertEqual("Failed to create " + dir2_label + " directory file", null, error);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -122,29 +124,29 @@ public class FileUtilsTests {
|
|||||||
|
|
||||||
// Create dir1/sub_dir1 directory file
|
// Create dir1/sub_dir1 directory file
|
||||||
label = dir1__sub_dir1_label; path = dir1__sub_dir1_path;
|
label = dir1__sub_dir1_label; path = dir1__sub_dir1_path;
|
||||||
errmsg = FileUtils.createDirectoryFile(context, label, path);
|
error = FileUtils.createDirectoryFile(label, path);
|
||||||
assertEqual("Failed to create " + label + " directory file", null, errmsg);
|
assertEqual("Failed to create " + label + " directory file", null, error);
|
||||||
if (!FileUtils.directoryFileExists(path, false))
|
if (!FileUtils.directoryFileExists(path, false))
|
||||||
throwException("The " + label + " directory file does not exist as expected after creation");
|
throwException("The " + label + " directory file does not exist as expected after creation");
|
||||||
|
|
||||||
// Create dir1/sub_reg1 regular file
|
// Create dir1/sub_reg1 regular file
|
||||||
label = dir1__sub_reg1_label; path = dir1__sub_reg1_path;
|
label = dir1__sub_reg1_label; path = dir1__sub_reg1_path;
|
||||||
errmsg = FileUtils.createRegularFile(context, label, path);
|
error = FileUtils.createRegularFile(label, path);
|
||||||
assertEqual("Failed to create " + label + " regular file", null, errmsg);
|
assertEqual("Failed to create " + label + " regular file", null, error);
|
||||||
if (!FileUtils.regularFileExists(path, false))
|
if (!FileUtils.regularFileExists(path, false))
|
||||||
throwException("The " + label + " regular file does not exist as expected after creation");
|
throwException("The " + label + " regular file does not exist as expected after creation");
|
||||||
|
|
||||||
// Create dir1/sub_sym1 -> dir2 absolute symlink file
|
// Create dir1/sub_sym1 -> dir2 absolute symlink file
|
||||||
label = dir1__sub_sym1_label; path = dir1__sub_sym1_path;
|
label = dir1__sub_sym1_label; path = dir1__sub_sym1_path;
|
||||||
errmsg = FileUtils.createSymlinkFile(context, label, dir2_path, path);
|
error = FileUtils.createSymlinkFile(label, dir2_path, path);
|
||||||
assertEqual("Failed to create " + label + " symlink file", null, errmsg);
|
assertEqual("Failed to create " + label + " symlink file", null, error);
|
||||||
if (!FileUtils.symlinkFileExists(path))
|
if (!FileUtils.symlinkFileExists(path))
|
||||||
throwException("The " + label + " symlink file does not exist as expected after creation");
|
throwException("The " + label + " symlink file does not exist as expected after creation");
|
||||||
|
|
||||||
// Copy dir1/sub_sym1 symlink file to dir1/sub_sym2
|
// Copy dir1/sub_sym1 symlink file to dir1/sub_sym2
|
||||||
label = dir1__sub_sym2_label; path = dir1__sub_sym2_path;
|
label = dir1__sub_sym2_label; path = dir1__sub_sym2_path;
|
||||||
errmsg = FileUtils.copySymlinkFile(context, label, dir1__sub_sym1_path, path, false);
|
error = FileUtils.copySymlinkFile(label, dir1__sub_sym1_path, path, false);
|
||||||
assertEqual("Failed to copy " + dir1__sub_sym1_label + " symlink file to " + label, null, errmsg);
|
assertEqual("Failed to copy " + dir1__sub_sym1_label + " symlink file to " + label, null, error);
|
||||||
if (!FileUtils.symlinkFileExists(path))
|
if (!FileUtils.symlinkFileExists(path))
|
||||||
throwException("The " + label + " symlink file does not exist as expected after copying it from " + dir1__sub_sym1_label);
|
throwException("The " + label + " symlink file does not exist as expected after copying it from " + dir1__sub_sym1_label);
|
||||||
if (!new File(path).getCanonicalPath().equals(dir2_path))
|
if (!new File(path).getCanonicalPath().equals(dir2_path))
|
||||||
@@ -156,25 +158,25 @@ public class FileUtilsTests {
|
|||||||
|
|
||||||
// Write "line1" to dir2/sub_reg1 regular file
|
// Write "line1" to dir2/sub_reg1 regular file
|
||||||
label = dir2__sub_reg1_label; path = dir2__sub_reg1_path;
|
label = dir2__sub_reg1_label; path = dir2__sub_reg1_path;
|
||||||
errmsg = FileUtils.writeStringToFile(context, label, path, Charset.defaultCharset(), "line1", false);
|
error = FileUtils.writeStringToFile(label, path, Charset.defaultCharset(), "line1", false);
|
||||||
assertEqual("Failed to write string to " + label + " file with append mode false", null, errmsg);
|
assertEqual("Failed to write string to " + label + " file with append mode false", null, error);
|
||||||
if (!FileUtils.regularFileExists(path, false))
|
if (!FileUtils.regularFileExists(path, false))
|
||||||
throwException("The " + label + " file does not exist as expected after writing to it with append mode false");
|
throwException("The " + label + " file does not exist as expected after writing to it with append mode false");
|
||||||
|
|
||||||
// Write "line2" to dir2/sub_reg1 regular file
|
// Write "line2" to dir2/sub_reg1 regular file
|
||||||
errmsg = FileUtils.writeStringToFile(context, label, path, Charset.defaultCharset(), "\nline2", true);
|
error = FileUtils.writeStringToFile(label, path, Charset.defaultCharset(), "\nline2", true);
|
||||||
assertEqual("Failed to write string to " + label + " file with append mode true", null, errmsg);
|
assertEqual("Failed to write string to " + label + " file with append mode true", null, error);
|
||||||
|
|
||||||
// Read dir2/sub_reg1 regular file
|
// Read dir2/sub_reg1 regular file
|
||||||
StringBuilder dataStringBuilder = new StringBuilder();
|
StringBuilder dataStringBuilder = new StringBuilder();
|
||||||
errmsg = FileUtils.readStringFromFile(context, label, path, Charset.defaultCharset(), dataStringBuilder, false);
|
error = FileUtils.readStringFromFile(label, path, Charset.defaultCharset(), dataStringBuilder, false);
|
||||||
assertEqual("Failed to read from " + label + " file", null, errmsg);
|
assertEqual("Failed to read from " + label + " file", null, error);
|
||||||
assertEqual("The data read from " + label + " file in not as expected", "line1\nline2", dataStringBuilder.toString());
|
assertEqual("The data read from " + label + " file in not as expected", "line1\nline2", dataStringBuilder.toString());
|
||||||
|
|
||||||
// Copy dir2/sub_reg1 regular file to dir2/sub_reg2 file
|
// Copy dir2/sub_reg1 regular file to dir2/sub_reg2 file
|
||||||
label = dir2__sub_reg2_label; path = dir2__sub_reg2_path;
|
label = dir2__sub_reg2_label; path = dir2__sub_reg2_path;
|
||||||
errmsg = FileUtils.copyRegularFile(context, label, dir2__sub_reg1_path, path, false);
|
error = FileUtils.copyRegularFile(label, dir2__sub_reg1_path, path, false);
|
||||||
assertEqual("Failed to copy " + dir2__sub_reg1_label + " regular file to " + label, null, errmsg);
|
assertEqual("Failed to copy " + dir2__sub_reg1_label + " regular file to " + label, null, error);
|
||||||
if (!FileUtils.regularFileExists(path, false))
|
if (!FileUtils.regularFileExists(path, false))
|
||||||
throwException("The " + label + " regular file does not exist as expected after copying it from " + dir2__sub_reg1_label);
|
throwException("The " + label + " regular file does not exist as expected after copying it from " + dir2__sub_reg1_label);
|
||||||
|
|
||||||
@@ -184,22 +186,22 @@ public class FileUtilsTests {
|
|||||||
|
|
||||||
// Copy dir1 directory file to dir3
|
// Copy dir1 directory file to dir3
|
||||||
label = dir3_label; path = dir3_path;
|
label = dir3_label; path = dir3_path;
|
||||||
errmsg = FileUtils.copyDirectoryFile(context, label, dir2_path, path, false);
|
error = FileUtils.copyDirectoryFile(label, dir2_path, path, false);
|
||||||
assertEqual("Failed to copy " + dir2_label + " directory file to " + label, null, errmsg);
|
assertEqual("Failed to copy " + dir2_label + " directory file to " + label, null, error);
|
||||||
if (!FileUtils.directoryFileExists(path, false))
|
if (!FileUtils.directoryFileExists(path, false))
|
||||||
throwException("The " + label + " directory file does not exist as expected after copying it from " + dir2_label);
|
throwException("The " + label + " directory file does not exist as expected after copying it from " + dir2_label);
|
||||||
|
|
||||||
// Copy dir1 directory file to dir3 again to test overwrite
|
// Copy dir1 directory file to dir3 again to test overwrite
|
||||||
label = dir3_label; path = dir3_path;
|
label = dir3_label; path = dir3_path;
|
||||||
errmsg = FileUtils.copyDirectoryFile(context, label, dir2_path, path, false);
|
error = FileUtils.copyDirectoryFile(label, dir2_path, path, false);
|
||||||
assertEqual("Failed to copy " + dir2_label + " directory file to " + label, null, errmsg);
|
assertEqual("Failed to copy " + dir2_label + " directory file to " + label, null, error);
|
||||||
if (!FileUtils.directoryFileExists(path, false))
|
if (!FileUtils.directoryFileExists(path, false))
|
||||||
throwException("The " + label + " directory file does not exist as expected after copying it from " + dir2_label);
|
throwException("The " + label + " directory file does not exist as expected after copying it from " + dir2_label);
|
||||||
|
|
||||||
// Move dir3 directory file to dir4
|
// Move dir3 directory file to dir4
|
||||||
label = dir4_label; path = dir4_path;
|
label = dir4_label; path = dir4_path;
|
||||||
errmsg = FileUtils.moveDirectoryFile(context, label, dir3_path, path, false);
|
error = FileUtils.moveDirectoryFile(label, dir3_path, path, false);
|
||||||
assertEqual("Failed to move " + dir3_label + " directory file to " + label, null, errmsg);
|
assertEqual("Failed to move " + dir3_label + " directory file to " + label, null, error);
|
||||||
if (!FileUtils.directoryFileExists(path, false))
|
if (!FileUtils.directoryFileExists(path, false))
|
||||||
throwException("The " + label + " directory file does not exist as expected after copying it from " + dir3_label);
|
throwException("The " + label + " directory file does not exist as expected after copying it from " + dir3_label);
|
||||||
|
|
||||||
@@ -209,16 +211,16 @@ public class FileUtilsTests {
|
|||||||
|
|
||||||
// Create dir1/sub_sym3 -> dir4 relative symlink file
|
// Create dir1/sub_sym3 -> dir4 relative symlink file
|
||||||
label = dir1__sub_sym3_label; path = dir1__sub_sym3_path;
|
label = dir1__sub_sym3_label; path = dir1__sub_sym3_path;
|
||||||
errmsg = FileUtils.createSymlinkFile(context, label, "../dir4", path);
|
error = FileUtils.createSymlinkFile(label, "../dir4", path);
|
||||||
assertEqual("Failed to create " + label + " symlink file", null, errmsg);
|
assertEqual("Failed to create " + label + " symlink file", null, error);
|
||||||
if (!FileUtils.symlinkFileExists(path))
|
if (!FileUtils.symlinkFileExists(path))
|
||||||
throwException("The " + label + " symlink file does not exist as expected after creation");
|
throwException("The " + label + " symlink file does not exist as expected after creation");
|
||||||
|
|
||||||
// Create dir1/sub_sym3 -> dirX relative dangling symlink file
|
// Create dir1/sub_sym3 -> dirX relative dangling symlink file
|
||||||
// This is to ensure that symlinkFileExists returns true if a symlink file exists but is dangling
|
// This is to ensure that symlinkFileExists returns true if a symlink file exists but is dangling
|
||||||
label = dir1__sub_sym3_label; path = dir1__sub_sym3_path;
|
label = dir1__sub_sym3_label; path = dir1__sub_sym3_path;
|
||||||
errmsg = FileUtils.createSymlinkFile(context, label, "../dirX", path);
|
error = FileUtils.createSymlinkFile(label, "../dirX", path);
|
||||||
assertEqual("Failed to create " + label + " symlink file", null, errmsg);
|
assertEqual("Failed to create " + label + " symlink file", null, error);
|
||||||
if (!FileUtils.symlinkFileExists(path))
|
if (!FileUtils.symlinkFileExists(path))
|
||||||
throwException("The " + label + " dangling symlink file does not exist as expected after creation");
|
throwException("The " + label + " dangling symlink file does not exist as expected after creation");
|
||||||
|
|
||||||
@@ -228,8 +230,8 @@ public class FileUtilsTests {
|
|||||||
|
|
||||||
// Delete dir1/sub_sym2 symlink file
|
// Delete dir1/sub_sym2 symlink file
|
||||||
label = dir1__sub_sym2_label; path = dir1__sub_sym2_path;
|
label = dir1__sub_sym2_label; path = dir1__sub_sym2_path;
|
||||||
errmsg = FileUtils.deleteSymlinkFile(context, label, path, false);
|
error = FileUtils.deleteSymlinkFile(label, path, false);
|
||||||
assertEqual("Failed to delete " + label + " symlink file", null, errmsg);
|
assertEqual("Failed to delete " + label + " symlink file", null, error);
|
||||||
if (FileUtils.fileExists(path, false))
|
if (FileUtils.fileExists(path, false))
|
||||||
throwException("The " + label + " symlink file still exist after deletion");
|
throwException("The " + label + " symlink file still exist after deletion");
|
||||||
|
|
||||||
@@ -245,8 +247,8 @@ public class FileUtilsTests {
|
|||||||
|
|
||||||
// Delete dir1 directory file
|
// Delete dir1 directory file
|
||||||
label = dir1_label; path = dir1_path;
|
label = dir1_label; path = dir1_path;
|
||||||
errmsg = FileUtils.deleteDirectoryFile(context, label, path, false);
|
error = FileUtils.deleteDirectoryFile(label, path, false);
|
||||||
assertEqual("Failed to delete " + label + " directory file", null, errmsg);
|
assertEqual("Failed to delete " + label + " directory file", null, error);
|
||||||
if (FileUtils.fileExists(path, false))
|
if (FileUtils.fileExists(path, false))
|
||||||
throwException("The " + label + " directory file still exist after deletion");
|
throwException("The " + label + " directory file still exist after deletion");
|
||||||
|
|
||||||
@@ -267,8 +269,8 @@ public class FileUtilsTests {
|
|||||||
|
|
||||||
// Delete dir2/sub_reg1 regular file
|
// Delete dir2/sub_reg1 regular file
|
||||||
label = dir2__sub_reg1_label; path = dir2__sub_reg1_path;
|
label = dir2__sub_reg1_label; path = dir2__sub_reg1_path;
|
||||||
errmsg = FileUtils.deleteRegularFile(context, label, path, false);
|
error = FileUtils.deleteRegularFile(label, path, false);
|
||||||
assertEqual("Failed to delete " + label + " regular file", null, errmsg);
|
assertEqual("Failed to delete " + label + " regular file", null, error);
|
||||||
if (FileUtils.fileExists(path, false))
|
if (FileUtils.fileExists(path, false))
|
||||||
throwException("The " + label + " regular file still exist after deletion");
|
throwException("The " + label + " regular file still exist after deletion");
|
||||||
|
|
||||||
@@ -276,6 +278,14 @@ public class FileUtilsTests {
|
|||||||
FileUtils.getFileType("/dev/null", false);
|
FileUtils.getFileType("/dev/null", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static void assertEqual(@NonNull final String message, final String expected, final Error actual) throws Exception {
|
||||||
|
String actualString = actual != null ? actual.getMessage() : null;
|
||||||
|
if (!equalsRegardingNull(expected, actualString))
|
||||||
|
throwException(message + "\nexpected: \"" + expected + "\"\nactual: \"" + actualString + "\"\nFull Error:\n" + (actual != null ? actual.toString() : ""));
|
||||||
|
}
|
||||||
|
|
||||||
public static void assertEqual(@NonNull final String message, final String expected, final String actual) throws Exception {
|
public static void assertEqual(@NonNull final String message, final String expected, final String actual) throws Exception {
|
||||||
if (!equalsRegardingNull(expected, actual))
|
if (!equalsRegardingNull(expected, actual))
|
||||||
throwException(message + "\nexpected: \"" + expected + "\"\nactual: \"" + actual + "\"");
|
throwException(message + "\nexpected: \"" + expected + "\"\nactual: \"" + actual + "\"");
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.termux.shared.interact;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.termux.shared.R;
|
||||||
|
|
||||||
|
public class MessageDialogUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a message in a dialog
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} to use to start the dialog. An {@link Activity} {@link Context}
|
||||||
|
* must be passed, otherwise exceptions will be thrown.
|
||||||
|
* @param titleText The title text of the dialog.
|
||||||
|
* @param messageText The message text of the dialog.
|
||||||
|
* @param onDismiss The {@link DialogInterface.OnDismissListener} to run when dialog is dismissed.
|
||||||
|
*/
|
||||||
|
public static void showMessage(Context context, String titleText, String messageText, final DialogInterface.OnDismissListener onDismiss) {
|
||||||
|
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.Theme_AppCompat_Light_Dialog)
|
||||||
|
.setPositiveButton(android.R.string.ok, null);
|
||||||
|
|
||||||
|
LayoutInflater inflater = (LayoutInflater) context.getSystemService( Context.LAYOUT_INFLATER_SERVICE );
|
||||||
|
View view = inflater.inflate(R.layout.dialog_show_message, null);
|
||||||
|
if (view != null) {
|
||||||
|
builder.setView(view);
|
||||||
|
|
||||||
|
TextView titleView = view.findViewById(R.id.dialog_title);
|
||||||
|
if (titleView != null)
|
||||||
|
titleView.setText(titleText);
|
||||||
|
|
||||||
|
TextView messageView = view.findViewById(R.id.dialog_message);
|
||||||
|
if (messageView != null)
|
||||||
|
messageView.setText(messageText);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onDismiss != null)
|
||||||
|
builder.setOnDismissListener(onDismiss);
|
||||||
|
|
||||||
|
builder.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void exitAppWithErrorMessage(Context context, String titleText, String messageText) {
|
||||||
|
showMessage(context, titleText, messageText, dialog -> System.exit(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ import android.widget.TextView;
|
|||||||
|
|
||||||
import com.termux.shared.R;
|
import com.termux.shared.R;
|
||||||
|
|
||||||
public final class DialogUtils {
|
public final class TextInputDialogUtils {
|
||||||
|
|
||||||
public interface TextSetListener {
|
public interface TextSetListener {
|
||||||
void onTextSet(String text);
|
void onTextSet(String text);
|
||||||
@@ -75,42 +75,4 @@ public final class DialogUtils {
|
|||||||
dialogHolder[0].show();
|
dialogHolder[0].show();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Show a message in a dialog
|
|
||||||
*
|
|
||||||
* @param context The {@link Context} to use to start the dialog. An {@link Activity} {@link Context}
|
|
||||||
* must be passed, otherwise exceptions will be thrown.
|
|
||||||
* @param titleText The title text of the dialog.
|
|
||||||
* @param messageText The message text of the dialog.
|
|
||||||
* @param onDismiss The {@link DialogInterface.OnDismissListener} to run when dialog is dismissed.
|
|
||||||
*/
|
|
||||||
public static void showMessage(Context context, String titleText, String messageText, final DialogInterface.OnDismissListener onDismiss) {
|
|
||||||
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.Theme_AppCompat_Light_Dialog)
|
|
||||||
.setPositiveButton(android.R.string.ok, null);
|
|
||||||
|
|
||||||
LayoutInflater inflater = (LayoutInflater) context.getSystemService( Context.LAYOUT_INFLATER_SERVICE );
|
|
||||||
View view = inflater.inflate(R.layout.dialog_show_message, null);
|
|
||||||
if (view != null) {
|
|
||||||
builder.setView(view);
|
|
||||||
|
|
||||||
TextView titleView = view.findViewById(R.id.dialog_title);
|
|
||||||
if (titleView != null)
|
|
||||||
titleView.setText(titleText);
|
|
||||||
|
|
||||||
TextView messageView = view.findViewById(R.id.dialog_message);
|
|
||||||
if (messageView != null)
|
|
||||||
messageView.setText(messageText);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onDismiss != null)
|
|
||||||
builder.setOnDismissListener(onDismiss);
|
|
||||||
|
|
||||||
builder.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void exitAppWithErrorMessage(Context context, String titleText, String messageText) {
|
|
||||||
showMessage(context, titleText, messageText, dialog -> System.exit(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,7 @@ import com.termux.shared.termux.TermuxConstants;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.io.StringWriter;
|
import java.io.StringWriter;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -27,7 +28,25 @@ public class Logger {
|
|||||||
public static final int DEFAULT_LOG_LEVEL = LOG_LEVEL_NORMAL;
|
public static final int DEFAULT_LOG_LEVEL = LOG_LEVEL_NORMAL;
|
||||||
private static int CURRENT_LOG_LEVEL = DEFAULT_LOG_LEVEL;
|
private static int CURRENT_LOG_LEVEL = DEFAULT_LOG_LEVEL;
|
||||||
|
|
||||||
public static final int LOGGER_ENTRY_SIZE_LIMIT_IN_BYTES = 4 * 1024; // 4KB
|
/**
|
||||||
|
* The maximum size of the log entry payload that can be written to the logger. An attempt to
|
||||||
|
* write more than this amount will result in a truncated log entry.
|
||||||
|
*
|
||||||
|
* The limit is 4068 but this includes log tag and log level prefix "D/" before log tag and ": "
|
||||||
|
* suffix after it.
|
||||||
|
*
|
||||||
|
* #define LOGGER_ENTRY_MAX_PAYLOAD 4068
|
||||||
|
* https://cs.android.com/android/_/android/platform/system/core/+/android10-release:liblog/include/log/log_read.h;l=127
|
||||||
|
*/
|
||||||
|
public static final int LOGGER_ENTRY_MAX_PAYLOAD = 4068; // 4068 bytes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum safe size of the log entry payload that can be written to the logger, based on
|
||||||
|
* {@link #LOGGER_ENTRY_MAX_PAYLOAD}. Using 4000 as a safe limit to give log tag and its
|
||||||
|
* prefix/suffix max 68 characters for itself. Use "log*Extended()" functions to use max possible
|
||||||
|
* limit if tag is already known.
|
||||||
|
*/
|
||||||
|
public static final int LOGGER_ENTRY_MAX_SAFE_PAYLOAD = 4000; // 4000 bytes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -44,6 +63,40 @@ public class Logger {
|
|||||||
Log.v(getFullTag(tag), message);
|
Log.v(getFullTag(tag), message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void logExtendedMessage(int logLevel, String tag, String message) {
|
||||||
|
if (message == null) return;
|
||||||
|
|
||||||
|
int cutOffIndex;
|
||||||
|
int nextNewlineIndex;
|
||||||
|
String prefix = "";
|
||||||
|
|
||||||
|
// -8 for prefix "(xx/xx)" (max 99 sections), - log tag length, -4 for log tag prefix "D/" and suffix ": "
|
||||||
|
int maxEntrySize = LOGGER_ENTRY_MAX_PAYLOAD - 8 - getFullTag(tag).length() - 4;
|
||||||
|
|
||||||
|
List<String> messagesList = new ArrayList<>();
|
||||||
|
|
||||||
|
while(!message.isEmpty()) {
|
||||||
|
if (message.length() > maxEntrySize) {
|
||||||
|
cutOffIndex = maxEntrySize;
|
||||||
|
nextNewlineIndex = message.lastIndexOf('\n', cutOffIndex);
|
||||||
|
if (nextNewlineIndex != -1) {
|
||||||
|
cutOffIndex = nextNewlineIndex + 1;
|
||||||
|
}
|
||||||
|
messagesList.add(message.substring(0, cutOffIndex));
|
||||||
|
message = message.substring(cutOffIndex);
|
||||||
|
} else {
|
||||||
|
messagesList.add(message);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int i=0; i<messagesList.size(); i++) {
|
||||||
|
if (messagesList.size() > 1)
|
||||||
|
prefix = "(" + (i + 1) + "/" + messagesList.size() + ")\n";
|
||||||
|
logMessage(logLevel, tag, prefix + messagesList.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public static void logError(String tag, String message) {
|
public static void logError(String tag, String message) {
|
||||||
@@ -54,6 +107,14 @@ public class Logger {
|
|||||||
logMessage(Log.ERROR, DEFAULT_LOG_TAG, message);
|
logMessage(Log.ERROR, DEFAULT_LOG_TAG, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void logErrorExtended(String tag, String message) {
|
||||||
|
logExtendedMessage(Log.ERROR, tag, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void logErrorExtended(String message) {
|
||||||
|
logExtendedMessage(Log.ERROR, DEFAULT_LOG_TAG, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public static void logWarn(String tag, String message) {
|
public static void logWarn(String tag, String message) {
|
||||||
@@ -64,6 +125,14 @@ public class Logger {
|
|||||||
logMessage(Log.WARN, DEFAULT_LOG_TAG, message);
|
logMessage(Log.WARN, DEFAULT_LOG_TAG, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void logWarnExtended(String tag, String message) {
|
||||||
|
logExtendedMessage(Log.WARN, tag, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void logWarnExtended(String message) {
|
||||||
|
logExtendedMessage(Log.WARN, DEFAULT_LOG_TAG, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public static void logInfo(String tag, String message) {
|
public static void logInfo(String tag, String message) {
|
||||||
@@ -74,6 +143,14 @@ public class Logger {
|
|||||||
logMessage(Log.INFO, DEFAULT_LOG_TAG, message);
|
logMessage(Log.INFO, DEFAULT_LOG_TAG, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void logInfoExtended(String tag, String message) {
|
||||||
|
logExtendedMessage(Log.INFO, tag, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void logInfoExtended(String message) {
|
||||||
|
logExtendedMessage(Log.INFO, DEFAULT_LOG_TAG, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public static void logDebug(String tag, String message) {
|
public static void logDebug(String tag, String message) {
|
||||||
@@ -84,6 +161,14 @@ public class Logger {
|
|||||||
logMessage(Log.DEBUG, DEFAULT_LOG_TAG, message);
|
logMessage(Log.DEBUG, DEFAULT_LOG_TAG, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void logDebugExtended(String tag, String message) {
|
||||||
|
logExtendedMessage(Log.DEBUG, tag, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void logDebugExtended(String message) {
|
||||||
|
logExtendedMessage(Log.DEBUG, DEFAULT_LOG_TAG, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public static void logVerbose(String tag, String message) {
|
public static void logVerbose(String tag, String message) {
|
||||||
@@ -94,6 +179,14 @@ public class Logger {
|
|||||||
logMessage(Log.VERBOSE, DEFAULT_LOG_TAG, message);
|
logMessage(Log.VERBOSE, DEFAULT_LOG_TAG, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void logVerboseExtended(String tag, String message) {
|
||||||
|
logExtendedMessage(Log.VERBOSE, tag, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void logVerboseExtended(String message) {
|
||||||
|
logExtendedMessage(Log.VERBOSE, DEFAULT_LOG_TAG, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public static void logErrorAndShowToast(Context context, String tag, String message) {
|
public static void logErrorAndShowToast(Context context, String tag, String message) {
|
||||||
@@ -127,8 +220,7 @@ public class Logger {
|
|||||||
|
|
||||||
|
|
||||||
public static void logStackTraceWithMessage(String tag, String message, Throwable throwable) {
|
public static void logStackTraceWithMessage(String tag, String message, Throwable throwable) {
|
||||||
if (CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL)
|
Logger.logErrorExtended(tag, getMessageAndStackTraceString(message, throwable));
|
||||||
Log.e(getFullTag(tag), getMessageAndStackTraceString(message, throwable));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void logStackTraceWithMessage(String message, Throwable throwable) {
|
public static void logStackTraceWithMessage(String message, Throwable throwable) {
|
||||||
@@ -143,11 +235,14 @@ public class Logger {
|
|||||||
logStackTraceWithMessage(DEFAULT_LOG_TAG, null, throwable);
|
logStackTraceWithMessage(DEFAULT_LOG_TAG, null, throwable);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void logStackTracesWithMessage(String tag, String message, List<Throwable> throwableList) {
|
|
||||||
if (CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL)
|
|
||||||
Log.e(getFullTag(tag), getMessageAndStackTracesString(message, throwableList));
|
public static void logStackTracesWithMessage(String tag, String message, List<Throwable> throwablesList) {
|
||||||
|
Logger.logErrorExtended(tag, getMessageAndStackTracesString(message, throwablesList));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public static String getMessageAndStackTraceString(String message, Throwable throwable) {
|
public static String getMessageAndStackTraceString(String message, Throwable throwable) {
|
||||||
if (message == null && throwable == null)
|
if (message == null && throwable == null)
|
||||||
return null;
|
return null;
|
||||||
@@ -159,17 +254,19 @@ public class Logger {
|
|||||||
return getStackTraceString(throwable);
|
return getStackTraceString(throwable);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getMessageAndStackTracesString(String message, List<Throwable> throwableList) {
|
public static String getMessageAndStackTracesString(String message, List<Throwable> throwablesList) {
|
||||||
if (message == null && (throwableList == null || throwableList.size() == 0))
|
if (message == null && (throwablesList == null || throwablesList.size() == 0))
|
||||||
return null;
|
return null;
|
||||||
else if (message != null && (throwableList != null && throwableList.size() != 0))
|
else if (message != null && (throwablesList != null && throwablesList.size() != 0))
|
||||||
return message + ":\n" + getStackTracesString(null, getStackTraceStringArray(throwableList));
|
return message + ":\n" + getStackTracesString(null, getStackTracesStringArray(throwablesList));
|
||||||
else if (throwableList == null || throwableList.size() == 0)
|
else if (throwablesList == null || throwablesList.size() == 0)
|
||||||
return message;
|
return message;
|
||||||
else
|
else
|
||||||
return getStackTracesString(null, getStackTraceStringArray(throwableList));
|
return getStackTracesString(null, getStackTracesStringArray(throwablesList));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public static String getStackTraceString(Throwable throwable) {
|
public static String getStackTraceString(Throwable throwable) {
|
||||||
if (throwable == null) return null;
|
if (throwable == null) return null;
|
||||||
|
|
||||||
@@ -182,27 +279,30 @@ public class Logger {
|
|||||||
pw.close();
|
pw.close();
|
||||||
stackTraceString = errors.toString();
|
stackTraceString = errors.toString();
|
||||||
errors.close();
|
errors.close();
|
||||||
} catch (IOException e1) {
|
} catch (IOException e) {
|
||||||
e1.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
return stackTraceString;
|
return stackTraceString;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String[] getStackTraceStringArray(Throwable throwable) {
|
|
||||||
return getStackTraceStringArray(Collections.singletonList(throwable));
|
|
||||||
|
public static String[] getStackTracesStringArray(Throwable throwable) {
|
||||||
|
return getStackTracesStringArray(Collections.singletonList(throwable));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String[] getStackTraceStringArray(List<Throwable> throwableList) {
|
public static String[] getStackTracesStringArray(List<Throwable> throwablesList) {
|
||||||
if (throwableList == null) return null;
|
if (throwablesList == null) return null;
|
||||||
|
final String[] stackTraceStringArray = new String[throwablesList.size()];
|
||||||
final String[] stackTraceStringArray = new String[throwableList.size()];
|
for (int i = 0; i < throwablesList.size(); i++) {
|
||||||
for (int i = 0; i < throwableList.size(); i++) {
|
stackTraceStringArray[i] = getStackTraceString(throwablesList.get(i));
|
||||||
stackTraceStringArray[i] = getStackTraceString(throwableList.get(i));
|
|
||||||
}
|
}
|
||||||
return stackTraceStringArray;
|
return stackTraceStringArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public static String getStackTracesString(String label, String[] stackTraceStringArray) {
|
public static String getStackTracesString(String label, String[] stackTraceStringArray) {
|
||||||
if (label == null) label = "StackTraces:";
|
if (label == null) label = "StackTraces:";
|
||||||
StringBuilder stackTracesString = new StringBuilder(label);
|
StringBuilder stackTracesString = new StringBuilder(label);
|
||||||
@@ -254,7 +354,7 @@ public class Logger {
|
|||||||
else
|
else
|
||||||
return label + ": " + def;
|
return label + ": " + def;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public static void showToast(final Context context, final String toastText, boolean longDuration) {
|
public static void showToast(final Context context, final String toastText, boolean longDuration) {
|
||||||
@@ -283,7 +383,7 @@ public class Logger {
|
|||||||
logLevelLabels[i] = getLogLevelLabel(context, Integer.parseInt(logLevels[i].toString()), addDefaultTag);
|
logLevelLabels[i] = getLogLevelLabel(context, Integer.parseInt(logLevels[i].toString()), addDefaultTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
return logLevelLabels;
|
return logLevelLabels;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getLogLevelLabel(final Context context, final int logLevel, final boolean addDefaultTag) {
|
public static String getLogLevelLabel(final Context context, final int logLevel, final boolean addDefaultTag) {
|
||||||
|
|||||||
@@ -90,10 +90,12 @@ public class MarkdownUtils {
|
|||||||
|
|
||||||
int maxCount = 0;
|
int maxCount = 0;
|
||||||
int matchCount;
|
int matchCount;
|
||||||
|
String match;
|
||||||
|
|
||||||
Matcher matcher = backticksPattern.matcher(string);
|
Matcher matcher = backticksPattern.matcher(string);
|
||||||
while(matcher.find()) {
|
while(matcher.find()) {
|
||||||
matchCount = matcher.group(1).length();
|
match = matcher.group(1);
|
||||||
|
matchCount = match != null ? match.length() : 0;
|
||||||
if (matchCount > maxCount)
|
if (matchCount > maxCount)
|
||||||
maxCount = matchCount;
|
maxCount = matchCount;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
package com.termux.shared.models;
|
package com.termux.shared.models;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.content.Intent;
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
import com.termux.shared.data.IntentUtils;
|
||||||
|
import com.termux.shared.models.errors.Error;
|
||||||
import com.termux.shared.logger.Logger;
|
import com.termux.shared.logger.Logger;
|
||||||
import com.termux.shared.markdown.MarkdownUtils;
|
import com.termux.shared.markdown.MarkdownUtils;
|
||||||
import com.termux.shared.data.DataUtils;
|
import com.termux.shared.data.DataUtils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class ExecutionCommand {
|
public class ExecutionCommand {
|
||||||
@@ -51,12 +51,6 @@ public class ExecutionCommand {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define errCode values
|
|
||||||
// TODO: Define custom values for different cases
|
|
||||||
public final static int RESULT_CODE_OK = Activity.RESULT_OK;
|
|
||||||
public final static int RESULT_CODE_OK_MINOR_FAILURES = Activity.RESULT_FIRST_USER;
|
|
||||||
public final static int RESULT_CODE_FAILED = Activity.RESULT_FIRST_USER + 1;
|
|
||||||
public final static int RESULT_CODE_CANCELED = Activity.RESULT_FIRST_USER + 2;
|
|
||||||
|
|
||||||
/** The optional unique id for the {@link ExecutionCommand}. */
|
/** The optional unique id for the {@link ExecutionCommand}. */
|
||||||
public Integer id;
|
public Integer id;
|
||||||
@@ -80,6 +74,10 @@ public class ExecutionCommand {
|
|||||||
public String workingDirectory;
|
public String workingDirectory;
|
||||||
|
|
||||||
|
|
||||||
|
/** The terminal transcript rows for the {@link ExecutionCommand}. */
|
||||||
|
public Integer terminalTranscriptRows;
|
||||||
|
|
||||||
|
|
||||||
/** If the {@link ExecutionCommand} is a background or a foreground terminal session command. */
|
/** If the {@link ExecutionCommand} is a background or a foreground terminal session command. */
|
||||||
public boolean inBackground;
|
public boolean inBackground;
|
||||||
/** If the {@link ExecutionCommand} is meant to start a failsafe terminal session. */
|
/** If the {@link ExecutionCommand} is meant to start a failsafe terminal session. */
|
||||||
@@ -105,32 +103,26 @@ public class ExecutionCommand {
|
|||||||
public String pluginAPIHelp;
|
public String pluginAPIHelp;
|
||||||
|
|
||||||
|
|
||||||
|
/** Defines the {@link Intent} received which started the command. */
|
||||||
|
public Intent commandIntent;
|
||||||
|
|
||||||
/** Defines if {@link ExecutionCommand} was started because of an external plugin request
|
/** Defines if {@link ExecutionCommand} was started because of an external plugin request
|
||||||
* like {@link TERMUX_SERVICE#ACTION_SERVICE_EXECUTE} intent or from within Termux app itself.
|
* like with an intent or from within Termux app itself. */
|
||||||
*/
|
|
||||||
public boolean isPluginExecutionCommand;
|
public boolean isPluginExecutionCommand;
|
||||||
/** Defines {@link PendingIntent} that should be sent if an external plugin requested the execution. */
|
|
||||||
public PendingIntent pluginPendingIntent;
|
|
||||||
|
|
||||||
|
/** Defines the {@link ResultConfig} for the {@link ExecutionCommand} containing information
|
||||||
|
* on how to handle the result. */
|
||||||
|
public final ResultConfig resultConfig = new ResultConfig();
|
||||||
|
|
||||||
/** The stdout of shell command. */
|
/** Defines the {@link ResultData} for the {@link ExecutionCommand} containing information
|
||||||
public String stdout;
|
* of the result. */
|
||||||
/** The stderr of shell command. */
|
public final ResultData resultData = new ResultData();
|
||||||
public String stderr;
|
|
||||||
/** The exit code of shell command. */
|
|
||||||
public Integer exitCode;
|
|
||||||
|
|
||||||
|
|
||||||
/** The internal error code of {@link ExecutionCommand}. */
|
|
||||||
public Integer errCode = RESULT_CODE_OK;
|
|
||||||
/** The internal error message of {@link ExecutionCommand}. */
|
|
||||||
public String errmsg;
|
|
||||||
/** The internal exceptions of {@link ExecutionCommand}. */
|
|
||||||
public List<Throwable> throwableList = new ArrayList<>();
|
|
||||||
|
|
||||||
/** Defines if processing results already called for this {@link ExecutionCommand}. */
|
/** Defines if processing results already called for this {@link ExecutionCommand}. */
|
||||||
public boolean processingResultsAlreadyCalled;
|
public boolean processingResultsAlreadyCalled;
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "ExecutionCommand";
|
||||||
|
|
||||||
|
|
||||||
public ExecutionCommand() {
|
public ExecutionCommand() {
|
||||||
@@ -150,13 +142,101 @@ public class ExecutionCommand {
|
|||||||
this.isFailsafe = isFailsafe;
|
this.isFailsafe = isFailsafe;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean isPluginExecutionCommandWithPendingResult() {
|
||||||
|
return isPluginExecutionCommand && resultConfig.isCommandWithPendingResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public synchronized boolean setState(ExecutionState newState) {
|
||||||
|
// The state transition cannot go back or change if already at {@link ExecutionState#SUCCESS}
|
||||||
|
if (newState.getValue() < currentState.getValue() || currentState == ExecutionState.SUCCESS) {
|
||||||
|
Logger.logError(LOG_TAG, "Invalid "+ getCommandIdAndLabelLogString() + " state transition from \"" + currentState.getName() + "\" to " + "\"" + newState.getName() + "\"");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The {@link ExecutionState#FAILED} can be set again, like to add more errors, but we don't update
|
||||||
|
// {@link #previousState} with the {@link #currentState} value if its at {@link ExecutionState#FAILED} to
|
||||||
|
// preserve the last valid state
|
||||||
|
if (currentState != ExecutionState.FAILED)
|
||||||
|
previousState = currentState;
|
||||||
|
|
||||||
|
currentState = newState;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean hasExecuted() {
|
||||||
|
return currentState.getValue() >= ExecutionState.EXECUTED.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean isExecuting() {
|
||||||
|
return currentState == ExecutionState.EXECUTING;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean isSuccessful() {
|
||||||
|
return currentState == ExecutionState.SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public synchronized boolean setStateFailed(@NonNull Error error) {
|
||||||
|
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean setStateFailed(@NonNull Error error, Throwable throwable) {
|
||||||
|
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), Collections.singletonList(throwable));
|
||||||
|
}
|
||||||
|
public synchronized boolean setStateFailed(@NonNull Error error, List<Throwable> throwablesList) {
|
||||||
|
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), throwablesList);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean setStateFailed(int code, String message) {
|
||||||
|
return setStateFailed(null, code, message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean setStateFailed(int code, String message, Throwable throwable) {
|
||||||
|
return setStateFailed(null, code, message, Collections.singletonList(throwable));
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean setStateFailed(int code, String message, List<Throwable> throwablesList) {
|
||||||
|
return setStateFailed(null, code, message, throwablesList);
|
||||||
|
}
|
||||||
|
public synchronized boolean setStateFailed(String type, int code, String message, List<Throwable> throwablesList) {
|
||||||
|
if (!this.resultData.setStateFailed(type, code, message, throwablesList)) {
|
||||||
|
Logger.logWarn(LOG_TAG, "setStateFailed for " + getCommandIdAndLabelLogString() + " resultData encountered an error.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return setState(ExecutionState.FAILED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean shouldNotProcessResults() {
|
||||||
|
if (processingResultsAlreadyCalled) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
processingResultsAlreadyCalled = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean isStateFailed() {
|
||||||
|
if (currentState != ExecutionState.FAILED)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!resultData.isStateFailed()) {
|
||||||
|
Logger.logWarn(LOG_TAG, "The " + getCommandIdAndLabelLogString() + " has an invalid errCode value set in errors list while having ExecutionState.FAILED state.\n" + resultData.errorsList);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
if (!hasExecuted())
|
if (!hasExecuted())
|
||||||
return getExecutionInputLogString(this, true);
|
return getExecutionInputLogString(this, true);
|
||||||
else {
|
else {
|
||||||
return getExecutionOutputLogString(this, true);
|
return getExecutionOutputLogString(this, true, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,15 +264,15 @@ public class ExecutionCommand {
|
|||||||
logString.append("\n").append(executionCommand.getInBackgroundLogString());
|
logString.append("\n").append(executionCommand.getInBackgroundLogString());
|
||||||
logString.append("\n").append(executionCommand.getIsFailsafeLogString());
|
logString.append("\n").append(executionCommand.getIsFailsafeLogString());
|
||||||
|
|
||||||
|
|
||||||
if (!ignoreNull || executionCommand.sessionAction != null)
|
if (!ignoreNull || executionCommand.sessionAction != null)
|
||||||
logString.append("\n").append(executionCommand.getSessionActionLogString());
|
logString.append("\n").append(executionCommand.getSessionActionLogString());
|
||||||
|
|
||||||
|
if (!ignoreNull || executionCommand.commandIntent != null)
|
||||||
|
logString.append("\n").append(executionCommand.getCommandIntentLogString());
|
||||||
|
|
||||||
logString.append("\n").append(executionCommand.getIsPluginExecutionCommandLogString());
|
logString.append("\n").append(executionCommand.getIsPluginExecutionCommandLogString());
|
||||||
if (!ignoreNull || executionCommand.isPluginExecutionCommand) {
|
if (executionCommand.isPluginExecutionCommand)
|
||||||
if (!ignoreNull || executionCommand.pluginPendingIntent != null)
|
logString.append("\n").append(ResultConfig.getResultConfigLogString(executionCommand.resultConfig, ignoreNull));
|
||||||
logString.append("\n").append(executionCommand.getPendingIntentCreatorLogString());
|
|
||||||
}
|
|
||||||
|
|
||||||
return logString.toString();
|
return logString.toString();
|
||||||
}
|
}
|
||||||
@@ -202,9 +282,10 @@ public class ExecutionCommand {
|
|||||||
*
|
*
|
||||||
* @param executionCommand The {@link ExecutionCommand} to convert.
|
* @param executionCommand The {@link ExecutionCommand} to convert.
|
||||||
* @param ignoreNull Set to {@code true} if non-critical {@code null} values are to be ignored.
|
* @param ignoreNull Set to {@code true} if non-critical {@code null} values are to be ignored.
|
||||||
|
* @param logResultData Set to {@code true} if {@link #resultData} should be logged.
|
||||||
* @return Returns the log friendly {@link String}.
|
* @return Returns the log friendly {@link String}.
|
||||||
*/
|
*/
|
||||||
public static String getExecutionOutputLogString(final ExecutionCommand executionCommand, boolean ignoreNull) {
|
public static String getExecutionOutputLogString(final ExecutionCommand executionCommand, boolean ignoreNull, boolean logResultData) {
|
||||||
if (executionCommand == null) return "null";
|
if (executionCommand == null) return "null";
|
||||||
|
|
||||||
StringBuilder logString = new StringBuilder();
|
StringBuilder logString = new StringBuilder();
|
||||||
@@ -214,32 +295,8 @@ public class ExecutionCommand {
|
|||||||
logString.append("\n").append(executionCommand.getPreviousStateLogString());
|
logString.append("\n").append(executionCommand.getPreviousStateLogString());
|
||||||
logString.append("\n").append(executionCommand.getCurrentStateLogString());
|
logString.append("\n").append(executionCommand.getCurrentStateLogString());
|
||||||
|
|
||||||
logString.append("\n").append(executionCommand.getStdoutLogString());
|
if (logResultData)
|
||||||
logString.append("\n").append(executionCommand.getStderrLogString());
|
logString.append("\n").append(ResultData.getResultDataLogString(executionCommand.resultData, ignoreNull));
|
||||||
logString.append("\n").append(executionCommand.getExitCodeLogString());
|
|
||||||
|
|
||||||
logString.append(getExecutionErrLogString(executionCommand, ignoreNull));
|
|
||||||
|
|
||||||
return logString.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a log friendly {@link String} for {@link ExecutionCommand} execution error parameters.
|
|
||||||
*
|
|
||||||
* @param executionCommand The {@link ExecutionCommand} to convert.
|
|
||||||
* @param ignoreNull Set to {@code true} if non-critical {@code null} values are to be ignored.
|
|
||||||
* @return Returns the log friendly {@link String}.
|
|
||||||
*/
|
|
||||||
public static String getExecutionErrLogString(final ExecutionCommand executionCommand, boolean ignoreNull) {
|
|
||||||
StringBuilder logString = new StringBuilder();
|
|
||||||
|
|
||||||
if (!ignoreNull || (executionCommand.isStateFailed())) {
|
|
||||||
logString.append("\n").append(executionCommand.getErrCodeLogString());
|
|
||||||
logString.append("\n").append(executionCommand.getErrmsgLogString());
|
|
||||||
logString.append("\n").append(executionCommand.geStackTracesLogString());
|
|
||||||
} else {
|
|
||||||
logString.append("");
|
|
||||||
}
|
|
||||||
|
|
||||||
return logString.toString();
|
return logString.toString();
|
||||||
}
|
}
|
||||||
@@ -256,7 +313,7 @@ public class ExecutionCommand {
|
|||||||
StringBuilder logString = new StringBuilder();
|
StringBuilder logString = new StringBuilder();
|
||||||
|
|
||||||
logString.append(getExecutionInputLogString(executionCommand, false));
|
logString.append(getExecutionInputLogString(executionCommand, false));
|
||||||
logString.append(getExecutionOutputLogString(executionCommand, false));
|
logString.append(getExecutionOutputLogString(executionCommand, false, true));
|
||||||
|
|
||||||
logString.append("\n").append(executionCommand.getCommandDescriptionLogString());
|
logString.append("\n").append(executionCommand.getCommandDescriptionLogString());
|
||||||
logString.append("\n").append(executionCommand.getCommandHelpLogString());
|
logString.append("\n").append(executionCommand.getCommandHelpLogString());
|
||||||
@@ -293,18 +350,10 @@ public class ExecutionCommand {
|
|||||||
|
|
||||||
|
|
||||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("isPluginExecutionCommand", executionCommand.isPluginExecutionCommand, "-"));
|
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("isPluginExecutionCommand", executionCommand.isPluginExecutionCommand, "-"));
|
||||||
if (executionCommand.pluginPendingIntent != null)
|
|
||||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Pending Intent Creator", executionCommand.pluginPendingIntent.getCreatorPackage(), "-"));
|
|
||||||
else
|
|
||||||
markdownString.append("\n").append("**Pending Intent Creator:** - ");
|
|
||||||
|
|
||||||
markdownString.append("\n\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Stdout", executionCommand.stdout, "-"));
|
markdownString.append("\n\n").append(ResultConfig.getResultConfigMarkdownString(executionCommand.resultConfig));
|
||||||
markdownString.append("\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Stderr", executionCommand.stderr, "-"));
|
|
||||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Exit Code", executionCommand.exitCode, "-"));
|
|
||||||
|
|
||||||
markdownString.append("\n\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Err Code", executionCommand.errCode, "-"));
|
markdownString.append("\n\n").append(ResultData.getResultDataMarkdownString(executionCommand.resultData));
|
||||||
markdownString.append("\n").append("**Errmsg:**\n").append(DataUtils.getDefaultIfNull(executionCommand.errmsg, "-"));
|
|
||||||
markdownString.append("\n\n").append(executionCommand.geStackTracesMarkdownString());
|
|
||||||
|
|
||||||
if (executionCommand.commandDescription != null || executionCommand.commandHelp != null) {
|
if (executionCommand.commandDescription != null || executionCommand.commandHelp != null) {
|
||||||
if (executionCommand.commandDescription != null)
|
if (executionCommand.commandDescription != null)
|
||||||
@@ -323,7 +372,6 @@ public class ExecutionCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public String getIdLogString() {
|
public String getIdLogString() {
|
||||||
if (id != null)
|
if (id != null)
|
||||||
return "(" + id + ") ";
|
return "(" + id + ") ";
|
||||||
@@ -370,21 +418,10 @@ public class ExecutionCommand {
|
|||||||
return "isFailsafe: `" + isFailsafe + "`";
|
return "isFailsafe: `" + isFailsafe + "`";
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getIsPluginExecutionCommandLogString() {
|
|
||||||
return "isPluginExecutionCommand: `" + isPluginExecutionCommand + "`";
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSessionActionLogString() {
|
public String getSessionActionLogString() {
|
||||||
return Logger.getSingleLineLogStringEntry("Session Action", sessionAction, "-");
|
return Logger.getSingleLineLogStringEntry("Session Action", sessionAction, "-");
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getPendingIntentCreatorLogString() {
|
|
||||||
if (pluginPendingIntent != null)
|
|
||||||
return "Pending Intent Creator: `" + pluginPendingIntent.getCreatorPackage() + "`";
|
|
||||||
else
|
|
||||||
return "Pending Intent Creator: -";
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCommandDescriptionLogString() {
|
public String getCommandDescriptionLogString() {
|
||||||
return Logger.getSingleLineLogStringEntry("Command Description", commandDescription, "-");
|
return Logger.getSingleLineLogStringEntry("Command Description", commandDescription, "-");
|
||||||
}
|
}
|
||||||
@@ -397,35 +434,49 @@ public class ExecutionCommand {
|
|||||||
return Logger.getSingleLineLogStringEntry("Plugin API Help", pluginAPIHelp, "-");
|
return Logger.getSingleLineLogStringEntry("Plugin API Help", pluginAPIHelp, "-");
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getStdoutLogString() {
|
public String getCommandIntentLogString() {
|
||||||
return Logger.getMultiLineLogStringEntry("Stdout", DataUtils.getTruncatedCommandOutput(stdout, Logger.LOGGER_ENTRY_SIZE_LIMIT_IN_BYTES / 5, false, false, true), "-");
|
if (commandIntent == null)
|
||||||
|
return "Command Intent: -";
|
||||||
|
else
|
||||||
|
return Logger.getMultiLineLogStringEntry("Command Intent", IntentUtils.getIntentString(commandIntent), "-");
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getStderrLogString() {
|
public String getIsPluginExecutionCommandLogString() {
|
||||||
return Logger.getMultiLineLogStringEntry("Stderr", DataUtils.getTruncatedCommandOutput(stderr, Logger.LOGGER_ENTRY_SIZE_LIMIT_IN_BYTES / 5, false, false, true), "-");
|
return "isPluginExecutionCommand: `" + isPluginExecutionCommand + "`";
|
||||||
}
|
|
||||||
|
|
||||||
public String getExitCodeLogString() {
|
|
||||||
return Logger.getSingleLineLogStringEntry("Exit Code", exitCode, "-");
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getErrCodeLogString() {
|
|
||||||
return Logger.getSingleLineLogStringEntry("Err Code", errCode, "-");
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getErrmsgLogString() {
|
|
||||||
return Logger.getMultiLineLogStringEntry("Errmsg", errmsg, "-");
|
|
||||||
}
|
|
||||||
|
|
||||||
public String geStackTracesLogString() {
|
|
||||||
return Logger.getStackTracesString("StackTraces:", Logger.getStackTraceStringArray(throwableList));
|
|
||||||
}
|
|
||||||
|
|
||||||
public String geStackTracesMarkdownString() {
|
|
||||||
return Logger.getStackTracesMarkdownString("StackTraces", Logger.getStackTraceStringArray(throwableList));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a log friendly {@link String} for {@link List<String>} argumentsArray.
|
||||||
|
* If argumentsArray are null or of size 0, then `Arguments: -` is returned. Otherwise
|
||||||
|
* following format is returned:
|
||||||
|
*
|
||||||
|
* Arguments:
|
||||||
|
* ```
|
||||||
|
* Arg 1: `value`
|
||||||
|
* Arg 2: 'value`
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param argumentsArray The {@link String[]} argumentsArray to convert.
|
||||||
|
* @return Returns the log friendly {@link String}.
|
||||||
|
*/
|
||||||
|
public static String getArgumentsLogString(final String[] argumentsArray) {
|
||||||
|
StringBuilder argumentsString = new StringBuilder("Arguments:");
|
||||||
|
|
||||||
|
if (argumentsArray != null && argumentsArray.length != 0) {
|
||||||
|
argumentsString.append("\n```\n");
|
||||||
|
for (int i = 0; i != argumentsArray.length; i++) {
|
||||||
|
argumentsString.append(Logger.getSingleLineLogStringEntry("Arg " + (i + 1),
|
||||||
|
DataUtils.getTruncatedCommandOutput(argumentsArray[i], Logger.LOGGER_ENTRY_MAX_SAFE_PAYLOAD / 5, true, false, true),
|
||||||
|
"-")).append("\n");
|
||||||
|
}
|
||||||
|
argumentsString.append("```");
|
||||||
|
} else{
|
||||||
|
argumentsString.append(" -");
|
||||||
|
}
|
||||||
|
|
||||||
|
return argumentsString.toString();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a markdown {@link String} for {@link String[]} argumentsArray.
|
* Get a markdown {@link String} for {@link String[]} argumentsArray.
|
||||||
@@ -461,110 +512,4 @@ public class ExecutionCommand {
|
|||||||
return argumentsString.toString();
|
return argumentsString.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a log friendly {@link String} for {@link List<String>} argumentsArray.
|
|
||||||
* If argumentsArray are null or of size 0, then `Arguments: -` is returned. Otherwise
|
|
||||||
* following format is returned:
|
|
||||||
*
|
|
||||||
* Arguments:
|
|
||||||
* ```
|
|
||||||
* Arg 1: `value`
|
|
||||||
* Arg 2: 'value`
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @param argumentsArray The {@link String[]} argumentsArray to convert.
|
|
||||||
* @return Returns the log friendly {@link String}.
|
|
||||||
*/
|
|
||||||
public static String getArgumentsLogString(final String[] argumentsArray) {
|
|
||||||
StringBuilder argumentsString = new StringBuilder("Arguments:");
|
|
||||||
|
|
||||||
if (argumentsArray != null && argumentsArray.length != 0) {
|
|
||||||
argumentsString.append("\n```\n");
|
|
||||||
for (int i = 0; i != argumentsArray.length; i++) {
|
|
||||||
argumentsString.append(Logger.getSingleLineLogStringEntry("Arg " + (i + 1),
|
|
||||||
DataUtils.getTruncatedCommandOutput(argumentsArray[i], Logger.LOGGER_ENTRY_SIZE_LIMIT_IN_BYTES / 5, true, false, true),
|
|
||||||
"-")).append("`\n");
|
|
||||||
}
|
|
||||||
argumentsString.append("```");
|
|
||||||
} else{
|
|
||||||
argumentsString.append(" -");
|
|
||||||
}
|
|
||||||
|
|
||||||
return argumentsString.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public synchronized boolean setState(ExecutionState newState) {
|
|
||||||
// The state transition cannot go back or change if already at {@link ExecutionState#SUCCESS}
|
|
||||||
if (newState.getValue() < currentState.getValue() || currentState == ExecutionState.SUCCESS) {
|
|
||||||
Logger.logError("Invalid "+ getCommandIdAndLabelLogString() + " state transition from \"" + currentState.getName() + "\" to " + "\"" + newState.getName() + "\"");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The {@link ExecutionState#FAILED} can be set again, like to add more errors, but we don't update
|
|
||||||
// {@link #previousState} with the {@link #currentState} value if its at {@link ExecutionState#FAILED} to
|
|
||||||
// preserve the last valid state
|
|
||||||
if (currentState != ExecutionState.FAILED)
|
|
||||||
previousState = currentState;
|
|
||||||
|
|
||||||
currentState = newState;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized boolean setStateFailed(int errCode, String errmsg, Throwable throwable) {
|
|
||||||
if (errCode > RESULT_CODE_OK) {
|
|
||||||
this.errCode = errCode;
|
|
||||||
} else {
|
|
||||||
Logger.logWarn("Ignoring invalid " + getCommandIdAndLabelLogString() + " errCode value \"" + errCode + "\". Force setting it to RESULT_CODE_FAILED \"" + RESULT_CODE_FAILED + "\"");
|
|
||||||
this.errCode = RESULT_CODE_FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.errmsg = errmsg;
|
|
||||||
|
|
||||||
if (!setState(ExecutionState.FAILED))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (this.throwableList == null)
|
|
||||||
this.throwableList = new ArrayList<>();
|
|
||||||
|
|
||||||
if (throwable != null)
|
|
||||||
this.throwableList.add(throwable);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized boolean shouldNotProcessResults() {
|
|
||||||
if (processingResultsAlreadyCalled) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
processingResultsAlreadyCalled = true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized boolean isStateFailed() {
|
|
||||||
if (currentState != ExecutionState.FAILED)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (errCode <= RESULT_CODE_OK) {
|
|
||||||
Logger.logWarn("The " + getCommandIdAndLabelLogString() + " has an invalid errCode value \"" + errCode + "\" while having ExecutionState.FAILED state.");
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized boolean hasExecuted() {
|
|
||||||
return currentState.getValue() >= ExecutionState.EXECUTED.getValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized boolean isExecuting() {
|
|
||||||
return currentState == ExecutionState.EXECUTING;
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized boolean isSuccessful() {
|
|
||||||
return currentState == ExecutionState.SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
package com.termux.app.models;
|
package com.termux.shared.models;
|
||||||
|
|
||||||
import com.termux.shared.markdown.MarkdownUtils;
|
import com.termux.shared.markdown.MarkdownUtils;
|
||||||
import com.termux.shared.termux.TermuxUtils;
|
import com.termux.shared.termux.AndroidUtils;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
public class ReportInfo implements Serializable {
|
public class ReportInfo implements Serializable {
|
||||||
|
|
||||||
/** The user action that was being processed for which the report was generated. */
|
/** The user action that was being processed for which the report was generated. */
|
||||||
public final UserAction userAction;
|
public final String userAction;
|
||||||
/** The internal app component that sent the report. */
|
/** The internal app component that sent the report. */
|
||||||
public final String sender;
|
public final String sender;
|
||||||
/** The report title. */
|
/** The report title. */
|
||||||
@@ -26,7 +26,7 @@ public class ReportInfo implements Serializable {
|
|||||||
/** The timestamp for the report. */
|
/** The timestamp for the report. */
|
||||||
public final String reportTimestamp;
|
public final String reportTimestamp;
|
||||||
|
|
||||||
public ReportInfo(UserAction userAction, String sender, String reportTitle, String reportStringPrefix, String reportString, String reportStringSuffix, boolean addReportInfoToMarkdown) {
|
public ReportInfo(String userAction, String sender, String reportTitle, String reportStringPrefix, String reportString, String reportStringSuffix, boolean addReportInfoToMarkdown) {
|
||||||
this.userAction = userAction;
|
this.userAction = userAction;
|
||||||
this.sender = sender;
|
this.sender = sender;
|
||||||
this.reportTitle = reportTitle;
|
this.reportTitle = reportTitle;
|
||||||
@@ -34,7 +34,7 @@ public class ReportInfo implements Serializable {
|
|||||||
this.reportString = reportString;
|
this.reportString = reportString;
|
||||||
this.reportStringSuffix = reportStringSuffix;
|
this.reportStringSuffix = reportStringSuffix;
|
||||||
this.addReportInfoToMarkdown = addReportInfoToMarkdown;
|
this.addReportInfoToMarkdown = addReportInfoToMarkdown;
|
||||||
this.reportTimestamp = TermuxUtils.getCurrentTimeStamp();
|
this.reportTimestamp = AndroidUtils.getCurrentTimeStamp();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
package com.termux.shared.models;
|
||||||
|
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.markdown.MarkdownUtils;
|
||||||
|
|
||||||
|
import java.util.Formatter;
|
||||||
|
|
||||||
|
public class ResultConfig {
|
||||||
|
|
||||||
|
/** Defines {@link PendingIntent} that should be sent with the result of the command. We cannot
|
||||||
|
* implement {@link java.io.Serializable} because {@link PendingIntent} cannot be serialized. */
|
||||||
|
public PendingIntent resultPendingIntent;
|
||||||
|
/** The key with which to send result {@link android.os.Bundle} in {@link #resultPendingIntent}. */
|
||||||
|
public String resultBundleKey;
|
||||||
|
/** The key with which to send {@link ResultData#stdout} in {@link #resultPendingIntent}. */
|
||||||
|
public String resultStdoutKey;
|
||||||
|
/** The key with which to send {@link ResultData#stderr} in {@link #resultPendingIntent}. */
|
||||||
|
public String resultStderrKey;
|
||||||
|
/** The key with which to send {@link ResultData#exitCode} in {@link #resultPendingIntent}. */
|
||||||
|
public String resultExitCodeKey;
|
||||||
|
/** The key with which to send {@link ResultData#errorsList} errCode in {@link #resultPendingIntent}. */
|
||||||
|
public String resultErrCodeKey;
|
||||||
|
/** The key with which to send {@link ResultData#errorsList} errmsg in {@link #resultPendingIntent}. */
|
||||||
|
public String resultErrmsgKey;
|
||||||
|
/** The key with which to send original length of {@link ResultData#stdout} in {@link #resultPendingIntent}. */
|
||||||
|
public String resultStdoutOriginalLengthKey;
|
||||||
|
/** The key with which to send original length of {@link ResultData#stderr} in {@link #resultPendingIntent}. */
|
||||||
|
public String resultStderrOriginalLengthKey;
|
||||||
|
|
||||||
|
|
||||||
|
/** Defines the directory path in which to write the result of the command. */
|
||||||
|
public String resultDirectoryPath;
|
||||||
|
/** Defines the directory path under which {@link #resultDirectoryPath} can exist. */
|
||||||
|
public String resultDirectoryAllowedParentPath;
|
||||||
|
/** Defines whether the result should be written to a single file or multiple files
|
||||||
|
* (err, error, stdout, stderr, exit_code) in {@link #resultDirectoryPath}. */
|
||||||
|
public boolean resultSingleFile;
|
||||||
|
/** Defines the basename of the result file that should be created in {@link #resultDirectoryPath}
|
||||||
|
* if {@link #resultSingleFile} is {@code true}. */
|
||||||
|
public String resultFileBasename;
|
||||||
|
/** Defines the output {@link Formatter} format of the {@link #resultFileBasename} result file. */
|
||||||
|
public String resultFileOutputFormat;
|
||||||
|
/** Defines the error {@link Formatter} format of the {@link #resultFileBasename} result file. */
|
||||||
|
public String resultFileErrorFormat;
|
||||||
|
/** Defines the suffix of the result files that should be created in {@link #resultDirectoryPath}
|
||||||
|
* if {@link #resultSingleFile} is {@code true}. */
|
||||||
|
public String resultFilesSuffix;
|
||||||
|
|
||||||
|
|
||||||
|
public ResultConfig() {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean isCommandWithPendingResult() {
|
||||||
|
return resultPendingIntent != null || resultDirectoryPath != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return getResultConfigLogString(this, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a log friendly {@link String} for {@link ResultConfig} parameters.
|
||||||
|
*
|
||||||
|
* @param resultConfig The {@link ResultConfig} to convert.
|
||||||
|
* @param ignoreNull Set to {@code true} if non-critical {@code null} values are to be ignored.
|
||||||
|
* @return Returns the log friendly {@link String}.
|
||||||
|
*/
|
||||||
|
public static String getResultConfigLogString(final ResultConfig resultConfig, boolean ignoreNull) {
|
||||||
|
if (resultConfig == null) return "null";
|
||||||
|
|
||||||
|
StringBuilder logString = new StringBuilder();
|
||||||
|
|
||||||
|
logString.append("Result Pending: `").append(resultConfig.isCommandWithPendingResult()).append("`\n");
|
||||||
|
|
||||||
|
if (resultConfig.resultPendingIntent != null) {
|
||||||
|
logString.append(resultConfig.getResultPendingIntentVariablesLogString(ignoreNull));
|
||||||
|
if (resultConfig.resultDirectoryPath != null)
|
||||||
|
logString.append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resultConfig.resultDirectoryPath != null && !resultConfig.resultDirectoryPath.isEmpty())
|
||||||
|
logString.append(resultConfig.getResultDirectoryVariablesLogString(ignoreNull));
|
||||||
|
|
||||||
|
return logString.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getResultPendingIntentVariablesLogString(boolean ignoreNull) {
|
||||||
|
if (resultPendingIntent == null) return "Result PendingIntent Creator: -";
|
||||||
|
|
||||||
|
StringBuilder resultPendingIntentVariablesString = new StringBuilder();
|
||||||
|
|
||||||
|
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, "-"));
|
||||||
|
|
||||||
|
return resultPendingIntentVariablesString.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
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, "-"));
|
||||||
|
|
||||||
|
return resultDirectoryVariablesString.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a markdown {@link String} for {@link ResultConfig}.
|
||||||
|
*
|
||||||
|
* @param resultConfig The {@link ResultConfig} to convert.
|
||||||
|
* @return Returns the markdown {@link String}.
|
||||||
|
*/
|
||||||
|
public static String getResultConfigMarkdownString(final ResultConfig resultConfig) {
|
||||||
|
if (resultConfig == null) return "null";
|
||||||
|
|
||||||
|
StringBuilder markdownString = new StringBuilder();
|
||||||
|
|
||||||
|
if (resultConfig.resultPendingIntent != null)
|
||||||
|
markdownString.append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result PendingIntent Creator", resultConfig.resultPendingIntent.getCreatorPackage(), "-"));
|
||||||
|
else
|
||||||
|
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, "-"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return markdownString.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
package com.termux.shared.models;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.termux.shared.data.DataUtils;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.markdown.MarkdownUtils;
|
||||||
|
import com.termux.shared.models.errors.Errno;
|
||||||
|
import com.termux.shared.models.errors.Error;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ResultData implements Serializable {
|
||||||
|
|
||||||
|
/** The stdout of command. */
|
||||||
|
public final StringBuilder stdout = new StringBuilder();
|
||||||
|
/** The stderr of command. */
|
||||||
|
public final StringBuilder stderr = new StringBuilder();
|
||||||
|
/** The exit code of command. */
|
||||||
|
public Integer exitCode;
|
||||||
|
|
||||||
|
/** The internal errors list of command. */
|
||||||
|
public List<Error> errorsList = new ArrayList<>();
|
||||||
|
|
||||||
|
|
||||||
|
public ResultData() {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void clearStdout() {
|
||||||
|
stdout.setLength(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringBuilder prependStdout(String message) {
|
||||||
|
return stdout.insert(0, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringBuilder prependStdoutLn(String message) {
|
||||||
|
return stdout.insert(0, message + "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringBuilder appendStdout(String message) {
|
||||||
|
return stdout.append(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringBuilder appendStdoutLn(String message) {
|
||||||
|
return stdout.append(message).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void clearStderr() {
|
||||||
|
stderr.setLength(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringBuilder prependStderr(String message) {
|
||||||
|
return stderr.insert(0, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringBuilder prependStderrLn(String message) {
|
||||||
|
return stderr.insert(0, message + "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringBuilder appendStderr(String message) {
|
||||||
|
return stderr.append(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringBuilder appendStderrLn(String message) {
|
||||||
|
return stderr.append(message).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public synchronized boolean setStateFailed(@NonNull Error error) {
|
||||||
|
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean setStateFailed(@NonNull Error error, Throwable throwable) {
|
||||||
|
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), Collections.singletonList(throwable));
|
||||||
|
}
|
||||||
|
public synchronized boolean setStateFailed(@NonNull Error error, List<Throwable> throwablesList) {
|
||||||
|
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), throwablesList);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean setStateFailed(int code, String message) {
|
||||||
|
return setStateFailed(null, code, message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean setStateFailed(int code, String message, Throwable throwable) {
|
||||||
|
return setStateFailed(null, code, message, Collections.singletonList(throwable));
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean setStateFailed(int code, String message, List<Throwable> throwablesList) {
|
||||||
|
return setStateFailed(null, code, message, throwablesList);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean setStateFailed(String type, int code, String message, List<Throwable> throwablesList) {
|
||||||
|
if (errorsList == null)
|
||||||
|
errorsList = new ArrayList<>();
|
||||||
|
|
||||||
|
Error error = new Error();
|
||||||
|
errorsList.add(error);
|
||||||
|
|
||||||
|
return error.setStateFailed(type, code, message, throwablesList);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isStateFailed() {
|
||||||
|
if (errorsList != null) {
|
||||||
|
for (Error error : errorsList)
|
||||||
|
if (error.isStateFailed())
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getErrCode() {
|
||||||
|
if (errorsList != null && errorsList.size() > 0)
|
||||||
|
return errorsList.get(errorsList.size() - 1).getCode();
|
||||||
|
else
|
||||||
|
return Errno.ERRNO_SUCCESS.getCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return getResultDataLogString(this, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a log friendly {@link String} for {@link ResultData} parameters.
|
||||||
|
*
|
||||||
|
* @param resultData The {@link ResultData} to convert.
|
||||||
|
* @param ignoreNull Set to {@code true} if non-critical {@code null} values are to be ignored.
|
||||||
|
* @return Returns the log friendly {@link String}.
|
||||||
|
*/
|
||||||
|
public static String getResultDataLogString(final ResultData resultData, boolean ignoreNull) {
|
||||||
|
if (resultData == null) return "null";
|
||||||
|
|
||||||
|
StringBuilder logString = new StringBuilder();
|
||||||
|
|
||||||
|
logString.append("\n").append(resultData.getStdoutLogString());
|
||||||
|
logString.append("\n").append(resultData.getStderrLogString());
|
||||||
|
logString.append("\n").append(resultData.getExitCodeLogString());
|
||||||
|
|
||||||
|
logString.append("\n\n").append(getErrorsListLogString(resultData));
|
||||||
|
|
||||||
|
return logString.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public String getStdoutLogString() {
|
||||||
|
if (stdout.toString().isEmpty())
|
||||||
|
return Logger.getSingleLineLogStringEntry("Stdout", null, "-");
|
||||||
|
else
|
||||||
|
return Logger.getMultiLineLogStringEntry("Stdout", DataUtils.getTruncatedCommandOutput(stdout.toString(), Logger.LOGGER_ENTRY_MAX_SAFE_PAYLOAD / 5, false, false, true), "-");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStderrLogString() {
|
||||||
|
if (stderr.toString().isEmpty())
|
||||||
|
return Logger.getSingleLineLogStringEntry("Stderr", null, "-");
|
||||||
|
else
|
||||||
|
return Logger.getMultiLineLogStringEntry("Stderr", DataUtils.getTruncatedCommandOutput(stderr.toString(), Logger.LOGGER_ENTRY_MAX_SAFE_PAYLOAD / 5, false, false, true), "-");
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return logString.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a markdown {@link String} for {@link ResultData}.
|
||||||
|
*
|
||||||
|
* @param resultData The {@link ResultData} to convert.
|
||||||
|
* @return Returns the markdown {@link String}.
|
||||||
|
*/
|
||||||
|
public static String getResultDataMarkdownString(final ResultData resultData) {
|
||||||
|
if (resultData == null) return "null";
|
||||||
|
|
||||||
|
StringBuilder markdownString = new StringBuilder();
|
||||||
|
|
||||||
|
if (resultData.stdout.toString().isEmpty())
|
||||||
|
markdownString.append(MarkdownUtils.getSingleLineMarkdownStringEntry("Stdout", null, "-"));
|
||||||
|
else
|
||||||
|
markdownString.append(MarkdownUtils.getMultiLineMarkdownStringEntry("Stdout", resultData.stdout.toString(), "-"));
|
||||||
|
|
||||||
|
if (resultData.stderr.toString().isEmpty())
|
||||||
|
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Stderr", null, "-"));
|
||||||
|
else
|
||||||
|
markdownString.append("\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Stderr", resultData.stderr.toString(), "-"));
|
||||||
|
|
||||||
|
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Exit Code", resultData.exitCode, "-"));
|
||||||
|
|
||||||
|
markdownString.append("\n\n").append(getErrorsListMarkdownString(resultData));
|
||||||
|
|
||||||
|
|
||||||
|
return markdownString.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return markdownString.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return minimalString.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package com.termux.shared.models.errors;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** The {@link Class} that defines error messages and codes. */
|
||||||
|
public class Errno {
|
||||||
|
|
||||||
|
public static final String TYPE = "Error";
|
||||||
|
|
||||||
|
|
||||||
|
public static final Errno ERRNO_SUCCESS = new Errno(TYPE, Activity.RESULT_OK, "Success");
|
||||||
|
public static final Errno ERRNO_CANCELLED = new Errno(TYPE, Activity.RESULT_CANCELED, "Cancelled");
|
||||||
|
public static final Errno ERRNO_MINOR_FAILURES = new Errno(TYPE, Activity.RESULT_FIRST_USER, "Minor failure");
|
||||||
|
public static final Errno ERRNO_FAILED = new Errno(TYPE, Activity.RESULT_FIRST_USER + 1, "Failed");
|
||||||
|
|
||||||
|
/** The errno type. */
|
||||||
|
protected String type;
|
||||||
|
/** The errno code. */
|
||||||
|
protected final int code;
|
||||||
|
/** The errno message. */
|
||||||
|
protected final String message;
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "Errno";
|
||||||
|
|
||||||
|
|
||||||
|
public Errno(final String type, final int code, final String message) {
|
||||||
|
this.type = type;
|
||||||
|
this.code = code;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "type=" + type + ", code=" + code + ", message=\"" + message + "\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public Error getError() {
|
||||||
|
return new Error(getType(), getCode(), getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Error getError(Object... args) {
|
||||||
|
try {
|
||||||
|
return new Error(getType(), getCode(), String.format(getMessage(), args));
|
||||||
|
} 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Error getError(Throwable throwable, Object... args) {
|
||||||
|
return getError(Collections.singletonList(throwable), args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Error getError(List<Throwable> throwablesList, Object... args) {
|
||||||
|
try {
|
||||||
|
return new Error(getType(), getCode(), String.format(getMessage(), args), throwablesList);
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
package com.termux.shared.models.errors;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.markdown.MarkdownUtils;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Error implements Serializable {
|
||||||
|
|
||||||
|
/** The error type. */
|
||||||
|
private String type;
|
||||||
|
/** The error code. */
|
||||||
|
private int code;
|
||||||
|
/** The error message. */
|
||||||
|
private String message;
|
||||||
|
/** The error exceptions. */
|
||||||
|
private List<Throwable> throwablesList = new ArrayList<>();
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "Error";
|
||||||
|
|
||||||
|
|
||||||
|
public Error() {
|
||||||
|
InitError(null, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Error(String type, Integer code, String message, List<Throwable> throwablesList) {
|
||||||
|
InitError(type, code, message, throwablesList);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Error(String type, Integer code, String message, Throwable throwable) {
|
||||||
|
InitError(type, code, message, Collections.singletonList(throwable));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Error(String type, Integer code, String message) {
|
||||||
|
InitError(type, code, message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Error(Integer code, String message, List<Throwable> throwablesList) {
|
||||||
|
InitError(null, code, message, throwablesList);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Error(Integer code, String message, Throwable throwable) {
|
||||||
|
InitError(null, code, message, Collections.singletonList(throwable));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Error(Integer code, String message) {
|
||||||
|
InitError(null, code, message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Error(String message, Throwable throwable) {
|
||||||
|
InitError(null, null, message, Collections.singletonList(throwable));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Error(String message, List<Throwable> throwablesList) {
|
||||||
|
InitError(null, null, message, throwablesList);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Error(String message) {
|
||||||
|
InitError(null, null, message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitError(String type, Integer code, String message, List<Throwable> throwablesList) {
|
||||||
|
if (type != null && !type.isEmpty())
|
||||||
|
this.type = type;
|
||||||
|
else
|
||||||
|
this.type = Errno.TYPE;
|
||||||
|
|
||||||
|
if (code != null && code > Errno.ERRNO_SUCCESS.getCode())
|
||||||
|
this.code = code;
|
||||||
|
else
|
||||||
|
this.code = Errno.ERRNO_SUCCESS.getCode();
|
||||||
|
|
||||||
|
this.message = message;
|
||||||
|
this.throwablesList = throwablesList;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void prependMessage(String message) {
|
||||||
|
if (message != null && isStateFailed())
|
||||||
|
this.message = message + this.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void appendMessage(String message) {
|
||||||
|
if (message != null && isStateFailed())
|
||||||
|
this.message = this.message + message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Throwable> getThrowablesList() {
|
||||||
|
return Collections.unmodifiableList(throwablesList);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public synchronized boolean setStateFailed(@NonNull Error error) {
|
||||||
|
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean setStateFailed(@NonNull Error error, Throwable throwable) {
|
||||||
|
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), Collections.singletonList(throwable));
|
||||||
|
}
|
||||||
|
public synchronized boolean setStateFailed(@NonNull Error error, List<Throwable> throwablesList) {
|
||||||
|
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), throwablesList);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean setStateFailed(int code, String message) {
|
||||||
|
return setStateFailed(this.type, code, message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean setStateFailed(int code, String message, Throwable throwable) {
|
||||||
|
return setStateFailed(this.type, code, message, Collections.singletonList(throwable));
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean setStateFailed(int code, String message, List<Throwable> throwablesList) {
|
||||||
|
return setStateFailed(this.type, code, message, throwablesList);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean setStateFailed(String type, int code, String message, List<Throwable> throwablesList) {
|
||||||
|
this.message = message;
|
||||||
|
this.throwablesList = throwablesList;
|
||||||
|
|
||||||
|
if (type != null && !type.isEmpty())
|
||||||
|
this.type = type;
|
||||||
|
|
||||||
|
if (code > Errno.ERRNO_SUCCESS.getCode()) {
|
||||||
|
this.code = code;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
Logger.logWarn(LOG_TAG, "Ignoring invalid error code value \"" + code + "\". Force setting it to RESULT_CODE_FAILED \"" + Errno.ERRNO_FAILED.getCode() + "\"");
|
||||||
|
this.code = Errno.ERRNO_FAILED.getCode();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isStateFailed() {
|
||||||
|
return code > Errno.ERRNO_SUCCESS.getCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return getErrorLogString(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a log friendly {@link String} for {@link Error} error parameters.
|
||||||
|
*
|
||||||
|
* @param error The {@link Error} to convert.
|
||||||
|
* @return Returns the log friendly {@link String}.
|
||||||
|
*/
|
||||||
|
public static String getErrorLogString(final Error error) {
|
||||||
|
if (error == null) return "null";
|
||||||
|
|
||||||
|
StringBuilder logString = new StringBuilder();
|
||||||
|
|
||||||
|
logString.append(error.getCodeString());
|
||||||
|
logString.append("\n").append(error.getTypeAndMessageLogString());
|
||||||
|
if (error.throwablesList != null)
|
||||||
|
logString.append("\n").append(error.geStackTracesLogString());
|
||||||
|
|
||||||
|
return logString.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a minimal log friendly {@link String} for {@link Error} error parameters.
|
||||||
|
*
|
||||||
|
* @param error The {@link Error} to convert.
|
||||||
|
* @return Returns the log friendly {@link String}.
|
||||||
|
*/
|
||||||
|
public static String getMinimalErrorLogString(final Error error) {
|
||||||
|
if (error == null) return "null";
|
||||||
|
|
||||||
|
StringBuilder logString = new StringBuilder();
|
||||||
|
|
||||||
|
logString.append(error.getCodeString());
|
||||||
|
logString.append(error.getTypeAndMessageLogString());
|
||||||
|
|
||||||
|
return logString.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a minimal {@link String} for {@link Error} error parameters.
|
||||||
|
*
|
||||||
|
* @param error The {@link Error} to convert.
|
||||||
|
* @return Returns the {@link String}.
|
||||||
|
*/
|
||||||
|
public static String getMinimalErrorString(final Error error) {
|
||||||
|
if (error == null) return "null";
|
||||||
|
|
||||||
|
StringBuilder logString = new StringBuilder();
|
||||||
|
|
||||||
|
logString.append("(").append(error.getCode()).append(") ");
|
||||||
|
logString.append(error.getType()).append(": ").append(error.getMessage());
|
||||||
|
|
||||||
|
return logString.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a markdown {@link String} for {@link Error}.
|
||||||
|
*
|
||||||
|
* @param error The {@link Error} to convert.
|
||||||
|
* @return Returns the markdown {@link String}.
|
||||||
|
*/
|
||||||
|
public static String getErrorMarkdownString(final Error error) {
|
||||||
|
if (error == null) return "null";
|
||||||
|
|
||||||
|
StringBuilder markdownString = new StringBuilder();
|
||||||
|
|
||||||
|
markdownString.append(MarkdownUtils.getSingleLineMarkdownStringEntry("Error Code", error.getCode(), "-"));
|
||||||
|
markdownString.append("\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry((Errno.TYPE.equals(error.getType()) ? "Error Message" : "Error Message (" + error.getType() + ")"), error.message, "-"));
|
||||||
|
markdownString.append("\n\n").append(error.geStackTracesMarkdownString());
|
||||||
|
|
||||||
|
return markdownString.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public String getCodeString() {
|
||||||
|
return Logger.getSingleLineLogStringEntry("Error Code", code, "-");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTypeAndMessageLogString() {
|
||||||
|
return Logger.getMultiLineLogStringEntry(Errno.TYPE.equals(type) ? "Error Message" : "Error Message (" + type + ")", message, "-");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String geStackTracesLogString() {
|
||||||
|
return Logger.getStackTracesString("StackTraces:", Logger.getStackTracesStringArray(throwablesList));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String geStackTracesMarkdownString() {
|
||||||
|
return Logger.getStackTracesMarkdownString("StackTraces", Logger.getStackTracesStringArray(throwablesList));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package com.termux.shared.models.errors;
|
||||||
|
|
||||||
|
/** The {@link Class} that defines FileUtils error messages and codes. */
|
||||||
|
public class FileUtilsErrno extends Errno {
|
||||||
|
|
||||||
|
public static final String TYPE = "FileUtils Error";
|
||||||
|
|
||||||
|
|
||||||
|
/* Errors for null or empty paths (100-150) */
|
||||||
|
public static final Errno ERRNO_EXECUTABLE_REQUIRED = new Errno(TYPE, 100, "Executable required.");
|
||||||
|
public static final Errno ERRNO_NULL_OR_EMPTY_REGULAR_FILE_PATH = new Errno(TYPE, 101, "The regular file path is null or empty.");
|
||||||
|
public static final Errno ERRNO_NULL_OR_EMPTY_REGULAR_FILE = new Errno(TYPE, 102, "The regular file is null or empty.");
|
||||||
|
public static final Errno ERRNO_NULL_OR_EMPTY_EXECUTABLE_FILE_PATH = new Errno(TYPE, 103, "The executable file path is null or empty.");
|
||||||
|
public static final Errno ERRNO_NULL_OR_EMPTY_EXECUTABLE_FILE = new Errno(TYPE, 104, "The executable file is null or empty.");
|
||||||
|
public static final Errno ERRNO_NULL_OR_EMPTY_DIRECTORY_FILE_PATH = new Errno(TYPE, 105, "The directory file path is null or empty.");
|
||||||
|
public static final Errno ERRNO_NULL_OR_EMPTY_DIRECTORY_FILE = new Errno(TYPE, 106, "The directory file is null or empty.");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Errors for invalid or not found files at path (150-200) */
|
||||||
|
public static final Errno ERRNO_FILE_NOT_FOUND_AT_PATH = new Errno(TYPE, 150, "The %1$s is not found at path \"%2$s\".");
|
||||||
|
|
||||||
|
public static final Errno ERRNO_NO_REGULAR_FILE_FOUND = new Errno(TYPE, 151, "Regular file not found at %1$s path.");
|
||||||
|
public static final Errno ERRNO_NOT_A_REGULAR_FILE = new Errno(TYPE, 152, "The %1$s at path \"%2$s\" is not a regular file.");
|
||||||
|
|
||||||
|
public static final Errno ERRNO_NON_REGULAR_FILE_FOUND = new Errno(TYPE, 153, "Non-regular file found at %1$s path.");
|
||||||
|
public static final Errno ERRNO_NON_DIRECTORY_FILE_FOUND = new Errno(TYPE, 154, "Non-directory file found at %1$s path.");
|
||||||
|
public static final Errno ERRNO_NON_SYMLINK_FILE_FOUND = new Errno(TYPE, 155, "Non-symlink file found at %1$s path.");
|
||||||
|
|
||||||
|
public static final Errno ERRNO_FILE_NOT_AN_ALLOWED_FILE_TYPE = new Errno(TYPE, 156, "The %1$s found at path \"%2$s\" is not one of allowed file types \"%3$s\".");
|
||||||
|
|
||||||
|
public static final Errno ERRNO_VALIDATE_FILE_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 157, "Validating file existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s");
|
||||||
|
public static final Errno ERRNO_VALIDATE_DIRECTORY_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 158, "Validating directory existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Errors for file creation (200-250) */
|
||||||
|
public static final Errno ERRNO_CREATING_FILE_FAILED = new Errno(TYPE, 200, "Creating %1$s at path \"%2$s\" failed.");
|
||||||
|
public static final Errno ERRNO_CREATING_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 201, "Creating %1$s at path \"%2$s\" failed.\nException: %3$s");
|
||||||
|
|
||||||
|
public static final Errno ERRNO_CANNOT_OVERWRITE_A_NON_SYMLINK_FILE_TYPE = new Errno(TYPE, 202, "Cannot overwrite %1$s while creating symlink at \"%2$s\" to \"%3$s\" since destination file type \"%4$s\" is not a symlink.");
|
||||||
|
public static final Errno ERRNO_CREATING_SYMLINK_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 203, "Creating %1$s at path \"%2$s\" to \"%3$s\" failed.\nException: %4$s");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Errors for file copying and moving (250-300) */
|
||||||
|
public static final Errno ERRNO_COPYING_OR_MOVING_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 250, "%1$s from \"%2$s\" to \"%3$s\" failed.\nException: %4$s");
|
||||||
|
public static final Errno ERRNO_COPYING_OR_MOVING_FILE_TO_SAME_PATH = new Errno(TYPE, 251, "%1$s from \"%2$s\" to \"%3$s\" cannot be done since they point to the same path.");
|
||||||
|
public static final Errno ERRNO_CANNOT_OVERWRITE_A_DIFFERENT_FILE_TYPE = new Errno(TYPE, 252, "Cannot overwrite %1$s while %2$s it from \"%3$s\" to \"%4$s\" since destination file type \"%5$s\" is different from source file type \"%6$s\".");
|
||||||
|
public static final Errno ERRNO_CANNOT_MOVE_DIRECTORY_TO_SUB_DIRECTORY_OF_ITSELF = new Errno(TYPE, 253, "Cannot move %1$s from \"%2$s\" to \"%3$s\" since destination is a subdirectory of the source.");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Errors for file deletion (300-350) */
|
||||||
|
public static final Errno ERRNO_DELETING_FILE_FAILED = new Errno(TYPE, 300, "Deleting %1$s at path \"%2$s\" failed.");
|
||||||
|
public static final Errno ERRNO_DELETING_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 301, "Deleting %1$s at path \"%2$s\" failed.\nException: %3$s");
|
||||||
|
public static final Errno ERRNO_CLEARING_DIRECTORY_FAILED_WITH_EXCEPTION = new Errno(TYPE, 302, "Clearing %1$s at path \"%2$s\" failed.\nException: %3$s");
|
||||||
|
public static final Errno ERRNO_FILE_STILL_EXISTS_AFTER_DELETING = new Errno(TYPE, 303, "The %1$s still exists after deleting it from \"%2$s\".");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Errors for file reading and writing (350-400) */
|
||||||
|
public static final Errno ERRNO_READING_STRING_TO_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 350, "Reading string from %1$s at path \"%2$s\" failed.\nException: %3$s");
|
||||||
|
public static final Errno ERRNO_WRITING_STRING_TO_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 351, "Writing string to %1$s at path \"%2$s\" failed.\nException: %3$s");
|
||||||
|
public static final Errno ERRNO_UNSUPPORTED_CHARSET = new Errno(TYPE, 352, "Unsupported charset \"%1$s\"");
|
||||||
|
public static final Errno ERRNO_CHECKING_IF_CHARSET_SUPPORTED_FAILED = new Errno(TYPE, 353, "Checking if charset \"%1$s\" is supported failed.\nException: %2$s");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Errors for invalid file permissions (400-450) */
|
||||||
|
public static final Errno ERRNO_INVALID_FILE_PERMISSIONS_STRING_TO_CHECK = new Errno(TYPE, 400, "The file permission string to check is invalid.");
|
||||||
|
public static final Errno ERRNO_FILE_NOT_READABLE = new Errno(TYPE, 401, "The %1$s at path is not readable. Permission Denied.");
|
||||||
|
public static final Errno ERRNO_FILE_NOT_WRITABLE = new Errno(TYPE, 402, "The %1$s at path is not writable. Permission Denied.");
|
||||||
|
public static final Errno ERRNO_FILE_NOT_EXECUTABLE = new Errno(TYPE, 403, "The %1$s at path is not executable. Permission Denied.");
|
||||||
|
|
||||||
|
|
||||||
|
FileUtilsErrno(final String type, final int code, final String message) {
|
||||||
|
super(type, code, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.termux.shared.models.errors;
|
||||||
|
|
||||||
|
/** The {@link Class} that defines function error messages and codes. */
|
||||||
|
public class FunctionErrno extends Errno {
|
||||||
|
|
||||||
|
public static final String TYPE = "Function Error";
|
||||||
|
|
||||||
|
|
||||||
|
/* Errors for null or empty parameters (100-150) */
|
||||||
|
public static final Errno ERRNO_NULL_OR_EMPTY_PARAMETER = new Errno(TYPE, 100, "The %1$s parameter passed to \"%2$s\" is null or empty.");
|
||||||
|
public static final Errno ERRNO_NULL_OR_EMPTY_PARAMETERS = new Errno(TYPE, 101, "The %1$s parameters passed to \"%2$s\" are null or empty.");
|
||||||
|
public static final Errno ERRNO_UNSET_PARAMETER = new Errno(TYPE, 102, "The %1$s parameter passed to \"%2$s\" must be set.");
|
||||||
|
public static final Errno ERRNO_UNSET_PARAMETERS = new Errno(TYPE, 103, "The %1$s parameters passed to \"%2$s\" must be set.");
|
||||||
|
|
||||||
|
|
||||||
|
FunctionErrno(final String type, final int code, final String message) {
|
||||||
|
super(type, code, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.termux.shared.models.errors;
|
||||||
|
|
||||||
|
/** The {@link Class} that defines ResultSender error messages and codes. */
|
||||||
|
public class ResultSenderErrno extends Errno {
|
||||||
|
|
||||||
|
public static final String TYPE = "ResultSender Error";
|
||||||
|
|
||||||
|
|
||||||
|
/* Errors for null or empty parameters (100-150) */
|
||||||
|
public static final Errno ERROR_RESULT_FILE_BASENAME_NULL_OR_INVALID = new Errno(TYPE, 100, "The result file basename \"%1$s\" is null, empty or contains forward slashes \"/\".");
|
||||||
|
public static final Errno ERROR_RESULT_FILES_SUFFIX_INVALID = new Errno(TYPE, 101, "The result files suffix \"%1$s\" contains forward slashes \"/\".");
|
||||||
|
public static final Errno ERROR_FORMAT_RESULT_ERROR_FAILED_WITH_EXCEPTION = new Errno(TYPE, 102, "Formatting result error failed.\nException: %1$s");
|
||||||
|
public static final Errno ERROR_FORMAT_RESULT_OUTPUT_FAILED_WITH_EXCEPTION = new Errno(TYPE, 103, "Formatting result output failed.\nException: %1$s");
|
||||||
|
|
||||||
|
|
||||||
|
ResultSenderErrno(final String type, final int code, final String message) {
|
||||||
|
super(type, code, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -10,9 +10,6 @@ import android.os.Build;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.termux.shared.logger.Logger;
|
import com.termux.shared.logger.Logger;
|
||||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
|
||||||
import com.termux.shared.settings.preferences.TermuxPreferenceConstants;
|
|
||||||
import com.termux.shared.termux.TermuxConstants;
|
|
||||||
|
|
||||||
public class NotificationUtils {
|
public class NotificationUtils {
|
||||||
|
|
||||||
@@ -49,41 +46,12 @@ public class NotificationUtils {
|
|||||||
return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Try to get the next unique notification id that isn't already being used by the app.
|
|
||||||
*
|
|
||||||
* Termux app and its plugin must use unique notification ids from the same pool due to usage of android:sharedUserId.
|
|
||||||
* https://commonsware.com/blog/2017/06/07/jobscheduler-job-ids-libraries.html
|
|
||||||
*
|
|
||||||
* @param context The {@link Context} for operations.
|
|
||||||
* @return Returns the notification id that should be safe to use.
|
|
||||||
*/
|
|
||||||
public synchronized static int getNextNotificationId(final Context context) {
|
|
||||||
if (context == null) return TermuxPreferenceConstants.TERMUX_APP.DEFAULT_VALUE_KEY_LAST_NOTIFICATION_ID;
|
|
||||||
|
|
||||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
|
||||||
if (preferences == null) return TermuxPreferenceConstants.TERMUX_APP.DEFAULT_VALUE_KEY_LAST_NOTIFICATION_ID;
|
|
||||||
|
|
||||||
int lastNotificationId = preferences.getLastNotificationId();
|
|
||||||
|
|
||||||
int nextNotificationId = lastNotificationId + 1;
|
|
||||||
while(nextNotificationId == TermuxConstants.TERMUX_APP_NOTIFICATION_ID || nextNotificationId == TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_ID) {
|
|
||||||
nextNotificationId++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextNotificationId == Integer.MAX_VALUE || nextNotificationId < 0)
|
|
||||||
nextNotificationId = TermuxPreferenceConstants.TERMUX_APP.DEFAULT_VALUE_KEY_LAST_NOTIFICATION_ID;
|
|
||||||
|
|
||||||
preferences.setLastNotificationId(nextNotificationId);
|
|
||||||
return nextNotificationId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get {@link Notification.Builder}.
|
* Get {@link Notification.Builder}.
|
||||||
*
|
*
|
||||||
* @param context The {@link Context} for operations.
|
* @param context The {@link Context} for operations.
|
||||||
* @param title The title for the notification.
|
* @param title The title for the notification.
|
||||||
* @param notifiationText The second line text of the notification.
|
* @param notificationText The second line text of the notification.
|
||||||
* @param notificationBigText The full text of the notification that may optionally be styled.
|
* @param notificationBigText The full text of the notification that may optionally be styled.
|
||||||
* @param pendingIntent The {@link PendingIntent} which should be sent when notification is clicked.
|
* @param pendingIntent The {@link PendingIntent} which should be sent when notification is clicked.
|
||||||
* @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}.
|
* @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}.
|
||||||
@@ -92,11 +60,11 @@ public class NotificationUtils {
|
|||||||
* @return Returns the {@link Notification.Builder}.
|
* @return Returns the {@link Notification.Builder}.
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
public static Notification.Builder geNotificationBuilder(final Context context, final String channelId, final int priority, final CharSequence title, final CharSequence notifiationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) {
|
public static Notification.Builder geNotificationBuilder(final Context context, final String channelId, final int priority, final CharSequence title, final CharSequence notificationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) {
|
||||||
if (context == null) return null;
|
if (context == null) return null;
|
||||||
Notification.Builder builder = new Notification.Builder(context);
|
Notification.Builder builder = new Notification.Builder(context);
|
||||||
builder.setContentTitle(title);
|
builder.setContentTitle(title);
|
||||||
builder.setContentText(notifiationText);
|
builder.setContentText(notificationText);
|
||||||
builder.setStyle(new Notification.BigTextStyle().bigText(notificationBigText));
|
builder.setStyle(new Notification.BigTextStyle().bigText(notificationBigText));
|
||||||
builder.setContentIntent(pendingIntent);
|
builder.setContentIntent(pendingIntent);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.termux.shared.notification;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxPreferenceConstants;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
|
||||||
|
public class TermuxNotificationUtils {
|
||||||
|
/**
|
||||||
|
* Try to get the next unique notification id that isn't already being used by the app.
|
||||||
|
*
|
||||||
|
* Termux app and its plugin must use unique notification ids from the same pool due to usage of android:sharedUserId.
|
||||||
|
* https://commonsware.com/blog/2017/06/07/jobscheduler-job-ids-libraries.html
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} for operations.
|
||||||
|
* @return Returns the notification id that should be safe to use.
|
||||||
|
*/
|
||||||
|
public synchronized static int getNextNotificationId(final Context context) {
|
||||||
|
if (context == null) return TermuxPreferenceConstants.TERMUX_APP.DEFAULT_VALUE_KEY_LAST_NOTIFICATION_ID;
|
||||||
|
|
||||||
|
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
||||||
|
if (preferences == null) return TermuxPreferenceConstants.TERMUX_APP.DEFAULT_VALUE_KEY_LAST_NOTIFICATION_ID;
|
||||||
|
|
||||||
|
int lastNotificationId = preferences.getLastNotificationId();
|
||||||
|
|
||||||
|
int nextNotificationId = lastNotificationId + 1;
|
||||||
|
while(nextNotificationId == TermuxConstants.TERMUX_APP_NOTIFICATION_ID || nextNotificationId == TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_ID) {
|
||||||
|
nextNotificationId++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextNotificationId == Integer.MAX_VALUE || nextNotificationId < 0)
|
||||||
|
nextNotificationId = TermuxPreferenceConstants.TERMUX_APP.DEFAULT_VALUE_KEY_LAST_NOTIFICATION_ID;
|
||||||
|
|
||||||
|
preferences.setLastNotificationId(nextNotificationId);
|
||||||
|
return nextNotificationId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import androidx.annotation.NonNull;
|
|||||||
|
|
||||||
import com.termux.shared.R;
|
import com.termux.shared.R;
|
||||||
import com.termux.shared.data.DataUtils;
|
import com.termux.shared.data.DataUtils;
|
||||||
import com.termux.shared.interact.DialogUtils;
|
import com.termux.shared.interact.MessageDialogUtils;
|
||||||
import com.termux.shared.logger.Logger;
|
import com.termux.shared.logger.Logger;
|
||||||
import com.termux.shared.termux.TermuxConstants;
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ public class PackageUtils {
|
|||||||
String errorMessage = context.getString(R.string.error_get_package_context_failed_message,
|
String errorMessage = context.getString(R.string.error_get_package_context_failed_message,
|
||||||
packageName, TermuxConstants.TERMUX_GITHUB_REPO_URL);
|
packageName, TermuxConstants.TERMUX_GITHUB_REPO_URL);
|
||||||
Logger.logError(LOG_TAG, errorMessage);
|
Logger.logError(LOG_TAG, errorMessage);
|
||||||
DialogUtils.exitAppWithErrorMessage(context,
|
MessageDialogUtils.exitAppWithErrorMessage(context,
|
||||||
context.getString(R.string.error_get_package_context_failed_title),
|
context.getString(R.string.error_get_package_context_failed_title),
|
||||||
errorMessage);
|
errorMessage);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,41 @@
|
|||||||
package com.termux.shared.packages;
|
package com.termux.shared.packages;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.os.PowerManager;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
|
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
import com.termux.shared.R;
|
import com.termux.shared.R;
|
||||||
import com.termux.shared.termux.TermuxConstants;
|
|
||||||
import com.termux.shared.logger.Logger;
|
import com.termux.shared.logger.Logger;
|
||||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
public class PermissionUtils {
|
public class PermissionUtils {
|
||||||
|
|
||||||
public static final int ACTION_MANAGE_OVERLAY_PERMISSION_REQUEST_CODE = 0;
|
public static final int REQUEST_GRANT_STORAGE_PERMISSION = 1000;
|
||||||
|
|
||||||
|
public static final int REQUEST_DISABLE_BATTERY_OPTIMIZATIONS = 2000;
|
||||||
|
public static final int REQUEST_GRANT_DISPLAY_OVER_OTHER_APPS_PERMISSION = 2001;
|
||||||
|
|
||||||
private static final String LOG_TAG = "PermissionUtils";
|
private static final String LOG_TAG = "PermissionUtils";
|
||||||
|
|
||||||
public static boolean checkPermissions(Context context, String[] permissions) {
|
|
||||||
int result;
|
|
||||||
|
|
||||||
|
public static boolean checkPermission(Context context, String permission) {
|
||||||
|
if (permission == null) return false;
|
||||||
|
return checkPermissions(context, new String[]{permission});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean checkPermissions(Context context, String[] permissions) {
|
||||||
|
if (permissions == null) return false;
|
||||||
|
|
||||||
|
int result;
|
||||||
for (String p:permissions) {
|
for (String p:permissions) {
|
||||||
result = ContextCompat.checkSelfPermission(context,p);
|
result = ContextCompat.checkSelfPermission(context,p);
|
||||||
if (result != PackageManager.PERMISSION_GRANTED) {
|
if (result != PackageManager.PERMISSION_GRANTED) {
|
||||||
@@ -35,18 +45,25 @@ public class PermissionUtils {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void askPermissions(Activity context, String[] permissions) {
|
|
||||||
if (context == null || permissions == null) return;
|
|
||||||
|
public static void requestPermission(Activity activity, String permission, int requestCode) {
|
||||||
|
if (permission == null) return;
|
||||||
|
requestPermissions(activity, new String[]{permission}, requestCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void requestPermissions(Activity activity, String[] permissions, int requestCode) {
|
||||||
|
if (activity == null || permissions == null) return;
|
||||||
|
|
||||||
int result;
|
int result;
|
||||||
Logger.showToast(context, context.getString(R.string.message_sudo_please_grant_permissions), true);
|
Logger.showToast(activity, activity.getString(R.string.message_sudo_please_grant_permissions), true);
|
||||||
try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}
|
try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}
|
||||||
|
|
||||||
for (String permission:permissions) {
|
for (String permission:permissions) {
|
||||||
result = ContextCompat.checkSelfPermission(context, permission);
|
result = ContextCompat.checkSelfPermission(activity, permission);
|
||||||
if (result != PackageManager.PERMISSION_GRANTED) {
|
if (result != PackageManager.PERMISSION_GRANTED) {
|
||||||
Logger.logDebug(LOG_TAG, "Requesting Permissions: " + Arrays.toString(permissions));
|
Logger.logDebug(LOG_TAG, "Requesting Permissions: " + Arrays.toString(permissions));
|
||||||
context.requestPermissions(new String[]{permission}, 0);
|
activity.requestPermissions(new String[]{permission}, requestCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,36 +71,40 @@ public class PermissionUtils {
|
|||||||
|
|
||||||
|
|
||||||
public static boolean checkDisplayOverOtherAppsPermission(Context context) {
|
public static boolean checkDisplayOverOtherAppsPermission(Context context) {
|
||||||
boolean permissionGranted;
|
return Settings.canDrawOverlays(context);
|
||||||
|
|
||||||
permissionGranted = Settings.canDrawOverlays(context);
|
|
||||||
if (!permissionGranted) {
|
|
||||||
Logger.logWarn(LOG_TAG, TermuxConstants.TERMUX_APP_NAME + " App does not have Display over other apps (SYSTEM_ALERT_WINDOW) permission");
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
Logger.logDebug(LOG_TAG, TermuxConstants.TERMUX_APP_NAME + " App already has Display over other apps (SYSTEM_ALERT_WINDOW) permission");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void askDisplayOverOtherAppsPermission(Activity context) {
|
public static void requestDisplayOverOtherAppsPermission(Activity context, int requestCode) {
|
||||||
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + context.getPackageName()));
|
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + context.getPackageName()));
|
||||||
context.startActivityForResult(intent, ACTION_MANAGE_OVERLAY_PERMISSION_REQUEST_CODE);
|
context.startActivityForResult(intent, requestCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean validateDisplayOverOtherAppsPermissionForPostAndroid10(Context context) {
|
public static boolean validateDisplayOverOtherAppsPermissionForPostAndroid10(Context context, boolean logResults) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return true;
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return true;
|
||||||
|
|
||||||
if (!PermissionUtils.checkDisplayOverOtherAppsPermission(context)) {
|
if (!PermissionUtils.checkDisplayOverOtherAppsPermission(context)) {
|
||||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
if (logResults)
|
||||||
if (preferences == null) return false;
|
Logger.logWarn(LOG_TAG, context.getPackageName() + " does not have Display over other apps (SYSTEM_ALERT_WINDOW) permission");
|
||||||
|
|
||||||
if (preferences.arePluginErrorNotificationsEnabled())
|
|
||||||
Logger.showToast(context, context.getString(R.string.error_display_over_other_apps_permission_not_granted), true);
|
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
|
if (logResults)
|
||||||
|
Logger.logDebug(LOG_TAG, context.getPackageName() + " already has Display over other apps (SYSTEM_ALERT_WINDOW) permission");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static boolean checkIfBatteryOptimizationsDisabled(Context context) {
|
||||||
|
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
|
||||||
|
return powerManager.isIgnoringBatteryOptimizations(context.getPackageName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("BatteryLife")
|
||||||
|
public static void requestDisableBatteryOptimizations(Activity activity, int requestCode) {
|
||||||
|
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
||||||
|
intent.setData(Uri.parse("package:" + activity.getPackageName()));
|
||||||
|
activity.startActivityForResult(intent, requestCode);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.termux.shared.settings.properties;
|
|||||||
import com.google.common.collect.ImmutableBiMap;
|
import com.google.common.collect.ImmutableBiMap;
|
||||||
import com.termux.shared.termux.TermuxConstants;
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
import com.termux.shared.logger.Logger;
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.terminal.TerminalEmulator;
|
||||||
import com.termux.view.TerminalView;
|
import com.termux.view.TerminalView;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -11,7 +12,7 @@ import java.util.HashSet;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Version: v0.10.0
|
* Version: v0.12.0
|
||||||
*
|
*
|
||||||
* Changelog
|
* Changelog
|
||||||
*
|
*
|
||||||
@@ -49,6 +50,11 @@ import java.util.Set;
|
|||||||
* - 0.10.0 (2021-05-15)
|
* - 0.10.0 (2021-05-15)
|
||||||
* - Add `MAP_BACK_KEY_BEHAVIOUR`, `MAP_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR`, `MAP_VOLUME_KEYS_BEHAVIOUR`.
|
* - Add `MAP_BACK_KEY_BEHAVIOUR`, `MAP_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR`, `MAP_VOLUME_KEYS_BEHAVIOUR`.
|
||||||
*
|
*
|
||||||
|
* - 0.11.0 (2021-06-10)
|
||||||
|
* - Add `*KEY_TERMINAL_TRANSCRIPT_ROWS*`.
|
||||||
|
*
|
||||||
|
* - 0.12.0 (2021-06-10)
|
||||||
|
* - Add `*KEY_TERMINAL_CURSOR_STYLE*`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,6 +72,18 @@ public final class TermuxPropertyConstants {
|
|||||||
|
|
||||||
/* boolean */
|
/* boolean */
|
||||||
|
|
||||||
|
/** Defines the key for whether terminal view margin adjustment that is done to prevent soft
|
||||||
|
* keyboard from covering bottom part of terminal view on some devices is disabled or not.
|
||||||
|
* Margin adjustment may cause screen flickering on some devices and so should be disabled. */
|
||||||
|
public static final String KEY_DISABLE_TERMINAL_MARGIN_ADJUSTMENT = "disable-terminal-margin-adjustment"; // Default: "disable-terminal-margin-adjustment"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** Defines the key for whether a toast will be shown when user changes the terminal session */
|
||||||
|
public static final String KEY_DISABLE_TERMINAL_SESSION_CHANGE_TOAST = "disable-terminal-session-change-toast"; // Default: "disable-terminal-session-change-toast"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/** Defines the key for whether to enforce character based input to fix the issue where for some devices like Samsung, the letters might not appear until enter is pressed */
|
/** Defines the key for whether to enforce character based input to fix the issue where for some devices like Samsung, the letters might not appear until enter is pressed */
|
||||||
public static final String KEY_ENFORCE_CHAR_BASED_INPUT = "enforce-char-based-input"; // Default: "enforce-char-based-input"
|
public static final String KEY_ENFORCE_CHAR_BASED_INPUT = "enforce-char-based-input"; // Default: "enforce-char-based-input"
|
||||||
|
|
||||||
@@ -76,6 +94,11 @@ public final class TermuxPropertyConstants {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** Defines the key for whether url links in terminal transcript will automatically open on click or on tap */
|
||||||
|
public static final String KEY_TERMINAL_ONCLICK_URL_OPEN = "terminal-onclick-url-open"; // Default: "terminal-onclick-url-open"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/** Defines the key for whether to use black UI */
|
/** Defines the key for whether to use black UI */
|
||||||
public static final String KEY_USE_BLACK_UI = "use-black-ui"; // Default: "use-black-ui"
|
public static final String KEY_USE_BLACK_UI = "use-black-ui"; // Default: "use-black-ui"
|
||||||
|
|
||||||
@@ -131,6 +154,36 @@ public final class TermuxPropertyConstants {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** Defines the key for the terminal cursor style */
|
||||||
|
public static final String KEY_TERMINAL_CURSOR_STYLE = "terminal-cursor-style"; // Default: "terminal-cursor-style"
|
||||||
|
|
||||||
|
public static final String VALUE_TERMINAL_CURSOR_STYLE_BLOCK = "block";
|
||||||
|
public static final String VALUE_TERMINAL_CURSOR_STYLE_UNDERLINE = "underline";
|
||||||
|
public static final String VALUE_TERMINAL_CURSOR_STYLE_BAR = "bar";
|
||||||
|
|
||||||
|
public static final int IVALUE_TERMINAL_CURSOR_STYLE_BLOCK = TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK;
|
||||||
|
public static final int IVALUE_TERMINAL_CURSOR_STYLE_UNDERLINE = TerminalEmulator.TERMINAL_CURSOR_STYLE_UNDERLINE;
|
||||||
|
public static final int IVALUE_TERMINAL_CURSOR_STYLE_BAR = TerminalEmulator.TERMINAL_CURSOR_STYLE_BAR;
|
||||||
|
public static final int DEFAULT_IVALUE_TERMINAL_CURSOR_STYLE = TerminalEmulator.DEFAULT_TERMINAL_CURSOR_STYLE;
|
||||||
|
|
||||||
|
/** Defines the bidirectional map for terminal cursor styles and their internal values */
|
||||||
|
public static final ImmutableBiMap<String, Integer> MAP_TERMINAL_CURSOR_STYLE =
|
||||||
|
new ImmutableBiMap.Builder<String, Integer>()
|
||||||
|
.put(VALUE_TERMINAL_CURSOR_STYLE_BLOCK, IVALUE_TERMINAL_CURSOR_STYLE_BLOCK)
|
||||||
|
.put(VALUE_TERMINAL_CURSOR_STYLE_UNDERLINE, IVALUE_TERMINAL_CURSOR_STYLE_UNDERLINE)
|
||||||
|
.put(VALUE_TERMINAL_CURSOR_STYLE_BAR, IVALUE_TERMINAL_CURSOR_STYLE_BAR)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** Defines the key for the terminal transcript rows */
|
||||||
|
public static final String KEY_TERMINAL_TRANSCRIPT_ROWS = "terminal-transcript-rows"; // Default: "terminal-transcript-rows"
|
||||||
|
public static final int IVALUE_TERMINAL_TRANSCRIPT_ROWS_MIN = TerminalEmulator.TERMINAL_TRANSCRIPT_ROWS_MIN;
|
||||||
|
public static final int IVALUE_TERMINAL_TRANSCRIPT_ROWS_MAX = TerminalEmulator.TERMINAL_TRANSCRIPT_ROWS_MAX;
|
||||||
|
public static final int DEFAULT_IVALUE_TERMINAL_TRANSCRIPT_ROWS = TerminalEmulator.DEFAULT_TERMINAL_TRANSCRIPT_ROWS;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* float */
|
/* float */
|
||||||
@@ -201,7 +254,8 @@ public final class TermuxPropertyConstants {
|
|||||||
|
|
||||||
/** Defines the key for extra keys */
|
/** Defines the key for extra keys */
|
||||||
public static final String KEY_EXTRA_KEYS = "extra-keys"; // Default: "extra-keys"
|
public static final String KEY_EXTRA_KEYS = "extra-keys"; // Default: "extra-keys"
|
||||||
public static final String DEFAULT_IVALUE_EXTRA_KEYS = "[[ESC, TAB, CTRL, ALT, {key: '-', popup: '|'}, DOWN, UP]]";
|
//public static final String DEFAULT_IVALUE_EXTRA_KEYS = "[[ESC, TAB, CTRL, ALT, {key: '-', popup: '|'}, DOWN, UP]]"; // Single row
|
||||||
|
public static final String DEFAULT_IVALUE_EXTRA_KEYS = "[['ESC','/',{key: '-', popup: '|'},'HOME','UP','END','PGUP'], ['TAB','CTRL','ALT','LEFT','DOWN','RIGHT','PGDN']]"; // Double row
|
||||||
|
|
||||||
/** Defines the key for extra keys style */
|
/** Defines the key for extra keys style */
|
||||||
public static final String KEY_EXTRA_KEYS_STYLE = "extra-keys-style"; // Default: "extra-keys-style"
|
public static final String KEY_EXTRA_KEYS_STYLE = "extra-keys-style"; // Default: "extra-keys-style"
|
||||||
@@ -248,8 +302,11 @@ public final class TermuxPropertyConstants {
|
|||||||
* */
|
* */
|
||||||
public static final Set<String> TERMUX_PROPERTIES_LIST = new HashSet<>(Arrays.asList(
|
public static final Set<String> TERMUX_PROPERTIES_LIST = new HashSet<>(Arrays.asList(
|
||||||
/* boolean */
|
/* boolean */
|
||||||
|
KEY_DISABLE_TERMINAL_MARGIN_ADJUSTMENT,
|
||||||
|
KEY_DISABLE_TERMINAL_SESSION_CHANGE_TOAST,
|
||||||
KEY_ENFORCE_CHAR_BASED_INPUT,
|
KEY_ENFORCE_CHAR_BASED_INPUT,
|
||||||
KEY_HIDE_SOFT_KEYBOARD_ON_STARTUP,
|
KEY_HIDE_SOFT_KEYBOARD_ON_STARTUP,
|
||||||
|
KEY_TERMINAL_ONCLICK_URL_OPEN,
|
||||||
KEY_USE_BLACK_UI,
|
KEY_USE_BLACK_UI,
|
||||||
KEY_USE_CTRL_SPACE_WORKAROUND,
|
KEY_USE_CTRL_SPACE_WORKAROUND,
|
||||||
KEY_USE_FULLSCREEN,
|
KEY_USE_FULLSCREEN,
|
||||||
@@ -259,6 +316,8 @@ public final class TermuxPropertyConstants {
|
|||||||
/* int */
|
/* int */
|
||||||
KEY_BELL_BEHAVIOUR,
|
KEY_BELL_BEHAVIOUR,
|
||||||
KEY_TERMINAL_CURSOR_BLINK_RATE,
|
KEY_TERMINAL_CURSOR_BLINK_RATE,
|
||||||
|
KEY_TERMINAL_CURSOR_STYLE,
|
||||||
|
KEY_TERMINAL_TRANSCRIPT_ROWS,
|
||||||
|
|
||||||
/* float */
|
/* float */
|
||||||
KEY_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR,
|
KEY_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR,
|
||||||
@@ -284,8 +343,11 @@ public final class TermuxPropertyConstants {
|
|||||||
* default: false
|
* default: false
|
||||||
* */
|
* */
|
||||||
public static final Set<String> TERMUX_DEFAULT_BOOLEAN_BEHAVIOUR_PROPERTIES_LIST = new HashSet<>(Arrays.asList(
|
public static final Set<String> TERMUX_DEFAULT_BOOLEAN_BEHAVIOUR_PROPERTIES_LIST = new HashSet<>(Arrays.asList(
|
||||||
|
KEY_DISABLE_TERMINAL_MARGIN_ADJUSTMENT,
|
||||||
|
KEY_DISABLE_TERMINAL_SESSION_CHANGE_TOAST,
|
||||||
KEY_ENFORCE_CHAR_BASED_INPUT,
|
KEY_ENFORCE_CHAR_BASED_INPUT,
|
||||||
KEY_HIDE_SOFT_KEYBOARD_ON_STARTUP,
|
KEY_HIDE_SOFT_KEYBOARD_ON_STARTUP,
|
||||||
|
KEY_TERMINAL_ONCLICK_URL_OPEN,
|
||||||
KEY_USE_CTRL_SPACE_WORKAROUND,
|
KEY_USE_CTRL_SPACE_WORKAROUND,
|
||||||
KEY_USE_FULLSCREEN,
|
KEY_USE_FULLSCREEN,
|
||||||
KEY_USE_FULLSCREEN_WORKAROUND,
|
KEY_USE_FULLSCREEN_WORKAROUND,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import java.util.Properties;
|
|||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
public class TermuxSharedProperties implements SharedPropertiesParser {
|
public class TermuxSharedProperties {
|
||||||
|
|
||||||
protected final Context mContext;
|
protected final Context mContext;
|
||||||
protected final SharedProperties mSharedProperties;
|
protected final SharedProperties mSharedProperties;
|
||||||
@@ -24,7 +24,7 @@ public class TermuxSharedProperties implements SharedPropertiesParser {
|
|||||||
public TermuxSharedProperties(@Nonnull Context context) {
|
public TermuxSharedProperties(@Nonnull Context context) {
|
||||||
mContext = context;
|
mContext = context;
|
||||||
mPropertiesFile = TermuxPropertyConstants.getTermuxPropertiesFile();
|
mPropertiesFile = TermuxPropertyConstants.getTermuxPropertiesFile();
|
||||||
mSharedProperties = new SharedProperties(context, mPropertiesFile, TermuxPropertyConstants.TERMUX_PROPERTIES_LIST, this);
|
mSharedProperties = new SharedProperties(context, mPropertiesFile, TermuxPropertyConstants.TERMUX_PROPERTIES_LIST, new SharedPropertiesParserClient());
|
||||||
loadTermuxPropertiesFromDisk();
|
loadTermuxPropertiesFromDisk();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,24 +146,47 @@ public class TermuxSharedProperties implements SharedPropertiesParser {
|
|||||||
// {@link #loadTermuxPropertiesFromDisk()} call
|
// {@link #loadTermuxPropertiesFromDisk()} call
|
||||||
// A null value can still be returned by
|
// A null value can still be returned by
|
||||||
// {@link #getInternalPropertyValueFromValue(Context,String,String)} for some keys
|
// {@link #getInternalPropertyValueFromValue(Context,String,String)} for some keys
|
||||||
value = getInternalPropertyValueFromValue(mContext, key, null);
|
value = getInternalTermuxPropertyValueFromValue(mContext, key, null);
|
||||||
Logger.logWarn(LOG_TAG, "The value for \"" + key + "\" not found in SharedProperties cache, force returning default value: `" + value + "`");
|
Logger.logWarn(LOG_TAG, "The value for \"" + key + "\" not found in SharedProperties cache, force returning default value: `" + value + "`");
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// We get the property value directly from file and return its internal value
|
// We get the property value directly from file and return its internal value
|
||||||
return getInternalPropertyValueFromValue(mContext, key, mSharedProperties.getProperty(key, false));
|
return getInternalTermuxPropertyValueFromValue(mContext, key, mSharedProperties.getProperty(key, false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override the
|
* Get the internal {@link Object} value for the key passed from the file returned by
|
||||||
* {@link SharedPropertiesParser#getInternalPropertyValueFromValue(Context,String,String)}
|
* {@link TermuxPropertyConstants#getTermuxPropertiesFile()}. The {@link Properties} object is
|
||||||
* interface function.
|
* read directly from the file and internal value is returned for the property value against the key.
|
||||||
|
*
|
||||||
|
* @param context The context for operations.
|
||||||
|
* @param key The key for which the internal object is required.
|
||||||
|
* @return Returns the {@link Object} object. This will be {@code null} if key is not found or
|
||||||
|
* the object stored against the key is {@code null}.
|
||||||
*/
|
*/
|
||||||
@Override
|
public static Object getInternalPropertyValue(Context context, String key) {
|
||||||
public Object getInternalPropertyValueFromValue(Context context, String key, String value) {
|
return SharedProperties.getInternalProperty(context, TermuxPropertyConstants.getTermuxPropertiesFile(), key, new SharedPropertiesParserClient());
|
||||||
return getInternalTermuxPropertyValueFromValue(context, key, value);
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The class that implements the {@link SharedPropertiesParser} interface.
|
||||||
|
*/
|
||||||
|
public static class SharedPropertiesParserClient implements SharedPropertiesParser {
|
||||||
|
/**
|
||||||
|
* Override the
|
||||||
|
* {@link SharedPropertiesParser#getInternalPropertyValueFromValue(Context,String,String)}
|
||||||
|
* interface function.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Object getInternalPropertyValueFromValue(Context context, String key, String value) {
|
||||||
|
return getInternalTermuxPropertyValueFromValue(context, key, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -194,6 +217,10 @@ public class TermuxSharedProperties implements SharedPropertiesParser {
|
|||||||
return (int) getBellBehaviourInternalPropertyValueFromValue(value);
|
return (int) getBellBehaviourInternalPropertyValueFromValue(value);
|
||||||
case TermuxPropertyConstants.KEY_TERMINAL_CURSOR_BLINK_RATE:
|
case TermuxPropertyConstants.KEY_TERMINAL_CURSOR_BLINK_RATE:
|
||||||
return (int) getTerminalCursorBlinkRateInternalPropertyValueFromValue(value);
|
return (int) getTerminalCursorBlinkRateInternalPropertyValueFromValue(value);
|
||||||
|
case TermuxPropertyConstants.KEY_TERMINAL_CURSOR_STYLE:
|
||||||
|
return (int) getTerminalCursorStyleInternalPropertyValueFromValue(value);
|
||||||
|
case TermuxPropertyConstants.KEY_TERMINAL_TRANSCRIPT_ROWS:
|
||||||
|
return (int) getTerminalTranscriptRowsInternalPropertyValueFromValue(value);
|
||||||
|
|
||||||
/* float */
|
/* float */
|
||||||
case TermuxPropertyConstants.KEY_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR:
|
case TermuxPropertyConstants.KEY_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR:
|
||||||
@@ -252,7 +279,7 @@ public class TermuxSharedProperties implements SharedPropertiesParser {
|
|||||||
/**
|
/**
|
||||||
* Returns the internal value after mapping it based on
|
* Returns the internal value after mapping it based on
|
||||||
* {@code TermuxPropertyConstants#MAP_BELL_BEHAVIOUR} if the value is not {@code null}
|
* {@code TermuxPropertyConstants#MAP_BELL_BEHAVIOUR} if the value is not {@code null}
|
||||||
* and is valid, otherwise returns {@code TermuxPropertyConstants#DEFAULT_IVALUE_BELL_BEHAVIOUR}.
|
* and is valid, otherwise returns {@link TermuxPropertyConstants#DEFAULT_IVALUE_BELL_BEHAVIOUR}.
|
||||||
*
|
*
|
||||||
* @param value The {@link String} value to convert.
|
* @param value The {@link String} value to convert.
|
||||||
* @return Returns the internal value for value.
|
* @return Returns the internal value for value.
|
||||||
@@ -263,14 +290,14 @@ public class TermuxSharedProperties implements SharedPropertiesParser {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the int for the value if its not null and is between
|
* Returns the int for the value if its not null and is between
|
||||||
* {@code TermuxPropertyConstants#IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR_MIN} and
|
* {@link TermuxPropertyConstants#IVALUE_TERMINAL_CURSOR_BLINK_RATE_MIN} and
|
||||||
* {@code TermuxPropertyConstants#IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR_MAX},
|
* {@link TermuxPropertyConstants#IVALUE_TERMINAL_CURSOR_BLINK_RATE_MAX},
|
||||||
* otherwise returns {@code TermuxPropertyConstants#DEFAULT_IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR}.
|
* otherwise returns {@link TermuxPropertyConstants#DEFAULT_IVALUE_TERMINAL_CURSOR_BLINK_RATE}.
|
||||||
*
|
*
|
||||||
* @param value The {@link String} value to convert.
|
* @param value The {@link String} value to convert.
|
||||||
* @return Returns the internal value for value.
|
* @return Returns the internal value for value.
|
||||||
*/
|
*/
|
||||||
public static float getTerminalCursorBlinkRateInternalPropertyValueFromValue(String value) {
|
public static int getTerminalCursorBlinkRateInternalPropertyValueFromValue(String value) {
|
||||||
return SharedProperties.getDefaultIfNotInRange(TermuxPropertyConstants.KEY_TERMINAL_CURSOR_BLINK_RATE,
|
return SharedProperties.getDefaultIfNotInRange(TermuxPropertyConstants.KEY_TERMINAL_CURSOR_BLINK_RATE,
|
||||||
DataUtils.getIntFromString(value, TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_CURSOR_BLINK_RATE),
|
DataUtils.getIntFromString(value, TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_CURSOR_BLINK_RATE),
|
||||||
TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_CURSOR_BLINK_RATE,
|
TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_CURSOR_BLINK_RATE,
|
||||||
@@ -279,11 +306,41 @@ public class TermuxSharedProperties implements SharedPropertiesParser {
|
|||||||
true, true, LOG_TAG);
|
true, true, LOG_TAG);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the internal value after mapping it based on
|
||||||
|
* {@link TermuxPropertyConstants#MAP_TERMINAL_CURSOR_STYLE} if the value is not {@code null}
|
||||||
|
* and is valid, otherwise returns {@link TermuxPropertyConstants#DEFAULT_IVALUE_TERMINAL_CURSOR_STYLE}.
|
||||||
|
*
|
||||||
|
* @param value The {@link String} value to convert.
|
||||||
|
* @return Returns the internal value for value.
|
||||||
|
*/
|
||||||
|
public static int getTerminalCursorStyleInternalPropertyValueFromValue(String value) {
|
||||||
|
return (int) SharedProperties.getDefaultIfNotInMap(TermuxPropertyConstants.KEY_TERMINAL_CURSOR_STYLE, TermuxPropertyConstants.MAP_TERMINAL_CURSOR_STYLE, SharedProperties.toLowerCase(value), TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_CURSOR_STYLE, true, LOG_TAG);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the int for the value if its not null and is between
|
* Returns the int for the value if its not null and is between
|
||||||
* {@code TermuxPropertyConstants#IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR_MIN} and
|
* {@link TermuxPropertyConstants#IVALUE_TERMINAL_TRANSCRIPT_ROWS_MIN} and
|
||||||
* {@code TermuxPropertyConstants#IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR_MAX},
|
* {@link TermuxPropertyConstants#IVALUE_TERMINAL_TRANSCRIPT_ROWS_MAX},
|
||||||
* otherwise returns {@code TermuxPropertyConstants#DEFAULT_IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR}.
|
* otherwise returns {@link TermuxPropertyConstants#DEFAULT_IVALUE_TERMINAL_TRANSCRIPT_ROWS}.
|
||||||
|
*
|
||||||
|
* @param value The {@link String} value to convert.
|
||||||
|
* @return Returns the internal value for value.
|
||||||
|
*/
|
||||||
|
public static int getTerminalTranscriptRowsInternalPropertyValueFromValue(String value) {
|
||||||
|
return SharedProperties.getDefaultIfNotInRange(TermuxPropertyConstants.KEY_TERMINAL_TRANSCRIPT_ROWS,
|
||||||
|
DataUtils.getIntFromString(value, TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_TRANSCRIPT_ROWS),
|
||||||
|
TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_TRANSCRIPT_ROWS,
|
||||||
|
TermuxPropertyConstants.IVALUE_TERMINAL_TRANSCRIPT_ROWS_MIN,
|
||||||
|
TermuxPropertyConstants.IVALUE_TERMINAL_TRANSCRIPT_ROWS_MAX,
|
||||||
|
true, true, LOG_TAG);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the int for the value if its not null and is between
|
||||||
|
* {@link TermuxPropertyConstants#IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR_MIN} and
|
||||||
|
* {@link TermuxPropertyConstants#IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR_MAX},
|
||||||
|
* otherwise returns {@link TermuxPropertyConstants#DEFAULT_IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR}.
|
||||||
*
|
*
|
||||||
* @param value The {@link String} value to convert.
|
* @param value The {@link String} value to convert.
|
||||||
* @return Returns the internal value for value.
|
* @return Returns the internal value for value.
|
||||||
@@ -403,6 +460,14 @@ public class TermuxSharedProperties implements SharedPropertiesParser {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public boolean isTerminalMarginAdjustmentDisabled() {
|
||||||
|
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_DISABLE_TERMINAL_MARGIN_ADJUSTMENT, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean areTerminalSessionChangeToastsDisabled() {
|
||||||
|
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_DISABLE_TERMINAL_SESSION_CHANGE_TOAST, true);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isEnforcingCharBasedInput() {
|
public boolean isEnforcingCharBasedInput() {
|
||||||
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_ENFORCE_CHAR_BASED_INPUT, true);
|
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_ENFORCE_CHAR_BASED_INPUT, true);
|
||||||
}
|
}
|
||||||
@@ -411,6 +476,10 @@ public class TermuxSharedProperties implements SharedPropertiesParser {
|
|||||||
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_HIDE_SOFT_KEYBOARD_ON_STARTUP, true);
|
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_HIDE_SOFT_KEYBOARD_ON_STARTUP, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean shouldOpenTerminalTranscriptURLOnClick() {
|
||||||
|
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_TERMINAL_ONCLICK_URL_OPEN, true);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isUsingBlackUI() {
|
public boolean isUsingBlackUI() {
|
||||||
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_USE_BLACK_UI, true);
|
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_USE_BLACK_UI, true);
|
||||||
}
|
}
|
||||||
@@ -435,6 +504,14 @@ public class TermuxSharedProperties implements SharedPropertiesParser {
|
|||||||
return (int) getInternalPropertyValue(TermuxPropertyConstants.KEY_TERMINAL_CURSOR_BLINK_RATE, true);
|
return (int) getInternalPropertyValue(TermuxPropertyConstants.KEY_TERMINAL_CURSOR_BLINK_RATE, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getTerminalCursorStyle() {
|
||||||
|
return (int) getInternalPropertyValue(TermuxPropertyConstants.KEY_TERMINAL_CURSOR_STYLE, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTerminalTranscriptRows() {
|
||||||
|
return (int) getInternalPropertyValue(TermuxPropertyConstants.KEY_TERMINAL_TRANSCRIPT_ROWS, true);
|
||||||
|
}
|
||||||
|
|
||||||
public float getTerminalToolbarHeightScaleFactor() {
|
public float getTerminalToolbarHeightScaleFactor() {
|
||||||
return (float) getInternalPropertyValue(TermuxPropertyConstants.KEY_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR, true);
|
return (float) getInternalPropertyValue(TermuxPropertyConstants.KEY_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,335 @@
|
|||||||
|
package com.termux.shared.shell;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import com.termux.shared.R;
|
||||||
|
import com.termux.shared.data.DataUtils;
|
||||||
|
import com.termux.shared.models.errors.Error;
|
||||||
|
import com.termux.shared.file.FileUtils;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.models.ResultConfig;
|
||||||
|
import com.termux.shared.models.ResultData;
|
||||||
|
import com.termux.shared.models.errors.FunctionErrno;
|
||||||
|
import com.termux.shared.models.errors.ResultSenderErrno;
|
||||||
|
import com.termux.shared.termux.AndroidUtils;
|
||||||
|
import com.termux.shared.termux.TermuxConstants.RESULT_SENDER;
|
||||||
|
|
||||||
|
public class ResultSender {
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "ResultSender";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send result stored in {@link ResultConfig} to command caller via
|
||||||
|
* {@link ResultConfig#resultPendingIntent} and/or by writing it to files in
|
||||||
|
* {@link ResultConfig#resultDirectoryPath}. If both are not {@code null}, then result will be
|
||||||
|
* sent via both.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} for operations.
|
||||||
|
* @param logTag The log tag to use for logging.
|
||||||
|
* @param label The label for the command.
|
||||||
|
* @param resultConfig The {@link ResultConfig} object containing information on how to send the result.
|
||||||
|
* @param resultData The {@link ResultData} object containing result data.
|
||||||
|
* @return Returns the {@link Error} if failed to send the result, otherwise {@code null}.
|
||||||
|
*/
|
||||||
|
public static Error sendCommandResultData(Context context, String logTag, String label, ResultConfig resultConfig, ResultData resultData) {
|
||||||
|
if (context == null || resultConfig == null || resultData == null)
|
||||||
|
return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETERS.getError("context, resultConfig or resultData", "sendCommandResultData");
|
||||||
|
|
||||||
|
Error error;
|
||||||
|
|
||||||
|
if (resultConfig.resultPendingIntent != null) {
|
||||||
|
error = sendCommandResultDataWithPendingIntent(context, logTag, label, resultConfig, resultData);
|
||||||
|
if (error != null || resultConfig.resultDirectoryPath == null)
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resultConfig.resultDirectoryPath != null) {
|
||||||
|
return sendCommandResultDataToDirectory(context, logTag, label, resultConfig, resultData);
|
||||||
|
} else {
|
||||||
|
return FunctionErrno.ERRNO_UNSET_PARAMETERS.getError("resultConfig.resultPendingIntent or resultConfig.resultDirectoryPath", "sendCommandResultData");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send result stored in {@link ResultConfig} to command caller via {@link ResultConfig#resultPendingIntent}.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} for operations.
|
||||||
|
* @param logTag The log tag to use for logging.
|
||||||
|
* @param label The label for the command.
|
||||||
|
* @param resultConfig The {@link ResultConfig} object containing information on how to send the result.
|
||||||
|
* @param resultData The {@link ResultData} object containing result data.
|
||||||
|
* @return Returns the {@link Error} if failed to send the result, otherwise {@code null}.
|
||||||
|
*/
|
||||||
|
public static Error sendCommandResultDataWithPendingIntent(Context context, String logTag, String label, ResultConfig resultConfig, ResultData resultData) {
|
||||||
|
if (context == null || resultConfig == null || resultData == null || resultConfig.resultPendingIntent == null || resultConfig.resultBundleKey == null)
|
||||||
|
return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError("context, resultConfig, resultData, resultConfig.resultPendingIntent or resultConfig.resultBundleKey", "sendCommandResultDataWithPendingIntent");
|
||||||
|
|
||||||
|
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||||
|
|
||||||
|
Logger.logDebugExtended(logTag, "Sending result for command \"" + label + "\":\n" + resultConfig.toString() + "\n" + resultData.toString());
|
||||||
|
|
||||||
|
String resultDataStdout = resultData.stdout.toString();
|
||||||
|
String resultDataStderr = resultData.stderr.toString();
|
||||||
|
|
||||||
|
String truncatedStdout = null;
|
||||||
|
String truncatedStderr = null;
|
||||||
|
|
||||||
|
String stdoutOriginalLength = String.valueOf(resultDataStdout.length());
|
||||||
|
String stderrOriginalLength = String.valueOf(resultDataStderr.length());
|
||||||
|
|
||||||
|
// Truncate stdout and stdout to max TRANSACTION_SIZE_LIMIT_IN_BYTES
|
||||||
|
if (resultDataStderr.isEmpty()) {
|
||||||
|
truncatedStdout = DataUtils.getTruncatedCommandOutput(resultDataStdout, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false);
|
||||||
|
} else if (resultDataStdout.isEmpty()) {
|
||||||
|
truncatedStderr = DataUtils.getTruncatedCommandOutput(resultDataStderr, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false);
|
||||||
|
} else {
|
||||||
|
truncatedStdout = DataUtils.getTruncatedCommandOutput(resultDataStdout, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 2, false, false, false);
|
||||||
|
truncatedStderr = DataUtils.getTruncatedCommandOutput(resultDataStderr, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 2, false, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (truncatedStdout != null && truncatedStdout.length() < resultDataStdout.length()) {
|
||||||
|
Logger.logWarn(logTag, "The result for command \"" + label + "\" stdout length truncated from " + stdoutOriginalLength + " to " + truncatedStdout.length());
|
||||||
|
resultDataStdout = truncatedStdout;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (truncatedStderr != null && truncatedStderr.length() < resultDataStderr.length()) {
|
||||||
|
Logger.logWarn(logTag, "The result for command \"" + label + "\" stderr length truncated from " + stderrOriginalLength + " to " + truncatedStderr.length());
|
||||||
|
resultDataStderr = truncatedStderr;
|
||||||
|
}
|
||||||
|
|
||||||
|
String resultDataErrmsg = null;
|
||||||
|
if (resultData.isStateFailed()) {
|
||||||
|
resultDataErrmsg = ResultData.getErrorsListLogString(resultData);
|
||||||
|
if (resultDataErrmsg.isEmpty()) resultDataErrmsg = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String errmsgOriginalLength = (resultDataErrmsg == null) ? null : String.valueOf(resultDataErrmsg.length());
|
||||||
|
|
||||||
|
// Truncate error to max TRANSACTION_SIZE_LIMIT_IN_BYTES / 4
|
||||||
|
// trim from end to preserve start of stacktraces
|
||||||
|
String truncatedErrmsg = DataUtils.getTruncatedCommandOutput(resultDataErrmsg, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 4, true, false, false);
|
||||||
|
if (truncatedErrmsg != null && truncatedErrmsg.length() < resultDataErrmsg.length()) {
|
||||||
|
Logger.logWarn(logTag, "The result for command \"" + label + "\" error length truncated from " + errmsgOriginalLength + " to " + truncatedErrmsg.length());
|
||||||
|
resultDataErrmsg = truncatedErrmsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
final Bundle resultBundle = new Bundle();
|
||||||
|
resultBundle.putString(resultConfig.resultStdoutKey, resultDataStdout);
|
||||||
|
resultBundle.putString(resultConfig.resultStdoutOriginalLengthKey, stdoutOriginalLength);
|
||||||
|
resultBundle.putString(resultConfig.resultStderrKey, resultDataStderr);
|
||||||
|
resultBundle.putString(resultConfig.resultStderrOriginalLengthKey, stderrOriginalLength);
|
||||||
|
if (resultData.exitCode != null)
|
||||||
|
resultBundle.putInt(resultConfig.resultExitCodeKey, resultData.exitCode);
|
||||||
|
resultBundle.putInt(resultConfig.resultErrCodeKey, resultData.getErrCode());
|
||||||
|
resultBundle.putString(resultConfig.resultErrmsgKey, resultDataErrmsg);
|
||||||
|
|
||||||
|
Intent resultIntent = new Intent();
|
||||||
|
resultIntent.putExtra(resultConfig.resultBundleKey, resultBundle);
|
||||||
|
|
||||||
|
try {
|
||||||
|
resultConfig.resultPendingIntent.send(context, Activity.RESULT_OK, resultIntent);
|
||||||
|
} catch (PendingIntent.CanceledException e) {
|
||||||
|
// The caller doesn't want the result? That's fine, just ignore
|
||||||
|
Logger.logDebug(logTag, "The command \"" + label + "\" creator " + resultConfig.resultPendingIntent.getCreatorPackage() + " does not want the results anymore");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send result stored in {@link ResultConfig} to command caller by writing it to files in
|
||||||
|
* {@link ResultConfig#resultDirectoryPath}.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} for operations.
|
||||||
|
* @param logTag The log tag to use for logging.
|
||||||
|
* @param label The label for the command.
|
||||||
|
* @param resultConfig The {@link ResultConfig} object containing information on how to send the result.
|
||||||
|
* @param resultData The {@link ResultData} object containing result data.
|
||||||
|
* @return Returns the {@link Error} if failed to send the result, otherwise {@code null}.
|
||||||
|
*/
|
||||||
|
public static Error sendCommandResultDataToDirectory(Context context, String logTag, String label, ResultConfig resultConfig, ResultData resultData) {
|
||||||
|
if (context == null || resultConfig == null || resultData == null || DataUtils.isNullOrEmpty(resultConfig.resultDirectoryPath))
|
||||||
|
return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError("context, resultConfig, resultData or resultConfig.resultDirectoryPath", "sendCommandResultDataToDirectory");
|
||||||
|
|
||||||
|
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||||
|
|
||||||
|
Error error;
|
||||||
|
|
||||||
|
String resultDataStdout = resultData.stdout.toString();
|
||||||
|
String resultDataStderr = resultData.stderr.toString();
|
||||||
|
|
||||||
|
String resultDataExitCode = "";
|
||||||
|
if (resultData.exitCode != null)
|
||||||
|
resultDataExitCode = String.valueOf(resultData.exitCode);
|
||||||
|
|
||||||
|
String resultDataErrmsg = null;
|
||||||
|
if (resultData.isStateFailed()) {
|
||||||
|
resultDataErrmsg = ResultData.getErrorsListLogString(resultData);
|
||||||
|
}
|
||||||
|
resultDataErrmsg = DataUtils.getDefaultIfNull(resultDataErrmsg, "");
|
||||||
|
|
||||||
|
resultConfig.resultDirectoryPath = FileUtils.getCanonicalPath(resultConfig.resultDirectoryPath, null);
|
||||||
|
|
||||||
|
Logger.logDebugExtended(logTag, "Writing result for command \"" + label + "\":\n" + resultConfig.toString() + "\n" + resultData.toString());
|
||||||
|
|
||||||
|
// If resultDirectoryPath 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 resultDirectoryPath is under resultDirectoryAllowedParentPath.
|
||||||
|
// We try to set execute permissions, but ignore if they are missing, since only read and write
|
||||||
|
// permissions are required for working directories.
|
||||||
|
error = FileUtils.validateDirectoryFileExistenceAndPermissions("result", resultConfig.resultDirectoryPath,
|
||||||
|
resultConfig.resultDirectoryAllowedParentPath, true,
|
||||||
|
FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS, true, true,
|
||||||
|
true, true);
|
||||||
|
if (error != null) {
|
||||||
|
error.appendMessage("\n" + context.getString(R.string.msg_directory_absolute_path, "Result", resultConfig.resultDirectoryPath));
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resultConfig.resultSingleFile) {
|
||||||
|
// If resultFileBasename is null, empty or contains forward slashes "/"
|
||||||
|
if (DataUtils.isNullOrEmpty(resultConfig.resultFileBasename) ||
|
||||||
|
resultConfig.resultFileBasename.contains("/")) {
|
||||||
|
error = ResultSenderErrno.ERROR_RESULT_FILE_BASENAME_NULL_OR_INVALID.getError(resultConfig.resultFileBasename);
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
String error_or_output;
|
||||||
|
|
||||||
|
if (resultData.isStateFailed()) {
|
||||||
|
try {
|
||||||
|
if (DataUtils.isNullOrEmpty(resultConfig.resultFileErrorFormat)) {
|
||||||
|
error_or_output = String.format(RESULT_SENDER.FORMAT_FAILED_ERR__ERRMSG__STDOUT__STDERR__EXIT_CODE,
|
||||||
|
resultData.getErrCode(), resultDataErrmsg, resultDataStdout, resultDataStderr, resultDataExitCode);
|
||||||
|
} else {
|
||||||
|
error_or_output = String.format(resultConfig.resultFileErrorFormat,
|
||||||
|
resultData.getErrCode(), resultDataErrmsg, resultDataStdout, resultDataStderr, resultDataExitCode);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
error = ResultSenderErrno.ERROR_FORMAT_RESULT_ERROR_FAILED_WITH_EXCEPTION.getError(e.getMessage());
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
if (DataUtils.isNullOrEmpty(resultConfig.resultFileOutputFormat)) {
|
||||||
|
if (resultDataStderr.isEmpty() && resultDataExitCode.equals("0"))
|
||||||
|
error_or_output = String.format(RESULT_SENDER.FORMAT_SUCCESS_STDOUT, resultDataStdout);
|
||||||
|
else if (resultDataStderr.isEmpty())
|
||||||
|
error_or_output = String.format(RESULT_SENDER.FORMAT_SUCCESS_STDOUT__EXIT_CODE, resultDataStdout, resultDataExitCode);
|
||||||
|
else
|
||||||
|
error_or_output = String.format(RESULT_SENDER.FORMAT_SUCCESS_STDOUT__STDERR__EXIT_CODE, resultDataStdout, resultDataStderr, resultDataExitCode);
|
||||||
|
} else {
|
||||||
|
error_or_output = String.format(resultConfig.resultFileOutputFormat, resultDataStdout, resultDataStderr, resultDataExitCode);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
error = ResultSenderErrno.ERROR_FORMAT_RESULT_OUTPUT_FAILED_WITH_EXCEPTION.getError(e.getMessage());
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write error or output to temp file
|
||||||
|
// Check errCode file creation below for explanation for why temp file is used
|
||||||
|
String temp_filename = resultConfig.resultFileBasename + "-" + AndroidUtils.getCurrentMilliSecondLocalTimeStamp();
|
||||||
|
error = FileUtils.writeStringToFile(temp_filename, resultConfig.resultDirectoryPath + "/" + temp_filename,
|
||||||
|
null, error_or_output, false);
|
||||||
|
if (error != null) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move error or output temp file to final destination
|
||||||
|
error = FileUtils.moveRegularFile("error or output temp file", resultConfig.resultDirectoryPath + "/" + temp_filename,
|
||||||
|
resultConfig.resultDirectoryPath + "/" + resultConfig.resultFileBasename, false);
|
||||||
|
if (error != null) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String filename;
|
||||||
|
|
||||||
|
// Default to no suffix, useful if user expects result in an empty directory, like created with mktemp
|
||||||
|
if (resultConfig.resultFilesSuffix == null)
|
||||||
|
resultConfig.resultFilesSuffix = "";
|
||||||
|
|
||||||
|
// If resultFilesSuffix contains forward slashes "/"
|
||||||
|
if (resultConfig.resultFilesSuffix.contains("/")) {
|
||||||
|
error = ResultSenderErrno.ERROR_RESULT_FILES_SUFFIX_INVALID.getError(resultConfig.resultFilesSuffix);
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write result to result files under resultDirectoryPath
|
||||||
|
|
||||||
|
// Write stdout to file
|
||||||
|
if (!resultDataStdout.isEmpty()) {
|
||||||
|
filename = RESULT_SENDER.RESULT_FILE_STDOUT_PREFIX + resultConfig.resultFilesSuffix;
|
||||||
|
error = FileUtils.writeStringToFile(filename, resultConfig.resultDirectoryPath + "/" + filename,
|
||||||
|
null, resultDataStdout, false);
|
||||||
|
if (error != null) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write stderr to file
|
||||||
|
if (!resultDataStderr.isEmpty()) {
|
||||||
|
filename = RESULT_SENDER.RESULT_FILE_STDERR_PREFIX + resultConfig.resultFilesSuffix;
|
||||||
|
error = FileUtils.writeStringToFile(filename, resultConfig.resultDirectoryPath + "/" + filename,
|
||||||
|
null, resultDataStderr, false);
|
||||||
|
if (error != null) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write exitCode to file
|
||||||
|
if (!resultDataExitCode.isEmpty()) {
|
||||||
|
filename = RESULT_SENDER.RESULT_FILE_EXIT_CODE_PREFIX + resultConfig.resultFilesSuffix;
|
||||||
|
error = FileUtils.writeStringToFile(filename, resultConfig.resultDirectoryPath + "/" + filename,
|
||||||
|
null, resultDataExitCode, false);
|
||||||
|
if (error != null) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write errmsg to file
|
||||||
|
if (resultData.isStateFailed() && !resultDataErrmsg.isEmpty()) {
|
||||||
|
filename = RESULT_SENDER.RESULT_FILE_ERRMSG_PREFIX + resultConfig.resultFilesSuffix;
|
||||||
|
error = FileUtils.writeStringToFile(filename, resultConfig.resultDirectoryPath + "/" + filename,
|
||||||
|
null, resultDataErrmsg, false);
|
||||||
|
if (error != null) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write errCode to file
|
||||||
|
// This must be created after writing to other result files has already finished since
|
||||||
|
// caller should wait for this file to be created to be notified that the command has
|
||||||
|
// finished and should then start reading from the rest of the result files if they exist.
|
||||||
|
// Since there may be a delay between creation of errCode file and writing to it or flushing
|
||||||
|
// to disk, we create a temp file first and then move it to the final destination, since
|
||||||
|
// caller may otherwise read from an empty file in some cases.
|
||||||
|
|
||||||
|
// Write errCode to temp file
|
||||||
|
String temp_filename = RESULT_SENDER.RESULT_FILE_ERR_PREFIX + "-" + AndroidUtils.getCurrentMilliSecondLocalTimeStamp();
|
||||||
|
if (!resultConfig.resultFilesSuffix.isEmpty()) temp_filename = temp_filename + "-" + resultConfig.resultFilesSuffix;
|
||||||
|
error = FileUtils.writeStringToFile(temp_filename, resultConfig.resultDirectoryPath + "/" + temp_filename,
|
||||||
|
null, String.valueOf(resultData.getErrCode()), false);
|
||||||
|
if (error != null) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move errCode temp file to final destination
|
||||||
|
filename = RESULT_SENDER.RESULT_FILE_ERR_PREFIX + resultConfig.resultFilesSuffix;
|
||||||
|
error = FileUtils.moveRegularFile(RESULT_SENDER.RESULT_FILE_ERR_PREFIX + " temp file", resultConfig.resultDirectoryPath + "/" + temp_filename,
|
||||||
|
resultConfig.resultDirectoryPath + "/" + filename, false);
|
||||||
|
if (error != null) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.termux.shared.shell;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public interface ShellEnvironmentClient {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default working directory path for the environment in case the path that was passed
|
||||||
|
* was {@code null} or empty.
|
||||||
|
*
|
||||||
|
* @return Should return the default working directory path.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
String getDefaultWorkingDirectoryPath();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default "/bin" path, likely $PREFIX/bin.
|
||||||
|
*
|
||||||
|
* @return Should return the "/bin" path.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
String getDefaultBinPath();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the shell environment to be used for commands.
|
||||||
|
*
|
||||||
|
* @param currentPackageContext The {@link Context} for the current package.
|
||||||
|
* @param isFailSafe If running a failsafe session.
|
||||||
|
* @param workingDirectory The working directory for the environment.
|
||||||
|
* @return Should return the build environment.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
String[] buildEnvironment(Context currentPackageContext, boolean isFailSafe, String workingDirectory);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup process arguments for the file to execute, like interpreter, etc.
|
||||||
|
*
|
||||||
|
* @param fileToExecute The file to execute.
|
||||||
|
* @param arguments The arguments to pass to the executable.
|
||||||
|
* @return Should return the final process arguments.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
String[] setupProcessArgs(@NonNull String fileToExecute, String[] arguments);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,81 +1,13 @@
|
|||||||
package com.termux.shared.shell;
|
package com.termux.shared.shell;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import com.termux.shared.termux.TermuxConstants;
|
|
||||||
import com.termux.shared.file.FileUtils;
|
|
||||||
import com.termux.shared.logger.Logger;
|
|
||||||
import com.termux.shared.packages.PackageUtils;
|
|
||||||
import com.termux.shared.termux.TermuxUtils;
|
|
||||||
import com.termux.terminal.TerminalBuffer;
|
import com.termux.terminal.TerminalBuffer;
|
||||||
import com.termux.terminal.TerminalEmulator;
|
import com.termux.terminal.TerminalEmulator;
|
||||||
import com.termux.terminal.TerminalSession;
|
import com.termux.terminal.TerminalSession;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class ShellUtils {
|
public class ShellUtils {
|
||||||
|
|
||||||
public static String[] buildEnvironment(Context currentPackageContext, boolean isFailSafe, String workingDirectory) {
|
|
||||||
TermuxConstants.TERMUX_HOME_DIR.mkdirs();
|
|
||||||
|
|
||||||
if (workingDirectory == null || workingDirectory.isEmpty()) workingDirectory = TermuxConstants.TERMUX_HOME_DIR_PATH;
|
|
||||||
|
|
||||||
List<String> environment = new ArrayList<>();
|
|
||||||
|
|
||||||
// This function may be called by a different package like a plugin, so we get version for Termux package via its context
|
|
||||||
Context termuxPackageContext = TermuxUtils.getTermuxPackageContext(currentPackageContext);
|
|
||||||
if (termuxPackageContext != null) {
|
|
||||||
String termuxVersionName = PackageUtils.getVersionNameForPackage(termuxPackageContext);
|
|
||||||
if (termuxVersionName != null)
|
|
||||||
environment.add("TERMUX_VERSION=" + termuxVersionName);
|
|
||||||
}
|
|
||||||
|
|
||||||
environment.add("TERM=xterm-256color");
|
|
||||||
environment.add("COLORTERM=truecolor");
|
|
||||||
environment.add("HOME=" + TermuxConstants.TERMUX_HOME_DIR_PATH);
|
|
||||||
environment.add("PREFIX=" + TermuxConstants.TERMUX_PREFIX_DIR_PATH);
|
|
||||||
environment.add("BOOTCLASSPATH=" + System.getenv("BOOTCLASSPATH"));
|
|
||||||
environment.add("ANDROID_ROOT=" + System.getenv("ANDROID_ROOT"));
|
|
||||||
environment.add("ANDROID_DATA=" + System.getenv("ANDROID_DATA"));
|
|
||||||
// EXTERNAL_STORAGE is needed for /system/bin/am to work on at least
|
|
||||||
// Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3.
|
|
||||||
environment.add("EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE"));
|
|
||||||
|
|
||||||
// These variables are needed if running on Android 10 and higher.
|
|
||||||
addToEnvIfPresent(environment, "ANDROID_ART_ROOT");
|
|
||||||
addToEnvIfPresent(environment, "DEX2OATBOOTCLASSPATH");
|
|
||||||
addToEnvIfPresent(environment, "ANDROID_I18N_ROOT");
|
|
||||||
addToEnvIfPresent(environment, "ANDROID_RUNTIME_ROOT");
|
|
||||||
addToEnvIfPresent(environment, "ANDROID_TZDATA_ROOT");
|
|
||||||
|
|
||||||
if (isFailSafe) {
|
|
||||||
// Keep the default path so that system binaries can be used in the failsafe session.
|
|
||||||
environment.add("PATH= " + System.getenv("PATH"));
|
|
||||||
} else {
|
|
||||||
environment.add("LANG=en_US.UTF-8");
|
|
||||||
environment.add("PATH=" + TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH);
|
|
||||||
environment.add("PWD=" + workingDirectory);
|
|
||||||
environment.add("TMPDIR=" + TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH);
|
|
||||||
}
|
|
||||||
|
|
||||||
return environment.toArray(new String[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void addToEnvIfPresent(List<String> environment, String name) {
|
|
||||||
String value = System.getenv(name);
|
|
||||||
if (value != null) {
|
|
||||||
environment.add(name + "=" + value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getPid(Process p) {
|
public static int getPid(Process p) {
|
||||||
try {
|
try {
|
||||||
Field f = p.getClass().getDeclaredField("pid");
|
Field f = p.getClass().getDeclaredField("pid");
|
||||||
@@ -90,77 +22,12 @@ public class ShellUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String[] setupProcessArgs(@NonNull String fileToExecute, String[] arguments) {
|
|
||||||
// The file to execute may either be:
|
|
||||||
// - An elf file, in which we execute it directly.
|
|
||||||
// - A script file without shebang, which we execute with our standard shell $PREFIX/bin/sh instead of the
|
|
||||||
// system /system/bin/sh. The system shell may vary and may not work at all due to LD_LIBRARY_PATH.
|
|
||||||
// - A file with shebang, which we try to handle with e.g. /bin/foo -> $PREFIX/bin/foo.
|
|
||||||
String interpreter = null;
|
|
||||||
try {
|
|
||||||
File file = new File(fileToExecute);
|
|
||||||
try (FileInputStream in = new FileInputStream(file)) {
|
|
||||||
byte[] buffer = new byte[256];
|
|
||||||
int bytesRead = in.read(buffer);
|
|
||||||
if (bytesRead > 4) {
|
|
||||||
if (buffer[0] == 0x7F && buffer[1] == 'E' && buffer[2] == 'L' && buffer[3] == 'F') {
|
|
||||||
// Elf file, do nothing.
|
|
||||||
} else if (buffer[0] == '#' && buffer[1] == '!') {
|
|
||||||
// Try to parse shebang.
|
|
||||||
StringBuilder builder = new StringBuilder();
|
|
||||||
for (int i = 2; i < bytesRead; i++) {
|
|
||||||
char c = (char) buffer[i];
|
|
||||||
if (c == ' ' || c == '\n') {
|
|
||||||
if (builder.length() == 0) {
|
|
||||||
// Skip whitespace after shebang.
|
|
||||||
} else {
|
|
||||||
// End of shebang.
|
|
||||||
String executable = builder.toString();
|
|
||||||
if (executable.startsWith("/usr") || executable.startsWith("/bin")) {
|
|
||||||
String[] parts = executable.split("/");
|
|
||||||
String binary = parts[parts.length - 1];
|
|
||||||
interpreter = TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/" + binary;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
builder.append(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No shebang and no ELF, use standard shell.
|
|
||||||
interpreter = TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/sh";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
// Ignore.
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> result = new ArrayList<>();
|
|
||||||
if (interpreter != null) result.add(interpreter);
|
|
||||||
result.add(fileToExecute);
|
|
||||||
if (arguments != null) Collections.addAll(result, arguments);
|
|
||||||
return result.toArray(new String[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getExecutableBasename(String executable) {
|
public static String getExecutableBasename(String executable) {
|
||||||
if (executable == null) return null;
|
if (executable == null) return null;
|
||||||
int lastSlash = executable.lastIndexOf('/');
|
int lastSlash = executable.lastIndexOf('/');
|
||||||
return (lastSlash == -1) ? executable : executable.substring(lastSlash + 1);
|
return (lastSlash == -1) ? executable : executable.substring(lastSlash + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void clearTermuxTMPDIR(Context context, boolean onlyIfExists) {
|
|
||||||
if(onlyIfExists && !FileUtils.directoryFileExists(TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH, false))
|
|
||||||
return;
|
|
||||||
|
|
||||||
String errmsg;
|
|
||||||
errmsg = FileUtils.clearDirectory(context, "$TMPDIR", FileUtils.getCanonicalPath(TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH, null, false));
|
|
||||||
if (errmsg != null) {
|
|
||||||
Logger.logError(errmsg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getTerminalSessionTranscriptText(TerminalSession terminalSession, boolean linesJoined, boolean trim) {
|
public static String getTerminalSessionTranscriptText(TerminalSession terminalSession, boolean linesJoined, boolean trim) {
|
||||||
if (terminalSession == null) return null;
|
if (terminalSession == null) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import androidx.annotation.NonNull;
|
|||||||
|
|
||||||
import com.termux.shared.R;
|
import com.termux.shared.R;
|
||||||
import com.termux.shared.models.ExecutionCommand;
|
import com.termux.shared.models.ExecutionCommand;
|
||||||
import com.termux.shared.termux.TermuxConstants;
|
import com.termux.shared.models.ResultData;
|
||||||
|
import com.termux.shared.models.errors.Errno;
|
||||||
import com.termux.shared.logger.Logger;
|
import com.termux.shared.logger.Logger;
|
||||||
import com.termux.terminal.TerminalSession;
|
import com.termux.terminal.TerminalSession;
|
||||||
import com.termux.terminal.TerminalSessionClient;
|
import com.termux.terminal.TerminalSessionClient;
|
||||||
@@ -50,8 +51,9 @@ public class TermuxSession {
|
|||||||
* @param executionCommand The {@link ExecutionCommand} containing the information for execution command.
|
* @param executionCommand The {@link ExecutionCommand} containing the information for execution command.
|
||||||
* @param terminalSessionClient The {@link TerminalSessionClient} interface implementation.
|
* @param terminalSessionClient The {@link TerminalSessionClient} interface implementation.
|
||||||
* @param termuxSessionClient The {@link TermuxSessionClient} interface implementation.
|
* @param termuxSessionClient The {@link TermuxSessionClient} interface implementation.
|
||||||
|
* @param shellEnvironmentClient The {@link ShellEnvironmentClient} interface implementation.
|
||||||
* @param sessionName The optional {@link TerminalSession} name.
|
* @param sessionName The optional {@link TerminalSession} name.
|
||||||
* @param setStdoutOnExit If set to {@code true}, then the {@link ExecutionCommand#stdout}
|
* @param setStdoutOnExit If set to {@code true}, then the {@link ResultData#stdout}
|
||||||
* available in the {@link TermuxSessionClient#onTermuxSessionExited(TermuxSession)}
|
* available in the {@link TermuxSessionClient#onTermuxSessionExited(TermuxSession)}
|
||||||
* callback will be set to the {@link TerminalSession} transcript. The session
|
* callback will be set to the {@link TerminalSession} transcript. The session
|
||||||
* transcript will contain both stdout and stderr combined, basically
|
* transcript will contain both stdout and stderr combined, basically
|
||||||
@@ -62,16 +64,24 @@ public class TermuxSession {
|
|||||||
*/
|
*/
|
||||||
public static TermuxSession execute(@NonNull final Context context, @NonNull ExecutionCommand executionCommand,
|
public static TermuxSession execute(@NonNull final Context context, @NonNull ExecutionCommand executionCommand,
|
||||||
@NonNull final TerminalSessionClient terminalSessionClient, final TermuxSessionClient termuxSessionClient,
|
@NonNull final TerminalSessionClient terminalSessionClient, final TermuxSessionClient termuxSessionClient,
|
||||||
|
@NonNull final ShellEnvironmentClient shellEnvironmentClient,
|
||||||
final String sessionName, final boolean setStdoutOnExit) {
|
final String sessionName, final boolean setStdoutOnExit) {
|
||||||
if (executionCommand.workingDirectory == null || executionCommand.workingDirectory.isEmpty()) executionCommand.workingDirectory = TermuxConstants.TERMUX_HOME_DIR_PATH;
|
if (executionCommand.workingDirectory == null || executionCommand.workingDirectory.isEmpty())
|
||||||
|
executionCommand.workingDirectory = shellEnvironmentClient.getDefaultWorkingDirectoryPath();
|
||||||
|
if (executionCommand.workingDirectory.isEmpty())
|
||||||
|
executionCommand.workingDirectory = "/";
|
||||||
|
|
||||||
String[] environment = ShellUtils.buildEnvironment(context, executionCommand.isFailsafe, executionCommand.workingDirectory);
|
String[] environment = shellEnvironmentClient.buildEnvironment(context, executionCommand.isFailsafe, executionCommand.workingDirectory);
|
||||||
|
|
||||||
|
String defaultBinPath = shellEnvironmentClient.getDefaultBinPath();
|
||||||
|
if (defaultBinPath.isEmpty())
|
||||||
|
defaultBinPath = "/system/bin";
|
||||||
|
|
||||||
boolean isLoginShell = false;
|
boolean isLoginShell = false;
|
||||||
if (executionCommand.executable == null) {
|
if (executionCommand.executable == null) {
|
||||||
if (!executionCommand.isFailsafe) {
|
if (!executionCommand.isFailsafe) {
|
||||||
for (String shellBinary : new String[]{"login", "bash", "zsh"}) {
|
for (String shellBinary : new String[]{"login", "bash", "zsh"}) {
|
||||||
File shellFile = new File(TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH, shellBinary);
|
File shellFile = new File(defaultBinPath, shellBinary);
|
||||||
if (shellFile.canExecute()) {
|
if (shellFile.canExecute()) {
|
||||||
executionCommand.executable = shellFile.getAbsolutePath();
|
executionCommand.executable = shellFile.getAbsolutePath();
|
||||||
break;
|
break;
|
||||||
@@ -81,12 +91,21 @@ public class TermuxSession {
|
|||||||
|
|
||||||
if (executionCommand.executable == null) {
|
if (executionCommand.executable == null) {
|
||||||
// Fall back to system shell as last resort:
|
// Fall back to system shell as last resort:
|
||||||
|
// Do not start a login shell since ~/.profile may cause startup failure if its invalid.
|
||||||
|
// /system/bin/sh is provided by mksh (not toybox) and does load .mkshrc but for android its set
|
||||||
|
// to /system/etc/mkshrc even though its default is ~/.mkshrc.
|
||||||
|
// So /system/etc/mkshrc must still be valid for failsafe session to start properly.
|
||||||
|
// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:external/mksh/src/main.c;l=663
|
||||||
|
// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:external/mksh/src/main.c;l=41
|
||||||
|
// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:external/mksh/Android.bp;l=114
|
||||||
executionCommand.executable = "/system/bin/sh";
|
executionCommand.executable = "/system/bin/sh";
|
||||||
|
} else {
|
||||||
|
isLoginShell = true;
|
||||||
}
|
}
|
||||||
isLoginShell = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String[] processArgs = ShellUtils.setupProcessArgs(executionCommand.executable, executionCommand.arguments);
|
String[] processArgs = shellEnvironmentClient.setupProcessArgs(executionCommand.executable, executionCommand.arguments);
|
||||||
|
|
||||||
executionCommand.executable = processArgs[0];
|
executionCommand.executable = processArgs[0];
|
||||||
String processName = (isLoginShell ? "-" : "") + ShellUtils.getExecutableBasename(executionCommand.executable);
|
String processName = (isLoginShell ? "-" : "") + ShellUtils.getExecutableBasename(executionCommand.executable);
|
||||||
@@ -101,7 +120,7 @@ public class TermuxSession {
|
|||||||
executionCommand.commandLabel = processName;
|
executionCommand.commandLabel = processName;
|
||||||
|
|
||||||
if (!executionCommand.setState(ExecutionCommand.ExecutionState.EXECUTING)) {
|
if (!executionCommand.setState(ExecutionCommand.ExecutionState.EXECUTING)) {
|
||||||
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, context.getString(R.string.error_failed_to_execute_termux_session_command, executionCommand.getCommandIdAndLabelLogString()), null);
|
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_failed_to_execute_termux_session_command, executionCommand.getCommandIdAndLabelLogString()));
|
||||||
TermuxSession.processTermuxSessionResult(null, executionCommand);
|
TermuxSession.processTermuxSessionResult(null, executionCommand);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -109,7 +128,7 @@ public class TermuxSession {
|
|||||||
Logger.logDebug(LOG_TAG, executionCommand.toString());
|
Logger.logDebug(LOG_TAG, executionCommand.toString());
|
||||||
|
|
||||||
Logger.logDebug(LOG_TAG, "Running \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession");
|
Logger.logDebug(LOG_TAG, "Running \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession");
|
||||||
TerminalSession terminalSession = new TerminalSession(executionCommand.executable, executionCommand.workingDirectory, executionCommand.arguments, environment, terminalSessionClient);
|
TerminalSession terminalSession = new TerminalSession(executionCommand.executable, executionCommand.workingDirectory, executionCommand.arguments, environment, executionCommand.terminalTranscriptRows, terminalSessionClient);
|
||||||
|
|
||||||
if (sessionName != null) {
|
if (sessionName != null) {
|
||||||
terminalSession.mSessionName = sessionName;
|
terminalSession.mSessionName = sessionName;
|
||||||
@@ -122,8 +141,8 @@ public class TermuxSession {
|
|||||||
* Signal that this {@link TermuxSession} has finished. This should be called when
|
* Signal that this {@link TermuxSession} has finished. This should be called when
|
||||||
* {@link TerminalSessionClient#onSessionFinished(TerminalSession)} callback is received by the caller.
|
* {@link TerminalSessionClient#onSessionFinished(TerminalSession)} callback is received by the caller.
|
||||||
*
|
*
|
||||||
* If the processes has finished, then sets {@link ExecutionCommand#stdout}, {@link ExecutionCommand#stderr}
|
* If the processes has finished, then sets {@link ResultData#stdout}, {@link ResultData#stderr}
|
||||||
* and {@link ExecutionCommand#exitCode} for the {@link #mExecutionCommand} of the {@code termuxTask}
|
* and {@link ResultData#exitCode} for the {@link #mExecutionCommand} of the {@code termuxTask}
|
||||||
* and then calls {@link #processTermuxSessionResult(TermuxSession, ExecutionCommand)} to process the result}.
|
* and then calls {@link #processTermuxSessionResult(TermuxSession, ExecutionCommand)} to process the result}.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@@ -134,9 +153,9 @@ public class TermuxSession {
|
|||||||
int exitCode = mTerminalSession.getExitStatus();
|
int exitCode = mTerminalSession.getExitStatus();
|
||||||
|
|
||||||
if (exitCode == 0)
|
if (exitCode == 0)
|
||||||
Logger.logDebug(LOG_TAG, "The \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession with exited normally");
|
Logger.logDebug(LOG_TAG, "The \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession exited normally");
|
||||||
else
|
else
|
||||||
Logger.logDebug(LOG_TAG, "The \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession with exited with code: " + exitCode);
|
Logger.logDebug(LOG_TAG, "The \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession exited with code: " + exitCode);
|
||||||
|
|
||||||
// If the execution command has already failed, like SIGKILL was sent, then don't continue
|
// If the execution command has already failed, like SIGKILL was sent, then don't continue
|
||||||
if (mExecutionCommand.isStateFailed()) {
|
if (mExecutionCommand.isStateFailed()) {
|
||||||
@@ -144,13 +163,10 @@ public class TermuxSession {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.mSetStdoutOnExit)
|
mExecutionCommand.resultData.exitCode = exitCode;
|
||||||
mExecutionCommand.stdout = ShellUtils.getTerminalSessionTranscriptText(mTerminalSession, true, false);
|
|
||||||
else
|
|
||||||
mExecutionCommand.stdout = null;
|
|
||||||
|
|
||||||
mExecutionCommand.stderr = null;
|
if (this.mSetStdoutOnExit)
|
||||||
mExecutionCommand.exitCode = exitCode;
|
mExecutionCommand.resultData.stdout.append(ShellUtils.getTerminalSessionTranscriptText(mTerminalSession, true, false));
|
||||||
|
|
||||||
if (!mExecutionCommand.setState(ExecutionCommand.ExecutionState.EXECUTED))
|
if (!mExecutionCommand.setState(ExecutionCommand.ExecutionState.EXECUTED))
|
||||||
return;
|
return;
|
||||||
@@ -162,8 +178,6 @@ public class TermuxSession {
|
|||||||
* Kill this {@link TermuxSession} by sending a {@link OsConstants#SIGILL} to its {@link #mTerminalSession}
|
* Kill this {@link TermuxSession} by sending a {@link OsConstants#SIGILL} to its {@link #mTerminalSession}
|
||||||
* if its still executing.
|
* if its still executing.
|
||||||
*
|
*
|
||||||
* We process the results even if
|
|
||||||
*
|
|
||||||
* @param context The {@link Context} for operations.
|
* @param context The {@link Context} for operations.
|
||||||
* @param processResult If set to {@code true}, then the {@link #processTermuxSessionResult(TermuxSession, ExecutionCommand)}
|
* @param processResult If set to {@code true}, then the {@link #processTermuxSessionResult(TermuxSession, ExecutionCommand)}
|
||||||
* will be called to process the failure.
|
* will be called to process the failure.
|
||||||
@@ -176,16 +190,13 @@ public class TermuxSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Logger.logDebug(LOG_TAG, "Send SIGKILL to \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession");
|
Logger.logDebug(LOG_TAG, "Send SIGKILL to \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession");
|
||||||
if (mExecutionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, context.getString(R.string.error_sending_sigkill_to_process), null)) {
|
if (mExecutionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_sending_sigkill_to_process))) {
|
||||||
if (processResult) {
|
if (processResult) {
|
||||||
|
mExecutionCommand.resultData.exitCode = 137; // SIGKILL
|
||||||
|
|
||||||
// Get whatever output has been set till now in case its needed
|
// Get whatever output has been set till now in case its needed
|
||||||
if (this.mSetStdoutOnExit)
|
if (this.mSetStdoutOnExit)
|
||||||
mExecutionCommand.stdout = ShellUtils.getTerminalSessionTranscriptText(mTerminalSession, true, false);
|
mExecutionCommand.resultData.stdout.append(ShellUtils.getTerminalSessionTranscriptText(mTerminalSession, true, false));
|
||||||
else
|
|
||||||
mExecutionCommand.stdout = null;
|
|
||||||
|
|
||||||
mExecutionCommand.stderr = null;
|
|
||||||
mExecutionCommand.exitCode = 137; // SIGKILL
|
|
||||||
|
|
||||||
TermuxSession.processTermuxSessionResult(this, null);
|
TermuxSession.processTermuxSessionResult(this, null);
|
||||||
}
|
}
|
||||||
@@ -205,10 +216,10 @@ public class TermuxSession {
|
|||||||
* callback will be called.
|
* callback will be called.
|
||||||
*
|
*
|
||||||
* @param termuxSession The {@link TermuxSession}, which should be set if
|
* @param termuxSession The {@link TermuxSession}, which should be set if
|
||||||
* {@link #execute(Context, ExecutionCommand, TerminalSessionClient, TermuxSessionClient, String, boolean)}
|
* {@link #execute(Context, ExecutionCommand, TerminalSessionClient, TermuxSessionClient, ShellEnvironmentClient, String, boolean)}
|
||||||
* successfully started the process.
|
* successfully started the process.
|
||||||
* @param executionCommand The {@link ExecutionCommand}, which should be set if
|
* @param executionCommand The {@link ExecutionCommand}, which should be set if
|
||||||
* {@link #execute(Context, ExecutionCommand, TerminalSessionClient, TermuxSessionClient, String, boolean)}
|
* {@link #execute(Context, ExecutionCommand, TerminalSessionClient, TermuxSessionClient, ShellEnvironmentClient, String, boolean)}
|
||||||
* failed to start the process.
|
* failed to start the process.
|
||||||
*/
|
*/
|
||||||
private static void processTermuxSessionResult(final TermuxSession termuxSession, ExecutionCommand executionCommand) {
|
private static void processTermuxSessionResult(final TermuxSession termuxSession, ExecutionCommand executionCommand) {
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.termux.shared.shell;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public class TermuxShellEnvironmentClient implements ShellEnvironmentClient {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String getDefaultWorkingDirectoryPath() {
|
||||||
|
return TermuxShellUtils.getDefaultWorkingDirectoryPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String getDefaultBinPath() {
|
||||||
|
return TermuxShellUtils.getDefaultBinPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String[] buildEnvironment(Context currentPackageContext, boolean isFailSafe, String workingDirectory) {
|
||||||
|
return TermuxShellUtils.buildEnvironment(currentPackageContext, isFailSafe, workingDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String[] setupProcessArgs(@NonNull String fileToExecute, String[] arguments) {
|
||||||
|
return TermuxShellUtils.setupProcessArgs(fileToExecute, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package com.termux.shared.shell;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.termux.shared.models.errors.Error;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
import com.termux.shared.file.FileUtils;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.packages.PackageUtils;
|
||||||
|
import com.termux.shared.termux.TermuxUtils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class TermuxShellUtils {
|
||||||
|
|
||||||
|
public static String getDefaultWorkingDirectoryPath() {
|
||||||
|
return TermuxConstants.TERMUX_HOME_DIR_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getDefaultBinPath() {
|
||||||
|
return TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String[] buildEnvironment(Context currentPackageContext, boolean isFailSafe, String workingDirectory) {
|
||||||
|
TermuxConstants.TERMUX_HOME_DIR.mkdirs();
|
||||||
|
|
||||||
|
if (workingDirectory == null || workingDirectory.isEmpty())
|
||||||
|
workingDirectory = getDefaultWorkingDirectoryPath();
|
||||||
|
|
||||||
|
List<String> environment = new ArrayList<>();
|
||||||
|
|
||||||
|
// This function may be called by a different package like a plugin, so we get version for Termux package via its context
|
||||||
|
Context termuxPackageContext = TermuxUtils.getTermuxPackageContext(currentPackageContext);
|
||||||
|
if (termuxPackageContext != null) {
|
||||||
|
String termuxVersionName = PackageUtils.getVersionNameForPackage(termuxPackageContext);
|
||||||
|
if (termuxVersionName != null)
|
||||||
|
environment.add("TERMUX_VERSION=" + termuxVersionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
environment.add("TERM=xterm-256color");
|
||||||
|
environment.add("COLORTERM=truecolor");
|
||||||
|
environment.add("HOME=" + TermuxConstants.TERMUX_HOME_DIR_PATH);
|
||||||
|
environment.add("PREFIX=" + TermuxConstants.TERMUX_PREFIX_DIR_PATH);
|
||||||
|
environment.add("BOOTCLASSPATH=" + System.getenv("BOOTCLASSPATH"));
|
||||||
|
environment.add("ANDROID_ROOT=" + System.getenv("ANDROID_ROOT"));
|
||||||
|
environment.add("ANDROID_DATA=" + System.getenv("ANDROID_DATA"));
|
||||||
|
// EXTERNAL_STORAGE is needed for /system/bin/am to work on at least
|
||||||
|
// Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3.
|
||||||
|
environment.add("EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE"));
|
||||||
|
|
||||||
|
// These variables are needed if running on Android 10 and higher.
|
||||||
|
addToEnvIfPresent(environment, "ANDROID_ART_ROOT");
|
||||||
|
addToEnvIfPresent(environment, "DEX2OATBOOTCLASSPATH");
|
||||||
|
addToEnvIfPresent(environment, "ANDROID_I18N_ROOT");
|
||||||
|
addToEnvIfPresent(environment, "ANDROID_RUNTIME_ROOT");
|
||||||
|
addToEnvIfPresent(environment, "ANDROID_TZDATA_ROOT");
|
||||||
|
|
||||||
|
if (isFailSafe) {
|
||||||
|
// Keep the default path so that system binaries can be used in the failsafe session.
|
||||||
|
environment.add("PATH= " + System.getenv("PATH"));
|
||||||
|
} else {
|
||||||
|
environment.add("LANG=en_US.UTF-8");
|
||||||
|
environment.add("PATH=" + TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH);
|
||||||
|
environment.add("PWD=" + workingDirectory);
|
||||||
|
environment.add("TMPDIR=" + TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
return environment.toArray(new String[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void addToEnvIfPresent(List<String> environment, String name) {
|
||||||
|
String value = System.getenv(name);
|
||||||
|
if (value != null) {
|
||||||
|
environment.add(name + "=" + value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String[] setupProcessArgs(@NonNull String fileToExecute, String[] arguments) {
|
||||||
|
// The file to execute may either be:
|
||||||
|
// - An elf file, in which we execute it directly.
|
||||||
|
// - A script file without shebang, which we execute with our standard shell $PREFIX/bin/sh instead of the
|
||||||
|
// system /system/bin/sh. The system shell may vary and may not work at all due to LD_LIBRARY_PATH.
|
||||||
|
// - A file with shebang, which we try to handle with e.g. /bin/foo -> $PREFIX/bin/foo.
|
||||||
|
String interpreter = null;
|
||||||
|
try {
|
||||||
|
File file = new File(fileToExecute);
|
||||||
|
try (FileInputStream in = new FileInputStream(file)) {
|
||||||
|
byte[] buffer = new byte[256];
|
||||||
|
int bytesRead = in.read(buffer);
|
||||||
|
if (bytesRead > 4) {
|
||||||
|
if (buffer[0] == 0x7F && buffer[1] == 'E' && buffer[2] == 'L' && buffer[3] == 'F') {
|
||||||
|
// Elf file, do nothing.
|
||||||
|
} else if (buffer[0] == '#' && buffer[1] == '!') {
|
||||||
|
// Try to parse shebang.
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
for (int i = 2; i < bytesRead; i++) {
|
||||||
|
char c = (char) buffer[i];
|
||||||
|
if (c == ' ' || c == '\n') {
|
||||||
|
if (builder.length() == 0) {
|
||||||
|
// Skip whitespace after shebang.
|
||||||
|
} else {
|
||||||
|
// End of shebang.
|
||||||
|
String executable = builder.toString();
|
||||||
|
if (executable.startsWith("/usr") || executable.startsWith("/bin")) {
|
||||||
|
String[] parts = executable.split("/");
|
||||||
|
String binary = parts[parts.length - 1];
|
||||||
|
interpreter = TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/" + binary;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
builder.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No shebang and no ELF, use standard shell.
|
||||||
|
interpreter = TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/sh";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
// Ignore.
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
|
if (interpreter != null) result.add(interpreter);
|
||||||
|
result.add(fileToExecute);
|
||||||
|
if (arguments != null) Collections.addAll(result, arguments);
|
||||||
|
return result.toArray(new String[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clearTermuxTMPDIR(boolean onlyIfExists) {
|
||||||
|
if(onlyIfExists && !FileUtils.directoryFileExists(TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH, false))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Error error;
|
||||||
|
error = FileUtils.clearDirectory("$TMPDIR", FileUtils.getCanonicalPath(TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH, null));
|
||||||
|
if (error != null) {
|
||||||
|
Logger.logErrorExtended(error.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -9,7 +9,8 @@ import androidx.annotation.NonNull;
|
|||||||
|
|
||||||
import com.termux.shared.R;
|
import com.termux.shared.R;
|
||||||
import com.termux.shared.models.ExecutionCommand;
|
import com.termux.shared.models.ExecutionCommand;
|
||||||
import com.termux.shared.termux.TermuxConstants;
|
import com.termux.shared.models.ResultData;
|
||||||
|
import com.termux.shared.models.errors.Errno;
|
||||||
import com.termux.shared.logger.Logger;
|
import com.termux.shared.logger.Logger;
|
||||||
import com.termux.shared.models.ExecutionCommand.ExecutionState;
|
import com.termux.shared.models.ExecutionCommand.ExecutionState;
|
||||||
|
|
||||||
@@ -29,9 +30,6 @@ public final class TermuxTask {
|
|||||||
private final ExecutionCommand mExecutionCommand;
|
private final ExecutionCommand mExecutionCommand;
|
||||||
private final TermuxTaskClient mTermuxTaskClient;
|
private final TermuxTaskClient mTermuxTaskClient;
|
||||||
|
|
||||||
private final StringBuilder mStdout = new StringBuilder();
|
|
||||||
private final StringBuilder mStderr = new StringBuilder();
|
|
||||||
|
|
||||||
private static final String LOG_TAG = "TermuxTask";
|
private static final String LOG_TAG = "TermuxTask";
|
||||||
|
|
||||||
private TermuxTask(@NonNull final Process process, @NonNull final ExecutionCommand executionCommand,
|
private TermuxTask(@NonNull final Process process, @NonNull final ExecutionCommand executionCommand,
|
||||||
@@ -55,6 +53,7 @@ public final class TermuxTask {
|
|||||||
* be called regardless of {@code isSynchronous} value but not if
|
* be called regardless of {@code isSynchronous} value but not if
|
||||||
* {@code null} is returned by this method. This can
|
* {@code null} is returned by this method. This can
|
||||||
* optionally be {@code null}.
|
* optionally be {@code null}.
|
||||||
|
* @param shellEnvironmentClient The {@link ShellEnvironmentClient} interface implementation.
|
||||||
* @param isSynchronous If set to {@code true}, then the command will be executed in the
|
* @param isSynchronous If set to {@code true}, then the command will be executed in the
|
||||||
* caller thread and results returned synchronously in the {@link ExecutionCommand}
|
* caller thread and results returned synchronously in the {@link ExecutionCommand}
|
||||||
* sub object of the {@link TermuxTask} returned.
|
* sub object of the {@link TermuxTask} returned.
|
||||||
@@ -63,15 +62,23 @@ public final class TermuxTask {
|
|||||||
* @return Returns the {@link TermuxTask}. This will be {@code null} if failed to start the execution command.
|
* @return Returns the {@link TermuxTask}. This will be {@code null} if failed to start the execution command.
|
||||||
*/
|
*/
|
||||||
public static TermuxTask execute(@NonNull final Context context, @NonNull ExecutionCommand executionCommand,
|
public static TermuxTask execute(@NonNull final Context context, @NonNull ExecutionCommand executionCommand,
|
||||||
final TermuxTaskClient termuxTaskClient, final boolean isSynchronous) {
|
final TermuxTaskClient termuxTaskClient,
|
||||||
if (executionCommand.workingDirectory == null || executionCommand.workingDirectory.isEmpty()) executionCommand.workingDirectory = TermuxConstants.TERMUX_HOME_DIR_PATH;
|
@NonNull final ShellEnvironmentClient shellEnvironmentClient,
|
||||||
|
final boolean isSynchronous) {
|
||||||
|
if (executionCommand.workingDirectory == null || executionCommand.workingDirectory.isEmpty())
|
||||||
|
executionCommand.workingDirectory = shellEnvironmentClient.getDefaultWorkingDirectoryPath();
|
||||||
|
if (executionCommand.workingDirectory.isEmpty())
|
||||||
|
executionCommand.workingDirectory = "/";
|
||||||
|
|
||||||
String[] env = ShellUtils.buildEnvironment(context, false, executionCommand.workingDirectory);
|
String[] env = shellEnvironmentClient.buildEnvironment(context, false, executionCommand.workingDirectory);
|
||||||
|
|
||||||
final String[] commandArray = ShellUtils.setupProcessArgs(executionCommand.executable, executionCommand.arguments);
|
final String[] commandArray = shellEnvironmentClient.setupProcessArgs(executionCommand.executable, executionCommand.arguments);
|
||||||
|
|
||||||
if (!executionCommand.setState(ExecutionState.EXECUTING))
|
if (!executionCommand.setState(ExecutionState.EXECUTING)) {
|
||||||
|
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_failed_to_execute_termux_task_command, executionCommand.getCommandIdAndLabelLogString()));
|
||||||
|
TermuxTask.processTermuxTaskResult(null, executionCommand);
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
Logger.logDebug(LOG_TAG, executionCommand.toString());
|
Logger.logDebug(LOG_TAG, executionCommand.toString());
|
||||||
|
|
||||||
@@ -85,7 +92,7 @@ public final class TermuxTask {
|
|||||||
try {
|
try {
|
||||||
process = Runtime.getRuntime().exec(commandArray, env, new File(executionCommand.workingDirectory));
|
process = Runtime.getRuntime().exec(commandArray, env, new File(executionCommand.workingDirectory));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, context.getString(R.string.error_failed_to_execute_termux_task_command, executionCommand.getCommandIdAndLabelLogString()), e);
|
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_failed_to_execute_termux_task_command, executionCommand.getCommandIdAndLabelLogString()), e);
|
||||||
TermuxTask.processTermuxTaskResult(null, executionCommand);
|
TermuxTask.processTermuxTaskResult(null, executionCommand);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -117,8 +124,8 @@ public final class TermuxTask {
|
|||||||
/**
|
/**
|
||||||
* Sets up stdout and stderr readers for the {@link #mProcess} and waits for the process to end.
|
* Sets up stdout and stderr readers for the {@link #mProcess} and waits for the process to end.
|
||||||
*
|
*
|
||||||
* If the processes finishes, then sets {@link ExecutionCommand#stdout}, {@link ExecutionCommand#stderr}
|
* If the processes finishes, then sets {@link ResultData#stdout}, {@link ResultData#stderr}
|
||||||
* and {@link ExecutionCommand#exitCode} for the {@link #mExecutionCommand} of the {@code termuxTask}
|
* and {@link ResultData#exitCode} for the {@link #mExecutionCommand} of the {@code termuxTask}
|
||||||
* and then calls {@link #processTermuxTaskResult(TermuxTask, ExecutionCommand) to process the result}.
|
* and then calls {@link #processTermuxTaskResult(TermuxTask, ExecutionCommand) to process the result}.
|
||||||
*
|
*
|
||||||
* @param context The {@link Context} for operations.
|
* @param context The {@link Context} for operations.
|
||||||
@@ -128,15 +135,12 @@ public final class TermuxTask {
|
|||||||
|
|
||||||
Logger.logDebug(LOG_TAG, "Running \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask with pid " + pid);
|
Logger.logDebug(LOG_TAG, "Running \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask with pid " + pid);
|
||||||
|
|
||||||
mExecutionCommand.stdout = null;
|
mExecutionCommand.resultData.exitCode = null;
|
||||||
mExecutionCommand.stderr = null;
|
|
||||||
mExecutionCommand.exitCode = null;
|
|
||||||
|
|
||||||
|
|
||||||
// setup stdin, and stdout and stderr gobblers
|
// setup stdin, and stdout and stderr gobblers
|
||||||
DataOutputStream STDIN = new DataOutputStream(mProcess.getOutputStream());
|
DataOutputStream STDIN = new DataOutputStream(mProcess.getOutputStream());
|
||||||
StreamGobbler STDOUT = new StreamGobbler(pid + "-stdout", mProcess.getInputStream(), mStdout);
|
StreamGobbler STDOUT = new StreamGobbler(pid + "-stdout", mProcess.getInputStream(), mExecutionCommand.resultData.stdout);
|
||||||
StreamGobbler STDERR = new StreamGobbler(pid + "-stderr", mProcess.getErrorStream(), mStderr);
|
StreamGobbler STDERR = new StreamGobbler(pid + "-stderr", mProcess.getErrorStream(), mExecutionCommand.resultData.stderr);
|
||||||
|
|
||||||
// start gobbling
|
// start gobbling
|
||||||
STDOUT.start();
|
STDOUT.start();
|
||||||
@@ -150,7 +154,7 @@ public final class TermuxTask {
|
|||||||
//STDIN.write("exit\n".getBytes(StandardCharsets.UTF_8));
|
//STDIN.write("exit\n".getBytes(StandardCharsets.UTF_8));
|
||||||
//STDIN.flush();
|
//STDIN.flush();
|
||||||
} catch(IOException e) {
|
} catch(IOException e) {
|
||||||
if (e.getMessage().contains("EPIPE") || e.getMessage().contains("Stream closed")) {
|
if (e.getMessage() != null && (e.getMessage().contains("EPIPE") || e.getMessage().contains("Stream closed"))) {
|
||||||
// Method most horrid to catch broken pipe, in which case we
|
// Method most horrid to catch broken pipe, in which case we
|
||||||
// do nothing. The command is not a shell, the shell closed
|
// do nothing. The command is not a shell, the shell closed
|
||||||
// STDIN, the script already contained the exit command, etc.
|
// STDIN, the script already contained the exit command, etc.
|
||||||
@@ -158,10 +162,8 @@ public final class TermuxTask {
|
|||||||
} else {
|
} else {
|
||||||
// other issues we don't know how to handle, leads to
|
// other issues we don't know how to handle, leads to
|
||||||
// returning null
|
// returning null
|
||||||
mExecutionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, context.getString(R.string.error_exception_received_while_executing_termux_task_command, mExecutionCommand.getCommandIdAndLabelLogString(), e.getMessage()), e);
|
mExecutionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_exception_received_while_executing_termux_task_command, mExecutionCommand.getCommandIdAndLabelLogString(), e.getMessage()), e);
|
||||||
mExecutionCommand.stdout = mStdout.toString();
|
mExecutionCommand.resultData.exitCode = 1;
|
||||||
mExecutionCommand.stderr = mStderr.toString();
|
|
||||||
mExecutionCommand.exitCode = -1;
|
|
||||||
TermuxTask.processTermuxTaskResult(this, null);
|
TermuxTask.processTermuxTaskResult(this, null);
|
||||||
kill();
|
kill();
|
||||||
return;
|
return;
|
||||||
@@ -198,9 +200,7 @@ public final class TermuxTask {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
mExecutionCommand.stdout = mStdout.toString();
|
mExecutionCommand.resultData.exitCode = exitCode;
|
||||||
mExecutionCommand.stderr = mStderr.toString();
|
|
||||||
mExecutionCommand.exitCode = exitCode;
|
|
||||||
|
|
||||||
if (!mExecutionCommand.setState(ExecutionState.EXECUTED))
|
if (!mExecutionCommand.setState(ExecutionState.EXECUTED))
|
||||||
return;
|
return;
|
||||||
@@ -225,13 +225,9 @@ public final class TermuxTask {
|
|||||||
|
|
||||||
Logger.logDebug(LOG_TAG, "Send SIGKILL to \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask");
|
Logger.logDebug(LOG_TAG, "Send SIGKILL to \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask");
|
||||||
|
|
||||||
if (mExecutionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, context.getString(R.string.error_sending_sigkill_to_process), null)) {
|
if (mExecutionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_sending_sigkill_to_process))) {
|
||||||
if (processResult) {
|
if (processResult) {
|
||||||
// Get whatever output has been set till now in case its needed
|
mExecutionCommand.resultData.exitCode = 137; // SIGKILL
|
||||||
mExecutionCommand.stdout = mStdout.toString();
|
|
||||||
mExecutionCommand.stderr = mStderr.toString();
|
|
||||||
mExecutionCommand.exitCode = 137; // SIGKILL
|
|
||||||
|
|
||||||
TermuxTask.processTermuxTaskResult(this, null);
|
TermuxTask.processTermuxTaskResult(this, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -263,10 +259,10 @@ public final class TermuxTask {
|
|||||||
* then the {@link TermuxTaskClient#onTermuxTaskExited(TermuxTask)} callback will be called.
|
* then the {@link TermuxTaskClient#onTermuxTaskExited(TermuxTask)} callback will be called.
|
||||||
*
|
*
|
||||||
* @param termuxTask The {@link TermuxTask}, which should be set if
|
* @param termuxTask The {@link TermuxTask}, which should be set if
|
||||||
* {@link #execute(Context, ExecutionCommand, TermuxTaskClient, boolean)}
|
* {@link #execute(Context, ExecutionCommand, TermuxTaskClient, ShellEnvironmentClient, boolean)}
|
||||||
* successfully started the process.
|
* successfully started the process.
|
||||||
* @param executionCommand The {@link ExecutionCommand}, which should be set if
|
* @param executionCommand The {@link ExecutionCommand}, which should be set if
|
||||||
* {@link #execute(Context, ExecutionCommand, TermuxTaskClient, boolean)}
|
* {@link #execute(Context, ExecutionCommand, TermuxTaskClient, ShellEnvironmentClient, boolean)}
|
||||||
* failed to start the process.
|
* failed to start the process.
|
||||||
*/
|
*/
|
||||||
private static void processTermuxTaskResult(final TermuxTask termuxTask, ExecutionCommand executionCommand) {
|
private static void processTermuxTaskResult(final TermuxTask termuxTask, ExecutionCommand executionCommand) {
|
||||||
|
|||||||
@@ -39,6 +39,13 @@ public class TermuxTerminalSessionClientBase implements TerminalSessionClient {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer getTerminalCursorStyle() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void logError(String tag, String message) {
|
public void logError(String tag, String message) {
|
||||||
Logger.logError(tag, message);
|
Logger.logError(tag, message);
|
||||||
|
|||||||
@@ -67,6 +67,11 @@ public class TermuxTerminalViewClientBase implements TerminalViewClient {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEmulatorSet() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void logError(String tag, String message) {
|
public void logError(String tag, String message) {
|
||||||
Logger.logError(tag, message);
|
Logger.logError(tag, message);
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
package com.termux.shared.termux;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.google.common.base.Joiner;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.markdown.MarkdownUtils;
|
||||||
|
import com.termux.shared.packages.PackageUtils;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class AndroidUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a markdown {@link String} for the app info for the package associated with the {@code context}.
|
||||||
|
*
|
||||||
|
* @param context The context for operations for the package.
|
||||||
|
* @return Returns the markdown {@link String}.
|
||||||
|
*/
|
||||||
|
public static String getAppInfoMarkdownString(@NonNull final Context context) {
|
||||||
|
StringBuilder markdownString = new StringBuilder();
|
||||||
|
|
||||||
|
AndroidUtils.appendPropertyToMarkdown(markdownString,"APP_NAME", PackageUtils.getAppNameForPackage(context));
|
||||||
|
AndroidUtils.appendPropertyToMarkdown(markdownString,"PACKAGE_NAME", PackageUtils.getPackageNameForPackage(context));
|
||||||
|
AndroidUtils.appendPropertyToMarkdown(markdownString,"VERSION_NAME", PackageUtils.getVersionNameForPackage(context));
|
||||||
|
AndroidUtils.appendPropertyToMarkdown(markdownString,"VERSION_CODE", PackageUtils.getVersionCodeForPackage(context));
|
||||||
|
AndroidUtils.appendPropertyToMarkdown(markdownString,"TARGET_SDK", PackageUtils.getTargetSDKForPackage(context));
|
||||||
|
AndroidUtils.appendPropertyToMarkdown(markdownString,"IS_DEBUG_BUILD", PackageUtils.isAppForPackageADebugBuild(context));
|
||||||
|
|
||||||
|
return markdownString.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a markdown {@link String} for the device info.
|
||||||
|
*
|
||||||
|
* @param context The context for operations.
|
||||||
|
* @return Returns the markdown {@link String}.
|
||||||
|
*/
|
||||||
|
public static String getDeviceInfoMarkdownString(@NonNull final Context context) {
|
||||||
|
// Some properties cannot be read with {@link System#getProperty(String)} but can be read
|
||||||
|
// directly by running getprop command
|
||||||
|
Properties systemProperties = getSystemProperties();
|
||||||
|
|
||||||
|
StringBuilder markdownString = new StringBuilder();
|
||||||
|
|
||||||
|
markdownString.append("## Device Info");
|
||||||
|
|
||||||
|
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);
|
||||||
|
appendPropertyToMarkdownIfSet(markdownString, "SECURITY_PATCH", systemProperties.getProperty("ro.build.version.security_patch"));
|
||||||
|
appendPropertyToMarkdownIfSet(markdownString, "IS_DEBUGGABLE", systemProperties.getProperty("ro.debuggable"));
|
||||||
|
appendPropertyToMarkdownIfSet(markdownString, "IS_EMULATOR", systemProperties.getProperty("ro.boot.qemu"));
|
||||||
|
appendPropertyToMarkdownIfSet(markdownString, "IS_TREBLE_ENABLED", systemProperties.getProperty("ro.treble.enabled"));
|
||||||
|
appendPropertyToMarkdown(markdownString, "TYPE", Build.TYPE);
|
||||||
|
appendPropertyToMarkdown(markdownString, "TAGS", Build.TAGS);
|
||||||
|
|
||||||
|
markdownString.append("\n\n### Hardware\n");
|
||||||
|
appendPropertyToMarkdown(markdownString, "MANUFACTURER", Build.MANUFACTURER);
|
||||||
|
appendPropertyToMarkdown(markdownString, "BRAND", Build.BRAND);
|
||||||
|
appendPropertyToMarkdown(markdownString, "MODEL", Build.MODEL);
|
||||||
|
appendPropertyToMarkdown(markdownString, "PRODUCT", Build.PRODUCT);
|
||||||
|
appendPropertyToMarkdown(markdownString, "BOARD", Build.BOARD);
|
||||||
|
appendPropertyToMarkdown(markdownString, "HARDWARE", Build.HARDWARE);
|
||||||
|
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();
|
||||||
|
|
||||||
|
// getprop commands returns values in the format `[key]: [value]`
|
||||||
|
// Regex matches string starting with a literal `[`,
|
||||||
|
// followed by one or more characters that do not match a closing square bracket as the key,
|
||||||
|
// followed by a literal `]: [`,
|
||||||
|
// followed by one or more characters as the value,
|
||||||
|
// followed by string ending with literal `]`
|
||||||
|
// multiline values will be ignored
|
||||||
|
Pattern propertiesPattern = Pattern.compile("^\\[([^]]+)]: \\[(.+)]$");
|
||||||
|
|
||||||
|
try {
|
||||||
|
Process process = new ProcessBuilder()
|
||||||
|
.command("/system/bin/getprop")
|
||||||
|
.redirectErrorStream(true)
|
||||||
|
.start();
|
||||||
|
|
||||||
|
InputStream inputStream = process.getInputStream();
|
||||||
|
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
|
||||||
|
String line, key, value;
|
||||||
|
|
||||||
|
while ((line = bufferedReader.readLine()) != null) {
|
||||||
|
Matcher matcher = propertiesPattern.matcher(line);
|
||||||
|
if (matcher.matches()) {
|
||||||
|
key = matcher.group(1);
|
||||||
|
value = matcher.group(2);
|
||||||
|
if (key != null && value != null && !key.isEmpty() && !value.isEmpty())
|
||||||
|
systemProperties.put(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bufferedReader.close();
|
||||||
|
process.destroy();
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
Logger.logStackTraceWithMessage("Failed to get run \"/system/bin/getprop\" to get system properties.", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
//for (String key : systemProperties.stringPropertyNames()) {
|
||||||
|
// Logger.logVerbose(key + ": " + systemProperties.get(key));
|
||||||
|
//}
|
||||||
|
|
||||||
|
return systemProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getSystemPropertyWithAndroidAPI(@NonNull String property) {
|
||||||
|
try {
|
||||||
|
return System.getProperty(property);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.logVerbose("Failed to get system property \"" + property + "\":" + e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void appendPropertyToMarkdownIfSet(StringBuilder markdownString, String label, Object value) {
|
||||||
|
if (value == null) return;
|
||||||
|
if (value instanceof String && (((String) value).isEmpty()) || "REL".equals(value)) return;
|
||||||
|
markdownString.append("\n").append(getPropertyMarkdown(label, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void appendPropertyToMarkdown(StringBuilder markdownString, String label, Object value) {
|
||||||
|
markdownString.append("\n").append(getPropertyMarkdown(label, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getPropertyMarkdown(String label, Object value) {
|
||||||
|
return MarkdownUtils.getSingleLineMarkdownStringEntry(label, value, "-");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static String getCurrentTimeStamp() {
|
||||||
|
@SuppressLint("SimpleDateFormat")
|
||||||
|
final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
|
||||||
|
df.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||||
|
return df.format(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getCurrentMilliSecondLocalTimeStamp() {
|
||||||
|
@SuppressLint("SimpleDateFormat")
|
||||||
|
final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss.SSS");
|
||||||
|
df.setTimeZone(TimeZone.getDefault());
|
||||||
|
return df.format(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,12 +2,17 @@ package com.termux.shared.termux;
|
|||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
|
||||||
|
import com.termux.shared.models.ResultConfig;
|
||||||
|
import com.termux.shared.models.errors.Errno;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Formatter;
|
||||||
|
import java.util.IllegalFormatException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Version: v0.21.0
|
* Version: v0.24.0
|
||||||
*
|
*
|
||||||
* Changelog
|
* Changelog
|
||||||
*
|
*
|
||||||
@@ -150,6 +155,23 @@ import java.util.List;
|
|||||||
* - 0.22.0 (2021-05-13)
|
* - 0.22.0 (2021-05-13)
|
||||||
* - Added `TERMUX_DONATE_URL`.
|
* - Added `TERMUX_DONATE_URL`.
|
||||||
*
|
*
|
||||||
|
* - 0.23.0 (2021-06-12)
|
||||||
|
* - Rename `INTERNAL_PRIVATE_APP_DATA_DIR_PATH` to `TERMUX_INTERNAL_PRIVATE_APP_DATA_DIR_PATH`.
|
||||||
|
*
|
||||||
|
* - 0.24.0 (2021-06-27)
|
||||||
|
* - Add `COMMA_NORMAL`, `COMMA_ALTERNATIVE`.
|
||||||
|
* - Added following to `TERMUX_APP.TERMUX_SERVICE`:
|
||||||
|
* `EXTRA_RESULT_DIRECTORY`, `EXTRA_RESULT_SINGLE_FILE`, `EXTRA_RESULT_FILE_BASENAME`,
|
||||||
|
* `EXTRA_RESULT_FILE_OUTPUT_FORMAT`, `EXTRA_RESULT_FILE_ERROR_FORMAT`, `EXTRA_RESULT_FILES_SUFFIX`.
|
||||||
|
* - Added following to `TERMUX_APP.RUN_COMMAND_SERVICE`:
|
||||||
|
* `EXTRA_RESULT_DIRECTORY`, `EXTRA_RESULT_SINGLE_FILE`, `EXTRA_RESULT_FILE_BASENAME`,
|
||||||
|
* `EXTRA_RESULT_FILE_OUTPUT_FORMAT`, `EXTRA_RESULT_FILE_ERROR_FORMAT`, `EXTRA_RESULT_FILES_SUFFIX`,
|
||||||
|
* `EXTRA_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS`, `EXTRA_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS`.
|
||||||
|
* - Added following to `RESULT_SENDER`:
|
||||||
|
* `FORMAT_SUCCESS_STDOUT`, `FORMAT_SUCCESS_STDOUT__EXIT_CODE`, `FORMAT_SUCCESS_STDOUT__STDERR__EXIT_CODE`
|
||||||
|
* `FORMAT_FAILED_ERR__ERRMSG__STDOUT__STDERR__EXIT_CODE`,
|
||||||
|
* `RESULT_FILE_ERR_PREFIX`, `RESULT_FILE_ERRMSG_PREFIX` `RESULT_FILE_STDOUT_PREFIX`,
|
||||||
|
* `RESULT_FILE_STDERR_PREFIX`, `RESULT_FILE_EXIT_CODE_PREFIX`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -464,14 +486,14 @@ public final class TermuxConstants {
|
|||||||
|
|
||||||
/** Termux app internal private app data directory path */
|
/** Termux app internal private app data directory path */
|
||||||
@SuppressLint("SdCardPath")
|
@SuppressLint("SdCardPath")
|
||||||
public static final String INTERNAL_PRIVATE_APP_DATA_DIR_PATH = "/data/data/" + TERMUX_PACKAGE_NAME; // Default: "/data/data/com.termux"
|
public static final String TERMUX_INTERNAL_PRIVATE_APP_DATA_DIR_PATH = "/data/data/" + TERMUX_PACKAGE_NAME; // Default: "/data/data/com.termux"
|
||||||
/** Termux app internal private app data directory */
|
/** Termux app internal private app data directory */
|
||||||
public static final File INTERNAL_PRIVATE_APP_DATA_DIR = new File(INTERNAL_PRIVATE_APP_DATA_DIR_PATH);
|
public static final File TERMUX_INTERNAL_PRIVATE_APP_DATA_DIR = new File(TERMUX_INTERNAL_PRIVATE_APP_DATA_DIR_PATH);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/** Termux app Files directory path */
|
/** Termux app Files directory path */
|
||||||
public static final String TERMUX_FILES_DIR_PATH = INTERNAL_PRIVATE_APP_DATA_DIR_PATH + "/files"; // Default: "/data/data/com.termux/files"
|
public static final String TERMUX_FILES_DIR_PATH = TERMUX_INTERNAL_PRIVATE_APP_DATA_DIR_PATH + "/files"; // Default: "/data/data/com.termux/files"
|
||||||
/** Termux app Files directory */
|
/** Termux app Files directory */
|
||||||
public static final File TERMUX_FILES_DIR = new File(TERMUX_FILES_DIR_PATH);
|
public static final File TERMUX_FILES_DIR = new File(TERMUX_FILES_DIR_PATH);
|
||||||
|
|
||||||
@@ -634,19 +656,22 @@ public final class TermuxConstants {
|
|||||||
public static final File TERMUX_BOOT_SCRIPTS_DIR = new File(TERMUX_BOOT_SCRIPTS_DIR_PATH);
|
public static final File TERMUX_BOOT_SCRIPTS_DIR = new File(TERMUX_BOOT_SCRIPTS_DIR_PATH);
|
||||||
|
|
||||||
|
|
||||||
/** Termux app directory path to store foreground scripts that can be run by the termux launcher widget provided by Termux:Widget */
|
/** Termux app directory path to store foreground scripts that can be run by the termux launcher
|
||||||
|
* widget provided by Termux:Widget */
|
||||||
public static final String TERMUX_SHORTCUT_SCRIPTS_DIR_PATH = TERMUX_DATA_HOME_DIR_PATH + "/shortcuts"; // Default: "/data/data/com.termux/files/home/.termux/shortcuts"
|
public static final String TERMUX_SHORTCUT_SCRIPTS_DIR_PATH = TERMUX_DATA_HOME_DIR_PATH + "/shortcuts"; // Default: "/data/data/com.termux/files/home/.termux/shortcuts"
|
||||||
/** Termux app directory to store foreground scripts that can be run by the termux launcher widget provided by Termux:Widget */
|
/** Termux app directory to store foreground scripts that can be run by the termux launcher widget provided by Termux:Widget */
|
||||||
public static final File TERMUX_SHORTCUT_SCRIPTS_DIR = new File(TERMUX_SHORTCUT_SCRIPTS_DIR_PATH);
|
public static final File TERMUX_SHORTCUT_SCRIPTS_DIR = new File(TERMUX_SHORTCUT_SCRIPTS_DIR_PATH);
|
||||||
|
|
||||||
|
|
||||||
/** Termux app directory path to store background scripts that can be run by the termux launcher widget provided by Termux:Widget */
|
/** Termux app directory path to store background scripts that can be run by the termux launcher
|
||||||
|
* widget provided by Termux:Widget */
|
||||||
public static final String TERMUX_SHORTCUT_TASKS_SCRIPTS_DIR_PATH = TERMUX_DATA_HOME_DIR_PATH + "/shortcuts/tasks"; // Default: "/data/data/com.termux/files/home/.termux/shortcuts/tasks"
|
public static final String TERMUX_SHORTCUT_TASKS_SCRIPTS_DIR_PATH = TERMUX_DATA_HOME_DIR_PATH + "/shortcuts/tasks"; // Default: "/data/data/com.termux/files/home/.termux/shortcuts/tasks"
|
||||||
/** Termux app directory to store background scripts that can be run by the termux launcher widget provided by Termux:Widget */
|
/** Termux app directory to store background scripts that can be run by the termux launcher widget provided by Termux:Widget */
|
||||||
public static final File TERMUX_SHORTCUT_TASKS_SCRIPTS_DIR = new File(TERMUX_SHORTCUT_TASKS_SCRIPTS_DIR_PATH);
|
public static final File TERMUX_SHORTCUT_TASKS_SCRIPTS_DIR = new File(TERMUX_SHORTCUT_TASKS_SCRIPTS_DIR_PATH);
|
||||||
|
|
||||||
|
|
||||||
/** Termux app directory path to store scripts to be run by 3rd party twofortyfouram locale plugin host apps like Tasker app via the Termux:Tasker plugin client */
|
/** Termux app directory path to store scripts to be run by 3rd party twofortyfouram locale plugin
|
||||||
|
* host apps like Tasker app via the Termux:Tasker plugin client */
|
||||||
public static final String TERMUX_TASKER_SCRIPTS_DIR_PATH = TERMUX_DATA_HOME_DIR_PATH + "/tasker"; // Default: "/data/data/com.termux/files/home/.termux/tasker"
|
public static final String TERMUX_TASKER_SCRIPTS_DIR_PATH = TERMUX_DATA_HOME_DIR_PATH + "/tasker"; // Default: "/data/data/com.termux/files/home/.termux/tasker"
|
||||||
/** Termux app directory to store scripts to be run by 3rd party twofortyfouram locale plugin host apps like Tasker app via the Termux:Tasker plugin client */
|
/** Termux app directory to store scripts to be run by 3rd party twofortyfouram locale plugin host apps like Tasker app via the Termux:Tasker plugin client */
|
||||||
public static final File TERMUX_TASKER_SCRIPTS_DIR = new File(TERMUX_TASKER_SCRIPTS_DIR_PATH);
|
public static final File TERMUX_TASKER_SCRIPTS_DIR = new File(TERMUX_TASKER_SCRIPTS_DIR_PATH);
|
||||||
@@ -691,10 +716,12 @@ public final class TermuxConstants {
|
|||||||
* Termux app and plugins miscellaneous variables.
|
* Termux app and plugins miscellaneous variables.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** Android OS permission declared by Termux app in AndroidManifest.xml which can be requested by 3rd party apps to run various commands in Termux app context */
|
/** Android OS permission declared by Termux app in AndroidManifest.xml which can be requested by
|
||||||
|
* 3rd party apps to run various commands in Termux app context */
|
||||||
public static final String PERMISSION_RUN_COMMAND = TERMUX_PACKAGE_NAME + ".permission.RUN_COMMAND"; // Default: "com.termux.permission.RUN_COMMAND"
|
public static final String PERMISSION_RUN_COMMAND = TERMUX_PACKAGE_NAME + ".permission.RUN_COMMAND"; // Default: "com.termux.permission.RUN_COMMAND"
|
||||||
|
|
||||||
/** Termux property defined in termux.properties file as a secondary check to PERMISSION_RUN_COMMAND to allow 3rd party apps to run various commands in Termux app context */
|
/** Termux property defined in termux.properties file as a secondary check to PERMISSION_RUN_COMMAND
|
||||||
|
* to allow 3rd party apps to run various commands in Termux app context */
|
||||||
public static final String PROP_ALLOW_EXTERNAL_APPS = "allow-external-apps"; // Default: "allow-external-apps"
|
public static final String PROP_ALLOW_EXTERNAL_APPS = "allow-external-apps"; // Default: "allow-external-apps"
|
||||||
/** Default value for {@link #PROP_ALLOW_EXTERNAL_APPS} */
|
/** Default value for {@link #PROP_ALLOW_EXTERNAL_APPS} */
|
||||||
public static final String PROP_DEFAULT_VALUE_ALLOW_EXTERNAL_APPS = "false"; // Default: "false"
|
public static final String PROP_DEFAULT_VALUE_ALLOW_EXTERNAL_APPS = "false"; // Default: "false"
|
||||||
@@ -705,6 +732,14 @@ public final class TermuxConstants {
|
|||||||
/** The Uri authority for Termux app file shares */
|
/** The Uri authority for Termux app file shares */
|
||||||
public static final String TERMUX_FILE_SHARE_URI_AUTHORITY = TERMUX_PACKAGE_NAME + ".files"; // Default: "com.termux.files"
|
public static final String TERMUX_FILE_SHARE_URI_AUTHORITY = TERMUX_PACKAGE_NAME + ".files"; // Default: "com.termux.files"
|
||||||
|
|
||||||
|
/** The normal comma character (U+002C, ,, ,, comma) */
|
||||||
|
public static final String COMMA_NORMAL = ","; // Default: ","
|
||||||
|
|
||||||
|
/** The alternate comma character (U+201A, ‚, ‚, single low-9 quotation mark) that
|
||||||
|
* may be used instead of {@link #COMMA_NORMAL} */
|
||||||
|
public static final String COMMA_ALTERNATIVE = "‚"; // Default: "‚"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -770,6 +805,7 @@ public final class TermuxConstants {
|
|||||||
|
|
||||||
/** Intent action to execute command with TERMUX_SERVICE */
|
/** Intent action to execute command with TERMUX_SERVICE */
|
||||||
public static final String ACTION_SERVICE_EXECUTE = TERMUX_PACKAGE_NAME + ".service_execute"; // Default: "com.termux.service_execute"
|
public static final String ACTION_SERVICE_EXECUTE = TERMUX_PACKAGE_NAME + ".service_execute"; // Default: "com.termux.service_execute"
|
||||||
|
|
||||||
/** Uri scheme for paths sent via intent to TERMUX_SERVICE */
|
/** Uri scheme for paths sent via intent to TERMUX_SERVICE */
|
||||||
public static final String URI_SCHEME_SERVICE_EXECUTE = TERMUX_PACKAGE_NAME + ".file"; // Default: "com.termux.file"
|
public static final String URI_SCHEME_SERVICE_EXECUTE = TERMUX_PACKAGE_NAME + ".file"; // Default: "com.termux.file"
|
||||||
/** Intent {@code String[]} extra for arguments to the executable of the command for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */
|
/** Intent {@code String[]} extra for arguments to the executable of the command for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */
|
||||||
@@ -790,8 +826,30 @@ public final class TermuxConstants {
|
|||||||
public static final String EXTRA_COMMAND_HELP = TERMUX_PACKAGE_NAME + ".execute.command_help"; // Default: "com.termux.execute.command_help"
|
public static final String EXTRA_COMMAND_HELP = TERMUX_PACKAGE_NAME + ".execute.command_help"; // Default: "com.termux.execute.command_help"
|
||||||
/** Intent markdown {@code String} extra for help of the plugin API for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent (Internal Use Only) */
|
/** Intent markdown {@code String} extra for help of the plugin API for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent (Internal Use Only) */
|
||||||
public static final String EXTRA_PLUGIN_API_HELP = TERMUX_PACKAGE_NAME + ".execute.plugin_api_help"; // Default: "com.termux.execute.plugin_help"
|
public static final String EXTRA_PLUGIN_API_HELP = TERMUX_PACKAGE_NAME + ".execute.plugin_api_help"; // Default: "com.termux.execute.plugin_help"
|
||||||
/** Intent {@code Parcelable} extra containing pending intent for the execute command caller */
|
/** Intent {@code Parcelable} extra for the pending intent that should be sent with the
|
||||||
|
* result of the execution command to the execute command caller for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */
|
||||||
public static final String EXTRA_PENDING_INTENT = "pendingIntent"; // Default: "pendingIntent"
|
public static final String EXTRA_PENDING_INTENT = "pendingIntent"; // Default: "pendingIntent"
|
||||||
|
/** Intent {@code String} extra for the directory path in which to write the result of the
|
||||||
|
* execution command for the execute command caller for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */
|
||||||
|
public static final String EXTRA_RESULT_DIRECTORY = TERMUX_PACKAGE_NAME + ".execute.result_directory"; // Default: "com.termux.execute.result_directory"
|
||||||
|
/** Intent {@code boolean} extra for whether the result should be written to a single file
|
||||||
|
* or multiple files (err, errmsg, stdout, stderr, exit_code) in
|
||||||
|
* {@link #EXTRA_RESULT_DIRECTORY} for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */
|
||||||
|
public static final String EXTRA_RESULT_SINGLE_FILE = TERMUX_PACKAGE_NAME + ".execute.result_single_file"; // Default: "com.termux.execute.result_single_file"
|
||||||
|
/** Intent {@code String} extra for the basename of the result file that should be created
|
||||||
|
* in {@link #EXTRA_RESULT_DIRECTORY} if {@link #EXTRA_RESULT_SINGLE_FILE} is {@code true}
|
||||||
|
* for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */
|
||||||
|
public static final String EXTRA_RESULT_FILE_BASENAME = TERMUX_PACKAGE_NAME + ".execute.result_file_basename"; // Default: "com.termux.execute.result_file_basename"
|
||||||
|
/** Intent {@code String} extra for the output {@link Formatter} format of the
|
||||||
|
* {@link #EXTRA_RESULT_FILE_BASENAME} result file for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */
|
||||||
|
public static final String EXTRA_RESULT_FILE_OUTPUT_FORMAT = TERMUX_PACKAGE_NAME + ".execute.result_file_output_format"; // Default: "com.termux.execute.result_file_output_format"
|
||||||
|
/** Intent {@code String} extra for the error {@link Formatter} format of the
|
||||||
|
* {@link #EXTRA_RESULT_FILE_BASENAME} result file for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */
|
||||||
|
public static final String EXTRA_RESULT_FILE_ERROR_FORMAT = TERMUX_PACKAGE_NAME + ".execute.result_file_error_format"; // Default: "com.termux.execute.result_file_error_format"
|
||||||
|
/** Intent {@code String} extra for the optional suffix of the result files that should
|
||||||
|
* be created in {@link #EXTRA_RESULT_DIRECTORY} if {@link #EXTRA_RESULT_SINGLE_FILE} is
|
||||||
|
* {@code false} for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */
|
||||||
|
public static final String EXTRA_RESULT_FILES_SUFFIX = TERMUX_PACKAGE_NAME + ".execute.result_files_suffix"; // Default: "com.termux.execute.result_files_suffix"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -862,12 +920,19 @@ public final class TermuxConstants {
|
|||||||
/** Termux RUN_COMMAND Intent help url */
|
/** Termux RUN_COMMAND Intent help url */
|
||||||
public static final String RUN_COMMAND_API_HELP_URL = TERMUX_GITHUB_WIKI_REPO_URL + "/RUN_COMMAND-Intent"; // Default: "https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent"
|
public static final String RUN_COMMAND_API_HELP_URL = TERMUX_GITHUB_WIKI_REPO_URL + "/RUN_COMMAND-Intent"; // Default: "https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent"
|
||||||
|
|
||||||
|
|
||||||
/** Intent action to execute command with RUN_COMMAND_SERVICE */
|
/** Intent action to execute command with RUN_COMMAND_SERVICE */
|
||||||
public static final String ACTION_RUN_COMMAND = TERMUX_PACKAGE_NAME + ".RUN_COMMAND"; // Default: "com.termux.RUN_COMMAND"
|
public static final String ACTION_RUN_COMMAND = TERMUX_PACKAGE_NAME + ".RUN_COMMAND"; // Default: "com.termux.RUN_COMMAND"
|
||||||
|
|
||||||
/** Intent {@code String} extra for absolute path of command for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */
|
/** Intent {@code String} extra for absolute path of command for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */
|
||||||
public static final String EXTRA_COMMAND_PATH = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_PATH"; // Default: "com.termux.RUN_COMMAND_PATH"
|
public static final String EXTRA_COMMAND_PATH = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_PATH"; // Default: "com.termux.RUN_COMMAND_PATH"
|
||||||
/** Intent {@code String[]} extra for arguments to the executable of the command for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */
|
/** Intent {@code String[]} extra for arguments to the executable of the command for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */
|
||||||
public static final String EXTRA_ARGUMENTS = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_ARGUMENTS"; // Default: "com.termux.RUN_COMMAND_ARGUMENTS"
|
public static final String EXTRA_ARGUMENTS = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_ARGUMENTS"; // Default: "com.termux.RUN_COMMAND_ARGUMENTS"
|
||||||
|
/** Intent {@code boolean} extra for whether to replace comma alternative characters in arguments with comma characters for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */
|
||||||
|
public static final String EXTRA_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS"; // Default: "com.termux.RUN_COMMAND_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS"
|
||||||
|
/** Intent {@code String} extra for the comma alternative characters in arguments that should be replaced instead of the default {@link #COMMA_ALTERNATIVE} for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */
|
||||||
|
public static final String EXTRA_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS"; // Default: "com.termux.RUN_COMMAND_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS"
|
||||||
|
|
||||||
/** Intent {@code String} extra for stdin of the command for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */
|
/** Intent {@code String} extra for stdin of the command for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */
|
||||||
public static final String EXTRA_STDIN = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_STDIN"; // Default: "com.termux.RUN_COMMAND_STDIN"
|
public static final String EXTRA_STDIN = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_STDIN"; // Default: "com.termux.RUN_COMMAND_STDIN"
|
||||||
/** Intent {@code String} extra for current working directory of command for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */
|
/** Intent {@code String} extra for current working directory of command for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */
|
||||||
@@ -882,8 +947,29 @@ public final class TermuxConstants {
|
|||||||
public static final String EXTRA_COMMAND_DESCRIPTION = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_COMMAND_DESCRIPTION"; // Default: "com.termux.RUN_COMMAND_COMMAND_DESCRIPTION"
|
public static final String EXTRA_COMMAND_DESCRIPTION = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_COMMAND_DESCRIPTION"; // Default: "com.termux.RUN_COMMAND_COMMAND_DESCRIPTION"
|
||||||
/** Intent markdown {@code String} extra for help of the command for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */
|
/** Intent markdown {@code String} extra for help of the command for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */
|
||||||
public static final String EXTRA_COMMAND_HELP = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_COMMAND_HELP"; // Default: "com.termux.RUN_COMMAND_COMMAND_HELP"
|
public static final String EXTRA_COMMAND_HELP = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_COMMAND_HELP"; // Default: "com.termux.RUN_COMMAND_COMMAND_HELP"
|
||||||
/** Intent {@code Parcelable} extra containing pending intent for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */
|
/** Intent {@code Parcelable} extra for the pending intent that should be sent with the result of the execution command to the execute command caller for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */
|
||||||
public static final String EXTRA_PENDING_INTENT = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_PENDING_INTENT"; // Default: "com.termux.RUN_COMMAND_PENDING_INTENT"
|
public static final String EXTRA_PENDING_INTENT = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_PENDING_INTENT"; // Default: "com.termux.RUN_COMMAND_PENDING_INTENT"
|
||||||
|
/** Intent {@code String} extra for the directory path in which to write the result of
|
||||||
|
* the execution command for the execute command caller for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */
|
||||||
|
public static final String EXTRA_RESULT_DIRECTORY = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_RESULT_DIRECTORY"; // Default: "com.termux.RUN_COMMAND_RESULT_DIRECTORY"
|
||||||
|
/** Intent {@code boolean} extra for whether the result should be written to a single file
|
||||||
|
* or multiple files (err, errmsg, stdout, stderr, exit_code) in
|
||||||
|
* {@link #EXTRA_RESULT_DIRECTORY} for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */
|
||||||
|
public static final String EXTRA_RESULT_SINGLE_FILE = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_RESULT_SINGLE_FILE"; // Default: "com.termux.RUN_COMMAND_RESULT_SINGLE_FILE"
|
||||||
|
/** Intent {@code String} extra for the basename of the result file that should be created
|
||||||
|
* in {@link #EXTRA_RESULT_DIRECTORY} if {@link #EXTRA_RESULT_SINGLE_FILE} is {@code true}
|
||||||
|
* for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */
|
||||||
|
public static final String EXTRA_RESULT_FILE_BASENAME = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_RESULT_FILE_BASENAME"; // Default: "com.termux.RUN_COMMAND_RESULT_FILE_BASENAME"
|
||||||
|
/** Intent {@code String} extra for the output {@link Formatter} format of the
|
||||||
|
* {@link #EXTRA_RESULT_FILE_BASENAME} result file for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */
|
||||||
|
public static final String EXTRA_RESULT_FILE_OUTPUT_FORMAT = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_RESULT_FILE_OUTPUT_FORMAT"; // Default: "com.termux.RUN_COMMAND_RESULT_FILE_OUTPUT_FORMAT"
|
||||||
|
/** Intent {@code String} extra for the error {@link Formatter} format of the
|
||||||
|
* {@link #EXTRA_RESULT_FILE_BASENAME} result file for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */
|
||||||
|
public static final String EXTRA_RESULT_FILE_ERROR_FORMAT = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_RESULT_FILE_ERROR_FORMAT"; // Default: "com.termux.RUN_COMMAND_RESULT_FILE_ERROR_FORMAT"
|
||||||
|
/** Intent {@code String} extra for the optional suffix of the result files that should be
|
||||||
|
* created in {@link #EXTRA_RESULT_DIRECTORY} if {@link #EXTRA_RESULT_SINGLE_FILE} is
|
||||||
|
* {@code false} for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */
|
||||||
|
public static final String EXTRA_RESULT_FILES_SUFFIX = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_RESULT_FILES_SUFFIX"; // Default: "com.termux.RUN_COMMAND_RESULT_FILES_SUFFIX"
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -892,6 +978,66 @@ public final class TermuxConstants {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Termux class to send back results of commands to their callers like plugin or 3rd party apps.
|
||||||
|
*/
|
||||||
|
public static final class RESULT_SENDER {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The default `Formatter` format strings to use for `ResultConfig#resultFileBasename`
|
||||||
|
* if `ResultConfig#resultSingleFile` is `true`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** The {@link Formatter} format string for success if only `stdout` needs to be written to
|
||||||
|
* {@link ResultConfig#resultFileBasename} where `stdout` maps to `%1$s`.
|
||||||
|
* This is used when `err` equals {@link Errno#ERRNO_SUCCESS} (-1) and `stderr` is empty
|
||||||
|
* and `exit_code` equals `0` and {@link ResultConfig#resultFileOutputFormat} is not passed. */
|
||||||
|
public static final String FORMAT_SUCCESS_STDOUT = "%1$s%n";
|
||||||
|
/** The {@link Formatter} format string for success if `stdout` and `exit_code` need to be written to
|
||||||
|
* {@link ResultConfig#resultFileBasename} where `stdout` maps to `%1$s` and `exit_code` to `%2$s`.
|
||||||
|
* This is used when `err` equals {@link Errno#ERRNO_SUCCESS} (-1) and `stderr` is empty
|
||||||
|
* and `exit_code` does not equal `0` and {@link ResultConfig#resultFileOutputFormat} is not passed. */
|
||||||
|
public static final String FORMAT_SUCCESS_STDOUT__EXIT_CODE = "%1$s%n%n%n%nexit_code=`%2$s`%n";
|
||||||
|
/** The {@link Formatter} format string for success if `stdout`, `stderr` and `exit_code` need to be
|
||||||
|
* written to {@link ResultConfig#resultFileBasename} where `stdout` maps to `%1$s`, `stderr`
|
||||||
|
* maps to `%2$s` and `exit_code` to `%3$s`.
|
||||||
|
* This is used when `err` equals {@link Errno#ERRNO_SUCCESS} (-1) and `stderr` is not empty
|
||||||
|
* and {@link ResultConfig#resultFileOutputFormat} is not passed. */
|
||||||
|
public static final String FORMAT_SUCCESS_STDOUT__STDERR__EXIT_CODE = "stdout=%n```%n%1$s%n```%n%n%n%nstderr=%n```%n%2$s%n```%n%n%n%nexit_code=`%3$s`%n";
|
||||||
|
/** The {@link Formatter} format string for failure if `err`, `errmsg`(`error`), `stdout`,
|
||||||
|
* `stderr` and `exit_code` need to be written to {@link ResultConfig#resultFileBasename} where
|
||||||
|
* `err` maps to `%1$s`, `errmsg` maps to `%2$s`, `stdout` maps
|
||||||
|
* to `%3$s`, `stderr` to `%4$s` and `exit_code` maps to `%5$s`.
|
||||||
|
* Do not define an argument greater than `5`, like `%6$s` if you change this value since it will
|
||||||
|
* raise {@link IllegalFormatException}.
|
||||||
|
* This is used when `err` does not equal {@link Errno#ERRNO_SUCCESS} (-1) and
|
||||||
|
* {@link ResultConfig#resultFileErrorFormat} is not passed. */
|
||||||
|
public static final String FORMAT_FAILED_ERR__ERRMSG__STDOUT__STDERR__EXIT_CODE = "err=`%1$s`%n%n%n%nerrmsg=%n```%n%2$s%n```%n%n%n%nstdout=%n```%n%3$s%n```%n%n%n%nstderr=%n```%n%4$s%n```%n%n%n%nexit_code=`%5$s`%n";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The default prefixes to use for result files under `ResultConfig#resultDirectoryPath`
|
||||||
|
* if `ResultConfig#resultSingleFile` is `false`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** The prefix for the err result file. */
|
||||||
|
public static final String RESULT_FILE_ERR_PREFIX = "err";
|
||||||
|
/** The prefix for the errmsg result file. */
|
||||||
|
public static final String RESULT_FILE_ERRMSG_PREFIX = "errmsg";
|
||||||
|
/** The prefix for the stdout result file. */
|
||||||
|
public static final String RESULT_FILE_STDOUT_PREFIX = "stdout";
|
||||||
|
/** The prefix for the stderr result file. */
|
||||||
|
public static final String RESULT_FILE_STDERR_PREFIX = "stderr";
|
||||||
|
/** The prefix for the exitCode result file. */
|
||||||
|
public static final String RESULT_FILE_EXIT_CODE_PREFIX = "exit_code";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Termux:Styling app constants.
|
* Termux:Styling app constants.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,36 +1,26 @@
|
|||||||
package com.termux.shared.termux;
|
package com.termux.shared.termux;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.ResolveInfo;
|
import android.content.pm.ResolveInfo;
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.google.common.base.Joiner;
|
|
||||||
|
|
||||||
import com.termux.shared.R;
|
import com.termux.shared.R;
|
||||||
import com.termux.shared.logger.Logger;
|
import com.termux.shared.logger.Logger;
|
||||||
import com.termux.shared.markdown.MarkdownUtils;
|
import com.termux.shared.markdown.MarkdownUtils;
|
||||||
import com.termux.shared.models.ExecutionCommand;
|
import com.termux.shared.models.ExecutionCommand;
|
||||||
import com.termux.shared.packages.PackageUtils;
|
import com.termux.shared.packages.PackageUtils;
|
||||||
|
import com.termux.shared.shell.TermuxShellEnvironmentClient;
|
||||||
import com.termux.shared.shell.TermuxTask;
|
import com.termux.shared.shell.TermuxTask;
|
||||||
|
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Properties;
|
|
||||||
import java.util.TimeZone;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class TermuxUtils {
|
public class TermuxUtils {
|
||||||
@@ -116,8 +106,6 @@ public class TermuxUtils {
|
|||||||
* @param context The Context to send the broadcast.
|
* @param context The Context to send the broadcast.
|
||||||
*/
|
*/
|
||||||
public static void sendTermuxOpenedBroadcast(@NonNull Context context) {
|
public static void sendTermuxOpenedBroadcast(@NonNull Context context) {
|
||||||
if (context == null) return;
|
|
||||||
|
|
||||||
Intent broadcast = new Intent(TermuxConstants.BROADCAST_TERMUX_OPENED);
|
Intent broadcast = new Intent(TermuxConstants.BROADCAST_TERMUX_OPENED);
|
||||||
List<ResolveInfo> matches = context.getPackageManager().queryBroadcastReceivers(broadcast, 0);
|
List<ResolveInfo> matches = context.getPackageManager().queryBroadcastReceivers(broadcast, 0);
|
||||||
|
|
||||||
@@ -138,7 +126,7 @@ public class TermuxUtils {
|
|||||||
* @param currentPackageContext The context of current package.
|
* @param currentPackageContext The context of current package.
|
||||||
* @return Returns the markdown {@link String}.
|
* @return Returns the markdown {@link String}.
|
||||||
*/
|
*/
|
||||||
public static String getTermuxPluginAppsInfoMarkdownString(@NonNull final Context currentPackageContext) {
|
public static String getTermuxPluginAppsInfoMarkdownString(final Context currentPackageContext) {
|
||||||
if (currentPackageContext == null) return "null";
|
if (currentPackageContext == null) return "null";
|
||||||
|
|
||||||
StringBuilder markdownString = new StringBuilder();
|
StringBuilder markdownString = new StringBuilder();
|
||||||
@@ -153,7 +141,7 @@ public class TermuxUtils {
|
|||||||
if (termuxPluginAppContext != null) {
|
if (termuxPluginAppContext != null) {
|
||||||
if (i != 0)
|
if (i != 0)
|
||||||
markdownString.append("\n\n");
|
markdownString.append("\n\n");
|
||||||
markdownString.append(TermuxUtils.getAppInfoMarkdownString(termuxPluginAppContext, false));
|
markdownString.append(getAppInfoMarkdownString(termuxPluginAppContext, false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,7 +163,7 @@ public class TermuxUtils {
|
|||||||
* {@link TermuxConstants#TERMUX_PACKAGE_NAME} package as well if its different from current package.
|
* {@link TermuxConstants#TERMUX_PACKAGE_NAME} package as well if its different from current package.
|
||||||
* @return Returns the markdown {@link String}.
|
* @return Returns the markdown {@link String}.
|
||||||
*/
|
*/
|
||||||
public static String getAppInfoMarkdownString(@NonNull final Context currentPackageContext, final boolean returnTermuxPackageInfoToo) {
|
public static String getAppInfoMarkdownString(final Context currentPackageContext, final boolean returnTermuxPackageInfoToo) {
|
||||||
if (currentPackageContext == null) return "null";
|
if (currentPackageContext == null) return "null";
|
||||||
|
|
||||||
StringBuilder markdownString = new StringBuilder();
|
StringBuilder markdownString = new StringBuilder();
|
||||||
@@ -201,7 +189,7 @@ public class TermuxUtils {
|
|||||||
markdownString.append("## ").append(currentAppName).append(" App Info\n");
|
markdownString.append("## ").append(currentAppName).append(" App Info\n");
|
||||||
markdownString.append(getAppInfoMarkdownStringInner(currentPackageContext));
|
markdownString.append(getAppInfoMarkdownStringInner(currentPackageContext));
|
||||||
|
|
||||||
if (returnTermuxPackageInfoToo && !isTermuxPackage) {
|
if (returnTermuxPackageInfoToo && termuxPackageContext != null && !isTermuxPackage) {
|
||||||
markdownString.append("\n\n## ").append(termuxAppName).append(" App Info\n");
|
markdownString.append("\n\n## ").append(termuxAppName).append(" App Info\n");
|
||||||
markdownString.append(getAppInfoMarkdownStringInner(termuxPackageContext));
|
markdownString.append(getAppInfoMarkdownStringInner(termuxPackageContext));
|
||||||
}
|
}
|
||||||
@@ -220,72 +208,17 @@ public class TermuxUtils {
|
|||||||
public static String getAppInfoMarkdownStringInner(@NonNull final Context context) {
|
public static String getAppInfoMarkdownStringInner(@NonNull final Context context) {
|
||||||
StringBuilder markdownString = new StringBuilder();
|
StringBuilder markdownString = new StringBuilder();
|
||||||
|
|
||||||
appendPropertyToMarkdown(markdownString,"APP_NAME", PackageUtils.getAppNameForPackage(context));
|
markdownString.append((AndroidUtils.getAppInfoMarkdownString(context)));
|
||||||
appendPropertyToMarkdown(markdownString,"PACKAGE_NAME", PackageUtils.getPackageNameForPackage(context));
|
|
||||||
appendPropertyToMarkdown(markdownString,"VERSION_NAME", PackageUtils.getVersionNameForPackage(context));
|
|
||||||
appendPropertyToMarkdown(markdownString,"VERSION_CODE", PackageUtils.getVersionCodeForPackage(context));
|
|
||||||
appendPropertyToMarkdown(markdownString,"TARGET_SDK", PackageUtils.getTargetSDKForPackage(context));
|
|
||||||
appendPropertyToMarkdown(markdownString,"IS_DEBUG_BUILD", PackageUtils.isAppForPackageADebugBuild(context));
|
|
||||||
|
|
||||||
String signingCertificateSHA256Digest = PackageUtils.getSigningCertificateSHA256DigestForPackage(context);
|
String signingCertificateSHA256Digest = PackageUtils.getSigningCertificateSHA256DigestForPackage(context);
|
||||||
if (signingCertificateSHA256Digest != null) {
|
if (signingCertificateSHA256Digest != null) {
|
||||||
appendPropertyToMarkdown(markdownString,"APK_RELEASE", getAPKRelease(signingCertificateSHA256Digest));
|
AndroidUtils.appendPropertyToMarkdown(markdownString,"APK_RELEASE", getAPKRelease(signingCertificateSHA256Digest));
|
||||||
appendPropertyToMarkdown(markdownString,"SIGNING_CERTIFICATE_SHA256_DIGEST", signingCertificateSHA256Digest);
|
AndroidUtils.appendPropertyToMarkdown(markdownString,"SIGNING_CERTIFICATE_SHA256_DIGEST", signingCertificateSHA256Digest);
|
||||||
}
|
}
|
||||||
|
|
||||||
return markdownString.toString();
|
return markdownString.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a markdown {@link String} for the device info.
|
|
||||||
*
|
|
||||||
* @param context The context for operations.
|
|
||||||
* @return Returns the markdown {@link String}.
|
|
||||||
*/
|
|
||||||
public static String getDeviceInfoMarkdownString(@NonNull final Context context) {
|
|
||||||
if (context == null) return "null";
|
|
||||||
|
|
||||||
// Some properties cannot be read with {@link System#getProperty(String)} but can be read
|
|
||||||
// directly by running getprop command
|
|
||||||
Properties systemProperties = getSystemProperties();
|
|
||||||
|
|
||||||
StringBuilder markdownString = new StringBuilder();
|
|
||||||
|
|
||||||
markdownString.append("## Device Info");
|
|
||||||
|
|
||||||
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);
|
|
||||||
appendPropertyToMarkdownIfSet(markdownString, "SECURITY_PATCH", systemProperties.getProperty("ro.build.version.security_patch"));
|
|
||||||
appendPropertyToMarkdownIfSet(markdownString, "IS_DEBUGGABLE", systemProperties.getProperty("ro.debuggable"));
|
|
||||||
appendPropertyToMarkdownIfSet(markdownString, "IS_EMULATOR", systemProperties.getProperty("ro.boot.qemu"));
|
|
||||||
appendPropertyToMarkdownIfSet(markdownString, "IS_TREBLE_ENABLED", systemProperties.getProperty("ro.treble.enabled"));
|
|
||||||
appendPropertyToMarkdown(markdownString, "TYPE", Build.TYPE);
|
|
||||||
appendPropertyToMarkdown(markdownString, "TAGS", Build.TAGS);
|
|
||||||
|
|
||||||
markdownString.append("\n\n### Hardware\n");
|
|
||||||
appendPropertyToMarkdown(markdownString, "MANUFACTURER", Build.MANUFACTURER);
|
|
||||||
appendPropertyToMarkdown(markdownString, "BRAND", Build.BRAND);
|
|
||||||
appendPropertyToMarkdown(markdownString, "MODEL", Build.MODEL);
|
|
||||||
appendPropertyToMarkdown(markdownString, "PRODUCT", Build.PRODUCT);
|
|
||||||
appendPropertyToMarkdown(markdownString, "BOARD", Build.BOARD);
|
|
||||||
appendPropertyToMarkdown(markdownString, "HARDWARE", Build.HARDWARE);
|
|
||||||
appendPropertyToMarkdown(markdownString, "DEVICE", Build.DEVICE);
|
|
||||||
appendPropertyToMarkdown(markdownString, "SUPPORTED_ABIS", Joiner.on(", ").skipNulls().join(Build.SUPPORTED_ABIS));
|
|
||||||
|
|
||||||
markdownString.append("\n##\n");
|
|
||||||
|
|
||||||
return markdownString.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a markdown {@link String} for reporting an issue.
|
* Get a markdown {@link String} for reporting an issue.
|
||||||
*
|
*
|
||||||
@@ -399,103 +332,24 @@ public class TermuxUtils {
|
|||||||
aptInfoScript = aptInfoScript.replaceAll(Pattern.quote("@TERMUX_PREFIX@"), TermuxConstants.TERMUX_PREFIX_DIR_PATH);
|
aptInfoScript = aptInfoScript.replaceAll(Pattern.quote("@TERMUX_PREFIX@"), TermuxConstants.TERMUX_PREFIX_DIR_PATH);
|
||||||
|
|
||||||
ExecutionCommand executionCommand = new ExecutionCommand(1, TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/bash", null, aptInfoScript, null, true, false);
|
ExecutionCommand executionCommand = new ExecutionCommand(1, TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/bash", null, aptInfoScript, null, true, false);
|
||||||
TermuxTask termuxTask = TermuxTask.execute(context, executionCommand, null, true);
|
TermuxTask termuxTask = TermuxTask.execute(context, executionCommand, null, new TermuxShellEnvironmentClient(), true);
|
||||||
if (termuxTask == null || !executionCommand.isSuccessful() || executionCommand.exitCode != 0) {
|
if (termuxTask == null || !executionCommand.isSuccessful() || executionCommand.resultData.exitCode != 0) {
|
||||||
Logger.logError(LOG_TAG, executionCommand.toString());
|
Logger.logError(LOG_TAG, executionCommand.toString());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (executionCommand.stderr != null && !executionCommand.stderr.isEmpty())
|
if (!executionCommand.resultData.stderr.toString().isEmpty())
|
||||||
Logger.logError(LOG_TAG, executionCommand.toString());
|
Logger.logError(LOG_TAG, executionCommand.toString());
|
||||||
|
|
||||||
StringBuilder markdownString = new StringBuilder();
|
StringBuilder markdownString = new StringBuilder();
|
||||||
|
|
||||||
markdownString.append("## ").append(TermuxConstants.TERMUX_APP_NAME).append(" APT Info\n\n");
|
markdownString.append("## ").append(TermuxConstants.TERMUX_APP_NAME).append(" APT Info\n\n");
|
||||||
markdownString.append(executionCommand.stdout);
|
markdownString.append(executionCommand.resultData.stdout.toString());
|
||||||
|
|
||||||
return markdownString.toString();
|
return markdownString.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public static Properties getSystemProperties() {
|
|
||||||
Properties systemProperties = new Properties();
|
|
||||||
|
|
||||||
// getprop commands returns values in the format `[key]: [value]`
|
|
||||||
// Regex matches string starting with a literal `[`,
|
|
||||||
// followed by one or more characters that do not match a closing square bracket as the key,
|
|
||||||
// followed by a literal `]: [`,
|
|
||||||
// followed by one or more characters as the value,
|
|
||||||
// followed by string ending with literal `]`
|
|
||||||
// multiline values will be ignored
|
|
||||||
Pattern propertiesPattern = Pattern.compile("^\\[([^]]+)]: \\[(.+)]$");
|
|
||||||
|
|
||||||
try {
|
|
||||||
Process process = new ProcessBuilder()
|
|
||||||
.command("/system/bin/getprop")
|
|
||||||
.redirectErrorStream(true)
|
|
||||||
.start();
|
|
||||||
|
|
||||||
InputStream inputStream = process.getInputStream();
|
|
||||||
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
|
|
||||||
String line, key, value;
|
|
||||||
|
|
||||||
while ((line = bufferedReader.readLine()) != null) {
|
|
||||||
Matcher matcher = propertiesPattern.matcher(line);
|
|
||||||
if (matcher.matches()) {
|
|
||||||
key = matcher.group(1);
|
|
||||||
value = matcher.group(2);
|
|
||||||
if (key != null && value != null && !key.isEmpty() && !value.isEmpty())
|
|
||||||
systemProperties.put(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bufferedReader.close();
|
|
||||||
process.destroy();
|
|
||||||
|
|
||||||
} catch (IOException e) {
|
|
||||||
Logger.logStackTraceWithMessage("Failed to get run \"/system/bin/getprop\" to get system properties.", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
//for (String key : systemProperties.stringPropertyNames()) {
|
|
||||||
// Logger.logVerbose(key + ": " + systemProperties.get(key));
|
|
||||||
//}
|
|
||||||
|
|
||||||
return systemProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getSystemPropertyWithAndroidAPI(@NonNull String property) {
|
|
||||||
try {
|
|
||||||
return System.getProperty(property);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Logger.logVerbose("Failed to get system property \"" + property + "\":" + e.getMessage());
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void appendPropertyToMarkdownIfSet(StringBuilder markdownString, String label, Object value) {
|
|
||||||
if (value == null) return;
|
|
||||||
if (value instanceof String && (((String) value).isEmpty()) || "REL".equals(value)) return;
|
|
||||||
markdownString.append("\n").append(getPropertyMarkdown(label, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void appendPropertyToMarkdown(StringBuilder markdownString, String label, Object value) {
|
|
||||||
markdownString.append("\n").append(getPropertyMarkdown(label, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getPropertyMarkdown(String label, Object value) {
|
|
||||||
return MarkdownUtils.getSingleLineMarkdownStringEntry(label, value, "-");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public static String getCurrentTimeStamp() {
|
|
||||||
@SuppressLint("SimpleDateFormat")
|
|
||||||
final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
|
|
||||||
df.setTimeZone(TimeZone.getTimeZone("UTC"));
|
|
||||||
return df.format(new Date());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getAPKRelease(String signingCertificateSHA256Digest) {
|
public static String getAPKRelease(String signingCertificateSHA256Digest) {
|
||||||
if (signingCertificateSHA256Digest == null) return "null";
|
if (signingCertificateSHA256Digest == null) return "null";
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ public class KeyboardUtils {
|
|||||||
activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
|
activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setResizeTerminalViewForSoftKeyboardFlags(final Activity activity) {
|
public static void setSoftInputModeAdjustResize(final Activity activity) {
|
||||||
// TODO: The flag is deprecated for API 30 and WindowInset API should be used
|
// TODO: The flag is deprecated for API 30 and WindowInset API should be used
|
||||||
// https://developer.android.com/reference/android/view/WindowManager.LayoutParams#SOFT_INPUT_ADJUST_RESIZE
|
// https://developer.android.com/reference/android/view/WindowManager.LayoutParams#SOFT_INPUT_ADJUST_RESIZE
|
||||||
// https://medium.com/androiddevelopers/animating-your-keyboard-fb776a8fb66d
|
// https://medium.com/androiddevelopers/animating-your-keyboard-fb776a8fb66d
|
||||||
|
|||||||
@@ -0,0 +1,221 @@
|
|||||||
|
package com.termux.shared.view;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.ContextWrapper;
|
||||||
|
import android.content.res.Configuration;
|
||||||
|
import android.graphics.Point;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
|
||||||
|
public class ViewUtils {
|
||||||
|
|
||||||
|
/** Log root view events. */
|
||||||
|
public static boolean VIEW_UTILS_LOGGING_ENABLED = false;
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "ViewUtils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether view utils logging is enabled or not.
|
||||||
|
*
|
||||||
|
* @param value The boolean value that defines the state.
|
||||||
|
*/
|
||||||
|
public static void setIsViewUtilsLoggingEnabled(boolean value) {
|
||||||
|
VIEW_UTILS_LOGGING_ENABLED = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a {@link View} is fully visible and not hidden or partially covered by another view.
|
||||||
|
*
|
||||||
|
* https://stackoverflow.com/a/51078418/14686958
|
||||||
|
*
|
||||||
|
* @param view The {@link View} to check.
|
||||||
|
* @param statusBarHeight The status bar height received by {@link View.OnApplyWindowInsetsListener}.
|
||||||
|
* @return Returns {@code true} if view is fully visible.
|
||||||
|
*/
|
||||||
|
public static boolean isViewFullyVisible(View view, int statusBarHeight) {
|
||||||
|
Rect[] windowAndViewRects = getWindowAndViewRects(view, statusBarHeight);
|
||||||
|
if (windowAndViewRects == null)
|
||||||
|
return false;
|
||||||
|
return windowAndViewRects[0].contains(windowAndViewRects[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the {@link Rect} of a {@link View} and the {@link Rect} of the window inside which it
|
||||||
|
* exists.
|
||||||
|
*
|
||||||
|
* https://stackoverflow.com/a/51078418/14686958
|
||||||
|
*
|
||||||
|
* @param view The {@link View} inside the window whose {@link Rect} to get.
|
||||||
|
* @param statusBarHeight The status bar height received by {@link View.OnApplyWindowInsetsListener}.
|
||||||
|
* @return Returns {@link Rect[]} if view is visible where Rect[0] will contain window
|
||||||
|
* {@link Rect} and Rect[1] will contain view {@link Rect}. This will be {@code null}
|
||||||
|
* if view is not visible.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static Rect[] getWindowAndViewRects(View view, int statusBarHeight) {
|
||||||
|
if (view == null || !view.isShown())
|
||||||
|
return null;
|
||||||
|
|
||||||
|
boolean view_utils_logging_enabled = VIEW_UTILS_LOGGING_ENABLED;
|
||||||
|
|
||||||
|
// windowRect - will hold available area where content remain visible to users
|
||||||
|
// Takes into account screen decorations (e.g. statusbar)
|
||||||
|
Rect windowRect = new Rect();
|
||||||
|
view.getWindowVisibleDisplayFrame(windowRect);
|
||||||
|
|
||||||
|
// If there is actionbar, get his height
|
||||||
|
int actionBarHeight = 0;
|
||||||
|
boolean isInMultiWindowMode = false;
|
||||||
|
Context context = view.getContext();
|
||||||
|
if (context instanceof AppCompatActivity) {
|
||||||
|
androidx.appcompat.app.ActionBar actionBar = ((AppCompatActivity) context).getSupportActionBar();
|
||||||
|
if (actionBar != null) actionBarHeight = actionBar.getHeight();
|
||||||
|
isInMultiWindowMode = ((AppCompatActivity) context).isInMultiWindowMode();
|
||||||
|
} else if (context instanceof Activity) {
|
||||||
|
android.app.ActionBar actionBar = ((Activity) context).getActionBar();
|
||||||
|
if (actionBar != null) actionBarHeight = actionBar.getHeight();
|
||||||
|
isInMultiWindowMode = ((Activity) context).isInMultiWindowMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
int displayOrientation = getDisplayOrientation(context);
|
||||||
|
|
||||||
|
// windowAvailableRect - takes into account actionbar and statusbar height
|
||||||
|
Rect windowAvailableRect;
|
||||||
|
windowAvailableRect = new Rect(windowRect.left, windowRect.top + actionBarHeight, windowRect.right, windowRect.bottom);
|
||||||
|
|
||||||
|
// viewRect - holds position of the view in window
|
||||||
|
// (methods as getGlobalVisibleRect, getHitRect, getDrawingRect can return different result,
|
||||||
|
// when partialy visible)
|
||||||
|
Rect viewRect;
|
||||||
|
final int[] viewsLocationInWindow = new int[2];
|
||||||
|
view.getLocationInWindow(viewsLocationInWindow);
|
||||||
|
int viewLeft = viewsLocationInWindow[0];
|
||||||
|
int viewTop = viewsLocationInWindow[1];
|
||||||
|
|
||||||
|
if (view_utils_logging_enabled) {
|
||||||
|
Logger.logVerbose(LOG_TAG, "getWindowAndViewRects:");
|
||||||
|
Logger.logVerbose(LOG_TAG, "windowRect: " + toRectString(windowRect) + ", windowAvailableRect: " + toRectString(windowAvailableRect));
|
||||||
|
Logger.logVerbose(LOG_TAG, "viewsLocationInWindow: " + toPointString(new Point(viewLeft, viewTop)));
|
||||||
|
Logger.logVerbose(LOG_TAG, "activitySize: " + toPointString(getDisplaySize(context, true)) +
|
||||||
|
", displaySize: " + toPointString(getDisplaySize(context, false)) +
|
||||||
|
", displayOrientation=" + displayOrientation);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int viewRight = viewLeft + view.getWidth();
|
||||||
|
int viewBottom = viewTop + view.getHeight();
|
||||||
|
viewRect = new Rect(viewLeft, viewTop, viewRight, viewBottom);
|
||||||
|
|
||||||
|
if (displayOrientation == Configuration.ORIENTATION_LANDSCAPE && viewRight > windowAvailableRect.right) {
|
||||||
|
if (view_utils_logging_enabled)
|
||||||
|
Logger.logVerbose(LOG_TAG, "viewRight " + viewRight + " is greater than windowAvailableRect.right " + windowAvailableRect.right + " in landscape mode. Setting windowAvailableRect.right to viewRight since it may not include navbar height.");
|
||||||
|
windowAvailableRect.right = viewRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Rect[]{windowAvailableRect, viewRect};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if {@link Rect} r2 is above r2. An empty rectangle never contains another rectangle.
|
||||||
|
*
|
||||||
|
* @param r1 The base rectangle.
|
||||||
|
* @param r2 The rectangle being tested that should be above.
|
||||||
|
* @return Returns {@code true} if r2 is above r1.
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get device orientation.
|
||||||
|
*
|
||||||
|
* Related: https://stackoverflow.com/a/29392593/14686958
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} to check with.
|
||||||
|
* @return {@link Configuration#ORIENTATION_PORTRAIT} or {@link Configuration#ORIENTATION_LANDSCAPE}.
|
||||||
|
*/
|
||||||
|
public static int getDisplayOrientation(@NonNull Context context) {
|
||||||
|
Point size = getDisplaySize(context, false);
|
||||||
|
return (size.x < size.y) ? Configuration.ORIENTATION_PORTRAIT : Configuration.ORIENTATION_LANDSCAPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get device display size.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} to check with.
|
||||||
|
* @param activitySize The set to {@link true}, then size returned will be that of the activity
|
||||||
|
* and can be smaller than physical display size in multi-window mode.
|
||||||
|
* @return Returns the display size as {@link Point}.
|
||||||
|
*/
|
||||||
|
public static Point getDisplaySize( @NonNull Context context, boolean activitySize) {
|
||||||
|
// android.view.WindowManager.getDefaultDisplay() and Display.getSize() are deprecated in
|
||||||
|
// API 30 and give wrong values in API 30 for activitySize=false in multi-window
|
||||||
|
androidx.window.WindowManager windowManager = new androidx.window.WindowManager(context);
|
||||||
|
androidx.window.WindowMetrics windowMetrics;
|
||||||
|
if (activitySize)
|
||||||
|
windowMetrics = windowManager.getCurrentWindowMetrics();
|
||||||
|
else
|
||||||
|
windowMetrics = windowManager.getMaximumWindowMetrics();
|
||||||
|
return new Point(windowMetrics.getBounds().width(), windowMetrics.getBounds().height());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convert {@link Rect} to {@link String}. */
|
||||||
|
public static String toRectString(Rect rect) {
|
||||||
|
if (rect == null) return "null";
|
||||||
|
return "(" + rect.left + "," + rect.top + "), (" + rect.right + "," + rect.bottom + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convert {@link Point} to {@link String}. */
|
||||||
|
public static String toPointString(Point point) {
|
||||||
|
if (point == null) return "null";
|
||||||
|
return "(" + point.x + "," + point.y + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the {@link Activity} associated with the {@link Context} if available. */
|
||||||
|
@Nullable
|
||||||
|
public static Activity getActivity(Context context) {
|
||||||
|
while (context instanceof ContextWrapper) {
|
||||||
|
if (context instanceof Activity) {
|
||||||
|
return (Activity)context;
|
||||||
|
}
|
||||||
|
context = ((ContextWrapper)context).getBaseContext();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convert value in device independent pixels (dp) to pixels (px) units. */
|
||||||
|
public static int dpToPx(Context context, int dp) {
|
||||||
|
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -13,52 +13,7 @@
|
|||||||
|
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- FileUtils -->
|
<string name="msg_directory_absolute_path">%1$s Directory Absolute Path: \"%2$s\"</string>
|
||||||
<string name="error_executable_required">Executable required.</string>
|
|
||||||
<string name="error_null_or_empty_parameter">The %1$s is to \"%2$s\" null or empty.</string>
|
|
||||||
<string name="error_null_or_empty_regular_file_path">The regular file path is null or empty.</string>
|
|
||||||
<string name="error_null_or_empty_regular_file">The regular file is null or empty.</string>
|
|
||||||
<string name="error_null_or_empty_executable_file_path">The executable file path is null or empty.</string>
|
|
||||||
<string name="error_null_or_empty_executable_file">The executable file is null or empty.</string>
|
|
||||||
<string name="error_null_or_empty_directory_file_path">The directory file path is null or empty.</string>
|
|
||||||
<string name="error_null_or_empty_directory_file">The directory file is null or empty.</string>
|
|
||||||
|
|
||||||
<string name="error_file_not_found_at_path">The %1$s is not found at path \"%2$s\".</string>
|
|
||||||
<string name="error_no_regular_file_found">Regular file not found at %1$s path.</string>
|
|
||||||
<string name="error_not_a_regular_file">The %1$s at path \"%2$s\" is not a regular file.</string>
|
|
||||||
<string name="error_non_regular_file_found">Non-regular file found at %1$s path.</string>
|
|
||||||
<string name="error_non_directory_file_found">Non-directory file found at %1$s path.</string>
|
|
||||||
<string name="error_non_symlink_file_found">Non-symlink file found at %1$s path.</string>
|
|
||||||
<string name="error_file_not_an_allowed_file_type">The %1$s found at path \"%2$s\" is not one of allowed file types \"%3$s\".</string>
|
|
||||||
|
|
||||||
<string name="error_validate_file_existence_and_permissions_failed_with_exception">Validating file existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s</string>
|
|
||||||
<string name="error_validate_directory_existence_and_permissions_failed_with_exception">Validating directory existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s</string>
|
|
||||||
|
|
||||||
<string name="error_creating_file_failed">Creating %1$s at path \"%2$s\" failed.</string>
|
|
||||||
<string name="error_creating_file_failed_with_exception">Creating %1$s at path \"%2$s\" failed.\nException: %3$s</string>
|
|
||||||
|
|
||||||
<string name="error_cannot_overwrite_a_non_symlink_file_type">Cannot overwrite %1$s while creating symlink at \"%2$s\" to \"%3$s\" since destination file type \"%4$s\" is not a symlink.</string>
|
|
||||||
<string name="error_creating_symlink_file_failed_with_exception">Creating %1$s at path \"%2$s\" to \"%3$s\" failed.\nException: %4$s</string>
|
|
||||||
|
|
||||||
<string name="error_copying_or_moving_file_failed_with_exception">%1$s from \"%2$s\" to \"%3$s\" failed.\nException: %4$s</string>
|
|
||||||
<string name="error_copying_or_moving_file_to_same_path">%1$s from \"%2$s\" to \"%3$s\" cannot be done since they point to the same path.</string>
|
|
||||||
<string name="error_cannot_overwrite_a_different_file_type">Cannot overwrite %1$s while %2$s it from \"%3$s\" to \"%4$s\" since destination file type \"%5$s\" is different from source file type \"%6$s\".</string>
|
|
||||||
<string name="error_cannot_move_directory_to_sub_directory_of_itself">Cannot move %1$s from \"%2$s\" to \"%3$s\" since destination is a subdirectory of the source.</string>
|
|
||||||
|
|
||||||
<string name="error_file_still_exists_after_deleting">The %1$s still exists after deleting it from \"%2$s\".</string>
|
|
||||||
<string name="error_deleting_file_failed">Deleting %1$s at path \"%2$s\" failed.</string>
|
|
||||||
<string name="error_deleting_file_failed_with_exception">Deleting %1$s at path \"%2$s\" failed.\nException: %3$s</string>
|
|
||||||
<string name="error_clearing_directory_failed_with_exception">Clearing %1$s at path \"%2$s\" failed.\nException: %3$s</string>
|
|
||||||
|
|
||||||
<string name="error_reading_string_to_file_failed_with_exception">Reading string from %1$s at path \"%2$s\" failed.\nException: %3$s</string>
|
|
||||||
<string name="error_writing_string_to_file_failed_with_exception">Writing string to %1$s at path \"%2$s\" failed.\nException: %3$s</string>
|
|
||||||
<string name="error_unsupported_charset">Unsupported charset \"%1$s\"</string>
|
|
||||||
<string name="error_checking_if_charset_supported_failed">Checking if charset \"%1$s\" is suppoted failed.\nException: %2$s</string>
|
|
||||||
|
|
||||||
<string name="error_invalid_file_permissions_string_to_check">The file permission string to check is invalid.</string>
|
|
||||||
<string name="error_file_not_readable">The %1$s at path is not readable. Permission Denied.</string>
|
|
||||||
<string name="error_file_not_writable">The %1$s at path is not writable. Permission Denied.</string>
|
|
||||||
<string name="error_file_not_executable">The %1$s at path is not executable. Permission Denied.</string>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -70,7 +25,13 @@
|
|||||||
|
|
||||||
<!-- PermissionUtils -->
|
<!-- PermissionUtils -->
|
||||||
<string name="message_sudo_please_grant_permissions">Please grant permissions on next screen</string>
|
<string name="message_sudo_please_grant_permissions">Please grant permissions on next screen</string>
|
||||||
<string name="error_display_over_other_apps_permission_not_granted">&TERMUX_APP_NAME; requires \"Display over other apps\" permission to start terminal sessions from background on Android >= 10. Grants it from Settings -> Apps -> &TERMUX_APP_NAME; -> Advanced</string>
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ReportActivity -->
|
||||||
|
<string name="action_copy">Copy</string>
|
||||||
|
<string name="action_share">Share</string>
|
||||||
|
<string name="title_report_text">Report Text</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
10
termux-shared/src/main/res/values/styles.xml
Normal file
10
termux-shared/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<style name="Theme.AppCompat.TermuxReportActivity" parent="Theme.AppCompat.Light.NoActionBar">
|
||||||
|
<item name="colorPrimaryDark">#FF0000</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Toolbar.Title" parent="TextAppearance.Widget.AppCompat.Toolbar.Title">
|
||||||
|
<item name="android:textSize">14sp</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
Reference in New Issue
Block a user