Compare commits

...

55 Commits

Author SHA1 Message Date
agnostic-apollo
bde9d01f76 Bump to v0.115 2021-07-01 07:13:03 +05:00
agnostic-apollo
5a511a2ba3 Revert some unneeded changes to Logger done in 679e0de0
Logger was updated to get suppressed exceptions by calling `Throwable[] getSuppressed()` but `printStackTrace()` would already log them, even though shortened stacktrace with `... n more` notation, but this should be enough for debugging since main throwable stacktrace should have enough class line info. Manually logging full suppressed stacktraces would likely trigger `LOGGER_ENTRY_MAX_PAYLOAD` and split the message into multiple log entries and also duplicate the suppressed stacktraces, so best revert this unless ever necessary.
2021-07-01 07:12:48 +05:00
agnostic-apollo
5c50964b1f Revert "Bump to v0.115"
This reverts commit dea8c987
2021-07-01 06:31:22 +05:00
agnostic-apollo
dea8c9879e Bump to v0.115 2021-07-01 05:15:34 +05:00
agnostic-apollo
2034121798 Fx issues where crash throwable message wasn't been added to crash log 2021-07-01 04:21:36 +05:00
agnostic-apollo
23a900c433 Move Termux app specific logic out of CrashHandler
Create the TermuxCrashUtils class that provides the default path and app for termux instead of hardcoding it in CrashHandler. TermuxCrashUtils can be used by termux plugins as well for their own usage or they can implement the CrashHandler.CrashHandlerClient if they want to log to different files or want custom logic.
2021-07-01 04:21:02 +05:00
agnostic-apollo
93a7525d9b Add comment about mkshrc validity when loading /system/bin/sh for failsafe session 2021-07-01 00:29:26 +05:00
Leonid Pliushch
5670128236 update bootstrap archives 2021-06-30 12:20:57 +03:00
agnostic-apollo
dfd32435af Bump gradle dependencies versions 2021-06-30 06:17:26 +05:00
agnostic-apollo
49265160f8 Update LICENSE.md 2021-06-30 06:10:00 +05:00
agnostic-apollo
70e1accafe Change license for non-termux utils to MIT
Changing the license for non-termux utils from GPLv3 to MIT so that they can be used by other termux plugin apps or apps that may be released under a different license. Termux is already using a lot of libraries that are not GPL and such general utils shouldn't be restrictive any ways.

Moreover, `TermuxConstants` and `TermuxPropertyConstants` should be MIT licensed as well so that other non-FOSS or non-GPLv3 apps can use them, like for `RUN_COMMAND` intent.

Any code not listed in exceptions of `LICENSE.md` files is still under GPLv3, mainly termux specific code and it will and should remain that way.

All code in files whose license is changed was authored by me as far as I can tell, but if any code in them is not that I missed, let me know, so that changes can be made since I can't and won't change the license of code authored by someone else. If some other objection is raised, let me know too.

Future contributors should check the `LICENSE.md` files and see if they are okay with contributing code as MIT and if they are not, then they should create separate file/package in termux-shared.
2021-06-30 06:10:00 +05:00
agnostic-apollo
1c7f9166f2 Move Termux app specific logic out of NotificationUtils 2021-06-30 06:10:00 +05:00
agnostic-apollo
553913cde1 Divide dialog utils 2021-06-30 06:10:00 +05:00
agnostic-apollo
6bca378cec Move Android specific utils from TermuxUtils to AndroidUtils 2021-06-30 06:10:00 +05:00
agnostic-apollo
12f910c32d Move Termux app specific logic out of PermissionUtils 2021-06-30 06:10:00 +05:00
agnostic-apollo
94c5f3674a Do not start login shell and load ~/.profile if starting a failsafe session
This is done by not starting arg `0` with `-`

Fixes #2150.
2021-06-30 06:10:00 +05:00
agnostic-apollo
28b9f93d13 Compile Url match regex once and not on every use
Needed for #2146.
2021-06-30 03:18:44 +05:00
agnostic-apollo
69bebb5916 Add termux.properties property for opening terminal transcript urls on click
The user can add `terminal-onclick-url-open` entry to `termux.properties` file to enable opening url links in terminal transcript on click or on tap. The default value is `false`. So adding the entry `terminal-onclick-url-open=true` to `termux.properties` file will enable url opening. Running `termux-reload-settings` command will also update the behaviour instantaneously if changed.

