Compare commits

...

481 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
agnostic-apollo
b711a467c1 Bump to v0.113 2021-05-16 23:44:43 +05:00
agnostic-apollo
d736b1eba5 Implement TermuxActivity callbacks in TermuxTerminalViewClient and TermuxTerminalSessionClient 2021-05-16 23:33:44 +05:00
agnostic-apollo
58d577066a Release terminal beep SoundPool resources on activity stop to attempt to prevent exception
The following exception may be thrown, likely because of unreleased resources.

Related https://stackoverflow.com/a/28708351/14686958

java.util.concurrent.TimeoutException: android.media.SoundPool.finalize() timed out after 10 seconds
  at android.media.SoundPool.native_release(Native Method)
  at android.media.SoundPool.release(SoundPool.java:177)
  at android.media.SoundPool.finalize(SoundPool.java:182)
  at java.lang.Daemons$FinalizerDaemon.doFinalize(Daemons.java:250)
  at java.lang.Daemons$FinalizerDaemon.runInternal(Daemons.java:237)
  at java.lang.Daemons$Daemon.run(Daemons.java:103)
  at java.lang.Thread.run(Thread.java:764)
2021-05-16 23:33:44 +05:00
agnostic-apollo
89a1e02713 Updates to terminal cursor blinking
Fixed bug where cursor would become invisible when long holding (arrow) keys when editing commands (outside of text editors like nano).

Updated javadocs with info on how cursor blinking works

"Performance Improvements" and removed redundant mRendering check
2021-05-16 23:33:44 +05:00
Leonid Pliushch
6524a619f6 update bootstrap archives 2021-05-16 19:40:25 +03:00
agnostic-apollo
f8ccbb4953 Log invalid values stored in termux.properties file during load time
All external and internal values were already logged and required log level to be set to "Verbose" in Termux Settings, but now invalid  values and the default value used instead will be logged at log level "Normal" as well.

The `TermuxPropertyConstants` class has been updated to `v0.10.0`. Check its Changelog sections for info on changes.
2021-05-16 01:31:34 +05:00
agnostic-apollo
31298b8857 Allow users to enable terminal cursor blinking with termux.properties
This `terminal-cursor-blink-rate` key can be used to enable terminal cursor blinking. The user can set an int value between `100` and `2000` which will be used as blink rate in millisecond. The default value is `0`, which disables cursor blinking. So adding an entry like `terminal-cursor-blink-rate=600` to `~/termux.properties` file will make the cursor attempt to blink every 600ms. Running `termux-reload-settings` command will also update the cursor blinking rate instantaneously if changed.

A background thread is used to control the blinking by toggling the cursor visibility and then invalidating the view every x milliseconds set. This will have a performance impact, so use wisely and at your own risk.

If the cursor itself is disabled, which is controlled by whether DECSET_BIT_CURSOR_ENABLED (DECSET 25, DECTCEM), then blinking will be automatically disabled. You can enable the cursor with `tput cnorm` or `echo -e '\e[?25h'` and disable it with `tput civis` or `echo -e '\e[?25l'`.

Note that you can also change the cursor color by adding `cursor` property to `~/colors.properties` file, like `cursor=#FFFFFF` for a white cursor.

The `TermuxPropertyConstants` class has been updated to `v0.9.0`. Check its Changelog sections for info on changes.

Closes #153
2021-05-15 16:35:54 +05:00
agnostic-apollo
11f5c0afd1 Normalize gradlew.bat 2021-05-14 08:41:15 +05:00
agnostic-apollo
27dc211e2d Update .gitattributes 2021-05-14 08:05:08 +05:00
agnostic-apollo
2f828255ee Generate potentially long running reports in background threads instead of main UI thread 2021-05-14 07:05:14 +05:00
agnostic-apollo
339b2a24a2 Add support for setting Termux:Tasker log level from TermuxSettings 2021-05-14 06:47:42 +05:00
agnostic-apollo
6de3713049 Add in-app Donation link in Termux Settings for non google playstore releases
The `TermuxConstants` class has been updated to `v0.22.0`. Check its Changelog sections for info on changes.
2021-05-14 05:19:18 +05:00
agnostic-apollo
79df863b75 Ensure we read/write to/from current SharedPreferences
When getting SharedPreferences of other termux sharedUserId app packages, we get its Context first and if its null, it would mean that the package is not installed or likely has a different signature. For this case, we force exit the app in some places, since that shouldn't occur. Previously, if it was null, we were defaulting to getting SharedPreferences of current package context instead, which would mix keys of other packages with current one. SharedPreferences of other app packages aren't being used currently, so this isn't an issue, this commit just fixes the issue for future.

Force exit will also be triggered if Termux is forked and TermuxConstants.TERMUX_PACKAGE_NAME is not updated to the same value as applicationId since TermuxActivity.onCreate() will fail to get SharedPreferences of TermuxConstants.TERMUX_PACKAGE_NAME.

Moreover, its normally not allowed to install apps with different signatures, but if its done, we "may" need AndroidManifest `queries` entries in andorid 11, check PackageUtils.getSigningCertificateSHA256DigestForPackage() for details.
2021-05-14 03:54:13 +05:00
agnostic-apollo
af115c9966 Add generic functions to show a message in dialog and exit app with an error message 2021-05-13 17:37:01 +05:00
agnostic-apollo
1e30022ce7 Add support for APK signing certificate SHA-256 digest and detecting APK release type and add them to App Info reports
The `TermuxConstants` class has been updated to `v0.21.0`. Check its Changelog sections for info on changes.
2021-05-13 15:40:09 +05:00
agnostic-apollo
4629276500 Changed TermuxAppSharedPreferences function naming convention 2021-05-13 05:08:25 +05:00
agnostic-apollo
d42514d8c9 Moved Termux app settings into dedicated "directory" in Termux Settings and added About page
The `TermuxConstants` class has been updated to `v0.20.0`. Check its Changelog sections for info on changes.
2021-05-13 05:07:45 +05:00
agnostic-apollo
90c9a7b3bc Allow users to disable soft keyboard automatically if hardware keyboard is connected
Users can enable this behaviour by enabling the `Termux Settings` -> `Keyboard I/O` -> `Soft Keyboard Only If No Hardware` toggle.

Currently, for this case, soft keyboard will be disabled on Termux app startup and when switching back from another app. Soft keyboard can be temporarily enabled in show/hide soft keyboard toggle behaviour with keyboard toggle buttons and will continue to work when tapping on terminal view for opening and back button for closing, until Termux app is switched to another app. After returning back, keyboard will be disabled until toggle is pressed again.

This also may help for the Lineage OS bug where blank space is shown where soft keyboard should be if "Show soft keyboard" toggle in "Language and Input" is disabled. Check KeyboardUtils.shouldSoftKeyboardBeDisabled() and https://github.com/termux/termux-app/issues/1995#issuecomment-837080079 for details.

The `TermuxPreferenceConstants` class has been updated to `v0.10.0`. Check its Changelog sections for info on changes.
2021-05-12 23:04:58 +05:00
agnostic-apollo
e6dac93352 Preserve the termux.properties literal string values internally that were being converted to boolean on load time
The `TermuxPropertyConstants` class has been updated to `v0.8.0`. Check its Changelog sections for info on changes.
2021-05-10 07:10:38 +05:00
agnostic-apollo
e4e638bd31 Allow users to enable/disable keyboard instead of just show/hide with keyboard toggle buttons
This `soft-keyboard-toggle-behaviour` key can be used to change the behaviour. The default behaviour is `show/hide`. The user can set the value to `enable/disable` in `termux.properties` file to change default behaviour of keyboard toggle buttons to enable/disable. In this mode, tapping the keyboard toggle button will disable (and hide) the keyboard and tapping on the terminal view will not open the keybaord automatically, until the keyboard toggle button is pressed again manually. This applies to split screen and floating keyboard as well. The keyboard can also be enabled from Settings -> Keyboard I/O -> Soft Keyboard toggle. Running `termux-reload-settings` command will also update the behaviour instantaneously if changed.

Fixed issue where "hide-soft-keyboard-on-startup" property wouldn't work if Termux app was switched back from another app. Fixes #1098

Fixed issue where soft keyboard may not show on startup on some devices but it still may fail sometimes.

The `TermuxPropertyConstants` class has been updated to `v0.7.0`. Check its Changelog sections for info on changes.
2021-05-10 06:03:29 +05:00
agnostic-apollo
fe8c3ba216 Update KeyboardUtils will null checks and add setSoftKeyboardVisibility() 2021-05-10 05:21:54 +05:00
agnostic-apollo
4ecea144bb Create KeyboardUtils 2021-05-09 21:08:54 +05:00
agnostic-apollo
116b9b42d8 Bump compileSdkVersion (NOT targetSdkVersion) to 30 2021-05-09 21:07:23 +05:00
agnostic-apollo
39c69db820 Fix issues where soft keyboard was not shown in some cases when hardware keyboard was attached v2
This is an update to 4d1851e6 commit.

The toggle logic change previously was actually being applied to ctrl+alt+k hardware keyboard shortcut instead of the mentioned extra keys "KEYBOARD" toggle. However, now it applies to the extra keys "KEYBOARD" toggle button as well, in addition to  drawer "KEYBOARD" toggle button and ctrl+alt+k hardware keyboard shortcut. They will all behave the same now.

Updated onSingleTapUp() to also forcefully show keyboard.

Fixed issue where "hide-soft-keyboard-on-startup" property wasn't respected anymore due to forced keyboard showing done in 4d1851e6.

Removed "stateAlwaysVisible" flag from AndroidManifest since its ignored in Android 10 by default and not needed due to usage of InputMethodManager.showSoftInput(). https://developer.android.com/reference/android/view/WindowManager.LayoutParams#SOFT_INPUT_STATE_ALWAYS_VISIBLE

Moved "adjustResize" from AndroidManifest into java code (which is also deprecated in API 30) to centralize keyboard logic.
2021-05-09 07:28:12 +05:00
agnostic-apollo
4d1851e6be Fix issues where soft keyboard was not shown in some cases when hardware keyboard was attached
For Termux app to be able to show a soft keyboard while a hardware keyboard is attached requires either of 2 cases:

1. User has enabled "Show on-screen keyboard while hardware keyboard is attached" toggle in Android "Language and Input" settings.
2. The toggle is disabled, but the soft keyboard app overrides the default implementation of `InputMethodService.onEvaluateInputViewShown()` and returns `true`. Some keyboard apps have a setting for this, like HackerKeyboard, but its not supported by all keyboard apps.

https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:frameworks/base/core/java/android/inputmethodservice/InputMethodService.java;l=1751

Termux previously didn't forcefully show a keyboard when the drawer "KEYBOARD" toggle button was pressed and only did that for the "KEYBOARD" extra keys toggle button. This prevented the keyboard to be shown for case 2 even when the user attempted to show the keyboard with the drawer "KEYBOARD" toggle. Now both buttons will forcefully show the keyboard.

Moreover, previously at app startup for case 2, the keyboard wasn't being shown. Now it will automatically be shown without requiring a manual press of a "KEYBOARD" toggle button.

This may also solve the issue where the soft keyboard wouldn't show even when the toggle of case 1 was enabled.
2021-05-08 04:16:51 +05:00
agnostic-apollo
596aa56b38 Update report issue message to ask users to provide details on what they were doing that caused the Termux app crash 2021-05-08 03:24:09 +05:00
agnostic-apollo
4850678d55 Move Build.ID and Build.DISPLAY to Software section of device info markdown 2021-05-08 03:14:20 +05:00
agnostic-apollo
bc52a4e90c Update README.md 2021-05-03 16:28:02 +05:00
agnostic-apollo
3e7b3604a4 Update LICENSE.md 2021-05-03 16:08:04 +05:00
agnostic-apollo
f3f58c8fc7 Update README.md 2021-05-03 16:07:18 +05:00
agnostic-apollo
4711094614 Bump ndk to v22.1.7171670
Will also remove requirement for F-Droid metadata/com.termux.yml ndk version patch

1fe5c6b905 (54c3cc576ac595d35a41ce9e4a69e1c905fd6ea4)
2021-05-03 00:40:03 +05:00
agnostic-apollo
42ad3723fd Fix NullPointerExceptions for cases when TermuxActivity tries to access TermuxService when it doesn't hold a reference
Fixes #2026
2021-05-03 00:39:42 +05:00
agnostic-apollo
b268b6edf7 Disable error flashes when clearing TMPDIR directory on termux app exit
Rooted users were getting `Clearing $TMPDIR directory at path "/data/data/com.termux/files/usr/tmp" failed` flash errors when they exited Termux if directories existed in TMPDIR that only had `root` user ownership, since they would fail to get cleared since clearing was being run as the termux app user instead of as the root user. Now errors will only be logged to logcat.
2021-05-01 23:31:02 +05:00
agnostic-apollo
b84854af92 Update README.md 2021-05-01 19:35:52 +05:00
agnostic-apollo
cfebb3358d Update README 2021-04-27 15:50:57 +05:00
agnostic-apollo
93e1b13278 Update README 2021-04-26 12:43:55 +05:00
Fredrik Fornwall
0d4bfb7bd5 Replace jcenter() with mavenCentral()
This is since JCenter is being shut down.
2021-04-26 01:11:35 +02:00
agnostic-apollo
0aa5a123b7 Bump to v0.112
This only reverts the versioning login change done in a6ae656c since that caused F-Droid bot and Github Packages to fail to pick up new releases. The versions must be bumped directly in `build.gradle` file in future and not through other files like `gradle.properties`.
2021-04-22 19:57:08 +05:00
agnostic-apollo
2e156d4621 Update LICENSE 2021-04-21 17:01:52 +05:00
agnostic-apollo
fdcf6cb6e1 Update LICENSE 2021-04-21 17:00:38 +05:00
agnostic-apollo
01f2ed0892 Update LICENSE 2021-04-21 16:51:20 +05:00
agnostic-apollo
c9abfe5438 Update README 2021-04-21 16:51:08 +05:00
agnostic-apollo
8f9771adce Bump to v0.111 2021-04-21 14:07:25 +05:00
agnostic-apollo
b34f60b1b0 Fix the bootstrap reinstallation logic for when PREFIX is a symlink
Changes were made to bootstrap reinstallation logic in 107927f5, but it wasn't considering that PREFIX may be a symlink file to a directory instead of a directory file. With this commit, the previous behaviour of termux is restored where PREFIX can optionally be a symlink to a valid directory where the symlink isn't broken/dangling.
2021-04-21 14:06:57 +05:00
agnostic-apollo
0fe608f91e Revert "Bump to v0.111"
This reverts commit 5d911ef93f.
2021-04-21 13:55:25 +05:00
agnostic-apollo
5d911ef93f Bump to v0.111 2021-04-21 07:01:59 +05:00
agnostic-apollo
1d06ff9bf0 Bump gradle to v7.0 2021-04-20 13:05:15 +05:00
agnostic-apollo
107927f5a1 Fix cases where bootstrap was not reinstalled even if PREFIX was broken
The TermuxInstaller.setupBootstrapIfNeeded() previously only checked if PREFIX directory existed or not to decide whether to install bootstrap or not. Now it will also check if its empty or only contains the tmp directory, since in that case the PREFIX must be deleted and bootstrap reinstalled, otherwise a broken environment will be loaded since no termux binaries/libs would exist.

It will now also delete any file at the prefix or staging prefix path, even if its not a directory. If the user does not want the bootstrap to be installed for some reason, then any other file other than "tmp" can be created under PREFIX.
2021-04-20 12:49:43 +05:00
agnostic-apollo
d6eb5e3511 Fix termux-reset
The TMPDIR was being automatically cleared and recreated even if it didn't already exist when TermuxService was stopped. This left an empty TMPDIR in the PREFIX directory when termux-reset was run and on termux restart the bootstrap wasn't installed again because PREFIX directory already existed. This resulted in a broken environment since no binaries/libs existed under PREFIX and /system/bin/sh was loaded.

This issue was created due to v0.109.
2021-04-20 12:39:54 +05:00
agnostic-apollo
a6ae656c9f Bump to v0.110 2021-04-15 05:14:31 +05:00
agnostic-apollo
3af5730354 Add support for allowing IDE to show documentation in .gradle files 2021-04-15 05:03:44 +05:00
agnostic-apollo
3306c3c2a2 Add support to include source jar files for libraries published by termux 2021-04-15 05:01:41 +05:00
agnostic-apollo
cde0bd2246 Prevent DebuggingPreferencesFragment and TerminalIOPreferencesFragment from being removed during minification
Fixes #2005
2021-04-15 02:15:55 +05:00
agnostic-apollo
354fe1948e Merge pull request #2002 from agnostic-apollo/termux-various-fixes-and-improvements 2021-04-13 17:18:27 +05:00
agnostic-apollo
ae1c9bacd6 Bump to v0.109 2021-04-13 16:58:18 +05:00
agnostic-apollo
d19cf435be Add ".sh" extension to apt_info_script file 2021-04-13 16:57:24 +05:00
agnostic-apollo
1132028bd2 Add TermuxTerminalViewClientBase to com.termux.shared.terminal package and extend that in TermuxTerminalViewClient 2021-04-13 16:34:24 +05:00
agnostic-apollo
b7b4a0a8a2 Move TermuxTerminalSessionClientBase to com.termux.shared.terminal package 2021-04-13 16:20:47 +05:00
agnostic-apollo
ffd8687b4c Revert "Bump to v0.109" 2021-04-13 16:09:31 +05:00
Leonid Pliushch
bbb6f4471f update bootstrap archives 2021-04-13 13:34:11 +03:00
agnostic-apollo
b33b906784 Merge pull request #1953 from agnostic-apollo/termux-various-fixes-and-improvements 2021-04-13 00:07:24 +05:00
agnostic-apollo
567eeca782 Bump to v0.109 2021-04-12 20:20:26 +05:00
agnostic-apollo
3d46849673 Replace "if(" with "if (" 2021-04-12 20:19:04 +05:00
agnostic-apollo
6293f5f170 Added APT package info when generating "Report Issue" text
This will now take a few more seconds due to "apt update" command being run.
2021-04-12 19:22:29 +05:00
agnostic-apollo
e5c5174f6f Fix TermuxActivityBroadcastReceiver wrongly designed intent actions and extras
From now on
- TERMUX_ACTIVITY.ACTION_REQUEST_PERMISSIONS should be used for requesting storage permissions.
- TERMUX_ACTIVITY.ACTION_RELOAD_STYLE should be used for reloading styling.
- TERMUX_ACTIVITY.EXTRA_RELOAD_STYLE which was previously used for requesting storage permissions if its value equaled "storage" has been deprecated.

If more actions need to be supported in future, add them to TermuxActivity.registerTermuxActivityBroadcastReceiver() IntentFilter.
2021-04-12 19:22:07 +05:00
agnostic-apollo
f1034c2e79 Add support for stdin for background RUN_COMMAND intent and TERMUX_SERVICE.ACTION_SERVICE_EXECUTE commands 2021-04-12 19:16:45 +05:00
agnostic-apollo
bbf03a0507 The TermuxConstants class has been updated to v0.19.0. Check its Changelog section for info on changes. 2021-04-12 19:16:21 +05:00
agnostic-apollo
153818f7fb Fix typos 2021-04-12 18:26:18 +05:00
agnostic-apollo
192b208883 Add support for stdin for background TermuxTasks
This will allow passing scripts (to bash or python) or other data to an executable via stdin. Arguments are passed to the executable and not the script.
2021-04-12 18:25:43 +05:00
agnostic-apollo
824b3e657f Rename termux terminal view and session client classes
- com.termux.app.terminal.TermuxViewClient has been renamed to TermuxTerminalViewClient
- com.termux.app.terminal.TermuxSessionClient has been renamed to TermuxTerminalSessionClient
- com.termux.shared.shell.TermuxSessionClientBase has been renamed to TermuxTerminalSessionClientBase

This was required because com.termux.app.terminal.TermuxSessionClient was in conflict with com.termux.shared.shell.TermuxSessionClient interface of com.termux.shared.shell.TermuxSession.
2021-04-12 15:19:25 +05:00
agnostic-apollo
a95e187b25 Assure "Process completed" string is written to terminal before session is finished 2021-04-12 14:49:49 +05:00
agnostic-apollo
f888f35e35 Fix issue where TermuxService was stopped and Termux notification canceled, if background tasks were still running but all sessions had been closed. 2021-04-12 14:43:39 +05:00
agnostic-apollo
24a5493ea5 Replace "if(" with "if (" 2021-04-12 14:32:02 +05:00
agnostic-apollo
0cd7cb8162 Improve TermuxSession and TermuxTask
TermuxSession, TermuxTask and TermuxSessionClientBase have been moved to termux-shared. They are now not dependent on TermuxService anymore and have become abstract so that they can be called from anywhere. TermuxSession.TermuxSessionClient and TermuxTask.TermuxTaskClient interfaces have been created for callbacks, which the TermuxService now implements.

The TermuxTask now also supports synchronous command execution as well to run shell commands from anywhere for internal use by termux app and its plugins.

TermuxTask now also supports killing the process being run by sending it a SIGKILL. This is used by TermuxService to kill all the TermuxTasks (in addition to TermuxSessions) it manages when it is destroyed, either by user exiting it or by android killing it. Only the tasks that were started by a plugin which **expects** the result back via a pending intent will be killed, but the remaining background tasks will keep on running until the termux app process is killed by android, like by OOM. Check TermuxService.killAllTermuxExecutionCommands() for more details on how TermuxService kills TermuxTasks and TermuxSessions.

Fixed null pointer exception when getting terminal transcript if TerminalEmulator not initialized.
2021-04-12 14:26:53 +05:00
agnostic-apollo
df4d8ac7e5 Add support for reporting issues to the termuxreports@groups.io email 2021-04-11 02:29:00 +05:00
agnostic-apollo
64fb2ce49b Update TermuxConstants
The `TermuxConstants` class has been updated to `v0.18.0`. Check its Changelog section for info on changes.
2021-04-11 02:28:00 +05:00
agnostic-apollo
0c9b85a4f9 Micro optimization for StreamGobbler 2021-04-10 02:32:21 +05:00
agnostic-apollo
62a2104adc Updated RunCommandService javadocs.
Documentation has been moved to https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent
2021-04-08 03:26:43 +05:00
agnostic-apollo
71dfefd4b7 Add termux-shared library to github workflow so that its published 2021-04-07 11:32:22 +05:00
agnostic-apollo
682ce08314 Create termux-shared library package for all termux constants and shared utils
The termux plugins should use this library instead of hardcoding "com.termux" values in their source code.

The library can be included as a dependency by plugins and third party apps by including the following line in the build.gradle where x.xxx is the version number, once its published.

`implementation 'com.termux:termux-shared:x.xxx'`

The `TermuxConstants` class has been updated to `v0.17.0`, `TermuxPreferenceConstants` to `v0.9.0` and `TermuxPropertyConstants` to `v0.6.0`. Check their Changelog sections for info on changes.

Some typos and redundant code has also been fixed.
2021-04-07 11:31:30 +05:00
agnostic-apollo
c9a476caf7 Add TermuxTaskerAppSharedPreferences to handle termux-tasker shared preferences 2021-04-06 17:23:12 +05:00
agnostic-apollo
9cee71004f Replace "if(" with "if (" 2021-04-06 17:17:12 +05:00
agnostic-apollo
81d97c3584 Fix potential TransactionTooLargeException when sharing text 2021-04-06 17:13:28 +05:00
agnostic-apollo
939338aaac Allow users to report an issue from terminal transcript by selection "Report Issue" from context menu 2021-04-06 17:09:54 +05:00
agnostic-apollo
067709bf4b Disable obfuscation of termux-app in release builds without disabling optimizations and shrinking
This will allow stacktraces to have full class and method names when a crash report is generated.
2021-04-06 16:15:50 +05:00
agnostic-apollo
69e4b575a8 Implement crash handler and reporting
Now whenever the Termux app crashes, the crash report (stacktrace, app and device info) will be logged to ~/crash_log.md file. When the user will reopen the app, a notification will be shown which when clicked will show the crash report content in the ReportActivity. The activity will have important links like email, reddit, github issues of termux app and packages at which the user can optionally report an issue if necessary after copying the crash report text. The ~/crash_log.md file will be moved to ~/crash_log-backup.md so that a notification is not shown again on next startup and can be viewed again via SAF, etc.

This will allow reports for bugs that are submitted to have complete and useful info, specially in markdown format, making lives of devs a tad bit easier. Also more bugs that are rare might be submitted since users will have the info to report with and know where to report at.

ToDo:
- The TermuxConstants.TERMUX_SUPPORT_EMAIL_URL needs to be updated with a valid support email once its set up. The TermuxUtils.getReportIssueMarkdownString() function currently also has "email" lines commented out which will need to be uncommented.
- Currently, crashes will only be handled for the main app thread, other threads will have to manually hooked into where necessary.
2021-04-06 16:15:00 +05:00
agnostic-apollo
07e6ecd3c3 Update TermuxConstants
The `TermuxConstants` classes has been updated to `v0.16.0`. Check its Changelog section for info on changes.
2021-04-06 16:13:28 +05:00
agnostic-apollo
325a6f7d66 Fix SettingsActivity shortcut not showing on android 7 and fixed shortcut actions and categories 2021-04-06 13:27:43 +05:00
agnostic-apollo
cf5bb69fc8 Add crash_report_notifications_enabled shared preferences
This will allow users to control if a notification should be shown with the crash info when app is restarted after a crash

The `TermuxPreferenceConstants` classes has been updated to `v0.8.0`. Check its Changelog section for info on changes.
2021-04-06 12:48:56 +05:00
agnostic-apollo
18b004a2ba Fix Logger function modifier order 2021-04-06 12:23:00 +05:00
agnostic-apollo
38323b1c2a Fix RUN_COMMAND Intent plugin api url 2021-04-06 12:21:22 +05:00
agnostic-apollo
8a5442f80d Update TermuxConstants
The `TermuxConstants` classes has been updated to `v0.15.0`. Check its Changelog sections for info on changes.
2021-04-06 12:19:37 +05:00
agnostic-apollo
8598b92dea Use FileUtils for clearing TMPDIR and move clearing function to ShellUtils from TermuxService 2021-04-06 06:00:41 +05:00
agnostic-apollo
b2cd20c035 Use FileUtils for bootstrap and shared storage symlinks setup 2021-04-06 06:00:21 +05:00
agnostic-apollo
d4fc34ca2d Move FileUtils to file package and define more file util functions
A lot of utils have been defined now that can be used to safely manage files.

The java java.io.File API has poor support for detecting symlinks including broken symlinks. Android implementation also has issues. Check FileTypes.getFileType() function for more info. For this reason, the UnixFileAttributes and related classes has been ported from AOSP to get file attributes and type.

Some file utils and android versions use google's Guava com.google.common.io.MoreFiles library for managing files, specially for safer directory deletion with SecureDirectoryStream.

Some file utils and android versions use org.apache.commons.io.FileUtils for managing files. The library version used is 2.5 and it must not be incremented for compatibility with android version < 8, otherwise runtime crashes will occur.
2021-04-06 06:00:05 +05:00
agnostic-apollo
c0323fe816 Move ShellUtils to shell package 2021-04-05 23:26:47 +05:00
agnostic-apollo
a32309827f Update gradle version to 4.1.3 2021-04-05 23:22:04 +05:00
agnostic-apollo
ada678dfe2 Move get*LogStringEntry() functions to logger class 2021-04-05 23:21:13 +05:00
agnostic-apollo
cdbd38faaa Move StreamGobbler to shell package 2021-04-05 23:18:43 +05:00
agnostic-apollo
d4653d0590 Get TERMUX_VERSION while building shell environment via Termux package context 2021-03-29 07:20:39 +05:00
agnostic-apollo
49f53f55f3 Renamed TextDataUtils to DataUtils 2021-03-28 09:08:45 +05:00
agnostic-apollo
15eb56d4dd Add PackageUtils and fix ReportActivity
- PackageUtils has been added to get various package related info. This will be used to get info based on Context objects instead of using BuildConfig which wouldn't have been available across termux plugins.

- Support for getting Context objects of all termux plugin apps have been added to TermuxUtils.

- Support for showing more details for the app has been added for ReportActivity. This will also allow app info of Termux app to be generated when TermuxUtils.getAppInfoMarkdownString() is called by a termux plugin so that both are shown so that devs/users can more easily detect compatibility issues.

- ReportActivity has been fixed to also include report and device info instead of just the ExecutionCommand info when copying and sharing.

- Moved the generation of markdown for ReportInfo to its own class and added creationTimestamp field.

- Increased markdown headings size for some cases.
2021-03-28 09:06:17 +05:00
agnostic-apollo
d7ea770d47 Move com.termux.models to com.termux.app.models package 2021-03-27 19:08:26 +05:00
agnostic-apollo
2a8d5e292d Add support for disabling soft keyboard completely with the "soft_keyboard_enabled" SharedPreferences key
Users can toggle the state from Settings -> Keyboard I/O -> Soft Keyboard toggle.

Android phone should also have an internal setting for disabling soft keyboard when a hardware keyboard is connected in Language and Input android settings or from the input mode selection notification, but the above setting will be Termux app specific and will allow soft keyboard to still be shown in other apps.

The `TermuxPreferenceConstants` classes has been updated to `v0.7.0`. Check its Changelog section for info on changes.
2021-03-27 07:55:21 +05:00
agnostic-apollo
04da4b2268 Send PendingIntent back for pre-execution errors generated for RUN_COMMAND intent
If the pending intent is not null, then the errors will be sent back to the caller without notifying the user, since we will let the caller handle the errors himself. They will still be logged in logcat.

However, if "allow-external-apps" is not true, then a flash and notification will be shown forcefully (regardless of "Plugin Execution Errors" toggle state), so that the user knows someone tried to run a command in termux context, since it may be malicious app or imported (tasker) plugin project and not the user himself. If a pending intent is also sent, then its creator is also logged and shown.
2021-03-26 16:33:46 +05:00
agnostic-apollo
34bacfd5b1 Fix terminal-emulator constructor used in tests 2021-03-26 02:11:09 +05:00
agnostic-apollo
006d5abb78 Disable markdown text selection for now since it stops URL clicks from working 2021-03-26 01:26:53 +05:00
agnostic-apollo
480bad181b Use TermuxConstants for getting urls instead of using hardcoded strings 2021-03-26 01:25:15 +05:00
agnostic-apollo
2afa4b4351 Update TermuxConstants
The `TermuxConstants` classes has been updated to `v0.14.0`. Check its Changelog sections for info on changes.
2021-03-25 23:30:36 +05:00
agnostic-apollo
a2209ddd5e Add support for sending back background and foreground command results for RUN_COMMAND intent and foreground command results for Termux:Tasker
Previously, termux only supported getting result of BACKGROUND commands back if they were started via Termux:Tasker plugin. Getting back result of foreground commands was not possible with any way.

Now with RUN_COMMAND intent or Termux:Tasker, the third party apps and users can get the foreground command results as well. Note that by "foreground results" we only mean the session transcript. The session transcript will contain both stdout and stderr combined, basically anything sent to the the pseudo terminal /dev/pts, including PS1 prefixes for interactive sessions. Getting separate stdout and stderr can currently only be done with background commands.

Moreover, with RUN_COMMAND intent, third party apps and users can get the background commands results as well. This means separate extras for stdout and stderr.

The exit code will also be returned for either case.

### RUN_COMMAND intent

The result extras are returned in the TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE bundle via the pending intent received.

The RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT extra can be used to send the pending intent with which termux should return the result bundle. The pending intent can be received back by the app with an IntentService. Check RunCommandService for reference implementation.

For foreground commands (RUN_COMMAND_SERVICE.EXTRA_BACKGROUND is false):
- EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT will contain session transcript.
- EXTRA_PLUGIN_RESULT_BUNDLE_STDERR will be null since its not used.
- EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE will contain exit code of session.

For background commands (RUN_COMMAND_SERVICE.EXTRA_BACKGROUND is true):
- EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT will contain stdout of commands.
- EXTRA_PLUGIN_RESULT_BUNDLE_STDERR will contain stderr of commands.
- EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE will contain exit code of command.

The internal errors raised by termux outside the shell will be sent in the the EXTRA_PLUGIN_RESULT_BUNDLE_ERR and EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG extras. These will contain errors like if starting a termux command failed or if the user manually exited the termux sessions or android killed the termux service before the commands had finished executing. The err value will be Activity.RESULT_OK(-1) if no internal errors are raised.

The stdout and stderr will be truncated from the start to max 100KB combined and errmsg will also be truncated from end to max 25KB. This is necessary to prevent TransactionTooLargeException exceptions from being raised if stdout or stderr are too large in length. The original length of stdout and stderr will be provided in EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH and EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH extras respectively, so that the caller can check if either of them were truncated.

### Termux:Tasker

Support for Termux:Tasker for getting back result of foreground commands will require an update to it since it currently immediately returns control to plugin host app like Tasker without waiting if a foreground command is to be executed.
2021-03-25 23:05:55 +05:00
agnostic-apollo
2cc6285a81 Remove leftover log entry from TermuxSessionClient 2021-03-25 23:01:45 +05:00
agnostic-apollo
5dee839230 Update TermuxConstants
The `TermuxConstants` classes has been updated to `v0.13.0`. Check its Changelog sections for info on changes.
2021-03-25 21:17:54 +05:00
agnostic-apollo
1ef8eb9219 Add hide-soft-keyboard-on-startup property
If its set to `true` in termux.properties file, then soft keyboard will automatically be hidden on Termux App start to solve issues for when users use hardware keyboard and soft keyboard is automatically opened and wastes terminal screen space.

The `TermuxPropertyConstants` classes has been updated to `v0.5.0`. Check its Changelog sections for info on changes.

Fixes #1978
2021-03-25 12:30:59 +05:00
agnostic-apollo
d3ddb21716 Warn users if Draw over other apps permission is missing
For android version >= 10(Q), a flash will be shown to users requesting them to grant the permission if they attempt to start a foreground terminal session command from background, like with the RUN_COMMAND intent. The flash will only be shown if "Plugin Error Notifications" toggle is enabled in settings.
2021-03-25 12:04:09 +05:00
agnostic-apollo
8e80e889f0 Set compileSdkVersion (NOT targetSdkVersion) to 29 so that Build.VERSION_CODES.Q checks can be made 2021-03-25 11:52:26 +05:00
agnostic-apollo
4e5d14e4a2 Use NotificationUtils to handle TermuxService and RunCommandService notifications 2021-03-25 11:00:00 +05:00
agnostic-apollo
0230698494 Fix unintentional wordwrap of ExecutionCommand arguments markdown 2021-03-25 10:11:43 +05:00
agnostic-apollo
977cb34fc7 Provide better errmsg if executable passed to RunCommandService is null or empty
Previously, the null or empty executable would be expanded to the literal root "/" string path by FileUtils.getCanonicalPath and then FileUtils.validateRegularFileExistenceAndPermissions() validation would fail since path will not be a regular file. So a user will be shown that "/" is not a regular file. Now we show that executable was not even passed.
2021-03-25 10:10:19 +05:00
agnostic-apollo
f62febbfb7 Add the TermuxTask class for linking a Process to an ExecutionCommand.
TermuxTask will maintain info for background Termux tasks. Each task started by TermuxService will now be linked to a ExecutionCommand that started it.

- StreamGobbler class has also been imported from https://github.com/Chainfire/libsuperuser and partially modified to read stdout and stderr of background commands. This should likely be much safer and efficient.
- Logging of every line has been disabled unless log level is set to verbose. This should have a performance increase and also prevent potentially private user data to be sent to logcat.
- This also solves 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 also solves the bug where stdout or stderr were too large in size and TransactionTooLargeException exception was raised and result TERMUX_SERVICE.EXTRA_PENDING_INTENT pending intent failed to be sent to the caller. This would have also hung up Termux:Tasker. Now the stdout and stderr sent back in TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE bundle will be truncated from the start to max 100KB combined. The original size of stdout and stderr will be provided in TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH and TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH extras respectively so that the caller can check if either of them were truncated. The errmsg will also be truncated from end to max 25KB to preserve start of stacktraces.
- The PluginUtils.processPluginExecutionCommandResult() has been updated to fully handle the result of plugin execution intents.
2021-03-25 09:47:59 +05:00
agnostic-apollo
7ca20fdeb3 Update ExecutionCommand to make current and previous state private to prevent direct modification
- Helper functions are now provided to check for common states. The currentState and previousState must only be modified via setState()
- Some errCode values are also provided to prevent hardcoded value usage.
- The stdout, stderr and arguments will now be truncated for logcat to honour its LOGGER_ENTRY_MAX_LEN limits
2021-03-25 09:20:09 +05:00
agnostic-apollo
922f4f4ae5 Update TextDataUtils.getTruncatedCommandOutput() to support truncation from start or end and for (truncated) prefix 2021-03-25 09:08:45 +05:00
agnostic-apollo
df03f0b7d6 Update TermuxConstants
The `TermuxConstants` classes has been updated to `v0.12.0`. Check its Changelog sections for info on changes.
2021-03-25 09:02:59 +05:00
agnostic-apollo
249f7c6b7c Remove BackgroundJob 2021-03-25 09:02:35 +05:00
agnostic-apollo
78a99fddfd Add the TermuxSession class for linking a TerminalSession to an ExecutionCommand.
TermuxSession will maintain info for foreground Termux sessions. Each terminal session started by TermuxService will now be linked to a ExectionCommand that started it.

This also fixes bugs where newly created session in some cases were not being automatically selected and scrolled to, like when adding a named or failsafe session or those which were created for executable intents.
2021-03-25 01:18:13 +05:00
agnostic-apollo
dff2794501 Add ShellUtils to define shell related utils since they don't belong in BackgroundJob 2021-03-25 01:13:51 +05:00
agnostic-apollo
3e0f74a894 Fixed keep screen on default value
The `TermuxPreferenceConstants` classes has been updated to `v0.6.0`. Check its Changelog sections for info on changes.
2021-03-24 06:52:17 +05:00
agnostic-apollo
2b3f681723 Add SettingsActivity to launcher shortcuts 2021-03-24 06:48:47 +05:00
agnostic-apollo
d3d6731d6f Fix typos 2021-03-24 06:47:12 +05:00
agnostic-apollo
9bbcc08ff3 Fix variable prefix for intent extra 2021-03-24 06:33:09 +05:00
agnostic-apollo
7aa957c684 Fix typos 2021-03-24 06:30:26 +05:00
agnostic-apollo
eeb8554535 Fix string resources naming convention 2021-03-24 06:15:45 +05:00
agnostic-apollo
4eced52c5f Fix xml files naming convention 2021-03-24 05:28:38 +05:00
agnostic-apollo
b856e16998 Move activities and fragments to respective packages
Move com.termux.TermuxSettingsActivity to com.termux.app.activities.SettingsActivity
Move com.termux.TermuxHepActivity to com.termux.app.activities.HelpActivity
Move com.termux.settings.DebuggingPreferencesFragment to com.termux.app.fragments.settings.DebuggingPreferencesFragment
2021-03-24 05:13:00 +05:00
agnostic-apollo
1bdf9bf2e3 Change log level to warn from error when termux.properties file is missing 2021-03-24 04:57:35 +05:00
agnostic-apollo
92b804dc9c Add logging for termux bootstrap package installation and setup of storage symlinks 2021-03-24 04:55:10 +05:00
agnostic-apollo
1b5e5b56cb Partially integrate ExectionCommand into TermuxService and BackgroundJob
The TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent received will be managed by the ExectionCommand now.
The cwd and failsafe have been renamed to workingDirectory and isFailsafe.
2021-03-24 03:59:18 +05:00
agnostic-apollo
31371b5e3d Fully integrate ExectionCommand into RunCommandService
Users will now also be shown flashes and notifications in addition to log entries for missing allow-external-apps permission or for invalid extras passed like the executable. The flashes and notifications can be controlled with the Termux Settings -> Debugging -> Plugin Error Notifications toggle
2021-03-24 03:56:25 +05:00
agnostic-apollo
ef1ab197b6 Update TermuxConstants
The `TermuxConstants` classes has been updated to `v0.11.0`. Check its Changelog sections for info on changes.
2021-03-24 03:51:57 +05:00
agnostic-apollo
bccc35bc3f Added ExectionCommand
ExectionCommand is a class that stores all data related to an execution command like:

- Input parameters like executable and arguments to be used to run the shell command, etc
- Output parameters like stdout, stderr and exitCode.
- Error info generated internally by termux outside the shell in errCode and errmsg.
- Command info like, id, label, description, help info, etc.
- Other config info like for how termux should handle the command.
- The pending intent if any that should be sent after execution to command requester.
- The help for the plugin API that was used to send the intent.
- Current and previous state of the command.

This allow easier management and passing of execution command data between classes and management of it. This will later allow each ExectionCommand command to be linked to a Terminal Session, to handle post processing and failure management.
The ExectionCommand also provides functions to get its data in markdown format, which can be used by failure or success reports generated for the command that are shown to the user. The commandHelp and pluginAPIHelp can also be specially useful to provide info to users on how to manage failures that are generated.
2021-03-24 03:48:24 +05:00
agnostic-apollo
20d20f42c0 Added NotificationUtils to build and manage notifications
The `TermuxPreferenceConstants` classes has been updated to `v0.5.0`. Check its Changelog sections for info on changes.
2021-03-24 03:25:25 +05:00
agnostic-apollo
9d36e9adde Add ReportActivity and ReportInfo
This implements the framework to report info to users. This may include reporting failure or result of commands or any exceptions that are raised.

The ReportInfo provides 5 fields:
- userAction: The user action that was being processed for which the report was generated.
- sender: The internal app component that sent the report.
- title: The report title.
- reportString: The markdown text for the report.
- addReportAndDeviceDetails: If set to true, then report and device details will be added to the report.

This should provide the basics parameters for showing a report to the user. The ReportActivity also allows user to copy and share the report.

In future this can also be used to allow users to easily email or post crash reports to github for Termux app crashes instead of going through logcat.
2021-03-24 03:22:12 +05:00
agnostic-apollo
3491956385 Update TermuxUtils to add support for getting device and termux info as markdown 2021-03-24 02:38:37 +05:00
agnostic-apollo
134e21765c Update Logger
- Add support to log Throwables instead of just Exception class objects.
- Adds utility functions to get Throwables as Strings and Markdown Strings.
2021-03-24 02:34:53 +05:00
agnostic-apollo
c28990a176 Add ShareUtils 2021-03-24 02:31:22 +05:00
agnostic-apollo
131f481750 Add commonmark-spec markdown support with markwon library
Also adds MarkdownUtitls to provide various utils for markdown processing.
2021-03-24 02:30:20 +05:00
agnostic-apollo
f393e9b2cf Allow mPropertiesList to be null for SharedProperties
This will allow all keys that are read from file to be stored in mMap without knowing their names beforehand.
2021-03-24 02:27:28 +05:00
agnostic-apollo
8612a1d0f8 Added plugin_error_notifications_enabled preference
This will allow user to control whether flashes and notifications for plugin errors are enabled or not.

The `TermuxPreferenceConstants` classes has been updated to `v0.4.0`. Check its Changelog sections for info on changes.
2021-03-19 22:49:06 +05:00
agnostic-apollo
7d53a147d8 Reordered TermuxService.actionAcquireWakeLock() 2021-03-19 21:18:39 +05:00
agnostic-apollo
5d6a98452a Added TERMUX_SERVICE#ACTION_SERVICE_EXECUTE Intent Logging 2021-03-19 21:18:37 +05:00
agnostic-apollo
b4995ef9a7 Refactor RunCommandService
- The `FileUtils` and `PluginUtils` have been added to provide utility functions.
- The executable and working directory validation has been added to check for existence and missing permissions.
- The `expandPath()` function is removed from `RunCommandService`.
- Working directory will automatically be created if under `TermuxConstants.TERMUX_FILES_DIR_PATH` if missing.
- Better logging has been added. This will later be used to notify the user in foreground.
- Javadocs have been updated.
2021-03-19 21:18:35 +05:00
agnostic-apollo
ec7568d28e Add support for session actions for foreground session commands
The `TERMUX_SERVICE.EXTRA_SESSION_ACTION` extra can be passed to define what should happen when a foreground session
command is received for the `TERMUX_SERVICE.ACTION_SERVICE_EXECUTE` intent to `TermuxService`, like from `RunCommandService` or `Termux:Tasker`. The user can define whether the new session should be automatically switched to or if existing session should remain as the current session. The user can also define if foreground session commands should open the `TermuxActivity` or if they should run in the "background" in the Termux notification. The user can click the notification to open the sessions. Check `TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION*` values to see various behaviors.

This also solves the old "issue" that if a foreground command was received while an existing session was already in the foreground, the new session won't be switched to automatically. It only brought the new session to the foreground if the activity was not already in foreground, since a call to `mTermuxSessionClient.setCurrentSession(newSession)` wasn't being made.
2021-03-19 21:18:33 +05:00
agnostic-apollo
607ba3ee56 Update TermuxConstants
The `TermuxConstants` classes has been updated to `v0.10.0`. Check its Changelog sections for info on changes.
2021-03-19 21:18:31 +05:00
agnostic-apollo
ae260fad9c Fix old bug where termux app would crash if sessions list ListView was not notified of new sessions
To reproduce:
1. Create 2 sessions.
2. From either session, run a random `RUN_COMMAND` intent command with `am` command and shift to the other session.

Termux app would crash and throw the `The content of the adapter has changed but ListView did not receive a notification.` exception. TermuxService was previously not notifying the ListView of the sessions list that a new session has been added, if the activity was in foreground.
2021-03-19 21:18:27 +05:00
agnostic-apollo
d9b5344b24 Refactor TermuxService
- Dedicated functions have been created for various actions and commands.
- The `startForeground()` call will be made both on `onCreate()` and `onStartCommand().
- The `stopForeground()` call will be made before every `onSelf()` call.
- The references to `TermuxActivity` will now be removed in `onUnbind()` as well if activity `onDestroy()` failed to do so.
- Appropriate log entries are added to help debug issues.
2021-03-19 21:18:25 +05:00
agnostic-apollo
bc02508102 Fix dependencies ordering 2021-03-19 21:18:24 +05:00
agnostic-apollo
f969c01f7e Update TermuxConstants
The `TermuxConstants` classes has been updated to `v0.9.0`. Check its Changelog sections for info on changes.
2021-03-19 21:18:21 +05:00
agnostic-apollo
fb6e9b69ab Update TermuxConstants
The `TermuxConstants` classes has been updated to `v0.8.0`. Check its Changelog sections for info on changes.
2021-03-19 21:18:13 +05:00
agnostic-apollo
c1a377d15d Update SharedPreferenceUtils
This SharedPreferenceUtils now supports:
- Getting `Context.MODE_PRIVATE` and/or `Context.MODE_MULTI_PROCESS` `SharedPreference` instances.
- Setting values to shared preferences in-memory cache and the file synchronously.
- Getting and setting `float`, `long` and `Set<Sting>` values in shared preferences.
2021-03-17 03:16:37 +05:00
agnostic-apollo
8c1c7ce727 Update TermuxConstants
The `TermuxConstants` classes has been updated to `v0.7.0`. Check its Changelog sections for info on changes.
2021-03-17 01:13:44 +05:00
agnostic-apollo
2a940dfca6 Move general static functions from TermuxSharedProperties to SharedProperties class
This will allow `SharedProperties` class to be used directly to get property values and their internal values via static functions instead of using `TermuxSharedProperties` where the functions don't belong in.

The `TermuxPropertyConstants` classes has been updated to `v0.4.0`. Check its Changelog sections for info on changes.
2021-03-16 22:52:20 +05:00
agnostic-apollo
2e7fd480f4 Update TermuxConstants
The `TermuxConstants` classes has been updated to `v0.6.0`. Check its Changelog sections for info on changes.
2021-03-16 21:29:25 +05:00
agnostic-apollo
9e82561804 Added SharedPreferenceUtils
The `SharedPreferenceUtils` class has been added to provide static util functions for shared preferences like get and set while safely handling exceptions. The `TermuxSharedPreferences` class has been renamed to `TermuxAppSharedPreferences` since its to be used to handle only Termux App related shared preferences and not of other plugin apps. For plugin apps, separate classes can be created. However, Termux app and its plugins will share the same `TermuxPreferenceConstants` class that now has per app scoping since `v0.3.0`.
2021-03-16 20:51:41 +05:00
agnostic-apollo
4427c1675d Update TermuxConstants
The `TermuxConstants` classes has been updated to `v0.5.0`. Check its Changelog sections for info on changes.
2021-03-16 19:07:54 +05:00
agnostic-apollo
6f6d11fbb8 Updated TermuxPreferenceConstants
The `TermuxPreferenceConstants` classes has been updated to `v0.3.0`. Check its Changelog sections for info on changes.
2021-03-16 19:06:17 +05:00
agnostic-apollo
ff0440d7d2 Allow users to adjust termux toolbar height with termux.properties
This `terminal-toolbar-height` key can be used to adjust the toolbar height. The user can set a float value between `0.4` and `3.0` which will be used as the scaling factor for the default height. The default scaling factor is `1`. So adding an entry like `terminal-toolbar-height=2.0` to `termux.properties` file will make the toolbar height twice its original height. Running `termux-reload-settings` command will also update the height instantaneously if changed.

Fixes #1857
2021-03-16 07:04:55 +05:00
agnostic-apollo
c9e18e5b93 Refactor TermuxActivity
This commit majorly refactors `TermuxActivity` and moves its view components and functions into dedicated classes.

- The view layouts and ids have been given meaningful names, like `termux_activity.xml`.
- The `TerminalToolbarViewPager` class has been created to handle the now called toolbar that shows on the bottom of the terminal view. It currently contains extra keys view defined by `terminal_toolbar_extra_keys_view.xml` file and a text input view defined by `terminal_toolbar_text_input_view.xml` file when user can switch to by swiping left. The input text will now be preserved if android destroys the activity or its recreated.
- The `TermuxSessionsListViewController` class has been created to handle view related functionality of the termux sessions list shown in the left drawer, namely view creation, `onItemClick()`, `onItemLongClick()`, etc. Its list view is defined by `termux_activity.xml` file and each item's layout is defined by the `terminal_sessions_list_item.xml` file.
- The `TextDataUtils` class has been added to the `com.termux.app.utils` package for text utils.
- The design for the `SessionChangedCallback` interface for `TerminalSession` has been majorly changed. Firstly, it has been renamed and moved from `TerminalSession` to the dedicated `TerminalSessionClient` class file. The interface now also supports the termux app centralized logging framework so that `TerminalSession` and `TerminalEmulator` can use them. Previously, `TermuxService` was implementing a wrapper interface, which would then call the real interface defined by the `TermuxActivity` if it was currently bound to the service. This cluttered and partially duplicated the code. Now, the implementation is defined by the `TermuxSessionClientBase` and `TermuxSessionClient` classes. The `TermuxSessionClientBase` implements the `TerminalSessionClient` interface but the definition of the activity related functions do not do anything, only the background ones like the logging functions are fully implemented. The `TermuxSessionClient` class inherits from the `TermuxSessionClientBase` class and provides the implementation for the activity related functions. The design for how this works is that if the `TermuxService` is not bound to `TermuxActivity`, it just passes the `TermuxSessionClientBase` implementation to `TerminalSession`. If the activity is bound at some point, then in `onServiceConnected()` it replaces/updates the client objects stored in `TerminalSession` and `TerminalEmulator` with `TermuxSessionClient`, and then replaces them back with `TermuxSessionClientBase` in `onDestroy()`. This seems to be working for now without an issue.
2021-03-16 05:01:09 +05:00
agnostic-apollo
5e0b29bb6d Move DialogUtils to com.termux.app.utils package 2021-03-16 04:07:01 +05:00
agnostic-apollo
73af5c2326 Move terminal io related classed to com.termux.app.terminal.io package 2021-03-16 04:05:15 +05:00
agnostic-apollo
7da847a485 Update TermuxConstants
The `TermuxConstants` classes has been updated to `v0.4.0`. Check its Changelog sections for info on changes.
2021-03-16 03:59:54 +05:00
agnostic-apollo
11a236a172 Update TerminalView and TerminalViewClient javadocs 2021-03-16 03:46:34 +05:00
agnostic-apollo
3b5d3114a6 Prefix DEFAULT_LOG_TAG even when a tag is passed while logging 2021-03-16 03:44:45 +05:00
agnostic-apollo
66f15d2a08 Remove moved files 2021-03-16 03:44:27 +05:00
agnostic-apollo
0225a8b1fc Fix hardcoded value in strings.xml 2021-03-16 03:41:17 +05:00
agnostic-apollo
d39972b3bf Implement GUI based Termux settings manager and a centralized logging framework
The settings activity can be accessed by long pressing on terminal view and selecting "Settings" from the popup shown. It uses the Android's Preference framework. Currently only debugging preferences to set log level and enabling terminal view key logging are provided. The Preference framework by default uses the keys set in `app:key` attribute in the respective preferences XML file to store the values in the default `SharedPreferences` file of the app. However, since we rely on `TermuxPreferenceConstants` and `TermuxPropertyConstants` classes to define key names so that they can be easily shared between termux and its plugin apps, we provide our own `PreferenceDataStore` for storing key/value pairs. The key name in the XML file can optionally be the same. Check `DebuggingPreferencesFragment` class for a sample. Each new preference category fragment should be added to `app/settings/` with its data store.

This commit may allow support to be added for modifying `termux.properties` file directly from the UI but that requires more work, since writing to property files with comments require in-place modification.

The `Logger` class provides various static functions for logging that should be used from now on instead of directly calling android `Log.*` functions. The log level is automatically loaded from shared preferences at application startup via `TermuxApplication` and set in the static `Logger.CURRENT_LOG_LEVEL` variable. Changing the log level through the settings activity also changes the log level immediately.

The 4 supported log levels are:
- LOG_LEVEL_OFF which will log nothing.
- LOG_LEVEL_NORMAL which will start logging error, warn and info messages and stacktraces.
- LOG_LEVEL_DEBUG which will start logging debug messages.
- LOG_LEVEL_VERBOSE which will start logging verbose messages.

The default log level is `LOG_LEVEL_NORMAL` which will not log debug or verbose messages. Contributors can add useful log entries at those levels where ever they feel is appropriate so that it allows users and devs to more easily help solve issues or find bugs, specially without having to recompile termux after having to manually add general log entries to the source. DO NOT log data that may have private info of users like command arguments at log levels below debug, like `BackgroundJob` was doing previously.

Logging to file support may be added later, will require log file rotation support and storage permissions.
2021-03-13 16:49:29 +05:00
agnostic-apollo
93b506a001 Move Termux Preferences to com.termux.app.settings.preferences package
The termux preferences handling was mixed in with termux properties before an earlier commit. They are now moved out of into a separate sub package, the following classes are added:

- `TermuxPreferenceConstants` class that defines shared constants of the preferences used by Termux app and its plugins. This class should be imported by other termux plugin apps instead of copying and defining their own constants.
- `TermuxSharedPreferences` class that acts as manager for handling termux preferences.
2021-03-12 06:01:31 +05:00
agnostic-apollo
ebf2e472b3 Fix typo in TermuxPropertyConstants 2021-03-12 05:54:12 +05:00
agnostic-apollo
e72841ba27 Update TermuxConstants
The `TermuxConstants` classes has been updated to `v0.3.0`. Check its Changelog sections for info on changes.
2021-03-12 05:53:10 +05:00
agnostic-apollo
5fd91b4f92 Rename com.termux.app.input package to com.termux.app.terminal
Also moves `TermuxViewClient` into com.termux.app.terminal package
2021-03-12 00:07:35 +05:00
agnostic-apollo
10d6eaa5d1 Make TerminalView agnostic of "termux.properties" files.
`TerminalView` will use the `TerminalViewClient` interface implemented by `TermuxViewClient` in termux-app to get "enforce-char-based-input" and "ctrl-space-workaround" property values. It will also not read the file every time it needs to get the property value and will get it from the in-memory cache of `TermuxSharedProperties`.
2021-03-11 21:06:42 +05:00
agnostic-apollo
319446fc15 Disable TerminalView logging. 2021-03-11 20:30:52 +05:00
agnostic-apollo
36be41d0de Remove function that reads the "termux.properties" files from RunCommandService
The `RunCommandService` will now call the `TermuxSharedProperties` for getting current value of `allow-external-apps`, instead of using its own duplicated function to read "termux.properties" files.
2021-03-11 20:30:16 +05:00
agnostic-apollo
ef9e406300 Update TermuxConstants
The `TermuxConstants` and `TermuxPropertyConstants` classes have both been updated to `v0.2.0`. Check their Changelog sections for info on changes.
Some other hardcoded termux paths have been removed as well and are now referenced from `TermuxConstants` class.
2021-03-11 20:11:59 +05:00
agnostic-apollo
7b4acb53c9 Move Termux Properties to com.termux.app.settings.properties package
The termux properties handling was mixed in with termux preferences. They are now moved out of into a separate sub package, the following classes are added:

- `SharedProperties` class which is an implementation similar to android's `SharedPreferences` interface for reading from ".properties" files which also maintains an in-memory cache for the key/value pairs. Two types of in-memory cache maps are maintained, one for the literal `String` values found in the file for the keys and an additional one that stores (near) primitive `Object` values for internal use by the caller. Write support is currently not implemented, but may be added if we provide users a GUI to modify the properties. We cannot just overwrite the ".properties" files, since comments also exits, so in-place editing would be required.
- `SharedPropertiesParser` interface that the caller of `SharedProperties` must implement. It is currently only used to map `String` values to internal `Object` values.
- `TermuxPropertyConstants` class that defines shared constants of the properties used by Termux app and its plugins. This class should be imported by other termux plugin apps instead of copying and defining their own constants.
- `TermuxSharedProperties` class that acts as manager for handling termux properties. It implements the `SharedPropertiesParser` interface and acts as the wrapper for the `SharedProperties` class.
2021-03-11 18:22:11 +05:00
agnostic-apollo
dbf84773d4 Fix potential null exceptions 2021-03-10 02:33:45 +05:00
agnostic-apollo
324a69f7fa Move terminal input related classed to com.termux.app.input package 2021-03-10 02:32:33 +05:00
agnostic-apollo
14c49867f7 Define TermuxConstants class to store all shared constants of Termux app and its plugins
This commit removes almost all hardcoded paths in Termux app and moves the references to the `TermuxConstants` class.

The `TermuxConstants` class should be imported by other termux plugin apps instead of copying and defining their own constants. The 3rd party apps can also import it for interacting with termux apps. App and sub class specific constants are defined in their own nested classes to keep them segregated from each other and for better readability.
2021-03-08 15:09:22 +05:00
agnostic-apollo
395759cc0a Restore keyboard input into terminal view when toggling extra-keys slider input with VOL_UP+q
Check #1420 for details.
2021-03-06 18:27:57 +05:00
agnostic-apollo
ada5087f67 Partial refactor of the mess that is TerminalView
- Decouple the `CursorController`, `TextSelectionCursorController`(previously `SelectionModifierCursorController`) and `TextSelectionHandleView` (previously `HandleView`) from `TerminalView` by moving them to their own class files.
- Fixes #1501 which caused the `java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.` exception to be thrown when long pressing the down key while simultaneously long pressing the terminal view for text selection.
2021-03-06 18:25:10 +05:00
agnostic-apollo
93a5bf8d29 Request android.permission.REQUEST_INSTALL_PACKAGES permission
This will allow users to access `Android/obb` on android 11 after explicitly granting Termux the permission by going to Termux `App Info` in Android `Settings` -> `Advance` -> `Install unknown apps`.

https://medium.com/androiddevelopers/android-11-storage-faq-78cefea52b7c
2021-03-02 16:48:15 +05:00
agnostic-apollo
356a442c6f Request android.permission.DUMP permission
This will allow users to run `dumpsys` command after running `adb shell pm grant com.termux android.permission.DUMP`.

https://developer.android.com/studio/command-line/dumpsys
2021-03-01 12:40:04 +05:00
agnostic-apollo
9fd2cf9834 Fix NoClassDefFoundError exceptions for TermuxActivity
This commit fixes the non-crashing exception `Rejecting re-init on previously-failed class java.lang.Class<androidx.core.view.ViewCompat$2>: java.lang.NoClassDefFoundError: Failed resolution of: Landroid/view/View$OnUnhandledKeyEventListener;` on termux startup due to `setContentView()` call by `TermuxActivity.onCreate()`. The recommended solution seems to be to add `androidx.core:core` dependency, which has solved the issue.

https://issuetracker.google.com/issues/117685087
2021-02-27 13:30:11 +05:00
agnostic-apollo
5a96075025 Update RunCommandService documentation 2021-02-27 13:23:37 +05:00
agnostic-apollo
85b2c44ac7 Fix current working directory default value
This commit fixes the issue when `cwd` is empty and is passed to `Runtime.getRuntime().exec(progArray, env, new File(cwd));`, it raises the `No such file or directory` exceptions when targeting sdk `29`.
2021-02-27 13:22:09 +05:00
agnostic-apollo
108e4cb391 Fix workdir logic in RunCommandService
This commit fixes the workdir logic to not send `EXTRA_CURRENT_WORKING_DIRECTORY` extra to `TermuxService` if workdir is empty, since that will raise `No such file or directory` exceptions if `cwd` is empty when targeting sdk `29`.
2021-02-27 13:14:13 +05:00
agnostic-apollo
80858bab6b Fix path expansion in RunCommandService
This commit fixes `getExpandedTermuxPath()` (previously `parsePath()`) not expanding path if exactly `$PREFIX` is passed and addition of extra trailing slashes in some cases.
2021-02-27 13:10:57 +05:00
agnostic-apollo
00194ebb90 Add logcat errors in RunCommandService
This commit adds `logcat` errors if an invalid intent action is passed or if `allow-external-apps` is not set to `true` while sending an intent to `RunCommandService`, so that users can detect issues.
2021-02-27 13:07:07 +05:00
agnostic-apollo
f50d15d353 Fix "Duplicate finish request for ActivityRecord" errors 2021-02-26 17:09:04 +05:00
Leonid Pliushch
edcebf1336 version 0.108 2021-02-19 01:54:46 +02:00
Leonid Pliushch
fcfd131ccd update bootstrap archives 2021-02-19 01:54:26 +02:00
Leonid Pliushch
ce5e9a9042 Revert "app: enable split apk builds"
Not compatible with F-Droid.

If you can fix F-Droid builds, send pull requests. For now I'm disabling this.

This reverts commit a7eb173178.
2021-02-19 01:45:39 +02:00
Leonid Pliushch
7884cb3bea github actions: test run for publishing maven packages 2021-02-11 23:10:44 +02:00
Leonid Pliushch
d8fcc1f221 github actions: add workflow for publishing terminal-view/terminal-emulator on Github Packages 2021-02-11 23:08:15 +02:00
Leonid Pliushch
397a78f248 publish termux-view and terminal-emulator on Github Packages
Moving from Bintray.
2021-02-11 22:58:56 +02:00
Leonid Pliushch
8baf53b09a update bootstrap archives 2021-02-11 17:51:46 +02:00
Leonid Pliushch
4139bf9424 version 0.107 2021-02-03 23:47:30 +02:00
Leonid Pliushch
94deecb7b1 Merge pull request #1889 from rakslice/numpad_arrows
Implement numpad arrows/home/end/pgup/pgdn/ins/del when num lock is off
2021-02-03 23:43:05 +02:00
Vincent Deville
a4381b7827 ExtraKeysView: SpecialButtons use Button instead of ToggleButton 2021-02-03 23:26:08 +02:00
Vincent Deville
866da75fa9 ExtraKeysView: Use Set instead of List for special buttons keys 2021-02-03 23:26:08 +02:00
Vincent Deville
2b6e9ade07 ExtraKeysView: Handle multiple and popup SpecialButtons 2021-02-03 23:26:08 +02:00
Leonid Pliushch
6d1b0efd3b update bootstrap archives 2021-02-03 23:15:26 +02:00
Leonid Pliushch
e03858f065 export app version in environment variable 2021-02-03 22:39:13 +02:00
Leonid Pliushch
01929397cf TermuxPreferences: get rid of unnecessary switch()'es 2021-02-03 20:27:13 +02:00
Leonid Pliushch
496da3f877 introduce a property for configuring default working directory
Issue https://github.com/termux/termux-app/issues/1192.
2021-02-03 20:22:38 +02:00
Leonid Pliushch
407e4e003a github actions: update actions/upload-artifact to v2 2021-02-03 19:05:19 +02:00
Leonid Pliushch
fec61d315f github actions: debug_build.yml is fixed now? 2021-02-03 18:59:22 +02:00
Leonid Pliushch
f3a3a89f93 github actions: use hardcoded list of debug apks 2021-02-03 18:15:03 +02:00
Leonid Pliushch
5b084f0851 github actions: include split apks to artifact archive 2021-02-03 17:53:08 +02:00
Emil Velikov
a7eb173178 app: enable split apk builds
Currently we build a single APK which handles the four supported ABIs.

Therefore each user, downloads 50-75% more than they need - adding
towards both client/server-side network as well as device storage.

Use a split APK approach - it costs nothing from build and server-side
storage POV.

Note: We're removing ndk:abiFilters - they're incompatible/superseded by
the splits:abi:include list.

Signed-off-by: Emil Velikov <emil.l.velikov@gmail.com>
2021-02-03 17:29:13 +02:00
Leonid Pliushch
fa5117a098 terminal-view: bump library version
Needs to be updated on Bintray: https://bintray.com/termux/maven/terminal-view
2021-02-03 17:02:29 +02:00
Leonid Pliushch
2ef45c85b2 terminal-emulator: bump library version
Needs to be updated on Bintray: https://bintray.com/termux/maven/terminal-emulator
2021-02-03 16:59:34 +02:00
Leonid Pliushch
58b7a26b33 add basic metadata for F-Droid.
Fixes https://github.com/termux/termux-app/issues/622.
2021-02-03 16:43:17 +02:00
Leonid Pliushch
6e2a2ed946 documents provider: fix application icon reference 2021-02-03 15:56:38 +02:00
Leonid Pliushch
7be1fe5555 icon: add regular png icon for compatibility with some launchers
See https://github.com/termux/termux-app/issues/991.

Adaptive icon will continue to be automatically used if supported.
2021-02-03 15:49:03 +02:00
Leonid Pliushch
0e94d52094 readme: remove links to Google Play and Nethunter stores
Both offer outdated builds.
2021-02-03 02:56:53 +02:00
David Kramer
096dedffb1 Remove height offset workarounds / config
* Use WindowInsetsListener to get nav bar height and use that
      instead for calculating adjusted height for extra keys view
2021-02-02 01:31:50 +02:00
David Kramer
b5d491a54c Implement full screen and add FullScreenWorkAround to fix ExtraKeysView
not displaying properly and inconsistently between various devices
2021-02-02 01:31:50 +02:00
rakslice
f6822d6c24 Merge remote-tracking branch 'origin/master' into numpad_arrows
# Conflicts:
#	terminal-view/src/main/java/com/termux/view/TerminalView.java
2021-01-17 14:57:47 -08:00
Leonid Pliushch
32c3ffd57b readme: still need to clarify that app is "not maintained"
Please note that I do not review changes made by project collaborators.
Before submitting changes to branch 'master' ensure they are working
properly. I may not find the issue and fix it in time, like in case
with v0.105 release.
2021-01-17 20:03:05 +02:00
Leonid Pliushch
92570bee06 version 0.106 2021-01-17 19:45:19 +02:00
Leonid Pliushch
05bb399893 terminal-view: fix array indexing 2021-01-17 19:40:29 +02:00
rakslice
fe584940e1 fix TerminalView possiblePropLocations IOBE 2021-01-16 19:54:11 -08:00
rakslice
78cdaef6d2 only send PF1 for num lock when in application mode 2021-01-16 19:53:22 -08:00
rakslice
7b4a69f839 when num lock is off, have numpad keys works as their numlock-off functions (arrows/home/end/pgup/pgdn/insert/delete) 2021-01-16 19:48:55 -08:00
Henrik Grimler
831aa69da8 Merge pull request #726 from dkramer95/OpenBroadcast
Sends broadcast on app open to notify addon termux receivers
2021-01-06 13:48:15 +01:00
Leonid Pliushch
a56cba6843 version 0.105 2021-01-06 14:31:56 +02:00
Leonid Pliushch
9228982632 remove restrictions from viewable file types
Fixes https://github.com/termux/termux-app/issues/1872 and
similar issues.
2021-01-06 14:28:28 +02:00
Leonid Pliushch
38114784f1 update bootstrap archives 2021-01-06 14:23:15 +02:00
Henrik Grimler
b805f1486c Merge pull request #1869 from termux/ctrlspace
Add workaround property to fix ctrl+space on devices where this does not work
2021-01-04 22:41:56 +01:00
Henrik Grimler
7d31b7f480 terminal-emulator: tests: change spaces to tabs for consistency 2021-01-02 09:12:14 +01:00
Henrik Grimler
a0298285e3 terminal-emulator: tests: avoid error about methods not being mocked
unitTests.returnDefaultValues = true fixes it.
More info: http://g.co/androidstudio/not-mocked
2021-01-02 09:12:14 +01:00
Lucy Phipps
538a1d5cdf F-Droid URL for Termux:Styling 2021-01-01 23:48:34 +02:00
Henrik Grimler
f1e973f0d2 terminal-view: add "ctrl-space-workaround" property
Makes it possible to run ctrl+space with hardware keyboards on
devices/ROMs where it otherwise is broken. On devices where it already
works this workaround breaks ctrl+space though.

Where to add this fix was investigated and found by @5bodnar.
2021-01-01 19:20:16 +01:00
Henrik Grimler
b467b68f7b terminal-view: mv code to get properties to its own function 2021-01-01 19:20:11 +01:00
Henrik Grimler
b895cbbb1e app: set importantForAutofill="no" for extra keys input field
Fixes warning "extra_keys_right.xml:2: Missing autofillHints attribute".
2021-01-01 18:13:37 +01:00
Henrik Grimler
fc30eba247 terminal-view: silence warning from use of SHOW_AS_ACTION_ALWAYS
```
Prefer "SHOW_AS_ACTION_IF_ROOM" instead of "SHOW_AS_ACTION_ALWAYS"
```
2021-01-01 18:13:36 +01:00
Henrik Grimler
b1d4c0c7fe app: disable test for "ProtectedPermissions"
Needed for tests to pass after READ_LOGS and WRITE_SECURE_SETTINGS
were added to AndroidManifest.xml.
2021-01-01 18:13:36 +01:00
Henrik Grimler
7fe5bd32c8 Update ndk to r22 2021-01-01 18:12:53 +01:00
Henrik Grimler
43bbef9a11 Update plugins and gradle to latest versions 2021-01-01 18:12:53 +01:00
Henrik Grimler
eaea0f74a5 Fix github links for terminal-{emulator,view} jcenter packages 2021-01-01 18:12:53 +01:00
Leonid Pliushch
cb13a5a531 version 0.104 2020-12-29 13:45:35 +02:00
Kevin LeBlanc
d267843e36 Don't exclude hidden files from document provider (#1220)
Credit to @johnmellor for requesting the document provider in the
first place via #79, mentioning this limitation in a comment on
that issue, and creating a commit like this one to address it.
2020-12-29 13:42:36 +02:00
Leonid Pliushch
5ca67dd885 update bootstrap archives 2020-12-29 13:40:25 +02:00
Leonid Pliushch
6b0d531758 Revert "update readme" 2020-12-17 20:27:46 +02:00
Leonid Pliushch
1b3283bd69 update bootstrap archives 2020-12-11 20:07:40 +02:00
Leonid Pliushch
2820f6a7b8 update readme 2020-12-11 17:36:14 +02:00
Leonid Pliushch
8925ae67bb update readme 2020-12-11 17:35:28 +02:00
Lucy Phipps
4066c5df42 don't add $PREFIX/bin/applets to $PATH
busybox doesn't use that folder anymore, and is deprecated anyway
2020-11-24 23:16:17 +02:00
Leonid Pliushch
20aac6aa72 copy FUNDING.yml from termux-packages repo 2020-11-18 03:04:38 +02:00
Leonid Pliushch
e1f799f9a1 version 0.103 2020-11-17 20:40:40 +02:00
Leonid Pliushch
4479de4b9b update bootstrap archives 2020-11-17 20:35:55 +02:00
Leonid Pliushch
3a8f53a54c add permission WRITE_SECURE_SETTINGS
https://github.com/termux/termux-api/issues/211
2020-11-17 20:07:33 +02:00
Leonid Pliushch
2f04a6186b add READ_LOGS permission
* https://github.com/termux/termux-app/issues/873
* https://github.com/termux/termux-app/issues/1821
2020-11-17 20:03:10 +02:00
Leonid Pliushch
ad64dd7c3d update bootstrap archives 2020-10-12 20:39:28 +03:00
Fredrik Fornwall
dfc4595ec5 version 0.102 2020-10-02 10:30:18 +02:00
Fabian Henneke
8c80efb904 Add an "Autofill password" context menu action 2020-10-02 10:21:34 +02:00
Leonid Pliushch
564079c7e9 update .gitattributes 2020-10-01 01:08:39 +03:00
Leonid Pliushch
5b6fd9b88c version 0.101 2020-09-28 03:30:21 +03:00
Leonid Pliushch
63bfe95848 update bootstrap archives again
Fixes faulty dpkg https://github.com/termux/termux-packages/issues/5858.
2020-09-28 03:28:49 +03:00
Leonid Pliushch
af95a99854 version 0.100 2020-09-28 00:59:52 +03:00
Leonid Pliushch
25523ae224 fix bootstrap archive checksum for arm
Due to leading zero issues, SHA-256 checksum for ARM variant will
have 63 characters instead of correct 64...
2020-09-28 00:52:44 +03:00
Leonid Pliushch
665adb6895 update bootstrap archives 2020-09-28 00:46:38 +03:00
Henrik Grimler
eb7fda28df Merge pull request #1764 from agnostic-apollo/termux-run-command-crash-and-foreground-patch
Fix RunCommandService crash and foreground issue
2020-09-27 21:02:56 +02:00
agnostic-apollo
7063a3a9da Add @override annotation to onStartCommand() function of RunCommandService. 2020-09-18 22:25:05 +05:00
agnostic-apollo
6e02b99ff6 Update RunCommandService docs. 2020-09-18 22:22:35 +05:00
agnostic-apollo
9aae665bfc Fix issue where termux session does not come to foreground automatically in android >= 10 unless user manually clicks termux notification for "RUN_COMMAND" intents and "Termux:Tasker" plugin actions that have background mode "false" because of new restrictions of starting activities from background. This is done by adding "android.permission.SYSTEM_ALERT_WINDOW" permission in AndroidManifest.xml so that the user may optionally grant "Draw Over Apps" permission to termux to fix the issue. 2020-09-18 22:21:58 +05:00
agnostic-apollo
52ce6cc94b Add support for "$PREFIX/" and "~/" prefix in "RUN_COMMAND" intent extras for paths. 2020-09-18 22:21:31 +05:00
agnostic-apollo
f91168eff4 Fix issue where termux crashes occasionally in android >= 8 because "startForeground()" function is not being called before running "startForegroundService()" in RunCommandService. 2020-09-18 22:20:23 +05:00
Henrik Grimler
8faa5b2151 TerminalEmulator: fix bug in DECRQM handling
Reported in https://github.com/termux/termux-app/issues/1752
2020-09-12 21:27:24 +02:00
Leonid Pliushch
216cc10f3c Revert "add gradle distribution sha256"
Was initially added to try fix F-Droid builds but appears missing
distribution sha256 is not a cause of the issue.

F-Droid maintains own set of Gradle SHA-256 checksums.

This reverts commit cba80b6c0b.
2020-09-12 19:06:54 +00:00
Leonid Pliushch
cba80b6c0b add gradle distribution sha256 2020-09-01 03:38:53 +03:00
Leonid Pliushch
850faa25dd version 0.99 2020-08-26 20:51:05 +03:00
Leonid Pliushch
a108b2bd6b gradle 6.6.1 2020-08-26 20:40:29 +03:00
Leonid Pliushch
b95823d7a8 update bootstrap archives 2020-08-26 20:27:47 +03:00
Leonid Pliushch
382da7e8f7 terminal view: provide a workaroud for issue with some keyboards
Issue https://github.com/termux/termux-app/issues/686.

Note that there can be a better workaround which I don't know...
2020-08-26 20:04:22 +03:00
blackcat-917
ba9c118b50 readme: fix a copy-paste typo (#1720) 2020-08-22 00:17:45 +03:00
Leonid Pliushch
531c32f3c9 CI: do builds for android-10 branch 2020-08-16 22:56:55 +03:00
Leonid Pliushch
db2f50c76e extra keys: use TextUtils.join instead of String.join
String.join() is available only on Android API 26+ but our current
minimal is 24.

See https://github.com/termux/termux-app/issues/1670.
2020-08-14 15:08:32 +03:00
Leonid Pliushch
784affe39c linter: fix wakelock log tag
See https://github.com/termux/termux-app/issues/1670.
2020-08-14 15:03:51 +03:00
Leonid Pliushch
b486d29d23 fix RUN_COMMAND permission description
See https://github.com/termux/termux-app/issues/1713.
2020-08-14 14:50:34 +03:00
Fredrik Fornwall
332f1104a3 Update drawerlayout library 2020-08-09 00:07:35 +02:00
Leonid Pliushch
5a70be1523 terminal emulator: flush remaining process output data when terminating session 2020-08-08 02:22:47 +03:00
Leonid Pliushch
619552ec5c version 0.98 (v0.97 is non-release, so updating from 0.96 to 0.98) 2020-08-07 15:39:44 +03:00
Leonid Pliushch
70580abd50 update bootstrap archives 2020-08-07 15:39:11 +03:00
Henrik Grimler
f191c35851 Merge pull request #1693 from landfillbaby/patch-1
update WcWidth.java, add note about c version
2020-08-07 13:39:33 +02:00
lucy phipps
bd7ed28981 update WcWidth.java, add note about c version 2020-08-07 07:38:06 +01:00
hannesa2
b68bd107c1 Gradle 6.5.1 (#1686) 2020-08-04 15:37:18 +03:00
hannesa2
5075273362 Android Studio 4.0.1 (#1687) 2020-08-04 15:36:41 +03:00
cn
04268f4c20 move sdk version configs to gradle.properties (#1685) 2020-08-04 15:36:14 +03:00
Leonid Pliushch
6f24628fd2 version 0.96 2020-07-30 23:56:27 +03:00
Leonid Pliushch
debbe44809 update bootstrap archives 2020-07-30 23:47:26 +03:00
Agnostic Apollo
b2ff0e4051 Changed static string "EXTRA_EXECUTE_IN_BACKGROUND" access to public 2020-07-30 21:39:41 +03:00
Agnostic Apollo
9e7029b76a Receive "RUN_COMMAND_ARGUMENTS" extra for "RUN_COMMAND_ACTION" intent
as a string array extra instead of a string extra since TermuxService expects it that way.

Added "RUN_COMMAND_BACKGROUND" boolean extra so that Termux session can be started in background
when running a command.

Updated usage docs.

Check #1029 for details.
2020-07-30 21:39:41 +03:00
Egor Zhdanov
51370799c7 update notification icon 2020-07-14 12:36:56 +03:00
Danny Lin
0910844896 Invert selected text instead of highlighting
Highlighting text in the terminal often makes it hard to read, which
can be problematic for users who want to adjust or review selections
before copying them. For example, the default theme makes white and
green text hard to read on its light gray selection background, and
there are plenty of other themes where the choice of text and cursor
colors would hinder selection readability.

To fix this issue and make selected text more legible in nearly all
combinations of colors, invert selected text instead of highlighting it.
This is more common among terminal emulators anyway:
    Invert:    xterm, fbcon, kitty, Konsole, Alacritty, Tilix,
               gnome-terminal (7)
    Highlight: Termux, Terminal.app, iTerm2, Windows Terminal (4)
2020-07-12 17:54:47 +03:00
Danny Lin
f33ebf810f Fix selection rendering with alternate cursor styles
There is currently a bug where selection rendering is broken if the
active cursor shape is anything other than the default solid box.
Selected text is normally highlighted by effectively rendering a cursor
over all of the characters in the selection region, but if the cursor is
a bar, the resulting selection highlight is too narrow to cover the full
width of the selection. Similarly, if the cursor is an underline, all of
the selected text will be underlined instead of highlighted.

To fix this issue, treat selections different from cursors in the
rendering logic and force the renderer to always use the block cursor
style for rendering selections. That way, we get correct behavior
regardless of what the current cursor shape is.
2020-07-04 15:31:45 +03:00
Felix C. Stegerman
930029b5d2 export COLORTERM=truecolor 2020-06-22 02:07:20 +03:00
Leonid Pliushch
33def928cf add DEX2OATBOOTCLASSPATH environment variable 2020-06-19 22:25:51 +03:00
Leonid Pliushch
fc04a93990 get rid of Android 5 legacy stuff
We don't need LD_LIBRARY_PATH as of Android 7.0.
2020-06-19 22:18:28 +03:00
Leonid Pliushch
8d302aa9fe fix am on Android R 2020-06-19 22:18:21 +03:00
Leonid Pliushch
2224d917a3 update ndk version 2020-06-17 14:08:58 +03:00
Leonid Pliushch
664ec43f94 version 0.95 2020-06-09 23:08:23 +03:00
Leonid Pliushch
6cf742460c extra keys: make popup & pressed button no longer transparent
Fixes visual issue when popup overlaps text of upper row.
2020-06-09 22:54:57 +03:00
Leonid Pliushch
72981fb981 Use vector drawables for text selection handle
https://github.com/termux/termux-app/issues/1036
2020-06-09 21:47:11 +03:00
Leonid Pliushch
2c5534e2c1 RunCommandService: update information about usage 2020-06-09 16:07:42 +03:00
Leonid Pliushch
5b32540635 minor refactoring: RunCommand => RunCommandService 2020-06-09 15:48:56 +03:00
Leonid Pliushch
db3ff7b24a Provide a service for executing commands by third-party applications
Re-implementation of https://github.com/termux/termux-app/pull/1029.

If Termux has property "allow-external-apps" set to "true", a third-party
program will be able to send intents for executing custom commands
within Termux environment.

Third-party program must declare permission "com.termux.permission.RUN_COMMAND".
2020-06-09 15:36:08 +03:00
Trygve Aaberge
5f71e3e73a Join lines that fills the width when selecting urls
Some terminal applications, like mutt and weechat, prints a newline at
the end of each line even if text is wrapped. This causes urls which are
wrapped to not be selectable in full.

By ignoring newlines when the text fills the entire width of the screen,
those urls can be selected. Many other terminal emulators do this as
well.

A drawback of this is that if a url happens to fill the width of the
screen, the url selection will include the first word of the next line,
but this doesn't happen that often so I think it's an okay tradeoff.

Fixes #313
2020-06-09 13:41:36 +03:00
Josh Home
3e04ea4cb0 Add Log file to view intent #1497 2020-06-09 12:31:59 +03:00
Trygve Aaberge
0af823607a Improvements to extra keys (#1479)
* Make popup keys for extra keys row configurable

This makes the keys you get when swiping up on a key configurable. You
can configure such a key by using an array of strings instead of a
single string in the row. The first entry will be the normal key and the
second will be the extra key.

This is a slightly breaking change, as people that have configured
custom extra keys with "-" or "/" will have to change the config to keep
the popup keys. The default config will remain the same in terms of
functionality, i.e. it includes the same popup key for "-".

* Make popup keys interact well with long press keys

This stops the repeat action when the popup is shown, and makes sure the
popup is closed when you release even if there has been some repeat
actions.

* Support configuring the style of the extra keys

This adds a setting for choosing between the different ways to render
key names that were already present in ExtraKeysView.

The available setting values are "arrows-only", "arrows-all", "all",
"none" and "default". Other values will fallback to "default".

Can be used as a workaround for #1410

* Support using modifier keys with letter keys in extra keys

This allows you to use the modifier keys on the extra keys rows, e.g.
ctrl, together with another button on the extra keys rows, as long as
that button is a normal letter and not a special key. Support for
special keys will come in the next commit.

* Support using modifier keys with special keys in extra keys

This allows you to use the modifier keys on the extra keys rows together
with a special key on the extra keys rows, e.g. CTRL+LEFT.

Fixes #745, fixes most of #895 and possibly #154

* Support mapping extra keys to other actions

This adds a setting called extra-keys-map which allows you to map a key
on the extra keys rows to another action. The value is a json object
where the key is the button text as configured in extra-keys and the
value is the action. Multiple actions can be used, but if they are
special characters (like ESC or LEFT) they have to be separated from the
other characters with a space on each side. If you want an actual space
character, use SPACE.

For example if you want to add a key to go to the next active channel in
weechat, you can use this:
    extra-keys-map = {"weechat next": "ESC a"}
And then add "weechat next" to extra-keys. The name can of course be
whatever you want.

Or if you want the button for the UP arrow to show ⇧ instead of ↑, you
can use this:
    extra-keys-map = {"⇧": "UP"}
And put "⇧" in extra-keys instead of "UP".

Modifier keys (ctrl, alt and shift) can't be used in this map yet.
Support for ctrl and alt will come in the next commit.

I think this fixes #1186

* Support CTRL and ALT in extra keys map

This allows you to use CTRL and ALT in extra-keys-map.

For example if you want a button to exit the terminal, you can use this:

    extra-keys-map = {"exit": "CTRL d"}

And add "exit" to extra-keys.

* Support a KEYBOARD button in extra keys

This toggles showing the keyboard input method.

* Support specifying macro keys in the extra-keys option

Instead of specifying macros in the separate extra-keys-map option by
matching the key name in the two options, you can now use "macro"
instead of "key" in extra-keys, and it will be a macro, i.e. a sequence
of multiple keys separated by space.

* Remove option extra-keys-map

Now that you can specify macro in extra-keys, there is no point in
having this separate option. Instead of specifying the value to display
as key, and the macro to perform in extra-keys-map, you would now
specify the value to display in the display property and the macro to
perform in the macro property.

* Lookup display text when creating ExtraKeyButton

This will make it easier to support key aliases for macros in the next
commit.

* Add support for a key to open the drawer

Fixes (I think) #1325
2020-06-09 12:17:07 +03:00
Trygve Aaberge
4d9c0c315e Update notification icon to match launcher icon
This is the launcher icon with a circle around it. I added the circle
because the icon has a transparent background, so it looks a bit weird
with just the >_.
2020-06-09 12:11:49 +03:00
Leonid Pliushch
9c32935ca2 fix ndk version for terminal-emulator module 2020-06-09 12:08:13 +03:00
Leonid Pliushch
669c336e2f update ndk version 2020-06-09 12:02:41 +03:00
Trygve Aaberge
35842cf4a6 Set orientation of HandleView in show (#1477)
* Place long press menu above selection

Previously, the long press menu would cover the first line of the
selection.

* Flip selection handle at different positions depending on drag direction

When the selection handle changes direction, the selection jumps to the
new point of the handle. When the handle changes direction at the same
place when you come from the left as from the right, that makes it
impossible to select the characters which are at the position where it
changes direction.

With this change the handle remains pointing towards the edge further
into the line when you drag it from the edge and against the center.

* Set orientation of HandleView when showing it

When you hold down on a word that starts or ends at the edge of the
screen, the handle will appear outside of the screen. This happens
because the orientation was only switched when the handle is dragged, so
when it is shown it just used the same orientation as it had for the
last selection.

Relates to #334, but not sure if it fixes it completely.
2020-06-09 11:59:59 +03:00
Trygve Aaberge
b086270a5a Support auto detection of dark theme
By default it uses the system setting. If use-black-ui is set to either
true or false, that overrides it.

Fixes #1351 properly, fixes #1354
2020-06-09 11:59:07 +03:00
Leonid Pliushch
f39f06a540 update bootstraps again
* Reduced archive size by using nano as preinstalled text editor instead of vim.
* Updated command-not-found and termux-tools.
2020-06-07 22:36:14 +03:00
Leonid Pliushch
58440bc88d update bootstraps again
Added few more packages to bootstraps which are replacements to
some busybox applets.
2020-06-07 03:56:51 +03:00
Leonid Pliushch
9f438e2912 update bootstraps 2020-06-06 22:42:29 +03:00
Leonid Pliushch
f794bfcadc Handle all exceptions when loading termux.properties (#1590)
* Catch all exceptions which can occur.
* Print short description in toast message about occurred exception.
2020-06-06 21:20:05 +03:00
hannesa2
b6d7831646 Fix CI build and see result in pull request (#1565) 2020-05-24 02:05:24 +02:00
Leonid Pliushch
d212198e30 update bootstraps 2020-05-24 02:44:06 +03:00
Fredrik Fornwall
c2843897ac Fix build by specifying ndkVersion 2020-05-24 01:27:01 +02:00
Fredrik Fornwall
c4c4912a7e Avoid specifying ndk version 2020-05-24 01:22:33 +02:00
Hannes Achleitner
38a3319ca2 show result in pull request 2020-05-24 01:08:01 +02:00
Hannes Achleitner
7e13b8aa2e Verify Gradle 2020-05-24 01:08:01 +02:00
Frieder Bluemle
6dca19ae00 Update Android Gradle plugin to 3.6.3 2020-05-24 01:06:15 +02:00
Leonid Plyushch
2659c06c5d Remove irrelevant message from bootstrap installation error
Bootstraps are no longer downloaded from the Internet.
2020-04-30 14:34:35 +03:00
Fredrik Fornwall
9b7c7102b2 Bump version to 0.94
Some checks failed
Build / build (push) Has been cancelled
Unit tests / testing (push) Has been cancelled
2020-03-24 23:25:49 +01:00
Daniel Lublin
1819087ca0 Add tests for URL fragment 2020-03-24 23:13:14 +01:00
Daniel Lublin
366a61f052 Grab fragment when extracting URLs for selection 2020-03-24 23:13:14 +01:00
Leonid Plyushch
6e224cabcf set ndk version in build.gradle
Fixing CI builds.
2020-03-21 11:31:52 +02:00
Leonid Plyushch
8c8fa96133 update bootstraps 2020-03-21 10:35:17 +02:00
Fredrik Fornwall
537f2ed97e Update gradle wrapper 2020-03-20 14:42:43 +01:00
Fredrik Fornwall
0e23315c41 Add android.useAndroidX=true to gradle.properties 2020-03-20 10:44:24 +01:00
Fredrik Fornwall
2cde986419 Update Android gradle plug-in 2020-03-16 13:58:34 +01:00
Fredrik Fornwall
d28939810c Update Robolectric from 4.3 to 4.3.1 2020-02-21 14:02:53 +01:00
Fredrik Fornwall
93724b7aa6 Bump version to 0.92
Some checks failed
Build / build (push) Has been cancelled
Unit tests / testing (push) Has been cancelled
2020-02-09 22:59:33 +01:00
x0b
5d06f040e8 Fix deletable flags in DocumentProvider
A file is deletable if the _parent_ is writable, not the file itself.
2020-02-09 22:57:27 +01:00
x0b
ed9afa082a Fix #1350: Support createDocument(...) 2020-02-09 22:57:27 +01:00
x0b
9703bd31ad Fix #1424: Support ACTION_OPEN_DOCUMENT_TREE 2020-02-09 22:57:27 +01:00
Leonid Plyushch
9749f25eba update bootstrap archives
v19 had a problem with metadata which broke the package manager.
2020-02-09 12:34:11 +02:00
Leonid Plyushch
453b838b24 update bootstrap archives 2020-02-08 20:33:34 +02:00
Leonid Plyushch
9fdf2a49fd update readme 2020-02-07 15:48:32 +02:00
Leonid Plyushch
3270506bff update issue templates 2020-02-07 15:05:16 +02:00
Leonid Plyushch
a240f4cf45 CI: fix job name in run_tests.yml 2020-02-07 14:02:34 +02:00
Leonid Plyushch
4647beb0d2 update readme 2020-02-07 13:57:08 +02:00
Leonid Plyushch
d92e806461 CI: update workflow labels 2020-02-07 13:55:45 +02:00
Leonid Plyushch
b75cf0bb84 CI: add configuration for running terminal emulator unit tests 2020-02-07 13:50:12 +02:00
Leonid Plyushch
f928efed4e CI: switch to Github Actions 2020-02-07 13:47:22 +02:00
Fredrik Fornwall
36db64d585 Bump version to 0.90 2020-02-02 03:36:52 +01:00
Jean Schurger
b8f0430699 Prevent kill by Samsung Dex on plug/unplug. 2020-02-02 03:36:13 +01:00
Edontin
90e6260d5e Allow the user to disable virtual key emulation.
Use volume-keys=volume within termux.properties to disable.
2020-01-06 10:21:32 +01:00
Fredrik Fornwall
566d656c16 Avoid trailing slash in CWD (fixes #1413) 2020-01-05 19:14:52 +01:00
Fredrik Fornwall
b729085d52 Bump version to 0.88 2020-01-05 19:14:35 +01:00
Fredrik Fornwall
a94bcb02f1 Bump version to 0.86 2020-01-05 02:02:05 +01:00
Fredrik Fornwall
e28be01dc2 Create new terminal sessions with directory of active session
This mimics the behaviour of most tabbed terminal emulators.

Fixes #1009.
2020-01-05 02:00:25 +01:00
Archenoth
7f7c1efac1 Add PendingIntent in TermuxService to return data from execution
This commit adds an optional final argument to the BackgroundJob
constructor for a PendingIntent to return the results of its
execution to, and also attempts to pass an optional pendingIntent to
it from the Service start intent
2020-01-05 01:07:11 +01:00
Archenoth
76df44e6bb Start stderr BackgroundJob Thread 2020-01-05 01:07:11 +01:00
Fredrik Fornwall
fd13f3f98d Handle magnet links with termux-url-opener
Fixes #1339 and #1382.
2020-01-05 01:02:47 +01:00
Leonid Plyushch
269c3cafb0 properly set bootclasspath environment variable 2020-01-05 00:38:49 +01:00
Fredrik Fornwall
662b5cace6 Update gradle wrapper 2020-01-01 22:57:52 +01:00
Fredrik Fornwall
3c01b09fff Bump version to 0.85 2019-12-29 18:53:00 +01:00
Fredrik Fornwall
1ce2db9929 Merge branch 'maoabc-master' 2019-12-29 18:50:34 +01:00
Fredrik Fornwall
c987ff718a Update Android Gradle plug-in 2019-12-29 18:49:41 +01:00
Leonid Plyushch
9e295b105c terminal emulator: clear scrollback buffer when resetting to initial state
Fixes utility 'reset' not being able to clear scrollback buffer.
2019-12-29 18:49:41 +01:00
Fredrik Fornwall
37b3a9f8db Update Android Gradle plug-in 2019-12-29 18:49:10 +01:00
Leonid Plyushch
490853e427 terminal emulator: clear scrollback buffer when resetting to initial state
Fixes utility 'reset' not being able to clear scrollback buffer.
2019-12-24 00:32:40 +01:00
mao
9ebedc4740 Merge remote-tracking branch 'remotes/origin/master' 2019-12-19 00:43:45 +08:00
mao
82730d9e08 confict 2019-12-19 00:34:14 +08:00
mao
3d2756f376 Optimize handleView move. 2019-12-18 23:50:23 +08:00
Leonid Plyushch
6db51bdec0 remove SDK_INT conditionals
We are targting on API 24, so conditionals for Android 5/6 compatibillity
are useless now.
2019-12-18 14:06:06 +02:00
mao
c238c8bddb Optimize handle view 2019-12-18 14:00:58 +02:00
mao
60e1556871 Stop selection mode on enter 2019-12-18 14:00:57 +02:00
mao
e47bd31681 Selection mode fling 2019-12-18 14:00:57 +02:00
mao
951df6b4e2 Add selection mode cursor controller 2019-12-18 14:00:32 +02:00
Leonid Plyushch
fcd3bc1133 update gradle wrapper 2019-12-02 17:19:17 +02:00
Fredrik Fornwall
f4db9ff212 Bump version to 0.84 2019-11-24 19:10:10 +01:00
Leonid Plyushch
87841886d4 extra keys: improve handling of DnD mode
Do not disturb mode is now handled only for SDKs pre-28. Extra keys will
not vibrate only when total silence mode is used.
2019-11-24 19:08:32 +01:00
Leonid Plyushch
cf883f5f05 extra keys: handle actions UP & CANCEL separately
Related issue: https://github.com/termux/termux-app/issues/905
2019-11-24 19:08:32 +01:00
Fredrik Fornwall
677d75e173 Remove outdated tests
Remove tests that asserted that Cursor Down (CUD) and Cursor Up (CUU)
escape sequences were affected by the scrolling region set by DECSTBM.

This was incorrect and recently fixed:
https://github.com/termux/termux-app/issues/1340
2019-11-24 19:07:34 +01:00
Fredrik Fornwall
cdccc2c433 Do not limit cursor movement to scroll region
The scrolling region set by DECSTBM should not affect the
Cursor Down (CUD) and Cursor Up (CUU) escape sequences.

Fixes #1340.
2019-11-10 22:05:06 +01:00
Fredrik Fornwall
070436a6ed Bump version to 0.82 2019-11-10 19:26:10 +01:00
Fredrik Fornwall
69ded4b33e Update Android gradle plug-in 2019-11-10 19:26:01 +01:00
Fredrik Fornwall
e5a8c0eb4a Cleanup after requiring Android 7.0 2019-11-03 22:39:21 +01:00
Fredrik Fornwall
29815fb3f0 Fix gradle deprecation warning 2019-11-03 22:35:52 +01:00
Fredrik Fornwall
e930ac08aa Bump version to 0.78 2019-11-03 19:18:31 +01:00
Fredrik Fornwall
3b969a0077 Bump version to 0.77 2019-10-28 01:54:50 +01:00
Leonid Plyushch
ae717d8f5f do not allow to backup data by Google
Why:

 * During backup process Termux is being killed in most cases.
 * Backup data is limited to 25 MB.
 * Backup may not be performed/updated in certain cases.
2019-10-27 20:33:12 +01:00
Leonid Plyushch
6c55c3c1be bootstraps: update to v18 2019-10-20 22:06:18 +03:00
Fredrik Fornwall
c0a0822843 Do not set LD_LIBRARY_PATH by default
See https://github.com/termux/termux-app/issues/1286
2019-10-20 20:48:10 +02:00
Fredrik Fornwall
08b08d1c47 Bump version to 0.76 2019-10-20 20:16:48 +02:00
Fredrik Fornwall
c76cec186a Include bootstrap zips as shared libraries 2019-10-20 20:15:50 +02:00
Leon Omelan
5ba3f7cf6d Made Black UI an option to configure 2019-10-20 20:05:48 +02:00
Leon Omelan
468f878a38 Unified UI colors across the app. Dark sidebar and dark app theme for
dark Alert Dialogs
2019-10-20 20:05:48 +02:00
Fredrik Fornwall
c50a367063 Add .cxx folder to gitignore 2019-10-13 20:48:24 +02:00
Leonid Plyushch
4189f598b9 add permission ACCESS_NETWORK_STATE
Seems to be required by some Android TV devices.
2019-10-13 20:05:55 +02:00
mao
fdb3764f5c Optimize handle view 2019-10-11 07:37:57 +08:00
Leonid Plyushch
cb67e805de update readme 2019-10-07 15:32:21 +03:00
Leonid Plyushch
3f83368f42 CI: update .cirrus.yml 2019-10-07 15:01:22 +03:00
Leonid Plyushch
8c7462678f update readme 2019-10-07 00:21:57 +03:00
Leonid Plyushch
6f11390cc4 update readme 2019-10-07 00:20:33 +03:00
Leonid Plyushch
33d390a228 CI: development builds are now signed with publically-shared key
This commit adds keystore which is shared with community. People freely can
use it for creating own Termux app and plugin builds. However, builds signed
with such keystore cannot be proven as official.

Signed-off-by: Leonid Plyushch <leonid.plyushch@gmail.com>
2019-10-06 23:24:15 +03:00
mao
937eb350b2 Stop selection mode on enter 2019-10-05 18:54:15 +08:00
mao
3b4ece6bd8 Selection mode fling 2019-10-05 18:30:54 +08:00
mao
35a4fdacbe Add selection mode cursor controller 2019-10-05 18:05:42 +08:00
Leonid Plyushch
0332779d6a update readme 2019-10-04 23:50:46 +03:00
Leonid Plyushch
f19575b942 readme: update status badge url 2019-10-04 02:16:22 +03:00
Leonid Plyushch
6a95809da3 CI: add test keystore 2019-10-04 02:08:44 +03:00
Leonid Plyushch
ac8dab70ef CI: add initial configuration for Cirrus 2019-10-04 01:59:49 +03:00
Leonid Plyushch
243285f7c2 CI: remove Travis setup 2019-10-04 01:59:49 +03:00
Fredrik Fornwall
b6182216d5 Update android gradle plug-in 2019-10-02 21:37:23 +02:00
Fredrik Fornwall
e206121bbc Bump version to 0.75 2019-09-08 21:43:52 +02:00
Fredrik Fornwall
deceffad00 Update gradle configuration 2019-09-08 21:43:07 +02:00
Leonid Plyushch
c19909cef1 improve url matching regex
Now it support complex URLs and some other schemes beyond just http/ftp.
2019-09-08 20:10:26 +02:00
Leonid Plyushch
5b7e40638c handle possible ActivityNotFoundException when requesting to disable battery optimizations
Related issue: https://github.com/termux/termux-app/issues/1224
2019-09-08 20:10:15 +02:00
Leonid Plyushch
a3673d1af5 extra keys: follow DnD mode and haptic feedback Android settings
Do not vibrate when:

 * Do not disturb mode is on.

 * Haptic feedback is disabled in Android settings.
2019-09-08 20:08:16 +02:00
David Kramer
3bb2849a88 Sends broadcast on app open to notify addon termux receivers 2018-06-18 13:06:34 -06:00
222 changed files with 22545 additions and 3765 deletions

8
.gitattributes vendored
View File

@@ -1,3 +1,5 @@
* text=auto
*.bat eol=crlf
*.sh eol=lf
* text=auto
*.bat text eol=crlf
*.gradle text eol=lf
*.mk text eol=lf
*.sh text eol=lf

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
patreon: termux
custom: https://paypal.me/fornwall

View File

@@ -1,20 +1,35 @@
---
name: Bug report
about: Create a report to help us improve termux-app
about: Create a report to help us improve Termux application
---
<!-- Important note: Refusing to provide needed information may result in issue closing. If you are having problems with a package in termux then a bug report should be filed in the termux-packages repo: https://github.com/termux/termux-packages -->
<!--
IMPORTANT:
1. Support of Android 5.x - 6.x is finished.
2. Fill the template AFTER comments.
-->
**Problem description**
A clear and concise description of what the problem with the termux app is. You may post screenshots in addition to description.
<!--
A clear and concise description of what the problem is.
You may post screenshots in addition to description.
-->
**Steps to reproduce**
Please post all steps that are needed to reproduce the issue.
<!--
Steps to reproduce the behavior. Please post all necessary
commands that are needed to reproduce the issue.
-->
**Expected behavior**
<!--
A clear and concise description of what you expected to happen.
-->
**Additional information**
Post output of command `termux-info`.
If you are rooted or have access to adb then capture a logcat with `logcat -d "*:W"`, from a adb or root shell.
* Termux application version:
* Android OS version:
* Device model:

View File

@@ -1,12 +1,22 @@
---
name: Feature request
about: Suggest a new feature in termux-app
about: Suggest a new feature for Termux application
---
<!--
IMPORTANT:
1. Support of Android 5.x - 6.x is finished.
2. Fill the template AFTER comments.
-->
**Feature description**
<!--
Describe the feature and why you want it.
-->
**Reference implementation**
Does another app/terminal emulator have this feature?
Provide links to more background information
Provide links to more background information.

26
.github/workflows/debug_build.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Build
on:
push:
branches:
- master
- android-10
pull_request:
branches:
- master
- android-10
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v2
- name: Build
run: |
./gradlew assembleDebug
- name: Store generated APK file
uses: actions/upload-artifact@v2
with:
name: termux-app
path: ./app/build/outputs/apk/debug

View File

@@ -0,0 +1,19 @@
name: "Validate Gradle Wrapper"
on:
push:
branches:
- master
- android-10
pull_request:
branches:
- master
- android-10
jobs:
validation:
name: "Validation"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: gradle/wrapper-validation-action@v1

21
.github/workflows/run_tests.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Unit tests
on:
push:
branches:
- master
- android-10
pull_request:
branches:
- master
- android-10
jobs:
testing:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v2
- name: Execute tests
run: |
./gradlew test

2
.gitignore vendored
View File

@@ -7,6 +7,8 @@ build/
*.apk
*.so
.externalNativeBuild
.cxx
*.zip
# Crashlytics configuations
com_crashlytics_export_strings.xml

View File

@@ -1,34 +0,0 @@
sudo: false
language: android
jdk: oraclejdk8
env:
global:
# The next declaration is the encrypted COVERITY_SCAN_TOKEN, created
# via the "travis encrypt" command using the project repo's public key
- secure: "LdajbHNfRlpnqzhX5KY2Vr7KtzU9vXDs1TCNn93J6Dt522f2AaiyUDJvISvz+uslk0WJiS5bB5vGwQmXginxz6Qi6uMgMbjWXulv1vfs6ZviKpUX348DOp1qKPa8WfVNB66F84SwGIfc8cRMAgCFw79l/DFgLErubF8vKo1wZ8Hmvrz//+RJ0BGMa3YRc4VyJhAL0P+0Wc1Q2Im7R9EovAxC5pZXBIMSgr6g5GzLWPisbNLXpMPGsDeYhcenO6XCtCCy+aNxUYM8vcrLDzlVXR5Hy7KEs/MGRTS0Yk13TWUEYa5wBpKelFTszdWYLVn5ANreh/aXRVfHpnW3epotMYguLx1kSvOhWEnc4F+qqv3nle2LpDg9Y9bcLyTTcYnPl9smqEVVjEDu0FoIr1V58xkG4Oc6BPIvLRjlMVU96PXh2HxMLuGsJ/xM+uAFU9oVMbC07xn42Eu5O4NHOHJNOwMWac4/lSKRK8W/7/vWuXj5vhkD9ZsGVpN70UtY5HAfNUGADnTeDblvjgFTNZ2mUN/u0o7Z8ZFURYllZ9YU+Vr2nPf9CAhVBjuwFWx8uRQpAg1aDmc1dVMJijRBeBeU/uWhYqsGp34wkNEl8VGzob4R4QTyI8+T7CndGqKVmbTK/SjqKhjjPpbXIAfOH+JtxvAnNmb8XeQSJ32uK2nexFo="
android:
components:
- platform-tools
- tools
- build-tools-28.0.3
- android-28
- extra-android-m2repository
before_install:
- yes | sdkmanager "ndk-bundle"
- yes | sdkmanager "platforms;android-28"
script:
- ./gradlew testDebugUnitTest
addons:
coverity_scan:
project:
name: "termux/termux-app"
description: "Terminal emulator and Linux environment for Android"
notification_email: fredrik@fornwall.net
build_command_prepend: "./gradlew clean"
build_command: "./gradlew build"
branch_pattern: master

View File

@@ -1,3 +1,6 @@
Released under [the GPLv3 license](https://www.gnu.org/licenses/gpl.html).
The `termux/termux-app` repository is released under [GPLv3 only](https://www.gnu.org/licenses/gpl-3.0.html) license.
Contains code from `Terminal Emulator for Android` by which is released under [the Apache License 2.0](https://www.apache.org/licenses/).
### 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) libraries.
- Check [`termux-shared/LICENSE.md`](termux-shared/LICENSE.md) for `termux-shared` library related exceptions.

161
README.md
View File

@@ -1,32 +1,145 @@
[![Travis build status](https://travis-ci.org/termux/termux-app.svg?branch=master)](https://travis-ci.org/termux/termux-app)
# Termux application
[![Build status](https://github.com/termux/termux-app/workflows/Build/badge.svg)](https://github.com/termux/termux-app/actions)
[![Testing status](https://github.com/termux/termux-app/workflows/Unit%20tests/badge.svg)](https://github.com/termux/termux-app/actions)
[![Join the chat at https://gitter.im/termux/termux](https://badges.gitter.im/termux/termux.svg)](https://gitter.im/termux/termux)
[Termux](https://termux.com) is an Android terminal application and Linux environment.
Termux app
==========
Note that this repository is for the app itself (the user interface and the terminal emulation). For the packages installable inside the app, see [termux/termux-packages](https://github.com/termux/termux-packages).
[Termux](https://termux.com) is an Android terminal app and Linux environment.
Quick how-to about Termux package management is available at [Package Management](https://github.com/termux/termux-packages/wiki/Package-Management). It also has info on how to fix **`repository is under maintenance or down`** errors when running `apt` or `pkg` commands.
* [Termux on Google Play Store](https://play.google.com/store/apps/details?id=com.termux)
* [Termux on F-Droid](https://f-droid.org/repository/browse/?fdid=com.termux)
* [Termux Google+ community](http://termux.com/community/)
* [Termux Wiki](https://wiki.termux.com/wiki/)
* [Termux Twitter](http://twitter.com/termux/)
***
Note that this repository is for the app itself (the user interface and the terminal emulation). For the packages installable inside the app, see [termux/termux-packages](https://github.com/termux/termux-packages)
**@termux is looking for Termux Application maintainers for implementing new features, fixing bugs and reviewing pull requests since current one (@fornwall) is inactive.**
Terminal resources
==================
* [XTerm control sequences](http://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
* [vt100.net](http://vt100.net/)
* [Terminal codes (ANSI and terminfo equivalents)](http://wiki.bash-hackers.org/scripting/terminalcodes)
Issue https://github.com/termux/termux-app/issues/1072 needs extra attention.
Terminal emulators
==================
* VTE (libvte): Terminal emulator widget for GTK+, mainly used in gnome-terminal. [Source](https://github.com/GNOME/vte), [Open Issues](https://bugzilla.gnome.org/buglist.cgi?quicksearch=product%3A%22vte%22+), and [All (including closed) issues](https://bugzilla.gnome.org/buglist.cgi?bug_status=RESOLVED&bug_status=VERIFIED&chfield=resolution&chfieldfrom=-2000d&chfieldvalue=FIXED&product=vte&resolution=FIXED).
* iTerm 2: OS X terminal application. [Source](https://github.com/gnachman/iTerm2), [Issues](https://gitlab.com/gnachman/iterm2/issues) and [Documentation](http://www.iterm2.com/documentation.html) (which includes [iTerm2 proprietary escape codes](http://www.iterm2.com/documentation-escape-codes.html)).
* Konsole: KDE terminal application. [Source](https://projects.kde.org/projects/kde/applications/konsole/repository), in particular [tests](https://projects.kde.org/projects/kde/applications/konsole/repository/revisions/master/show/tests), [Bugs](https://bugs.kde.org/buglist.cgi?bug_severity=critical&bug_severity=grave&bug_severity=major&bug_severity=crash&bug_severity=normal&bug_severity=minor&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole) and [Wishes](https://bugs.kde.org/buglist.cgi?bug_severity=wishlist&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole).
* hterm: JavaScript terminal implementation from Chromium. [Source](https://github.com/chromium/hterm), including [tests](https://github.com/chromium/hterm/blob/master/js/hterm_vt_tests.js), and [Google group](https://groups.google.com/a/chromium.org/forum/#!forum/chromium-hterm).
* xterm: The grandfather of terminal emulators. [Source](http://invisible-island.net/datafiles/release/xterm.tar.gz).
* Connectbot: Android SSH client. [Source](https://github.com/connectbot/connectbot)
* Android Terminal Emulator: Android terminal app which Termux terminal handling is based on. Inactive. [Source](https://github.com/jackpal/Android-Terminal-Emulator).
***
### Contents
- [Termux App and Plugins](#Termux-App-and-Plugins)
- [Installation](#Installation)
- [Uninstallation](#Uninstallation)
- [Important Links](#Important-Links)
- [For Devs and Contributors](#For-Devs-and-Contributors)
##
## Termux App and Plugins
The core [Termux](https://github.com/termux/termux-app) app comes with the following optional plugin apps.
- [Termux:API](https://github.com/termux/termux-api)
- [Termux:Boot](https://github.com/termux/termux-boot)
- [Termux:Float](https://github.com/termux/termux-float)
- [Termux:Styling](https://github.com/termux/termux-styling)
- [Termux:Tasker](https://github.com/termux/termux-tasker)
- [Termux:Widget](https://github.com/termux/termux-widget)
##
## Installation
Termux can be obtained through various sources listed below for **only** Android `>= 7`. Support was dropped for Android `5` and `6` on [2020-01-01](https://www.reddit.com/r/termux/comments/dnzdbs/end_of_android56_support_on_20200101/) at `v0.83`, old builds are available on [archive.org](https://archive.org/details/termux-repositories-legacy).
The APK files of different sources are signed with different signature keys. The `Termux` app and all its plugins use the same [sharedUserId](https://developer.android.com/guide/topics/manifest/manifest-element) `com.termux` and so all their APKs installed on a device must have been signed with the same signature key to work together and so they must all be installed from the same source. Do not attempt to mix them together, i.e do not try to install an app or plugin from F-Droid and another one from a different source. Android Package Manager will also normally not allow installation of APKs with a different signatures and you will get errors on installation like `App not installed`, `Failed to install due to an unknown error`, `INSTALL_FAILED_UPDATE_INCOMPATIBLE`, `INSTALL_FAILED_SHARED_USER_INCOMPATIBLE`, `signatures do not match previously installed version`, etc. This restriction can be bypassed with root or with custom roms.
If you wish to install from a different source, then you must uninstall **any and all existing Termux or its plugin app APKs** from your device first, then install all new APKs from the same new source. Check [Uninstallation](#Uninstallation) section for details. You may also want to consider [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux) before uninstallation.
### F-Droid
Termux application can be obtained from F-Droid [here](https://f-droid.org/en/packages/com.termux/). It usually takes a few days (or even a week or more) for updates to be available on F-Droid once an update has been released on Github. F-Droid releases are built and published by F-Droid once they detect a new Github release. The Termux maintainers **do not** have any control over building and publishing of Termux app on F-Droid. Moreover, the Termux maintainers also do not have access to the APK signing keys of F-Droid releases, so we cannot release an APK ourselves on Github that would be compatible with F-Droid releases.
### Debug Builds
For users who don't want to wait for F-Droid releases and want to try out the latest features immediately or want to test their pull requests can get the APKs from [Github Actions](https://github.com/termux/termux-app/actions) page from the workflow runs labeled `Build`. The APK will be listed under `Artifacts` section. These are published for each commit done to the repository. These APKs are [debuggable](https://developer.android.com/studio/debug) and are also not compatible with other sources.
### Google Playstore **(Deprecated)**
**Termux and its plugins are no longer updated on [Google playstore](https://play.google.com/store/apps/details?id=com.termux) due to [android 10 issues](https://github.com/termux/termux-packages/wiki/Termux-and-Android-10).** The last version released for Android `>= 7` was `v0.101`. There are currently no immediate plans to resume updates on Google playstore. **It is highly recommended to not install Termux from playstore for now.** Any current users **should switch** to a different source like F-Droid.
If for some reason you don't want to switch, then at least check [Package Management](https://github.com/termux/termux-packages/wiki/Package-Management) to **change your mirror**, otherwise you will get **`repository is under maintenance or down`** errors when running `apt` or `pkg` commands. After that, it is also **highly advisable** to run `pkg upgrade` command to update all packages to the latest available versions, or at least update `termux-tools` package with `pkg install termux-tools` command.
##
## Uninstallation
Uninstallation may be required if a user doesn't want Termux installed in their device anymore or is switching to a different [install source](#Installation). You may also want to consider [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux) before uninstallation.
To uninstall Termux completely, you must uninstall **any and all existing Termux or its plugin app APKs** listed in [Termux App and Plugins](#Termux-App-and-Plugins).
Go to `Android Settings` -> `Applications` and then look for those apps. You can also use the search feature if its available on your device and search `termux` in the applications list.
Even if you think you have not installed any of the plugins, its strongly suggesting to go through the application list in Android settings and double check.
##
## Important Links
### Community
All community links are available [here](https://wiki.termux.com/wiki/Community).
The main ones are the following.
- [Termux Reddit community](https://reddit.com/r/termux)
- [Termux Matrix Channel](https://matrix.to/#termux_termux:gitter.im)
- [Termux Dev Matrix Channel](https://matrix.to/#termux_dev:gitter.im)
- [Termux Twitter](http://twitter.com/termux/)
- [Termux Reports Email](mailto:termuxreports@groups.io)
### Wikis
- [Termux Wiki](https://wiki.termux.com/wiki/)
- [Termux App Wiki](https://github.com/termux/termux-app/wiki)
- [Termux Packages Wiki](https://github.com/termux/termux-packages/wiki)
### Miscellaneous
- [FAQ](https://wiki.termux.com/wiki/FAQ)
- [Termux File System Layout](https://github.com/termux/termux-packages/wiki/Termux-file-system-layout)
- [Differences From Linux](https://wiki.termux.com/wiki/Differences_from_Linux)
- [Package Management](https://wiki.termux.com/wiki/Package_Management)
- [Remote_Access](https://wiki.termux.com/wiki/Remote_Access)
- [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux)
- [Terminal Settings](https://wiki.termux.com/wiki/Terminal_Settings)
- [Touch Keyboard](https://wiki.termux.com/wiki/Touch_Keyboard)
- [Android Storage and Sharing Data with Other Apps](https://wiki.termux.com/wiki/Internal_and_external_storage)
- [Android APIs](https://wiki.termux.com/wiki/Termux:API)
- [Moved Termux Packages Hosting From Bintray to IPFS](https://github.com/termux/termux-packages/issues/6348)
- [Termux and Android 10](https://github.com/termux/termux-packages/wiki/Termux-and-Android-10)
### Terminal resources
- [XTerm control sequences](http://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
- [vt100.net](http://vt100.net/)
- [Terminal codes (ANSI and terminfo equivalents)](http://wiki.bash-hackers.org/scripting/terminalcodes)
### Terminal emulators
- VTE (libvte): Terminal emulator widget for GTK+, mainly used in gnome-terminal. [Source](https://github.com/GNOME/vte), [Open Issues](https://bugzilla.gnome.org/buglist.cgi?quicksearch=product%3A%22vte%22+), and [All (including closed) issues](https://bugzilla.gnome.org/buglist.cgi?bug_status=RESOLVED&bug_status=VERIFIED&chfield=resolution&chfieldfrom=-2000d&chfieldvalue=FIXED&product=vte&resolution=FIXED).
- iTerm 2: OS X terminal application. [Source](https://github.com/gnachman/iTerm2), [Issues](https://gitlab.com/gnachman/iterm2/issues) and [Documentation](http://www.iterm2.com/documentation.html) (which includes [iTerm2 proprietary escape codes](http://www.iterm2.com/documentation-escape-codes.html)).
- Konsole: KDE terminal application. [Source](https://projects.kde.org/projects/kde/applications/konsole/repository), in particular [tests](https://projects.kde.org/projects/kde/applications/konsole/repository/revisions/master/show/tests), [Bugs](https://bugs.kde.org/buglist.cgi?bug_severity=critical&bug_severity=grave&bug_severity=major&bug_severity=crash&bug_severity=normal&bug_severity=minor&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole) and [Wishes](https://bugs.kde.org/buglist.cgi?bug_severity=wishlist&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole).
- hterm: JavaScript terminal implementation from Chromium. [Source](https://github.com/chromium/hterm), including [tests](https://github.com/chromium/hterm/blob/master/js/hterm_vt_tests.js), and [Google group](https://groups.google.com/a/chromium.org/forum/#!forum/chromium-hterm).
- xterm: The grandfather of terminal emulators. [Source](http://invisible-island.net/datafiles/release/xterm.tar.gz).
- Connectbot: Android SSH client. [Source](https://github.com/connectbot/connectbot)
- Android Terminal Emulator: Android terminal app which Termux terminal handling is based on. Inactive. [Source](https://github.com/jackpal/Android-Terminal-Emulator).
##
## For Devs and Contributors
The [termux-shared](termux-shared) library was added in [`v0.109`](https://github.com/termux/termux-app/releases/tag/v0.109). It defines shared constants and utils of Termux app and its plugins. It was created to allow for removal of all hardcoded paths in Termux app. The termux plugins will hopefully use this in future as well. If you are contributing code that is using a constant or a util that may be shared, then define it in `termux-shared` library if it currently doesn't exist and reference it from there. Update the relevant changelogs as well. Pull requests using hardcoded values **will/should not** be accepted.
The main Termux constants are defined by [`TermuxConstants`](https://github.com/termux/termux-app/blob/master/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java) class. It also contains information on how to fork Termux or build it with your own package name. Changing the package name will require building the bootstrap zip packages and other packages with the new `$PREFIX`, check [Building Packages](https://github.com/termux/termux-packages/wiki/Building-packages) for more info.

View File

@@ -1,21 +1,62 @@
apply plugin: 'com.android.application'
plugins {
id "com.android.application"
}
android {
compileSdkVersion 28
compileSdkVersion project.properties.compileSdkVersion.toInteger()
ndkVersion project.properties.ndkVersion
dependencies {
implementation "androidx.annotation:annotation:1.0.1"
implementation "androidx.annotation:annotation:1.2.0"
implementation "androidx.core:core:1.6.0-rc01"
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
implementation "androidx.preference:preference:1.1.1"
implementation "androidx.viewpager:viewpager:1.0.0"
implementation "androidx.drawerlayout:drawerlayout:1.0.0"
implementation "com.google.guava:guava:24.1-jre"
implementation "io.noties.markwon:core:$markwonVersion"
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
implementation "io.noties.markwon:linkify:$markwonVersion"
implementation "io.noties.markwon:recycler:$markwonVersion"
implementation project(":terminal-view")
implementation project(":termux-shared")
}
defaultConfig {
applicationId "com.termux"
minSdkVersion 21
targetSdkVersion 28
versionCode 74
versionName "0.74"
minSdkVersion project.properties.minSdkVersion.toInteger()
targetSdkVersion project.properties.targetSdkVersion.toInteger()
versionCode 115
versionName "0.115"
manifestPlaceholders.TERMUX_PACKAGE_NAME = "com.termux"
manifestPlaceholders.TERMUX_APP_NAME = "Termux"
manifestPlaceholders.TERMUX_API_APP_NAME = "Termux:API"
manifestPlaceholders.TERMUX_BOOT_APP_NAME = "Termux:Boot"
manifestPlaceholders.TERMUX_FLOAT_APP_NAME = "Termux:Float"
manifestPlaceholders.TERMUX_STYLING_APP_NAME = "Termux:Styling"
manifestPlaceholders.TERMUX_TASKER_APP_NAME = "Termux:Tasker"
manifestPlaceholders.TERMUX_WIDGET_APP_NAME = "Termux:Widget"
externalNativeBuild {
ndkBuild {
cFlags "-std=c11", "-Wall", "-Wextra", "-Werror", "-Os", "-fno-stack-protector", "-Wl,--gc-sections"
}
}
ndk {
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
}
}
signingConfigs {
debug {
storeFile file('dev_keystore.jks')
keyAlias 'alias'
storePassword 'xrj45yWGLbsO7W0v'
keyPassword 'xrj45yWGLbsO7W0v'
}
}
buildTypes {
@@ -24,16 +65,37 @@ android {
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
signingConfig signingConfigs.debug
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
externalNativeBuild {
ndkBuild {
path "src/main/cpp/Android.mk"
}
}
lintOptions {
disable 'ProtectedPermissions'
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
dependencies {
testImplementation 'junit:junit:4.12'
testImplementation "junit:junit:4.13.2"
testImplementation "org.robolectric:robolectric:4.4"
}
task versionName {
@@ -41,3 +103,68 @@ task versionName {
print android.defaultConfig.versionName
}
}
def downloadBootstrap(String arch, String expectedChecksum, String version) {
def digest = java.security.MessageDigest.getInstance("SHA-256")
def localUrl = "src/main/cpp/bootstrap-" + arch + ".zip"
def file = new File(projectDir, localUrl)
if (file.exists()) {
def buffer = new byte[8192]
def input = new FileInputStream(file)
while (true) {
def readBytes = input.read(buffer)
if (readBytes < 0) break
digest.update(buffer, 0, readBytes)
}
def checksum = new BigInteger(1, digest.digest()).toString(16)
if (checksum == expectedChecksum) {
return
} else {
logger.quiet("Deleting old local file with wrong hash: " + localUrl)
file.delete()
}
}
def remoteUrl = "https://github.com/termux/termux-packages/releases/download/bootstrap-" + version + "/bootstrap-" + arch + ".zip"
logger.quiet("Downloading " + remoteUrl + " ...")
file.parentFile.mkdirs()
def out = new BufferedOutputStream(new FileOutputStream(file))
def connection = new URL(remoteUrl).openConnection()
connection.setInstanceFollowRedirects(true)
def digestStream = new java.security.DigestInputStream(connection.inputStream, digest)
out << digestStream
out.close()
def checksum = new BigInteger(1, digest.digest()).toString(16)
if (checksum != expectedChecksum) {
file.delete()
throw new GradleException("Wrong checksum for " + remoteUrl + ": expected: " + expectedChecksum + ", actual: " + checksum)
}
}
clean {
doLast {
def tree = fileTree(new File(projectDir, 'src/main/cpp'))
tree.include 'bootstrap-*.zip'
tree.each { it.delete() }
}
}
task downloadBootstraps() {
doLast {
def version = "2021.06.30-r1"
downloadBootstrap("aarch64", "ce56ce9a4e8845bd1d35cc2695bbdd636c72625ee10ce21c9b98ab38ebbee5ab", version)
downloadBootstrap("arm", "537e81951c7d3d3f3def9ce6778e1032457488e21edb2c037a1e0e680c39e747", version)
downloadBootstrap("i686", "3c2ca858c0225671c00c44ac182e31819ffa93ec624e95e02824e7d6d30ca1b4", version)
downloadBootstrap("x86_64", "93c50d36b45bca42bb014395e8e184e5b540adcad5d4e215f7e64ebf0d655d2b", version)
}
}
afterEvaluate {
android.applicationVariants.all { variant ->
variant.javaCompileProvider.get().dependsOn(downloadBootstraps)
}
}

BIN
app/dev_keystore.jks Normal file

Binary file not shown.

View File

@@ -7,5 +7,6 @@
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
-renamesourcefileattribute SourceFile
-keepattributes SourceFile,LineNumberTable
-dontobfuscate
#-renamesourcefileattribute SourceFile
#-keepattributes SourceFile,LineNumberTable

View File

@@ -1,71 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.termux"
android:installLocation="internalOnly"
android:sharedUserId="com.termux"
android:sharedUserLabel="@string/shared_user_label" >
android:sharedUserId="${TERMUX_PACKAGE_NAME}"
android:sharedUserLabel="@string/shared_user_label">
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<uses-feature android:name="android.software.leanback" android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<permission
android:name="${TERMUX_PACKAGE_NAME}.permission.RUN_COMMAND"
android:description="@string/permission_run_command_description"
android:icon="@mipmap/ic_launcher"
android:label="@string/permission_run_command_label"
android:protectionLevel="dangerous" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.READ_LOGS" />
<uses-permission android:name="android.permission.DUMP" />
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
android:extractNativeLibs="true"
android:allowBackup="true"
android:fullBackupContent="@xml/backupscheme"
android:icon="@drawable/ic_launcher"
android:name=".app.TermuxApplication"
android:allowBackup="false"
android:banner="@drawable/banner"
android:extractNativeLibs="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/application_name"
android:theme="@style/Theme.Termux"
android:supportsRtl="false" >
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="false"
android:theme="@style/Theme.Termux">
<!-- This (or rather, value 2.1 or higher) is needed to make the Samsung Galaxy S8
mark the app with "This app is optimized to run in full screen." -->
<meta-data android:name="android.max_aspect" android:value="10.0" />
<!--
This (or rather, value 2.1 or higher) is needed to make the Samsung Galaxy S8
mark the app with "This app is optimized to run in full screen."
-->
<meta-data
android:name="android.max_aspect"
android:value="10.0" />
<activity
android:name="com.termux.app.TermuxActivity"
android:name=".app.TermuxActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|uiMode|keyboard|keyboardHidden|navigation"
android:label="@string/application_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:launchMode="singleTask"
android:resizeableActivity="true"
android:windowSoftInputMode="adjustResize|stateAlwaysVisible" >
android:resizeableActivity="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts" />
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity
android:name="com.termux.app.TermuxHelpActivity"
android:exported="false"
android:theme="@android:style/Theme.Material.Light.DarkActionBar"
android:parentActivityName=".app.TermuxActivity"
android:resizeableActivity="true"
android:label="@string/application_name" />
<activity-alias
android:name=".HomeActivity"
android:targetActivity=".app.TermuxActivity">
<!-- Launch activity automatically on boot on Android Things devices -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.IOT_LAUNCHER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity-alias>
<activity
android:name="com.termux.filepicker.TermuxFileReceiverActivity"
android:name=".app.activities.HelpActivity"
android:exported="false"
android:label="@string/application_name"
android:taskAffinity="com.termux.filereceiver"
android:excludeFromRecents="true"
android:parentActivityName=".app.TermuxActivity"
android:resizeableActivity="true"
android:noHistory="true">
android:theme="@android:style/Theme.Material.Light.DarkActionBar" />
<activity
android:name=".app.activities.SettingsActivity"
android:exported="true"
android:label="@string/title_activity_termux_settings"
android:theme="@style/Theme.AppCompat.Light.DarkActionBar" />
<activity
android:name=".shared.activities.ReportActivity"
android:theme="@style/Theme.AppCompat.TermuxReportActivity"
android:documentLaunchMode="intoExisting"
/>
<activity
android:name=".filepicker.TermuxFileReceiverActivity"
android:excludeFromRecents="true"
android:label="@string/application_name"
android:noHistory="true"
android:resizeableActivity="true"
android:taskAffinity="${TERMUX_PACKAGE_NAME}.filereceiver">
<!-- Accept multiple file types when sending. -->
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/*" />
<data android:mimeType="audio/*" />
<data android:mimeType="image/*" />
@@ -74,35 +129,25 @@
<data android:mimeType="text/*" />
<data android:mimeType="video/*" />
</intent-filter>
<!-- Be more restrictive for viewing files, restricting ourselves to text files. -->
<!-- Accept multiple file types to let Termux be usable as generic file viewer. -->
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW"/>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/*" />
<data android:mimeType="audio/*" />
<data android:mimeType="image/*" />
<data android:mimeType="text/*" />
<data android:mimeType="application/json" />
<data android:mimeType="application/*xml*" />
<data android:mimeType="application/*latex*" />
<data android:mimeType="application/javascript" />
<data android:mimeType="video/*" />
</intent-filter>
</activity>
<activity-alias
android:name=".HomeActivity"
android:targetActivity="com.termux.app.TermuxActivity">
<!-- Launch activity automatically on boot on Android Things devices -->
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.IOT_LAUNCHER"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity-alias>
<provider
android:name=".filepicker.TermuxDocumentsProvider"
android:authorities="com.termux.documents"
android:grantUriPermissions="true"
android:authorities="${TERMUX_PACKAGE_NAME}.documents"
android:exported="true"
android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
@@ -110,18 +155,32 @@
</provider>
<service
android:name="com.termux.app.TermuxService"
android:name=".app.TermuxService"
android:exported="false" />
<service
android:name=".app.RunCommandService"
android:exported="true"
android:permission="${TERMUX_PACKAGE_NAME}.permission.RUN_COMMAND">
<intent-filter>
<action android:name="${TERMUX_PACKAGE_NAME}.RUN_COMMAND" />
</intent-filter>
</service>
<receiver android:name=".app.TermuxOpenReceiver" />
<provider android:authorities="com.termux.files"
android:readPermission="android.permission.permRead"
android:exported="true"
android:grantUriPermissions="true"
android:name="com.termux.app.TermuxOpenReceiver$ContentProvider" />
<provider
android:name=".app.TermuxOpenReceiver$ContentProvider"
android:authorities="${TERMUX_PACKAGE_NAME}.files"
android:exported="true"
android:grantUriPermissions="true"
android:readPermission="android.permission.permRead" />
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
<meta-data
android:name="com.sec.android.support.multiwindow"
android:value="true" />
<meta-data
android:name="com.samsung.android.multidisplay.keep_process_alive"
android:value="true" />
</application>
</manifest>

View File

@@ -0,0 +1,5 @@
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := libtermux-bootstrap
LOCAL_SRC_FILES := termux-bootstrap-zip.S termux-bootstrap.c
include $(BUILD_SHARED_LIBRARY)

View File

@@ -0,0 +1,18 @@
.global blob
.global blob_size
.section .rodata
blob:
#if defined __i686__
.incbin "bootstrap-i686.zip"
#elif defined __x86_64__
.incbin "bootstrap-x86_64.zip"
#elif defined __aarch64__
.incbin "bootstrap-aarch64.zip"
#elif defined __arm__
.incbin "bootstrap-arm.zip"
#else
# error Unsupported arch
#endif
1:
blob_size:
.int 1b - blob

View File

@@ -0,0 +1,11 @@
#include <jni.h>
extern jbyte blob[];
extern int blob_size;
JNIEXPORT jbyteArray JNICALL Java_com_termux_app_TermuxInstaller_getZip(JNIEnv *env, __attribute__((__unused__)) jobject This)
{
jbyteArray ret = (*env)->NewByteArray(env, blob_size);
(*env)->SetByteArrayRegion(env, ret, 0, blob_size, blob);
return ret;
}

View File

@@ -1,220 +0,0 @@
package com.termux.app;
import android.util.Log;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* A background job launched by Termux.
*/
public final class BackgroundJob {
private static final String LOG_TAG = "termux-task";
final Process mProcess;
public BackgroundJob(String cwd, String fileToExecute, final String[] args, final TermuxService service) {
String[] env = buildEnvironment(false, cwd);
if (cwd == null) cwd = TermuxService.HOME_PATH;
final String[] progArray = setupProcessArgs(fileToExecute, args);
final String processDescription = Arrays.toString(progArray);
Process process;
try {
process = Runtime.getRuntime().exec(progArray, env, new File(cwd));
} catch (IOException e) {
mProcess = null;
// TODO: Visible error message?
Log.e(LOG_TAG, "Failed running background job: " + processDescription, e);
return;
}
mProcess = process;
final int pid = getPid(mProcess);
new Thread() {
@Override
public void run() {
Log.i(LOG_TAG, "[" + pid + "] starting: " + processDescription);
InputStream stdout = mProcess.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(stdout, StandardCharsets.UTF_8));
String line;
try {
// FIXME: Long lines.
while ((line = reader.readLine()) != null) {
Log.i(LOG_TAG, "[" + pid + "] stdout: " + line);
}
} catch (IOException e) {
Log.e(LOG_TAG, "Error reading output", e);
}
try {
int exitCode = mProcess.waitFor();
service.onBackgroundJobExited(BackgroundJob.this);
if (exitCode == 0) {
Log.i(LOG_TAG, "[" + pid + "] exited normally");
} else {
Log.w(LOG_TAG, "[" + pid + "] exited with code: " + exitCode);
}
} catch (InterruptedException e) {
// Ignore.
}
}
}.start();
new Thread() {
@Override
public void run() {
InputStream stderr = mProcess.getErrorStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(stderr, StandardCharsets.UTF_8));
String line;
try {
// FIXME: Long lines.
while ((line = reader.readLine()) != null) {
Log.i(LOG_TAG, "[" + pid + "] stderr: " + line);
}
} catch (IOException e) {
// Ignore.
}
}
};
}
private static void addToEnvIfPresent(List<String> environment, String name) {
String value = System.getenv(name);
if (value != null) {
environment.add(name + "=" + value);
}
}
static String[] buildEnvironment(boolean failSafe, String cwd) {
new File(TermuxService.HOME_PATH).mkdirs();
if (cwd == null) cwd = TermuxService.HOME_PATH;
List<String> environment = new ArrayList<>();
environment.add("TERM=xterm-256color");
environment.add("HOME=" + TermuxService.HOME_PATH);
environment.add("PREFIX=" + TermuxService.PREFIX_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"));
// ANDROID_RUNTIME_ROOT and ANDROID_TZDATA_ROOT are required for `am` to run on Android Q
addToEnvIfPresent(environment, "ANDROID_RUNTIME_ROOT");
addToEnvIfPresent(environment, "ANDROID_TZDATA_ROOT");
if (failSafe) {
// Keep the default path so that system binaries can be used in the failsafe session.
environment.add("PATH= " + System.getenv("PATH"));
} else {
if (shouldAddLdLibraryPath()) {
environment.add("LD_LIBRARY_PATH=" + TermuxService.PREFIX_PATH + "/lib");
}
environment.add("LANG=en_US.UTF-8");
environment.add("PATH=" + TermuxService.PREFIX_PATH + "/bin:" + TermuxService.PREFIX_PATH + "/bin/applets");
environment.add("PWD=" + cwd);
environment.add("TMPDIR=" + TermuxService.PREFIX_PATH + "/tmp");
}
return environment.toArray(new String[0]);
}
private static boolean shouldAddLdLibraryPath() {
try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(TermuxService.PREFIX_PATH + "/etc/apt/sources.list")))) {
String line;
while ((line = in.readLine()) != null) {
if (!line.startsWith("#") && line.contains("https://dl.bintray.com/termux/termux-packages-24")) {
return false;
}
}
} catch (IOException e) {
Log.e(LOG_TAG, "Error trying to read sources.list", e);
}
return true;
}
public static int getPid(Process p) {
try {
Field f = p.getClass().getDeclaredField("pid");
f.setAccessible(true);
try {
return f.getInt(p);
} finally {
f.setAccessible(false);
}
} catch (Throwable e) {
return -1;
}
}
static String[] setupProcessArgs(String fileToExecute, String[] args) {
// 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 = TermuxService.PREFIX_PATH + "/bin/" + binary;
}
break;
}
} else {
builder.append(c);
}
}
} else {
// No shebang and no ELF, use standard shell.
interpreter = TermuxService.PREFIX_PATH + "/bin/sh";
}
}
}
} catch (IOException e) {
// Ignore.
}
List<String> result = new ArrayList<>();
if (interpreter != null) result.add(interpreter);
result.add(fileToExecute);
if (args != null) Collections.addAll(result, args);
return result.toArray(new String[0]);
}
}

View File

@@ -1,436 +0,0 @@
package com.termux.app;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ScheduledExecutorService;
import java.util.Map;
import java.util.HashMap;
import java.util.Arrays;
import android.view.HapticFeedbackConstants;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.GridLayout;
import android.widget.PopupWindow;
import android.widget.ToggleButton;
import com.termux.R;
import com.termux.terminal.TerminalSession;
import com.termux.view.TerminalView;
/**
* A view showing extra keys (such as Escape, Ctrl, Alt) not normally available on an Android soft
* keyboard.
*/
public final class ExtraKeysView extends GridLayout {
private static final int TEXT_COLOR = 0xFFFFFFFF;
private static final int BUTTON_COLOR = 0x00000000;
private static final int INTERESTING_COLOR = 0xFF80DEEA;
private static final int BUTTON_PRESSED_COLOR = 0x7FFFFFFF;
public ExtraKeysView(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* HashMap that implements Python dict.get(key, default) function.
* Default java.util .get(key) is then the same as .get(key, null);
*/
static class CleverMap<K,V> extends HashMap<K,V> {
V get(K key, V defaultValue) {
if(containsKey(key))
return get(key);
else
return defaultValue;
}
}
static class CharDisplayMap extends CleverMap<String, String> {}
/**
* Keys are displayed in a natural looking way, like "→" for "RIGHT"
*/
static final Map<String, Integer> keyCodesForString = new HashMap<String, Integer>() {{
put("ESC", KeyEvent.KEYCODE_ESCAPE);
put("TAB", KeyEvent.KEYCODE_TAB);
put("HOME", KeyEvent.KEYCODE_MOVE_HOME);
put("END", KeyEvent.KEYCODE_MOVE_END);
put("PGUP", KeyEvent.KEYCODE_PAGE_UP);
put("PGDN", KeyEvent.KEYCODE_PAGE_DOWN);
put("INS", KeyEvent.KEYCODE_INSERT);
put("DEL", KeyEvent.KEYCODE_FORWARD_DEL);
put("BKSP", KeyEvent.KEYCODE_DEL);
put("UP", KeyEvent.KEYCODE_DPAD_UP);
put("LEFT", KeyEvent.KEYCODE_DPAD_LEFT);
put("RIGHT", KeyEvent.KEYCODE_DPAD_RIGHT);
put("DOWN", KeyEvent.KEYCODE_DPAD_DOWN);
put("ENTER", KeyEvent.KEYCODE_ENTER);
put("F1", KeyEvent.KEYCODE_F1);
put("F2", KeyEvent.KEYCODE_F2);
put("F3", KeyEvent.KEYCODE_F3);
put("F4", KeyEvent.KEYCODE_F4);
put("F5", KeyEvent.KEYCODE_F5);
put("F6", KeyEvent.KEYCODE_F6);
put("F7", KeyEvent.KEYCODE_F7);
put("F8", KeyEvent.KEYCODE_F8);
put("F9", KeyEvent.KEYCODE_F9);
put("F10", KeyEvent.KEYCODE_F10);
put("F11", KeyEvent.KEYCODE_F11);
put("F12", KeyEvent.KEYCODE_F12);
}};
static void sendKey(View view, String keyName) {
TerminalView terminalView = view.findViewById(R.id.terminal_view);
if (keyCodesForString.containsKey(keyName)) {
int keyCode = keyCodesForString.get(keyName);
terminalView.onKeyDown(keyCode, new KeyEvent(KeyEvent.ACTION_UP, keyCode));
// view.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
} else {
// not a control char
TerminalSession session = terminalView.getCurrentSession();
if (session != null && keyName.length() > 0)
session.write(keyName);
}
}
public enum SpecialButton {
CTRL, ALT, FN
}
private static class SpecialButtonState {
boolean isOn = false;
ToggleButton button = null;
}
private Map<SpecialButton, SpecialButtonState> specialButtons = new HashMap<SpecialButton, SpecialButtonState>() {{
put(SpecialButton.CTRL, new SpecialButtonState());
put(SpecialButton.ALT, new SpecialButtonState());
put(SpecialButton.FN, new SpecialButtonState());
}};
private ScheduledExecutorService scheduledExecutor;
private PopupWindow popupWindow;
private int longPressCount;
public boolean readSpecialButton(SpecialButton name) {
SpecialButtonState state = specialButtons.get(name);
if (state == null)
throw new RuntimeException("Must be a valid special button (see source)");
if (! state.isOn)
return false;
if (state.button == null) {
return false;
}
if (state.button.isPressed())
return true;
if (! state.button.isChecked())
return false;
state.button.setChecked(false);
state.button.setTextColor(TEXT_COLOR);
return true;
}
void popup(View view, String text) {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
Button button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
button.setText(text);
button.setTextColor(TEXT_COLOR);
button.setPadding(0, 0, 0, 0);
button.setMinHeight(0);
button.setMinWidth(0);
button.setMinimumWidth(0);
button.setMinimumHeight(0);
button.setWidth(width);
button.setHeight(height);
button.setBackgroundColor(BUTTON_PRESSED_COLOR);
popupWindow = new PopupWindow(this);
popupWindow.setWidth(LayoutParams.WRAP_CONTENT);
popupWindow.setHeight(LayoutParams.WRAP_CONTENT);
popupWindow.setContentView(button);
popupWindow.setOutsideTouchable(true);
popupWindow.setFocusable(false);
popupWindow.showAsDropDown(view, 0, -2 * height);
}
static final CharDisplayMap classicArrowsDisplay = new CharDisplayMap() {{
// classic arrow keys (for ◀ ▶ ▲ ▼ @see arrowVariationDisplay)
put("LEFT", ""); // U+2190 ← LEFTWARDS ARROW
put("RIGHT", ""); // U+2192 → RIGHTWARDS ARROW
put("UP", ""); // U+2191 ↑ UPWARDS ARROW
put("DOWN", ""); // U+2193 ↓ DOWNWARDS ARROW
}};
static final CharDisplayMap wellKnownCharactersDisplay = new CharDisplayMap() {{
// well known characters // https://en.wikipedia.org/wiki/{Enter_key, Tab_key, Delete_key}
put("ENTER", ""); // U+21B2 ↲ DOWNWARDS ARROW WITH TIP LEFTWARDS
put("TAB", ""); // U+21B9 ↹ LEFTWARDS ARROW TO BAR OVER RIGHTWARDS ARROW TO BAR
put("BKSP", ""); // U+232B ⌫ ERASE TO THE LEFT sometimes seen and easy to understand
put("DEL", ""); // U+2326 ⌦ ERASE TO THE RIGHT not well known but easy to understand
}};
static final CharDisplayMap lessKnownCharactersDisplay = new CharDisplayMap() {{
// https://en.wikipedia.org/wiki/{Home_key, End_key, Page_Up_and_Page_Down_keys}
// home key can mean "goto the beginning of line" or "goto first page" depending on context, hence the diagonal
put("HOME", ""); // from IEC 9995 // U+21F1 ⇱ NORTH WEST ARROW TO CORNER
put("END", ""); // from IEC 9995 // ⇲ // U+21F2 ⇲ SOUTH EAST ARROW TO CORNER
put("PGUP", ""); // no ISO character exists, U+21D1 ⇑ UPWARDS DOUBLE ARROW will do the trick
put("PGDN", ""); // no ISO character exists, U+21D3 ⇓ DOWNWARDS DOUBLE ARROW will do the trick
}};
static final CharDisplayMap arrowTriangleVariationDisplay = new CharDisplayMap() {{
// alternative to classic arrow keys
put("LEFT", ""); // U+25C0 ◀ BLACK LEFT-POINTING TRIANGLE
put("RIGHT", ""); // U+25B6 ▶ BLACK RIGHT-POINTING TRIANGLE
put("UP", ""); // U+25B2 ▲ BLACK UP-POINTING TRIANGLE
put("DOWN", ""); // U+25BC ▼ BLACK DOWN-POINTING TRIANGLE
}};
static final CharDisplayMap notKnownIsoCharacters = new CharDisplayMap() {{
// Control chars that are more clear as text // https://en.wikipedia.org/wiki/{Function_key, Alt_key, Control_key, Esc_key}
// put("FN", "FN"); // no ISO character exists
put("CTRL", ""); // ISO character "U+2388 ⎈ HELM SYMBOL" is unknown to people and never printed on computers, however "U+25C7 ◇ WHITE DIAMOND" is a nice presentation, and "^" for terminal app and mac is often used
put("ALT", ""); // ISO character "U+2387 ⎇ ALTERNATIVE KEY SYMBOL'" is unknown to people and only printed as the Option key "⌥" on Mac computer
put("ESC", ""); // ISO character "U+238B ⎋ BROKEN CIRCLE WITH NORTHWEST ARROW" is unknown to people and not often printed on computers
}};
static final CharDisplayMap nicerLookingDisplay = new CharDisplayMap() {{
// nicer looking for most cases
put("-", ""); // U+2015 ― HORIZONTAL BAR
}};
/**
* Keys are displayed in a natural looking way, like "→" for "RIGHT" or "↲" for ENTER
*/
public static final CharDisplayMap defaultCharDisplay = new CharDisplayMap() {{
putAll(classicArrowsDisplay);
putAll(wellKnownCharactersDisplay);
putAll(nicerLookingDisplay);
// all other characters are displayed as themselves
}};
public static final CharDisplayMap lotsOfArrowsCharDisplay = new CharDisplayMap() {{
putAll(classicArrowsDisplay);
putAll(wellKnownCharactersDisplay);
putAll(lessKnownCharactersDisplay); // NEW
putAll(nicerLookingDisplay);
}};
public static final CharDisplayMap arrowsOnlyCharDisplay = new CharDisplayMap() {{
putAll(classicArrowsDisplay);
// putAll(wellKnownCharactersDisplay); // REMOVED
// putAll(lessKnownCharactersDisplay); // REMOVED
putAll(nicerLookingDisplay);
}};
public static final CharDisplayMap fullIsoCharDisplay = new CharDisplayMap() {{
putAll(classicArrowsDisplay);
putAll(wellKnownCharactersDisplay);
putAll(lessKnownCharactersDisplay); // NEW
putAll(nicerLookingDisplay);
putAll(notKnownIsoCharacters); // NEW
}};
/**
* Some people might call our keys differently
*/
static final CharDisplayMap controlCharsAliases = new CharDisplayMap() {{
put("ESCAPE", "ESC");
put("CONTROL", "CTRL");
put("RETURN", "ENTER"); // Technically different keys, but most applications won't see the difference
put("FUNCTION", "FN");
// no alias for ALT
// Directions are sometimes written as first and last letter for brevety
put("LT", "LEFT");
put("RT", "RIGHT");
put("DN", "DOWN");
// put("UP", "UP"); well, "UP" is already two letters
put("PAGEUP", "PGUP");
put("PAGE_UP", "PGUP");
put("PAGE UP", "PGUP");
put("PAGE-UP", "PGUP");
// no alias for HOME
// no alias for END
put("PAGEDOWN", "PGDN");
put("PAGE_DOWN", "PGDN");
put("PAGE-DOWN", "PGDN");
put("DELETE", "DEL");
put("BACKSPACE", "BKSP");
// easier for writing in termux.properties
put("BACKSLASH", "\\");
put("QUOTE", "\"");
put("APOSTROPHE", "'");
}};
/**
* Applies the 'controlCharsAliases' mapping to all the strings in *buttons*
* Modifies the array, doesn't return a new one.
*/
void replaceAliases(String[][] buttons) {
for(int i = 0; i < buttons.length; i++)
for(int j = 0; j < buttons[i].length; j++)
buttons[i][j] = controlCharsAliases.get(buttons[i][j], buttons[i][j]);
}
/**
* General util function to compute the longest column length in a matrix.
*/
static int maximumLength(String[][] matrix) {
int m = 0;
for (String[] aMatrix : matrix) m = Math.max(m, aMatrix.length);
return m;
}
/**
* Reload the view given parameters in termux.properties
*
* @param buttons matrix of String as defined in termux.properties extrakeys
* Can Contain The Strings CTRL ALT TAB FN ENTER LEFT RIGHT UP DOWN or normal strings
* Some aliases are possible like RETURN for ENTER, LT for LEFT and more (@see controlCharsAliases for the whole list).
* Any string of length > 1 in total Uppercase will print a warning
*
* Examples:
* "ENTER" will trigger the ENTER keycode
* "LEFT" will trigger the LEFT keycode and be displayed as "←"
* "→" will input a "→" character
* "" will input a "" character
* "-_-" will input the string "-_-"
*/
void reload(String[][] buttons, CharDisplayMap charDisplayMap) {
for(SpecialButtonState state : specialButtons.values())
state.button = null;
removeAllViews();
replaceAliases(buttons); // modifies the array
final int rows = buttons.length;
final int cols = maximumLength(buttons);
setRowCount(rows);
setColumnCount(cols);
for (int row = 0; row < rows; row++) {
for (int col = 0; col < buttons[row].length; col++) {
final String buttonText = buttons[row][col];
Button button;
if(Arrays.asList("CTRL", "ALT", "FN").contains(buttonText)) {
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonText)); // for valueOf: https://stackoverflow.com/a/604426/1980630
state.isOn = true;
button = state.button = new ToggleButton(getContext(), null, android.R.attr.buttonBarButtonStyle);
button.setClickable(true);
} else {
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
}
final String displayedText = charDisplayMap.get(buttonText, buttonText);
button.setText(displayedText);
button.setTextColor(TEXT_COLOR);
button.setPadding(0, 0, 0, 0);
final Button finalButton = button;
button.setOnClickListener(v -> {
finalButton.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
View root = getRootView();
if(Arrays.asList("CTRL", "ALT", "FN").contains(buttonText)) {
ToggleButton self = (ToggleButton) finalButton;
self.setChecked(self.isChecked());
self.setTextColor(self.isChecked() ? INTERESTING_COLOR : TEXT_COLOR);
} else {
sendKey(root, buttonText);
}
});
button.setOnTouchListener((v, event) -> {
final View root = getRootView();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
longPressCount = 0;
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
if (Arrays.asList("UP", "DOWN", "LEFT", "RIGHT").contains(buttonText)) {
scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
scheduledExecutor.scheduleWithFixedDelay(() -> {
longPressCount++;
sendKey(root, buttonText);
}, 400, 80, TimeUnit.MILLISECONDS);
}
return true;
case MotionEvent.ACTION_MOVE:
// These two keys have a Move-Up button appearing
if (Arrays.asList("/", "-").contains(buttonText)) {
if (popupWindow == null && event.getY() < 0) {
v.setBackgroundColor(BUTTON_COLOR);
String text = "-".equals(buttonText) ? "|" : "\\";
popup(v, text);
}
if (popupWindow != null && event.getY() > 0) {
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
popupWindow.dismiss();
popupWindow = null;
}
}
return true;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
v.setBackgroundColor(BUTTON_COLOR);
if (scheduledExecutor != null) {
scheduledExecutor.shutdownNow();
scheduledExecutor = null;
}
if (longPressCount == 0) {
if (popupWindow != null && Arrays.asList("/", "-").contains(buttonText)) {
popupWindow.setContentView(null);
popupWindow.dismiss();
popupWindow = null;
sendKey(root, "-".equals(buttonText) ? "|" : "\\");
} else {
v.performClick();
}
}
return true;
default:
return true;
}
});
LayoutParams param = new GridLayout.LayoutParams();
param.width = 0;
if(Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) { // special handle api 21
param.height = (int)(37.5 * getResources().getDisplayMetrics().density + 0.5); // 37.5 equal to R.id.viewpager layout_height / rows in DP
} else {
param.height = 0;
}
param.setMargins(0, 0, 0, 0);
param.columnSpec = GridLayout.spec(col, GridLayout.FILL, 1.f);
param.rowSpec = GridLayout.spec(row, GridLayout.FILL, 1.f);
button.setLayoutParams(param);
addView(button);
}
}
}
}

View File

@@ -0,0 +1,259 @@
package com.termux.app;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Intent;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
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.TERMUX_APP.RUN_COMMAND_SERVICE;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
import com.termux.shared.file.FileUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.notification.NotificationUtils;
import com.termux.app.utils.PluginUtils;
import com.termux.shared.models.ExecutionCommand;
/**
* A service that receives {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent from third party apps and
* plugins that contains info on command execution and forwards the extras to {@link TermuxService}
* for the actual execution.
*
* Check https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent for more info.
*/
public class RunCommandService extends Service {
private static final String LOG_TAG = "RunCommandService";
class LocalBinder extends Binder {
public final RunCommandService service = RunCommandService.this;
}
private final IBinder mBinder = new RunCommandService.LocalBinder();
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
@Override
public void onCreate() {
Logger.logVerbose(LOG_TAG, "onCreate");
runStartForeground();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Logger.logDebug(LOG_TAG, "onStartCommand");
if (intent == null) return Service.START_NOT_STICKY;
// Run again in case service is already started and onCreate() is not called
runStartForeground();
ExecutionCommand executionCommand = new ExecutionCommand();
executionCommand.pluginAPIHelp = this.getString(R.string.error_run_command_service_api_help, RUN_COMMAND_SERVICE.RUN_COMMAND_API_HELP_URL);
Error error;
String errmsg;
// If invalid action passed, then just return
if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) {
errmsg = this.getString(R.string.error_run_command_service_invalid_intent_action, intent.getAction());
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return Service.START_NOT_STICKY;
}
executionCommand.executable = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH, null);
executionCommand.arguments = IntentUtils.getStringArrayExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS, null);
/*
* If intent was sent with `am` command, then normal comma characters may have been replaced
* with alternate characters if a normal comma existed in an argument itself to prevent it
* splitting into multiple arguments by `am` command.
* If `tudo` or `sudo` are used, then simply using their `-r` and `--comma-alternative` command
* options can be used without passing the below extras, but native supports is helpful if
* they are not being used.
* https://github.com/agnostic-apollo/tudo#passing-arguments-using-run_command-intent
* https://android.googlesource.com/platform/frameworks/base/+/21bdaf1/cmds/am/src/com/android/commands/am/Am.java#572
*/
boolean replaceCommaAlternativeCharsInArguments = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS, false);
if (replaceCommaAlternativeCharsInArguments) {
String commaAlternativeCharsInArguments = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS, null);
if (commaAlternativeCharsInArguments == null)
commaAlternativeCharsInArguments = TermuxConstants.COMMA_ALTERNATIVE;
// Replace any commaAlternativeCharsInArguments characters with normal commas
DataUtils.replaceSubStringsInStringArrayItems(executionCommand.arguments, commaAlternativeCharsInArguments, TermuxConstants.COMMA_NORMAL);
}
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.sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION);
executionCommand.commandLabel = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_LABEL, "RUN_COMMAND Execution Intent Command");
executionCommand.commandDescription = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION, null);
executionCommand.commandHelp = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_HELP, null);
executionCommand.isPluginExecutionCommand = true;
executionCommand.resultConfig.resultPendingIntent = intent.getParcelableExtra(RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT);
executionCommand.resultConfig.resultDirectoryPath = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_DIRECTORY, null);
if (executionCommand.resultConfig.resultDirectoryPath != null) {
executionCommand.resultConfig.resultSingleFile = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_RESULT_SINGLE_FILE, false);
executionCommand.resultConfig.resultFileBasename = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_BASENAME, null);
executionCommand.resultConfig.resultFileOutputFormat = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, null);
executionCommand.resultConfig.resultFileErrorFormat = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, null);
executionCommand.resultConfig.resultFilesSuffix = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILES_SUFFIX, null);
}
// If "allow-external-apps" property to not set to "true", then just return
// We enable force notifications if "allow-external-apps" policy is violated so that the
// user knows someone tried to run a command in termux context, since it may be malicious
// app or imported (tasker) plugin project and not the user himself. If a pending intent is
// also sent, then its creator is also logged and shown.
errmsg = PluginUtils.checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(this);
if (errmsg != null) {
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true);
return Service.START_NOT_STICKY;
}
// If executable is null or empty, then exit here instead of getting canonical path which would expand to "/"
if (executionCommand.executable == null || executionCommand.executable.isEmpty()) {
errmsg = this.getString(R.string.error_run_command_service_mandatory_extra_missing, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH);
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return Service.START_NOT_STICKY;
}
// Get canonical path of executable
executionCommand.executable = TermuxFileUtils.getCanonicalPath(executionCommand.executable, null, true);
// If executable is not a regular file, or is not readable or executable, then just return
// Setting of missing read and execute permissions is not done
error = FileUtils.validateRegularFileExistenceAndPermissions("executable", executionCommand.executable, null,
FileUtils.APP_EXECUTABLE_FILE_PERMISSIONS, true, true,
false);
if (error != null) {
error.appendMessage("\n" + this.getString(R.string.msg_executable_absolute_path, executionCommand.executable));
executionCommand.setStateFailed(error);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return Service.START_NOT_STICKY;
}
// If workingDirectory is not null or empty
if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) {
// Get canonical path of workingDirectory
executionCommand.workingDirectory = TermuxFileUtils.getCanonicalPath(executionCommand.workingDirectory, null, true);
// If workingDirectory is not a directory, or is not readable or writable, then just return
// Creation of missing directory and setting of read, write and execute permissions are only done if workingDirectory is
// under allowed termux working directory paths.
// We try to set execute permissions, but ignore if they are missing, since only read and write permissions are required
// for working directories.
error = TermuxFileUtils.validateDirectoryFileExistenceAndPermissions("working", executionCommand.workingDirectory,
true, true, true,
false, true);
if (error != null) {
error.appendMessage("\n" + this.getString(R.string.msg_working_directory_absolute_path, executionCommand.workingDirectory));
executionCommand.setStateFailed(error);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return Service.START_NOT_STICKY;
}
}
executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(TermuxFileUtils.getExpandedTermuxPath(executionCommand.executable)).build();
Logger.logVerbose(LOG_TAG, executionCommand.toString());
// Create execution intent with the action TERMUX_SERVICE#ACTION_SERVICE_EXECUTE to be sent to the TERMUX_SERVICE
Intent execIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, executionCommand.executableUri);
execIntent.setClass(this, TermuxService.class);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, executionCommand.arguments);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_STDIN, executionCommand.stdin);
if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) execIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, executionCommand.workingDirectory);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, executionCommand.inBackground);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION, executionCommand.sessionAction);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_LABEL, executionCommand.commandLabel);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, executionCommand.commandDescription);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_HELP, executionCommand.commandHelp);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP, executionCommand.pluginAPIHelp);
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
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
this.startForegroundService(execIntent);
} else {
this.startService(execIntent);
}
runStopForeground();
return Service.START_NOT_STICKY;
}
private void runStartForeground() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
setupNotificationChannel();
startForeground(TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_ID, buildNotification());
}
}
private void runStopForeground() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
stopForeground(true);
}
}
private Notification buildNotification() {
// Build the notification
Notification.Builder builder = NotificationUtils.geNotificationBuilder(this,
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_LOW,
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_NAME, null, null,
null, NotificationUtils.NOTIFICATION_MODE_SILENT);
if (builder == null) return null;
// No need to show a timestamp:
builder.setShowWhen(false);
// Set notification icon
builder.setSmallIcon(R.drawable.ic_service_notification);
// Set background color for small notification icon
builder.setColor(0xFF607D8B);
return builder.build();
}
private void setupNotificationChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
NotificationUtils.setupNotificationChannel(this, TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_ID,
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
package com.termux.app;
import android.app.Application;
import com.termux.shared.crash.TermuxCrashUtils;
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
import com.termux.shared.logger.Logger;
public class TermuxApplication extends Application {
public void onCreate() {
super.onCreate();
// Set crash handler for the app
TermuxCrashUtils.setCrashHandler(this);
// Set log level for the app
setLogLevel();
}
private void setLogLevel() {
// Load the log level from shared preferences and set it to the {@link Logger.CURRENT_LOG_LEVEL}
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(getApplicationContext());
if (preferences == null) return;
preferences.setLogLevel(null, preferences.getLogLevel());
Logger.logDebug("Starting Application");
}
}

View File

@@ -4,26 +4,26 @@ import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.Build;
import android.os.Environment;
import android.os.UserManager;
import android.system.Os;
import android.util.Log;
import android.util.Pair;
import android.view.WindowManager;
import com.termux.R;
import com.termux.terminal.EmulatorDebug;
import com.termux.app.utils.CrashUtils;
import com.termux.shared.file.FileUtils;
import com.termux.shared.interact.MessageDialogUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.models.errors.Error;
import com.termux.shared.termux.TermuxConstants;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@@ -32,13 +32,13 @@ import java.util.zip.ZipInputStream;
* Install the Termux bootstrap packages if necessary by following the below steps:
* <p/>
* (1) If $PREFIX already exist, assume that it is correct and be done. Note that this relies on that we do not create a
* broken $PREFIX folder below.
* broken $PREFIX directory below.
* <p/>
* (2) A progress dialog is shown with "Installing..." message and a spinner.
* <p/>
* (3) A staging folder, $STAGING_PREFIX, is {@link #deleteFolder(File)} if left over from broken installation below.
* (3) A staging directory, $STAGING_PREFIX, is cleared if left over from broken installation below.
* <p/>
* (4) The architecture is determined and an appropriate bootstrap zip url is determined in {@link #determineZipUrl()}.
* (4) The zip file is loaded from a shared library.
* <p/>
* (5) The zip, containing entries relative to the $PREFIX, is is downloaded and extracted by a zip input stream
* continuously encountering zip file entries:
@@ -49,22 +49,38 @@ import java.util.zip.ZipInputStream;
*/
final class TermuxInstaller {
/** Performs setup if necessary. */
static void setupIfNeeded(final Activity activity, final Runnable whenDone) {
private static final String LOG_TAG = "TermuxInstaller";
/** Performs bootstrap setup if necessary. */
static void setupBootstrapIfNeeded(final Activity activity, final Runnable whenDone) {
// Termux can only be run as the primary user (device owner) since only that
// account has the expected file system paths. Verify that:
UserManager um = (UserManager) activity.getSystemService(Context.USER_SERVICE);
boolean isPrimaryUser = um.getSerialNumberForUser(android.os.Process.myUserHandle()) == 0;
if (!isPrimaryUser) {
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_not_primary_user_message)
.setOnDismissListener(dialog -> System.exit(0)).setPositiveButton(android.R.string.ok, null).show();
String bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message, TermuxConstants.TERMUX_PREFIX_DIR_PATH);
Logger.logError(LOG_TAG, bootstrapErrorMessage);
MessageDialogUtils.exitAppWithErrorMessage(activity,
activity.getString(R.string.bootstrap_error_title),
bootstrapErrorMessage);
return;
}
final File PREFIX_FILE = new File(TermuxService.PREFIX_PATH);
if (PREFIX_FILE.isDirectory()) {
whenDone.run();
return;
final String PREFIX_FILE_PATH = TermuxConstants.TERMUX_PREFIX_DIR_PATH;
final File PREFIX_FILE = TermuxConstants.TERMUX_PREFIX_DIR;
// 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)) {
File[] PREFIX_FILE_LIST = PREFIX_FILE.listFiles();
// If prefix directory is empty or only contains the tmp directory
if(PREFIX_FILE_LIST == null || PREFIX_FILE_LIST.length == 0 || (PREFIX_FILE_LIST.length == 1 && TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH.equals(PREFIX_FILE_LIST[0].getAbsolutePath()))) {
Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" exists but is empty or only contains the tmp directory.");
} else {
whenDone.run();
return;
}
} 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.");
}
final ProgressDialog progress = ProgressDialog.show(activity, null, activity.getString(R.string.bootstrap_installer_body), true, false);
@@ -72,18 +88,34 @@ final class TermuxInstaller {
@Override
public void run() {
try {
final String STAGING_PREFIX_PATH = TermuxService.FILES_PATH + "/usr-staging";
Logger.logInfo(LOG_TAG, "Installing " + TermuxConstants.TERMUX_APP_NAME + " bootstrap packages.");
Error error;
final String STAGING_PREFIX_PATH = TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH;
final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH);
if (STAGING_PREFIX_FILE.exists()) {
deleteFolder(STAGING_PREFIX_FILE);
// Delete prefix staging directory or any file at its destination
error = FileUtils.deleteFile("prefix staging directory", STAGING_PREFIX_PATH, true);
if (error != null) {
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
return;
}
// Delete prefix directory or any file at its destination
error = FileUtils.deleteFile("prefix directory", PREFIX_FILE_PATH, true);
if (error != null) {
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
return;
}
Logger.logInfo(LOG_TAG, "Extracting bootstrap zip to prefix staging directory \"" + STAGING_PREFIX_PATH + "\".");
final byte[] buffer = new byte[8096];
final List<Pair<String, String>> symlinks = new ArrayList<>(50);
final URL zipUrl = determineZipUrl();
try (ZipInputStream zipInput = new ZipInputStream(zipUrl.openStream())) {
final byte[] zipBytes = loadZipBytes();
try (ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
ZipEntry zipEntry;
while ((zipEntry = zipInput.getNextEntry()) != null) {
if (zipEntry.getName().equals("SYMLINKS.txt")) {
@@ -97,14 +129,22 @@ final class TermuxInstaller {
String newPath = STAGING_PREFIX_PATH + "/" + parts[1];
symlinks.add(Pair.create(oldPath, newPath));
ensureDirectoryExists(new File(newPath).getParentFile());
error = ensureDirectoryExists(new File(newPath).getParentFile());
if (error != null) {
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
return;
}
}
} else {
String zipEntryName = zipEntry.getName();
File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName);
boolean isDirectory = zipEntry.isDirectory();
ensureDirectoryExists(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) {
try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
@@ -127,27 +167,18 @@ final class TermuxInstaller {
Os.symlink(symlink.first, symlink.second);
}
Logger.logInfo(LOG_TAG, "Moving prefix staging to prefix directory.");
if (!STAGING_PREFIX_FILE.renameTo(PREFIX_FILE)) {
throw new RuntimeException("Unable to rename staging folder");
throw new RuntimeException("Moving prefix staging to prefix directory failed");
}
Logger.logInfo(LOG_TAG, "Bootstrap packages installed successfully.");
activity.runOnUiThread(whenDone);
} catch (final Exception e) {
Log.e(EmulatorDebug.LOG_TAG, "Bootstrap error", 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.setupIfNeeded(activity, whenDone);
}).show();
} catch (WindowManager.BadTokenException e1) {
// Activity already dismissed - ignore.
}
});
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)));
} finally {
activity.runOnUiThread(() -> {
try {
@@ -161,79 +192,51 @@ final class TermuxInstaller {
}.start();
}
private static void ensureDirectoryExists(File directory) {
if (!directory.isDirectory() && !directory.mkdirs()) {
throw new RuntimeException("Unable to create directory: " + directory.getAbsolutePath());
}
}
public static void showBootstrapErrorDialog(Activity activity, String PREFIX_FILE_PATH, Runnable whenDone, String message) {
Logger.logErrorExtended(LOG_TAG, "Bootstrap Error:\n" + message);
/** Get bootstrap zip url for this systems cpu architecture. */
private static URL determineZipUrl() throws MalformedURLException {
String archName = determineTermuxArchName();
String url = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
? "https://termux.org/bootstrap-" + archName + ".zip"
: "https://termux.net/bootstrap/bootstrap-" + archName + ".zip";
return new URL(url);
}
// 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);
private static String determineTermuxArchName() {
// Note that we cannot use System.getProperty("os.arch") since that may give e.g. "aarch64"
// while a 64-bit runtime may not be installed (like on the Samsung Galaxy S5 Neo).
// Instead we search through the supported abi:s on the device, see:
// http://developer.android.com/ndk/guides/abis.html
// Note that we search for abi:s in preferred order (the ordering of the
// Build.SUPPORTED_ABIS list) to avoid e.g. installing arm on an x86 system where arm
// emulation is available.
for (String androidArch : Build.SUPPORTED_ABIS) {
switch (androidArch) {
case "arm64-v8a": return "aarch64";
case "armeabi-v7a": return "arm";
case "x86_64": return "x86_64";
case "x86": return "i686";
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.
}
}
throw new RuntimeException("Unable to determine arch from Build.SUPPORTED_ABIS = " +
Arrays.toString(Build.SUPPORTED_ABIS));
}
/** Delete a folder and all its content or throw. Don't follow symlinks. */
static void deleteFolder(File fileOrDirectory) throws IOException {
if (fileOrDirectory.getCanonicalPath().equals(fileOrDirectory.getAbsolutePath()) && fileOrDirectory.isDirectory()) {
File[] children = fileOrDirectory.listFiles();
if (children != null) {
for (File child : children) {
deleteFolder(child);
}
}
}
if (!fileOrDirectory.delete()) {
throw new RuntimeException("Unable to delete " + (fileOrDirectory.isDirectory() ? "directory " : "file ") + fileOrDirectory.getAbsolutePath());
}
});
}
static void setupStorageSymlinks(final Context context) {
final String LOG_TAG = "termux-storage";
Logger.logInfo(LOG_TAG, "Setting up storage symlinks.");
new Thread() {
public void run() {
try {
File storageDir = new File(TermuxService.HOME_PATH, "storage");
Error error;
File storageDir = TermuxConstants.TERMUX_STORAGE_HOME_DIR;
if (storageDir.exists()) {
try {
deleteFolder(storageDir);
} catch (IOException e) {
Log.e(LOG_TAG, "Could not delete old $HOME/storage, " + e.getMessage());
return;
}
}
if (!storageDir.mkdirs()) {
Log.e(LOG_TAG, "Unable to mkdirs() for $HOME/storage");
error = FileUtils.clearDirectory("~/storage", storageDir.getAbsolutePath());
if (error != null) {
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;
}
Logger.logInfo(LOG_TAG, "Setting up storage symlinks at ~/storage/shared, ~/storage/downloads, ~/storage/dcim, ~/storage/pictures, ~/storage/music and ~/storage/movies for directories in \"" + Environment.getExternalStorageDirectory().getAbsolutePath() + "\".");
File sharedDir = Environment.getExternalStorageDirectory();
Os.symlink(sharedDir.getAbsolutePath(), new File(storageDir, "shared").getAbsolutePath());
@@ -258,14 +261,31 @@ final class TermuxInstaller {
File dir = dirs[i];
if (dir == null) continue;
String symlinkName = "external-" + i;
Logger.logInfo(LOG_TAG, "Setting up storage symlinks at ~/storage/" + symlinkName + " for \"" + dir.getAbsolutePath() + "\".");
Os.symlink(dir.getAbsolutePath(), new File(storageDir, symlinkName).getAbsolutePath());
}
}
Logger.logInfo(LOG_TAG, "Storage symlinks created successfully.");
} catch (Exception e) {
Log.e(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();
}
private static Error ensureDirectoryExists(File directory) {
return FileUtils.createDirectoryFile(directory.getAbsolutePath());
}
public static byte[] loadZipBytes() {
// Only load the shared library when necessary to save memory usage.
System.loadLibrary("termux-bootstrap");
return getZip();
}
public static native byte[] getZip();
}

View File

@@ -11,10 +11,10 @@ import android.net.Uri;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore;
import android.util.Log;
import android.webkit.MimeTypeMap;
import com.termux.terminal.EmulatorDebug;
import com.termux.shared.logger.Logger;
import com.termux.shared.termux.TermuxConstants;
import java.io.File;
import java.io.FileNotFoundException;
@@ -24,11 +24,13 @@ import androidx.annotation.NonNull;
public class TermuxOpenReceiver extends BroadcastReceiver {
private static final String LOG_TAG = "TermuxOpenReceiver";
@Override
public void onReceive(Context context, Intent intent) {
final Uri data = intent.getData();
if (data == null) {
Log.e(EmulatorDebug.LOG_TAG, "termux-open: Called without intent data");
Logger.logError(LOG_TAG, "termux-open: Called without intent data");
return;
}
@@ -42,7 +44,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
// Ok.
break;
default:
Log.e(EmulatorDebug.LOG_TAG, "Invalid action '" + intentAction + "', using 'view'");
Logger.logError(LOG_TAG, "Invalid action '" + intentAction + "', using 'view'");
break;
}
@@ -59,14 +61,14 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
try {
context.startActivity(urlIntent);
} catch (ActivityNotFoundException e) {
Log.e(EmulatorDebug.LOG_TAG, "termux-open: No app handles the url " + data);
Logger.logError(LOG_TAG, "termux-open: No app handles the url " + data);
}
return;
}
final File fileToShare = new File(filePath);
if (!(fileToShare.isFile() && fileToShare.canRead())) {
Log.e(EmulatorDebug.LOG_TAG, "termux-open: Not a readable file: '" + fileToShare.getAbsolutePath() + "'");
Logger.logError(LOG_TAG, "termux-open: Not a readable file: '" + fileToShare.getAbsolutePath() + "'");
return;
}
@@ -87,7 +89,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
contentTypeToUse = contentTypeExtra;
}
Uri uriToShare = Uri.parse("content://com.termux.files" + fileToShare.getAbsolutePath());
Uri uriToShare = Uri.parse("content://" + TermuxConstants.TERMUX_FILE_SHARE_URI_AUTHORITY + fileToShare.getAbsolutePath());
if (Intent.ACTION_SEND.equals(intentAction)) {
sendIntent.putExtra(Intent.EXTRA_STREAM, uriToShare);
@@ -103,7 +105,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
try {
context.startActivity(sendIntent);
} catch (ActivityNotFoundException e) {
Log.e(EmulatorDebug.LOG_TAG, "termux-open: No app handles the url " + data);
Logger.logError(LOG_TAG, "termux-open: No app handles the url " + data);
}
}
@@ -178,7 +180,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
String path = file.getCanonicalPath();
String storagePath = Environment.getExternalStorageDirectory().getCanonicalPath();
// See https://support.google.com/faqs/answer/7496913:
if (!(path.startsWith(TermuxService.FILES_PATH) || path.startsWith(storagePath))) {
if (!(path.startsWith(TermuxConstants.TERMUX_FILES_DIR_PATH) || path.startsWith(storagePath))) {
throw new IllegalArgumentException("Invalid path: " + path);
}
} catch (IOException e) {

View File

@@ -1,225 +0,0 @@
package com.termux.app;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.Log;
import android.util.TypedValue;
import android.widget.Toast;
import com.termux.terminal.TerminalSession;
import org.json.JSONArray;
import org.json.JSONException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import androidx.annotation.IntDef;
final class TermuxPreferences {
@IntDef({BELL_VIBRATE, BELL_BEEP, BELL_IGNORE})
@Retention(RetentionPolicy.SOURCE)
@interface AsciiBellBehaviour {
}
final static class KeyboardShortcut {
KeyboardShortcut(int codePoint, int shortcutAction) {
this.codePoint = codePoint;
this.shortcutAction = shortcutAction;
}
final int codePoint;
final int shortcutAction;
}
static final int SHORTCUT_ACTION_CREATE_SESSION = 1;
static final int SHORTCUT_ACTION_NEXT_SESSION = 2;
static final int SHORTCUT_ACTION_PREVIOUS_SESSION = 3;
static final int SHORTCUT_ACTION_RENAME_SESSION = 4;
static final int BELL_VIBRATE = 1;
static final int BELL_BEEP = 2;
static final int BELL_IGNORE = 3;
private final int MIN_FONTSIZE;
private static final int MAX_FONTSIZE = 256;
private static final String SHOW_EXTRA_KEYS_KEY = "show_extra_keys";
private static final String FONTSIZE_KEY = "fontsize";
private static final String CURRENT_SESSION_KEY = "current_session";
private static final String SCREEN_ALWAYS_ON_KEY = "screen_always_on";
private boolean mScreenAlwaysOn;
private int mFontSize;
@AsciiBellBehaviour
int mBellBehaviour = BELL_VIBRATE;
boolean mBackIsEscape;
boolean mShowExtraKeys;
String[][] mExtraKeys;
final List<KeyboardShortcut> shortcuts = new ArrayList<>();
/**
* If value is not in the range [min, max], set it to either min or max.
*/
static int clamp(int value, int min, int max) {
return Math.min(Math.max(value, min), max);
}
TermuxPreferences(Context context) {
reloadFromProperties(context);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, context.getResources().getDisplayMetrics());
// This is a bit arbitrary and sub-optimal. We want to give a sensible default for minimum font size
// to prevent invisible text due to zoom be mistake:
MIN_FONTSIZE = (int) (4f * dipInPixels);
mShowExtraKeys = prefs.getBoolean(SHOW_EXTRA_KEYS_KEY, true);
mScreenAlwaysOn = prefs.getBoolean(SCREEN_ALWAYS_ON_KEY, false);
// http://www.google.com/design/spec/style/typography.html#typography-line-height
int defaultFontSize = Math.round(12 * dipInPixels);
// Make it divisible by 2 since that is the minimal adjustment step:
if (defaultFontSize % 2 == 1) defaultFontSize--;
try {
mFontSize = Integer.parseInt(prefs.getString(FONTSIZE_KEY, Integer.toString(defaultFontSize)));
} catch (NumberFormatException | ClassCastException e) {
mFontSize = defaultFontSize;
}
mFontSize = clamp(mFontSize, MIN_FONTSIZE, MAX_FONTSIZE);
}
boolean toggleShowExtraKeys(Context context) {
mShowExtraKeys = !mShowExtraKeys;
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SHOW_EXTRA_KEYS_KEY, mShowExtraKeys).apply();
return mShowExtraKeys;
}
int getFontSize() {
return mFontSize;
}
void changeFontSize(Context context, boolean increase) {
mFontSize += (increase ? 1 : -1) * 2;
mFontSize = Math.max(MIN_FONTSIZE, Math.min(mFontSize, MAX_FONTSIZE));
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
prefs.edit().putString(FONTSIZE_KEY, Integer.toString(mFontSize)).apply();
}
boolean isScreenAlwaysOn() {
return mScreenAlwaysOn;
}
void setScreenAlwaysOn(Context context, boolean newValue) {
mScreenAlwaysOn = newValue;
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SCREEN_ALWAYS_ON_KEY, newValue).apply();
}
static void storeCurrentSession(Context context, TerminalSession session) {
PreferenceManager.getDefaultSharedPreferences(context).edit().putString(TermuxPreferences.CURRENT_SESSION_KEY, session.mHandle).apply();
}
static TerminalSession getCurrentSession(TermuxActivity context) {
String sessionHandle = PreferenceManager.getDefaultSharedPreferences(context).getString(TermuxPreferences.CURRENT_SESSION_KEY, "");
for (int i = 0, len = context.mTermService.getSessions().size(); i < len; i++) {
TerminalSession session = context.mTermService.getSessions().get(i);
if (session.mHandle.equals(sessionHandle)) return session;
}
return null;
}
void reloadFromProperties(Context context) {
File propsFile = new File(TermuxService.HOME_PATH + "/.termux/termux.properties");
if (!propsFile.exists())
propsFile = new File(TermuxService.HOME_PATH + "/.config/termux/termux.properties");
Properties props = new Properties();
try {
if (propsFile.isFile() && propsFile.canRead()) {
try (FileInputStream in = new FileInputStream(propsFile)) {
props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
}
}
} catch (IOException e) {
Toast.makeText(context, "Could not open properties file termux.properties.", Toast.LENGTH_LONG).show();
Log.e("termux", "Error loading props", e);
}
switch (props.getProperty("bell-character", "vibrate")) {
case "beep":
mBellBehaviour = BELL_BEEP;
break;
case "ignore":
mBellBehaviour = BELL_IGNORE;
break;
default: // "vibrate".
mBellBehaviour = BELL_VIBRATE;
break;
}
try {
JSONArray arr = new JSONArray(props.getProperty("extra-keys", "[['ESC', 'TAB', 'CTRL', 'ALT', '-', 'DOWN', 'UP']]"));
mExtraKeys = new String[arr.length()][];
for (int i = 0; i < arr.length(); i++) {
JSONArray line = arr.getJSONArray(i);
mExtraKeys[i] = new String[line.length()];
for (int j = 0; j < line.length(); j++) {
mExtraKeys[i][j] = line.getString(j);
}
}
} catch (JSONException e) {
Toast.makeText(context, "Could not load the extra-keys property from the config: " + e.toString(), Toast.LENGTH_LONG).show();
Log.e("termux", "Error loading props", e);
mExtraKeys = new String[0][];
}
mBackIsEscape = "escape".equals(props.getProperty("back-key", "back"));
shortcuts.clear();
parseAction("shortcut.create-session", SHORTCUT_ACTION_CREATE_SESSION, props);
parseAction("shortcut.next-session", SHORTCUT_ACTION_NEXT_SESSION, props);
parseAction("shortcut.previous-session", SHORTCUT_ACTION_PREVIOUS_SESSION, props);
parseAction("shortcut.rename-session", SHORTCUT_ACTION_RENAME_SESSION, props);
}
private void parseAction(String name, int shortcutAction, Properties props) {
String value = props.getProperty(name);
if (value == null) return;
String[] parts = value.toLowerCase().trim().split("\\+");
String input = parts.length == 2 ? parts[1].trim() : null;
if (!(parts.length == 2 && parts[0].trim().equals("ctrl")) || input.isEmpty() || input.length() > 2) {
Log.e("termux", "Keyboard shortcut '" + name + "' is not Ctrl+<something>");
return;
}
char c = input.charAt(0);
int codePoint = c;
if (Character.isLowSurrogate(c)) {
if (input.length() != 2 || Character.isHighSurrogate(input.charAt(1))) {
Log.e("termux", "Keyboard shortcut '" + name + "' is not Ctrl+<something>");
return;
} else {
codePoint = Character.toCodePoint(input.charAt(1), c);
}
}
shortcuts.add(new KeyboardShortcut(codePoint, shortcutAction));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,281 +0,0 @@
package com.termux.app;
import android.content.Context;
import android.media.AudioManager;
import android.view.Gravity;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.inputmethod.InputMethodManager;
import com.termux.terminal.KeyHandler;
import com.termux.terminal.TerminalEmulator;
import com.termux.terminal.TerminalSession;
import com.termux.view.TerminalViewClient;
import java.util.List;
import androidx.drawerlayout.widget.DrawerLayout;
public final class TermuxViewClient implements TerminalViewClient {
final TermuxActivity mActivity;
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
public TermuxViewClient(TermuxActivity activity) {
this.mActivity = activity;
}
@Override
public float onScale(float scale) {
if (scale < 0.9f || scale > 1.1f) {
boolean increase = scale > 1.f;
mActivity.changeFontSize(increase);
return 1.0f;
}
return scale;
}
@Override
public void onSingleTapUp(MotionEvent e) {
InputMethodManager mgr = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
mgr.showSoftInput(mActivity.mTerminalView, InputMethodManager.SHOW_IMPLICIT);
}
@Override
public boolean shouldBackButtonBeMappedToEscape() {
return mActivity.mSettings.mBackIsEscape;
}
@Override
public void copyModeChanged(boolean copyMode) {
// Disable drawer while copying.
mActivity.getDrawer().setDrawerLockMode(copyMode ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED : DrawerLayout.LOCK_MODE_UNLOCKED);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession currentSession) {
if (handleVirtualKeys(keyCode, e, true)) return true;
if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) {
mActivity.removeFinishedSession(currentSession);
return true;
} else if (e.isCtrlPressed() && e.isAltPressed()) {
// Get the unmodified code point:
int unicodeChar = e.getUnicodeChar(0);
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || unicodeChar == 'n'/* next */) {
mActivity.switchToSession(true);
} else if (keyCode == KeyEvent.KEYCODE_DPAD_UP || unicodeChar == 'p' /* previous */) {
mActivity.switchToSession(false);
} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
mActivity.getDrawer().openDrawer(Gravity.LEFT);
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
mActivity.getDrawer().closeDrawers();
} else if (unicodeChar == 'k'/* keyboard */) {
InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
} else if (unicodeChar == 'm'/* menu */) {
mActivity.mTerminalView.showContextMenu();
} else if (unicodeChar == 'r'/* rename */) {
mActivity.renameSession(currentSession);
} else if (unicodeChar == 'c'/* create */) {
mActivity.addNewSession(false, null);
} else if (unicodeChar == 'u' /* urls */) {
mActivity.showUrlSelection();
} else if (unicodeChar == 'v') {
mActivity.doPaste();
} else if (unicodeChar == '+' || e.getUnicodeChar(KeyEvent.META_SHIFT_ON) == '+') {
// We also check for the shifted char here since shift may be required to produce '+',
// see https://github.com/termux/termux-api/issues/2
mActivity.changeFontSize(true);
} else if (unicodeChar == '-') {
mActivity.changeFontSize(false);
} else if (unicodeChar >= '1' && unicodeChar <= '9') {
int num = unicodeChar - '1';
TermuxService service = mActivity.mTermService;
if (service.getSessions().size() > num)
mActivity.switchToSession(service.getSessions().get(num));
}
return true;
}
return false;
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent e) {
return handleVirtualKeys(keyCode, e, false);
}
@Override
public boolean readControlKey() {
return (mActivity.mExtraKeysView != null && mActivity.mExtraKeysView.readSpecialButton(ExtraKeysView.SpecialButton.CTRL)) || mVirtualControlKeyDown;
}
@Override
public boolean readAltKey() {
return (mActivity.mExtraKeysView != null && mActivity.mExtraKeysView.readSpecialButton(ExtraKeysView.SpecialButton.ALT));
}
@Override
public boolean onCodePoint(final int codePoint, boolean ctrlDown, TerminalSession session) {
if (mVirtualFnKeyDown) {
int resultingKeyCode = -1;
int resultingCodePoint = -1;
boolean altDown = false;
int lowerCase = Character.toLowerCase(codePoint);
switch (lowerCase) {
// Arrow keys.
case 'w':
resultingKeyCode = KeyEvent.KEYCODE_DPAD_UP;
break;
case 'a':
resultingKeyCode = KeyEvent.KEYCODE_DPAD_LEFT;
break;
case 's':
resultingKeyCode = KeyEvent.KEYCODE_DPAD_DOWN;
break;
case 'd':
resultingKeyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
break;
// Page up and down.
case 'p':
resultingKeyCode = KeyEvent.KEYCODE_PAGE_UP;
break;
case 'n':
resultingKeyCode = KeyEvent.KEYCODE_PAGE_DOWN;
break;
// Some special keys:
case 't':
resultingKeyCode = KeyEvent.KEYCODE_TAB;
break;
case 'i':
resultingKeyCode = KeyEvent.KEYCODE_INSERT;
break;
case 'h':
resultingCodePoint = '~';
break;
// Special characters to input.
case 'u':
resultingCodePoint = '_';
break;
case 'l':
resultingCodePoint = '|';
break;
// Function keys.
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
resultingKeyCode = (codePoint - '1') + KeyEvent.KEYCODE_F1;
break;
case '0':
resultingKeyCode = KeyEvent.KEYCODE_F10;
break;
// Other special keys.
case 'e':
resultingCodePoint = /*Escape*/ 27;
break;
case '.':
resultingCodePoint = /*^.*/ 28;
break;
case 'b': // alt+b, jumping backward in readline.
case 'f': // alf+f, jumping forward in readline.
case 'x': // alt+x, common in emacs.
resultingCodePoint = lowerCase;
altDown = true;
break;
// Volume control.
case 'v':
resultingCodePoint = -1;
AudioManager audio = (AudioManager) mActivity.getSystemService(Context.AUDIO_SERVICE);
audio.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME, AudioManager.USE_DEFAULT_STREAM_TYPE, AudioManager.FLAG_SHOW_UI);
break;
// Writing mode:
case 'q':
case 'k':
mActivity.toggleShowExtraKeys();
break;
}
if (resultingKeyCode != -1) {
TerminalEmulator term = session.getEmulator();
session.write(KeyHandler.getCode(resultingKeyCode, 0, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode()));
} else if (resultingCodePoint != -1) {
session.writeCodePoint(altDown, resultingCodePoint);
}
return true;
} else if (ctrlDown) {
if (codePoint == 106 /* Ctrl+j or \n */ && !session.isRunning()) {
mActivity.removeFinishedSession(session);
return true;
}
List<TermuxPreferences.KeyboardShortcut> shortcuts = mActivity.mSettings.shortcuts;
if (!shortcuts.isEmpty()) {
int codePointLowerCase = Character.toLowerCase(codePoint);
for (int i = shortcuts.size() - 1; i >= 0; i--) {
TermuxPreferences.KeyboardShortcut shortcut = shortcuts.get(i);
if (codePointLowerCase == shortcut.codePoint) {
switch (shortcut.shortcutAction) {
case TermuxPreferences.SHORTCUT_ACTION_CREATE_SESSION:
mActivity.addNewSession(false, null);
return true;
case TermuxPreferences.SHORTCUT_ACTION_PREVIOUS_SESSION:
mActivity.switchToSession(false);
return true;
case TermuxPreferences.SHORTCUT_ACTION_NEXT_SESSION:
mActivity.switchToSession(true);
return true;
case TermuxPreferences.SHORTCUT_ACTION_RENAME_SESSION:
mActivity.renameSession(mActivity.getCurrentTermSession());
return true;
}
}
}
}
}
return false;
}
@Override
public boolean onLongPress(MotionEvent event) {
return false;
}
/** Handle dedicated volume buttons as virtual keys if applicable. */
private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) {
InputDevice inputDevice = event.getDevice();
if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
// Do not steal dedicated buttons from a full external keyboard.
return false;
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
mVirtualControlKeyDown = down;
return true;
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
mVirtualFnKeyDown = down;
return true;
}
return false;
}
}

View File

@@ -1,4 +1,4 @@
package com.termux.app;
package com.termux.app.activities;
import android.app.Activity;
import android.content.ActivityNotFoundException;
@@ -12,8 +12,10 @@ import android.webkit.WebViewClient;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import com.termux.shared.termux.TermuxConstants;
/** Basic embedded browser for viewing help pages. */
public final class TermuxHelpActivity extends Activity {
public final class HelpActivity extends Activity {
WebView mWebView;
@@ -39,7 +41,7 @@ public final class TermuxHelpActivity extends Activity {
mWebView.setWebViewClient(new WebViewClient() {
@Override
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.
setContentView(progressLayout);
return false;
@@ -60,7 +62,7 @@ public final class TermuxHelpActivity extends Activity {
setContentView(mWebView);
}
});
mWebView.loadUrl("https://wiki.termux.com/wiki/Main_Page");
mWebView.loadUrl(TermuxConstants.TERMUX_WIKI_URL);
}
@Override

View File

@@ -0,0 +1,123 @@
package com.termux.app.activities;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import com.termux.R;
import com.termux.shared.activities.ReportActivity;
import com.termux.shared.models.ReportInfo;
import com.termux.app.models.UserAction;
import com.termux.shared.interact.ShareUtils;
import com.termux.shared.packages.PackageUtils;
import com.termux.shared.settings.preferences.TermuxTaskerAppSharedPreferences;
import com.termux.shared.termux.AndroidUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxUtils;
public class SettingsActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
if (savedInstanceState == null) {
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.settings, new RootPreferencesFragment())
.commit();
}
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setDisplayShowHomeEnabled(true);
}
}
@Override
public boolean onSupportNavigateUp() {
onBackPressed();
return true;
}
public static class RootPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
setPreferencesFromResource(R.xml.root_preferences, rootKey);
configureTermuxTaskerPreference(context);
configureAboutPreference(context);
configureDonatePreference(context);
}
private void configureTermuxTaskerPreference(@NonNull Context context) {
Preference termuxTaskerPrefernce = findPreference("termux_tasker");
if (termuxTaskerPrefernce != null) {
TermuxTaskerAppSharedPreferences preferences = TermuxTaskerAppSharedPreferences.build(context, false);
// If failed to get app preferences, then likely app is not installed, so do not show its preference
termuxTaskerPrefernce.setVisible(preferences != null);
}
}
private void configureAboutPreference(@NonNull Context context) {
Preference aboutPreference = findPreference("about");
if (aboutPreference != null) {
aboutPreference.setOnPreferenceClickListener(preference -> {
new Thread() {
@Override
public void run() {
String title = "About";
StringBuilder aboutString = new StringBuilder();
aboutString.append(TermuxUtils.getAppInfoMarkdownString(context, false));
String termuxPluginAppsInfo = TermuxUtils.getTermuxPluginAppsInfoMarkdownString(context);
if (termuxPluginAppsInfo != null)
aboutString.append("\n\n").append(termuxPluginAppsInfo);
aboutString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context));
aboutString.append("\n\n").append(TermuxUtils.getImportantLinksMarkdownString(context));
ReportActivity.startReportActivity(context, new ReportInfo(UserAction.ABOUT.getName(), TermuxConstants.TERMUX_APP.TERMUX_SETTINGS_ACTIVITY_NAME, title, null, aboutString.toString(), null, false));
}
}.start();
return true;
});
}
}
private void configureDonatePreference(@NonNull Context context) {
Preference donatePreference = findPreference("donate");
if (donatePreference != null) {
String signingCertificateSHA256Digest = PackageUtils.getSigningCertificateSHA256DigestForPackage(context);
if (signingCertificateSHA256Digest != null) {
// If APK is a Google Playstore release, then do not show the donation link
// since Termux isn't exempted from the playstore policy donation links restriction
// Check Fund solicitations: https://pay.google.com/intl/en_in/about/policy/
String apkRelease = TermuxUtils.getAPKRelease(signingCertificateSHA256Digest);
if (apkRelease == null || apkRelease.equals(TermuxConstants.APK_RELEASE_GOOGLE_PLAYSTORE_SIGNING_CERTIFICATE_SHA256_DIGEST)) {
donatePreference.setVisible(false);
return;
} else {
donatePreference.setVisible(true);
}
}
donatePreference.setOnPreferenceClickListener(preference -> {
ShareUtils.openURL(context, TermuxConstants.TERMUX_DONATE_URL);
return true;
});
}
}
}
}

View File

@@ -0,0 +1,49 @@
package com.termux.app.fragments.settings;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Keep;
import androidx.preference.PreferenceDataStore;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import com.termux.R;
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
@Keep
public class TermuxPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(TermuxPreferencesDataStore.getInstance(context));
setPreferencesFromResource(R.xml.termux_preferences, rootKey);
}
}
class TermuxPreferencesDataStore extends PreferenceDataStore {
private final Context mContext;
private final TermuxAppSharedPreferences mPreferences;
private static TermuxPreferencesDataStore mInstance;
private TermuxPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxAppSharedPreferences.build(context, true);
}
public static synchronized TermuxPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new TermuxPreferencesDataStore(context);
}
return mInstance;
}
}

View File

@@ -0,0 +1,49 @@
package com.termux.app.fragments.settings;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Keep;
import androidx.preference.PreferenceDataStore;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import com.termux.R;
import com.termux.shared.settings.preferences.TermuxTaskerAppSharedPreferences;
@Keep
public class TermuxTaskerPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(TermuxTaskerPreferencesDataStore.getInstance(context));
setPreferencesFromResource(R.xml.termux_tasker_preferences, rootKey);
}
}
class TermuxTaskerPreferencesDataStore extends PreferenceDataStore {
private final Context mContext;
private final TermuxTaskerAppSharedPreferences mPreferences;
private static TermuxTaskerPreferencesDataStore mInstance;
private TermuxTaskerPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxTaskerAppSharedPreferences.build(context, true);
}
public static synchronized TermuxTaskerPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new TermuxTaskerPreferencesDataStore(context);
}
return mInstance;
}
}

View File

@@ -0,0 +1,155 @@
package com.termux.app.fragments.settings.termux;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.ListPreference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceDataStore;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import com.termux.R;
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
import com.termux.shared.logger.Logger;
@Keep
public class DebuggingPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(DebuggingPreferencesDataStore.getInstance(context));
setPreferencesFromResource(R.xml.termux_debugging_preferences, rootKey);
configureLoggingPreferences(context);
}
private void configureLoggingPreferences(@NonNull Context context) {
PreferenceCategory loggingCategory = findPreference("logging");
if (loggingCategory == null) return;
ListPreference logLevelListPreference = findPreference("log_level");
if (logLevelListPreference != null) {
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context, true);
if (preferences == null) return;
setLogLevelListPreferenceData(logLevelListPreference, context, preferences.getLogLevel());
loggingCategory.addPreference(logLevelListPreference);
}
}
public static ListPreference setLogLevelListPreferenceData(ListPreference logLevelListPreference, Context context, int logLevel) {
if (logLevelListPreference == null)
logLevelListPreference = new ListPreference(context);
CharSequence[] logLevels = Logger.getLogLevelsArray();
CharSequence[] logLevelLabels = Logger.getLogLevelLabelsArray(context, logLevels, true);
logLevelListPreference.setEntryValues(logLevels);
logLevelListPreference.setEntries(logLevelLabels);
logLevelListPreference.setValue(String.valueOf(logLevel));
logLevelListPreference.setDefaultValue(Logger.DEFAULT_LOG_LEVEL);
return logLevelListPreference;
}
}
class DebuggingPreferencesDataStore extends PreferenceDataStore {
private final Context mContext;
private final TermuxAppSharedPreferences mPreferences;
private static DebuggingPreferencesDataStore mInstance;
private DebuggingPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxAppSharedPreferences.build(context, true);
}
public static synchronized DebuggingPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new DebuggingPreferencesDataStore(context);
}
return mInstance;
}
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
if (mPreferences == null) return null;
if (key == null) return null;
switch (key) {
case "log_level":
return String.valueOf(mPreferences.getLogLevel());
default:
return null;
}
}
@Override
public void putString(String key, @Nullable String value) {
if (mPreferences == null) return;
if (key == null) return;
switch (key) {
case "log_level":
if (value != null) {
mPreferences.setLogLevel(mContext, Integer.parseInt(value));
}
break;
default:
break;
}
}
@Override
public void putBoolean(String key, boolean value) {
if (mPreferences == null) return;
if (key == null) return;
switch (key) {
case "terminal_view_key_logging_enabled":
mPreferences.setTerminalViewKeyLoggingEnabled(value);
break;
case "plugin_error_notifications_enabled":
mPreferences.setPluginErrorNotificationsEnabled(value);
break;
case "crash_report_notifications_enabled":
mPreferences.setCrashReportNotificationsEnabled(value);
break;
default:
break;
}
}
@Override
public boolean getBoolean(String key, boolean defValue) {
if (mPreferences == null) return false;
switch (key) {
case "terminal_view_key_logging_enabled":
return mPreferences.isTerminalViewKeyLoggingEnabled();
case "plugin_error_notifications_enabled":
return mPreferences.arePluginErrorNotificationsEnabled();
case "crash_report_notifications_enabled":
return mPreferences.areCrashReportNotificationsEnabled();
default:
return false;
}
}
}

View File

@@ -0,0 +1,82 @@
package com.termux.app.fragments.settings.termux;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Keep;
import androidx.preference.PreferenceDataStore;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import com.termux.R;
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
@Keep
public class TerminalIOPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(TerminalIOPreferencesDataStore.getInstance(context));
setPreferencesFromResource(R.xml.termux_terminal_io_preferences, rootKey);
}
}
class TerminalIOPreferencesDataStore extends PreferenceDataStore {
private final Context mContext;
private final TermuxAppSharedPreferences mPreferences;
private static TerminalIOPreferencesDataStore mInstance;
private TerminalIOPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxAppSharedPreferences.build(context, true);
}
public static synchronized TerminalIOPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new TerminalIOPreferencesDataStore(context);
}
return mInstance;
}
@Override
public void putBoolean(String key, boolean value) {
if (mPreferences == null) return;
if (key == null) return;
switch (key) {
case "soft_keyboard_enabled":
mPreferences.setSoftKeyboardEnabled(value);
break;
case "soft_keyboard_enabled_only_if_no_hardware":
mPreferences.setSoftKeyboardEnabledOnlyIfNoHardware(value);
break;
default:
break;
}
}
@Override
public boolean getBoolean(String key, boolean defValue) {
if (mPreferences == null) return false;
switch (key) {
case "soft_keyboard_enabled":
return mPreferences.isSoftKeyboardEnabled();
case "soft_keyboard_enabled_only_if_no_hardware":
return mPreferences.isSoftKeyboardEnabledOnlyIfNoHardware();
default:
return false;
}
}
}

View File

@@ -0,0 +1,101 @@
package com.termux.app.fragments.settings.termux_tasker;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.ListPreference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceDataStore;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import com.termux.R;
import com.termux.shared.settings.preferences.TermuxTaskerAppSharedPreferences;
@Keep
public class DebuggingPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(DebuggingPreferencesDataStore.getInstance(context));
setPreferencesFromResource(R.xml.termux_tasker_debugging_preferences, rootKey);
configureLoggingPreferences(context);
}
private void configureLoggingPreferences(@NonNull Context context) {
PreferenceCategory loggingCategory = findPreference("logging");
if (loggingCategory == null) return;
ListPreference logLevelListPreference = findPreference("log_level");
if (logLevelListPreference != null) {
TermuxTaskerAppSharedPreferences preferences = TermuxTaskerAppSharedPreferences.build(context, true);
if (preferences == null) return;
com.termux.app.fragments.settings.termux.DebuggingPreferencesFragment.
setLogLevelListPreferenceData(logLevelListPreference, context, preferences.getLogLevel(true));
loggingCategory.addPreference(logLevelListPreference);
}
}
}
class DebuggingPreferencesDataStore extends PreferenceDataStore {
private final Context mContext;
private final TermuxTaskerAppSharedPreferences mPreferences;
private static DebuggingPreferencesDataStore mInstance;
private DebuggingPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxTaskerAppSharedPreferences.build(context, true);
}
public static synchronized DebuggingPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new DebuggingPreferencesDataStore(context);
}
return mInstance;
}
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
if (mPreferences == null) return null;
if (key == null) return null;
switch (key) {
case "log_level":
return String.valueOf(mPreferences.getLogLevel(true));
default:
return null;
}
}
@Override
public void putString(String key, @Nullable String value) {
if (mPreferences == null) return;
if (key == null) return;
switch (key) {
case "log_level":
if (value != null) {
mPreferences.setLogLevel(mContext, Integer.parseInt(value), true);
}
break;
default:
break;
}
}
}

View File

@@ -0,0 +1,20 @@
package com.termux.app.models;
public enum UserAction {
ABOUT("about"),
CRASH_REPORT("crash report"),
PLUGIN_EXECUTION_COMMAND("plugin execution command"),
REPORT_ISSUE_FROM_TRANSCRIPT("report issue from transcript");
private final String name;
UserAction(final String name) {
this.name = name;
}
public String getName() {
return name;
}
}

View File

@@ -0,0 +1,107 @@
package com.termux.app.settings.properties;
import android.content.Context;
import com.termux.app.terminal.io.KeyboardShortcut;
import com.termux.app.terminal.io.extrakeys.ExtraKeysInfo;
import com.termux.shared.logger.Logger;
import com.termux.shared.settings.properties.TermuxPropertyConstants;
import com.termux.shared.settings.properties.TermuxSharedProperties;
import org.json.JSONException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;
public class TermuxAppSharedProperties extends TermuxSharedProperties {
private ExtraKeysInfo mExtraKeysInfo;
private List<KeyboardShortcut> mSessionShortcuts = new ArrayList<>();
private static final String LOG_TAG = "TermuxAppSharedProperties";
public TermuxAppSharedProperties(@Nonnull Context context) {
super(context);
}
/**
* Reload the termux properties from disk into an in-memory cache.
*/
@Override
public void loadTermuxPropertiesFromDisk() {
super.loadTermuxPropertiesFromDisk();
setExtraKeys();
setSessionShortcuts();
}
/**
* Set the terminal extra keys and style.
*/
private void setExtraKeys() {
mExtraKeysInfo = null;
try {
// The mMap stores the extra key and style string values while loading properties
// Check {@link #getExtraKeysInternalPropertyValueFromValue(String)} and
// {@link #getExtraKeysStyleInternalPropertyValueFromValue(String)}
String extrakeys = (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS, true);
String extraKeysStyle = (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE, true);
mExtraKeysInfo = new ExtraKeysInfo(extrakeys, extraKeysStyle);
} catch (JSONException e) {
Logger.showToast(mContext, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: " + e.toString(), true);
Logger.logStackTraceWithMessage(LOG_TAG, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: ", e);
try {
mExtraKeysInfo = new ExtraKeysInfo(TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE);
} catch (JSONException e2) {
Logger.showToast(mContext, "Can't create default extra keys",true);
Logger.logStackTraceWithMessage(LOG_TAG, "Could create default extra keys: ", e);
mExtraKeysInfo = null;
}
}
}
/**
* Set the terminal sessions shortcuts.
*/
private void setSessionShortcuts() {
if (mSessionShortcuts == null)
mSessionShortcuts = new ArrayList<>();
else
mSessionShortcuts.clear();
// The {@link TermuxPropertyConstants#MAP_SESSION_SHORTCUTS} stores the session shortcut key and action pair
for (Map.Entry<String, Integer> entry : TermuxPropertyConstants.MAP_SESSION_SHORTCUTS.entrySet()) {
// The mMap stores the code points for the session shortcuts while loading properties
Integer codePoint = (Integer) getInternalPropertyValue(entry.getKey(), true);
// If codePoint is null, then session shortcut did not exist in properties or was invalid
// as parsed by {@link #getCodePointForSessionShortcuts(String,String)}
// If codePoint is not null, then get the action for the MAP_SESSION_SHORTCUTS key and
// add the code point to sessionShortcuts
if (codePoint != null)
mSessionShortcuts.add(new KeyboardShortcut(codePoint, entry.getValue()));
}
}
public List<KeyboardShortcut> getSessionShortcuts() {
return mSessionShortcuts;
}
public ExtraKeysInfo getExtraKeysInfo() {
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

@@ -0,0 +1,107 @@
package com.termux.app.terminal;
import android.annotation.SuppressLint;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.StyleSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import com.termux.R;
import com.termux.app.TermuxActivity;
import com.termux.shared.shell.TermuxSession;
import com.termux.terminal.TerminalSession;
import java.util.List;
public class TermuxSessionsListViewController extends ArrayAdapter<TermuxSession> implements AdapterView.OnItemClickListener, AdapterView.OnItemLongClickListener {
final TermuxActivity mActivity;
final StyleSpan boldSpan = new StyleSpan(Typeface.BOLD);
final StyleSpan italicSpan = new StyleSpan(Typeface.ITALIC);
public TermuxSessionsListViewController(TermuxActivity activity, List<TermuxSession> sessionList) {
super(activity.getApplicationContext(), R.layout.item_terminal_sessions_list, sessionList);
this.mActivity = activity;
}
@SuppressLint("SetTextI18n")
@NonNull
@Override
public View getView(int position, View convertView, @NonNull ViewGroup parent) {
View sessionRowView = convertView;
if (sessionRowView == null) {
LayoutInflater inflater = mActivity.getLayoutInflater();
sessionRowView = inflater.inflate(R.layout.item_terminal_sessions_list, parent, false);
}
TextView sessionTitleView = sessionRowView.findViewById(R.id.session_title);
TerminalSession sessionAtRow = getItem(position).getTerminalSession();
if (sessionAtRow == null) {
sessionTitleView.setText("null session");
return sessionRowView;
}
boolean isUsingBlackUI = mActivity.getProperties().isUsingBlackUI();
if (isUsingBlackUI) {
sessionTitleView.setBackground(
ContextCompat.getDrawable(mActivity, R.drawable.session_background_black_selected)
);
}
String name = sessionAtRow.mSessionName;
String sessionTitle = sessionAtRow.getTitle();
String numberPart = "[" + (position + 1) + "] ";
String sessionNamePart = (TextUtils.isEmpty(name) ? "" : name);
String sessionTitlePart = (TextUtils.isEmpty(sessionTitle) ? "" : ((sessionNamePart.isEmpty() ? "" : "\n") + sessionTitle));
String fullSessionTitle = numberPart + sessionNamePart + sessionTitlePart;
SpannableString fullSessionTitleStyled = new SpannableString(fullSessionTitle);
fullSessionTitleStyled.setSpan(boldSpan, 0, numberPart.length() + sessionNamePart.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
fullSessionTitleStyled.setSpan(italicSpan, numberPart.length() + sessionNamePart.length(), fullSessionTitle.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
sessionTitleView.setText(fullSessionTitleStyled);
boolean sessionRunning = sessionAtRow.isRunning();
if (sessionRunning) {
sessionTitleView.setPaintFlags(sessionTitleView.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
} else {
sessionTitleView.setPaintFlags(sessionTitleView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
}
int defaultColor = isUsingBlackUI ? Color.WHITE : Color.BLACK;
int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? defaultColor : Color.RED;
sessionTitleView.setTextColor(color);
return sessionRowView;
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
TermuxSession clickedSession = getItem(position);
mActivity.getTermuxTerminalSessionClient().setCurrentSession(clickedSession.getTerminalSession());
mActivity.getDrawer().closeDrawers();
}
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
final TermuxSession selectedSession = getItem(position);
mActivity.getTermuxTerminalSessionClient().renameSession(selectedSession.getTerminalSession());
return true;
}
}

View File

@@ -0,0 +1,474 @@
package com.termux.app.terminal;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Typeface;
import android.media.AudioAttributes;
import android.media.SoundPool;
import android.text.TextUtils;
import android.widget.ListView;
import com.termux.R;
import com.termux.shared.shell.TermuxSession;
import com.termux.shared.interact.TextInputDialogUtils;
import com.termux.app.TermuxActivity;
import com.termux.shared.terminal.TermuxTerminalSessionClientBase;
import com.termux.shared.termux.TermuxConstants;
import com.termux.app.TermuxService;
import com.termux.shared.settings.properties.TermuxPropertyConstants;
import com.termux.app.terminal.io.BellHandler;
import com.termux.shared.logger.Logger;
import com.termux.terminal.TerminalColors;
import com.termux.terminal.TerminalSession;
import com.termux.terminal.TextStyle;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Properties;
public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase {
private final TermuxActivity mActivity;
private static final int MAX_SESSIONS = 8;
private SoundPool mBellSoundPool;
private int mBellSoundId;
private static final String LOG_TAG = "TermuxTerminalSessionClient";
public TermuxTerminalSessionClient(TermuxActivity activity) {
this.mActivity = activity;
}
/**
* Should be called when mActivity.onCreate() is called
*/
public void onCreate() {
// Set terminal fonts and colors
checkForFontAndColors();
}
/**
* Should be called when mActivity.onStart() is called
*/
public void onStart() {
// The service has connected, but data may have changed since we were last in the foreground.
// Get the session stored in shared preferences stored by {@link #onStop} if its valid,
// otherwise get the last session currently running.
if (mActivity.getTermuxService() != null) {
setCurrentSession(getCurrentStoredSessionOrLast());
termuxSessionListNotifyUpdated();
}
// The current terminal session may have changed while being away, force
// a refresh of the displayed terminal.
mActivity.getTerminalView().onScreenUpdated();
}
/**
* Should be called when mActivity.onResume() is called
*/
public void onResume() {
// Just initialize the mBellSoundPool and load the sound, otherwise bell might not run
// the first time bell key is pressed and play() is called, since sound may not be loaded
// quickly enough before the call to play(). https://stackoverflow.com/questions/35435625
getBellSoundPool();
}
/**
* Should be called when mActivity.onStop() is called
*/
public void onStop() {
// Store current session in shared preferences so that it can be restored later in
// {@link #onStart} if needed.
setCurrentStoredSession();
// Release mBellSoundPool resources, specially to prevent exceptions like the following to be thrown
// java.util.concurrent.TimeoutException: android.media.SoundPool.finalize() timed out after 10 seconds
// Bell is not played in background anyways
// Related: https://stackoverflow.com/a/28708351/14686958
releaseBellSoundPool();
}
/**
* Should be called when mActivity.reloadActivityStyling() is called
*/
public void onReload() {
// Set terminal fonts and colors
checkForFontAndColors();
}
@Override
public void onTextChanged(TerminalSession changedSession) {
if (!mActivity.isVisible()) return;
if (mActivity.getCurrentSession() == changedSession) mActivity.getTerminalView().onScreenUpdated();
}
@Override
public void onTitleChanged(TerminalSession updatedSession) {
if (!mActivity.isVisible()) return;
if (updatedSession != mActivity.getCurrentSession()) {
// Only show toast for other sessions than the current one, since the user
// probably consciously caused the title change to change in the current session
// and don't want an annoying toast for that.
mActivity.showToast(toToastTitle(updatedSession), true);
}
termuxSessionListNotifyUpdated();
}
@Override
public void onSessionFinished(final TerminalSession finishedSession) {
TermuxService service = mActivity.getTermuxService();
if (service == null || service.wantsToStop()) {
// The service wants to stop as soon as possible.
mActivity.finishActivityIfNotFinishing();
return;
}
if (mActivity.isVisible() && finishedSession != mActivity.getCurrentSession()) {
// Show toast for non-current sessions that exit.
int indexOfSession = service.getIndexOfSession(finishedSession);
// Verify that session was not removed before we got told about it finishing:
if (indexOfSession >= 0)
mActivity.showToast(toToastTitle(finishedSession) + " - exited", true);
}
if (mActivity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
// On Android TV devices we need to use older behaviour because we may
// not be able to have multiple launcher icons.
if (service.getTermuxSessionsSize() > 1) {
removeFinishedSession(finishedSession);
}
} else {
// Once we have a separate launcher icon for the failsafe session, it
// should be safe to auto-close session on exit code '0' or '130'.
if (finishedSession.getExitStatus() == 0 || finishedSession.getExitStatus() == 130) {
removeFinishedSession(finishedSession);
}
}
}
@Override
public void onClipboardText(TerminalSession session, String text) {
if (!mActivity.isVisible()) return;
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(text)));
}
@Override
public void onBell(TerminalSession session) {
if (!mActivity.isVisible()) return;
switch (mActivity.getProperties().getBellBehaviour()) {
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_VIBRATE:
BellHandler.getInstance(mActivity).doBell();
break;
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_BEEP:
getBellSoundPool().play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
break;
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_IGNORE:
// Ignore the bell character.
break;
}
}
@Override
public void onColorsChanged(TerminalSession changedSession) {
if (mActivity.getCurrentSession() == changedSession)
updateBackgroundColor();
}
@Override
public void onTerminalCursorStateChange(boolean enabled) {
// Do not start cursor blinking thread if activity is not visible
if (enabled && !mActivity.isVisible()) {
Logger.logVerbose(LOG_TAG, "Ignoring call to start cursor blinking since activity is not visible");
return;
}
// If cursor is to enabled now, then start cursor blinking if blinking is enabled
// otherwise stop cursor blinking
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 */
private synchronized SoundPool getBellSoundPool() {
if (mBellSoundPool == null) {
mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
mBellSoundId = mBellSoundPool.load(mActivity, R.raw.bell, 1);
}
return mBellSoundPool;
}
/** Release mBellSoundPool resources */
private synchronized void releaseBellSoundPool() {
if (mBellSoundPool != null) {
mBellSoundPool.release();
mBellSoundPool = null;
}
}
/** Try switching to session. */
public void setCurrentSession(TerminalSession session) {
if (session == null) return;
if (mActivity.getTerminalView().attachSession(session)) {
// notify about switched session if not already displaying the session
notifyOfSessionChange();
}
// We call the following even when the session is already being displayed since config may
// be stale, like current session not selected or scrolled to.
checkAndScrollToSession(session);
updateBackgroundColor();
}
void notifyOfSessionChange() {
if (!mActivity.isVisible()) return;
if (!mActivity.getProperties().areTerminalSessionChangeToastsDisabled()) {
TerminalSession session = mActivity.getCurrentSession();
mActivity.showToast(toToastTitle(session), false);
}
}
public void switchToSession(boolean forward) {
TermuxService service = mActivity.getTermuxService();
if (service == null) return;
TerminalSession currentTerminalSession = mActivity.getCurrentSession();
int index = service.getIndexOfSession(currentTerminalSession);
int size = service.getTermuxSessionsSize();
if (forward) {
if (++index >= size) index = 0;
} else {
if (--index < 0) index = size - 1;
}
TermuxSession termuxSession = service.getTermuxSession(index);
if (termuxSession != null)
setCurrentSession(termuxSession.getTerminalSession());
}
public void switchToSession(int index) {
TermuxService service = mActivity.getTermuxService();
if (service == null) return;
TermuxSession termuxSession = service.getTermuxSession(index);
if (termuxSession != null)
setCurrentSession(termuxSession.getTerminalSession());
}
@SuppressLint("InflateParams")
public void renameSession(final TerminalSession sessionToRename) {
if (sessionToRename == null) return;
TextInputDialogUtils.textInput(mActivity, R.string.title_rename_session, sessionToRename.mSessionName, R.string.action_rename_session_confirm, text -> {
sessionToRename.mSessionName = text;
termuxSessionListNotifyUpdated();
}, -1, null, -1, null, null);
}
public void addNewSession(boolean isFailSafe, String sessionName) {
TermuxService service = mActivity.getTermuxService();
if (service == null) return;
if (service.getTermuxSessionsSize() >= MAX_SESSIONS) {
new AlertDialog.Builder(mActivity).setTitle(R.string.title_max_terminals_reached).setMessage(R.string.msg_max_terminals_reached)
.setPositiveButton(android.R.string.ok, null).show();
} else {
TerminalSession currentSession = mActivity.getCurrentSession();
String workingDirectory;
if (currentSession == null) {
workingDirectory = mActivity.getProperties().getDefaultWorkingDirectory();
} else {
workingDirectory = currentSession.getCwd();
}
TermuxSession newTermuxSession = service.createTermuxSession(null, null, null, workingDirectory, isFailSafe, sessionName);
if (newTermuxSession == null) return;
TerminalSession newTerminalSession = newTermuxSession.getTerminalSession();
setCurrentSession(newTerminalSession);
mActivity.getDrawer().closeDrawers();
}
}
public void setCurrentStoredSession() {
TerminalSession currentSession = mActivity.getCurrentSession();
if (currentSession != null)
mActivity.getPreferences().setCurrentSession(currentSession.mHandle);
else
mActivity.getPreferences().setCurrentSession(null);
}
/** The current session as stored or the last one if that does not exist. */
public TerminalSession getCurrentStoredSessionOrLast() {
TerminalSession stored = getCurrentStoredSession();
if (stored != null) {
// If a stored session is in the list of currently running sessions, then return it
return stored;
} else {
// Else return the last session currently running
TermuxService service = mActivity.getTermuxService();
if (service == null) return null;
TermuxSession termuxSession = service.getLastTermuxSession();
if (termuxSession != null)
return termuxSession.getTerminalSession();
else
return null;
}
}
private TerminalSession getCurrentStoredSession() {
String sessionHandle = mActivity.getPreferences().getCurrentSession();
// If no session is stored in shared preferences
if (sessionHandle == null)
return null;
// Check if the session handle found matches one of the currently running sessions
TermuxService service = mActivity.getTermuxService();
if (service == null) return null;
return service.getTerminalSessionForHandle(sessionHandle);
}
public void removeFinishedSession(TerminalSession finishedSession) {
// Return pressed with finished session - remove it.
TermuxService service = mActivity.getTermuxService();
if (service == null) return;
int index = service.removeTermuxSession(finishedSession);
int size = service.getTermuxSessionsSize();
if (size == 0) {
// There are no sessions to show, so finish the activity.
mActivity.finishActivityIfNotFinishing();
} else {
if (index >= size) {
index = size - 1;
}
TermuxSession termuxSession = service.getTermuxSession(index);
if (termuxSession != null)
setCurrentSession(termuxSession.getTerminalSession());
}
}
public void termuxSessionListNotifyUpdated() {
mActivity.termuxSessionListNotifyUpdated();
}
public void checkAndScrollToSession(TerminalSession session) {
if (!mActivity.isVisible()) return;
TermuxService service = mActivity.getTermuxService();
if (service == null) return;
final int indexOfSession = service.getIndexOfSession(session);
if (indexOfSession < 0) return;
final ListView termuxSessionsListView = mActivity.findViewById(R.id.terminal_sessions_list);
if (termuxSessionsListView == null) return;
termuxSessionsListView.setItemChecked(indexOfSession, true);
// Delay is necessary otherwise sometimes scroll to newly added session does not happen
termuxSessionsListView.postDelayed(() -> termuxSessionsListView.smoothScrollToPosition(indexOfSession), 1000);
}
String toToastTitle(TerminalSession session) {
TermuxService service = mActivity.getTermuxService();
if (service == null) return null;
final int indexOfSession = service.getIndexOfSession(session);
if (indexOfSession < 0) return null;
StringBuilder toastTitle = new StringBuilder("[" + (indexOfSession + 1) + "]");
if (!TextUtils.isEmpty(session.mSessionName)) {
toastTitle.append(" ").append(session.mSessionName);
}
String title = session.getTitle();
if (!TextUtils.isEmpty(title)) {
// Space to "[${NR}] or newline after session name:
toastTitle.append(session.mSessionName == null ? " " : "\n");
toastTitle.append(title);
}
return toastTitle.toString();
}
public void checkForFontAndColors() {
try {
File colorsFile = TermuxConstants.TERMUX_COLOR_PROPERTIES_FILE;
File fontFile = TermuxConstants.TERMUX_FONT_FILE;
final Properties props = new Properties();
if (colorsFile.isFile()) {
try (InputStream in = new FileInputStream(colorsFile)) {
props.load(in);
}
}
TerminalColors.COLOR_SCHEME.updateWith(props);
TerminalSession session = mActivity.getCurrentSession();
if (session != null && session.getEmulator() != null) {
session.getEmulator().mColors.reset();
}
updateBackgroundColor();
final Typeface newTypeface = (fontFile.exists() && fontFile.length() > 0) ? Typeface.createFromFile(fontFile) : Typeface.MONOSPACE;
mActivity.getTerminalView().setTypeface(newTypeface);
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Error in checkForFontAndColors()", e);
}
}
public void updateBackgroundColor() {
if (!mActivity.isVisible()) return;
TerminalSession session = mActivity.getCurrentSession();
if (session != null && session.getEmulator() != null) {
mActivity.getWindow().getDecorView().setBackgroundColor(session.getEmulator().mColors.mCurrentColors[TextStyle.COLOR_INDEX_BACKGROUND]);
}
}
}

View File

@@ -0,0 +1,692 @@
package com.termux.app.terminal;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.net.Uri;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.Toast;
import com.termux.R;
import com.termux.app.TermuxActivity;
import com.termux.shared.data.UrlUtils;
import com.termux.shared.shell.ShellUtils;
import com.termux.shared.terminal.TermuxTerminalViewClientBase;
import com.termux.shared.termux.AndroidUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.activities.ReportActivity;
import com.termux.shared.models.ReportInfo;
import com.termux.app.models.UserAction;
import com.termux.app.terminal.io.KeyboardShortcut;
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
import com.termux.shared.settings.properties.TermuxPropertyConstants;
import com.termux.shared.data.DataUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.termux.TermuxUtils;
import com.termux.shared.view.KeyboardUtils;
import com.termux.shared.view.ViewUtils;
import com.termux.terminal.KeyHandler;
import com.termux.terminal.TerminalEmulator;
import com.termux.terminal.TerminalSession;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import androidx.drawerlayout.widget.DrawerLayout;
public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
final TermuxActivity mActivity;
final TermuxTerminalSessionClient mTermuxTerminalSessionClient;
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
private Runnable mShowSoftKeyboardRunnable;
private boolean mShowSoftKeyboardIgnoreOnce;
private boolean mShowSoftKeyboardWithDelayOnce;
private boolean mTerminalCursorBlinkerStateAlreadySet;
private static final String LOG_TAG = "TermuxTerminalViewClient";
public TermuxTerminalViewClient(TermuxActivity activity, TermuxTerminalSessionClient termuxTerminalSessionClient) {
this.mActivity = activity;
this.mTermuxTerminalSessionClient = termuxTerminalSessionClient;
}
/**
* Should be called when mActivity.onCreate() is called
*/
public void onCreate() {
mActivity.getTerminalView().setTextSize(mActivity.getPreferences().getFontSize());
mActivity.getTerminalView().setKeepScreenOn(mActivity.getPreferences().shouldKeepScreenOn());
}
/**
* Should be called when mActivity.onStart() is called
*/
public void onStart() {
// Set {@link TerminalView#TERMINAL_VIEW_KEY_LOGGING_ENABLED} value
// Also required if user changed the preference from {@link TermuxSettings} activity and returns
boolean isTerminalViewKeyLoggingEnabled = mActivity.getPreferences().isTerminalViewKeyLoggingEnabled();
mActivity.getTerminalView().setIsTerminalViewKeyLoggingEnabled(isTerminalViewKeyLoggingEnabled);
// Piggyback on the terminal view key logging toggle for now, should add a separate toggle in future
mActivity.getTermuxActivityRootView().setIsRootViewLoggingEnabled(isTerminalViewKeyLoggingEnabled);
ViewUtils.setIsViewUtilsLoggingEnabled(isTerminalViewKeyLoggingEnabled);
}
/**
* Should be called when mActivity.onResume() is called
*/
public void onResume() {
// Show the soft keyboard if required
setSoftKeyboardState(true, false);
mTerminalCursorBlinkerStateAlreadySet = false;
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;
}
}
/**
* Should be called when mActivity.onStop() is called
*/
public void onStop() {
// Stop terminal cursor blinking if enabled
setTerminalCursorBlinkerState(false);
}
/**
* Should be called when mActivity.reloadActivityStyling() is called
*/
public void onReload() {
// Show the soft keyboard if required
setSoftKeyboardState(false, true);
// Start terminal cursor blinking if enabled
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
public float onScale(float scale) {
if (scale < 0.9f || scale > 1.1f) {
boolean increase = scale > 1.f;
changeFontSize(increase);
return 1.0f;
}
return scale;
}
@Override
public void onSingleTapUp(MotionEvent e) {
if (!KeyboardUtils.areDisableSoftKeyboardFlagsSet(mActivity))
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
else
Logger.logVerbose(LOG_TAG, "Not showing soft keyboard onSingleTapUp since its disabled");
}
@Override
public boolean shouldBackButtonBeMappedToEscape() {
return mActivity.getProperties().isBackKeyTheEscapeKey();
}
@Override
public boolean shouldEnforceCharBasedInput() {
return mActivity.getProperties().isEnforcingCharBasedInput();
}
@Override
public boolean shouldUseCtrlSpaceWorkaround() {
return mActivity.getProperties().isUsingCtrlSpaceWorkaround();
}
@Override
public void copyModeChanged(boolean copyMode) {
// Disable drawer while copying.
mActivity.getDrawer().setDrawerLockMode(copyMode ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED : DrawerLayout.LOCK_MODE_UNLOCKED);
}
@SuppressLint("RtlHardcoded")
@Override
public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession currentSession) {
if (handleVirtualKeys(keyCode, e, true)) return true;
if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) {
mTermuxTerminalSessionClient.removeFinishedSession(currentSession);
return true;
} else if (e.isCtrlPressed() && e.isAltPressed()) {
// Get the unmodified code point:
int unicodeChar = e.getUnicodeChar(0);
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || unicodeChar == 'n'/* next */) {
mTermuxTerminalSessionClient.switchToSession(true);
} else if (keyCode == KeyEvent.KEYCODE_DPAD_UP || unicodeChar == 'p' /* previous */) {
mTermuxTerminalSessionClient.switchToSession(false);
} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
mActivity.getDrawer().openDrawer(Gravity.LEFT);
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
mActivity.getDrawer().closeDrawers();
} else if (unicodeChar == 'k'/* keyboard */) {
onToggleSoftKeyboardRequest();
} else if (unicodeChar == 'm'/* menu */) {
mActivity.getTerminalView().showContextMenu();
} else if (unicodeChar == 'r'/* rename */) {
mTermuxTerminalSessionClient.renameSession(currentSession);
} else if (unicodeChar == 'c'/* create */) {
mTermuxTerminalSessionClient.addNewSession(false, null);
} else if (unicodeChar == 'u' /* urls */) {
showUrlSelection();
} else if (unicodeChar == 'v') {
doPaste();
} else if (unicodeChar == '+' || e.getUnicodeChar(KeyEvent.META_SHIFT_ON) == '+') {
// We also check for the shifted char here since shift may be required to produce '+',
// see https://github.com/termux/termux-api/issues/2
changeFontSize(true);
} else if (unicodeChar == '-') {
changeFontSize(false);
} else if (unicodeChar >= '1' && unicodeChar <= '9') {
int index = unicodeChar - '1';
mTermuxTerminalSessionClient.switchToSession(index);
}
return true;
}
return false;
}
@Override
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);
}
/** Handle dedicated volume buttons as virtual keys if applicable. */
private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) {
InputDevice inputDevice = event.getDevice();
if (mActivity.getProperties().areVirtualVolumeKeysDisabled()) {
return false;
} else if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
// Do not steal dedicated buttons from a full external keyboard.
return false;
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
mVirtualControlKeyDown = down;
return true;
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
mVirtualFnKeyDown = down;
return true;
}
return false;
}
@Override
public boolean readControlKey() {
return (mActivity.getExtraKeysView() != null && mActivity.getExtraKeysView().readSpecialButton(ExtraKeysView.SpecialButton.CTRL)) || mVirtualControlKeyDown;
}
@Override
public boolean readAltKey() {
return (mActivity.getExtraKeysView() != null && mActivity.getExtraKeysView().readSpecialButton(ExtraKeysView.SpecialButton.ALT));
}
@Override
public boolean onLongPress(MotionEvent event) {
return false;
}
@Override
public boolean onCodePoint(final int codePoint, boolean ctrlDown, TerminalSession session) {
if (mVirtualFnKeyDown) {
int resultingKeyCode = -1;
int resultingCodePoint = -1;
boolean altDown = false;
int lowerCase = Character.toLowerCase(codePoint);
switch (lowerCase) {
// Arrow keys.
case 'w':
resultingKeyCode = KeyEvent.KEYCODE_DPAD_UP;
break;
case 'a':
resultingKeyCode = KeyEvent.KEYCODE_DPAD_LEFT;
break;
case 's':
resultingKeyCode = KeyEvent.KEYCODE_DPAD_DOWN;
break;
case 'd':
resultingKeyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
break;
// Page up and down.
case 'p':
resultingKeyCode = KeyEvent.KEYCODE_PAGE_UP;
break;
case 'n':
resultingKeyCode = KeyEvent.KEYCODE_PAGE_DOWN;
break;
// Some special keys:
case 't':
resultingKeyCode = KeyEvent.KEYCODE_TAB;
break;
case 'i':
resultingKeyCode = KeyEvent.KEYCODE_INSERT;
break;
case 'h':
resultingCodePoint = '~';
break;
// Special characters to input.
case 'u':
resultingCodePoint = '_';
break;
case 'l':
resultingCodePoint = '|';
break;
// Function keys.
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
resultingKeyCode = (codePoint - '1') + KeyEvent.KEYCODE_F1;
break;
case '0':
resultingKeyCode = KeyEvent.KEYCODE_F10;
break;
// Other special keys.
case 'e':
resultingCodePoint = /*Escape*/ 27;
break;
case '.':
resultingCodePoint = /*^.*/ 28;
break;
case 'b': // alt+b, jumping backward in readline.
case 'f': // alf+f, jumping forward in readline.
case 'x': // alt+x, common in emacs.
resultingCodePoint = lowerCase;
altDown = true;
break;
// Volume control.
case 'v':
resultingCodePoint = -1;
AudioManager audio = (AudioManager) mActivity.getSystemService(Context.AUDIO_SERVICE);
audio.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME, AudioManager.USE_DEFAULT_STREAM_TYPE, AudioManager.FLAG_SHOW_UI);
break;
// Writing mode:
case 'q':
case 'k':
mActivity.toggleTerminalToolbar();
mVirtualFnKeyDown=false; // force disable fn key down to restore keyboard input into terminal view, fixes termux/termux-app#1420
break;
}
if (resultingKeyCode != -1) {
TerminalEmulator term = session.getEmulator();
session.write(KeyHandler.getCode(resultingKeyCode, 0, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode()));
} else if (resultingCodePoint != -1) {
session.writeCodePoint(altDown, resultingCodePoint);
}
return true;
} else if (ctrlDown) {
if (codePoint == 106 /* Ctrl+j or \n */ && !session.isRunning()) {
mTermuxTerminalSessionClient.removeFinishedSession(session);
return true;
}
List<KeyboardShortcut> shortcuts = mActivity.getProperties().getSessionShortcuts();
if (shortcuts != null && !shortcuts.isEmpty()) {
int codePointLowerCase = Character.toLowerCase(codePoint);
for (int i = shortcuts.size() - 1; i >= 0; i--) {
KeyboardShortcut shortcut = shortcuts.get(i);
if (codePointLowerCase == shortcut.codePoint) {
switch (shortcut.shortcutAction) {
case TermuxPropertyConstants.ACTION_SHORTCUT_CREATE_SESSION:
mTermuxTerminalSessionClient.addNewSession(false, null);
return true;
case TermuxPropertyConstants.ACTION_SHORTCUT_NEXT_SESSION:
mTermuxTerminalSessionClient.switchToSession(true);
return true;
case TermuxPropertyConstants.ACTION_SHORTCUT_PREVIOUS_SESSION:
mTermuxTerminalSessionClient.switchToSession(false);
return true;
case TermuxPropertyConstants.ACTION_SHORTCUT_RENAME_SESSION:
mTermuxTerminalSessionClient.renameSession(mActivity.getCurrentSession());
return true;
}
}
}
}
}
return false;
}
public void changeFontSize(boolean increase) {
mActivity.getPreferences().changeFontSize(increase);
mActivity.getTerminalView().setTextSize(mActivity.getPreferences().getFontSize());
}
/**
* Called when user requests the soft keyboard to be toggled via "KEYBOARD" toggle button in
* drawer or extra keys, or with ctrl+alt+k hardware keyboard shortcut.
*/
public void onToggleSoftKeyboardRequest() {
// If soft keyboard toggle behaviour is enable/disabled
if (mActivity.getProperties().shouldEnableDisableSoftKeyboardOnToggle()) {
// If soft keyboard is visible
if (!KeyboardUtils.areDisableSoftKeyboardFlagsSet(mActivity)) {
Logger.logVerbose(LOG_TAG, "Disabling soft keyboard on toggle");
mActivity.getPreferences().setSoftKeyboardEnabled(false);
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
} 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");
mActivity.getPreferences().setSoftKeyboardEnabled(true);
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
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
else {
// If soft keyboard is disabled by user for Termux
if (!mActivity.getPreferences().isSoftKeyboardEnabled()) {
Logger.logVerbose(LOG_TAG, "Maintaining disabled soft keyboard on toggle");
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
} else {
Logger.logVerbose(LOG_TAG, "Showing/Hiding soft keyboard on toggle");
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
KeyboardUtils.toggleSoftKeyboard(mActivity);
}
}
}
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 (KeyboardUtils.shouldSoftKeyboardBeDisabled(mActivity,
mActivity.getPreferences().isSoftKeyboardEnabled(),
mActivity.getPreferences().isSoftKeyboardEnabledOnlyIfNoHardware())) {
Logger.logVerbose(LOG_TAG, "Maintaining disabled soft keyboard");
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 {
// Set flag to automatically push up TerminalView when keyboard is opened instead of showing over it
KeyboardUtils.setSoftInputModeAdjustResize(mActivity);
// Clear any previous flags to disable soft keyboard in case setting updated
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
// If soft keyboard is to be hidden on startup
if (isStartup && mActivity.getProperties().shouldSoftKeyboardBeHiddenOnStartup()) {
Logger.logVerbose(LOG_TAG, "Hiding soft keyboard on startup");
KeyboardUtils.hideSoftKeyboard(mActivity, mActivity.getTerminalView());
// Required to keep keyboard hidden when Termux app is switched back from another app
KeyboardUtils.setSoftKeyboardAlwaysHiddenFlags(mActivity);
noRequestFocus = true;
// Required to keep keyboard hidden on app startup
mShowSoftKeyboardIgnoreOnce = true;
}
}
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;
}
public void setTerminalCursorBlinkerState(boolean start) {
if (start) {
// If set/update the cursor blinking rate is successful, then enable cursor blinker
if (mActivity.getTerminalView().setTerminalCursorBlinkerRate(mActivity.getProperties().getTerminalCursorBlinkRate()))
mActivity.getTerminalView().setTerminalCursorBlinkerState(true, true);
else
Logger.logError(LOG_TAG,"Failed to start cursor blinker");
} else {
// Disable cursor blinker
mActivity.getTerminalView().setTerminalCursorBlinkerState(false, true);
}
}
public void shareSessionTranscript() {
TerminalSession session = mActivity.getCurrentSession();
if (session == null) return;
String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
if (transcriptText == null) return;
try {
// See https://github.com/termux/termux-app/issues/1166.
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
transcriptText = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
intent.putExtra(Intent.EXTRA_TEXT, transcriptText);
intent.putExtra(Intent.EXTRA_SUBJECT, mActivity.getString(R.string.title_share_transcript));
mActivity.startActivity(Intent.createChooser(intent, mActivity.getString(R.string.title_share_transcript_with)));
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG,"Failed to get share session transcript of length " + transcriptText.length(), e);
}
}
public void showUrlSelection() {
TerminalSession session = mActivity.getCurrentSession();
if (session == null) return;
String text = ShellUtils.getTerminalSessionTranscriptText(session, true, true);
LinkedHashSet<CharSequence> urlSet = UrlUtils.extractUrls(text);
if (urlSet.isEmpty()) {
new AlertDialog.Builder(mActivity).setMessage(R.string.title_select_url_none_found).show();
return;
}
final CharSequence[] urls = urlSet.toArray(new CharSequence[0]);
Collections.reverse(Arrays.asList(urls)); // Latest first.
// Click to copy url to clipboard:
final AlertDialog dialog = new AlertDialog.Builder(mActivity).setItems(urls, (di, which) -> {
String url = (String) urls[which];
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(url)));
Toast.makeText(mActivity, R.string.msg_select_url_copied_to_clipboard, Toast.LENGTH_LONG).show();
}).setTitle(R.string.title_select_url_dialog).create();
// Long press to open URL:
dialog.setOnShowListener(di -> {
ListView lv = dialog.getListView(); // this is a ListView with your "buds" in it
lv.setOnItemLongClickListener((parent, view, position, id) -> {
dialog.dismiss();
String url = (String) urls[position];
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
try {
mActivity.startActivity(i, null);
} catch (ActivityNotFoundException e) {
// If no applications match, Android displays a system message.
mActivity.startActivity(Intent.createChooser(i, null));
}
return true;
});
});
dialog.show();
}
public void reportIssueFromTranscript() {
TerminalSession session = mActivity.getCurrentSession();
if (session == null) return;
final String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
if (transcriptText == null) return;
Logger.showToast(mActivity, mActivity.getString(R.string.msg_generating_report), true);
new Thread() {
@Override
public void run() {
String transcriptTextTruncated = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
StringBuilder reportString = new StringBuilder();
String title = TermuxConstants.TERMUX_APP_NAME + " Report Issue";
reportString.append("## Transcript\n");
reportString.append("\n").append(MarkdownUtils.getMarkdownCodeForString(transcriptTextTruncated, true));
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, true));
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(mActivity));
String termuxAptInfo = TermuxUtils.geAPTInfoMarkdownString(mActivity);
if (termuxAptInfo != null)
reportString.append("\n\n").append(termuxAptInfo);
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();
}
public void doPaste() {
TerminalSession session = mActivity.getCurrentSession();
if (session == null) return;
if (!session.isRunning()) return;
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboard.getPrimaryClip();
if (clipData == null) return;
CharSequence paste = clipData.getItemAt(0).coerceToText(mActivity);
if (!TextUtils.isEmpty(paste))
session.getEmulator().paste(paste.toString());
}
}

View File

@@ -1,4 +1,4 @@
package com.termux.app;
package com.termux.app.terminal.io;
import android.content.Context;
import android.os.Handler;
@@ -6,15 +6,15 @@ import android.os.Looper;
import android.os.SystemClock;
import android.os.Vibrator;
public class BellUtil {
private static BellUtil instance = null;
public class BellHandler {
private static BellHandler instance = null;
private static final Object lock = new Object();
public static BellUtil getInstance(Context context) {
public static BellHandler getInstance(Context context) {
if (instance == null) {
synchronized (lock) {
if (instance == null) {
instance = new BellUtil((Vibrator) context.getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE));
instance = new BellHandler((Vibrator) context.getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE));
}
}
}
@@ -29,7 +29,7 @@ public class BellUtil {
private long lastBell = 0;
private final Runnable bellRunnable;
private BellUtil(final Vibrator vibrator) {
private BellHandler(final Vibrator vibrator) {
bellRunnable = new Runnable() {
@Override
public void run() {
@@ -47,7 +47,7 @@ public class BellUtil {
if (timeSinceLastBell < 0) {
// there is a next bell pending; don't schedule another one
} else if (timeSinceLastBell < MIN_PAUSE) {
// there was a bell recently, scheudle the next one
// there was a bell recently, schedule the next one
handler.postDelayed(bellRunnable, MIN_PAUSE - timeSinceLastBell);
lastBell = lastBell + MIN_PAUSE;
} else {

View File

@@ -0,0 +1,68 @@
package com.termux.app.terminal.io;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewGroup;
import com.termux.app.TermuxActivity;
/**
* Work around for fullscreen mode in Termux to fix ExtraKeysView not being visible.
* This class is derived from:
* https://stackoverflow.com/questions/7417123/android-how-to-adjust-layout-in-full-screen-mode-when-softkeyboard-is-visible
* and has some additional tweaks
* ---
* For more information, see https://issuetracker.google.com/issues/36911528
*/
public class FullScreenWorkAround {
private final View mChildOfContent;
private int mUsableHeightPrevious;
private final ViewGroup.LayoutParams mViewGroupLayoutParams;
private final int mNavBarHeight;
public static void apply(TermuxActivity activity) {
new FullScreenWorkAround(activity);
}
private FullScreenWorkAround(TermuxActivity activity) {
ViewGroup content = activity.findViewById(android.R.id.content);
mChildOfContent = content.getChildAt(0);
mViewGroupLayoutParams = mChildOfContent.getLayoutParams();
mNavBarHeight = activity.getNavBarHeight();
mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(this::possiblyResizeChildOfContent);
}
private void possiblyResizeChildOfContent() {
int usableHeightNow = computeUsableHeight();
if (usableHeightNow != mUsableHeightPrevious) {
int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();
int heightDifference = usableHeightSansKeyboard - usableHeightNow;
if (heightDifference > (usableHeightSansKeyboard / 4)) {
// keyboard probably just became visible
// ensures that usable layout space does not extend behind the
// soft keyboard, causing the extra keys to not be visible
mViewGroupLayoutParams.height = (usableHeightSansKeyboard - heightDifference) + getNavBarHeight();
} else {
// keyboard probably just became hidden
mViewGroupLayoutParams.height = usableHeightSansKeyboard;
}
mChildOfContent.requestLayout();
mUsableHeightPrevious = usableHeightNow;
}
}
private int getNavBarHeight() {
return mNavBarHeight;
}
private int computeUsableHeight() {
Rect r = new Rect();
mChildOfContent.getWindowVisibleDisplayFrame(r);
return (r.bottom - r.top);
}
}

View File

@@ -0,0 +1,13 @@
package com.termux.app.terminal.io;
public class KeyboardShortcut {
public final int codePoint;
public final int shortcutAction;
public KeyboardShortcut(int codePoint, int shortcutAction) {
this.codePoint = codePoint;
this.shortcutAction = shortcutAction;
}
}

View File

@@ -0,0 +1,115 @@
package com.termux.app.terminal.io;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import com.termux.R;
import com.termux.app.TermuxActivity;
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
import com.termux.terminal.TerminalSession;
public class TerminalToolbarViewPager {
public static class PageAdapter extends PagerAdapter {
final TermuxActivity mActivity;
String mSavedTextInput;
public PageAdapter(TermuxActivity activity, String savedTextInput) {
this.mActivity = activity;
this.mSavedTextInput = savedTextInput;
}
@Override
public int getCount() {
return 2;
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return view == object;
}
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup collection, int position) {
LayoutInflater inflater = LayoutInflater.from(mActivity);
View layout;
if (position == 0) {
layout = inflater.inflate(R.layout.view_terminal_toolbar_extra_keys, collection, false);
ExtraKeysView extraKeysView = (ExtraKeysView) layout;
extraKeysView.setTermuxTerminalViewClient(mActivity.getTermuxTerminalViewClient());
mActivity.setExtraKeysView(extraKeysView);
extraKeysView.reload(mActivity.getProperties().getExtraKeysInfo());
// apply extra keys fix if enabled in prefs
if (mActivity.getProperties().isUsingFullScreen() && mActivity.getProperties().isUsingFullScreenWorkAround()) {
FullScreenWorkAround.apply(mActivity);
}
} else {
layout = inflater.inflate(R.layout.view_terminal_toolbar_text_input, collection, false);
final EditText editText = layout.findViewById(R.id.terminal_toolbar_text_input);
if (mSavedTextInput != null) {
editText.setText(mSavedTextInput);
mSavedTextInput = null;
}
editText.setOnEditorActionListener((v, actionId, event) -> {
TerminalSession session = mActivity.getCurrentSession();
if (session != null) {
if (session.isRunning()) {
String textToSend = editText.getText().toString();
if (textToSend.length() == 0) textToSend = "\r";
session.write(textToSend);
} else {
mActivity.getTermuxTerminalSessionClient().removeFinishedSession(session);
}
editText.setText("");
}
return true;
});
}
collection.addView(layout);
return layout;
}
@Override
public void destroyItem(@NonNull ViewGroup collection, int position, @NonNull Object view) {
collection.removeView((View) view);
}
}
public static class OnPageChangeListener extends ViewPager.SimpleOnPageChangeListener {
final TermuxActivity mActivity;
final ViewPager mTerminalToolbarViewPager;
public OnPageChangeListener(TermuxActivity activity, ViewPager viewPager) {
this.mActivity = activity;
this.mTerminalToolbarViewPager = viewPager;
}
@Override
public void onPageSelected(int position) {
if (position == 0) {
mActivity.getTerminalView().requestFocus();
} else {
final EditText editText = mTerminalToolbarViewPager.findViewById(R.id.terminal_toolbar_text_input);
if (editText != null) editText.requestFocus();
}
}
}
}

View File

@@ -0,0 +1,92 @@
package com.termux.app.terminal.io.extrakeys;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Arrays;
import java.util.stream.Collectors;
public class ExtraKeyButton {
/**
* The key that will be sent to the terminal, either a control character
* defined in ExtraKeysView.keyCodesForString (LEFT, RIGHT, PGUP...) or
* some text.
*/
private final String key;
/**
* If the key is a macro, i.e. a sequence of keys separated by space.
*/
private final boolean macro;
/**
* The text that will be shown on the button.
*/
private final String display;
/**
* The information of the popup (triggered by swipe up).
*/
@Nullable
private ExtraKeyButton popup;
public ExtraKeyButton(ExtraKeysInfo.CharDisplayMap charDisplayMap, JSONObject config) throws JSONException {
this(charDisplayMap, config, null);
}
public ExtraKeyButton(ExtraKeysInfo.CharDisplayMap charDisplayMap, JSONObject config, @Nullable ExtraKeyButton popup) throws JSONException {
String keyFromConfig = config.optString("key", null);
String macroFromConfig = config.optString("macro", null);
String[] keys;
if (keyFromConfig != null && macroFromConfig != null) {
throw new JSONException("Both key and macro can't be set for the same key");
} else if (keyFromConfig != null) {
keys = new String[]{keyFromConfig};
this.macro = false;
} else if (macroFromConfig != null) {
keys = macroFromConfig.split(" ");
this.macro = true;
} else {
throw new JSONException("All keys have to specify either key or macro");
}
for (int i = 0; i < keys.length; i++) {
keys[i] = ExtraKeysInfo.replaceAlias(keys[i]);
}
this.key = TextUtils.join(" ", keys);
String displayFromConfig = config.optString("display", null);
if (displayFromConfig != null) {
this.display = displayFromConfig;
} else {
this.display = Arrays.stream(keys)
.map(key -> charDisplayMap.get(key, key))
.collect(Collectors.joining(" "));
}
this.popup = popup;
}
public String getKey() {
return key;
}
public boolean isMacro() {
return macro;
}
public String getDisplay() {
return display;
}
@Nullable
public ExtraKeyButton getPopup() {
return popup;
}
}

View File

@@ -0,0 +1,259 @@
package com.termux.app.terminal.io.extrakeys;
import com.termux.shared.logger.Logger;
import com.termux.shared.settings.properties.TermuxPropertyConstants;
import com.termux.shared.settings.properties.TermuxSharedProperties;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashMap;
public class ExtraKeysInfo {
/**
* Matrix of buttons displayed
*/
private final ExtraKeyButton[][] buttons;
/**
* This corresponds to one of the CharMapDisplay below
*/
private String style;
public ExtraKeysInfo(String propertiesInfo, String style) throws JSONException {
this.style = style;
// Convert String propertiesInfo to Array of Arrays
JSONArray arr = new JSONArray(propertiesInfo);
Object[][] matrix = new Object[arr.length()][];
for (int i = 0; i < arr.length(); i++) {
JSONArray line = arr.getJSONArray(i);
matrix[i] = new Object[line.length()];
for (int j = 0; j < line.length(); j++) {
matrix[i][j] = line.get(j);
}
}
// convert matrix to buttons
this.buttons = new ExtraKeyButton[matrix.length][];
for (int i = 0; i < matrix.length; i++) {
this.buttons[i] = new ExtraKeyButton[matrix[i].length];
for (int j = 0; j < matrix[i].length; j++) {
Object key = matrix[i][j];
JSONObject jobject = normalizeKeyConfig(key);
ExtraKeyButton button;
if (! jobject.has("popup")) {
// no popup
button = new ExtraKeyButton(getSelectedCharMap(), jobject);
} else {
// a popup
JSONObject popupJobject = normalizeKeyConfig(jobject.get("popup"));
ExtraKeyButton popup = new ExtraKeyButton(getSelectedCharMap(), popupJobject);
button = new ExtraKeyButton(getSelectedCharMap(), jobject, popup);
}
this.buttons[i][j] = button;
}
}
}
/**
* "hello" -> {"key": "hello"}
*/
private static JSONObject normalizeKeyConfig(Object key) throws JSONException {
JSONObject jobject;
if (key instanceof String) {
jobject = new JSONObject();
jobject.put("key", key);
} else if (key instanceof JSONObject) {
jobject = (JSONObject) key;
} else {
throw new JSONException("An key in the extra-key matrix must be a string or an object");
}
return jobject;
}
public ExtraKeyButton[][] getMatrix() {
return buttons;
}
/**
* HashMap that implements Python dict.get(key, default) function.
* Default java.util .get(key) is then the same as .get(key, null);
*/
static class CleverMap<K,V> extends HashMap<K,V> {
V get(K key, V defaultValue) {
if (containsKey(key))
return get(key);
else
return defaultValue;
}
}
static class CharDisplayMap extends CleverMap<String, String> {}
/**
* Keys are displayed in a natural looking way, like "→" for "RIGHT"
*/
static final CharDisplayMap classicArrowsDisplay = new CharDisplayMap() {{
// classic arrow keys (for ◀ ▶ ▲ ▼ @see arrowVariationDisplay)
put("LEFT", ""); // U+2190 ← LEFTWARDS ARROW
put("RIGHT", ""); // U+2192 → RIGHTWARDS ARROW
put("UP", ""); // U+2191 ↑ UPWARDS ARROW
put("DOWN", ""); // U+2193 ↓ DOWNWARDS ARROW
}};
static final CharDisplayMap wellKnownCharactersDisplay = new CharDisplayMap() {{
// well known characters // https://en.wikipedia.org/wiki/{Enter_key, Tab_key, Delete_key}
put("ENTER", ""); // U+21B2 ↲ DOWNWARDS ARROW WITH TIP LEFTWARDS
put("TAB", ""); // U+21B9 ↹ LEFTWARDS ARROW TO BAR OVER RIGHTWARDS ARROW TO BAR
put("BKSP", ""); // U+232B ⌫ ERASE TO THE LEFT sometimes seen and easy to understand
put("DEL", ""); // U+2326 ⌦ ERASE TO THE RIGHT not well known but easy to understand
put("DRAWER", ""); // U+2630 ☰ TRIGRAM FOR HEAVEN not well known but easy to understand
put("KEYBOARD", ""); // U+2328 ⌨ KEYBOARD not well known but easy to understand
}};
static final CharDisplayMap lessKnownCharactersDisplay = new CharDisplayMap() {{
// https://en.wikipedia.org/wiki/{Home_key, End_key, Page_Up_and_Page_Down_keys}
// home key can mean "goto the beginning of line" or "goto first page" depending on context, hence the diagonal
put("HOME", ""); // from IEC 9995 // U+21F1 ⇱ NORTH WEST ARROW TO CORNER
put("END", ""); // from IEC 9995 // ⇲ // U+21F2 ⇲ SOUTH EAST ARROW TO CORNER
put("PGUP", ""); // no ISO character exists, U+21D1 ⇑ UPWARDS DOUBLE ARROW will do the trick
put("PGDN", ""); // no ISO character exists, U+21D3 ⇓ DOWNWARDS DOUBLE ARROW will do the trick
}};
static final CharDisplayMap arrowTriangleVariationDisplay = new CharDisplayMap() {{
// alternative to classic arrow keys
put("LEFT", ""); // U+25C0 ◀ BLACK LEFT-POINTING TRIANGLE
put("RIGHT", ""); // U+25B6 ▶ BLACK RIGHT-POINTING TRIANGLE
put("UP", ""); // U+25B2 ▲ BLACK UP-POINTING TRIANGLE
put("DOWN", ""); // U+25BC ▼ BLACK DOWN-POINTING TRIANGLE
}};
static final CharDisplayMap notKnownIsoCharacters = new CharDisplayMap() {{
// Control chars that are more clear as text // https://en.wikipedia.org/wiki/{Function_key, Alt_key, Control_key, Esc_key}
// put("FN", "FN"); // no ISO character exists
put("CTRL", ""); // ISO character "U+2388 ⎈ HELM SYMBOL" is unknown to people and never printed on computers, however "U+25C7 ◇ WHITE DIAMOND" is a nice presentation, and "^" for terminal app and mac is often used
put("ALT", ""); // ISO character "U+2387 ⎇ ALTERNATIVE KEY SYMBOL'" is unknown to people and only printed as the Option key "⌥" on Mac computer
put("ESC", ""); // ISO character "U+238B ⎋ BROKEN CIRCLE WITH NORTHWEST ARROW" is unknown to people and not often printed on computers
}};
static final CharDisplayMap nicerLookingDisplay = new CharDisplayMap() {{
// nicer looking for most cases
put("-", ""); // U+2015 ― HORIZONTAL BAR
}};
/*
* Multiple maps are available to quickly change
* the style of the keys.
*/
/**
* Some classic symbols everybody knows
*/
private static final CharDisplayMap defaultCharDisplay = new CharDisplayMap() {{
putAll(classicArrowsDisplay);
putAll(wellKnownCharactersDisplay);
putAll(nicerLookingDisplay);
// all other characters are displayed as themselves
}};
/**
* Classic symbols and less known symbols
*/
private static final CharDisplayMap lotsOfArrowsCharDisplay = new CharDisplayMap() {{
putAll(classicArrowsDisplay);
putAll(wellKnownCharactersDisplay);
putAll(lessKnownCharactersDisplay); // NEW
putAll(nicerLookingDisplay);
}};
/**
* Only arrows
*/
private static final CharDisplayMap arrowsOnlyCharDisplay = new CharDisplayMap() {{
putAll(classicArrowsDisplay);
// putAll(wellKnownCharactersDisplay); // REMOVED
// putAll(lessKnownCharactersDisplay); // REMOVED
putAll(nicerLookingDisplay);
}};
/**
* Full Iso
*/
private static final CharDisplayMap fullIsoCharDisplay = new CharDisplayMap() {{
putAll(classicArrowsDisplay);
putAll(wellKnownCharactersDisplay);
putAll(lessKnownCharactersDisplay); // NEW
putAll(nicerLookingDisplay);
putAll(notKnownIsoCharacters); // NEW
}};
/**
* Some people might call our keys differently
*/
static private final CharDisplayMap controlCharsAliases = new CharDisplayMap() {{
put("ESCAPE", "ESC");
put("CONTROL", "CTRL");
put("RETURN", "ENTER"); // Technically different keys, but most applications won't see the difference
put("FUNCTION", "FN");
// no alias for ALT
// Directions are sometimes written as first and last letter for brevety
put("LT", "LEFT");
put("RT", "RIGHT");
put("DN", "DOWN");
// put("UP", "UP"); well, "UP" is already two letters
put("PAGEUP", "PGUP");
put("PAGE_UP", "PGUP");
put("PAGE UP", "PGUP");
put("PAGE-UP", "PGUP");
// no alias for HOME
// no alias for END
put("PAGEDOWN", "PGDN");
put("PAGE_DOWN", "PGDN");
put("PAGE-DOWN", "PGDN");
put("DELETE", "DEL");
put("BACKSPACE", "BKSP");
// easier for writing in termux.properties
put("BACKSLASH", "\\");
put("QUOTE", "\"");
put("APOSTROPHE", "'");
}};
CharDisplayMap getSelectedCharMap() {
switch (style) {
case "arrows-only":
return arrowsOnlyCharDisplay;
case "arrows-all":
return lotsOfArrowsCharDisplay;
case "all":
return fullIsoCharDisplay;
case "none":
return new CharDisplayMap();
default:
if (!TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE.equals(style))
Logger.logError(TermuxSharedProperties.LOG_TAG, "The style \"" + style + "\" for the key \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE + "\" is invalid. Using default style instead.");
return defaultCharDisplay;
}
}
/**
* Applies the 'controlCharsAliases' mapping to all the strings in *buttons*
* Modifies the array, doesn't return a new one.
*/
public static String replaceAlias(String key) {
return controlCharsAliases.get(key, key);
}
}

View File

@@ -0,0 +1,388 @@
package com.termux.app.terminal.io.extrakeys;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.provider.Settings;
import android.util.AttributeSet;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ScheduledExecutorService;
import java.util.Map;
import java.util.HashMap;
import java.util.Arrays;
import java.util.stream.Collectors;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.GridLayout;
import android.widget.PopupWindow;
import com.termux.R;
import com.termux.app.terminal.TermuxTerminalViewClient;
import com.termux.view.TerminalView;
import androidx.drawerlayout.widget.DrawerLayout;
/**
* A view showing extra keys (such as Escape, Ctrl, Alt) not normally available on an Android soft
* keyboard.
*/
public final class ExtraKeysView extends GridLayout {
private static final int TEXT_COLOR = 0xFFFFFFFF;
private static final int BUTTON_COLOR = 0x00000000;
private static final int INTERESTING_COLOR = 0xFF80DEEA;
private static final int BUTTON_PRESSED_COLOR = 0xFF7F7F7F;
TermuxTerminalViewClient mTermuxTerminalViewClient;
public ExtraKeysView(Context context, AttributeSet attrs) {
super(context, attrs);
}
static final Map<String, Integer> keyCodesForString = new HashMap<String, Integer>() {{
put("SPACE", KeyEvent.KEYCODE_SPACE);
put("ESC", KeyEvent.KEYCODE_ESCAPE);
put("TAB", KeyEvent.KEYCODE_TAB);
put("HOME", KeyEvent.KEYCODE_MOVE_HOME);
put("END", KeyEvent.KEYCODE_MOVE_END);
put("PGUP", KeyEvent.KEYCODE_PAGE_UP);
put("PGDN", KeyEvent.KEYCODE_PAGE_DOWN);
put("INS", KeyEvent.KEYCODE_INSERT);
put("DEL", KeyEvent.KEYCODE_FORWARD_DEL);
put("BKSP", KeyEvent.KEYCODE_DEL);
put("UP", KeyEvent.KEYCODE_DPAD_UP);
put("LEFT", KeyEvent.KEYCODE_DPAD_LEFT);
put("RIGHT", KeyEvent.KEYCODE_DPAD_RIGHT);
put("DOWN", KeyEvent.KEYCODE_DPAD_DOWN);
put("ENTER", KeyEvent.KEYCODE_ENTER);
put("F1", KeyEvent.KEYCODE_F1);
put("F2", KeyEvent.KEYCODE_F2);
put("F3", KeyEvent.KEYCODE_F3);
put("F4", KeyEvent.KEYCODE_F4);
put("F5", KeyEvent.KEYCODE_F5);
put("F6", KeyEvent.KEYCODE_F6);
put("F7", KeyEvent.KEYCODE_F7);
put("F8", KeyEvent.KEYCODE_F8);
put("F9", KeyEvent.KEYCODE_F9);
put("F10", KeyEvent.KEYCODE_F10);
put("F11", KeyEvent.KEYCODE_F11);
put("F12", KeyEvent.KEYCODE_F12);
}};
@SuppressLint("RtlHardcoded")
private void sendKey(View view, String keyName, boolean forceCtrlDown, boolean forceLeftAltDown) {
TerminalView terminalView = view.findViewById(R.id.terminal_view);
if ("KEYBOARD".equals(keyName)) {
if(mTermuxTerminalViewClient != null)
mTermuxTerminalViewClient.onToggleSoftKeyboardRequest();
} else if ("DRAWER".equals(keyName)) {
DrawerLayout drawer = view.findViewById(R.id.drawer_layout);
drawer.openDrawer(Gravity.LEFT);
} else if (keyCodesForString.containsKey(keyName)) {
Integer keyCode = keyCodesForString.get(keyName);
if (keyCode == null) return;
int metaState = 0;
if (forceCtrlDown) {
metaState |= KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON;
}
if (forceLeftAltDown) {
metaState |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON;
}
KeyEvent keyEvent = new KeyEvent(0, 0, KeyEvent.ACTION_UP, keyCode, 0, metaState);
terminalView.onKeyDown(keyCode, keyEvent);
} else {
// not a control char
keyName.codePoints().forEach(codePoint -> {
terminalView.inputCodePoint(codePoint, forceCtrlDown, forceLeftAltDown);
});
}
}
private void sendKey(View view, ExtraKeyButton buttonInfo) {
if (buttonInfo.isMacro()) {
String[] keys = buttonInfo.getKey().split(" ");
boolean ctrlDown = false;
boolean altDown = false;
for (String key : keys) {
if ("CTRL".equals(key)) {
ctrlDown = true;
} else if ("ALT".equals(key)) {
altDown = true;
} else {
sendKey(view, key, ctrlDown, altDown);
ctrlDown = false;
altDown = false;
}
}
} else {
sendKey(view, buttonInfo.getKey(), false, false);
}
}
public enum SpecialButton {
CTRL, ALT, FN
}
private static class SpecialButtonState {
boolean isOn = false;
boolean isActive = false;
List<Button> buttons = new ArrayList<>();
void setIsActive(boolean value) {
isActive = value;
buttons.forEach(button -> button.setTextColor(value ? INTERESTING_COLOR : TEXT_COLOR));
}
}
private final Map<SpecialButton, SpecialButtonState> specialButtons = new HashMap<SpecialButton, SpecialButtonState>() {{
put(SpecialButton.CTRL, new SpecialButtonState());
put(SpecialButton.ALT, new SpecialButtonState());
put(SpecialButton.FN, new SpecialButtonState());
}};
private final Set<String> specialButtonsKeys = specialButtons.keySet().stream().map(Enum::name).collect(Collectors.toSet());
private boolean isSpecialButton(ExtraKeyButton button) {
return specialButtonsKeys.contains(button.getKey());
}
private ScheduledExecutorService scheduledExecutor;
private PopupWindow popupWindow;
private int longPressCount;
public boolean readSpecialButton(SpecialButton name) {
SpecialButtonState state = specialButtons.get(name);
if (state == null)
throw new RuntimeException("Must be a valid special button (see source)");
if (!state.isOn || !state.isActive)
return false;
state.setIsActive(false);
return true;
}
private Button createSpecialButton(String buttonKey, boolean needUpdate) {
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonKey));
if (state == null) return null;
state.isOn = true;
Button button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
button.setTextColor(state.isActive ? INTERESTING_COLOR : TEXT_COLOR);
if (needUpdate) {
state.buttons.add(button);
}
return button;
}
void popup(View view, ExtraKeyButton extraButton) {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
Button button;
if (isSpecialButton(extraButton)) {
button = createSpecialButton(extraButton.getKey(), false);
if (button == null) return;
} else {
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
button.setTextColor(TEXT_COLOR);
}
button.setText(extraButton.getDisplay());
button.setPadding(0, 0, 0, 0);
button.setMinHeight(0);
button.setMinWidth(0);
button.setMinimumWidth(0);
button.setMinimumHeight(0);
button.setWidth(width);
button.setHeight(height);
button.setBackgroundColor(BUTTON_PRESSED_COLOR);
popupWindow = new PopupWindow(this);
popupWindow.setWidth(LayoutParams.WRAP_CONTENT);
popupWindow.setHeight(LayoutParams.WRAP_CONTENT);
popupWindow.setContentView(button);
popupWindow.setOutsideTouchable(true);
popupWindow.setFocusable(false);
popupWindow.showAsDropDown(view, 0, -2 * height);
}
/**
* General util function to compute the longest column length in a matrix.
*/
static int maximumLength(Object[][] matrix) {
int m = 0;
for (Object[] row : matrix)
m = Math.max(m, row.length);
return m;
}
/**
* Reload the view given parameters in termux.properties
*
* @param infos matrix as defined in termux.properties extrakeys
* Can Contain The Strings CTRL ALT TAB FN ENTER LEFT RIGHT UP DOWN or normal strings
* Some aliases are possible like RETURN for ENTER, LT for LEFT and more (@see controlCharsAliases for the whole list).
* Any string of length > 1 in total Uppercase will print a warning
*
* Examples:
* "ENTER" will trigger the ENTER keycode
* "LEFT" will trigger the LEFT keycode and be displayed as "←"
* "→" will input a "→" character
* "" will input a "" character
* "-_-" will input the string "-_-"
*/
@SuppressLint("ClickableViewAccessibility")
public void reload(ExtraKeysInfo infos) {
if (infos == null)
return;
for(SpecialButtonState state : specialButtons.values())
state.buttons = new ArrayList<>();
removeAllViews();
ExtraKeyButton[][] buttons = infos.getMatrix();
setRowCount(buttons.length);
setColumnCount(maximumLength(buttons));
for (int row = 0; row < buttons.length; row++) {
for (int col = 0; col < buttons[row].length; col++) {
final ExtraKeyButton buttonInfo = buttons[row][col];
Button button;
if (isSpecialButton(buttonInfo)) {
button = createSpecialButton(buttonInfo.getKey(), true);
if (button == null) return;
} else {
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
}
button.setText(buttonInfo.getDisplay());
button.setTextColor(TEXT_COLOR);
button.setPadding(0, 0, 0, 0);
final Button finalButton = button;
button.setOnClickListener(v -> {
if (Settings.System.getInt(getContext().getContentResolver(),
Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) != 0) {
if (Build.VERSION.SDK_INT >= 28) {
finalButton.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
} else {
// Perform haptic feedback only if no total silence mode enabled.
if (Settings.Global.getInt(getContext().getContentResolver(), "zen_mode", 0) != 2) {
finalButton.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
}
}
}
View root = getRootView();
if (isSpecialButton(buttonInfo)) {
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonInfo.getKey()));
if (state == null) return;
state.setIsActive(!state.isActive);
} else {
sendKey(root, buttonInfo);
}
});
button.setOnTouchListener((v, event) -> {
final View root = getRootView();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
longPressCount = 0;
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
if (Arrays.asList("UP", "DOWN", "LEFT", "RIGHT", "BKSP", "DEL").contains(buttonInfo.getKey())) {
// autorepeat
scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
scheduledExecutor.scheduleWithFixedDelay(() -> {
longPressCount++;
sendKey(root, buttonInfo);
}, 400, 80, TimeUnit.MILLISECONDS);
}
return true;
case MotionEvent.ACTION_MOVE:
if (buttonInfo.getPopup() != null) {
if (popupWindow == null && event.getY() < 0) {
if (scheduledExecutor != null) {
scheduledExecutor.shutdownNow();
scheduledExecutor = null;
}
v.setBackgroundColor(BUTTON_COLOR);
popup(v, buttonInfo.getPopup());
}
if (popupWindow != null && event.getY() > 0) {
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
popupWindow.dismiss();
popupWindow = null;
}
}
return true;
case MotionEvent.ACTION_CANCEL:
v.setBackgroundColor(BUTTON_COLOR);
if (scheduledExecutor != null) {
scheduledExecutor.shutdownNow();
scheduledExecutor = null;
}
return true;
case MotionEvent.ACTION_UP:
v.setBackgroundColor(BUTTON_COLOR);
if (scheduledExecutor != null) {
scheduledExecutor.shutdownNow();
scheduledExecutor = null;
}
if (longPressCount == 0 || popupWindow != null) {
if (popupWindow != null) {
popupWindow.setContentView(null);
popupWindow.dismiss();
popupWindow = null;
if (buttonInfo.getPopup() != null) {
if (isSpecialButton(buttonInfo.getPopup())) {
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonInfo.getPopup().getKey()));
if (state == null) return true;
state.setIsActive(!state.isActive);
} else {
sendKey(root, buttonInfo.getPopup());
}
}
} else {
v.performClick();
}
}
return true;
default:
return true;
}
});
LayoutParams param = new GridLayout.LayoutParams();
param.width = 0;
param.height = 0;
param.setMargins(0, 0, 0, 0);
param.columnSpec = GridLayout.spec(col, GridLayout.FILL, 1.f);
param.rowSpec = GridLayout.spec(row, GridLayout.FILL, 1.f);
button.setLayoutParams(param);
addView(button);
}
}
}
public void setTermuxTerminalViewClient(TermuxTerminalViewClient termuxTerminalViewClient) {
this.mTermuxTerminalViewClient = termuxTerminalViewClient;
}
}

View File

@@ -0,0 +1,188 @@
package com.termux.app.utils;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import androidx.annotation.Nullable;
import com.termux.R;
import com.termux.shared.activities.ReportActivity;
import com.termux.shared.models.errors.Error;
import com.termux.shared.notification.NotificationUtils;
import com.termux.shared.file.FileUtils;
import com.termux.shared.models.ReportInfo;
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.TermuxPreferenceConstants;
import com.termux.shared.data.DataUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.termux.TermuxUtils;
import com.termux.shared.termux.TermuxConstants;
import java.nio.charset.Charset;
public class CrashUtils {
private static final String LOG_TAG = "CrashUtils";
/**
* Notify the user of an app crash at last run by reading the crash info from the crash log file
* at {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}. The crash log file would have been
* created by {@link com.termux.shared.crash.CrashHandler}.
*
* If the crash log file exists and is not empty and
* {@link TermuxPreferenceConstants.TERMUX_APP#KEY_CRASH_REPORT_NOTIFICATIONS_ENABLED} is
* enabled, then a notification will be shown for the crash on the
* {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME} channel, otherwise nothing will be done.
*
* After reading from the crash log file, it will be moved to {@link TermuxConstants#TERMUX_CRASH_LOG_BACKUP_FILE_PATH}.
*
* @param context The {@link Context} for operations.
* @param logTagParam The log tag to use for logging.
*/
public static void notifyAppCrashOnLastRun(final Context context, final String logTagParam) {
if (context == null) return;
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
if (preferences == null) return;
// If user has disabled notifications for crashes
if (!preferences.areCrashReportNotificationsEnabled())
return;
new Thread() {
@Override
public void run() {
String logTag = DataUtils.getDefaultIfNull(logTagParam, LOG_TAG);
if (!FileUtils.regularFileExists(TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, false))
return;
Error error;
StringBuilder reportStringBuilder = new StringBuilder();
// Read report string from crash log file
error = FileUtils.readStringFromFile("crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, Charset.defaultCharset(), reportStringBuilder, false);
if (error != null) {
Logger.logErrorExtended(logTag, error.toString());
return;
}
// Move crash log file to backup location if it exists
error = FileUtils.moveRegularFile("crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, TermuxConstants.TERMUX_CRASH_LOG_BACKUP_FILE_PATH, true);
if (error != null) {
Logger.logErrorExtended(logTag, error.toString());
}
String reportString = reportStringBuilder.toString();
if (reportString.isEmpty())
return;
Logger.logDebug(logTag, "A crash log file found at \"" + TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH + "\".");
sendCrashReportNotification(context, logTag, reportString, false);
}
}.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}
* and {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}.
*
* @param context The {@link Context} for operations.
* @param title The title for 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 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_*}.
* @return Returns the {@link Notification.Builder}.
*/
@Nullable
public static Notification.Builder getCrashReportsNotificationBuilder(final Context context, final CharSequence title, final CharSequence notificationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) {
Notification.Builder builder = NotificationUtils.geNotificationBuilder(context,
TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_HIGH,
title, notificationText, notificationBigText, pendingIntent, notificationMode);
if (builder == null) return null;
// Enable timestamp
builder.setShowWhen(true);
// Set notification icon
builder.setSmallIcon(R.drawable.ic_error_notification);
// Set background color for small notification icon
builder.setColor(0xFF607D8B);
// Dismiss on click
builder.setAutoCancel(true);
return builder;
}
/**
* Setup the notification channel 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.
*/
public static void setupCrashReportsNotificationChannel(final Context context) {
NotificationUtils.setupNotificationChannel(context, TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID,
TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
}
}

View File

@@ -0,0 +1,307 @@
package com.termux.app.utils;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import androidx.annotation.Nullable;
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.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.TERMUX_APP.TERMUX_SERVICE;
import com.termux.shared.logger.Logger;
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_APP;
import com.termux.shared.settings.properties.SharedProperties;
import com.termux.shared.settings.properties.TermuxPropertyConstants;
import com.termux.shared.models.ReportInfo;
import com.termux.shared.models.ExecutionCommand;
import com.termux.app.models.UserAction;
import com.termux.shared.data.DataUtils;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.termux.TermuxUtils;
public class PluginUtils {
private static final String LOG_TAG = "PluginUtils";
/**
* Process {@link ExecutionCommand} result.
*
* The ExecutionCommand currentState must be greater or equal to
* {@link ExecutionCommand.ExecutionState#EXECUTED}.
* If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and
* {@link ResultConfig#resultPendingIntent} or {@link ResultConfig#resultDirectoryPath}
* 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 logTag The log tag to use for logging.
* @param executionCommand The {@link ExecutionCommand} to process.
*/
public static void processPluginExecutionCommandResult(final Context context, String logTag, final ExecutionCommand executionCommand) {
if (executionCommand == null) return;
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
Error error = null;
ResultData resultData = executionCommand.resultData;
if (!executionCommand.hasExecuted()) {
Logger.logWarn(logTag, executionCommand.getCommandIdAndLabelLogString() + ": Ignoring call to processPluginExecutionCommandResult() since the execution command state is not higher than the ExecutionState.EXECUTED");
return;
}
boolean isPluginExecutionCommandWithPendingResult = executionCommand.isPluginExecutionCommandWithPendingResult();
// 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 execution command was started by a plugin which expects the result back
if (isPluginExecutionCommandWithPendingResult) {
// Set variables which will be used by sendCommandResultData to send back the result
if (executionCommand.resultConfig.resultPendingIntent != null)
setPluginResultPendingIntentVariables(executionCommand);
if (executionCommand.resultConfig.resultDirectoryPath != null)
setPluginResultDirectoryVariables(executionCommand);
// Send result to caller
error = ResultSender.sendCommandResultData(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.resultConfig, executionCommand.resultData);
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));
}
}
if (!executionCommand.isStateFailed() && error == null)
executionCommand.setState(ExecutionCommand.ExecutionState.SUCCESS);
}
/**
* Process {@link ExecutionCommand} error.
*
* The ExecutionCommand currentState must be equal to {@link ExecutionCommand.ExecutionState#FAILED}.
* The {@link ResultData#getErrCode()} must have been set to a value greater than
* {@link Errno#ERRNO_SUCCESS}.
* The {@link ResultData#errorsList} must also be set with appropriate error info.
*
* If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and
* {@link ResultConfig#resultPendingIntent} or {@link ResultConfig#resultDirectoryPath}
* 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
* enabled, then a flash and a notification will be shown for the error as well
* on the {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME} channel instead of just logging
* the error.
*
* @param context The {@link Context} for operations.
* @param logTag The log tag to use for logging.
* @param executionCommand The {@link ExecutionCommand} that failed.
* @param forceNotification If set to {@code true}, then a flash and notification will be shown
* regardless of if pending intent is {@code null} or
* {@link TERMUX_APP#KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED}
* is {@code false}.
*/
public static void processPluginExecutionCommandError(final Context context, String logTag, final ExecutionCommand executionCommand, boolean forceNotification) {
if (context == null || executionCommand == null) return;
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
Error error;
ResultData resultData = executionCommand.resultData;
if (!executionCommand.isStateFailed()) {
Logger.logWarn(logTag, executionCommand.getCommandIdAndLabelLogString() + ": Ignoring call to processPluginExecutionCommandError() since the execution command is not in ExecutionState.FAILED");
return;
}
boolean isPluginExecutionCommandWithPendingResult = executionCommand.isPluginExecutionCommandWithPendingResult();
// 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 execution command was started by a plugin which expects the result back
if (isPluginExecutionCommandWithPendingResult) {
// Set variables which will be used by sendCommandResultData to send back the result
if (executionCommand.resultConfig.resultPendingIntent != null)
setPluginResultPendingIntentVariables(executionCommand);
if (executionCommand.resultConfig.resultDirectoryPath != null)
setPluginResultDirectoryVariables(executionCommand);
// Send result to caller
error = ResultSender.sendCommandResultData(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.resultConfig, executionCommand.resultData);
if (error != null) {
// error will be added to existing Errors
resultData.setStateFailed(error);
Logger.logErrorExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, true));
forceNotification = true;
}
// No need to show notifications if a pending intent was sent, let the caller handle the result himself
if (!forceNotification) return;
}
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
if (preferences == null) return;
// If user has disabled notifications for plugin commands, then just return
if (!preferences.arePluginErrorNotificationsEnabled() && !forceNotification)
return;
// Flash and send notification for the error
Logger.showToast(context, ResultData.getErrorsListMinimalString(resultData), true);
sendPluginCommandErrorNotification(context, logTag, executionCommand, ResultData.getErrorsListMinimalString(resultData));
}
/** 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
String title = TermuxConstants.TERMUX_APP_NAME + " Plugin Execution Command Error";
StringBuilder reportString = new StringBuilder();
reportString.append(ExecutionCommand.getExecutionCommandMarkdownString(executionCommand));
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, true));
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context));
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);
// Setup the notification channel if not already set up
setupPluginCommandErrorsNotificationChannel(context);
// Use markdown in notification
CharSequence notificationTextCharSequence = MarkdownUtils.getSpannedMarkdownText(context, notificationTextString);
//CharSequence notificationTextCharSequence = notificationTextString;
// Build the notification
Notification.Builder builder = getPluginCommandErrorsNotificationBuilder(context, title, notificationTextCharSequence, notificationTextCharSequence, 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_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
* and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
*
* @param context The {@link Context} for operations.
* @param title The title for 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 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_*}.
* @return Returns the {@link Notification.Builder}.
*/
@Nullable
public static Notification.Builder getPluginCommandErrorsNotificationBuilder(final Context context, final CharSequence title, final CharSequence notificationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) {
Notification.Builder builder = NotificationUtils.geNotificationBuilder(context,
TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_HIGH,
title, notificationText, notificationBigText, pendingIntent, notificationMode);
if (builder == null) return null;
// Enable timestamp
builder.setShowWhen(true);
// Set notification icon
builder.setSmallIcon(R.drawable.ic_error_notification);
// Set background color for small notification icon
builder.setColor(0xFF607D8B);
// Dismiss on click
builder.setAutoCancel(true);
return builder;
}
/**
* Setup the notification channel 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.
*/
public static void setupPluginCommandErrorsNotificationChannel(final Context context) {
NotificationUtils.setupNotificationChannel(context, TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID,
TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
}
/**
* Check if {@link TermuxConstants#PROP_ALLOW_EXTERNAL_APPS} property is not set to "true".
*
* @param context The {@link Context} to get error string.
* @return Returns the {@code error} if policy is violated, otherwise {@code null}.
*/
public static String checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(final Context context) {
String errmsg = null;
if (!SharedProperties.isPropertyValueTrue(context, TermuxPropertyConstants.getTermuxPropertiesFile(), TermuxConstants.PROP_ALLOW_EXTERNAL_APPS, true)) {
errmsg = context.getString(R.string.error_run_command_service_allow_external_apps_ungranted);
}
return errmsg;
}
}

View File

@@ -12,7 +12,7 @@ import android.provider.DocumentsProvider;
import android.webkit.MimeTypeMap;
import com.termux.R;
import com.termux.app.TermuxService;
import com.termux.shared.termux.TermuxConstants;
import java.io.File;
import java.io.FileNotFoundException;
@@ -22,7 +22,7 @@ import java.util.LinkedList;
/**
* A document provider for the Storage Access Framework which exposes the files in the
* $HOME/ folder to other apps.
* $HOME/ directory to other apps.
* <p/>
* Note that this replaces providing an activity matching the ACTION_GET_CONTENT intent:
* <p/>
@@ -35,7 +35,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
private static final String ALL_MIME_TYPES = "*/*";
private static final File BASE_DIR = new File(TermuxService.HOME_PATH);
private static final File BASE_DIR = TermuxConstants.TERMUX_HOME_DIR;
// The default columns to return information about a root if no specific
@@ -63,19 +63,19 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
};
@Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
public Cursor queryRoots(String[] projection) {
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION);
@SuppressWarnings("ConstantConditions") final String applicationName = getContext().getString(R.string.application_name);
final String applicationName = getContext().getString(R.string.application_name);
final MatrixCursor.RowBuilder row = result.newRow();
row.add(Root.COLUMN_ROOT_ID, getDocIdForFile(BASE_DIR));
row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(BASE_DIR));
row.add(Root.COLUMN_SUMMARY, null);
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH);
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD);
row.add(Root.COLUMN_TITLE, applicationName);
row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES);
row.add(Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.getFreeSpace());
row.add(Root.COLUMN_ICON, R.drawable.ic_launcher);
row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher);
return result;
}
@@ -91,9 +91,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
final File parent = getFileForDocId(parentDocumentId);
for (File file : parent.listFiles()) {
if (!file.getName().startsWith(".")) {
includeFile(result, null, file);
}
includeFile(result, null, file);
}
return result;
}
@@ -117,6 +115,29 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
return true;
}
@Override
public String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException {
File newFile = new File(parentDocumentId, displayName);
int noConflictId = 2;
while (newFile.exists()) {
newFile = new File(parentDocumentId, displayName + " (" + noConflictId++ + ")");
}
try {
boolean succeeded;
if (Document.MIME_TYPE_DIR.equals(mimeType)) {
succeeded = newFile.mkdir();
} else {
succeeded = newFile.createNewFile();
}
if (!succeeded) {
throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
}
} catch (IOException e) {
throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
}
return newFile.getPath();
}
@Override
public void deleteDocument(String documentId) throws FileNotFoundException {
File file = getFileForDocId(documentId);
@@ -146,16 +167,15 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
final int MAX_SEARCH_RESULTS = 50;
while (!pending.isEmpty() && result.getCount() < MAX_SEARCH_RESULTS) {
final File file = pending.removeFirst();
// Avoid folders outside the $HOME folders linked in to symlinks (to avoid e.g. search
// Avoid directories outside the $HOME directory linked with symlinks (to avoid e.g. search
// through the whole SD card).
boolean isInsideHome;
try {
isInsideHome = file.getCanonicalPath().startsWith(TermuxService.HOME_PATH);
isInsideHome = file.getCanonicalPath().startsWith(TermuxConstants.TERMUX_HOME_DIR_PATH);
} catch (IOException e) {
isInsideHome = true;
}
final boolean isHidden = file.getName().startsWith(".");
if (isInsideHome && !isHidden) {
if (isInsideHome) {
if (file.isDirectory()) {
Collections.addAll(pending, file.listFiles());
} else {
@@ -169,6 +189,11 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
return result;
}
@Override
public boolean isChildDocument(String parentDocumentId, String documentId) {
return documentId.startsWith(parentDocumentId);
}
/**
* Get the document id given a file. This document id must be consistent across time as other
* applications may save the ID and use it to reference documents later.
@@ -220,10 +245,11 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
int flags = 0;
if (file.isDirectory()) {
if (file.isDirectory() && file.canWrite()) flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
if (file.canWrite()) flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
} else if (file.canWrite()) {
flags |= Document.FLAG_SUPPORTS_WRITE | Document.FLAG_SUPPORTS_DELETE;
flags |= Document.FLAG_SUPPORTS_WRITE;
}
if (file.getParentFile().canWrite()) flags |= Document.FLAG_SUPPORTS_DELETE;
final String displayName = file.getName();
final String mimeType = getMimeType(file);
@@ -236,7 +262,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
row.add(Document.COLUMN_MIME_TYPE, mimeType);
row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
row.add(Document.COLUMN_FLAGS, flags);
row.add(Document.COLUMN_ICON, R.drawable.ic_launcher);
row.add(Document.COLUMN_ICON, R.mipmap.ic_launcher);
}
}

View File

@@ -6,12 +6,14 @@ import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.provider.OpenableColumns;
import android.util.Log;
import android.util.Patterns;
import com.termux.R;
import com.termux.app.DialogUtils;
import com.termux.shared.interact.TextInputDialogUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
import com.termux.app.TermuxService;
import com.termux.shared.logger.Logger;
import java.io.ByteArrayInputStream;
import java.io.File;
@@ -21,12 +23,13 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.regex.Pattern;
public class TermuxFileReceiverActivity extends Activity {
static final String TERMUX_RECEIVEDIR = TermuxService.FILES_PATH + "/home/downloads";
static final String EDITOR_PROGRAM = TermuxService.HOME_PATH + "/bin/termux-file-editor";
static final String URL_OPENER_PROGRAM = TermuxService.HOME_PATH + "/bin/termux-url-opener";
static final String TERMUX_RECEIVEDIR = TermuxConstants.TERMUX_FILES_DIR_PATH + "/home/downloads";
static final String EDITOR_PROGRAM = TermuxConstants.TERMUX_HOME_DIR_PATH + "/bin/termux-file-editor";
static final String URL_OPENER_PROGRAM = TermuxConstants.TERMUX_HOME_DIR_PATH + "/bin/termux-url-opener";
/**
* If the activity should be finished when the name input dialog is dismissed. This is disabled
@@ -36,6 +39,13 @@ public class TermuxFileReceiverActivity extends Activity {
*/
boolean mFinishOnDismissNameDialog = true;
private static final String LOG_TAG = "TermuxFileReceiverActivity";
static boolean isSharedTextAnUrl(String sharedText) {
return Patterns.WEB_URL.matcher(sharedText).matches()
|| Pattern.matches("magnet:\\?xt=urn:btih:.*?", sharedText);
}
@Override
protected void onResume() {
super.onResume();
@@ -50,7 +60,7 @@ public class TermuxFileReceiverActivity extends Activity {
final Uri sharedUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
if (sharedText != null) {
if (Patterns.WEB_URL.matcher(sharedText).matches()) {
if (isSharedTextAnUrl(sharedText)) {
handleUrlAndFinish(sharedText);
} else {
String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
@@ -103,12 +113,12 @@ public class TermuxFileReceiverActivity extends Activity {
promptNameAndSave(in, attachmentFileName);
} catch (Exception e) {
showErrorDialogAndQuit("Unable to handle shared content:\n\n" + e.getMessage());
Log.e("termux", "handleContentUri(uri=" + uri + ") failed", e);
Logger.logStackTraceWithMessage(LOG_TAG, "handleContentUri(uri=" + uri + ") failed", e);
}
}
void promptNameAndSave(final InputStream in, final String attachmentFileName) {
DialogUtils.textInput(this, R.string.file_received_title, attachmentFileName, R.string.file_received_edit_button, text -> {
TextInputDialogUtils.textInput(this, R.string.title_file_received, attachmentFileName, R.string.action_file_received_edit, text -> {
File outFile = saveStreamWithName(in, text);
if (outFile == null) return;
@@ -125,17 +135,17 @@ public class TermuxFileReceiverActivity extends Activity {
final Uri scriptUri = new Uri.Builder().scheme("file").path(EDITOR_PROGRAM).build();
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE, scriptUri);
Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, scriptUri);
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
executeIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, new String[]{outFile.getAbsolutePath()});
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, new String[]{outFile.getAbsolutePath()});
startService(executeIntent);
finish();
},
R.string.file_received_open_folder_button, text -> {
R.string.action_file_received_open_directory, text -> {
if (saveStreamWithName(in, text) == null) return;
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE);
executeIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, TERMUX_RECEIVEDIR);
Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE);
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, TERMUX_RECEIVEDIR);
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
startService(executeIntent);
finish();
@@ -163,7 +173,7 @@ public class TermuxFileReceiverActivity extends Activity {
return outFile;
} catch (IOException e) {
showErrorDialogAndQuit("Error saving file:\n\n" + e);
Log.e("termux", "Error saving file", e);
Logger.logStackTraceWithMessage(LOG_TAG, "Error saving file", e);
return null;
}
}
@@ -182,9 +192,9 @@ public class TermuxFileReceiverActivity extends Activity {
final Uri urlOpenerProgramUri = new Uri.Builder().scheme("file").path(URL_OPENER_PROGRAM).build();
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE, urlOpenerProgramUri);
Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, urlOpenerProgramUri);
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
executeIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, new String[]{url});
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, new String[]{url});
startService(executeIntent);
finish();
}

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="#E0E0E0" />
</shape>
</shape>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="#212325" />
</shape>

View File

@@ -1,34 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="108dp"
android:width="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#000000"
android:pathData="M18,54
A36,36 0 1,1 90,54
A36,36 0 1,1 18,54 Z" />
<!-- Keep in sync with adaptive ic_foreground.xml: -->
<path
android:fillColor="#FFFFFF"
android:pathData="M34,38
h6
l12,16
l-12,16
h-6
l12,-16
"
/>
<path
android:fillColor="#FFFFFF"
android:pathData="M56,66
h18
v4
h-18
"
/>
</vector>

View File

@@ -1,33 +1,24 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="48dp"
android:width="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<!--
https://material.google.com/style/icons.html
Updated notification icon compliant with system icons guidelines
https://material.io/design/iconography/system-icons.html
-->
<!-- Screen border. -->
<path android:fillColor="#00000000"
android:strokeColor="#FFF"
android:strokeWidth="3"
android:pathData="M7,4
l34,0
q3 0,3 3
l0,34
q0 3, -3 3
l-34,0
q-3 0, -3-3
l0 -34
q0 -3, 3 -3"
/>
<group>
<clip-path
android:pathData="M0,0h24v24h-24z"/>
<!-- Block cursor. -->
<path android:fillColor="#FFF"
android:pathData="M14,14
l5,0
l0,10
l-5,0"
/>
<path
android:pathData="M5,4H2L8,12L2,20H5L11,12L5,4Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M13,18H22V20H13V18Z"
android:fillColor="#ffffff"/>
</group>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FF000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8" ?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_activated="true" android:drawable="@drawable/current_session_black"/>
<item android:state_activated="false" android:drawable="@drawable/session_ripple_black"/>
</selector>

View File

@@ -4,4 +4,4 @@
<item>
<color android:color="@android:color/white" />
</item>
</ripple>
</ripple>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@android:color/darker_gray" >
<item>
<color android:color="@android:color/background_dark" />
</item>
</ripple>

View File

@@ -0,0 +1,9 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/settings"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@@ -0,0 +1,109 @@
<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_height="match_parent"
android:orientation="vertical"
android:fitsSystemWindows="true">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical">
<androidx.drawerlayout.widget.DrawerLayout
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_alignParentTop="true"
android:layout_above="@+id/terminal_toolbar_view_pager"
android:layout_height="match_parent">
<com.termux.view.TerminalView
android:id="@+id/terminal_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginRight="3dp"
android:layout_marginLeft="3dp"
android:focusableInTouchMode="true"
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
android:scrollbars="vertical"
android:importantForAutofill="no"
android:autofillHints="password" />
<LinearLayout
android:id="@+id/left_drawer"
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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<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>
<ListView
android:id="@+id/terminal_sessions_list"
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_height="wrap_content"
android:orientation="horizontal">
<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>
</androidx.drawerlayout.widget.DrawerLayout>
<androidx.viewpager.widget.ViewPager
android:id="@+id/terminal_toolbar_view_pager"
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_height="1dp"
android:background="@android:color/transparent" />
</com.termux.app.terminal.TermuxActivityRootView>

View File

@@ -1,78 +0,0 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:fitsSystemWindows="true">
<androidx.drawerlayout.widget.DrawerLayout
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_alignParentTop="true"
android:layout_above="@+id/viewpager"
android:layout_height="match_parent">
<com.termux.view.TerminalView
android:id="@+id/terminal_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginRight="3dp"
android:layout_marginLeft="3dp"
android:focusableInTouchMode="true"
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
android:scrollbars="vertical" />
<LinearLayout
android:id="@+id/left_drawer"
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/left_drawer_list"
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_height="wrap_content"
android:orientation="horizontal">
<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/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/new_session" />
</LinearLayout>
</LinearLayout>
</androidx.drawerlayout.widget.DrawerLayout>
<androidx.viewpager.widget.ViewPager
android:id="@+id/viewpager"
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 File

@@ -1,9 +1,9 @@
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/row_line"
android:id="@+id/session_title"
android:layout_width="fill_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:background="@drawable/selected_session_background"
android:background="@drawable/session_background_selected"
android:ellipsize="marquee"
android:gravity="start|center_vertical"
android:padding="6dip"
android:textSize="14sp" />
android:textSize="14sp" />

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-appcompat-release/preference/preference/res/layout/preference.xml
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="vertical">
<TextView android:id="@android:id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="?android:attr/textColorPrimary" />
<include android:id="@android:id/summary" layout="@layout/markdown_adapter_node_default" />
</LinearLayout>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<com.termux.app.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/extra_keys"
<com.termux.app.terminal.io.extrakeys.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/terminal_toolbar_extra_keys"
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="match_parent"

View File

@@ -1,12 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<EditText xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/text_input"
android:id="@+id/terminal_toolbar_text_input"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:imeOptions="actionSend|flagNoFullscreen"
android:maxLines="1"
android:inputType="text"
android:importantForAutofill="no"
android:textColor="@android:color/white"
android:textColorHighlight="@android:color/darker_gray"
android:paddingTop="0dp"

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@android:color/black"/>
<foreground android:drawable="@drawable/ic_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View File

@@ -1,51 +1,182 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="application_name">Termux</string>
<string name="shared_user_label">Termux user</string>
<string name="new_session">New session</string>
<string name="new_session_failsafe">Failsafe</string>
<string name="toggle_soft_keyboard">Keyboard</string>
<string name="reset_terminal">Reset</string>
<string name="style_terminal">Style</string>
<string name="share_transcript_title">Terminal transcript</string>
<string name="help">Help</string>
<string name="toggle_keep_screen_on">Keep screen on</string>
<string name="bootstrap_installer_body">Installing…</string>
<string name="bootstrap_error_title">Unable to install</string>
<string name="bootstrap_error_body">Termux was unable to install the bootstrap packages.\n\nCheck your network connection and try again.</string>
<!DOCTYPE resources [
<!ENTITY TERMUX_PACKAGE_NAME "com.termux">
<!ENTITY TERMUX_APP_NAME "Termux">
<!ENTITY TERMUX_API_APP_NAME "Termux:API">
<!ENTITY TERMUX_BOOT_APP_NAME "Termux:Boot">
<!ENTITY TERMUX_FLOAT_APP_NAME "Termux:Float">
<!ENTITY TERMUX_STYLING_APP_NAME "Termux:Styling">
<!ENTITY TERMUX_TASKER_APP_NAME "Termux:Tasker">
<!ENTITY TERMUX_WIDGET_APP_NAME "Termux:Widget">
<!ENTITY TERMUX_PROPERTIES_PRIMARY_PATH_SHORT "~/.termux/termux.properties">
]>
<resources>
<string name="application_name">&TERMUX_APP_NAME;</string>
<string name="shared_user_label">&TERMUX_APP_NAME; user</string>
<!-- Termux RUN_COMMAND permission -->
<string name="permission_run_command_label">Run commands in &TERMUX_APP_NAME; environment</string>
<string name="permission_run_command_description">execute arbitrary commands within &TERMUX_APP_NAME;
environment</string>
<!-- Termux Bootstrap Packages Installation -->
<string name="bootstrap_installer_body">Installing bootstrap packages…</string>
<string name="bootstrap_error_title">Unable to install bootstrap</string>
<string name="bootstrap_error_body">&TERMUX_APP_NAME; was unable to install the bootstrap packages.</string>
<string name="bootstrap_error_abort">Abort</string>
<string name="bootstrap_error_try_again">Try again</string>
<string name="bootstrap_error_not_primary_user_message">Termux can only be installed on the primary user account.</string>
<string name="bootstrap_error_not_primary_user_message">&TERMUX_APP_NAME; can only be run as the primary user.\nBootstrap binaries compiled for &TERMUX_APP_NAME; have hardcoded $PREFIX path and cannot be installed under any path other than \"%1$s\".</string>
<string name="max_terminals_reached_title">Max terminals reached</string>
<string name="max_terminals_reached_message">Close down existing ones before creating new.</string>
<string name="reset_toast_notification">Terminal reset.</string>
<string name="select_url">Select URL</string>
<string name="select_url_dialog_title">Click URL to copy or long press to open</string>
<string name="select_all_and_share">Share transcript</string>
<string name="select_url_no_found">No URL found in the terminal.</string>
<string name="select_url_copied_to_clipboard">URL copied to clipboard</string>
<string name="share_transcript_chooser_title">Send text to:</string>
<!-- Terminal Sidebar and Shortcuts -->
<string name="action_new_session">New session</string>
<string name="action_new_session_failsafe">Failsafe</string>
<string name="title_max_terminals_reached">Max terminals reached</string>
<string name="msg_max_terminals_reached">Close down existing ones before creating new.</string>
<string name="kill_process">Kill process (%d)</string>
<string name="confirm_kill_process">Really kill this session?</string>
<string name="title_rename_session">Set session name</string>
<string name="action_rename_session_confirm">Set</string>
<string name="title_create_named_session">New named session</string>
<string name="action_create_named_session_confirm">Create</string>
<string name="session_rename_title">Set session name</string>
<string name="session_rename_positive_button">Set</string>
<string name="session_new_named_title">New named session</string>
<string name="session_new_named_positive_button">Create</string>
<string name="action_toggle_soft_keyboard">Keyboard</string>
<string name="styling_not_installed">The Termux:Style add-on is not installed.</string>
<string name="styling_install">Install</string>
<string name="msg_enabling_terminal_toolbar">Enabling Terminal Toolbar</string>
<string name="msg_disabling_terminal_toolbar">Disabling Terminal Toolbar</string>
<!-- Terminal Popup -->
<string name="action_select_url">Select URL</string>
<string name="title_select_url_dialog">Click URL to copy or long press to open</string>
<string name="title_select_url_none_found">No URL found in the terminal.</string>
<string name="msg_select_url_copied_to_clipboard">URL copied to clipboard</string>
<string name="action_share_transcript">Share transcript</string>
<string name="title_share_transcript">Terminal transcript</string>
<string name="title_share_transcript_with">Send transcript to:</string>
<string name="action_autofill_password">Autofill password</string>
<string name="action_reset_terminal">Reset</string>
<string name="msg_terminal_reset">Terminal reset</string>
<string name="action_kill_process">Kill process (%d)</string>
<string name="title_confirm_kill_process">Really kill this session?</string>
<string name="action_style_terminal">Style</string>
<string name="action_toggle_keep_screen_on">Keep screen on</string>
<string name="action_open_help">Help</string>
<string name="action_open_settings">Settings</string>
<string name="action_report_issue">Report Issue</string>
<string name="msg_generating_report">Generating Report</string>
<string name="error_styling_not_installed">The &TERMUX_STYLING_APP_NAME; Plugin App is not installed.</string>
<string name="action_styling_install">Install</string>
<!-- Termux Notifications -->
<string name="notification_action_exit">Exit</string>
<string name="notification_action_wake_lock">Acquire wakelock</string>
<string name="notification_action_wake_unlock">Release wakelock</string>
<string name="file_received_title">Save file in ~/downloads/</string>
<string name="file_received_edit_button">Edit</string>
<string name="file_received_open_folder_button">Open folder</string>
<!-- 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 -->
<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_allow_external_apps_ungranted">RunCommandService require `allow-external-apps` property to be set to `true` in `&TERMUX_PROPERTIES_PRIMARY_PATH_SHORT;` file.</string>
<string name="error_run_command_service_api_help">Visit %1$s for more info on RUN_COMMAND Intent usage.</string>
<!-- Termux Execution Commands -->
<string name="msg_executable_absolute_path">Executable Absolute Path: \"%1$s\"</string>
<string name="msg_working_directory_absolute_path">Working Directory Absolute Path: \"%1$s\"</string>
<!-- Termux File Receiver -->
<string name="title_file_received">Save file in ~/downloads/</string>
<string name="action_file_received_edit">Edit</string>
<string name="action_file_received_open_directory">Open directory</string>
<!-- Termux Settings -->
<string name="title_activity_termux_settings">&TERMUX_APP_NAME; Settings</string>
<!-- Termux App Preferences -->
<string name="termux_preferences_title">&TERMUX_APP_NAME;</string>
<string name="termux_preferences_summary">Preferences for &TERMUX_APP_NAME; app</string>
<!-- Debugging Preferences -->
<string name="termux_debugging_preferences_title">Debugging</string>
<string name="termux_debugging_preferences_summary">Preferences for debugging</string>
<!-- Logging Category -->
<string name="termux_logging_header">Logging</string>
<!-- Log Level -->
<string name="termux_log_level_title">Log Level</string>
<!-- Terminal View Key Logging -->
<string name="termux_terminal_view_key_logging_enabled_title">Terminal View Key Logging</string>
<string name="termux_terminal_view_key_logging_enabled_off">Logs will not have entries for terminal view keys. (Default)</string>
<string name="termux_terminal_view_key_logging_enabled_on">Logcat logs will have entries for terminal view keys. These are very verbose and should be disabled under normal circumstances or will cause performance issues.</string>
<!-- Plugin Error Notifications -->
<string name="termux_plugin_error_notifications_enabled_title">Plugin Error Notifications</string>
<string name="termux_plugin_error_notifications_enabled_off">Disable flashes and notifications for plugin errors.</string>
<string name="termux_plugin_error_notifications_enabled_on">Show flashes and notifications for plugin errors. (Default)</string>
<!-- Crash Report Notifications -->
<string name="termux_crash_report_notifications_enabled_title">Crash Report Notifications</string>
<string name="termux_crash_report_notifications_enabled_off">Disable notifications for crash reports.</string>
<string name="termux_crash_report_notifications_enabled_on">Show notifications for crash reports. (Default)</string>
<!-- Terminal IO Preferences -->
<string name="termux_terminal_io_preferences_title">Terminal I/O</string>
<string name="termux_terminal_io_preferences_summary">Preferences for terminal I/O</string>
<!-- Keyboard Category -->
<string name="termux_keyboard_header">Keyboard</string>
<!-- Soft Keyboard -->
<string name="termux_soft_keyboard_enabled_title">Soft Keyboard Enabled</string>
<string name="termux_soft_keyboard_enabled_off">Soft keyboard will be disabled.</string>
<string name="termux_soft_keyboard_enabled_on">Soft keyboard will be enabled. (Default)</string>
<!-- Soft Keyboard Only If No Hardware-->
<string name="termux_soft_keyboard_enabled_only_if_no_hardware_title">Soft Keyboard Only If No Hardware</string>
<string name="termux_soft_keyboard_enabled_only_if_no_hardware_off">Soft keyboard will be enabled even if hardware keyboard is connected. (Default)</string>
<string name="termux_soft_keyboard_enabled_only_if_no_hardware_on">Soft keyboard will be enabled only if no hardware keyboard is connected.</string>
<!-- Termux Tasker App Preferences -->
<string name="termux_tasker_preferences_title">&TERMUX_TASKER_APP_NAME;</string>
<string name="termux_tasker_preferences_summary">Preferences for &TERMUX_TASKER_APP_NAME; app</string>
<!-- About Preference -->
<string name="about_preference_title">About</string>
<!-- Donate Preference -->
<string name="donate_preference_title">Donate</string>
</resources>

View File

@@ -1,8 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<!-- See https://developer.android.com/training/material/theme.html for how to customize the Material theme. -->
<!-- NOTE: Cannot use "Light." since it hides the terminal scrollbar on the default black background. -->
<style name="Theme.Termux" parent="@android:style/Theme.Material.Light.NoActionBar">
<item name="android:statusBarColor">#000000</item>
<item name="android:colorPrimary">#FF000000</item>
@@ -23,8 +21,31 @@
<item name="android:windowAllowEnterTransitionOverlap">true</item>
</style>
<style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert">
<!-- Seen in buttons on alert dialog: -->
<!-- See https://developer.android.com/training/material/theme.html for how to customize the Material theme. -->
<!-- NOTE: Cannot use "Light." since it hides the terminal scrollbar on the default black background. -->
<style name="Theme.Termux.Black" parent="@android:style/Theme.Material.NoActionBar">
<item name="android:statusBarColor">#000000</item>
<item name="android:colorPrimary">#FF000000</item>
<item name="android:windowBackground">@android:color/black</item>
<!-- Seen in buttons on left drawer: -->
<item name="android:colorAccent">#FDFDFD</item>
<!-- Avoid action mode toolbar pushing down terminal content when
selecting text on pre-6.0 (non-floating toolbar). -->
<item name="android:windowActionModeOverlay">true</item>
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowTranslucentNavigation">true</item>
<!-- https://developer.android.com/training/tv/start/start.html#transition-color -->
<item name="android:windowAllowReturnTransitionOverlap">true</item>
<item name="android:windowAllowEnterTransitionOverlap">true</item>
</style>
<style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert">
<!-- Seen in buttons on alert dialog: -->
<item name="android:colorAccent">#212121</item>
</style>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<!-- See https://developer.android.com/training/backup/autosyncapi.html -->
<include domain="file" path="home/backup" />
</full-backup-content>

View File

@@ -0,0 +1,28 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference
app:key="termux"
app:title="@string/termux_preferences_title"
app:summary="@string/termux_preferences_summary"
app:fragment="com.termux.app.fragments.settings.TermuxPreferencesFragment"/>
<Preference
app:key="termux_tasker"
app:title="@string/termux_tasker_preferences_title"
app:summary="@string/termux_tasker_preferences_summary"
app:isPreferenceVisible="false"
app:fragment="com.termux.app.fragments.settings.TermuxTaskerPreferencesFragment"/>
<Preference
app:key="about"
app:title="@string/about_preference_title"
app:persistent="false"/>
<!-- app:layout="@layout/preference_markdown_text" -->
<Preference
app:key="donate"
app:title="@string/donate_preference_title"
app:persistent="false"
app:isPreferenceVisible="false"/>
</PreferenceScreen>

View File

@@ -1,30 +1,54 @@
<shortcuts xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
xmlns:android="http://schemas.android.com/apk/res/android">
<!--
For shortcut.xml:
If applicationId in build.gradle is changed from "com.termux", then targetPackage will
need to be manually patched since ${applicationId} variable or resource string does not work.
If package name in AndroidManifest is changed from "com.termux", then targetClass will
need to be manually patched since dot (.) prefix does not work to automatically prefix the
package name.
-->
<shortcut
android:shortcutId="new_session"
android:enabled="true"
android:icon="@drawable/ic_new_session"
android:shortcutShortLabel="@string/new_session"
android:shortcutShortLabel="@string/action_new_session"
tools:targetApi="n_mr1">
<intent
android:action="android.intent.action.RUN"
android:targetPackage="com.termux"
android:targetClass="com.termux.app.TermuxActivity"/>
android:targetClass="com.termux.app.TermuxActivity"
android:name="android.shortcut.conversation"/>
</shortcut>
<shortcut
android:shortcutId="new_failsafe_session"
android:enabled="true"
android:icon="@drawable/ic_new_session"
android:shortcutShortLabel="@string/new_session_failsafe"
android:shortcutShortLabel="@string/action_new_session_failsafe"
tools:targetApi="n_mr1">
<intent
android:action="android.intent.action.RUN"
android:targetPackage="com.termux"
android:targetClass="com.termux.app.TermuxActivity">
<extra android:name="com.termux.app.failsafe_session" android:value="true" />
android:targetClass="com.termux.app.TermuxActivity"
android:name="android.shortcut.conversation">
<extra android:name="com.termux.app.failsafe_session" android:value="true"/>
</intent>
</shortcut>
<shortcut
android:shortcutId="settings"
android:enabled="true"
android:icon="@drawable/ic_settings"
android:shortcutShortLabel="@string/action_open_settings"
tools:targetApi="n_mr1">
<intent
android:action="android.intent.action.VIEW"
android:targetPackage="com.termux"
android:targetClass="com.termux.app.activities.SettingsActivity"
android:name="android.shortcut.conversation"/>
</shortcut>
</shortcuts>

View File

@@ -0,0 +1,33 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:key="logging"
app:title="@string/termux_logging_header">
<ListPreference
app:defaultValue="1"
app:key="log_level"
app:title="@string/termux_log_level_title"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
app:key="terminal_view_key_logging_enabled"
app:summaryOff="@string/termux_terminal_view_key_logging_enabled_off"
app:summaryOn="@string/termux_terminal_view_key_logging_enabled_on"
app:title="@string/termux_terminal_view_key_logging_enabled_title" />
<SwitchPreferenceCompat
app:key="plugin_error_notifications_enabled"
app:summaryOff="@string/termux_plugin_error_notifications_enabled_off"
app:summaryOn="@string/termux_plugin_error_notifications_enabled_on"
app:title="@string/termux_plugin_error_notifications_enabled_title" />
<SwitchPreferenceCompat
app:key="crash_report_notifications_enabled"
app:summaryOff="@string/termux_crash_report_notifications_enabled_off"
app:summaryOn="@string/termux_crash_report_notifications_enabled_on"
app:title="@string/termux_crash_report_notifications_enabled_title" />
</PreferenceCategory>
</PreferenceScreen>

View File

@@ -0,0 +1,13 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference
app:title="@string/termux_debugging_preferences_title"
app:summary="@string/termux_debugging_preferences_summary"
app:fragment="com.termux.app.fragments.settings.termux.DebuggingPreferencesFragment"/>
<Preference
app:title="@string/termux_terminal_io_preferences_title"
app:summary="@string/termux_terminal_io_preferences_summary"
app:fragment="com.termux.app.fragments.settings.termux.TerminalIOPreferencesFragment"/>
</PreferenceScreen>

View File

@@ -0,0 +1,15 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:key="logging"
app:title="@string/termux_logging_header">
<ListPreference
app:defaultValue="1"
app:key="log_level"
app:title="@string/termux_log_level_title"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
</PreferenceScreen>

View File

@@ -0,0 +1,8 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference
app:title="@string/termux_debugging_preferences_title"
app:summary="@string/termux_debugging_preferences_summary"
app:fragment="com.termux.app.fragments.settings.termux_tasker.DebuggingPreferencesFragment"/>
</PreferenceScreen>

View File

@@ -0,0 +1,21 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:key="keyboard"
app:title="@string/termux_keyboard_header">
<SwitchPreferenceCompat
app:key="soft_keyboard_enabled"
app:summaryOff="@string/termux_soft_keyboard_enabled_off"
app:summaryOn="@string/termux_soft_keyboard_enabled_on"
app:title="@string/termux_soft_keyboard_enabled_title" />
<SwitchPreferenceCompat
app:key="soft_keyboard_enabled_only_if_no_hardware"
app:summaryOff="@string/termux_soft_keyboard_enabled_only_if_no_hardware_off"
app:summaryOn="@string/termux_soft_keyboard_enabled_only_if_no_hardware_on"
app:title="@string/termux_soft_keyboard_enabled_only_if_no_hardware_title" />
</PreferenceCategory>
</PreferenceScreen>

View File

@@ -1,25 +1,32 @@
package com.termux.app;
import junit.framework.TestCase;
import com.termux.shared.data.UrlUtils;
import org.junit.Assert;
import org.junit.Test;
import java.util.Collections;
import java.util.LinkedHashSet;
public class TermuxActivityTest extends TestCase {
public class TermuxActivityTest {
private void assertUrlsAre(String text, String... urls) {
LinkedHashSet<String> expected = new LinkedHashSet<>();
Collections.addAll(expected, urls);
assertEquals(expected, TermuxActivity.extractUrls(text));
}
private void assertUrlsAre(String text, String... urls) {
LinkedHashSet<String> expected = new LinkedHashSet<>();
Collections.addAll(expected, urls);
Assert.assertEquals(expected, UrlUtils.extractUrls(text));
}
public void testExtractUrls() {
assertUrlsAre("hello http://example.com world", "http://example.com");
@Test
public void testExtractUrls() {
assertUrlsAre("hello http://example.com world", "http://example.com");
assertUrlsAre("http://example.com\nhttp://another.com", "http://example.com", "http://another.com");
assertUrlsAre("http://example.com\nhttp://another.com", "http://example.com", "http://another.com");
assertUrlsAre("hello http://example.com world and http://more.example.com with secure https://more.example.com",
"http://example.com", "http://more.example.com", "https://more.example.com");
}
assertUrlsAre("hello http://example.com world and http://more.example.com with secure https://more.example.com",
"http://example.com", "http://more.example.com", "https://more.example.com");
assertUrlsAre("hello https://example.com/#bar https://example.com/foo#bar",
"https://example.com/#bar", "https://example.com/foo#bar");
}
}

View File

@@ -0,0 +1,32 @@
package com.termux.filepicker;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import java.util.ArrayList;
import java.util.List;
@RunWith(RobolectricTestRunner.class)
public class TermuxFileReceiverActivityTest {
@Test
public void testIsSharedTextAnUrl() {
List<String> validUrls = new ArrayList<>();
validUrls.add("http://example.com");
validUrls.add("https://example.com");
validUrls.add("https://example.com/path/parameter=foo");
validUrls.add("magnet:?xt=urn:btih:d540fc48eb12f2833163eed6421d449dd8f1ce1f&dn=Ubuntu+desktop+19.04+%2864bit%29&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=udp%3A%2F%2Ftracker.publicbt.com%3A80&tr=udp%3A%2F%2Ftracker.ccc.de%3A80");
for (String url : validUrls) {
Assert.assertTrue(TermuxFileReceiverActivity.isSharedTextAnUrl(url));
}
List<String> invalidUrls = new ArrayList<>();
invalidUrls.add("a test with example.com");
for (String url : invalidUrls) {
Assert.assertFalse(TermuxFileReceiverActivity.isSharedTextAnUrl(url));
}
}
}

BIN
art/ic_launcher2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

BIN
art/ic_launcher2_round.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,17 +1,18 @@
buildscript {
repositories {
jcenter()
mavenCentral()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.4.2'
classpath 'com.android.tools.build:gradle:4.2.1'
}
}
allprojects {
repositories {
google()
jcenter()
mavenCentral()
maven { url "https://jitpack.io" }
}
}

View File

@@ -0,0 +1,13 @@
Termux is a terminal emulator application enhanced with a large set of command line utilities ported to Android OS. The main goal is to bring a Linux command line experience to users of mobile devices with no rooting or other special setup required.
* Enjoy the Bash and Zsh shells.
* Edit files with nano and vim.
* Access servers over SSH.
* Compile C/C++ code with clang.
* Use the Python console as a pocket calculator.
* Check out projects with Git and Subversion.
* Run text-based games with frotz.
At first start a small base system is being configured. The GNU Bash, Coreutils, Findutils and other core utilities are available out-of-box. Additionally, we provide more than 1000 other packages installable by using the 'pkg' utility which currently is a frontend for the 'apt' package manager. All provided software has been patched and compiled with Android NDK to provide max compatibility with Android OS.
To learn more about application usage tips and tricks, long-press anywhere on the terminal and select the Help menu option to access Termux Wiki. This resource is also accessible directly in a web browser: https://wiki.termux.com/wiki/Main_Page.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Some files were not shown because too many files have changed in this diff Show More