This commit just adds the property and doesn't implement the functionality. That will later be merged from #2146.
2021-06-30 03:04:56 +05:00
agnostic-apollo
321350256e Allow users to disable terminal margin adjustment
The user can add `disable-terminal-margin-adjustment=true` entry to `termux.properties` file to disable terminal view margin adjustment that is done to prevent soft keyboard from covering bottom part of terminal view on some devices. Margin adjustment may cause screen flickering on some devices and so should be disabled. The default value is `false`. So adding the entry `disable-terminal-margin-adjustment=true` to `termux.properties` file will disable margin adjustment. Exit termux and restart for changes to take affect after updating value.

In case e5a9b99a did not fix screen flickering issues for #2127, then this can be used to disable it. Closes #2127.
2021-06-30 02:49:00 +05:00
agnostic-apollo
e5a9b99afe Fix issues with TermuxActivityRootView margin adjustment
Margin adjustment was causing screen flickering due to invalid values being calculated in landscape and split screen mode.

Attempts to fix issue #2127
2021-06-30 02:31:47 +05:00
agnostic-apollo
00f805f7ec Fix issue where cursor blinker wouldn't automatically start after terminal reset if it was disabled before reset 2021-06-28 12:19:06 +05:00
agnostic-apollo
d3c34ad1f5 Fix issue where cursor blinker wouldn't automatically start after session change
The reason was that mTerminalCursorBlinkerRunnable inner class mEmulator wouldn't get updated to the new mEmulator on session change and would still be using the old session's.
2021-06-28 11:57:12 +05:00
agnostic-apollo
59877a08d1 Add termux settings button to left drawer too since apparently people can't find the one in context menu 2021-06-28 11:05:20 +05:00
agnostic-apollo
9c92251595 Fixed issue where back button would not exit the activity if bootstrap installation failed and users dismissed the error dialog, 2021-06-28 09:26:42 +05:00
agnostic-apollo
e408fdcc08 Show crash notification when bootstrap installation or setup storage failures
Sometimes users report that bootstrap installation failed on their devices but provide no details. Since they don't check logcat for the exception or exception is one time only, we can't know what happened. Although, reasons are likely root ownership files.

The notification will show the full stacktrace including suppressed ones for why failure occurred and hopefully be easier to find the problems and we can get reports too.
2021-06-28 09:19:20 +05:00
agnostic-apollo
53c1a49b5b Make TermuxTask and TermuxSession agnostic to termux environment
Those classes shouldn't be tied to termux environment like variables, interpreters and working directory since commands may need to be executed with a different environment like android's or with a different logic. Now both classes use the ShellEnvironmentClient interface to dynamically get the environment to be used which currently for Termux's case is implemented by TermuxShellEnvironmentClient which is just a wrapper for TermuxShellUtils since later implements static functions.
2021-06-28 05:57:45 +05:00
agnostic-apollo
2aafcf8435 Add support to send back or store RUN_COMMAND intent command results in files and provide way to fix argument splitting sent with am command
### `RUN_COMMAND` Results in Files

Previously in `v0.109` with a2209dd support was added in RUN_COMMAND intent to send back foreground and background command results with `PendingIntent` to the intent sender. However, this was only usable with java code by android apps. But if you were sending the intent with the `am` command from inside a shell, like tasker `Run Shell` action, you could not get the result back directly. You could technically manually save the output of your script in files under `/sdcard` with redirection and wait for them to be created in the `Run Shell` so that you could process the result. However, this was only possible for background commands and the caller would hang indefinitely if a termux internal `errmsg` was generated like it does for termux-tasker, likely caused by incorrect intent extra arguments, an exception being raised when executing the executable/script, or termux being closed with the exit button, etc.

Now native support has been added inside termux to store results of both foreground and background commands inside files, that also sends back internal `errmsgs` as long as result files extras are valid. This can be used to run synchronous commands from inside termux, with other apps that have `Run commands in Termux environment` (`com.termux.permission.RUN_COMMAND`) like Tasker, from pc over `adb` or inside `adb shell` if you have a rooted device, or from pc if you have setup termux `sshd`. The `RUN_COMMAND` intent can only be sent by the `termux` user itself, by an app that has the permission or by the `root` user. The `shell` user of `adb` cannot send it. A script will be provided at a later time that will automatically detect these cases to easily run `RUN_COMMAND` intent commands which will also automatically create temp directories and do cleanup. This can also be useful inside termux itself, like if you want to start a new foreground session and to automatically store its output to a log file when you exit. Support can also be added for this to be done for termux-boot and termux-widget as well but will require updates for them.

There is obviously a security and privacy concern for this if you use shared storage `/sdcard` to store the result files since malicious apps could read them and optionally modify them for MITM attacks if you are reading the result and processing it unsafely. But users access other files from shared storage anyways for other scripts. Saving the result files on shared storage would only be necessary if you want to read the result back, like in Tasker or over adb since non-termux and non-root users can't access termux private app data directory `/data/data/com.termux`. For internal termux usage, this shouldn't be a concern if files are saved inside termux private app data directory.

The extra constant values are defined by [`TermuxConstants`](https://github.com/termux/termux-app/tree/master/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java) class of the [`termux-shared`](https://github.com/termux/termux-app/tree/master/termux-shared) library. The [`ResultSender`](https://github.com/termux/termux-app/tree/master/termux-shared/src/main/java/com/termux/shared/shell/ResultSender.java) class actually sends back the results.

The following extras have been added:

- The `String` `RUN_COMMAND_SERVICE.EXTRA_RESULT_DIRECTORY` extra for the directory path in which to write the result of the execution command for the execute command caller.

- The `boolean` `RUN_COMMAND_SERVICE.EXTRA_RESULT_SINGLE_FILE` extra for whether the result should be written to a single file or multiple files (`err`, `errmsg`, `stdout`, `stderr`, `exit_code`) in `EXTRA_RESULT_DIRECTORY`.

- The `String` `RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_BASENAME` extra for the basename of the result file that should be created in `EXTRA_RESULT_DIRECTORY` if `EXTRA_RESULT_SINGLE_FILE` is `true`.

- The `String` `RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT` extra for the output [`Formatter`](https://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html) format of the `EXTRA_RESULT_FILE_BASENAME` result file.

- The `String` `RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT` extra for the error [`Formatter`](https://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html) format of the `EXTRA_RESULT_FILE_BASENAME` result file.

- The `String` `RUN_COMMAND_SERVICE.EXTRA_RESULT_FILES_SUFFIX` extra for the optional suffix of the result files that should be created in `EXTRA_RESULT_DIRECTORY` if `EXTRA_RESULT_SINGLE_FILE` is `false`.

The `err` and `errmsg` are for internal termux errors like invalid intent extras, etc and not related to the shell commands itself. This is the same way Tasker actions and plugins system work with [`%err` and `%errmsg`](https://tasker.joaoapps.com/userguide/en/variables.html#localbuiltin). The `err` will be equal to `Errno.ERRNO_SUCCESS` (`-1`) if no internal errors are set. The `stdout`, `stderr` and `exit_code` are for the shell commands. The `exit_code` is normally `0` for success.

There are two modes for getting back the result in results files.

##### `EXTRA_RESULT_SINGLE_FILE` extra is `true`

Only a single file will be created under `EXTRA_RESULT_DIRECTORY` that will contain the `err`, `errmsg`, `stdout`, `stderr` and `exit_code` in a specific format defined by `RESULT_SENDER.FORMAT_*` constants in `TermuxConstants` class depending on the exit status of the command. By default if the `EXTRA_RESULT_FILE_BASENAME` extra is not passed, the basename of the result file will be set to `<command_path_basename>-<timestamp>.log` where `<timestamp>` will be in the `yyyy-MM-dd_HH.mm.ss.SSS` format. The `EXTRA_RESULT_FILE_OUTPUT_FORMAT` extra can be passed with a custom format that should be used when `err` equals `-1` and `EXTRA_RESULT_FILE_ERROR_FORMAT` extra for when its greater than `-1`. The value `0` is for `Errno.ERRNO_CANCELLED` and should also be considered a failure unlike `exit_code`.

```
am startservice --user 0 -n 'com.termux/com.termux.app.RunCommandService' -a 'com.termux.RUN_COMMAND' --es 'com.termux.RUN_COMMAND_PATH' '$PREFIX/bin/top' --esa 'com.termux.RUN_COMMAND_ARGUMENTS' '-n,5' --ez 'com.termux.RUN_COMMAND_BACKGROUND' '0' --es 'com.termux.RUN_COMMAND_RESULT_DIRECTORY' '/sdcard/.termux-app' --ez 'com.termux.RUN_COMMAND_RESULT_SINGLE_FILE' 'true' --es 'com.termux.RUN_COMMAND_RESULT_FILE_BASENAME' 'top.log'
```

##### `EXTRA_RESULT_SINGLE_FILE` extra is `false`

Separate files will be created under `EXTRA_RESULT_DIRECTORY` for each of the `err`, `errmsg`, `stdout`, `stderr` and `exit_code`. Their basenames (same as mentioned) are defined by the `RESULT_FILE_*` constants in `TermuxConstants` class. If the `EXTRA_RESULT_FILES_SUFFIX` extra is passed, then that will be suffixed to the basename of each file like `err<suffix>`, `stdout<suffix>`, etc.

The `err` file will be created after writing to other result files has already finished and this is the file the caller should optionally wait for  to be created to be notified that the command has finished, like with `test -f "$result_directory/err"` command in an infinite loop (with sleep+timeout) or with `inotify`. After it has been read, caller can start reading from the rest of the result files if they exist. The `errmsg`, `stdout`, `stderr` and `exit_code` files will not be created if nothing is to be written to them, so no do wait for these files.

If you are not passing a unique suffix for each intent, then result files of multiple simultaneous intent commands will conflict with each other. So ideally a temp directory should be created for each intent command and that should be passed as `EXTRA_RESULT_DIRECTORY`. You can use `mktemp` command to create a unique name and create the directory for you.

```
temp_directory="$(/system/bin/mktemp -d --tmpdir="/sdcard/.termux-app" "top.XXXXXX")" || return $?

am startservice --user 0 -n 'com.termux/com.termux.app.RunCommandService' -a 'com.termux.RUN_COMMAND' --es 'com.termux.RUN_COMMAND_PATH' '$PREFIX/bin/top' --esa 'com.termux.RUN_COMMAND_ARGUMENTS' '-n,5' --ez 'com.termux.RUN_COMMAND_BACKGROUND' '1' --es 'com.termux.RUN_COMMAND_RESULT_DIRECTORY' "$temp_directory" --ez 'com.termux.RUN_COMMAND_RESULT_SINGLE_FILE' 'false'
```

Use following if in termux and not in tasker/rooted shell.

```
temp_directory="$(PATH=/system/bin; LD_LIBRARY_PATH=/system/lib64:/system/lib; unset LD_PRELOAD; mktemp -d --tmpdir="/sdcard/.termux-app" "top.XXXXXX")" || return $?
```

Note that since there may be a delay between creation of `result_file`/`err` file and writing to it or flushing to disk, a temp file is created first suffixed with `-<timestamp>` which is then moved to the final destination, since caller may otherwise read from an empty file in some cases otherwise.

Commands will automatically be killed and result up till that point returned if user exits termux app like with the `Exit` button in the notification. The exit code will be `137` (`SIGKILL`).

--------------------

### `RUN_COMMAND` Arguments Splitting with `am` Command

If `am` command is used to send the `RUN_COMMAND` intent and you want to pass an argument with the `--esa com.termux.RUN_COMMAND_ARGUMENTS` string array extra that itself contains a normal comma `,` (`U+002C`, `&comma;`, `&#44;`, `comma`), it must be escaped with a backslash `\,` so that the  argument isn't split into multiple arguments. The only problem is that, the arguments received by the termux will contain `\,` instead of `,` since the reversal isn't done as described in the [am command source](https://android.googlesource.com/platform/frameworks/base/+/21bdaf1/cmds/am/src/com/android/commands/am/Am.java#572) while converting to a string array. There is also no way for the `am` command or termux to know whether `\,` was done to prevent arguments splitting or `\,` was a literal string naturally part of the argument.

```
// Split on commas unless they are preceeded by an escape.
// The escape character must be escaped for the string and
// again for the regex, thus four escape characters become one.
intent.putExtra(key, strings);
```

To fix this termux now supports an alternative method to handle such conditions. If an argument contains a normal comma `,`, then instead of escaping them with a backslash `\,`, replace all normal commas with the comma alternate character `‚` (`#U+201A`, `&sbquo;`, `&#8218;`, `single low-9 quotation mark`) before sending the intent with the `am` command. This way argument splitting will not be done. You can pass the `com.termux.RUN_COMMAND_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS` `boolean` extra in the `RUN_COMMAND` intent so that termux replaces all the comma alternate characters back to normal commas. It would be unlikely for the the arguments to naturally contain the comma alternate characters for this to be a problem. Even if they do, they might not be significant for any logic. If they are, then you can set a different character that should be replaced, by passing it in the `com.termux.RUN_COMMAND_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS` `String` extra.

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

The following extras have been added:

- The `boolean` `RUN_COMMAND_SERVICE.EXTRA_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS` extra for whether to replace comma alternative characters in arguments with normal comma `,` (`U+002C`, `&comma;`, `&#44;`, `comma`).
- The `String` `RUN_COMMAND_SERVICE.EXTRA_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS` extra for the comma alternative characters in arguments that should be replaced instead of the default comma alternate character `‚` (`#U+201A`, `&sbquo;`, `&#8218;`, `single low-9 quotation mark`).

```
am startservice --user 0 -n 'com.termux/com.termux.app.RunCommandService' -a 'com.termux.RUN_COMMAND' --es 'com.termux.RUN_COMMAND_PATH' '$PREFIX/bin/bash' --esa 'com.termux.RUN_COMMAND_ARGUMENTS' '-c,echo "Argument with commas here _ and here _ that have been converted to an underscore before sending"; sleep 5' --ez 'com.termux.RUN_COMMAND_BACKGROUND' '0' --ez 'com.termux.RUN_COMMAND_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS' 'true' --es 'com.termux.RUN_COMMAND_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS' '_'
```

Note that since `0.109`, the `RUN_COMMAND` intent supports `RUN_COMMAND_SERVICE.EXTRA_STDIN`, so instead of passing arguments, just pass a script as `stdin` to the `bash` executable so that you don't have to deal with this "mess". You will have to surround the script with single quotes and escape any single quotes inside the script itself, like each single quote `'` with `'\''`.

--------------------

### Internal Changes

This commit also adds onto 679e0de0 and 4494bc66

The `ExecutionCommand` has been updated and command result variables have been moved to `ResultData` and result configuration to `ResultConfig` since the later two should be agnostic of what type of command there are for. They don't necessarily have to be for terminal/shell commands and can be used for plugin APIs, etc.

The `ResultData` instead of a `String` `errmsg` now stores a list of `Error` objects. This is necessary since multiple errors may be picked up while a command is run, like say working directory is invalid and an error is returned by FileUtils and while sending the result to the caller, the `ResultSender` returns an additional error because result configuration like result directory or result output format was invalid. In these situations `PluginUtils` will show a notification to the user with info of each error thrown.

In addition to above, in `ResultData`, the `stdout` and `stderr` are converted to `StringBuilder` instead of a `String`. This allows for data to be appended to each from various places in code like log debug or error entries for API commands without having to create a new `String` object each time value needs to updated. This can be useful so that the caller doesn't have to check `logcat` for API commands. This does not apply to `ExecutionCommand` since only `TermuxSession` and `TermuxTask` set the data.

The `ResultSender` class is what handles the result of commands whether they need to be sent via `PendingIntent` or to a result directory based on the `ResultConfig` object passed. Result will be sent through both if both of them are not `null`.

The `TermuxConstants` class has been updated to `v0.24.0`. Check its Changelog section for info on changes.
2021-06-28 04:54:39 +05:00
agnostic-apollo
1c1af34374 Bump gradle to 4.2.1 2021-06-27 05:57:32 +05:00
agnostic-apollo
52f18a73fb Bump gradle wrapper to v7.1 2021-06-27 05:57:16 +05:00
agnostic-apollo
28f81f2cc7 Fix minor typos and potential errors 2021-06-26 08:51:30 +05:00
agnostic-apollo
4494bc66e4 Implement Errno system
This commit adds onto 679e0de0

If an exception is thrown, the exception message might not contain the full errors. Individual failures may get added to suppressed throwables. FileUtils functions previously just returned the exception message as errmsg which did not contain full error info.

Now `Error` class has been implemented which will used to return errors, including suppressed throwables. Each `Error` object will have an error type, code, message and a list of throwables in case multiple throwables need to returned, in addition to the suppressed throwables list in each throwable.

A supportive `Errno` base class has been implemented as well which other errno classes can inherit of which some have been added. Each `Errno` object will have an error type, code and message and can be converted to an `Error` object if needed.

Requirement for `Context` object has been removed from FileUtils so that they can be called from anywhere in code instead of having to pass around `Context` objects. Previously, `string.xml` was used to store error messages in case multi language support had to be added in future since error messages are displayed to users and not just for dev usage. However, now this will have to handled in java code if needed, based on locale.

The termux related file utils have also been moved from FileUtils to TermuxFileUtils
2021-06-26 07:23:34 +05:00
agnostic-apollo
679e0de044 Fix suppressed exceptions not being logged and long logcat message being truncated
If an exception is thrown, the exception message might not contain the full errors. Individual failures may get added to suppressed throwables which can be extracted from the exception object by calling `Throwable[] getSuppressed()`. So just logging the exception message and stacktrace may not be enough, the suppressed throwables need to be logged as well.

The Logger class will now log the suppressed throwables as well if they are found in the exception.

This was mainly a concern for FileUtils where guava MoreUtils library was used to delete directories but exceptions weren't being fully logged on failures, like bootstrap failures, so user wouldn't know what really caused the failure.

https://github.com/google/guava/blob/v30.1.1/guava/src/com/google/common/io/MoreFiles.java#L775

The FileUtils will be fixed in a future commit.

This also adds support with "log*Extended()" functions so that logcat entries longer than LOGGER_ENTRY_MAX_PAYLOAD do not get truncated by android. This is done by splitting the log message into multiple messages if the limit is crossed. This is specially necessary for logging long stacktraces, suppressed throwables and errmsg of ExecutionCommand, etc.
2021-06-26 06:01:06 +05:00
agnostic-apollo
80b495e50b Move storage permission logic to PermissionUtils and add disable battery optimizations code
Option to disable battery optimizations will be added in termux settings later.
2021-06-24 23:59:56 +05:00
agnostic-apollo
69e5deedc7 Move to com.termux domain for termux libraries published with jitpack
A DNS TXT record has been added from git.termux.com to https://github.com/termux at termux.com by @fornwall

```
dig txt git.termux.com

;; ANSWER SECTION:
git.termux.com.300INTXT"https://github.com/termux"
```

https://jitpack.io/docs/#custom-domain-name
2021-06-23 03:36:36 +05:00
agnostic-apollo
7f36d7bbd0 Move ReportActivity to termux-shared so that other termux plugins can use it too 2021-06-21 04:59:11 +05:00
agnostic-apollo
b7b12ebe84 Move from github packages to jitpack.io for hosting termux library packages
Github Package hosting is considered a private repository since it requires github APIs keys if a hosted library needs to be imported as a dependency. Importing from private repositories is not allowed as per F-Droid policy so termux plugin apps can't import termux libraries as dependencies so hence we move to Jitpack. Check https://github.com/termux/termux-app/issues/2011#issuecomment-824837387.

Version number of all published libraries from termux-app must be the same.

Importing can be done with the following way.

Add to root level build.gradle

```
allprojects {
    repositories {
        google()
        mavenCentral()
        //mavenLocal()
        maven { url "https://jitpack.io" }
    }
}
```

Add to app module level build.gradle if you want to import `termux-shared`

```
 dependencies {
    implementation 'com.github.termux:termux-shared:0.115'
}
```

Check https://github.com/jitpack/jitpack.io#building-with-jitpack for other details, like including commit or branch level import.

If you are updating the libraries as well and want to test locally, run `./gradlew publishReleasePublicationToMavenLocal` from root directory of termux-app to publish library to local maven repository. You may need to rebuild project before it, library files will be published at `~/.m2/repository/com/github/termux/termux-shared/0.115`. If you want to import the updated library in a project, then uncomment the `mavenLocal()` line in the build.gradle and run sync gradle with project files.

Making changes to library after dependencies have already been cached without incrementing version number may need deleting gradle cache if syncing gradle files doesn't work after publishing changes. Open gradle right sidebar in android studio, then right click on top level entry, then select "Refresh Gradle Dependencies", which will redownload/refresh all dependencies and will take a lot of time. Instead running `find ~/.gradle/caches/ -type d -name "*com.github.termux*" -prune -exec rm -rf "{}" \; -print` and then running gradle sync should be enough.

Using "com.termux" instead of "com.github.termux" will require a DNS TXT record to be added from git.termux.com to https://github.com/termux at termux.com

https://jitpack.io/docs/#custom-domain-name
2021-06-21 03:36:20 +05:00
agnostic-apollo
f77c88633e Fix issue where terminal cursor blinking would not automatically start again if termux activity is started after device display timeout with double tap and not power button.
Fixes #2138
2021-06-20 22:18:19 +05:00
agnostic-apollo
5f2ccca423 Redo fix execution commands exceptions not being logged or sent back to plugins
The f62febbf commit mentioned that it solved "the bug where Termux:Tasker would hang indefinitely if Runtime.getRuntime().exec raised an exception, like for invalid or missing interpreter errors and Termux:Tasker wasn't notified of it. Now the errmsg will be used to send any exceptions back to Termux:Tasker and other 3rd party calls."

This however was still broken due to local design changes made to TermuxTask after testing was already done. This commit should solve that problem. Moreover, now a notification will be shown if execution commands **fail to start** that are run by plugins that don't expect the result back, like with Termux:Widget or RUN_COMMAND intent. This should make it easier for users to debug problems, since otherwise logcat needs to be looked. But logcat would still need to be looked if commands/scripts fail after they have started due to internal errors. Notifications can be disabled from Termux Settings by disabling the "Plugin Error Notifications" toggle.
2021-06-13 00:44:56 +05:00
agnostic-apollo
f0f6927273 Rename variable 2021-06-13 00:29:52 +05:00
agnostic-apollo
0fb18c0c8b Remove left over lines from gradle.properties 2021-06-13 00:25:09 +05:00
agnostic-apollo
4dfed3320e Bump to v0.114 2021-06-11 02:57:56 +05:00
agnostic-apollo
7ac62c9840 Allow users to disable terminal session change toast
The user can add `disable-terminal-session-change-toast=true` entry to `termux.properties` file to disable terminal session change toast. The default value is `false`. Running `termux-reload-settings` command will also update the behaviour instantaneously if changed.

Closes #2118
2021-06-11 02:54:47 +05:00
agnostic-apollo
fd80cdaf23 Change default extra keys style
If a user does not define a custom value in termux.properties file, then by default 2 rows will be shown with all arrow keys (up/down/left/right) for ease of terminal use.
2021-06-11 02:39:11 +05:00
agnostic-apollo
19c690d02b Fix issue where if termux installer failed with an exception after prefix directory was already created, then try again would load a broken environment. 2021-06-10 08:06:43 +05:00
agnostic-apollo
e119d34bca Fix issue where terminal cursor blinking would not automatically start again if termux activity was restarted after exiting it with double back press 2021-06-10 08:03:12 +05:00
agnostic-apollo
f545ebf0bd Allow users to set terminal cursor style with termux.properties
This `terminal-cursor-style` key can be used to set the terminal cursor style. The user can set a string value to `block` for `■`, `underline` for `_` or `bar` for `|` cursor style. The default value is still `block`. So adding an entry like `terminal-cursor-style=bar` to `termux.properties` file will allow users to change to the `bar` cursor style. After updating the value, termux must be restarted. You can also run `termux-reload-settings` command so that termux loads the updated value, but only new sessions will use the updated value, existing sessions will not be affected unless you Reset them from terminal's long hold options menu `More` -> `Reset` or restart termux activity after double back press to exit.

You can temporarily switch to different cursor styles with (or add to `.bashrc` but resetting will restore default `bar` style):

- block: `echo -e "\033[2 q"`
- underline: `echo -e "\033[4 q"`
- bar: ` echo -e "\033[6 q"`

Closes #2075
2021-06-10 06:14:12 +05:00
agnostic-apollo
0b4bbaf23d Allow users to adjust terminal transcript rows with termux.properties
This `terminal-transcript-rows` key can be used to adjust the terminal transcript rows. The user can set an integer value between `100` and `50000`. The default value is still `2000`. So adding an entry like `terminal-transcript-rows=10000` to `termux.properties` file will allow users to scroll back ~10000 lines of command output. After updating the value, termux must be restarted. You can also run `termux-reload-settings` command so that termux loads the updated value, but only new sessions will use the updated value, existing sessions will not be affected.

You can test this with the following, where `70` is number of `x` characters per line and `10001` is the number of lines to print.
`x="$(printf 'x%.0s' {1..70})"; for i in {1..10001}; do echo "$i:$x"; done`

Be advised that using large values may have a performance impact depending on your device capabilities, so use at your own risk.

Closes #2071
2021-06-10 03:55:31 +05:00
agnostic-apollo
e7dd0eeebe Fix issue where soft keyboard overlaps extra keys or terminal in some cases
Check TermuxActivityRootView javadocs for details.
2021-06-10 03:06:10 +05:00
agnostic-apollo
7ef9255437 Remove hardcoded wiki.termux.com url from HelpActivity 2021-06-06 22:15:21 +05:00
agnostic-apollo
7225e2b379 Merge pull request #2114 from agnostic-apollo/fix-soft-keyboard-not-showing-in-some-case
Fix issue where soft keyboard would not show in some cases
2021-06-06 21:53:33 +05:00
agnostic-apollo
1ad038ece5 Fix issue where soft keyboard would not show in some cases
1. If `soft-keyboard-toggle-behaviour=enable/disable` was set, then pressing keyboard toggle wouldn't show the keyboard after switching back from another app if keyboard was previously disabled by user.
2. If switching back from another app, like when opening url with context menu "Select URL" long press and returning to termux with back button, then soft keyboard wouldn't automatically open like it does on app startup.

Also fixed issue where OnFocusChangeListener wasn't being set up if keyboard had to be hidden or disabled on startup.

Fixes #2111, Fixes #2112
2021-06-06 21:34:12 +05:00
agnostic-apollo
cb8b0225ca Add pluginIntent field to ExecutionCommand 2021-06-06 06:15:47 +05:00
Leonid Pliushch
7620800cd5 bump bootstraps again
Include latest changes to pkg & termux-change-repo
2021-06-04 19:17:19 +03:00
Leonid Pliushch
6837db0015 update bootstrap archives 2021-06-03 20:52:17 +03:00
agnostic-apollo
e08e3b536e Do not close soft keyboard when toolbar text input view is focused on
The TerminalToolbarViewPager EditText was requesting focus when it was selected. This called the TerminalView.onFocusChange() event with hasFocus=false, which closed the soft keyboard. Now soft keyboard will only be closed if both of them don't have focus.

Fixes #2077
2021-05-23 21:14:30 +05:00
90 changed files with 4712 additions and 1788 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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)
} }
} }

View File

@@ -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"
/> />

View File

@@ -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) {

View File

@@ -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

View File

@@ -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();

View File

@@ -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() {

View File

@@ -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);
} }
} }

View File

@@ -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

View File

@@ -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();

View File

@@ -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);
}
} }

View File

@@ -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);
}
}
}

View File

@@ -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);

View File

@@ -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();
} }

View File

@@ -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}.

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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" }
} }
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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")
} }
} }
} }

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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() {

View File

@@ -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")
} }
} }
} }

View File

@@ -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);
} }

View File

@@ -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) {

View File

@@ -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
View 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).
##

View File

@@ -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")
} }
} }
} }

View File

@@ -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;

View File

@@ -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);
}
} }

View File

@@ -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);
}
}

View File

@@ -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;
} }
} }

View File

@@ -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();
}
}

View 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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
} }

View File

@@ -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 + "\"");

View File

@@ -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));
}
}

View File

@@ -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));
}
} }

View File

@@ -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) {

View File

@@ -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;
} }

View File

@@ -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;
}
} }

View File

@@ -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();
} }
/** /**

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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);
} }

View File

@@ -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);
}
} }

View File

@@ -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,

View File

@@ -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);
} }

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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);
}
}

View File

@@ -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());
}
}
}

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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);

View File

@@ -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());
}
}

View File

@@ -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;, &#44;, comma) */
public static final String COMMA_NORMAL = ","; // Default: ","
/** The alternate comma character (U+201A, &sbquo;, &#8218;, 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.
*/ */

View File

@@ -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";

View File

@@ -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

View File

@@ -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());
}
}

View File

@@ -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>

View 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>