Compare commits

...

180 Commits

Author SHA1 Message Date
agnostic-apollo
6e2689f552 Changed: Bump to v0.118.0 2022-01-08 03:32:35 +05:00
agnostic-apollo
32dcea72b5 Fixed: Fix bootstrap checksum check if it contained leading zeros 2022-01-08 03:30:15 +05:00
agnostic-apollo
c8480f96b9 Docs: Update README.md 2022-01-08 00:28:52 +05:00
agnostic-apollo
d37cd40528 Changed: Bump bootstrap to v2022.01.07-r1 2022-01-08 00:24:48 +05:00
agnostic-apollo
97af79431f Fixed: Fix bootstrap checksum check if it contained leading zeros 2022-01-08 00:23:40 +05:00
agnostic-apollo
bc637ad840 Changed: Bump dependency versions 2022-01-07 23:09:23 +05:00
agnostic-apollo
177fb04869 Changed: Move to semantic versioning for app and library versions and add commit hash and github to APK file names
The `versionName` will now follow semantic version `2.0.0` spec in the format `major.minor.patch(-prerelease)(+buildmetadata)`. This will make versioning the prerelease and github debug builds versions easier and follow a spec. The @termux devs should make sure that when bumping `versionName` in `build.gradle` files and when creating a tag for new releases on github that they include the patch number as well, like `v0.1.0` instead of just `v0.1`. The `build.gradle` files and `attach_debug_apks_to_release` workflow will now validate the version as well and the build/attachment will fail if `versionName` does not follow the spec. https://semver.org/spec/v2.0.0.html

APKs released on github for debug build workflows and releases are now referred as `Github` releases as per 7b10a35f and 94e01d68, so APK filenames have been modified to include `github` in the filename. The APKs are still debuggable, so that tag remains too.

For github workflows the apk filename format will be `termux-app_<current_version>+<last_commit_hash>-github-debug_<arch>.apk`, like `termux-app_v0.1.0+xxxxxxxx-github-debug_arm64-v8a.apk` and for github releases it will be `termux-app_<release_version>+github-debug_<arch>.apk`, like `termux-app_v0.1+github-debug_arm64-v8a.apk`. The `last_commit_hash` will be the first `8` characters of the commit hash. The `<last_commit_hash>-github-debug` will act as `buildmetadata` and will not affect versioning precedence.

For github workflows triggered by `push` and `pull_request` triggers, `<current_version>+<last_commit_hash>` will be used as new `versionName`, like `v0.1.0+xxxxxxxx`. This will make tracking which build a user is using easier and help in resolving issues as well.

Note that users using github releases and termux devs using `$TERMUX_VERSION` environment variables in scripts should take commit hash into consideration and possibly use something like `dpkg --compare-versions "$TERMUX_VERSION" ge 0.1` where appropriate instead of mathematical comparisons.

The `app/build.gradle` now also supports following `TERMUX_` scoped environmental variables and `RELEASE_TAG` variable will not be used anymore since it may conflict with possibly other variables used by users. They will also allow enabling split APKs for both debug and release builds.

- `TERMUX_APP_VERSION_NAME` will be used as `versionName` if its set.
- `TERMUX_APK_VERSION_TAG` will be used as `termux-app_<TERMUX_APK_VERSION_TAG>_<arch>.apk` if its set. The `_<arch>` will only exist for split APKs.
- `TERMUX_SPLIT_APKS_FOR_DEBUG_BUILDS` will define whether split APKs should be enabled for debug builds. Default value is `1`.
- `TERMUX_SPLIT_APKS_FOR_RELEASE_BUILDS` will define whether split APKs should be enabled for release builds. Default value is `0` since F-Droid does not support split APKs, check #1904.

So based on above, if in future github releases are to be converted to `release` builds instead of `debug` builds, something like following can be done and even a workflow can be created for it. Users can also build split APKs release builds for themselves if they want.

```
export TERMUX_SPLIT_APKS_FOR_RELEASE_BUILDS=1
./gradlew assembleRelease -Pandroid.injected.signing.store.file="$(pwd)/app/dev_keystore.jks" -Pandroid.injected.signing.store.password=xrj45yWGLbsO7W0v -Pandroid.injected.signing.key.alias=alias -Pandroid.injected.signing.key.password=xrj45yWGLbsO7W0v
```

The APK will be found at `./app/build/outputs/apk/release/termux-app_<version>_<arch>.apk`

The `TERMUX_SPLIT_APKS_FOR_DEBUG_BUILDS` can be set to `0` to disable building split APKs which may be helpful for users building termux on device considering they will extra space and build time. Instructions for building are at https://github.com/termux/termux-packages/pull/7227#issuecomment-893022283.

```
export TERMUX_SPLIT_APKS_FOR_DEBUG_BUILDS=0
./gradlew assembleDebug
```

The APK will be found at `./app/build/outputs/apk/debug/termux-app_debug_universal.apk`

Note that F-Droid uses algorithm at https://gitlab.com/fdroid/fdroidserver/-/blob/2.1a0/fdroidserver/build.py#L746 to automatically detect built APKs, so ensure any modifications to location or file name are compliant. Current updates should be.

Auto updates are detected by checkupdates bot at https://gitlab.com/fdroid/fdroidserver/-/blob/master/fdroidserver/checkupdates.py
2022-01-07 22:31:06 +05:00
Henrik Grimler
3134bf8655 bootstrap archives: update to 2022.01.02-r1 2022-01-05 10:15:58 +01:00
Henrik Grimler
fcc0d36258 Fixed: Fix copy&paste error in areHardwareKeyboardShortcutsDisabled
Fixes 829cc39868 ("Allow users to disable hardware keyboard
shortcuts").

Reported-by: @amogusissofunnyhahalmaogenzhumorbelike
2021-12-30 22:01:44 +01:00
Leonid Pliushch
077a149c9c bootstrap archives: update to 2021.12.02-r1 2021-12-02 11:52:11 +02:00
Henrik Grimler
6c24e6ac3b termux-shared: add android.permission.VIBRATE to manifest
./gradlew lint complains about vibrations being used in
termux-shared/src/main/java/com/termux/shared/terminal/io/BellHandler.java
without the permission being declared.
2021-11-20 22:03:09 +01:00
YAKSH BARIYA
edf3b622e4 chore: Fix Discord server ID in shields.io badge
Based on dd59986a8c
2021-11-05 11:36:14 +05:30
agnostic-apollo
af16e79bf8 Merge pull request #2146 from trygveaa/click-on-url
Added: Allow users to directly open URL links in terminal transcript when clicked or tapped

The user can add `terminal-onclick-url-open=true` entry to `termux.properties` file to enable opening of URL links in terminal transcript when clicked or tapped. The default value is `false`. Running `termux-reload-settings` command will also update the behaviour instantaneously if changed.

Implemented in #2146
2021-10-13 22:36:52 +05:00
Leonid Pliushch
da6174e4c4 bootstrap archives: update to 2021.10.03-r1 2021-10-07 20:27:03 +03:00
agnostic-apollo
dcedf39434 Changed: Only allow ContentProvider access if allow-external-apps is set to true 2021-09-24 00:47:38 +05:00
agnostic-apollo
e302a14cd0 Fixed: Do not allow external apps to modify termux properties files with ContentProvider 2021-09-24 00:29:06 +05:00
agnostic-apollo
f3ffc36bfd Added: Add TermuxFileUtils.getExpandedTermuxPaths() and TermuxFileUtils.getUnExpandedTermuxPaths() 2021-09-23 21:40:37 +05:00
agnostic-apollo
1f0f80b0c9 Added: Add FileUtils.isPathInDirPaths() 2021-09-23 21:39:51 +05:00
agnostic-apollo
5e2bec0f4c Added: Add constants for launcher activities of termux plugins 2021-09-23 16:51:08 +05:00
agnostic-apollo
075a080f00 Added: Add functions to PackageUtils to check/modify app Component states
These can be used by Termux app and its plugin to disable launcher icons/activities if they are enabled at install time
2021-09-23 16:46:59 +05:00
agnostic-apollo
0bf4b1eca4 Added: Add Theme.MaterialComponents.DayNight.TermuxPrimaryActivity theme can be used by activities for day and night mode 2021-09-23 16:44:38 +05:00
agnostic-apollo
4f66786b98 Changed: Store termux-widget token synchronously to the SharedPreferences file on creation
Attempt to solve termux/termux-widget#16
2021-09-23 04:58:14 +05:00
agnostic-apollo
fefbf2ec03 Update LICENSE.md 2021-09-22 14:19:19 +05:00
Trygve Aaberge
54bb83de41 Fix calculation of row number for selection and URL clicking
When calculating the row that is clicked, for mouse tracking
mFontLineSpacingAndAscent was taken into account, but for selection and
URL clicking it wasn't. This adds a common function for calculating the
column and row which does take it into account and use that for all
three.

I'm not quite sure why it's necessary to subtract
mFontLineSpacingAndAscent, but with this calculation the click location
matches the line that is acted on for me with both touch and mouse and
on different font sizes.

It also removes the offset for finger the selection/url used because I
don't think it's common for apps on Android to have such an offset, and
because the mouse tracking did not use such an offset.
2021-09-19 18:10:46 +02:00
Trygve Aaberge
1a5a66d0ee Support clicking directly on a URL to open it
This allows you to click/press directly on a URL in the terminal view to
open it. It takes priority over opening the keyboard, so if you click on
a URL it is opened, and if you click anywhere else the keyboard opens
like before.

Currently, if the application in the terminal is tracking the mouse and
you click on a URL, both actions happen. The mouse event is sent to the
application, and the URL is also opened.

To enable support for this, you have to set
`terminal-onclick-url-open=true` in `termux.properties`.
2021-09-15 01:58:30 +02:00
agnostic-apollo
865f29d49a Added: Request android.permission.PACKAGE_USAGE_STATS permission
The permission can be granted from `Android Settings` -> `System` -> `Usage Access`.

Closes #2269
2021-09-12 15:50:52 +05:00
agnostic-apollo
22811167ac Update README.md 2021-09-12 10:06:01 +05:00
agnostic-apollo
819571a03a Update README.md 2021-09-11 19:07:59 +05:00
agnostic-apollo
c3280a94f0 Added: Add TextIOActivity and TextIOInfo
The `TextIOActivity` can be used to edit or view text based on various config options defined by `TextIOInfo`
and supports `monospace` font and horizontal scrolling for editing scripts, etc.

Current max text limit is `95KB`, which can be increased in future.
2021-09-11 15:15:51 +05:00
agnostic-apollo
5f3b1ccf90 Added: Add getDefaultIfUnset() to DataUtils and update comment 2021-09-11 15:15:51 +05:00
agnostic-apollo
0b47b20a9c Changed: Minor refactor and comment updates of ReportActivity and ReportInfo 2021-09-11 13:48:30 +05:00
agnostic-apollo
783a840e3a Added: Add MIN_VALUE_EXTRA_SESSION_ACTION and MAX_VALUE_EXTRA_SESSION_ACTION to TermuxConstants 2021-09-09 08:19:51 +05:00
agnostic-apollo
c19e01fc1b Changed!: Do not wait for the user to press enter for failed terminal session commands if plugin expects the result back 2021-09-09 07:25:43 +05:00
agnostic-apollo
9ffcd21ce1 Update README.md 2021-09-09 04:34:24 +05:00
agnostic-apollo
94e01d68d6 Update README.md 2021-09-08 17:53:23 +05:00
agnostic-apollo
0cf3cef7de Added: Add TERMUX_API_VERSION to termux shell environment
This can be used to check if `Termux:API` is installed and enabled for cases where users try to run `termux-api` commands and it hangs. The check can be added to start of each `termux-api` script during build time by replacing a placeholder with `sed`.

```
if dpkg --compare-versions "$TERMUX_VERSION" ge 0.118 && [ -z "$TERMUX_API_VERSION" ]; then
echo "The Termux:API app is not installed or enabled which is required by termux-api commands to work." 1>&2
exit 1
fi

current_user="$(id -un)"
termux_user="$(stat -c "%U" "/data/data/com.termux/files/usr")"
if [ "$current_user" != "$termux_user" ]; then
echo "The termux-api commands must be run as the termux user \"$termux_user\" instead of as \"$current_user\"." 1>&2
echo "Trying to run with \"su $termux_user -c termux-api-command\" will fail as well." 1>&2
exit 1
fi
```
2021-09-08 11:24:11 +05:00
agnostic-apollo
7b10a35f24 Changed!: Change TERMUX_IS_DEBUG_BUILD env variable name to TERMUX_IS_DEBUGGABLE_BUILD and change GITHUB_DEBUG_BUILD release type to just GITHUB
This is being done since github release artifacts may be converted to non-debuggable if felt appropriate in future or at least is a more appropriate name. Signing keys can stay same as per commit/push builds. Currently, no changes are planned, just future proofing. The `TERMUX_IS_DEBUGGABLE_BUILD` env variable could be used to differentiate if needed.

Will also check if Termux app is installed and not disabled and will calculate APK signature only when needed since its a slightly expensive operation.

This commit breaks da07826a.
2021-09-08 11:05:29 +05:00
agnostic-apollo
e36c5294db Changed: Only show system chooser if ActivityNotFoundException is thrown when opening url 2021-09-08 08:46:29 +05:00
agnostic-apollo
dd952a90ad Changed: Show system chooser if failed to find activity to handle url 2021-09-06 04:40:49 +05:00
agnostic-apollo
da07826a0c Added: Add TERMUX_IS_DEBUG_BUILD, TERMUX_APK_RELEASE and TERMUX_APP_PID to termux shell environment
The `TERMUX_IS_DEBUG_BUILD` env variable will be set to `1` if termux APK is a debuggable APK and `0` otherwise. Note that the `dev_keystore.jks` shipped with termux app and plugin source code can also be used to create a release APK even though its mainly used for Github Debug Builds, in which case value will be `0`.

The `TERMUX_APK_RELEASE` will be set to `GITHUB_DEBUG_BUILD`, `F_DROID` or `GOOGLE_PLAY_STORE` depending on release type. It will be set to `UNKNOWN` if signed with a custom key.

The `TERMUX_APP_PID` will be set to the process of the main app process of the termux app package (`com.termux`), assuming its running when shell is started, like for `termux-float`. This variable is included since `pidof com.termux` does not return anything for release builds. It does work for debug builds and over adb/root. However, you still won't be able to get additional process info with `ps`, like that of threads, even with the pid and will need to use adb/root. However, `kill $TERMUX_APP_PID` will work from `termux-app` and `termux-float`.

These variables can be used by termux devs and users for custom logic in future depending on release type.
2021-09-06 04:14:57 +05:00
agnostic-apollo
1259a212aa Changed: Make allowed custom log level added in 60f37bde to be more restrictive 2021-09-06 01:24:22 +05:00
agnostic-apollo
ac32fbc53d Added: Add SharedPreferences KEY_LAST_PENDING_INTENT_REQUEST_CODE for termux-tasker 2021-09-05 14:52:53 +05:00
agnostic-apollo
f00738fe3a Changed: Make sure full path is included in FileUtilsErrnos
Previously, `FileUtilsErrno` had some errors that didn't include the full path passed to the `FileUtils` functions and caller had to manually append the path to the error. This was done due to `termux-tasker` plugin config activity was using these errors in the executable and working directory text fields and we had to keep the error short as possible to reduce clutter. Now by default, the path will be included so that its not missing for other cases and the `FileUtils.getShortFileUtilsError()` function is provided to get a shorter version from the original error if its possible to do so if caller like `termux-tasker` requires it.
2021-09-05 10:09:18 +05:00
agnostic-apollo
5c72c3ca1b Added: Allow users to disable auto capitalization of extra keys text
The user can add `extra-keys-text-all-cap=false` entry to `termux.properties` file to disable auto capitalization of extra keys text for both normal and popup buttons. The default value is `true`. Running `termux-reload-settings` command will also update the behaviour instantaneously if changed.
2021-09-05 04:24:00 +05:00
agnostic-apollo
b62645cd03 Fixed: Fix typos and refactor 2021-09-05 03:37:07 +05:00
agnostic-apollo
23b707a819 Changed: Disable shrinkResources for testing reproducible builds 2021-09-04 08:34:32 +05:00
agnostic-apollo
4953b1269c Added: Add log level setting in Termux Settings for termux-widget 2021-09-04 08:33:30 +05:00
agnostic-apollo
d5ffb116b8 Added: Add constants and functions for termux-widget in TermuxConstants and TermuxPreferenceConstants 2021-09-04 08:08:51 +05:00
agnostic-apollo
e5c0548942 Added: Add isTermuxAppInstalled() and isTermuxAppAccessible() functions to TermuxUtils
The `TermuxUtils.isTermuxAppInstalled()` function can be used by external apps to check if termux app is installed and enabled.

The `TermuxUtils.isTermuxAppAccessible()` function can be used by termux plugin apps to check if termux app is installed, enabled, accessible as per `sharedUserId` and `TERMUX_PREFIX_DIR_PATH` is accessible and has read, write and execute permission.
2021-09-04 08:06:54 +05:00
agnostic-apollo
4e5f2c7e01 Changed/Fixed: Ensure bootstrap installation creates prefix and prefix staging directory before extraction
We manually create the parent directories first so that bootstrap failures are detected early on instead of some sub directory during extraction.

Also fixed issue where `TermuxFileUtils.isTermuxFilesDirectoryAccessible()` would not check if a directory file actually existed at TERMUX_FILES_DIR_PATH and may set permissions for a non-directory file at the path. The `TermuxInstaller` was testing if `TERMUX_PREFIX_DIR_PATH` existed later on so check wasn't necessary but function may be called from elsewhere too.

Also removed legacy `PREFIX_FILE*` and `STAGING_PREFIX_FILE*` local constants and use the ones provided by `TermuxConstants` directly.
2021-09-04 08:06:25 +05:00
agnostic-apollo
3373a1f41c Changed: Split long resource string on multiple lines 2021-09-04 07:25:21 +05:00
agnostic-apollo
52c1ee520f Added/Fixed: Add support to consider empty String values as null for SharedPreferences 2021-09-04 07:25:21 +05:00
agnostic-apollo
197979fdcc Fixed: Ensure custom log level doesn't log if its off or null 2021-09-04 05:28:39 +05:00
agnostic-apollo
bc779d2ffb Added: Add support for ~/.termux/termux.float.properties 2021-09-02 12:45:28 +05:00
agnostic-apollo
9f1203f049 Changed: Use multi-process SharedPrefernces for log level of plugin apps
Since termux-app runs in a separate process from other apps, if a user sets log level in termux settings, then it would require exiting the `termux-app` completely since android caches `SharedPrefernces` in memory and only writes to the file on app exit. Now updated value will be instantly written to the file so that plugins can directly read at startup. If plugins are already running, they would need to be restarted since usually log levels are loaded at startup.
2021-09-02 06:40:02 +05:00
agnostic-apollo
d55c1001c8 Added: Add termux-float log level settings in termux app settings 2021-09-02 06:21:16 +05:00
agnostic-apollo
36557b2166 Added: Add more SharedPrefernces for termux-float and use multi-process for log level 2021-09-02 06:20:39 +05:00
agnostic-apollo
1cf1e612e5 Added: Add constants for termux-float in TermuxConstants 2021-09-02 06:16:28 +05:00
agnostic-apollo
e7d06aebb5 Merge pull request #2237 from agnostic-apollo/extra-keys-conversion-to-agnosticism
Extra keys conversion to agnosticism and disabling hardware keyboard shortcuts and terminal margin customization support
2021-08-28 01:26:53 +05:00
agnostic-apollo
582e56938a Added: Add SharedPrefernces controllers for all current published termux plugin app
Also added log level setting in Termux Settings for Termux:API. Others can be added when logging is implemented in the plugin apps via `Logger` class provided by `termux-shared`.
2021-08-28 00:29:53 +05:00
agnostic-apollo
5a8c4f10ee Fixed|Changed: Fix TermuxFileReceiverActivity incorrect handling of intent extras
- If the `EXTRA_TEXT` value of the intent passed was empty instead of `null`, it was incorrectly assumed that text was passed, even though a valid `EXTRA_STREAM` may have been passed. Now `EXTRA_STREAM` will be checked first.
- Added empty extra and empty/`null` filename checks before trying to create a file with an empty filename and failing.
- Enable logging of intent passed at verbose log level.
- Changed to a better error dialog.

Closes #2247
2021-08-27 05:39:04 +05:00
agnostic-apollo
8387b70f64 Fixed: Fix terminal cursor blinker not stopping when typing a character in non-gboard keyboards 2021-08-26 06:03:23 +05:00
agnostic-apollo
994df1c4af Fixed|Added: Fix extra-keys shift key not uppercasing for all soft keyboards and added docs for keyboard key characters mapping 2021-08-26 06:01:25 +05:00
agnostic-apollo
63504f0adc Added: Allow users to adjust terminal horizontal and vertical margin
The `terminal-margin-horizontal` key can be used to adjust the terminal left/right margin and the `terminal-margin-vertical` can be used to adjust the terminal top/bottom margin. This will also affect drawer. The user can set an integer value between `0` and `100` as `dp` units. The default value is still `3` for horizontal and `0` for vertical margin. So adding an entry like `terminal-margin-horizontal=10` to `termux.properties` file will allow users to set a horizontal margin of `10dp`. After updating the value, either restart termux or run `termux-reload-settings` for changes to take effect.

This was added since for some users text on edges would not be shown on the screen or they had screen protectors/cases that covered screen edges (Of course, that would require fixing every single app and android system UI itself, so kinda stupid to use). Moreover, horizontal margin of like `10dp` may be helpful with peek-and-slide for people having gesture navigation enabled on android `10+` since they won't be to touch at exactly the edge of the screen to trigger peek (#1325).

Closes #2210
2021-08-25 23:18:17 +05:00
agnostic-apollo
829cc39868 Added: Allow users to disable hardware keyboard shortcuts
The user can add `disable-hardware-keyboard-shortcuts=true` entry to `termux.properties` file to disable hardware keyboard shortcuts. The default value is `false`. Running `termux-reload-settings` command will also update the behaviour instantaneously if changed. Note that for `ctrl+alt+p` to work, you need to unset `shortcut.rename-session = ctrl + n`. https://wiki.termux.com/wiki/Terminal_Settings

Closes #1825
2021-08-25 23:18:17 +05:00
agnostic-apollo
16c56a968e Changed|Fixed: Drawer extra-keys button will toggle instead of just opening
Also fixed NullPointerException due to changes in 2a74d43c
2021-08-25 23:18:17 +05:00
agnostic-apollo
b68a398fa8 Changed: Renamed typo TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION to TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION 2021-08-25 02:01:28 +05:00
agnostic-apollo
f97f07df3f Changed: Add selinux context info to termux files info of debug output 2021-08-23 21:51:43 +05:00
agnostic-apollo
c59835ed93 Revert "Changed: Bump compileSdkVersion to 31"
This reverts commit 296ee60d

We do not need to bump to compileSdkVersion 31 currently, since I have decided not to bump `androidx.window` to `1.0.0-alpha10` or higher currently, since it has changed APIs and ViewUtils will break.

https://developer.android.com/jetpack/androidx/releases/window

Moreover, bumping compileSdkVersion to 31 requires openjdk 11 in build environment, which will break Jitpack library build and possibly still F-Droid as well, unless changes are made.

https://gitlab.com/fdroid/fdroiddata/-/issues/2441

https://github.com/jitpack/jitpack.io/issues/4474

https://jitpack.io/docs/BUILDING/#java-version

Also in android studio stub files are loaded when opening class sources since android 12 sources aren't available.
2021-08-23 14:59:02 +05:00
agnostic-apollo
d1478fb6c3 Fixed: Ensure FN extra key is read by the terminal
Can't find info on why it wasn't being read before
2021-08-23 08:56:36 +05:00
agnostic-apollo
9117240961 Added: Add shift key support in extra keys and terminal with SHIFT or SHFT
Closes #1038
2021-08-23 08:51:30 +05:00
agnostic-apollo
fbb91149b5 Fixed: Use default values if extra-keys or extra-keys-style termux.properties values are empty 2021-08-23 08:48:25 +05:00
agnostic-apollo
2a74d43ca5 Added!: Convert extra-keys to agnosticism
The termux `extra-keys` have been moved to `termux-shared` library so that they can be imported and used by other apps for their own needs as long as they comply with GPLv3 license.

Almost everything is customizable and has no dependency on termux specific logic. Check the javadocs of files of `com.termux.shared.terminal.io.extrakeys` package for more info, specially, `ExtraKeysView`, `ExtraKeysInfo`, `ExtraKeyButton`, `TerminalExtraKeys` and  `TermuxTerminalExtraKeys`.

Moreover, you can now long hold on `CTRL`, `ALT`, `SHIFT` and `FN` to lock those control keys. They will not be released when you press another key and will only be released by pressing the respective control key again.

Closes #2049, Closes #1861
2021-08-23 08:48:24 +05:00
Leonid Pliushch
f65f384acf Merge pull request #2228 from termux/cursor-colors
terminal: invert text color under block cursor
2021-08-21 11:39:32 +03:00
agnostic-apollo
f055305790 Changed: Bump gradle to 7.2 2021-08-21 06:01:58 +05:00
agnostic-apollo
296ee60dc8 Changed: Bump compileSdkVersion to 31
Needed since gradle library dependencies have minCompileSdk set to 31
2021-08-21 05:48:00 +05:00
agnostic-apollo
1c01f4df08 Changed: Bump gradle library dependency versions 2021-08-21 05:33:00 +05:00
agnostic-apollo
e889d84dc4 Changed!: Changes introduced to disable/change logging in 60f37bde now also apply to stdin and plugin command results 2021-08-21 05:26:33 +05:00
agnostic-apollo
956e20e53d Fixed: Fix NullPointerException when running bell/vibrate on Samsung devices on android 8 and handled deprecated code
Apparently occurs on only Samsung android 8 devices and there is no fix for vibrator except catching the exception so that app doesn't crash.

https://gitlab.com/juanitobananas/wave-up/-/issues/131
https://github.com/overbound/SonicTimeTwisted/issues/131
https://web.archive.org/web/20201114040257/https://www.badlogicgames.com/forum/viewtopic.php?t=28507

```
java.lang.NullPointerException: Attempt to read from field 'android.os.VibrationEffect com.android.server.VibratorService$Vibration.mEffect' on a null object reference
at android.os.Parcel.readException(Parcel.java:2035)
at android.os.Parcel.readException(Parcel.java:1975)
at android.os.IVibratorService$Stub$Proxy.vibrate(IVibratorService.java:292)
at android.os.SystemVibrator.vibrate(SystemVibrator.java:81)
at android.os.Vibrator.vibrate(Vibrator.java:191)
at android.os.Vibrator.vibrate(Vibrator.java:110)
at android.os.Vibrator.vibrate(Vibrator.java:89)
at com.termux.app.terminal.io.BellHandler$1.run(BellHandler.java:37)
at com.termux.app.terminal.io.BellHandler.doBell(BellHandler.java:55)
at com.termux.app.terminal.TermuxTerminalSessionClient.onBell(TermuxTerminalSessionClient.java:178)
at com.termux.terminal.TerminalSession.onBell(TerminalSession.java:278)
```
2021-08-21 04:22:43 +05:00
agnostic-apollo
10704b1dad Changed: Use extended version of Logger functions for logging execution commands 2021-08-21 03:48:32 +05:00
agnostic-apollo
19f4084099 Added: Add labels for ExecutionCommand for termux internal commands 2021-08-21 03:41:29 +05:00
agnostic-apollo
486faf7fad Fixed: Stdin not being logged for background execution commands 2021-08-21 03:40:31 +05:00
agnostic-apollo
6409019a40 Added: Add warning that hax support is not provided and asking questions will likely result in issue automatically closed or even ban 2021-08-21 02:55:13 +05:00
agnostic-apollo
7047bbefbb Added: Add warning reports with (partial) screenshots of error reports instead of text will likely be automatically closed/deleted 2021-08-21 02:47:13 +05:00
agnostic-apollo
24ea83d6c0 Added: Bootstrap error and report issue (optionally) will contain primary termux files stat info and logcat dump
Users have been reporting issues with bootstrap installation (and `login` file access) failure on email and github but "most" have been useless since they don't follow instructions to debug the issue and report back. The real reason may depend on device. One could be that `/data/data/com.termux` does not exist on the device in which case termux won't work on the device, at least without root. Other reasons could be wrong ownership or selinux context, selinux denials or attempting to install on external sd card (as reported by a user) where likely files dir was different from `/data/data/com.termux/files`.

This commit will save dev and possibly user time and automatically generate the required info to debug such issues. The `ls` command will generate `stat` info for all the major termux directories and files so that existence or ownership issues can be shown. It will also run `logcat` command to take a dump (last `3000` lines) in case other failures are being logged, like selinux denials as per `avc` entries. It will also show if app is installed on external sd card. This info will automatically be shown on bootstrap install failure report.

Moreover, users can generate termux files `stat` info and `logcat` dump manually too with terminal's long hold options menu `More` -> `Report Issue` option and selecting `YES` in the prompt shown to add debug info. This can be helpful for reporting and debugging other issues. If the report generated is too large, then `Save To File` option in context menu (3 dots on top right) of `ReportActivity` can be used and the file viewed/shared instead.

Users must post complete report (optionally without sensitive info) when reporting issues, instead of (partial) screenshots which won't be accepted anymore.

There has been some design changes in android 11 for `/data/data` and `/data/user/0` directory. You can check javadoc for `isTermuxFilesDirectoryAccessible()` function in [`TermuxFileUtils`](termux-shared/src/main/java/com/termux/shared/file/TermuxFileUtils.java) for details.
2021-08-21 02:44:51 +05:00
agnostic-apollo
351934a619 Added|Fixed!: Added support to save reports to files and fixed large reports generating TransactionTooLargeException
If `ReportActivity` was started with a large report, i.e a few hundred `KB`, like for terminal transcript or other command output, the activity start would fail. To solve the issue, if the serialized size of the ReportInfo info object is above `DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES` (`100KB`), it will be saved to a file in a cache directory `/data/data/com.termux/cache/report_activity` as a serialized object and loaded when activity is started. The file will be automatically deleted when activity is destroyed (`Activity.onDetroy()`) or when notification that would have started the activity is deleted (`Notification.deleteIntent`). In case, these two didn't happen, then on `TermuxActivity` startup, a thread will be started to delete files older than `14` days so that unneeded left over files are deleted. If user tries to open plugin error or crash report notifications after 14 days, they will get `ReportInfo` file not found errors, assuming `TermuxActivity` was started to run the cleanup routine.

Now these large reports can't be copied or shared with other apps since that would again result in `TransactionTooLargeException` exceptions and `ShareUtils` automatically truncates the data (now from end) to `100KB` length so that the exception doesn't occur. So now a `Save To File` option has been added in context menu (3 dots on top right) of `ReportActivity` so that large or small reports can be saved to a file if needed. They will be save in root of `/storage/emulated/0` or whatever is the default public external storage directory. The filename would depend on type of report. The storage permissions will be asked if missing. On android `11`, if you get permission denied errors even after granting permission, disable permission and grant it again. To solve privacy issues of report being saved to public storage since it may contain private info, an option for custom path will be added in future. The default directory is public storage instead of termux home since its easily accessible via all file managers or from pc. Instructing amateur users to get files via `SAF` from termux home is not something I wanna take on.

Another issue is that `ReportActivity` itself may not be able to show the entire report since Android may throw `OutOfMemoryError` exceptions if device memory is low. To solve this issue, `ReportActivity` will truncate the report to `1MB` from end that's shown to the user. It will add a header showing that report was truncated. To view the full report, the user will have to use the `Save To File` option and view the file in an external app or on pc that supports opening large files. The `QuickEdit` app on Android has been a reliable one in my experience that supports large files, although it has max row/column limits too at a few hundred thousand, depending on android version.

Despite all this, `OutOfMemoryError` exceptions could still be thrown if you try to view too large a report, like a few MB, since original report + the truncated report is still held in memory by the app and will consume `2-3` times memory when saving. It's fun coding for android, right?

The terminal transcript will not be truncated anymore that's generated via `Report Issue` option in terminal.

The `ShareUtils.copyTextToClipboard()` will truncate data now automatically, apparently all phones don't do it automatically and exception is raised.

The `ShareUtils.saveTextToFile()` has been added that will automatically ask for storage permissions if missing.

The `ReportInfo` now expects a `reportSaveFileLabel` and `reportSaveFilePath` arguments so that `ReportActivity` can use them to know where to save the file if users selects `Save To File` option.

The `ReportActivityBroadcastReceiver` must now be registered in `AndroidManifest.xml` if you are using `ReportActivity` in your app. Check `ReportActivity` javadoc for details. Moreover, an incremental call to `ReportActivity.deleteReportInfoFilesOlderThanXDays()` must also be made.
2021-08-20 23:31:12 +05:00
agnostic-apollo
e7fc60af72 Fixed: New plugin error or crash notifications overriding content of old ones 2021-08-20 22:20:18 +05:00
agnostic-apollo
baacabdfbf Added!: Support for delete intent for Notification.Builder in NotificationUtils 2021-08-20 22:12:24 +05:00
agnostic-apollo
35ea19dd75 Added: Support for reading and writing serialized objects to files and deleting files older than x days in FileUtils 2021-08-20 06:36:01 +05:00
agnostic-apollo
7de0613617 Fixed: Catch exception when requesting permissions, like if request code is negative 2021-08-20 06:19:25 +05:00
agnostic-apollo
5e09a501c9 Added: Support for MessageDialogUtils.showMessage() to receive positive and negative button OnClickListeners 2021-08-20 06:19:25 +05:00
agnostic-apollo
60f37bde8d Changed!: StreamGobbler needs to be passed log level parameter
When `Logger.CURRENT_LOG_LEVEL` set by user is `Logger.LOG_VERBOSE`, then background (not foreground sessions) command output was being logged to logcat, however, if command outputted too much data to logcat, then logcat clients like in Android Studio would crash. Also if a logcat dump is being taken inside termux, then duplicate lines would occur, first one due to of original entry, and second one due to StreamGobbler logging output at verbose level for logcat command.

This would be a concern for plugins as well like `RUN_COMMAND` intent or Termux:Tasker, etc if they ran commands with lot of data and user had set log level to verbose.

For plugins, TermuxService now supports `com.termux.execute.background_custom_log_level` `String` extra for custom log level. Termux:Tasker, etc will have to be updated with support. For `RUN_COMMAND` intent, the `com.termux.RUN_COMMAND_BACKGROUND_CUSTOM_LOG_LEVEL` `String` extra is now provided to set custom log level for only the command output. Check `TermuxConstants`.

So one can pass a custom log level that is `>=` to the log level set it termux settings where (OFF=0, NORMAL=1, DEBUG=2, VERBOSE=3). If you pass `0`, it will completely disable logging. If you pass `1`, logging will only be enabled if log level in termux settings is `NORMAL` or higher. If custom log level is not passed, then old behaviour will remain and log level in termux settings must be `VERBOSE` or higher for logging to be enabled. Note that the log entries will still be logged with priority `Log.VERBOSE` regardless of log level, i.e `logcat` will have `V/`.

The entries logcat component has now changed from `StreamGobbler` to `TermuxCommand`. For output at `stdout`, the entry format is `[<pid>-stdout] ...` and for the output at `stderr`, the entry format is `[<pid>-stderr] ...`. The `<pid>` will be process id as an integer that was started by termux. For example: `V/TermuxCommand: [66666-stdout] ...`.

While doing this I realize that instead of using `am` command to send messages back to tasker, you can use tasker `Logcat Entry` profile event to listen to messages from termux at both `stdout` and `stderr`. This might be faster than `am` command intent systems or at least possibly more convenient in some use cases.

So setup a profile with the `Component` value set to `TermuxCommand` and `Filter` value set to `-E 'TermuxCommand: \[[0-9]+-((stdout)|(stderr))\] message_tag: .*'` and enable the `Grep Filter` toggle so that entry matching is done in native code. Check https://github.com/joaomgcd/TaskerDocumentation/blob/master/en/help/logcat%20info.md for details. Also enable `Enforce Task Order` in profile settings and set collision handling to `Run Both Together` so that if two or more entries are sent quickly, entry task is run for all. Tasker currently (v5.13.16) is not maintaining order of entry tasks despite the setting.

Then you can send an intent from tasker via `Run Shell` action with `root` (since `am` command won't work without it on android >=8) or normally in termux from a script, you should be able to receive the entries as `@lc_text` in entry task of tasker `Logcat Entry` profile. The following just passes two `echo` commands to `bash` as a script via `stdin`. If you don't have root, then you can call a wrapper script with `TermuxCommand` function in `Tasker Function` action that sends another `RUN_COMMAND` intent with termux provide `am` command which will work without root.

```
am startservice --user 0 -n com.termux/com.termux.app.RunCommandService -a com.termux.RUN_COMMAND --es com.termux.RUN_COMMAND_PATH '/data/data/com.termux/files/usr/bin/bash' --es com.termux.RUN_COMMAND_STDIN 'echo "message_tag: Sending message from tasker to termux"' --ez com.termux.RUN_COMMAND_BACKGROUND true --es com.termux.RUN_COMMAND_BACKGROUND_CUSTOM_LOG_LEVEL '1'
```
2021-08-20 06:19:25 +05:00
agnostic-apollo
fabcc4fa35 Fixed: RunCommandService notification was not being cleared if an error was raised 2021-08-20 06:19:25 +05:00
agnostic-apollo
98edf1fbc7 Changed: Use millisecond timestamps for reports 2021-08-20 06:19:25 +05:00
agnostic-apollo
8ee0c5a6ec Fixed: Fix markdown link generation
The `]` characters in label and `)` characters in url must be escaped.
2021-08-20 06:19:25 +05:00
Leonid Pliushch
4a74618f17 terminal: set default cursor color to white 2021-08-19 16:44:28 +03:00
Leonid Pliushch
19c6134c71 terminal: invert text color under block cursor
Issue: https://github.com/termux/termux-app/issues/219
2021-08-19 16:29:12 +03:00
Leonid Pliushch
501d13a0cb update bootstrap archives 2021-08-18 00:10:40 +03:00
Henrik Grimler
e13773fd83 bug-report template: format text a bit 2021-08-17 09:51:34 +02:00
Henrik Grimler
23d2c1f0e9 github: convert issue templates to forms 2021-08-17 09:48:43 +02:00
agnostic-apollo
cac9a769c0 Merge pull request #2217 from the-blank-x/supportgemini
Add gemini to the list of url regex protocols
2021-08-11 23:42:20 +05:00
blank X
e30812af22 Add Gemini to the list of protocols 2021-08-12 00:34:21 +07:00
agnostic-apollo
1578ab5547 Merge pull request #2199 from WMCB-Tech/master
README: use the latest discord invite badge
2021-08-01 17:39:37 +05:00
marcusz
d5d87639ce update the discord badge link to updated one 2021-08-01 20:27:49 +08:00
agnostic-apollo
8ba5458221 Merge pull request #2198 from WMCB-Tech/master
Add Discord badge link
2021-08-01 14:28:09 +05:00
marcusz
a7596e7d03 add "join the discord" badge
discord may be the popular platform and bridged through gitter/irc. so i guess why not
2021-08-01 17:17:26 +08:00
agnostic-apollo
2b7aa5e803 Fix issue where wrong IME inputType would be set if termux was returned to from another app with text input view mode selected 2021-07-30 00:32:46 +05:00
agnostic-apollo
2b386efc3c Update strings.xml 2021-07-27 21:20:47 +05:00
TotalCaesar659
9a306ca1c5 readme: update urls to https (#2190) 2021-07-27 01:44:28 +03:00
agnostic-apollo
9febca9567 Fix comment 2021-07-19 18:12:57 +05:00
agnostic-apollo
7d76e8b185 Add PASTE extra key for pasting text from clipboard 2021-07-19 17:52:11 +05:00
agnostic-apollo
00d80b9e02 Automatically attach debug APKs when a release is created 2021-07-16 17:08:26 +05:00
agnostic-apollo
f10de462d2 Fix app packaging warning
PackagingOptions.jniLibs.useLegacyPackaging should be set to true because android:extractNativeLibs is set to "true" in AndroidManifest.xml.

https://monitor.f-droid.org/builds/log/com.termux/117
2021-07-16 17:05:43 +05:00
agnostic-apollo
f837ddef23 Bump gradle to 4.2.2 2021-07-16 14:40:22 +05:00
agnostic-apollo
f4e70678b1 Ensure that markdown code formatting is not broken for ResultSender if data itself contains any backticks 2021-07-14 17:39:05 +05:00
agnostic-apollo
a189f63604 Ensure failsafe session can still be opened if files directory is not accessible and fix comment
The `/data/data/com.termux` directory will not be created if it did not already exist and android did not already create it instead of as mentioned in 6fa4b9b7. Check https://github.com/termux/termux-app/issues/2168#issuecomment-879705552
2021-07-14 13:37:25 +05:00
Leonid Pliushch
0308d6a6ca extra keys: avoid scheduled executor leak
Under certain cases scheduled executor may leak causing repeatable input to
stuck.

Issue: https://github.com/termux/termux-app/issues/2156
2021-07-11 18:17:19 +03:00
Leonid Pliushch
1b62f7c9a9 installer: fix permissions for lib/apt/apt-helper
It should have executable bit set, otherwise it won't be possible to use tools such as 'apt-file' without reinstalling 'apt'.
2021-07-10 18:25:50 +03:00
agnostic-apollo
6fa4b9b7cd Ensure termux files directory is accessible before bootstrap installation and provide better info when running as secondary user/profile
Termux will check if termux files directory `/data/data/com.termux/files` has rwx permission access before installing bootstrap or starting terminal. Missing permission will automatically be set if possible. The `/data/data/com.termux` directory will also be created if it did not already exist, like if android did not already create it.

Users will now also be shown a crash notification if they attempt to start termux as a secondary user or in a work profile with info of the "alternate" termux files directory `/data/user/<id>/com.termux` set by android and the profile owner app if running under work profile (not secondary user). A notification will also be shown if the termux files directory (not "alternate") is not accessible.

Related #2168
2021-07-10 16:00:28 +05:00
agnostic-apollo
b2a071aad9 Update trigger_library_builds_on_jitpack.yml 2021-07-09 11:14:11 +05:00
agnostic-apollo
9272a757af Bump to v0.117 2021-07-08 13:12:31 +05:00
agnostic-apollo
d49fd6b00c Trigger termux library builds on jitpack on releases 2021-07-08 13:10:50 +05:00
agnostic-apollo
e0ad9ff573 Allow users to disable terminal margin adjustment from termux settings
Previously in (32135025) support was added with `disable-terminal-margin-adjustment` `termux.properties` property to disable terminal margin adjustment in case in causes screen flickering or other issues on some devices. It has now been removed in (7aefd943) and moved to Termux Settings since if it causes issues at startup and users can't access `termux.properties` file from the terminal, they will have to use SAF or root to access it, which will require an external app.

Users can set the value from the `Termux Settings` -> `Termux` -> `Terminal View` -> `Terminal Margin Adjustment` toggle. The `Termux Settings` can be accessed from left drawer in termux and from the android launcher shortcut for Termux Settings, usually accessible by long holding on Termux icon.
2021-07-08 12:17:49 +05:00
agnostic-apollo
7aefd94369 Revert "Allow users to disable terminal margin adjustment"
This reverts commit 32135025
2021-07-08 11:24:29 +05:00
agnostic-apollo
dc8bdfe675 Attempt to fix bootstrap installation failure that may be caused by invalid mkdirs return value 2021-07-08 10:50:30 +05:00
agnostic-apollo
c6b4114f86 Fix issue where a colour tint/highlight would be added to the terminal
This would happen when soft keyboard was to be disabled or hidden at startup and a hardware keyboard was attached and user started typing on hardware keyboard without tapping on the terminal first.
2021-07-08 10:01:47 +05:00
agnostic-apollo
cce6dfed22 Fix issue where RUN_COMMAND intent was failing for coreutils/busybox applets 2021-07-08 09:20:25 +05:00
agnostic-apollo
56c3826680 Add app and device info too for crash notification shown when bootstrap installation or setup storage fails 2021-07-08 08:49:32 +05:00
agnostic-apollo
2cf21c8409 Update .gitignore 2021-07-08 08:28:31 +05:00
agnostic-apollo
4361c5e0c5 Fix java.lang.AbstractMethodError: androidx.window.sidecar.SidecarInterface$SidecarCallback.onDeviceStateChanged
The crash was reported for `Microsoft Surface Duo`, which would affect some samsung and other devices as well, mainly dual screens/foldables. It was caused by androidx:window library that has been used by termux-shared since v0.115 having a typo in its proguard rules which didn't stop the removal of the required method for release builds (not debug) by proguard.

The library has been patched and fix should be available on next version but doing an emergency patch now for termux as well.

For people who are getting the crash should set `disable-terminal-margin-adjustment=true` in `termux.properties` created as per instructions in the link below and then start termux again and see if it fixes the issue. If you had termux installed before updating, you should be able to directly access the `~/.termux/termux.properties` file with SAF.

https://github.com/termux/termux-app/issues/1896#issuecomment-766188879

------

**Crash Message**:
```
abstract method "void androidx.window.sidecar.SidecarInterface$SidecarCallback.onDeviceStateChanged(androidx.window.sidecar.SidecarDeviceState)"
```

### Stacktrace

```
java.lang.AbstractMethodError: abstract method "void androidx.window.sidecar.SidecarInterface$SidecarCallback.onDeviceStateChanged(androidx.window.sidecar.SidecarDeviceState)"
at androidx.window.sidecar.MicrosoftSurfaceSidecar.updateDeviceState(MicrosoftSurfaceSidecar.java:159)
at androidx.window.sidecar.MicrosoftSurfaceSidecar$1.deviceStateChanged(MicrosoftSurfaceSidecar.java:192)
at android.vendor.screenlayout.service.IWindowExtensionCallbackInterface$Stub.onTransact(IWindowExtensionCallbackInterface.java:94)
at android.os.Binder.execTransactInternal(Binder.java:1021)
at android.os.Binder.execTransact(Binder.java:994)

```

https://issuetracker.google.com/issues/189001730
https://android-review.googlesource.com/c/platform/frameworks/support/+/1757630
2021-07-08 08:27:44 +05:00
agnostic-apollo
a53cc88688 Bump gradle dependencies versions 2021-07-08 08:14:42 +05:00
agnostic-apollo
48161816e0 Merge pull request #2163 from arib21/patch-1
Fixed grammar in the README.md file...
2021-07-07 15:39:14 +05:00
Arib Muhtasim
eabbda8efd Fixed grammar in the README.md file...
Went through the README.md file and fixed a lot of grammatical mistakes.
I know this is useless but I was bored...
2021-07-07 16:31:33 +06:00
agnostic-apollo
b90d59479a Fix typo in dccd155 2021-07-02 06:29:05 +05:00
agnostic-apollo
dccd155ba6 Enable split apks for debug builds
APKs for each architecture and a universal APK that is compatible for all architectures will now be available from Github Actions page from the workflow runs labeled `Build`. The APKs will be available as zips under the Artifact section named `termux-app-*`.

Architecture specific APKs can be used by users with low disk space since F-Droid releases are universal (since it doesn't support split APKs #1904) and their install+bootstrap installation size is ~180MB instead of ~120MB if an architecture specific APK is used.

This should also reduce bandwidth usage and download time for debug builds users if they download an architecture specific zip instead of the universal one.

Related #2153
2021-07-02 06:14:38 +05:00
agnostic-apollo
78be0e793e Update README.md 2021-07-01 11:17:47 +05:00
agnostic-apollo
e547c15481 Bump to v0.116 2021-07-01 10:52:51 +05:00
agnostic-apollo
c621c35827 Bump to v0.115 2021-07-01 08:36:03 +05:00
agnostic-apollo
886e52dcff Export JITPACK_NDK_VERSION for jitpack
Jitpack build is failing with the following error

```
> Configure project :
Gradle version Gradle 7.1

FAILURE: Build failed with an exception.

* What went wrong:
A problem occurred configuring project ':app'.
> com.android.builder.errors.EvalIssueException: NDK from ndk.dir at /opt/android-sdk-linux/ndk-bundle had version [21.1.6352462] which disagrees with android.ndkVersion [22.1.7171670]
```

So attempting to manually export an env variable for jitpack which uses ndk 21.1.6352462 instead of the termux default 22.1.7171670 and which is also used by F-Droid

https://jitpack.io/com/github/termux/termux-app/0.115/build.log

https://github.com/jitpack/jitpack.io/blob/master/BUILDING.md#custom-commands

https://gitlab.com/fdroid/fdroiddata/-/blob/master/metadata/com.termux.yml#L726
2021-07-01 08:34:00 +05:00
agnostic-apollo
8e4da6cbcd Revert "Bump to v0.115"
This reverts commit bde9d01f
2021-07-01 08:29:19 +05:00
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
158 changed files with 11187 additions and 3100 deletions

View File

@@ -0,0 +1,44 @@
name: "Bug report"
description: "Create a report to help us improve"
title: "[Bug]: "
labels: ["bug report"]
body:
- type: markdown
attributes:
value: |
This is a bug tracker of the Termux app. If you have issues with a package inside the app, then please open an issue at [termux-packages](https://github.com/termux/termux-packages) instead.
Use search before you open an issue to check whether your issue has been already reported and perhaps solved.
Android versions 5.x and 6.x are not supported anymore.
If you have issues installing packages then please see https://github.com/termux/termux-packages/issues/6726.
- type: textarea
attributes:
label: Problem description
description: |
A clear and concise description of what the problem is. You may attach the logs, screenshots, screen video recording and whatever else that will help to understand the issue.
Issues without proper description will be closed without solution.
validations:
required: true
- type: textarea
attributes:
label: Steps to reproduce the behavior.
description: |
Please post all necessary commands that are needed to reproduce the issue.
validations:
required: true
- type: textarea
attributes:
label: What is the expected behavior?
- type: textarea
attributes:
label: System information
description: Please provide info about your device
value: |
* Termux application version:
* Android OS version:
* Device model:
validations:
required: true

View File

@@ -0,0 +1,19 @@
name: "Feature request"
description: "Suggest a new feature for Termux application"
title: "[Feature]: "
labels: ["feature request"]
body:
- type: textarea
attributes:
label: Feature description
description: Describe the feature and why you want it.
validations:
required: true
- type: textarea
attributes:
label: Additional information
description: |
Does another app/terminal emulator have this feature?
Provide links to more background information.
validations:
required: true

View File

@@ -1,35 +0,0 @@
---
name: Bug report
about: Create a report to help us improve Termux application
---
<!--
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 is.
You may post screenshots in addition to description.
-->
**Steps to reproduce**
<!--
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**
* Termux application version:
* Android OS version:
* Device model:

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Want ask questions about the project?
url: https://github.com/termux/termux-app/discussions
about: Join GitHub Discussions

View File

@@ -1,22 +0,0 @@
---
name: Feature request
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.

View File

@@ -0,0 +1,76 @@
name: Attach Debug APKs To Release
on:
release:
types:
- published
jobs:
attach-apks:
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Clone repository
uses: actions/checkout@v2
with:
ref: ${{ env.GITHUB_REF }}
- name: Build and attach APKs to release
shell: bash {0}
run: |
exit_on_error() {
echo "$1"
echo "Deleting '$RELEASE_VERSION_NAME' release and '$GITHUB_REF' tag"
hub release delete "$RELEASE_VERSION_NAME"
git push --delete origin "$GITHUB_REF"
exit 1
}
echo "Setting vars"
RELEASE_VERSION_NAME="${GITHUB_REF/refs\/tags\//}"
if ! printf "%s" "${RELEASE_VERSION_NAME/v/}" | grep -qP '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'; then
exit_on_error "The versionName '${RELEASE_VERSION_NAME/v/}' is not a valid version as per semantic version '2.0.0' spec in the format 'major.minor.patch(-prerelease)(+buildmetadata)'. https://semver.org/spec/v2.0.0.html."
fi
APK_DIR_PATH="./app/build/outputs/apk/debug"
APK_VERSION_TAG="$RELEASE_VERSION_NAME+github-debug"
APK_BASENAME_PREFIX="termux-app_$APK_VERSION_TAG"
echo "Building APKs for '$RELEASE_VERSION_NAME' release"
export TERMUX_APK_VERSION_TAG="$APK_VERSION_TAG" # Used by app/build.gradle
if ! ./gradlew assembleDebug; then
exit_on_error "Build failed for '$RELEASE_VERSION_NAME' release."
fi
echo "Validating APKs"
for abi in universal arm64-v8a armeabi-v7a x86_64 x86; do
if ! test -f "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_$abi.apk"; then
files_found="$(ls "$APK_DIR_PATH")"
exit_on_error "Failed to find built APK at '$APK_DIR_PATH/${APK_BASENAME_PREFIX}_$abi.apk'. Files found: "$'\n'"$files_found"
fi
done
echo "Generating sha25sums file"
if ! (cd "$APK_DIR_PATH"; sha256sum \
"${APK_BASENAME_PREFIX}_universal.apk" \
"${APK_BASENAME_PREFIX}_arm64-v8a.apk" \
"${APK_BASENAME_PREFIX}_armeabi-v7a.apk" \
"${APK_BASENAME_PREFIX}_x86_64.apk" \
"${APK_BASENAME_PREFIX}_x86.apk" \
> sha256sums); then
exit_on_error "Generate sha25sums failed for '$RELEASE_VERSION_NAME' release."
fi
echo "Attaching APKs to github release"
if ! hub release edit \
-m "" \
-a "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_universal.apk" \
-a "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_arm64-v8a.apk" \
-a "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_armeabi-v7a.apk" \
-a "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_x86_64.apk" \
-a "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_x86.apk" \
-a "$APK_DIR_PATH/sha256sums" \
"$RELEASE_VERSION_NAME"; then
exit_on_error "Attach APKs to release failed for '$RELEASE_VERSION_NAME' release."
fi

View File

@@ -4,23 +4,110 @@ 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
- name: Clone repository
uses: actions/checkout@v2
- name: Build APKs
shell: bash {0}
run: |
exit_on_error() { echo "$1"; exit 1; }
echo "Setting vars"
# Set RELEASE_VERSION_NAME to "<CURRENT_VERSION_NAME>+<last_commit_hash>"
CURRENT_VERSION_NAME_REGEX='\s+versionName "([^"]+)"$'
CURRENT_VERSION_NAME="$(grep -m 1 -E "$CURRENT_VERSION_NAME_REGEX" ./app/build.gradle | sed -r "s/$CURRENT_VERSION_NAME_REGEX/\1/")"
RELEASE_VERSION_NAME="v$CURRENT_VERSION_NAME+${GITHUB_SHA:0:7}" # The "+" is necessary so that versioning precedence is not affected
if ! printf "%s" "${RELEASE_VERSION_NAME/v/}" | grep -qP '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'; then
exit_on_error "The versionName '${RELEASE_VERSION_NAME/v/}' is not a valid version as per semantic version '2.0.0' spec in the format 'major.minor.patch(-prerelease)(+buildmetadata)'. https://semver.org/spec/v2.0.0.html."
fi
APK_DIR_PATH="./app/build/outputs/apk/debug"
APK_VERSION_TAG="$RELEASE_VERSION_NAME-github-debug" # Note the "-", GITHUB_SHA will already have "+" before it
APK_BASENAME_PREFIX="termux-app_$APK_VERSION_TAG"
# Used by attachment steps later
echo "APK_DIR_PATH=$APK_DIR_PATH" >> $GITHUB_ENV
echo "APK_VERSION_TAG=$APK_VERSION_TAG" >> $GITHUB_ENV
echo "APK_BASENAME_PREFIX=$APK_BASENAME_PREFIX" >> $GITHUB_ENV
echo "Building APKs for '$RELEASE_VERSION_NAME' build"
export TERMUX_APP_VERSION_NAME="${RELEASE_VERSION_NAME/v/}" # Used by app/build.gradle
export TERMUX_APK_VERSION_TAG="$APK_VERSION_TAG" # Used by app/build.gradle
if ! ./gradlew assembleDebug; then
exit_on_error "Build failed for '$RELEASE_VERSION_NAME' build."
fi
echo "Validating APKs"
for abi in universal arm64-v8a armeabi-v7a x86_64 x86; do
if ! test -f "$APK_DIR_PATH/${APK_BASENAME_PREFIX}_$abi.apk"; then
files_found="$(ls "$APK_DIR_PATH")"
exit_on_error "Failed to find built APK at '$APK_DIR_PATH/${APK_BASENAME_PREFIX}_$abi.apk'. Files found: "$'\n'"$files_found"
fi
done
echo "Generating sha25sums file"
if ! (cd "$APK_DIR_PATH"; sha256sum \
"${APK_BASENAME_PREFIX}_universal.apk" \
"${APK_BASENAME_PREFIX}_arm64-v8a.apk" \
"${APK_BASENAME_PREFIX}_armeabi-v7a.apk" \
"${APK_BASENAME_PREFIX}_x86_64.apk" \
"${APK_BASENAME_PREFIX}_x86.apk" \
> sha256sums); then
exit_on_error "Generate sha25sums failed for '$RELEASE_VERSION_NAME' release."
fi
- name: Attach universal APK file
uses: actions/upload-artifact@v2
with:
name: ${{ env.APK_BASENAME_PREFIX }}_universal
path: |
${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_universal.apk
${{ env.APK_DIR_PATH }}/output-metadata.json
- name: Attach arm64-v8a APK file
uses: actions/upload-artifact@v2
with:
name: ${{ env.APK_BASENAME_PREFIX }}_arm64-v8a
path: |
${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_arm64-v8a.apk
${{ env.APK_DIR_PATH }}/output-metadata.json
- name: Attach armeabi-v7a APK file
uses: actions/upload-artifact@v2
with:
name: ${{ env.APK_BASENAME_PREFIX }}_armeabi-v7a
path: |
${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_armeabi-v7a.apk
${{ env.APK_DIR_PATH }}/output-metadata.json
- name: Attach x86_64 APK file
uses: actions/upload-artifact@v2
with:
name: ${{ env.APK_BASENAME_PREFIX }}_x86_64
path: |
${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_x86_64.apk
${{ env.APK_DIR_PATH }}/output-metadata.json
- name: Attach x86 APK file
uses: actions/upload-artifact@v2
with:
name: ${{ env.APK_BASENAME_PREFIX }}_x86
path: |
${{ env.APK_DIR_PATH }}/${{ env.APK_BASENAME_PREFIX }}_x86.apk
${{ env.APK_DIR_PATH }}/output-metadata.json
- name: Attach sha256sums file
uses: actions/upload-artifact@v2
with:
name: sha256sums
path: |
${{ env.APK_DIR_PATH }}/sha256sums
${{ env.APK_DIR_PATH }}/output-metadata.json

View File

@@ -1,26 +0,0 @@
name: Publish library packages
on:
push:
branches:
- master
paths:
- 'terminal-emulator/build.gradle'
- 'terminal-view/build.gradle'
- 'termux-shared/build.gradle'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v2
- name: Perform release build
run: |
./gradlew assembleRelease
- name: Publish libraries on Github Packages
env:
GH_USERNAME: xeffyr
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
./gradlew publish

View File

@@ -0,0 +1,21 @@
name: Trigger Termux Library Builds on Jitpack
on:
release:
types:
- published
jobs:
trigger-termux-library-builds:
runs-on: ubuntu-latest
steps:
- name: Set vars
run: echo "TERMUX_LIB_VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_ENV # Do not include "v" prefix
- name: Echo release
run: echo "Triggering termux library builds on jitpack for '$TERMUX_LIB_VERSION' release after waiting for 3 mins"
- name: Trigger termux library builds on jitpack
run: |
sleep 180 # It will take some time for the new tag to be detected by Jitpack
curl --max-time 600 --no-progress-meter "https://jitpack.io/com/termux/termux-app/terminal-emulator/$TERMUX_LIB_VERSION/terminal-emulator-$TERMUX_LIB_VERSION.pom"
curl --max-time 600 --no-progress-meter "https://jitpack.io/com/termux/termux-app/terminal-view/$TERMUX_LIB_VERSION/terminal-view-$TERMUX_LIB_VERSION.pom"
curl --max-time 600 --no-progress-meter "https://jitpack.io/com/termux/termux-app/termux-shared/$TERMUX_LIB_VERSION/termux-shared-$TERMUX_LIB_VERSION.pom"

1
.gitignore vendored
View File

@@ -4,6 +4,7 @@
# Built application files
build/
release/
*.apk
*.so
.externalNativeBuild

View File

@@ -2,6 +2,5 @@ The `termux/termux-app` repository is released under [GPLv3 only](https://www.gn
### Exceptions
- [Terminal Emulator for Android](https://github.com/jackpal/Android-Terminal-Emulator) code is used which is released under [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). Check [terminal-view](terminal-view) and [terminal-emulator](terminal-emulator) modules.
- [libcore/ojluni](https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/) code is used which is released under [GPLv2 only with "Classpath" exception](https://openjdk.java.net/legal/gplv2+ce.html). Check `com.termux.shared.file` package under [termux-shared](termux-shared) module.
- [libsuperuser ](https://github.com/Chainfire/libsuperuser) code is used which is released under [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). Check `com.termux.shared.shell.StreamGobbler` class under [termux-shared](termux-shared) module.
- [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.

155
README.md
View File

@@ -3,6 +3,9 @@
[![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)
[![Join the Termux discord server](https://img.shields.io/discord/641256914684084234.svg?label=&logo=discord&logoColor=ffffff&color=5865F2)](https://discord.gg/HXpF69X)
[![Termux library releases at Jitpack](https://jitpack.io/v/termux/termux-app.svg)](https://jitpack.io/#termux/termux-app)
[Termux](https://termux.com) is an Android terminal application and Linux environment.
@@ -12,7 +15,7 @@ Quick how-to about Termux package management is available at [Package Management
***
**@termux is looking for Termux Application maintainers for implementing new features, fixing bugs and reviewing pull requests since current one (@fornwall) is inactive.**
**@termux is looking for Termux Application maintainers for implementing new features, fixing bugs and reviewing pull requests since the current one (@fornwall) is inactive.**
Issue https://github.com/termux/termux-app/issues/1072 needs extra attention.
@@ -23,7 +26,9 @@ Issue https://github.com/termux/termux-app/issues/1072 needs extra attention.
- [Installation](#Installation)
- [Uninstallation](#Uninstallation)
- [Important Links](#Important-Links)
- [For Devs and Contributors](#For-Devs-and-Contributors)
- [Debugging](#Debugging)
- [For Maintainers and Contributors](#For-Maintainers-and-Contributors)
- [Forking](#Forking)
##
@@ -44,38 +49,87 @@ The core [Termux](https://github.com/termux/termux-app) app comes with the follo
## Installation
Latest version is `v0.118.0`.
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.
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 like `Github`. Android Package Manager will also normally not allow installation of APKs with 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.
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 the uninstallation so that you can restore it after re-installing from Termux different source.
In the following paragraphs, *"bootstrap"* refers to the minimal packages that are shipped with the `termux-app` itself to start a working shell environment. Its zips are built and released [here](https://github.com/termux/termux-packages/releases).
### 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.
Termux application can be obtained from `F-Droid` from [here](https://f-droid.org/en/packages/com.termux/).
### Debug Builds
You **do not** need to download the `F-Droid` app (via the `Download F-Droid` link) to install Termux. You can download the Termux APK directly from the site by clicking the `Download APK` link at the bottom of each version section.
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.
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`. The `F-Droid` releases are built and published by `F-Droid` once they [detect](https://gitlab.com/fdroid/fdroiddata/-/blob/master/metadata/com.termux.yml) a new `Github` release. The Termux maintainers **do not** have any control over the building and publishing of the Termux apps 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.
### Google Playstore **(Deprecated)**
The `F-Droid` app often may not notify you of updates and you will manually have to do a pull down swipe action in the `Updates` tab of the app for it to check updates. Make sure battery optimizations are disabled for the app, check https://dontkillmyapp.com/ for details on how to do that.
**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.
Only a universal APK is released, which will work on all supported architectures. The APK and bootstrap installation size will be `~180MB`. `F-Droid` does [not support](https://github.com/termux/termux-app/pull/1904) architecture specific APKs.
### Github
Termux application can be obtained on `Github` either from [`Github Releases`](https://github.com/termux/termux-app/releases) for version `>= 0.118.0` or from [`Github Build`](https://github.com/termux/termux-app/actions/workflows/debug_build.yml) action workflows.
The APKs for `Github Releases` will be listed under `Assets` drop-down of a release. These are automatically attached when a new version is released.
The APKs for `Github Build` action workflows will be listed under `Artifacts` section of a workflow run. These are created for each commit/push done to the repository and can be used by users who don't want to wait for releases and want to try out the latest features immediately or want to test their pull requests. Note that for action workflows, you need to be [**logged into a `Github` account**](https://github.com/login) for the `Artifacts` links to be enabled/clickable. If you are using the [`Github` app](https://github.com/mobile), then make sure to open workflow link in a browser like Chrome or Firefox that has your Github account logged in since the in-app browser may not be logged in.
The APKs for both of these are [`debuggable`](https://developer.android.com/studio/debug) and are compatible with each other but they are not compatible with other sources.
Both universal and architecture specific APKs are released. The APK and bootstrap installation size will be `~180MB` if using universal and `~120MB` if using architecture specific. Check [here](https://github.com/termux/termux-app/issues/2153) for details.
### Google Play Store **(Deprecated)**
**Termux and its plugins are no longer updated on [Google Play Store](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) and have been deprecated.** The last version released for Android `>= 7` was `v0.101`. **It is highly recommended to not install Termux apps from Play Store any more.**
There are plans for **unpublishing** the Termux app and all its plugins on Play Store soon so that new users cannot install it and for **disabling** the Termux apps with updates so that existing users **cannot continue using outdated versions**. You are encouraged to move to `F-Droid` or `Github` builds as soon as possible.
You **will not need to buy plugins again** if you bought them on Play Store. All plugins are free on `F-Droid` and `Github`.
You can backup all your data under `$HOME/` and `$PREFIX/` before changing installation source, and then restore it afterwards, by following instructions at [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux) before the uninstallation.
There is currently no work being done to solve android `10` issues and *working* updates will not be resumed on Google Play Store any time soon. We will continue targeting sdk `28` for now. So there is not much point in staying on Play Store builds and waiting for updates to be resumed. If for some reason you don't want to move to `F-Droid` or `Github` sources for now, 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.
Note that by upgrading old packages to latest versions, like that of `python` may break your setups/scripts since they may not be compatible anymore. Moreover, you will not be able to downgrade the package versions since termux repos only keep the latest version and you will have to manually rebuild the old versions of the packages if required as per https://github.com/termux/termux-packages/wiki/Building-packages.
If you plan on staying on Play Store sources in future as well, then you may want to **disable automatic updates in Play Store** for Termux apps, since if and when updates to disable Termux apps are released, then **you will not be able to downgrade** and **will be forced** to move since apps won't work anymore. Only a way to backup `termux-app` data may be provided. The `termux-tools` [version `>= 0.135`](https://github.com/termux/termux-packages/pull/7493) will also show a banner at the top of the terminal saying `You are likely using a very old version of Termux, probably installed from the Google Play Store.`, you can remove it by running `rm -f /data/data/com.termux/files/usr/etc/motd-playstore` and restarting the app.
#### Why Disable?
<details>
<summary></summary>
- They should be disabled because deprecated things get removed and are not supported after some time, its the standard practice. It has been many months now since deprecation was announced and updates have not been released on Play Store since after `29 September 2020`.
- The new versions have lots of **new features and fixes** which you can mostly check out in the Changelog of [`Github Releases`](https://github.com/termux/termux-app/releases) that you may be missing out. Extra detail is usually provided in [commit messages](https://github.com/termux/termux-app/commits/master).
- Users on old versions are quite often reporting issues in multiple repositories and support forums that were **fixed months ago**, which we then have to deal with. The maintainers of @termux work in their free time, majorly for free, to work on development and provide support and having to re-re-deal with old issues takes away the already limited time from current work and is not possible to continue doing. Play Store page of `termux-app` has been filled with bad reviews of *"broken app"*, even though its clearly mentioned on the page that app is not being updated, yet users don't read and still install and report issues.
- Asking people to pay for plugins when the `termux-app` at installation time is broken due to repository issues and has bugs is unethical.
- Old versions don't have proper logging/debugging and crash report support. Reporting bugs without logs or detailed info is not helpful in solving them.
- It's also easier for us to solve package related issues and provide custom functionality with app updates, which can't be done if users continue using old versions. For example, the [bintray shudown](https://github.com/termux/termux-packages/wiki/Package-Management) causing package install/update failures for new Play Store users is/was not an issue for F-Droid users since it is being shipped with updated bootstrap and repo info, hence no reported issues from new F-Droid users.
</details>
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.
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 the 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.
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.
Even if you think you have not installed any of the plugins, it's strongly suggested to go through the application list in Android settings and double-check.
##
@@ -90,7 +144,7 @@ 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 Twitter](https://twitter.com/termux/)
- [Termux Reports Email](mailto:termuxreports@groups.io)
### Wikis
@@ -104,42 +158,99 @@ The main ones are the following.
- [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)
- [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)
- [Running Commands in Termux From Other Apps via `RUN_COMMAND` intent](https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent)
- [Termux and Android 10](https://github.com/termux/termux-packages/wiki/Termux-and-Android-10)
### Terminal
<details>
<summary></summary>
### 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)
- [XTerm control sequences](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
- [vt100.net](https://vt100.net/)
- [Terminal codes (ANSI and terminfo equivalents)](https://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)).
- iTerm 2: OS X terminal application. [Source](https://github.com/gnachman/iTerm2), [Issues](https://gitlab.com/gnachman/iterm2/issues) and [Documentation](https://iterm2.com/documentation.html) (which includes [iTerm2 proprietary escape codes](https://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).
- xterm: The grandfather of terminal emulators. [Source](https://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).
</details>
##
## For Devs and Contributors
### Debugging
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.
You can help debug problems of the `Termux` app and its plugins by setting appropriate `logcat` `Log Level` in `Termux` app settings -> `<APP_NAME>` -> `Debugging` -> `Log Level` (Requires `Termux` app version `>= 0.118.0`). The `Log Level` defaults to `Normal` and log level `Verbose` currently logs additional information. Its best to revert log level to `Normal` after you have finished debugging since private data may otherwise be passed to `logcat` during normal operation and moreover, additional logging increases execution time.
The plugin apps **do not execute the commands themselves** but send execution intents to `Termux` app, which has its own log level which can be set in `Termux` app settings -> `Termux` -> `Debugging` -> `Log Level`. So you must set log level for both `Termux` and the respective plugin app settings to get all the info.
Once log levels have been set, you can run the `logcat` command in `Termux` app terminal to view the logs in realtime (`Ctrl+c` to stop) or use `logcat -d > logcat.txt` to take a dump of the log. You can also view the logs from a PC over `ADB`. For more information, check official android `logcat` guide [here](https://developer.android.com/studio/command-line/logcat).
Moreover, users can generate termux files `stat` info and `logcat` dump automatically too with terminal's long hold options menu `More` -> `Report Issue` option and selecting `YES` in the prompt shown to add debug info. This can be helpful for reporting and debugging other issues. If the report generated is too large, then `Save To File` option in context menu (3 dots on top right) of `ReportActivity` can be used and the file viewed/shared instead.
Users must post complete report (optionally without sensitive info) when reporting issues. Issues opened with **(partial) screenshots of error reports** instead of text will likely be automatically closed/deleted.
##### Log Levels
- `Off` - Log nothing.
- `Normal` - Start logging error, warn and info messages and stacktraces.
- `Debug` - Start logging debug messages.
- `Verbose` - Start logging verbose messages.
##
## For Maintainers 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 the Termux app and its plugins. It was created to allow for the removal of all hardcoded paths in the Termux app. Some of the termux plugins are using this as well and rest will in future. 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. Termux app and plugin specific classes must be added under `com.termux.shared.termux` package and general classes outside it. The [`termux-shared` `LICENSE`](termux-shared/LICENSE.md) must also be checked and updated if necessary when contributing code. The licenses of any external library or code must be honoured.
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.
Check [Termux Libraries](https://github.com/termux/termux-app/wiki/Termux-Libraries) for how to import termux libraries in plugin apps and [Forking and Local Development](https://github.com/termux/termux-app/wiki/Termux-Libraries#forking-and-local-development) for how to update termux libraries for plugins.
Commit messages **must** use [Conventional Commits](https://www.conventionalcommits.org) specs so that chagelogs can automatically be generated by the [`create-conventional-changelog`](https://github.com/termux/create-conventional-changelog) script, check its repo for further details on the spec. Use the following `types` as `Added: Add foo`, `Added|Fixed: Add foo and fix bar`, `Changed!: Change baz as a breaking change`, etc. You can optionally add a scope as well, like `Fixed(terminal): Some bug`. The space after `:` is necessary.
- **Added** for new features.
- **Changed** for changes in existing functionality.
- **Deprecated** for soon-to-be removed features.
- **Removed** for now removed features.
- **Fixed** for any bug fixes.
- **Security** in case of vulnerabilities.
- **Docs** for updating documentation.
Changelogs for releases are generated based on [Keep a Changelog](https://github.com/olivierlacan/keep-a-changelog) specs.
The `versionName` in `build.gradle` files of Termux and its plugin apps must follow the [semantic version `2.0.0` spec](https://semver.org/spec/v2.0.0.html) in the format `major.minor.patch(-prerelease)(+buildmetadata)`. When bumping `versionName` in `build.gradle` files and when creating a tag for new releases on github, make sure to include the patch number as well, like `v0.1.0` instead of just `v0.1`. The `build.gradle` files and `attach_debug_apks_to_release` workflow validates the version as well and the build/attachment will fail if `versionName` does not follow the spec.
##
## Forking
- Check [`TermuxConstants`](https://github.com/termux/termux-app/blob/master/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java) javadocs for instructions on what changes to make in the app to change package name.
- You also need to recompile bootstrap zip for the new package name. Check [here](https://github.com/termux/termux-app/issues/1983) and [here](https://github.com/termux/termux-app/issues/2081#issuecomment-865280111) for experimental work on it.
- Currently, not all plugins use `TermuxConstants` from `termux-shared` library and have hardcoded `com.termux` values and will need to be manually patched.
- If forking termux plugins, check [Forking and Local Development](https://github.com/termux/termux-app/wiki/Termux-Libraries#forking-and-local-development) for info on how to use termux libraries for plugins.

View File

@@ -1,14 +1,18 @@
plugins {
id "com.android.application"
id "com.android.application"
}
android {
compileSdkVersion project.properties.compileSdkVersion.toInteger()
ndkVersion project.properties.ndkVersion
ndkVersion = System.getenv("JITPACK_NDK_VERSION") ?: project.properties.ndkVersion
def appVersionName = System.getenv("TERMUX_APP_VERSION_NAME") ?: ""
def apkVersionTag = System.getenv("TERMUX_APK_VERSION_TAG") ?: ""
def splitAPKsForDebugBuilds = System.getenv("TERMUX_SPLIT_APKS_FOR_DEBUG_BUILDS") ?: "1"
def splitAPKsForReleaseBuilds = System.getenv("TERMUX_SPLIT_APKS_FOR_RELEASE_BUILDS") ?: "0" // F-Droid does not support split APKs #1904
dependencies {
implementation "androidx.annotation:annotation:1.2.0"
implementation "androidx.core:core:1.5.0-rc01"
implementation "androidx.annotation:annotation:1.3.0"
implementation "androidx.core:core:1.6.0"
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
implementation "androidx.preference:preference:1.1.1"
implementation "androidx.viewpager:viewpager:1.0.0"
@@ -26,8 +30,11 @@ android {
applicationId "com.termux"
minSdkVersion project.properties.minSdkVersion.toInteger()
targetSdkVersion project.properties.targetSdkVersion.toInteger()
versionCode 114
versionName "0.114"
versionCode 118
versionName "0.118.0"
if (appVersionName) versionName = appVersionName
validateVersionName(versionName)
manifestPlaceholders.TERMUX_PACKAGE_NAME = "com.termux"
manifestPlaceholders.TERMUX_APP_NAME = "Termux"
@@ -44,10 +51,15 @@ android {
}
}
ndk {
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
splits {
abi {
enable ((gradle.startParameter.taskNames.any { it.contains("Debug") } && splitAPKsForDebugBuilds == "1") ||
(gradle.startParameter.taskNames.any { it.contains("Release") } && splitAPKsForReleaseBuilds == "1"))
reset ()
include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
universalApk true
}
}
}
signingConfigs {
@@ -62,7 +74,7 @@ android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
shrinkResources false // Reproducible builds
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
@@ -91,6 +103,25 @@ android {
includeAndroidResources = true
}
}
packagingOptions {
jniLibs {
useLegacyPackaging true
}
}
applicationVariants.all { variant ->
variant.outputs.all { output ->
if (variant.buildType.name == "debug") {
def abi = output.getFilter(com.android.build.OutputFile.ABI)
outputFileName = new File("termux-app_" + (apkVersionTag ? apkVersionTag : "debug") + "_" + (abi ? abi : "universal") + ".apk")
} else if (variant.buildType.name == "release") {
def abi = output.getFilter(com.android.build.OutputFile.ABI)
outputFileName = new File("termux-app_" + (apkVersionTag ? apkVersionTag : "release") + "_" + (abi ? abi : "universal") + ".apk")
}
}
}
}
dependencies {
@@ -99,9 +130,16 @@ dependencies {
}
task versionName {
doLast {
print android.defaultConfig.versionName
}
doLast {
print android.defaultConfig.versionName
}
}
def validateVersionName(String versionName) {
// https://semver.org/spec/v2.0.0.html#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
// ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$
if (!java.util.regex.Pattern.matches("^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?\$", versionName))
throw new GradleException("The versionName '" + versionName + "' is not a valid version as per semantic version '2.0.0' spec in the format 'major.minor.patch(-prerelease)(+buildmetadata)'. https://semver.org/spec/v2.0.0.html.")
}
def downloadBootstrap(String arch, String expectedChecksum, String version) {
@@ -118,6 +156,7 @@ def downloadBootstrap(String arch, String expectedChecksum, String version) {
digest.update(buffer, 0, readBytes)
}
def checksum = new BigInteger(1, digest.digest()).toString(16)
while (checksum.length() < 64) { checksum = "0" + checksum }
if (checksum == expectedChecksum) {
return
} else {
@@ -139,6 +178,7 @@ def downloadBootstrap(String arch, String expectedChecksum, String version) {
out.close()
def checksum = new BigInteger(1, digest.digest()).toString(16)
while (checksum.length() < 64) { checksum = "0" + checksum }
if (checksum != expectedChecksum) {
file.delete()
throw new GradleException("Wrong checksum for " + remoteUrl + ": expected: " + expectedChecksum + ", actual: " + checksum)
@@ -155,16 +195,16 @@ clean {
task downloadBootstraps() {
doLast {
def version = "2021.06.04-r1"
downloadBootstrap("aarch64", "115409ec8abe096080f9e6d9b2765e009c818b3e5aff0133792182009388b81e", version)
downloadBootstrap("arm", "c289688d4f9aabd86240a4d3e25bd89c1a96641e0d085d9ee5a471e9f019e16f", version)
downloadBootstrap("i686", "63cfb8e39cac20cecbe9e9890f49a30d7454d4c4dfa2b22b5d0cc6de1dc59726", version)
downloadBootstrap("x86_64", "db47007825e65f8ed15fc46d6b8e3119b01ca4a9856d7c7e5604cf804e753ba2", version)
def version = "2022.01.07-r1"
downloadBootstrap("aarch64", "0fe6d0159d12fcb8baf7750ce9072b9b36f742662b02ad4da145ab85873614cd", version)
downloadBootstrap("arm", "0a6014e2ec3b7079524fee3caabd02be05bcb4add3c6cd9e5ad98408b428c717", version)
downloadBootstrap("i686", "c55369a9af1316dc3d99457aa23cce64b29c1e60e375159352c0d4b9cfed4ac6", version)
downloadBootstrap("x86_64", "e93a7c66d15edb7d1b5ceca906255c85ead35a8b6a52f93524e117cfb07e6778", version)
}
}
afterEvaluate {
android.applicationVariants.all { variant ->
variant.javaCompileProvider.get().dependsOn(downloadBootstraps)
}
android.applicationVariants.all { variant ->
variant.javaCompileProvider.get().dependsOn(downloadBootstraps)
}
}

View File

@@ -10,3 +10,8 @@
-dontobfuscate
#-renamesourcefileattribute SourceFile
#-keepattributes SourceFile,LineNumberTable
# Temp fix for androidx.window:window:1.0.0-alpha09 imported by termux-shared
# https://issuetracker.google.com/issues/189001730
# https://android-review.googlesource.com/c/platform/frameworks/support/+/1757630
-keep class androidx.window.** { *; }

View File

@@ -32,6 +32,7 @@
<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" />
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" tools:ignore="ProtectedPermissions" />
<application
android:name=".app.TermuxApplication"
@@ -44,14 +45,6 @@
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" />
<activity
android:name=".app.TermuxActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|uiMode|keyboard|keyboardHidden|navigation"
@@ -102,7 +95,7 @@
android:theme="@style/Theme.AppCompat.Light.DarkActionBar" />
<activity
android:name=".app.activities.ReportActivity"
android:name=".shared.activities.ReportActivity"
android:theme="@style/Theme.AppCompat.TermuxReportActivity"
android:documentLaunchMode="intoExisting"
/>
@@ -143,6 +136,7 @@
</intent-filter>
</activity>
<provider
android:name=".filepicker.TermuxDocumentsProvider"
android:authorities="${TERMUX_PACKAGE_NAME}.documents"
@@ -154,9 +148,23 @@
</intent-filter>
</provider>
<provider
android:name=".app.TermuxOpenReceiver$ContentProvider"
android:authorities="${TERMUX_PACKAGE_NAME}.files"
android:exported="true"
android:grantUriPermissions="true"
android:permission="${TERMUX_PACKAGE_NAME}.permission.RUN_COMMAND" />
<receiver android:name=".app.TermuxOpenReceiver" android:exported="false" />
<receiver android:name=".shared.activities.ReportActivity$ReportActivityBroadcastReceiver" android:exported="false" />
<service
android:name=".app.TermuxService"
android:exported="false" />
<service
android:name=".app.RunCommandService"
android:exported="true"
@@ -166,21 +174,19 @@
</intent-filter>
</service>
<receiver android:name=".app.TermuxOpenReceiver" />
<provider
android:name=".app.TermuxOpenReceiver$ContentProvider"
android:authorities="${TERMUX_PACKAGE_NAME}.files"
android:exported="true"
android:grantUriPermissions="true"
android:readPermission="android.permission.permRead" />
<!-- 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" />
<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

@@ -10,6 +10,12 @@ 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.file.filesystem.FileType;
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;
@@ -17,7 +23,6 @@ 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.data.DataUtils;
import com.termux.shared.models.ExecutionCommand;
/**
@@ -60,40 +65,68 @@ public class RunCommandService extends Service {
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(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return Service.START_NOT_STICKY;
return stopService();
}
executionCommand.executable = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH);
executionCommand.arguments = intent.getStringArrayExtra(RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS);
executionCommand.stdin = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_STDIN);
executionCommand.workingDirectory = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_WORKDIR);
String executableExtra = executionCommand.executable = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH, null);
executionCommand.arguments = IntentUtils.getStringArrayExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS, null);
/*
* If intent was sent with `am` command, then normal comma characters may have been replaced
* with alternate characters if a normal comma existed in an argument itself to prevent it
* splitting into multiple arguments by `am` command.
* If `tudo` or `sudo` are used, then simply using their `-r` and `--comma-alternative` command
* options can be used without passing the below extras, but native supports is helpful if
* they are not being used.
* https://github.com/agnostic-apollo/tudo#passing-arguments-using-run_command-intent
* https://android.googlesource.com/platform/frameworks/base/+/21bdaf1/cmds/am/src/com/android/commands/am/Am.java#572
*/
boolean replaceCommaAlternativeCharsInArguments = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS, false);
if (replaceCommaAlternativeCharsInArguments) {
String commaAlternativeCharsInArguments = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS, null);
if (commaAlternativeCharsInArguments == null)
commaAlternativeCharsInArguments = TermuxConstants.COMMA_ALTERNATIVE;
// Replace any commaAlternativeCharsInArguments characters with normal commas
DataUtils.replaceSubStringsInStringArrayItems(executionCommand.arguments, commaAlternativeCharsInArguments, TermuxConstants.COMMA_NORMAL);
}
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.backgroundCustomLogLevel = IntentUtils.getIntegerExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL, null);
executionCommand.sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION);
executionCommand.commandLabel = DataUtils.getDefaultIfNull(intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_LABEL), "RUN_COMMAND Execution Intent Command");
executionCommand.commandDescription = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION);
executionCommand.commandHelp = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_HELP);
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.pluginPendingIntent = intent.getParcelableExtra(RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT);
executionCommand.resultConfig.resultPendingIntent = intent.getParcelableExtra(RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT);
executionCommand.resultConfig.resultDirectoryPath = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_DIRECTORY, null);
if (executionCommand.resultConfig.resultDirectoryPath != null) {
executionCommand.resultConfig.resultSingleFile = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_RESULT_SINGLE_FILE, false);
executionCommand.resultConfig.resultFileBasename = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_BASENAME, null);
executionCommand.resultConfig.resultFileOutputFormat = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, null);
executionCommand.resultConfig.resultFileErrorFormat = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, null);
executionCommand.resultConfig.resultFilesSuffix = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILES_SUFFIX, null);
}
// If "allow-external-apps" property to not set to "true", then just return
// 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);
errmsg = PluginUtils.checkIfAllowExternalAppsPolicyIsViolated(this, LOG_TAG);
if (errmsg != null) {
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true);
return Service.START_NOT_STICKY;
return stopService();
}
@@ -101,24 +134,23 @@ public class RunCommandService extends Service {
// 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(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return Service.START_NOT_STICKY;
return stopService();
}
// Get canonical path of executable
executionCommand.executable = FileUtils.getCanonicalPath(executionCommand.executable, null, true);
executionCommand.executable = TermuxFileUtils.getCanonicalPath(executionCommand.executable, null, true);
// If executable is not a regular file, or is not readable or executable, then just return
// Setting of missing read and execute permissions is not done
errmsg = FileUtils.validateRegularFileExistenceAndPermissions(this, "executable", executionCommand.executable, null,
PluginUtils.PLUGIN_EXECUTABLE_FILE_PERMISSIONS, true, true,
error = FileUtils.validateRegularFileExistenceAndPermissions("executable", executionCommand.executable, null,
FileUtils.APP_EXECUTABLE_FILE_PERMISSIONS, true, true,
false);
if (errmsg != null) {
errmsg += "\n" + this.getString(R.string.msg_executable_absolute_path, executionCommand.executable);
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
if (error != null) {
executionCommand.setStateFailed(error);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return Service.START_NOT_STICKY;
return stopService();
}
@@ -126,29 +158,36 @@ public class RunCommandService extends Service {
// If workingDirectory is not null or empty
if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) {
// Get canonical path of workingDirectory
executionCommand.workingDirectory = FileUtils.getCanonicalPath(executionCommand.workingDirectory, null, true);
executionCommand.workingDirectory = TermuxFileUtils.getCanonicalPath(executionCommand.workingDirectory, null, true);
// If workingDirectory is not a directory, or is not readable or writable, then just return
// Creation of missing directory and setting of read, write and execute permissions are only done if workingDirectory is
// under {@link TermuxConstants#TERMUX_FILES_DIR_PATH}
// under allowed termux working directory paths.
// We try to set execute permissions, but ignore if they are missing, since only read and write permissions are required
// for working directories.
errmsg = FileUtils.validateDirectoryFileExistenceAndPermissions(this, "working", executionCommand.workingDirectory, TermuxConstants.TERMUX_FILES_DIR_PATH, true,
PluginUtils.PLUGIN_WORKING_DIRECTORY_PERMISSIONS, true, true,
true, true);
if (errmsg != null) {
errmsg += "\n" + this.getString(R.string.msg_working_directory_absolute_path, executionCommand.workingDirectory);
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
error = TermuxFileUtils.validateDirectoryFileExistenceAndPermissions("working", executionCommand.workingDirectory,
true, true, true,
false, true);
if (error != null) {
executionCommand.setStateFailed(error);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return Service.START_NOT_STICKY;
return stopService();
}
}
// If the executable passed as the extra was an applet for coreutils/busybox, then we must
// use it instead of the canonical path above since otherwise arguments would be passed to
// coreutils/busybox instead and command would fail. Broken symlinks would already have been
// validated so it should be fine to use it.
executableExtra = TermuxFileUtils.getExpandedTermuxPath(executableExtra);
if (FileUtils.getFileType(executableExtra, false) == FileType.SYMLINK) {
Logger.logVerbose(LOG_TAG, "The executableExtra path \"" + executableExtra + "\" is a symlink so using it instead of the canonical path \"" + executionCommand.executable + "\"");
executionCommand.executable = executableExtra;
}
executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(executionCommand.executable).build();
executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(FileUtils.getExpandedTermuxPath(executionCommand.executable)).build();
Logger.logVerbose(LOG_TAG, executionCommand.toString());
Logger.logVerboseExtended(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);
@@ -157,12 +196,21 @@ public class RunCommandService extends Service {
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_BACKGROUND_CUSTOM_LOG_LEVEL, DataUtils.getStringFromInteger(executionCommand.backgroundCustomLogLevel, null));
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.pluginPendingIntent);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT, executionCommand.resultConfig.resultPendingIntent);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_DIRECTORY, executionCommand.resultConfig.resultDirectoryPath);
if (executionCommand.resultConfig.resultDirectoryPath != null) {
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_SINGLE_FILE, executionCommand.resultConfig.resultSingleFile);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_BASENAME, executionCommand.resultConfig.resultFileBasename);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, executionCommand.resultConfig.resultFileOutputFormat);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, executionCommand.resultConfig.resultFileErrorFormat);
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILES_SUFFIX, executionCommand.resultConfig.resultFilesSuffix);
}
// Start TERMUX_SERVICE and pass it execution intent
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -171,8 +219,11 @@ public class RunCommandService extends Service {
this.startService(execIntent);
}
runStopForeground();
return stopService();
}
private int stopService() {
runStopForeground();
return Service.START_NOT_STICKY;
}
@@ -194,7 +245,7 @@ public class RunCommandService extends Service {
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);
null, null, NotificationUtils.NOTIFICATION_MODE_SILENT);
if (builder == null) return null;
// No need to show a timestamp:

View File

@@ -12,6 +12,7 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@@ -26,11 +27,15 @@ import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.autofill.AutofillManager;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.Toast;
import com.termux.R;
import com.termux.app.terminal.TermuxActivityRootView;
import com.termux.shared.activities.ReportActivity;
import com.termux.shared.packages.PermissionUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
import com.termux.app.activities.HelpActivity;
@@ -40,11 +45,12 @@ import com.termux.app.terminal.TermuxSessionsListViewController;
import com.termux.app.terminal.io.TerminalToolbarViewPager;
import com.termux.app.terminal.TermuxTerminalSessionClient;
import com.termux.app.terminal.TermuxTerminalViewClient;
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
import com.termux.shared.terminal.io.extrakeys.ExtraKeysView;
import com.termux.app.settings.properties.TermuxAppSharedProperties;
import com.termux.shared.interact.DialogUtils;
import com.termux.shared.interact.TextInputDialogUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.termux.TermuxUtils;
import com.termux.shared.view.ViewUtils;
import com.termux.terminal.TerminalSession;
import com.termux.terminal.TerminalSessionClient;
import com.termux.app.utils.CrashUtils;
@@ -165,8 +171,6 @@ public final class TermuxActivity extends Activity implements ServiceConnection
private static final int CONTEXT_MENU_SETTINGS_ID = 8;
private static final int CONTEXT_MENU_REPORT_ID = 9;
private static final int REQUESTCODE_PERMISSION_STORAGE = 1234;
private static final String ARG_TERMINAL_TOOLBAR_TEXT_INPUT = "terminal_toolbar_text_input";
private static final String LOG_TAG = "TermuxActivity";
@@ -179,7 +183,10 @@ public final class TermuxActivity extends Activity implements ServiceConnection
// Check if a crash happened on last run of the app and show a
// notification with the crash details if it did
CrashUtils.notifyCrash(this, LOG_TAG);
CrashUtils.notifyAppCrashOnLastRun(this, LOG_TAG);
// Delete ReportInfo serialized object files from cache older than 14 days
ReportActivity.deleteReportInfoFilesOlderThanXDays(this, 14, false);
// Load termux shared properties
mProperties = new TermuxAppSharedProperties(this);
@@ -199,9 +206,12 @@ public final class TermuxActivity extends Activity implements ServiceConnection
return;
}
setMargins();
mTermuxActivityRootView = findViewById(R.id.activity_termux_root_view);
mTermuxActivityRootView.setActivity(this);
mTermuxActivityBottomSpaceView = findViewById(R.id.activity_termux_bottom_space_view);
mTermuxActivityRootView.setOnApplyWindowInsetsListener(new TermuxActivityRootView.WindowInsetsListener());
View content = findViewById(android.R.id.content);
content.setOnApplyWindowInsetsListener((v, insets) -> {
@@ -219,6 +229,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
setTerminalToolbarView(savedInstanceState);
setSettingsButtonView();
setNewSessionButtonView();
setToggleKeyboardView();
@@ -255,7 +267,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
if (mTermuxTerminalViewClient != null)
mTermuxTerminalViewClient.onStart();
addTermuxActivityRootViewGlobalLayoutListener();
if (mPreferences.isTerminalMarginAdjustmentEnabled())
addTermuxActivityRootViewGlobalLayoutListener();
registerTermuxActivityBroadcastReceiver();
}
@@ -352,7 +365,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
Bundle bundle = getIntent().getExtras();
boolean launchFailsafe = false;
if (bundle != null) {
launchFailsafe = bundle.getBoolean(TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION, false);
launchFailsafe = bundle.getBoolean(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false);
}
mTermuxTerminalSessionClient.addNewSession(launchFailsafe, null);
} catch (WindowManager.BadTokenException e) {
@@ -367,7 +380,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
Intent i = getIntent();
if (i != null && Intent.ACTION_RUN.equals(i.getAction())) {
// Android 7.1 app shortcut from res/xml/shortcuts.xml.
boolean isFailSafe = i.getBooleanExtra(TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION, false);
boolean isFailSafe = i.getBooleanExtra(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false);
mTermuxTerminalSessionClient.addNewSession(isFailSafe, null);
} else {
mTermuxTerminalSessionClient.setCurrentSession(mTermuxTerminalSessionClient.getCurrentStoredSessionOrLast());
@@ -403,9 +416,17 @@ public final class TermuxActivity extends Activity implements ServiceConnection
if (mProperties.isUsingBlackUI()) {
findViewById(R.id.left_drawer).setBackgroundColor(ContextCompat.getColor(this,
android.R.color.background_dark));
((ImageButton) findViewById(R.id.settings_button)).setColorFilter(Color.WHITE);
}
}
private void setMargins() {
RelativeLayout relativeLayout = findViewById(R.id.activity_termux_root_relative_layout);
int marginHorizontal = mProperties.getTerminalMarginHorizontal();
int marginVertical = mProperties.getTerminalMarginVertical();
ViewUtils.setLayoutMarginsInDp(relativeLayout, marginHorizontal, marginVertical, marginHorizontal, marginVertical);
}
public void addTermuxActivityRootViewGlobalLayoutListener() {
@@ -446,7 +467,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
private void setTerminalToolbarView(Bundle savedInstanceState) {
final ViewPager terminalToolbarViewPager = findViewById(R.id.terminal_toolbar_view_pager);
final ViewPager terminalToolbarViewPager = getTerminalToolbarViewPager();
if (mPreferences.shouldShowTerminalToolbar()) terminalToolbarViewPager.setVisibility(View.VISIBLE);
ViewGroup.LayoutParams layoutParams = terminalToolbarViewPager.getLayoutParams();
@@ -463,8 +484,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
}
private void setTerminalToolbarHeight() {
final ViewPager terminalToolbarViewPager = findViewById(R.id.terminal_toolbar_view_pager);
final ViewPager terminalToolbarViewPager = getTerminalToolbarViewPager();
if (terminalToolbarViewPager == null) return;
ViewGroup.LayoutParams layoutParams = terminalToolbarViewPager.getLayoutParams();
layoutParams.height = (int) Math.round(mTerminalToolbarDefaultHeight *
(mProperties.getExtraKeysInfo() == null ? 0 : mProperties.getExtraKeysInfo().getMatrix().length) *
@@ -473,13 +495,13 @@ public final class TermuxActivity extends Activity implements ServiceConnection
}
public void toggleTerminalToolbar() {
final ViewPager terminalToolbarViewPager = findViewById(R.id.terminal_toolbar_view_pager);
final ViewPager terminalToolbarViewPager = getTerminalToolbarViewPager();
if (terminalToolbarViewPager == null) return;
final boolean showNow = mPreferences.toogleShowTerminalToolbar();
Logger.showToast(this, (showNow ? getString(R.string.msg_enabling_terminal_toolbar) : getString(R.string.msg_disabling_terminal_toolbar)), true);
terminalToolbarViewPager.setVisibility(showNow ? View.VISIBLE : View.GONE);
if (showNow && terminalToolbarViewPager.getCurrentItem() == 1) {
if (showNow && isTerminalToolbarTextInputViewSelected()) {
// Focus the text input view if just revealed.
findViewById(R.id.terminal_toolbar_text_input).requestFocus();
}
@@ -497,11 +519,18 @@ public final class TermuxActivity extends Activity implements ServiceConnection
private void setSettingsButtonView() {
ImageButton settingsButton = findViewById(R.id.settings_button);
settingsButton.setOnClickListener(v -> {
startActivity(new Intent(this, SettingsActivity.class));
});
}
private void setNewSessionButtonView() {
View newSessionButton = findViewById(R.id.new_session_button);
newSessionButton.setOnClickListener(v -> mTermuxTerminalSessionClient.addNewSession(false, null));
newSessionButton.setOnLongClickListener(v -> {
DialogUtils.textInput(TermuxActivity.this, R.string.title_create_named_session, null,
TextInputDialogUtils.textInput(TermuxActivity.this, R.string.title_create_named_session, null,
R.string.action_create_named_session_confirm, text -> mTermuxTerminalSessionClient.addNewSession(false, text),
R.string.action_new_session_failsafe, text -> mTermuxTerminalSessionClient.addNewSession(true, text),
-1, null, null);
@@ -600,7 +629,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
requestAutoFill();
return true;
case CONTEXT_MENU_RESET_TERMINAL_ID:
resetSession(session);
onResetTerminalSession(session);
return true;
case CONTEXT_MENU_KILL_PROCESS_ID:
showKillSessionDialog(session);
@@ -639,10 +668,13 @@ public final class TermuxActivity extends Activity implements ServiceConnection
b.show();
}
private void resetSession(TerminalSession session) {
private void onResetTerminalSession(TerminalSession session) {
if (session != null) {
session.reset();
showToast(getResources().getString(R.string.msg_terminal_reset), true);
if (mTermuxTerminalSessionClient != null)
mTermuxTerminalSessionClient.onResetTerminalSession();
}
}
@@ -683,22 +715,22 @@ public final class TermuxActivity extends Activity implements ServiceConnection
* For processes to access shared internal storage (/sdcard) we need this permission.
*/
public boolean ensureStoragePermissionGranted() {
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
if (PermissionUtils.checkPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
return true;
} else {
Logger.logDebug(LOG_TAG, "Storage permission not granted, requesting permission.");
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUESTCODE_PERMISSION_STORAGE);
Logger.logInfo(LOG_TAG, "Storage permission not granted, requesting permission.");
PermissionUtils.requestPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE, PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION);
return false;
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == REQUESTCODE_PERMISSION_STORAGE && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Logger.logDebug(LOG_TAG, "Storage permission granted by user on request.");
if (requestCode == PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Logger.logInfo(LOG_TAG, "Storage permission granted by user on request.");
TermuxInstaller.setupStorageSymlinks(this);
} else {
Logger.logDebug(LOG_TAG, "Storage permission denied by user on request.");
Logger.logInfo(LOG_TAG, "Storage permission denied by user on request.");
}
}
@@ -728,6 +760,20 @@ public final class TermuxActivity extends Activity implements ServiceConnection
return (DrawerLayout) findViewById(R.id.drawer_layout);
}
public ViewPager getTerminalToolbarViewPager() {
return (ViewPager) findViewById(R.id.terminal_toolbar_view_pager);
}
public boolean isTerminalViewSelected() {
return getTerminalToolbarViewPager().getCurrentItem() == 0;
}
public boolean isTerminalToolbarTextInputViewSelected() {
return getTerminalToolbarViewPager().getCurrentItem() == 1;
}
public void termuxSessionListNotifyUpdated() {
mTermuxSessionListViewController.notifyDataSetChanged();
}
@@ -834,10 +880,12 @@ public final class TermuxActivity extends Activity implements ServiceConnection
mProperties.loadTermuxPropertiesFromDisk();
if (mExtraKeysView != null) {
mExtraKeysView.setButtonTextAllCaps(mProperties.shouldExtraKeysTextBeAllCaps());
mExtraKeysView.reload(mProperties.getExtraKeysInfo());
}
}
setMargins();
setTerminalToolbarHeight();
if (mTermuxTerminalSessionClient != null)

View File

@@ -2,7 +2,7 @@ package com.termux.app;
import android.app.Application;
import com.termux.shared.crash.CrashHandler;
import com.termux.shared.crash.TermuxCrashUtils;
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
import com.termux.shared.logger.Logger;
@@ -12,7 +12,7 @@ public class TermuxApplication extends Application {
super.onCreate();
// Set crash handler for the app
CrashHandler.setCrashHandler(this);
TermuxCrashUtils.setCrashHandler(this);
// Set log level for the app
setLogLevel();

View File

@@ -5,16 +5,21 @@ import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.Environment;
import android.os.UserManager;
import android.system.Os;
import android.util.Pair;
import android.view.WindowManager;
import com.termux.R;
import com.termux.app.utils.CrashUtils;
import com.termux.shared.file.FileUtils;
import com.termux.shared.interact.DialogUtils;
import com.termux.shared.file.TermuxFileUtils;
import com.termux.shared.interact.MessageDialogUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.models.errors.Error;
import com.termux.shared.packages.PackageUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxUtils;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
@@ -26,6 +31,11 @@ import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import static com.termux.shared.termux.TermuxConstants.TERMUX_PREFIX_DIR;
import static com.termux.shared.termux.TermuxConstants.TERMUX_PREFIX_DIR_PATH;
import static com.termux.shared.termux.TermuxConstants.TERMUX_STAGING_PREFIX_DIR;
import static com.termux.shared.termux.TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH;
/**
* Install the Termux bootstrap packages if necessary by following the below steps:
* <p/>
@@ -51,34 +61,49 @@ final class TermuxInstaller {
/** Performs bootstrap setup if necessary. */
static void setupBootstrapIfNeeded(final Activity activity, final Runnable whenDone) {
String bootstrapErrorMessage;
Error filesDirectoryAccessibleError;
// This will also call Context.getFilesDir(), which should ensure that termux files directory
// is created if it does not already exist
filesDirectoryAccessibleError = TermuxFileUtils.isTermuxFilesDirectoryAccessible(activity, true, true);
boolean isFilesDirectoryAccessible = filesDirectoryAccessibleError == null;
// 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) {
String bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message, TermuxConstants.TERMUX_PREFIX_DIR_PATH);
if (!PackageUtils.isCurrentUserThePrimaryUser(activity)) {
bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message, MarkdownUtils.getMarkdownCodeForString(TERMUX_PREFIX_DIR_PATH, false));
Logger.logError(LOG_TAG, "isFilesDirectoryAccessible: " + isFilesDirectoryAccessible);
Logger.logError(LOG_TAG, bootstrapErrorMessage);
DialogUtils.exitAppWithErrorMessage(activity,
sendBootstrapCrashReportNotification(activity, bootstrapErrorMessage);
MessageDialogUtils.exitAppWithErrorMessage(activity,
activity.getString(R.string.bootstrap_error_title),
bootstrapErrorMessage);
return;
}
final String PREFIX_FILE_PATH = TermuxConstants.TERMUX_PREFIX_DIR_PATH;
final File PREFIX_FILE = TermuxConstants.TERMUX_PREFIX_DIR;
if (!isFilesDirectoryAccessible) {
bootstrapErrorMessage = Error.getMinimalErrorString(filesDirectoryAccessibleError) + "\nTERMUX_FILES_DIR: " + MarkdownUtils.getMarkdownCodeForString(TermuxConstants.TERMUX_FILES_DIR_PATH, false);
Logger.logError(LOG_TAG, bootstrapErrorMessage);
sendBootstrapCrashReportNotification(activity, bootstrapErrorMessage);
MessageDialogUtils.showMessage(activity,
activity.getString(R.string.bootstrap_error_title),
bootstrapErrorMessage, null);
return;
}
// 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 (FileUtils.directoryFileExists(TERMUX_PREFIX_DIR_PATH, true)) {
File[] PREFIX_FILE_LIST = TERMUX_PREFIX_DIR.listFiles();
// If prefix directory is empty or only contains the tmp directory
if(PREFIX_FILE_LIST == null || PREFIX_FILE_LIST.length == 0 || (PREFIX_FILE_LIST.length == 1 && TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH.equals(PREFIX_FILE_LIST[0].getAbsolutePath()))) {
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.");
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 termux prefix directory \"" + TERMUX_PREFIX_DIR_PATH + "\" exists but is empty or only contains the tmp directory.");
} else {
whenDone.run();
return;
}
} else if (FileUtils.fileExists(TERMUX_PREFIX_DIR_PATH, false)) {
Logger.logInfo(LOG_TAG, "The termux prefix directory \"" + TERMUX_PREFIX_DIR_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);
@@ -88,24 +113,37 @@ final class TermuxInstaller {
try {
Logger.logInfo(LOG_TAG, "Installing " + TermuxConstants.TERMUX_APP_NAME + " bootstrap packages.");
String errmsg;
final String STAGING_PREFIX_PATH = TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH;
final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH);
Error error;
// Delete prefix staging directory or any file at its destination
errmsg = FileUtils.deleteFile(activity, "prefix staging directory", STAGING_PREFIX_PATH, true);
if (errmsg != null) {
throw new RuntimeException(errmsg);
error = FileUtils.deleteFile("termux prefix staging directory", TERMUX_STAGING_PREFIX_DIR_PATH, true);
if (error != null) {
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
return;
}
// Delete prefix directory or any file at its destination
errmsg = FileUtils.deleteFile(activity, "prefix directory", PREFIX_FILE_PATH, true);
if (errmsg != null) {
throw new RuntimeException(errmsg);
error = FileUtils.deleteFile("termux prefix directory", TERMUX_PREFIX_DIR_PATH, true);
if (error != null) {
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
return;
}
Logger.logInfo(LOG_TAG, "Extracting bootstrap zip to prefix staging directory \"" + STAGING_PREFIX_PATH + "\".");
// Create prefix staging directory if it does not already exist and set required permissions
error = TermuxFileUtils.isTermuxPrefixStagingDirectoryAccessible(true, true);
if (error != null) {
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
return;
}
// Create prefix directory if it does not already exist and set required permissions
error = TermuxFileUtils.isTermuxPrefixDirectoryAccessible(true, true);
if (error != null) {
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
return;
}
Logger.logInfo(LOG_TAG, "Extracting bootstrap zip to prefix staging directory \"" + TERMUX_STAGING_PREFIX_DIR_PATH + "\".");
final byte[] buffer = new byte[8096];
final List<Pair<String, String>> symlinks = new ArrayList<>(50);
@@ -122,17 +160,25 @@ final class TermuxInstaller {
if (parts.length != 2)
throw new RuntimeException("Malformed symlink line: " + line);
String oldPath = parts[0];
String newPath = STAGING_PREFIX_PATH + "/" + parts[1];
String newPath = TERMUX_STAGING_PREFIX_DIR_PATH + "/" + parts[1];
symlinks.add(Pair.create(oldPath, newPath));
ensureDirectoryExists(activity, new File(newPath).getParentFile());
error = ensureDirectoryExists(new File(newPath).getParentFile());
if (error != null) {
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
return;
}
}
} else {
String zipEntryName = zipEntry.getName();
File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName);
File targetFile = new File(TERMUX_STAGING_PREFIX_DIR_PATH, zipEntryName);
boolean isDirectory = zipEntry.isDirectory();
ensureDirectoryExists(activity, isDirectory ? targetFile : targetFile.getParentFile());
error = ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile());
if (error != null) {
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
return;
}
if (!isDirectory) {
try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
@@ -140,7 +186,8 @@ final class TermuxInstaller {
while ((readBytes = zipInput.read(buffer)) != -1)
outStream.write(buffer, 0, readBytes);
}
if (zipEntryName.startsWith("bin/") || zipEntryName.startsWith("libexec") || zipEntryName.startsWith("lib/apt/methods")) {
if (zipEntryName.startsWith("bin/") || zipEntryName.startsWith("libexec") ||
zipEntryName.startsWith("lib/apt/apt-helper") || zipEntryName.startsWith("lib/apt/methods")) {
//noinspection OctalInteger
Os.chmod(targetFile.getAbsolutePath(), 0700);
}
@@ -155,31 +202,18 @@ final class TermuxInstaller {
Os.symlink(symlink.first, symlink.second);
}
Logger.logInfo(LOG_TAG, "Moving prefix staging to prefix directory.");
Logger.logInfo(LOG_TAG, "Moving termux prefix staging to prefix directory.");
if (!STAGING_PREFIX_FILE.renameTo(PREFIX_FILE)) {
throw new RuntimeException("Moving prefix staging to prefix directory failed");
if (!TERMUX_STAGING_PREFIX_DIR.renameTo(TERMUX_PREFIX_DIR)) {
throw new RuntimeException("Moving termux prefix staging to prefix directory failed");
}
Logger.logInfo(LOG_TAG, "Bootstrap packages installed successfully.");
activity.runOnUiThread(whenDone);
} catch (final Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Bootstrap error", e);
activity.runOnUiThread(() -> {
try {
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
.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(activity, "prefix directory", PREFIX_FILE_PATH, true);
TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone);
}).show();
} catch (WindowManager.BadTokenException e1) {
// Activity already dismissed - ignore.
}
});
showBootstrapErrorDialog(activity, whenDone, Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)));
} finally {
activity.runOnUiThread(() -> {
try {
@@ -193,6 +227,37 @@ final class TermuxInstaller {
}.start();
}
public static void showBootstrapErrorDialog(Activity activity, Runnable whenDone, String message) {
Logger.logErrorExtended(LOG_TAG, "Bootstrap Error:\n" + message);
// Send a notification with the exception so that the user knows why bootstrap setup failed
sendBootstrapCrashReportNotification(activity, message);
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("termux prefix directory", TERMUX_PREFIX_DIR_PATH, true);
TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone);
}).show();
} catch (WindowManager.BadTokenException e1) {
// Activity already dismissed - ignore.
}
});
}
private static void sendBootstrapCrashReportNotification(Activity activity, String message) {
CrashUtils.sendCrashReportNotification(activity, LOG_TAG,
"## Bootstrap Error\n\n" + message + "\n\n" +
TermuxUtils.getTermuxDebugMarkdownString(activity),
true, true);
}
static void setupStorageSymlinks(final Context context) {
final String LOG_TAG = "termux-storage";
@@ -201,12 +266,14 @@ final class TermuxInstaller {
new Thread() {
public void run() {
try {
String errmsg;
Error error;
File storageDir = TermuxConstants.TERMUX_STORAGE_HOME_DIR;
errmsg = FileUtils.clearDirectory(context, "~/storage", storageDir.getAbsolutePath());
if (errmsg != null) {
Logger.logErrorAndShowToast(context, LOG_TAG, errmsg);
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, true);
return;
}
@@ -243,19 +310,16 @@ final class TermuxInstaller {
Logger.logInfo(LOG_TAG, "Storage symlinks created successfully.");
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Error setting up link", e);
Logger.logErrorAndShowToast(context, LOG_TAG, e.getMessage());
Logger.logStackTraceWithMessage(LOG_TAG, "Setup Storage Error: Error setting up link", e);
CrashUtils.sendCrashReportNotification(context, LOG_TAG, "## Setup Storage Error\n\n" + Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)), true, true);
}
}
}.start();
}
private static void ensureDirectoryExists(Context context, File directory) {
String errmsg;
errmsg = FileUtils.createDirectoryFile(context, directory.getAbsolutePath());
if (errmsg != null) {
throw new RuntimeException(errmsg);
}
private static Error ensureDirectoryExists(File directory) {
return FileUtils.createDirectoryFile(directory.getAbsolutePath());
}
public static byte[] loadZipBytes() {

View File

@@ -13,6 +13,8 @@ import android.os.ParcelFileDescriptor;
import android.provider.MediaStore;
import android.webkit.MimeTypeMap;
import com.termux.app.utils.PluginUtils;
import com.termux.shared.data.IntentUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.termux.TermuxConstants;
@@ -34,6 +36,8 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
return;
}
Logger.logVerbose(LOG_TAG, "Intent Received:\n" + IntentUtils.getIntentString(intent));
final String filePath = data.getPath();
final String contentTypeExtra = intent.getStringExtra("content-type");
final boolean useChooser = intent.getBooleanExtra("chooser", false);
@@ -111,6 +115,8 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
public static class ContentProvider extends android.content.ContentProvider {
private static final String LOG_TAG = "TermuxContentProvider";
@Override
public boolean onCreate() {
return true;
@@ -178,15 +184,33 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
File file = new File(uri.getPath());
try {
String path = file.getCanonicalPath();
Logger.logDebug(LOG_TAG, "Open file request received for \"" + path + "\" with mode \"" + mode + "\"");
String storagePath = Environment.getExternalStorageDirectory().getCanonicalPath();
// See https://support.google.com/faqs/answer/7496913:
if (!(path.startsWith(TermuxConstants.TERMUX_FILES_DIR_PATH) || path.startsWith(storagePath))) {
throw new IllegalArgumentException("Invalid path: " + path);
}
// If "allow-external-apps" property to not set to "true", then throw exception
String errmsg = PluginUtils.checkIfAllowExternalAppsPolicyIsViolated(getContext(), LOG_TAG);
if (errmsg != null) {
throw new IllegalArgumentException(errmsg);
}
// Do not allow apps with RUN_COMMAND permission to modify termux apps properties files,
// including allow-external-apps
if (TermuxConstants.TERMUX_PROPERTIES_PRIMARY_FILE_PATH.equals(path) ||
TermuxConstants.TERMUX_PROPERTIES_SECONDARY_FILE_PATH.equals(path) ||
TermuxConstants.TERMUX_FLOAT_PROPERTIES_PRIMARY_FILE_PATH.equals(path) ||
TermuxConstants.TERMUX_FLOAT_PROPERTIES_SECONDARY_FILE_PATH.equals(path)) {
mode = "r";
}
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode(mode));
}
}

View File

@@ -19,10 +19,17 @@ import android.os.PowerManager;
import android.provider.Settings;
import android.widget.ArrayAdapter;
import androidx.annotation.Nullable;
import com.termux.R;
import com.termux.app.settings.properties.TermuxAppSharedProperties;
import com.termux.app.terminal.TermuxTerminalSessionClient;
import com.termux.app.utils.PluginUtils;
import com.termux.shared.data.IntentUtils;
import com.termux.shared.models.errors.Errno;
import com.termux.shared.shell.ShellUtils;
import com.termux.shared.shell.TermuxShellEnvironmentClient;
import com.termux.shared.shell.TermuxShellUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
@@ -32,7 +39,6 @@ import com.termux.shared.terminal.TermuxTerminalSessionClientBase;
import com.termux.shared.logger.Logger;
import com.termux.shared.notification.NotificationUtils;
import com.termux.shared.packages.PermissionUtils;
import com.termux.shared.shell.ShellUtils;
import com.termux.shared.data.DataUtils;
import com.termux.shared.models.ExecutionCommand;
import com.termux.shared.shell.TermuxTask;
@@ -43,8 +49,6 @@ import com.termux.terminal.TerminalSessionClient;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
/**
* A service holding a list of {@link TermuxSession} in {@link #mTermuxSessions} and background {@link TermuxTask}
* in {@link #mTermuxTasks}, showing a foreground notification while running so that it is not terminated.
@@ -160,7 +164,7 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
public void onDestroy() {
Logger.logVerbose(LOG_TAG, "onDestroy");
ShellUtils.clearTermuxTMPDIR(this, true);
TermuxShellUtils.clearTermuxTMPDIR(true);
actionReleaseWakeLock(false);
if (!mWantsToStop)
@@ -254,22 +258,22 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
List<TermuxSession> termuxSessions = new ArrayList<>(mTermuxSessions);
for (int i = 0; i < termuxSessions.size(); i++) {
ExecutionCommand executionCommand = termuxSessions.get(i).getExecutionCommand();
processResult = mWantsToStop || (executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null);
processResult = mWantsToStop || executionCommand.isPluginExecutionCommandWithPendingResult();
termuxSessions.get(i).killIfExecuting(this, processResult);
}
List<TermuxTask> termuxTasks = new ArrayList<>(mTermuxTasks);
for (int i = 0; i < termuxTasks.size(); i++) {
ExecutionCommand executionCommand = termuxTasks.get(i).getExecutionCommand();
if (executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null)
if (executionCommand.isPluginExecutionCommandWithPendingResult())
termuxTasks.get(i).killIfExecuting(this, true);
}
List<ExecutionCommand> pendingPluginExecutionCommands = new ArrayList<>(mPendingPluginExecutionCommands);
for (int i = 0; i < pendingPluginExecutionCommands.size(); i++) {
ExecutionCommand executionCommand = pendingPluginExecutionCommands.get(i);
if (!executionCommand.shouldNotProcessResults() && executionCommand.pluginPendingIntent != null) {
if (executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_CANCELED, this.getString(com.termux.shared.R.string.error_execution_cancelled), null)) {
if (!executionCommand.shouldNotProcessResults() && executionCommand.isPluginExecutionCommandWithPendingResult()) {
if (executionCommand.setStateFailed(Errno.ERRNO_CANCELLED.getCode(), this.getString(com.termux.shared.R.string.error_execution_cancelled))) {
PluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
}
}
@@ -357,20 +361,29 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
if (executionCommand.executableUri != null) {
executionCommand.executable = executionCommand.executableUri.getPath();
executionCommand.arguments = intent.getStringArrayExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS);
executionCommand.arguments = IntentUtils.getStringArrayExtraIfSet(intent, TERMUX_SERVICE.EXTRA_ARGUMENTS, null);
if (executionCommand.inBackground)
executionCommand.stdin = intent.getStringExtra(TERMUX_SERVICE.EXTRA_STDIN);
executionCommand.stdin = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_STDIN, null);
executionCommand.backgroundCustomLogLevel = IntentUtils.getIntegerExtraIfSet(intent, TERMUX_SERVICE.EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL, null);
}
executionCommand.workingDirectory = intent.getStringExtra(TERMUX_SERVICE.EXTRA_WORKDIR);
executionCommand.isFailsafe = intent.getBooleanExtra(TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION, false);
executionCommand.workingDirectory = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_WORKDIR, null);
executionCommand.isFailsafe = intent.getBooleanExtra(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false);
executionCommand.sessionAction = intent.getStringExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION);
executionCommand.commandLabel = DataUtils.getDefaultIfNull(intent.getStringExtra(TERMUX_SERVICE.EXTRA_COMMAND_LABEL), "Execution Intent Command");
executionCommand.commandDescription = intent.getStringExtra(TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION);
executionCommand.commandHelp = intent.getStringExtra(TERMUX_SERVICE.EXTRA_COMMAND_HELP);
executionCommand.pluginAPIHelp = intent.getStringExtra(TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP);
executionCommand.commandLabel = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_LABEL, "Execution Intent Command");
executionCommand.commandDescription = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, null);
executionCommand.commandHelp = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_HELP, null);
executionCommand.pluginAPIHelp = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP, null);
executionCommand.isPluginExecutionCommand = true;
executionCommand.pluginPendingIntent = intent.getParcelableExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT);
executionCommand.resultConfig.resultPendingIntent = intent.getParcelableExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT);
executionCommand.resultConfig.resultDirectoryPath = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_DIRECTORY, null);
if (executionCommand.resultConfig.resultDirectoryPath != null) {
executionCommand.resultConfig.resultSingleFile = intent.getBooleanExtra(TERMUX_SERVICE.EXTRA_RESULT_SINGLE_FILE, false);
executionCommand.resultConfig.resultFileBasename = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILE_BASENAME, null);
executionCommand.resultConfig.resultFileOutputFormat = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, null);
executionCommand.resultConfig.resultFileErrorFormat = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, null);
executionCommand.resultConfig.resultFilesSuffix = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILES_SUFFIX, null);
}
// Add the execution command to pending plugin execution commands list
mPendingPluginExecutionCommands.add(executionCommand);
@@ -414,11 +427,16 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
}
if (Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE)
Logger.logVerbose(LOG_TAG, executionCommand.toString());
Logger.logVerboseExtended(LOG_TAG, executionCommand.toString());
TermuxTask newTermuxTask = TermuxTask.execute(this, executionCommand, this, false);
TermuxTask newTermuxTask = TermuxTask.execute(this, executionCommand, this, new TermuxShellEnvironmentClient(), false);
if (newTermuxTask == null) {
Logger.logError(LOG_TAG, "Failed to execute new TermuxTask command for:\n" + executionCommand.getCommandIdAndLabelLogString());
// If the execution command was started for a plugin, then process the error
if (executionCommand.isPluginExecutionCommand)
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
else
Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
return null;
}
@@ -501,15 +519,20 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
}
if (Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE)
Logger.logVerbose(LOG_TAG, executionCommand.toString());
Logger.logVerboseExtended(LOG_TAG, executionCommand.toString());
// If the execution command was started for a plugin, only then will the stdout be set
// Otherwise if command was manually started by the user like by adding a new terminal session,
// then no need to set stdout
executionCommand.terminalTranscriptRows = getTerminalTranscriptRows();
TermuxSession newTermuxSession = TermuxSession.execute(this, executionCommand, getTermuxTerminalSessionClient(), this, sessionName, executionCommand.isPluginExecutionCommand);
TermuxSession newTermuxSession = TermuxSession.execute(this, executionCommand, getTermuxTerminalSessionClient(), this, new TermuxShellEnvironmentClient(), sessionName, executionCommand.isPluginExecutionCommand);
if (newTermuxSession == null) {
Logger.logError(LOG_TAG, "Failed to execute new TermuxSession command for:\n" + executionCommand.getCommandIdAndLabelLogString());
// If the execution command was started for a plugin, then process the error
if (executionCommand.isPluginExecutionCommand)
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
else
Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
return null;
}
@@ -618,8 +641,13 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
// For android >= 10, apps require Display over other apps permission to start foreground activities
// from background (services). If it is not granted, then TermuxSessions that are started will
// show in Termux notification but will not run until user manually clicks the notification.
if (PermissionUtils.validateDisplayOverOtherAppsPermissionForPostAndroid10(this)) {
if (PermissionUtils.validateDisplayOverOtherAppsPermissionForPostAndroid10(this, true)) {
TermuxActivity.startTermuxActivity(this);
} else {
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(this);
if (preferences == null) return;
if (preferences.arePluginErrorNotificationsEnabled())
Logger.showToast(this, this.getString(R.string.error_display_over_other_apps_permission_not_granted), true);
}
}
@@ -680,7 +708,7 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
// Set pending intent to be launched when notification is clicked
Intent notificationIntent = TermuxActivity.newInstance(this);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
// Set notification text
@@ -704,8 +732,8 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
// Build the notification
Notification.Builder builder = NotificationUtils.geNotificationBuilder(this,
TermuxConstants.TERMUX_APP_NOTIFICATION_CHANNEL_ID, priority,
getText(R.string.application_name), notificationText, null,
pendingIntent, NotificationUtils.NOTIFICATION_MODE_SILENT);
TermuxConstants.TERMUX_APP_NAME, notificationText, null,
contentIntent, null, NotificationUtils.NOTIFICATION_MODE_SILENT);
if (builder == null) return null;
// No need to show a timestamp:

View File

@@ -1,180 +0,0 @@
package com.termux.app.activities;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import com.termux.R;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.interact.ShareUtils;
import com.termux.app.models.ReportInfo;
import org.commonmark.node.FencedCodeBlock;
import io.noties.markwon.Markwon;
import io.noties.markwon.recycler.MarkwonAdapter;
import io.noties.markwon.recycler.SimpleEntry;
public class ReportActivity extends AppCompatActivity {
private static final String EXTRA_REPORT_INFO = "report_info";
ReportInfo mReportInfo;
String mReportMarkdownString;
String mReportActivityMarkdownString;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_report);
Toolbar toolbar = findViewById(R.id.toolbar);
if (toolbar != null) {
setSupportActionBar(toolbar);
}
Bundle bundle = null;
Intent intent = getIntent();
if (intent != null)
bundle = intent.getExtras();
else if (savedInstanceState != null)
bundle = savedInstanceState;
updateUI(bundle);
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
if (intent != null)
updateUI(intent.getExtras());
}
private void updateUI(Bundle bundle) {
if (bundle == null) {
finish();
return;
}
mReportInfo = (ReportInfo) bundle.getSerializable(EXTRA_REPORT_INFO);
if (mReportInfo == null) {
finish();
return;
}
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
if (mReportInfo.reportTitle != null)
actionBar.setTitle(mReportInfo.reportTitle);
else
actionBar.setTitle(TermuxConstants.TERMUX_APP_NAME + " App Report");
}
RecyclerView recyclerView = findViewById(R.id.recycler_view);
final Markwon markwon = MarkdownUtils.getRecyclerMarkwonBuilder(this);
final MarkwonAdapter adapter = MarkwonAdapter.builderTextViewIsRoot(R.layout.markdown_adapter_node_default)
.include(FencedCodeBlock.class, SimpleEntry.create(R.layout.markdown_adapter_node_code_block, R.id.code_text_view))
.build();
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(adapter);
generateReportActivityMarkdownString();
adapter.setMarkdown(markwon, mReportActivityMarkdownString);
adapter.notifyDataSetChanged();
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable(EXTRA_REPORT_INFO, mReportInfo);
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
final MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_report, menu);
return true;
}
@Override
public void onBackPressed() {
// Remove activity from recents menu on back button press
finishAndRemoveTask();
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
int id = item.getItemId();
if (id == R.id.menu_item_share_report) {
if (mReportMarkdownString != null)
ShareUtils.shareText(this, getString(R.string.title_report_text), mReportMarkdownString);
} else if (id == R.id.menu_item_copy_report) {
if (mReportMarkdownString != null)
ShareUtils.copyTextToClipboard(this, mReportMarkdownString, null);
}
return false;
}
/**
* Generate the markdown {@link String} to be shown in {@link ReportActivity}.
*/
private void generateReportActivityMarkdownString() {
mReportMarkdownString = ReportInfo.getReportInfoMarkdownString(mReportInfo);
mReportActivityMarkdownString = "";
if (mReportInfo.reportStringPrefix != null)
mReportActivityMarkdownString += mReportInfo.reportStringPrefix;
mReportActivityMarkdownString += mReportMarkdownString;
if (mReportInfo.reportStringSuffix != null)
mReportActivityMarkdownString += mReportInfo.reportStringSuffix;
}
public static void startReportActivity(@NonNull final Context context, @NonNull final ReportInfo reportInfo) {
context.startActivity(newInstance(context, reportInfo));
}
public static Intent newInstance(@NonNull final Context context, @NonNull final ReportInfo reportInfo) {
Intent intent = new Intent(context, ReportActivity.class);
Bundle bundle = new Bundle();
bundle.putSerializable(EXTRA_REPORT_INFO, reportInfo);
intent.putExtras(bundle);
// Note that ReportActivity task has documentLaunchMode="intoExisting" set in AndroidManifest.xml
// which has equivalent behaviour to the following. The following dynamic way doesn't seem to
// work for notification pending intent, i.e separate task isn't created and activity is
// launched in the same task as TermuxActivity.
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
return intent;
}
}

View File

@@ -2,6 +2,7 @@ package com.termux.app.activities;
import android.content.Context;
import android.os.Bundle;
import android.os.Environment;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
@@ -10,11 +11,17 @@ import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import com.termux.R;
import com.termux.app.models.ReportInfo;
import com.termux.shared.activities.ReportActivity;
import com.termux.shared.file.FileUtils;
import com.termux.shared.models.ReportInfo;
import com.termux.app.models.UserAction;
import com.termux.shared.interact.ShareUtils;
import com.termux.shared.packages.PackageUtils;
import com.termux.shared.settings.preferences.TermuxAPIAppSharedPreferences;
import com.termux.shared.settings.preferences.TermuxFloatAppSharedPreferences;
import com.termux.shared.settings.preferences.TermuxTaskerAppSharedPreferences;
import com.termux.shared.settings.preferences.TermuxWidgetAppSharedPreferences;
import com.termux.shared.termux.AndroidUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxUtils;
@@ -51,17 +58,47 @@ public class SettingsActivity extends AppCompatActivity {
setPreferencesFromResource(R.xml.root_preferences, rootKey);
configureTermuxAPIPreference(context);
configureTermuxFloatPreference(context);
configureTermuxTaskerPreference(context);
configureTermuxWidgetPreference(context);
configureAboutPreference(context);
configureDonatePreference(context);
}
private void configureTermuxAPIPreference(@NonNull Context context) {
Preference termuxAPIPreference = findPreference("termux_api");
if (termuxAPIPreference != null) {
TermuxAPIAppSharedPreferences preferences = TermuxAPIAppSharedPreferences.build(context, false);
// If failed to get app preferences, then likely app is not installed, so do not show its preference
termuxAPIPreference.setVisible(preferences != null);
}
}
private void configureTermuxFloatPreference(@NonNull Context context) {
Preference termuxFloatPreference = findPreference("termux_float");
if (termuxFloatPreference != null) {
TermuxFloatAppSharedPreferences preferences = TermuxFloatAppSharedPreferences.build(context, false);
// If failed to get app preferences, then likely app is not installed, so do not show its preference
termuxFloatPreference.setVisible(preferences != null);
}
}
private void configureTermuxTaskerPreference(@NonNull Context context) {
Preference termuxTaskerPrefernce = findPreference("termux_tasker");
if (termuxTaskerPrefernce != null) {
Preference termuxTaskerPreference = findPreference("termux_tasker");
if (termuxTaskerPreference != null) {
TermuxTaskerAppSharedPreferences preferences = TermuxTaskerAppSharedPreferences.build(context, false);
// If failed to get app preferences, then likely app is not installed, so do not show its preference
termuxTaskerPrefernce.setVisible(preferences != null);
termuxTaskerPreference.setVisible(preferences != null);
}
}
private void configureTermuxWidgetPreference(@NonNull Context context) {
Preference termuxWidgetPreference = findPreference("termux_widget");
if (termuxWidgetPreference != null) {
TermuxWidgetAppSharedPreferences preferences = TermuxWidgetAppSharedPreferences.build(context, false);
// If failed to get app preferences, then likely app is not installed, so do not show its preference
termuxWidgetPreference.setVisible(preferences != null);
}
}
@@ -81,10 +118,16 @@ public class SettingsActivity extends AppCompatActivity {
if (termuxPluginAppsInfo != null)
aboutString.append("\n\n").append(termuxPluginAppsInfo);
aboutString.append("\n\n").append(TermuxUtils.getDeviceInfoMarkdownString(context));
aboutString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context));
aboutString.append("\n\n").append(TermuxUtils.getImportantLinksMarkdownString(context));
ReportActivity.startReportActivity(context, new ReportInfo(UserAction.ABOUT, TermuxConstants.TERMUX_APP.TERMUX_SETTINGS_ACTIVITY_NAME, title, null, aboutString.toString(), null, false));
String userActionName = UserAction.ABOUT.getName();
ReportActivity.startReportActivity(context, new ReportInfo(userActionName,
TermuxConstants.TERMUX_APP.TERMUX_SETTINGS_ACTIVITY_NAME, title, null,
aboutString.toString(), null, false,
userActionName,
Environment.getExternalStorageDirectory() + "/" +
FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true)));
}
}.start();

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.TermuxAPIAppSharedPreferences;
@Keep
public class TermuxAPIPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(TermuxAPIPreferencesDataStore.getInstance(context));
setPreferencesFromResource(R.xml.termux_api_preferences, rootKey);
}
}
class TermuxAPIPreferencesDataStore extends PreferenceDataStore {
private final Context mContext;
private final TermuxAPIAppSharedPreferences mPreferences;
private static TermuxAPIPreferencesDataStore mInstance;
private TermuxAPIPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxAPIAppSharedPreferences.build(context, true);
}
public static synchronized TermuxAPIPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new TermuxAPIPreferencesDataStore(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.TermuxFloatAppSharedPreferences;
@Keep
public class TermuxFloatPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(TermuxFloatPreferencesDataStore.getInstance(context));
setPreferencesFromResource(R.xml.termux_float_preferences, rootKey);
}
}
class TermuxFloatPreferencesDataStore extends PreferenceDataStore {
private final Context mContext;
private final TermuxFloatAppSharedPreferences mPreferences;
private static TermuxFloatPreferencesDataStore mInstance;
private TermuxFloatPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxFloatAppSharedPreferences.build(context, true);
}
public static synchronized TermuxFloatPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new TermuxFloatPreferencesDataStore(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.TermuxWidgetAppSharedPreferences;
@Keep
public class TermuxWidgetPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(TermuxWidgetPreferencesDataStore.getInstance(context));
setPreferencesFromResource(R.xml.termux_widget_preferences, rootKey);
}
}
class TermuxWidgetPreferencesDataStore extends PreferenceDataStore {
private final Context mContext;
private final TermuxWidgetAppSharedPreferences mPreferences;
private static TermuxWidgetPreferencesDataStore mInstance;
private TermuxWidgetPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxWidgetAppSharedPreferences.build(context, true);
}
public static synchronized TermuxWidgetPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new TermuxWidgetPreferencesDataStore(context);
}
return mInstance;
}
}

View File

@@ -0,0 +1,77 @@
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 TerminalViewPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
Context context = getContext();
if (context == null) return;
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(TerminalViewPreferencesDataStore.getInstance(context));
setPreferencesFromResource(R.xml.termux_terminal_view_preferences, rootKey);
}
}
class TerminalViewPreferencesDataStore extends PreferenceDataStore {
private final Context mContext;
private final TermuxAppSharedPreferences mPreferences;
private static TerminalViewPreferencesDataStore mInstance;
private TerminalViewPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxAppSharedPreferences.build(context, true);
}
public static synchronized TerminalViewPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new TerminalViewPreferencesDataStore(context);
}
return mInstance;
}
@Override
public void putBoolean(String key, boolean value) {
if (mPreferences == null) return;
if (key == null) return;
switch (key) {
case "terminal_margin_adjustment":
mPreferences.setTerminalMarginAdjustment(value);
break;
default:
break;
}
}
@Override
public boolean getBoolean(String key, boolean defValue) {
if (mPreferences == null) return false;
switch (key) {
case "terminal_margin_adjustment":
return mPreferences.isTerminalMarginAdjustmentEnabled();
default:
return false;
}
}
}

View File

@@ -0,0 +1,101 @@
package com.termux.app.fragments.settings.termux_api;
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.TermuxAPIAppSharedPreferences;
@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_api_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) {
TermuxAPIAppSharedPreferences preferences = TermuxAPIAppSharedPreferences.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 TermuxAPIAppSharedPreferences mPreferences;
private static DebuggingPreferencesDataStore mInstance;
private DebuggingPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxAPIAppSharedPreferences.build(context, true);
}
public static synchronized DebuggingPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new DebuggingPreferencesDataStore(context);
}
return mInstance;
}
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
if (mPreferences == null) return null;
if (key == null) return null;
switch (key) {
case "log_level":
return String.valueOf(mPreferences.getLogLevel(true));
default:
return null;
}
}
@Override
public void putString(String key, @Nullable String value) {
if (mPreferences == null) return;
if (key == null) return;
switch (key) {
case "log_level":
if (value != null) {
mPreferences.setLogLevel(mContext, Integer.parseInt(value), true);
}
break;
default:
break;
}
}
}

View File

@@ -0,0 +1,126 @@
package com.termux.app.fragments.settings.termux_float;
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.TermuxFloatAppSharedPreferences;
@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_float_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) {
TermuxFloatAppSharedPreferences preferences = TermuxFloatAppSharedPreferences.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 TermuxFloatAppSharedPreferences mPreferences;
private static DebuggingPreferencesDataStore mInstance;
private DebuggingPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxFloatAppSharedPreferences.build(context, true);
}
public static synchronized DebuggingPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new DebuggingPreferencesDataStore(context);
}
return mInstance;
}
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
if (mPreferences == null) return null;
if (key == null) return null;
switch (key) {
case "log_level":
return String.valueOf(mPreferences.getLogLevel(true));
default:
return null;
}
}
@Override
public void putString(String key, @Nullable String value) {
if (mPreferences == null) return;
if (key == null) return;
switch (key) {
case "log_level":
if (value != null) {
mPreferences.setLogLevel(mContext, Integer.parseInt(value), true);
}
break;
default:
break;
}
}
@Override
public void putBoolean(String key, boolean value) {
if (mPreferences == null) return;
if (key == null) return;
switch (key) {
case "terminal_view_key_logging_enabled":
mPreferences.setTerminalViewKeyLoggingEnabled(value, true);
break;
default:
break;
}
}
@Override
public boolean getBoolean(String key, boolean defValue) {
if (mPreferences == null) return false;
switch (key) {
case "terminal_view_key_logging_enabled":
return mPreferences.isTerminalViewKeyLoggingEnabled(true);
default:
return false;
}
}
}

View File

@@ -0,0 +1,101 @@
package com.termux.app.fragments.settings.termux_widget;
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.TermuxWidgetAppSharedPreferences;
@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_widget_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) {
TermuxWidgetAppSharedPreferences preferences = TermuxWidgetAppSharedPreferences.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 TermuxWidgetAppSharedPreferences mPreferences;
private static DebuggingPreferencesDataStore mInstance;
private DebuggingPreferencesDataStore(Context context) {
mContext = context;
mPreferences = TermuxWidgetAppSharedPreferences.build(context, true);
}
public static synchronized DebuggingPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new DebuggingPreferencesDataStore(context);
}
return mInstance;
}
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
if (mPreferences == null) return null;
if (key == null) return null;
switch (key) {
case "log_level":
return String.valueOf(mPreferences.getLogLevel(true));
default:
return null;
}
}
@Override
public void putString(String key, @Nullable String value) {
if (mPreferences == null) return;
if (key == null) return;
switch (key) {
case "log_level":
if (value != null) {
mPreferences.setLogLevel(mContext, Integer.parseInt(value), true);
}
break;
default:
break;
}
}
}

View File

@@ -2,11 +2,16 @@ package com.termux.app.settings.properties;
import android.content.Context;
import androidx.annotation.NonNull;
import com.termux.app.terminal.io.KeyboardShortcut;
import com.termux.app.terminal.io.extrakeys.ExtraKeysInfo;
import com.termux.shared.terminal.io.extrakeys.ExtraKeysConstants;
import com.termux.shared.terminal.io.extrakeys.ExtraKeysConstants.EXTRA_KEY_DISPLAY_MAPS;
import com.termux.shared.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 com.termux.shared.termux.TermuxConstants;
import org.json.JSONException;
@@ -14,8 +19,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;
public class TermuxAppSharedProperties extends TermuxSharedProperties {
private ExtraKeysInfo mExtraKeysInfo;
@@ -23,8 +26,9 @@ public class TermuxAppSharedProperties extends TermuxSharedProperties {
private static final String LOG_TAG = "TermuxAppSharedProperties";
public TermuxAppSharedProperties(@Nonnull Context context) {
super(context);
public TermuxAppSharedProperties(@NonNull Context context) {
super(context, TermuxConstants.TERMUX_APP_NAME, TermuxPropertyConstants.getTermuxPropertiesFile(),
TermuxPropertyConstants.TERMUX_PROPERTIES_LIST, new SharedPropertiesParserClient());
}
/**
@@ -50,13 +54,20 @@ public class TermuxAppSharedProperties extends TermuxSharedProperties {
// {@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);
ExtraKeysConstants.ExtraKeyDisplayMap extraKeyDisplayMap = ExtraKeysInfo.getCharDisplayMapForStyle(extraKeysStyle);
if (EXTRA_KEY_DISPLAY_MAPS.DEFAULT_CHAR_DISPLAY.equals(extraKeyDisplayMap) && !TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE.equals(extraKeysStyle)) {
Logger.logError(TermuxSharedProperties.LOG_TAG, "The style \"" + extraKeysStyle + "\" for the key \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE + "\" is invalid. Using default style instead.");
extraKeysStyle = TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE;
}
mExtraKeysInfo = new ExtraKeysInfo(extrakeys, extraKeysStyle, ExtraKeysConstants.CONTROL_CHARS_ALIASES);
} 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);
mExtraKeysInfo = new ExtraKeysInfo(TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE, ExtraKeysConstants.CONTROL_CHARS_ALIASES);
} catch (JSONException e2) {
Logger.showToast(mContext, "Can't create default extra keys",true);
Logger.logStackTraceWithMessage(LOG_TAG, "Could create default extra keys: ", e);
@@ -101,7 +112,8 @@ public class TermuxAppSharedProperties extends TermuxSharedProperties {
* 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);
return (int) TermuxSharedProperties.getInternalPropertyValue(context, TermuxPropertyConstants.getTermuxPropertiesFile(),
TermuxPropertyConstants.KEY_TERMINAL_TRANSCRIPT_ROWS, new SharedPropertiesParserClient());
}
}

View File

@@ -7,11 +7,13 @@ 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;
@@ -64,15 +66,18 @@ public class TermuxActivityRootView extends LinearLayout implements ViewTreeObse
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) {
@@ -118,10 +123,15 @@ public class TermuxActivityRootView extends LinearLayout implements ViewTreeObse
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);
Rect[] windowAndViewRects = ViewUtils.getWindowAndViewRects(bottomSpaceView, mStatusBarHeight);
if (windowAndViewRects == null)
return;
@@ -129,12 +139,19 @@ public class TermuxActivityRootView extends LinearLayout implements ViewTreeObse
Rect bottomSpaceViewRect = windowAndViewRects[1];
// If the bottomSpaceViewRect is inside the windowAvailableRect, then it must be completely visible
boolean isVisible = windowAvailableRect.contains(bottomSpaceViewRect);
//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;
boolean isVisibleBecauseExtraMargin = ((bottomSpaceViewRect.bottom - windowAvailableRect.bottom) < 0);
if (ROOT_VIEW_LOGGING_ENABLED)
Logger.logVerbose(LOG_TAG, "onGlobalLayout: windowAvailableRect " + windowAvailableRect.bottom + ", bottomSpaceViewRect " + bottomSpaceViewRect.bottom + ", diff " + (bottomSpaceViewRect.bottom - windowAvailableRect.bottom) + ", bottom " + params.bottomMargin + ", isVisible " + isVisible + ", isVisibleBecauseMargin " + isVisibleBecauseMargin + ", isVisibleBecauseExtraMargin " + isVisibleBecauseExtraMargin);
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) {
@@ -148,15 +165,25 @@ public class TermuxActivityRootView extends LinearLayout implements ViewTreeObse
// 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, "onGlobalLayout: Visible due to margin");
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.
marginBottom = 0;
// 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;
}
@@ -166,21 +193,28 @@ public class TermuxActivityRootView extends LinearLayout implements ViewTreeObse
// onGlobalLayout: windowAvailableRect 1408, bottomSpaceViewRect 1232, diff -176, bottom 0, isVisible true, isVisibleBecauseMargin false, isVisibleBecauseExtraMargin false
// onGlobalLayout: Bottom margin already equals 0
if (isVisibleBecauseExtraMargin) {
if (ROOT_VIEW_LOGGING_ENABLED)
Logger.logVerbose(LOG_TAG, "onGlobalLayout: Resetting margin since visible due to extra margin");
setMargin = true;
// lastMarginBottom must be invalid. May also happen when keyboards are changed.
lastMarginBottom = null;
// 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, "onGlobalLayout: Setting bottom margin to 0");
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, "onGlobalLayout: Bottom margin already equals 0");
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
@@ -193,8 +227,9 @@ public class TermuxActivityRootView extends LinearLayout implements ViewTreeObse
// 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, "onGlobalLayout: pxHidden " + pxHidden + ", bottom " + params.bottomMargin);
if (root_view_logging_enabled)
Logger.logVerbose(LOG_TAG, "pxHidden " + pxHidden + ", bottom " + params.bottomMargin);
boolean setMargin = params.bottomMargin != pxHidden;
@@ -205,23 +240,45 @@ public class TermuxActivityRootView extends LinearLayout implements ViewTreeObse
// 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 ((bottomSpaceViewRect.bottom - windowAvailableRect.bottom) > 0) {
if (ROOT_VIEW_LOGGING_ENABLED)
Logger.logVerbose(LOG_TAG, "onGlobalLayout: Force setting margin since not visible despite margin");
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, "onGlobalLayout: Setting bottom margin to " + pxHidden);
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, "onGlobalLayout: Bottom margin already equals " + pxHidden);
if (root_view_logging_enabled)
Logger.logVerbose(LOG_TAG, "Bottom margin already equals " + pxHidden);
}
}
}
public static class WindowInsetsListener implements View.OnApplyWindowInsetsListener {
@Override
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
mStatusBarHeight = WindowInsetsCompat.toWindowInsetsCompat(insets).getInsets(WindowInsetsCompat.Type.statusBars()).top;
// Let view window handle insets however it wants
return v.onApplyWindowInsets(insets);
}
}
}

View File

@@ -14,13 +14,13 @@ import android.widget.ListView;
import com.termux.R;
import com.termux.shared.shell.TermuxSession;
import com.termux.shared.interact.DialogUtils;
import com.termux.shared.interact.TextInputDialogUtils;
import com.termux.app.TermuxActivity;
import com.termux.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.terminal.io.BellHandler;
import com.termux.shared.logger.Logger;
import com.termux.terminal.TerminalColors;
import com.termux.terminal.TerminalSession;
@@ -138,37 +138,61 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
return;
}
int index = service.getIndexOfSession(finishedSession);
// For plugin commands that expect the result back, we should immediately close the session
// and send the result back instead of waiting fo the user to press enter.
// The plugin can handle/show errors itself.
boolean isPluginExecutionCommandWithPendingResult = false;
TermuxSession termuxSession = service.getTermuxSession(index);
if (termuxSession != null) {
isPluginExecutionCommandWithPendingResult = termuxSession.getExecutionCommand().isPluginExecutionCommandWithPendingResult();
if (isPluginExecutionCommandWithPendingResult)
Logger.logVerbose(LOG_TAG, "The \"" + finishedSession.mSessionName + "\" session will be force finished automatically since result in pending.");
}
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)
if (index >= 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) {
if (service.getTermuxSessionsSize() > 1 || isPluginExecutionCommandWithPendingResult) {
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) {
if (finishedSession.getExitStatus() == 0 || finishedSession.getExitStatus() == 130 || isPluginExecutionCommandWithPendingResult) {
removeFinishedSession(finishedSession);
}
}
}
@Override
public void onClipboardText(TerminalSession session, String text) {
public void onCopyTextToClipboard(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 onPasteTextFromClipboard(TerminalSession session) {
if (!mActivity.isVisible()) return;
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboard.getPrimaryClip();
if (clipData != null) {
CharSequence paste = clipData.getItemAt(0).coerceToText(mActivity);
if (!TextUtils.isEmpty(paste)) mActivity.getTerminalView().mEmulator.paste(paste.toString());
}
}
@Override
public void onBell(TerminalSession session) {
if (!mActivity.isVisible()) return;
@@ -205,6 +229,15 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
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
@@ -292,7 +325,7 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
public void renameSession(final TerminalSession sessionToRename) {
if (sessionToRename == null) return;
DialogUtils.textInput(mActivity, R.string.title_rename_session, sessionToRename.mSessionName, R.string.action_rename_session_confirm, text -> {
TextInputDialogUtils.textInput(mActivity, R.string.title_rename_session, sessionToRename.mSessionName, R.string.action_rename_session_confirm, text -> {
sessionToRename.mSessionName = text;
termuxSessionListNotifyUpdated();
}, -1, null, -1, null, null);

View File

@@ -9,6 +9,7 @@ import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Environment;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.InputDevice;
@@ -21,21 +22,28 @@ import android.widget.Toast;
import com.termux.R;
import com.termux.app.TermuxActivity;
import com.termux.shared.data.UrlUtils;
import com.termux.shared.file.FileUtils;
import com.termux.shared.interact.MessageDialogUtils;
import com.termux.shared.interact.ShareUtils;
import com.termux.shared.shell.ShellUtils;
import com.termux.shared.terminal.TermuxTerminalViewClientBase;
import com.termux.shared.terminal.io.extrakeys.SpecialButton;
import com.termux.shared.termux.AndroidUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.app.activities.ReportActivity;
import com.termux.app.models.ReportInfo;
import com.termux.shared.activities.ReportActivity;
import com.termux.shared.models.ReportInfo;
import com.termux.app.models.UserAction;
import com.termux.app.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.TerminalBuffer;
import com.termux.terminal.TerminalEmulator;
import com.termux.terminal.TerminalSession;
@@ -69,6 +77,10 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
this.mTermuxTerminalSessionClient = termuxTerminalSessionClient;
}
public TermuxActivity getActivity() {
return mActivity;
}
/**
* Should be called when mActivity.onCreate() is called
*/
@@ -88,6 +100,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
// Piggyback on the terminal view key logging toggle for now, should add a separate toggle in future
mActivity.getTermuxActivityRootView().setIsRootViewLoggingEnabled(isTerminalViewKeyLoggingEnabled);
ViewUtils.setIsViewUtilsLoggingEnabled(isTerminalViewKeyLoggingEnabled);
}
/**
@@ -98,6 +111,15 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
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;
}
}
/**
@@ -120,7 +142,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
}
/**
* Should be called when {@link com.termux.view.TerminalView#mEmulator}
* Should be called when {@link com.termux.view.TerminalView#mEmulator} is set
*/
@Override
public void onEmulatorSet() {
@@ -152,10 +174,26 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
@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");
TerminalEmulator term = mActivity.getCurrentSession().getEmulator();
if (mActivity.getProperties().shouldOpenTerminalTranscriptURLOnClick()) {
int[] columnAndRow = mActivity.getTerminalView().getColumnAndRow(e, true);
String wordAtTap = term.getScreen().getWordAtLocation(columnAndRow[0], columnAndRow[1]);
LinkedHashSet<CharSequence> urlSet = UrlUtils.extractUrls(wordAtTap);
if (!urlSet.isEmpty()) {
String url = (String) urlSet.iterator().next();
ShareUtils.openURL(mActivity, url);
return;
}
}
if (!term.isMouseTrackingActive() && !e.isFromSource(InputDevice.SOURCE_MOUSE)) {
if (!KeyboardUtils.areDisableSoftKeyboardFlagsSet(mActivity))
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
else
Logger.logVerbose(LOG_TAG, "Not showing soft keyboard onSingleTapUp since its disabled");
}
}
@Override
@@ -173,6 +211,11 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
return mActivity.getProperties().isUsingCtrlSpaceWorkaround();
}
@Override
public boolean isTerminalViewSelected() {
return mActivity.getTerminalToolbarViewPager() == null || mActivity.isTerminalViewSelected();
}
@Override
@@ -191,7 +234,8 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) {
mTermuxTerminalSessionClient.removeFinishedSession(currentSession);
return true;
} else if (e.isCtrlPressed() && e.isAltPressed()) {
} else if (!mActivity.getProperties().areHardwareKeyboardShortcutsDisabled() &&
e.isCtrlPressed() && e.isAltPressed()) {
// Get the unmodified code point:
int unicodeChar = e.getUnicodeChar(0);
@@ -236,6 +280,13 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
@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);
}
@@ -261,12 +312,32 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
@Override
public boolean readControlKey() {
return (mActivity.getExtraKeysView() != null && mActivity.getExtraKeysView().readSpecialButton(ExtraKeysView.SpecialButton.CTRL)) || mVirtualControlKeyDown;
return readExtraKeysSpecialButton(SpecialButton.CTRL) || mVirtualControlKeyDown;
}
@Override
public boolean readAltKey() {
return (mActivity.getExtraKeysView() != null && mActivity.getExtraKeysView().readSpecialButton(ExtraKeysView.SpecialButton.ALT));
return readExtraKeysSpecialButton(SpecialButton.ALT);
}
@Override
public boolean readShiftKey() {
return readExtraKeysSpecialButton(SpecialButton.SHIFT);
}
@Override
public boolean readFnKey() {
return readExtraKeysSpecialButton(SpecialButton.FN);
}
public boolean readExtraKeysSpecialButton(SpecialButton specialButton) {
if (mActivity.getExtraKeysView() == null) return false;
Boolean state = mActivity.getExtraKeysView().readSpecialButton(specialButton, true);
if (state == null) {
Logger.logError(LOG_TAG,"Failed to read an unregistered " + specialButton + " special button value from extra keys.");
return false;
}
return state;
}
@Override
@@ -464,7 +535,13 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
}
public void setSoftKeyboardState(boolean isStartup, boolean isReloadTermuxProperties) {
boolean noRequestFocus = false;
boolean noShowKeyboard = false;
// Requesting terminal view focus is necessary regardless of if soft keyboard is to be
// disabled or hidden at startup, otherwise if hardware keyboard is attached and user
// starts typing on hardware keyboard without tapping on the terminal first, then a colour
// tint will be added to the terminal as highlight for the focussed view. Test with a light
// theme.
// If soft keyboard is disabled by user for Termux (check function docs for Termux behaviour info)
if (KeyboardUtils.shouldSoftKeyboardBeDisabled(mActivity,
@@ -472,7 +549,8 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
mActivity.getPreferences().isSoftKeyboardEnabledOnlyIfNoHardware())) {
Logger.logVerbose(LOG_TAG, "Maintaining disabled soft keyboard");
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
noRequestFocus = true;
mActivity.getTerminalView().requestFocus();
noShowKeyboard = 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
@@ -488,10 +566,12 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
// 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;
KeyboardUtils.hideSoftKeyboard(mActivity, mActivity.getTerminalView());
mActivity.getTerminalView().requestFocus();
noShowKeyboard = true;
// Required to keep keyboard hidden on app startup
mShowSoftKeyboardIgnoreOnce = true;
}
@@ -521,7 +601,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
// 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) {
if (!isReloadTermuxProperties && !noShowKeyboard) {
// 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
@@ -585,7 +665,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
String text = ShellUtils.getTerminalSessionTranscriptText(session, true, true);
LinkedHashSet<CharSequence> urlSet = DataUtils.extractUrls(text);
LinkedHashSet<CharSequence> urlSet = UrlUtils.extractUrls(text);
if (urlSet.isEmpty()) {
new AlertDialog.Builder(mActivity).setMessage(R.string.title_select_url_none_found).show();
return;
@@ -608,13 +688,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
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));
}
ShareUtils.openURL(mActivity, url);
return true;
});
});
@@ -629,29 +703,49 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
final String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
if (transcriptText == null) return;
MessageDialogUtils.showMessage(mActivity, TermuxConstants.TERMUX_APP_NAME + " Report Issue",
mActivity.getString(R.string.msg_add_termux_debug_info),
mActivity.getString(R.string.action_yes), (dialog, which) -> reportIssueFromTranscript(transcriptText, true),
mActivity.getString(R.string.action_no), (dialog, which) -> reportIssueFromTranscript(transcriptText, false),
null);
}
private void reportIssueFromTranscript(String transcriptText, boolean addTermuxDebugInfo) {
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").append(MarkdownUtils.getMarkdownCodeForString(transcriptText, true));
reportString.append("\n##\n");
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, true));
reportString.append("\n\n").append(TermuxUtils.getDeviceInfoMarkdownString(mActivity));
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(mActivity));
String termuxAptInfo = TermuxUtils.geAPTInfoMarkdownString(mActivity);
if (termuxAptInfo != null)
reportString.append("\n\n").append(termuxAptInfo);
ReportActivity.startReportActivity(mActivity, new ReportInfo(UserAction.REPORT_ISSUE_FROM_TRANSCRIPT, TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY_NAME, title, null, reportString.toString(), "\n\n" + TermuxUtils.getReportIssueMarkdownString(mActivity), false));
if (addTermuxDebugInfo) {
String termuxDebugInfo = TermuxUtils.getTermuxDebugMarkdownString(mActivity);
if (termuxDebugInfo != null)
reportString.append("\n\n").append(termuxDebugInfo);
}
String userActionName = UserAction.REPORT_ISSUE_FROM_TRANSCRIPT.getName();
ReportActivity.startReportActivity(mActivity,
new ReportInfo(userActionName,
TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY_NAME, title, null,
reportString.toString(), "\n\n" + TermuxUtils.getReportIssueMarkdownString(mActivity),
false,
userActionName,
Environment.getExternalStorageDirectory() + "/" +
FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true)));
}
}.start();
}

View File

@@ -11,7 +11,7 @@ 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.shared.terminal.io.extrakeys.ExtraKeysView;
import com.termux.terminal.TerminalSession;
public class TerminalToolbarViewPager {
@@ -44,7 +44,9 @@ public class TerminalToolbarViewPager {
if (position == 0) {
layout = inflater.inflate(R.layout.view_terminal_toolbar_extra_keys, collection, false);
ExtraKeysView extraKeysView = (ExtraKeysView) layout;
extraKeysView.setTermuxTerminalViewClient(mActivity.getTermuxTerminalViewClient());
extraKeysView.setExtraKeysViewClient(new TermuxTerminalExtraKeys(mActivity.getTerminalView(),
mActivity.getTermuxTerminalViewClient(), mActivity.getTermuxTerminalSessionClient()));
extraKeysView.setButtonTextAllCaps(mActivity.getProperties().shouldExtraKeysTextBeAllCaps());
mActivity.setExtraKeysView(extraKeysView);
extraKeysView.reload(mActivity.getProperties().getExtraKeysInfo());

View File

@@ -0,0 +1,49 @@
package com.termux.app.terminal.io;
import android.annotation.SuppressLint;
import android.view.Gravity;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.drawerlayout.widget.DrawerLayout;
import com.termux.app.terminal.TermuxTerminalSessionClient;
import com.termux.app.terminal.TermuxTerminalViewClient;
import com.termux.shared.terminal.io.TerminalExtraKeys;
import com.termux.view.TerminalView;
public class TermuxTerminalExtraKeys extends TerminalExtraKeys {
TermuxTerminalViewClient mTermuxTerminalViewClient;
TermuxTerminalSessionClient mTermuxTerminalSessionClient;
public TermuxTerminalExtraKeys(@NonNull TerminalView terminalView,
TermuxTerminalViewClient termuxTerminalViewClient,
TermuxTerminalSessionClient termuxTerminalSessionClient) {
super(terminalView);
mTermuxTerminalViewClient = termuxTerminalViewClient;
mTermuxTerminalSessionClient = termuxTerminalSessionClient;
}
@SuppressLint("RtlHardcoded")
@Override
public void onTerminalExtraKeyButtonClick(View view, String key, boolean ctrlDown, boolean altDown, boolean shiftDown, boolean fnDown) {
if ("KEYBOARD".equals(key)) {
if(mTermuxTerminalViewClient != null)
mTermuxTerminalViewClient.onToggleSoftKeyboardRequest();
} else if ("DRAWER".equals(key)) {
DrawerLayout drawerLayout = mTermuxTerminalViewClient.getActivity().getDrawer();
if (drawerLayout.isDrawerOpen(Gravity.LEFT))
drawerLayout.closeDrawer(Gravity.LEFT);
else
drawerLayout.openDrawer(Gravity.LEFT);
} else if ("PASTE".equals(key)) {
if(mTermuxTerminalSessionClient != null)
mTermuxTerminalSessionClient.onPasteTextFromClipboard(null);
} else {
super.onTerminalExtraKeyButtonClick(view, key, ctrlDown, altDown, shiftDown, fnDown);
}
}
}

View File

@@ -1,92 +0,0 @@
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

@@ -1,259 +0,0 @@
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

@@ -1,388 +0,0 @@
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

@@ -4,20 +4,23 @@ import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Environment;
import androidx.annotation.Nullable;
import com.termux.R;
import com.termux.app.activities.ReportActivity;
import com.termux.shared.activities.ReportActivity;
import com.termux.shared.models.errors.Error;
import com.termux.shared.notification.NotificationUtils;
import com.termux.shared.file.FileUtils;
import com.termux.app.models.ReportInfo;
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.AndroidUtils;
import com.termux.shared.termux.TermuxUtils;
import com.termux.shared.termux.TermuxConstants;
@@ -29,8 +32,8 @@ public class CrashUtils {
private static final String LOG_TAG = "CrashUtils";
/**
* Notify the user of a previous app crash by reading the crash info from the crash log file at
* {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}. The crash log file would have been
* 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
@@ -43,10 +46,9 @@ public class CrashUtils {
* @param context The {@link Context} for operations.
* @param logTagParam The log tag to use for logging.
*/
public static void notifyCrash(final Context context, final String logTagParam) {
public static void notifyAppCrashOnLastRun(final Context context, final String logTagParam) {
if (context == null) return;
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
if (preferences == null) return;
@@ -62,52 +64,105 @@ public class CrashUtils {
if (!FileUtils.regularFileExists(TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, false))
return;
String errmsg;
Error error;
StringBuilder reportStringBuilder = new StringBuilder();
// Read report string from crash log file
errmsg = FileUtils.readStringFromFile(context, "crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, Charset.defaultCharset(), reportStringBuilder, false);
if (errmsg != null) {
Logger.logError(logTag, errmsg);
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
FileUtils.moveRegularFile(context, "crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, TermuxConstants.TERMUX_CRASH_LOG_BACKUP_FILE_PATH, true);
if (errmsg != null) {
Logger.logError(logTag, errmsg);
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 == null || reportString.isEmpty())
if (reportString.isEmpty())
return;
// 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, "A crash log file found at \"" + TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH + "\".");
Logger.logDebug(logTag, "The crash log file at \"" + TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH + "\" found. Sending \"" + title + "\" notification.");
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.CRASH_REPORT, logTag, title, null, reportString, "\n\n" + TermuxUtils.getReportIssueMarkdownString(context), true));
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
// Setup the notification channel if not already set up
setupCrashReportsNotificationChannel(context);
// Build the notification
Notification.Builder builder = getCrashReportsNotificationBuilder(context, title, null, null, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
if (builder == null) return;
// Send the notification
int nextNotificationId = NotificationUtils.getNextNotificationId(context);
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
if (notificationManager != null)
notificationManager.notify(nextNotificationId, builder.build());
sendCrashReportNotification(context, logTag, reportString, false, 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 message The message for the crash report.
* @param forceNotification If set to {@code true}, then a notification will be shown
* regardless of if pending intent is {@code null} or
* {@link TermuxPreferenceConstants.TERMUX_APP#KEY_CRASH_REPORT_NOTIFICATIONS_ENABLED}
* is {@code false}.
* @param addAppAndDeviceInfo If set to {@code true}, then app and device info will be appended
* to the message.
*/
public static void sendCrashReportNotification(final Context context, String logTag, String message, boolean forceNotification, boolean addAppAndDeviceInfo) {
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.");
StringBuilder reportString = new StringBuilder(message);
if (addAppAndDeviceInfo) {
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, true));
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context));
}
String userActionName = UserAction.CRASH_REPORT.getName();
ReportActivity.NewInstanceResult result = ReportActivity.newInstance(context, new ReportInfo(userActionName,
logTag, title, null, reportString.toString(),
"\n\n" + TermuxUtils.getReportIssueMarkdownString(context), true,
userActionName,
Environment.getExternalStorageDirectory() + "/" +
FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true)));
if (result.contentIntent == null) return;
// Must ensure result code for PendingIntents and id for notification are unique otherwise will override previous
int nextNotificationId = TermuxNotificationUtils.getNextNotificationId(context);
PendingIntent contentIntent = PendingIntent.getActivity(context, nextNotificationId, result.contentIntent, PendingIntent.FLAG_UPDATE_CURRENT);
PendingIntent deleteIntent = null;
if (result.deleteIntent != null)
deleteIntent = PendingIntent.getBroadcast(context, nextNotificationId, result.deleteIntent, 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, contentIntent, deleteIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
if (builder == null) return;
// Send the notification
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}.
@@ -116,16 +171,17 @@ public class CrashUtils {
* @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 contentIntent The {@link PendingIntent} which should be sent when notification is clicked.
* @param deleteIntent The {@link PendingIntent} which should be sent when notification is deleted.
* @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) {
public static Notification.Builder getCrashReportsNotificationBuilder(final Context context, final CharSequence title, final CharSequence notificationText, final CharSequence notificationBigText, final PendingIntent contentIntent, final PendingIntent deleteIntent, final int notificationMode) {
Notification.Builder builder = NotificationUtils.geNotificationBuilder(context,
TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_HIGH,
title, notificationText, notificationBigText, pendingIntent, notificationMode);
title, notificationText, notificationBigText, contentIntent, deleteIntent, notificationMode);
if (builder == null) return null;

View File

@@ -1,26 +1,34 @@
package com.termux.app.utils;
import android.app.Activity;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Environment;
import androidx.annotation.Nullable;
import com.termux.R;
import com.termux.shared.activities.ReportActivity;
import com.termux.shared.file.FileUtils;
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.app.activities.ReportActivity;
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.app.models.ReportInfo;
import com.termux.shared.models.ReportInfo;
import com.termux.shared.models.ExecutionCommand;
import com.termux.app.models.UserAction;
import com.termux.shared.data.DataUtils;
@@ -29,12 +37,6 @@ import com.termux.shared.termux.TermuxUtils;
public class PluginUtils {
/** Required file permissions for the executable file of execute intent. Executable file must have read and execute permissions */
public static final String PLUGIN_EXECUTABLE_FILE_PERMISSIONS = "r-x"; // Default: "r-x"
/** Required file permissions for the working directory of execute intent. Working directory must have read and write permissions.
* Execute permissions should be attempted to be set, but ignored if they are missing */
public static final String PLUGIN_WORKING_DIRECTORY_PERMISSIONS = "rwx"; // Default: "rwx"
private static final String LOG_TAG = "PluginUtils";
/**
@@ -43,8 +45,8 @@ public class PluginUtils {
* The ExecutionCommand currentState must be greater or equal to
* {@link ExecutionCommand.ExecutionState#EXECUTED}.
* If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and
* {@link ExecutionCommand#pluginPendingIntent} is not {@code null}, then the result of commands
* are sent back to the {@link PendingIntent} creator.
* {@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.
@@ -54,31 +56,46 @@ public class PluginUtils {
if (executionCommand == null) return;
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
Error error = null;
ResultData resultData = executionCommand.resultData;
if (!executionCommand.hasExecuted()) {
Logger.logWarn(logTag, "Ignoring call to processPluginExecutionCommandResult() since the execution command state is not higher than the ExecutionState.EXECUTED");
Logger.logWarn(logTag, executionCommand.getCommandIdAndLabelLogString() + ": Ignoring call to processPluginExecutionCommandResult() since the execution command state is not higher than the ExecutionState.EXECUTED");
return;
}
Logger.logDebug(LOG_TAG, executionCommand.toString());
boolean isPluginExecutionCommandWithPendingResult = executionCommand.isPluginExecutionCommandWithPendingResult();
boolean isExecutionCommandLoggingEnabled = Logger.shouldEnableLoggingForCustomLogLevel(executionCommand.backgroundCustomLogLevel);
boolean result = true;
// Log the output. ResultData should not be logged if pending result since ResultSender will do it
// or if logging is disabled
Logger.logDebugExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true,
!isPluginExecutionCommandWithPendingResult, isExecutionCommandLoggingEnabled));
// If isPluginExecutionCommand is true and pluginPendingIntent is not null, then
// send pluginPendingIntent to its creator with the result
if (executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null) {
String errmsg = executionCommand.errmsg;
// 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);
//Combine errmsg and stacktraces
if (executionCommand.isStateFailed()) {
errmsg = Logger.getMessageAndStackTracesString(executionCommand.errmsg, executionCommand.throwableList);
// Send result to caller
error = ResultSender.sendCommandResultData(context, logTag, executionCommand.getCommandIdAndLabelLogString(),
executionCommand.resultConfig, executionCommand.resultData, isExecutionCommandLoggingEnabled);
if (error != null) {
// error will be added to existing Errors
resultData.setStateFailed(error);
Logger.logDebugExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, true, isExecutionCommandLoggingEnabled));
// Flash and send notification for the error
Logger.showToast(context, ResultData.getErrorsListMinimalString(resultData), true);
sendPluginCommandErrorNotification(context, logTag, executionCommand, ResultData.getErrorsListMinimalString(resultData));
}
// Send pluginPendingIntent to its creator
result = sendPluginExecutionCommandResultPendingIntent(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.stdout, executionCommand.stderr, executionCommand.exitCode, executionCommand.errCode, errmsg, executionCommand.pluginPendingIntent);
}
if (!executionCommand.isStateFailed() && result)
if (!executionCommand.isStateFailed() && error == null)
executionCommand.setState(ExecutionCommand.ExecutionState.SUCCESS);
}
@@ -86,14 +103,13 @@ public class PluginUtils {
* Process {@link ExecutionCommand} error.
*
* The ExecutionCommand currentState must be equal to {@link ExecutionCommand.ExecutionState#FAILED}.
* The {@link ExecutionCommand#errCode} must have been set to a value greater than
* {@link ExecutionCommand#RESULT_CODE_OK}.
* The {@link ExecutionCommand#errmsg} and any {@link ExecutionCommand#throwableList} must also
* be set with appropriate error info.
* 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 ExecutionCommand#pluginPendingIntent} is not {@code null}, then the errors of commands
* are sent back to the {@link PendingIntent} creator.
* {@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
@@ -112,44 +128,96 @@ public class PluginUtils {
if (context == null || executionCommand == null) return;
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
Error error;
ResultData resultData = executionCommand.resultData;
if (!executionCommand.isStateFailed()) {
Logger.logWarn(logTag, "Ignoring call to processPluginExecutionCommandError() since the execution command is not in ExecutionState.FAILED");
Logger.logWarn(logTag, executionCommand.getCommandIdAndLabelLogString() + ": Ignoring call to processPluginExecutionCommandError() since the execution command is not in ExecutionState.FAILED");
return;
}
// Log the error and any exception
Logger.logStackTracesWithMessage(logTag, "(" + executionCommand.errCode + ") " + executionCommand.errmsg, executionCommand.throwableList);
boolean isPluginExecutionCommandWithPendingResult = executionCommand.isPluginExecutionCommandWithPendingResult();
boolean isExecutionCommandLoggingEnabled = Logger.shouldEnableLoggingForCustomLogLevel(executionCommand.backgroundCustomLogLevel);
// 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, isExecutionCommandLoggingEnabled));
// If isPluginExecutionCommand is true and pluginPendingIntent is not null, then
// send pluginPendingIntent to its creator with the errors
if (executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null) {
String errmsg = executionCommand.errmsg;
// 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);
//Combine errmsg and stacktraces
if (executionCommand.isStateFailed()) {
errmsg = Logger.getMessageAndStackTracesString(executionCommand.errmsg, executionCommand.throwableList);
// Send result to caller
error = ResultSender.sendCommandResultData(context, logTag, executionCommand.getCommandIdAndLabelLogString(),
executionCommand.resultConfig, executionCommand.resultData, isExecutionCommandLoggingEnabled);
if (error != null) {
// error will be added to existing Errors
resultData.setStateFailed(error);
Logger.logErrorExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, true, isExecutionCommandLoggingEnabled));
forceNotification = true;
}
sendPluginExecutionCommandResultPendingIntent(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.stdout, executionCommand.stderr, executionCommand.exitCode, executionCommand.errCode, errmsg, executionCommand.pluginPendingIntent);
// No need to show notifications if a pending intent was sent, let the caller handle the result himself
if (!forceNotification) return;
}
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
if (preferences == null) return;
// If user has disabled notifications for plugin, then just return
// If user has disabled notifications for plugin commands, then just return
if (!preferences.arePluginErrorNotificationsEnabled() && !forceNotification)
return;
// Flash the errmsg
Logger.showToast(context, executionCommand.errmsg, true);
// Flash and send notification for the error
Logger.showToast(context, ResultData.getErrorsListMinimalString(resultData), true);
sendPluginCommandErrorNotification(context, logTag, executionCommand, ResultData.getErrorsListMinimalString(resultData));
// Send a notification to show the errmsg which when clicked will open the {@link ReportActivity}
}
/** Set variables which will be used by {@link ResultSender#sendCommandResultData(Context, String, String, ResultConfig, ResultData, boolean)}
* 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, boolean)}
* 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";
@@ -157,114 +225,44 @@ public class PluginUtils {
reportString.append(ExecutionCommand.getExecutionCommandMarkdownString(executionCommand));
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, true));
reportString.append("\n\n").append(TermuxUtils.getDeviceInfoMarkdownString(context));
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context));
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.PLUGIN_EXECUTION_COMMAND, logTag, title, null, reportString.toString(), null,true));
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
String userActionName = UserAction.PLUGIN_EXECUTION_COMMAND.getName();
ReportActivity.NewInstanceResult result = ReportActivity.newInstance(context,
new ReportInfo(userActionName, logTag, title, null,
reportString.toString(), null,true,
userActionName,
Environment.getExternalStorageDirectory() + "/" +
FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true)));
if (result.contentIntent == null) return;
// Must ensure result code for PendingIntents and id for notification are unique otherwise will override previous
int nextNotificationId = TermuxNotificationUtils.getNextNotificationId(context);
PendingIntent contentIntent = PendingIntent.getActivity(context, nextNotificationId, result.contentIntent, PendingIntent.FLAG_UPDATE_CURRENT);
PendingIntent deleteIntent = null;
if (result.deleteIntent != null)
deleteIntent = PendingIntent.getBroadcast(context, nextNotificationId, result.deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT);
// Setup the notification channel if not already set up
setupPluginCommandErrorsNotificationChannel(context);
// Use markdown in notification
CharSequence notificationText = MarkdownUtils.getSpannedMarkdownText(context, executionCommand.errmsg);
//CharSequence notificationText = executionCommand.errmsg;
CharSequence notificationTextCharSequence = MarkdownUtils.getSpannedMarkdownText(context, notificationTextString);
//CharSequence notificationTextCharSequence = notificationTextString;
// Build the notification
Notification.Builder builder = getPluginCommandErrorsNotificationBuilder(context, title, notificationText, notificationText, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
if (builder == null) return;
Notification.Builder builder = getPluginCommandErrorsNotificationBuilder(context, title,
notificationTextCharSequence, notificationTextCharSequence, contentIntent, deleteIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
if (builder == null) return;
// Send the notification
int nextNotificationId = NotificationUtils.getNextNotificationId(context);
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
if (notificationManager != null)
notificationManager.notify(nextNotificationId, builder.build());
}
/**
* Send {@link ExecutionCommand} result {@link PendingIntent} in the
* {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE} bundle.
*
*
* @param context The {@link Context} that will be used to send result intent to the {@link PendingIntent} creator.
* @param logTag The log tag to use for logging.
* @param label The label of {@link ExecutionCommand}.
* @param stdout The stdout of {@link ExecutionCommand}.
* @param stderr The stderr of {@link ExecutionCommand}.
* @param exitCode The exitCode of {@link ExecutionCommand}.
* @param errCode The errCode of {@link ExecutionCommand}.
* @param errmsg The errmsg of {@link ExecutionCommand}.
* @param pluginPendingIntent The pluginPendingIntent of {@link ExecutionCommand}.
* @return Returns {@code true} if pluginPendingIntent was successfully send, otherwise [@code false}.
*/
public static boolean sendPluginExecutionCommandResultPendingIntent(Context context, String logTag, String label, String stdout, String stderr, Integer exitCode, Integer errCode, String errmsg, PendingIntent pluginPendingIntent) {
if (context == null || pluginPendingIntent == null) return false;
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
Logger.logDebug(logTag, "Sending execution result for Execution Command \"" + label + "\" to " + pluginPendingIntent.getCreatorPackage());
String truncatedStdout = null;
String truncatedStderr = null;
String stdoutOriginalLength = (stdout == null) ? null: String.valueOf(stdout.length());
String stderrOriginalLength = (stderr == null) ? null: String.valueOf(stderr.length());
// Truncate stdout and stdout to max TRANSACTION_SIZE_LIMIT_IN_BYTES
if (stderr == null || stderr.isEmpty()) {
truncatedStdout = DataUtils.getTruncatedCommandOutput(stdout, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false);
} else if (stdout == null || stdout.isEmpty()) {
truncatedStderr = DataUtils.getTruncatedCommandOutput(stderr, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false);
} else {
truncatedStdout = DataUtils.getTruncatedCommandOutput(stdout, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 2, false, false, false);
truncatedStderr = DataUtils.getTruncatedCommandOutput(stderr, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 2, false, false, false);
}
if (truncatedStdout != null && truncatedStdout.length() < stdout.length()) {
Logger.logWarn(logTag, "Execution Result for Execution Command \"" + label + "\" stdout length truncated from " + stdoutOriginalLength + " to " + truncatedStdout.length());
stdout = truncatedStdout;
}
if (truncatedStderr != null && truncatedStderr.length() < stderr.length()) {
Logger.logWarn(logTag, "Execution Result for Execution Command \"" + label + "\" stderr length truncated from " + stderrOriginalLength + " to " + truncatedStderr.length());
stderr = truncatedStderr;
}
String errmsgOriginalLength = (errmsg == null) ? null: String.valueOf(errmsg.length());
// Truncate errmsg to max TRANSACTION_SIZE_LIMIT_IN_BYTES / 4
// trim from end to preserve start of stacktraces
String truncatedErrmsg = DataUtils.getTruncatedCommandOutput(errmsg, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 4, true, false, false);
if (truncatedErrmsg != null && truncatedErrmsg.length() < errmsg.length()) {
Logger.logWarn(logTag, "Execution Result for Execution Command \"" + label + "\" errmsg length truncated from " + errmsgOriginalLength + " to " + truncatedErrmsg.length());
errmsg = truncatedErrmsg;
}
final Bundle resultBundle = new Bundle();
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT, stdout);
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH, stdoutOriginalLength);
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR, stderr);
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH, stderrOriginalLength);
if (exitCode != null) resultBundle.putInt(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE, exitCode);
if (errCode != null) resultBundle.putInt(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERR, errCode);
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG, errmsg);
Intent resultIntent = new Intent();
resultIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE, resultBundle);
try {
pluginPendingIntent.send(context, Activity.RESULT_OK, resultIntent);
} catch (PendingIntent.CanceledException e) {
// The caller doesn't want the result? That's fine, just ignore
Logger.logDebug(logTag, "The Execution Command \"" + label + "\" creator " + pluginPendingIntent.getCreatorPackage() + " does not want the results anymore");
}
return true;
}
/**
* Get {@link Notification.Builder} for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
* and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
@@ -273,16 +271,19 @@ public class PluginUtils {
* @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 contentIntent The {@link PendingIntent} which should be sent when notification is clicked.
* @param deleteIntent The {@link PendingIntent} which should be sent when notification is deleted.
* @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) {
public static Notification.Builder getPluginCommandErrorsNotificationBuilder(
final Context context, final CharSequence title, final CharSequence notificationText,
final CharSequence notificationBigText, final PendingIntent contentIntent, final PendingIntent deleteIntent, 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);
title, notificationText, notificationBigText, contentIntent, deleteIntent, notificationMode);
if (builder == null) return null;
@@ -318,12 +319,14 @@ public class PluginUtils {
* 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 errmsg} if policy is violated, otherwise {@code null}.
* @return Returns the {@code error} if policy is violated, otherwise {@code null}.
*/
public static String checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(final Context context) {
public static String checkIfAllowExternalAppsPolicyIsViolated(final Context context, String apiName) {
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);
if (!SharedProperties.isPropertyValueTrue(context, TermuxPropertyConstants.getTermuxPropertiesFile(),
TermuxConstants.PROP_ALLOW_EXTERNAL_APPS, true)) {
errmsg = context.getString(R.string.error_allow_external_apps_ungranted, apiName,
TermuxFileUtils.getUnExpandedTermuxPath(TermuxConstants.TERMUX_PROPERTIES_PRIMARY_FILE_PATH));
}
return errmsg;

View File

@@ -1,7 +1,6 @@
package com.termux.filepicker;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
@@ -9,7 +8,10 @@ import android.provider.OpenableColumns;
import android.util.Patterns;
import com.termux.R;
import com.termux.shared.interact.DialogUtils;
import com.termux.shared.data.DataUtils;
import com.termux.shared.data.IntentUtils;
import com.termux.shared.interact.MessageDialogUtils;
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;
@@ -39,6 +41,8 @@ public class TermuxFileReceiverActivity extends Activity {
*/
boolean mFinishOnDismissNameDialog = true;
private static final String API_TAG = TermuxConstants.TERMUX_APP_NAME + "FileReceiver";
private static final String LOG_TAG = "TermuxFileReceiverActivity";
static boolean isSharedTextAnUrl(String sharedText) {
@@ -55,44 +59,66 @@ public class TermuxFileReceiverActivity extends Activity {
final String type = intent.getType();
final String scheme = intent.getScheme();
Logger.logVerbose(LOG_TAG, "Intent Received:\n" + IntentUtils.getIntentString(intent));
final String sharedTitle = IntentUtils.getStringExtraIfSet(intent, Intent.EXTRA_TITLE, null);
if (Intent.ACTION_SEND.equals(action) && type != null) {
final String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
final Uri sharedUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
if (sharedText != null) {
if (sharedUri != null) {
handleContentUri(sharedUri, sharedTitle);
} else if (sharedText != null) {
if (isSharedTextAnUrl(sharedText)) {
handleUrlAndFinish(sharedText);
} else {
String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
if (subject == null) subject = intent.getStringExtra(Intent.EXTRA_TITLE);
String subject = IntentUtils.getStringExtraIfSet(intent, Intent.EXTRA_SUBJECT, null);
if (subject == null) subject = sharedTitle;
if (subject != null) subject += ".txt";
promptNameAndSave(new ByteArrayInputStream(sharedText.getBytes(StandardCharsets.UTF_8)), subject);
}
} else if (sharedUri != null) {
handleContentUri(sharedUri, intent.getStringExtra(Intent.EXTRA_TITLE));
} else {
showErrorDialogAndQuit("Send action without content - nothing to save.");
}
} else if ("content".equals(scheme)) {
handleContentUri(intent.getData(), intent.getStringExtra(Intent.EXTRA_TITLE));
} else if ("file".equals(scheme)) {
// When e.g. clicking on a downloaded apk:
String path = intent.getData().getPath();
File file = new File(path);
try {
FileInputStream in = new FileInputStream(file);
promptNameAndSave(in, file.getName());
} catch (FileNotFoundException e) {
showErrorDialogAndQuit("Cannot open file: " + e.getMessage() + ".");
}
} else {
showErrorDialogAndQuit("Unable to receive any file or URL.");
Uri dataUri = intent.getData();
if (dataUri == null) {
showErrorDialogAndQuit("Data uri not passed.");
return;
}
if ("content".equals(scheme)) {
handleContentUri(dataUri, sharedTitle);
} else if ("file".equals(scheme)) {
// When e.g. clicking on a downloaded apk:
String path = dataUri.getPath();
if (DataUtils.isNullOrEmpty(path)) {
showErrorDialogAndQuit("File path from data uri is null, empty or invalid.");
return;
}
File file = new File(path);
try {
FileInputStream in = new FileInputStream(file);
promptNameAndSave(in, file.getName());
} catch (FileNotFoundException e) {
showErrorDialogAndQuit("Cannot open file: " + e.getMessage() + ".");
}
} else {
showErrorDialogAndQuit("Unable to receive any file or URL.");
}
}
}
void showErrorDialogAndQuit(String message) {
mFinishOnDismissNameDialog = false;
new AlertDialog.Builder(this).setMessage(message).setOnDismissListener(dialog -> finish()).setPositiveButton(android.R.string.ok, (dialog, which) -> finish()).show();
MessageDialogUtils.showMessage(this,
API_TAG, message,
null, (dialog, which) -> finish(),
null, null,
dialog -> finish());
}
void handleContentUri(final Uri uri, String subjectFromIntent) {
@@ -118,7 +144,7 @@ public class TermuxFileReceiverActivity extends Activity {
}
void promptNameAndSave(final InputStream in, final String attachmentFileName) {
DialogUtils.textInput(this, R.string.title_file_received, attachmentFileName, R.string.action_file_received_edit, text -> {
TextInputDialogUtils.textInput(this, R.string.title_file_received, attachmentFileName, R.string.action_file_received_edit, text -> {
File outFile = saveStreamWithName(in, text);
if (outFile == null) return;
@@ -157,10 +183,17 @@ public class TermuxFileReceiverActivity extends Activity {
public File saveStreamWithName(InputStream in, String attachmentFileName) {
File receiveDir = new File(TERMUX_RECEIVEDIR);
if (DataUtils.isNullOrEmpty(attachmentFileName)) {
showErrorDialogAndQuit("File name cannot be null or empty");
return null;
}
if (!receiveDir.isDirectory() && !receiveDir.mkdirs()) {
showErrorDialogAndQuit("Cannot create directory: " + receiveDir.getAbsolutePath());
return null;
}
try {
final File outFile = new File(receiveDir, attachmentFileName);
try (FileOutputStream f = new FileOutputStream(outFile)) {
@@ -182,7 +215,7 @@ public class TermuxFileReceiverActivity extends Activity {
final File urlOpenerProgramFile = new File(URL_OPENER_PROGRAM);
if (!urlOpenerProgramFile.isFile()) {
showErrorDialogAndQuit("The following file does not exist:\n$HOME/bin/termux-url-opener\n\n"
+ "Create this file as a script or a symlink - it will be called with the shared URL as only argument.");
+ "Create this file as a script or a symlink - it will be called with the shared URL as the first argument.");
return;
}

View File

@@ -6,9 +6,12 @@
android:fitsSystemWindows="true">
<RelativeLayout
android:id="@+id/activity_termux_root_relative_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginHorizontal="3dp"
android:layout_marginVertical="0dp"
android:orientation="vertical">
<androidx.drawerlayout.widget.DrawerLayout
@@ -22,8 +25,6 @@
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"
@@ -42,6 +43,19 @@
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"

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<com.termux.app.terminal.io.extrakeys.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
<com.termux.shared.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"

View File

@@ -9,10 +9,10 @@
<!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>
@@ -21,7 +21,7 @@
<!-- 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>
environment and access files</string>
@@ -31,7 +31,9 @@
<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_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="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>
@@ -78,6 +80,7 @@
<string name="action_report_issue">Report Issue</string>
<string name="msg_generating_report">Generating Report</string>
<string name="msg_add_termux_debug_info">Add termux debug info to 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>
@@ -91,29 +94,20 @@
<!-- 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 Report And ShareUtils -->
<string name="action_copy">Copy</string>
<string name="action_share">Share</string>
<string name="title_share_with">Share With</string>
<string name="title_report_text">Report Text</string>
<!-- Termux File Receiver -->
<string name="title_file_received">Save file in ~/downloads/</string>
<string name="action_file_received_edit">Edit</string>
@@ -121,6 +115,12 @@
<!-- Miscellaneous -->
<string name="error_allow_external_apps_ungranted">%1$s requires `allow-external-apps`
property to be set to `true` in `%2$s` file.</string>
<!-- Termux Settings -->
<string name="title_activity_termux_settings">&TERMUX_APP_NAME; Settings</string>
@@ -141,7 +141,8 @@
<!-- 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>
<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>
@@ -168,15 +169,52 @@
<!-- 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>
<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 -->
<!-- Terminal View Preferences -->
<string name="termux_terminal_view_preferences_title">Terminal View</string>
<string name="termux_terminal_view_preferences_summary">Preferences for terminal view</string>
<!-- View Category -->
<string name="termux_terminal_view_view_header">View</string>
<!-- Terminal View Margin Adjustment -->
<string name="termux_terminal_view_terminal_margin_adjustment_title">Terminal Margin Adjustment</string>
<string name="termux_terminal_view_terminal_margin_adjustment_off">Terminal margin adjustment will be disabled.</string>
<string name="termux_terminal_view_terminal_margin_adjustment_on">Terminal margin adjustment will be enabled.
It should be enabled to try to fix the issue where soft keyboard covers part of extra keys/terminal view.
If it causes screen flickering on your devices, then disable it. (Default)</string>
<!-- Termux:API App Preferences -->
<string name="termux_api_preferences_title">&TERMUX_API_APP_NAME;</string>
<string name="termux_api_preferences_summary">Preferences for &TERMUX_API_APP_NAME; app</string>
<!-- Termux:Float App Preferences -->
<string name="termux_float_preferences_title">&TERMUX_FLOAT_APP_NAME;</string>
<string name="termux_float_preferences_summary">Preferences for &TERMUX_FLOAT_APP_NAME; app</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>
<!-- Termux:Widget App Preferences -->
<string name="termux_widget_preferences_title">&TERMUX_WIDGET_APP_NAME;</string>
<string name="termux_widget_preferences_summary">Preferences for &TERMUX_WIDGET_APP_NAME; app</string>
<!-- About Preference -->
<string name="about_preference_title">About</string>

View File

@@ -44,15 +44,6 @@
</style>
<style name="Theme.AppCompat.TermuxReportActivity" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimaryDark">#FF0000</item>
</style>
<style name="Toolbar.Title" parent="TextAppearance.Widget.AppCompat.Toolbar.Title">
<item name="android:textSize">14sp</item>
</style>
<style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert">
<!-- Seen in buttons on alert dialog: -->
<item name="android:colorAccent">#212121</item>

View File

@@ -6,6 +6,20 @@
app:summary="@string/termux_preferences_summary"
app:fragment="com.termux.app.fragments.settings.TermuxPreferencesFragment"/>
<Preference
app:key="termux_api"
app:title="@string/termux_api_preferences_title"
app:summary="@string/termux_api_preferences_summary"
app:isPreferenceVisible="false"
app:fragment="com.termux.app.fragments.settings.TermuxAPIPreferencesFragment"/>
<Preference
app:key="termux_float"
app:title="@string/termux_float_preferences_title"
app:summary="@string/termux_float_preferences_summary"
app:isPreferenceVisible="false"
app:fragment="com.termux.app.fragments.settings.TermuxFloatPreferencesFragment"/>
<Preference
app:key="termux_tasker"
app:title="@string/termux_tasker_preferences_title"
@@ -13,6 +27,13 @@
app:isPreferenceVisible="false"
app:fragment="com.termux.app.fragments.settings.TermuxTaskerPreferencesFragment"/>
<Preference
app:key="termux_widget"
app:title="@string/termux_widget_preferences_title"
app:summary="@string/termux_widget_preferences_summary"
app:isPreferenceVisible="false"
app:fragment="com.termux.app.fragments.settings.TermuxWidgetPreferencesFragment"/>
<Preference
app:key="about"
app:title="@string/about_preference_title"

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_api.DebuggingPreferencesFragment"/>
</PreferenceScreen>

View File

@@ -0,0 +1,21 @@
<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" />
</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_float.DebuggingPreferencesFragment"/>
</PreferenceScreen>

View File

@@ -10,4 +10,9 @@
app:summary="@string/termux_terminal_io_preferences_summary"
app:fragment="com.termux.app.fragments.settings.termux.TerminalIOPreferencesFragment"/>
<Preference
app:title="@string/termux_terminal_view_preferences_title"
app:summary="@string/termux_terminal_view_preferences_summary"
app:fragment="com.termux.app.fragments.settings.termux.TerminalViewPreferencesFragment"/>
</PreferenceScreen>

View File

@@ -0,0 +1,15 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:key="view"
app:title="@string/termux_terminal_view_view_header">
<SwitchPreferenceCompat
app:key="terminal_margin_adjustment"
app:summaryOff="@string/termux_terminal_view_terminal_margin_adjustment_off"
app:summaryOn="@string/termux_terminal_view_terminal_margin_adjustment_on"
app:title="@string/termux_terminal_view_terminal_margin_adjustment_title" />
</PreferenceCategory>
</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_widget.DebuggingPreferencesFragment"/>
</PreferenceScreen>

View File

@@ -1,6 +1,6 @@
package com.termux.app;
import com.termux.shared.data.DataUtils;
import com.termux.shared.data.UrlUtils;
import org.junit.Assert;
import org.junit.Test;
@@ -13,7 +13,7 @@ public class TermuxActivityTest {
private void assertUrlsAre(String text, String... urls) {
LinkedHashSet<String> expected = new LinkedHashSet<>();
Collections.addAll(expected, urls);
Assert.assertEquals(expected, DataUtils.extractUrls(text));
Assert.assertEquals(expected, UrlUtils.extractUrls(text));
}
@Test

View File

@@ -4,7 +4,7 @@ buildscript {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.3'
classpath 'com.android.tools.build:gradle:4.2.2'
}
}
@@ -12,6 +12,7 @@ allprojects {
repositories {
google()
mavenCentral()
maven { url "https://jitpack.io" }
}
}

View File

@@ -15,9 +15,6 @@
org.gradle.jvmargs=-Xmx2048M
android.useAndroidX=true
termuxVersion=0.111
termuxVersionCode=111
minSdkVersion=24
targetSdkVersion=28
ndkVersion=22.1.7171670

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

2
jitpack.yml Normal file
View File

@@ -0,0 +1,2 @@
env:
JITPACK_NDK_VERSION: "21.1.6352462"

View File

@@ -3,7 +3,7 @@ apply plugin: 'maven-publish'
android {
compileSdkVersion project.properties.compileSdkVersion.toInteger()
ndkVersion project.properties.ndkVersion
ndkVersion = System.getenv("JITPACK_NDK_VERSION") ?: project.properties.ndkVersion
defaultConfig {
minSdkVersion project.properties.minSdkVersion.toInteger()
@@ -58,25 +58,16 @@ task sourceJar(type: Jar) {
classifier "sources"
}
publishing {
publications {
bar(MavenPublication) {
groupId 'com.termux'
artifactId 'terminal-emulator'
version "0.114"
artifact(sourceJar)
artifact("$buildDir/outputs/aar/terminal-emulator-release.aar")
}
}
repositories {
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/termux/termux-app")
credentials {
username = System.getenv("GH_USERNAME")
password = System.getenv("GH_TOKEN")
afterEvaluate {
publishing {
publications {
// Creates a Maven publication called "release".
release(MavenPublication) {
from components.release
groupId = 'com.termux'
artifactId = 'terminal-emulator'
version = '0.118.0'
artifact(sourceJar)
}
}
}

View File

@@ -102,6 +102,45 @@ public final class TerminalBuffer {
return builder.toString();
}
public String getWordAtLocation(int x, int y) {
// Set y1 and y2 to the lines where the wrapped line starts and ends.
// I.e. if a line that is wrapped to 3 lines starts at line 4, and this
// is called with y=5, then y1 would be set to 4 and y2 would be set to 6.
int y1 = y;
int y2 = y;
while (y1 > 0 && !getSelectedText(0, y1 - 1, mColumns, y, true, true).contains("\n")) {
y1--;
}
while (y2 < mScreenRows && !getSelectedText(0, y, mColumns, y2 + 1, true, true).contains("\n")) {
y2++;
}
// Get the text for the whole wrapped line
String text = getSelectedText(0, y1, mColumns, y2, true, true);
// The index of x in text
int textOffset = (y - y1) * mColumns + x;
if (textOffset >= text.length()) {
// The click was to the right of the last word on the line, so
// there's no word to return
return "";
}
// Set x1 and x2 to the indices of the last space before x and the
// first space after x in text respectively
int x1 = text.lastIndexOf(' ', textOffset);
int x2 = text.indexOf(' ', textOffset);
if (x2 == -1) {
x2 = text.length();
}
if (x1 == x2) {
// The click was on a space, so there's no word to return
return "";
}
return text.substring(x1 + 1, x2);
}
public int getActiveTranscriptRows() {
return mActiveTranscriptRows;
}

View File

@@ -57,7 +57,7 @@ public final class TerminalColorScheme {
0xff808080, 0xff8a8a8a, 0xff949494, 0xff9e9e9e, 0xffa8a8a8, 0xffb2b2b2, 0xffbcbcbc, 0xffc6c6c6, 0xffd0d0d0, 0xffdadada, 0xffe4e4e4, 0xffeeeeee,
// COLOR_INDEX_DEFAULT_FOREGROUND, COLOR_INDEX_DEFAULT_BACKGROUND and COLOR_INDEX_DEFAULT_CURSOR:
0xffffffff, 0xff000000, 0xffA9AAA9};
0xffffffff, 0xff000000, 0xffffffff};
public final int[] mDefaultColors = new int[TextStyle.NUM_INDEXED_COLORS];

View File

@@ -1980,7 +1980,7 @@ public final class TerminalEmulator {
int startIndex = textParameter.indexOf(";") + 1;
try {
String clipboardText = new String(Base64.decode(textParameter.substring(startIndex), 0), StandardCharsets.UTF_8);
mSession.clipboardText(clipboardText);
mSession.onCopyTextToClipboard(clipboardText);
} catch (Exception e) {
mClient.logError(LOG_TAG, "OSC Manipulate selection, invalid string '" + textParameter + "");
}

View File

@@ -18,8 +18,11 @@ public abstract class TerminalOutput {
/** Notify the terminal client that the terminal title has changed. */
public abstract void titleChanged(String oldTitle, String newTitle);
/** Notify the terminal client that the terminal title has changed. */
public abstract void clipboardText(String text);
/** Notify the terminal client that text should be copied to clipboard. */
public abstract void onCopyTextToClipboard(String text);
/** Notify the terminal client that text should be pasted from clipboard. */
public abstract void onPasteTextFromClipboard();
/** Notify the terminal client that a bell character (ASCII 7, bell, BEL, \a, ^G)) has been received. */
public abstract void onBell();

View File

@@ -269,8 +269,13 @@ public final class TerminalSession extends TerminalOutput {
}
@Override
public void clipboardText(String text) {
mClient.onClipboardText(this, text);
public void onCopyTextToClipboard(String text) {
mClient.onCopyTextToClipboard(this, text);
}
@Override
public void onPasteTextFromClipboard() {
mClient.onPasteTextFromClipboard(this);
}
@Override

View File

@@ -13,7 +13,9 @@ public interface TerminalSessionClient {
void onSessionFinished(TerminalSession finishedSession);
void onClipboardText(TerminalSession session, String text);
void onCopyTextToClipboard(TerminalSession session, String text);
void onPasteTextFromClipboard(TerminalSession session);
void onBell(TerminalSession session);

View File

@@ -45,4 +45,21 @@ public class ScreenBufferTest extends TerminalTestCase {
withTerminalSized(5, 3).enterString("ABC\r\nFG");
assertEquals("ABC\nFG", mTerminal.getScreen().getSelectedText(0, 0, 1, 1, true, true));
}
public void testGetWordAtLocation() {
withTerminalSized(5, 3).enterString("ABCDEFGHIJ\r\nKLMNO");
assertEquals("ABCDEFGHIJKLMNO", mTerminal.getScreen().getWordAtLocation(0, 0));
assertEquals("ABCDEFGHIJKLMNO", mTerminal.getScreen().getWordAtLocation(4, 1));
assertEquals("ABCDEFGHIJKLMNO", mTerminal.getScreen().getWordAtLocation(4, 2));
withTerminalSized(5, 3).enterString("ABC DEF GHI ");
assertEquals("ABC", mTerminal.getScreen().getWordAtLocation(0, 0));
assertEquals("", mTerminal.getScreen().getWordAtLocation(3, 0));
assertEquals("DEF", mTerminal.getScreen().getWordAtLocation(4, 0));
assertEquals("DEF", mTerminal.getScreen().getWordAtLocation(0, 1));
assertEquals("DEF", mTerminal.getScreen().getWordAtLocation(1, 1));
assertEquals("GHI", mTerminal.getScreen().getWordAtLocation(0, 2));
assertEquals("", mTerminal.getScreen().getWordAtLocation(1, 2));
assertEquals("", mTerminal.getScreen().getWordAtLocation(2, 2));
}
}

View File

@@ -37,10 +37,14 @@ public abstract class TerminalTestCase extends TestCase {
}
@Override
public void clipboardText(String text) {
public void onCopyTextToClipboard(String text) {
clipboardPuts.add(text);
}
@Override
public void onPasteTextFromClipboard() {
}
@Override
public void onBell() {
bellsRung++;

View File

@@ -5,7 +5,7 @@ android {
compileSdkVersion project.properties.compileSdkVersion.toInteger()
dependencies {
implementation "androidx.annotation:annotation:1.1.0"
implementation "androidx.annotation:annotation:1.3.0"
api project(":terminal-emulator")
}
@@ -37,25 +37,16 @@ task sourceJar(type: Jar) {
classifier "sources"
}
publishing {
publications {
bar(MavenPublication) {
groupId 'com.termux'
artifactId 'terminal-view'
version "0.114"
artifact(sourceJar)
artifact("$buildDir/outputs/aar/terminal-view-release.aar")
}
}
repositories {
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/termux/termux-app")
credentials {
username = System.getenv("GH_USERNAME")
password = System.getenv("GH_TOKEN")
afterEvaluate {
publishing {
publications {
// Creates a Maven publication called "release".
release(MavenPublication) {
from components.release
groupId = 'com.termux'
artifactId = 'terminal-view'
version = '0.118.0'
artifact(sourceJar)
}
}
}

View File

@@ -118,9 +118,13 @@ public final class TerminalRenderer {
final int columnWidthSinceLastRun = column - lastRunStartColumn;
final int charsSinceLastRun = currentCharIndex - lastRunStartIndex;
int cursorColor = lastRunInsideCursor ? mEmulator.mColors.mCurrentColors[TextStyle.COLOR_INDEX_CURSOR] : 0;
boolean invertCursorTextColor = false;
if (lastRunInsideCursor && cursorShape == TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK) {
invertCursorTextColor = true;
}
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun,
lastRunStartIndex, charsSinceLastRun, measuredWidthForRun,
cursorColor, cursorShape, lastRunStyle, reverseVideo || lastRunInsideSelection);
cursorColor, cursorShape, lastRunStyle, reverseVideo || invertCursorTextColor || lastRunInsideSelection);
}
measuredWidthForRun = 0.f;
lastRunStyle = style;
@@ -143,8 +147,12 @@ public final class TerminalRenderer {
final int columnWidthSinceLastRun = columns - lastRunStartColumn;
final int charsSinceLastRun = currentCharIndex - lastRunStartIndex;
int cursorColor = lastRunInsideCursor ? mEmulator.mColors.mCurrentColors[TextStyle.COLOR_INDEX_CURSOR] : 0;
boolean invertCursorTextColor = false;
if (lastRunInsideCursor && cursorShape == TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK) {
invertCursorTextColor = true;
}
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun,
measuredWidthForRun, cursorColor, cursorShape, lastRunStyle, reverseVideo || lastRunInsideSelection);
measuredWidthForRun, cursorColor, cursorShape, lastRunStyle, reverseVideo || invertCursorTextColor || lastRunInsideSelection);
}
}

View File

@@ -94,7 +94,7 @@ public final class TerminalView extends View {
@Override
public boolean onUp(MotionEvent event) {
mScrollRemainder = 0.0f;
if (mEmulator != null && mEmulator.isMouseTrackingActive() && !isSelectingText() && !scrolledWithFinger) {
if (mEmulator != null && mEmulator.isMouseTrackingActive() && !event.isFromSource(InputDevice.SOURCE_MOUSE) && !isSelectingText() && !scrolledWithFinger) {
// Quick event processing when mouse tracking is active - do not wait for check of double tapping
// for zooming.
sendMouseEventCode(event, TerminalEmulator.MOUSE_LEFT_BUTTON, true);
@@ -114,13 +114,8 @@ public final class TerminalView extends View {
return true;
}
requestFocus();
if (!mEmulator.isMouseTrackingActive()) {
if (!event.isFromSource(InputDevice.SOURCE_MOUSE)) {
mClient.onSingleTapUp(event);
return true;
}
}
return false;
mClient.onSingleTapUp(event);
return true;
}
@Override
@@ -262,20 +257,33 @@ public final class TerminalView extends View {
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
if (mClient.shouldEnforceCharBasedInput()) {
// Some keyboards seems do not reset the internal state on TYPE_NULL.
// Affects mostly Samsung stock keyboards.
// https://github.com/termux/termux-app/issues/686
outAttrs.inputType = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
// Ensure that inputType is only set if TerminalView is selected view with the keyboard and
// an alternate view is not selected, like an EditText. This is necessary if an activity is
// initially started with the alternate view or if activity is returned to from another app
// and the alternate view was the one selected the last time.
if (mClient.isTerminalViewSelected()) {
if (mClient.shouldEnforceCharBasedInput()) {
// Some keyboards seems do not reset the internal state on TYPE_NULL.
// Affects mostly Samsung stock keyboards.
// https://github.com/termux/termux-app/issues/686
// However, this is not a valid value as per AOSP since `InputType.TYPE_CLASS_*` is
// not set and it logs a warning:
// W/InputAttributes: Unexpected input class: inputType=0x00080090 imeOptions=0x02000000
// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:packages/inputmethods/LatinIME/java/src/com/android/inputmethod/latin/InputAttributes.java;l=79
outAttrs.inputType = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
} else {
// Using InputType.NULL is the most correct input type and avoids issues with other hacks.
//
// Previous keyboard issues:
// https://github.com/termux/termux-packages/issues/25
// https://github.com/termux/termux-app/issues/87.
// https://github.com/termux/termux-app/issues/126.
// https://github.com/termux/termux-app/issues/137 (japanese chars and TYPE_NULL).
outAttrs.inputType = InputType.TYPE_NULL;
}
} else {
// Using InputType.NULL is the most correct input type and avoids issues with other hacks.
//
// Previous keyboard issues:
// https://github.com/termux/termux-packages/issues/25
// https://github.com/termux/termux-app/issues/87.
// https://github.com/termux/termux-app/issues/126.
// https://github.com/termux/termux-app/issues/137 (japanese chars and TYPE_NULL).
outAttrs.inputType = InputType.TYPE_NULL;
// Corresponds to android:inputType="text"
outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL;
}
// Note that IME_ACTION_NONE cannot be used as that makes it impossible to input newlines using the on-screen
@@ -337,6 +345,10 @@ public final class TerminalView extends View {
codePoint = firstChar;
}
// Check onKeyDown() for details.
if (mClient.readShiftKey())
codePoint = Character.toUpperCase(codePoint);
boolean ctrlHeld = false;
if (codePoint <= 31 && codePoint != 27) {
if (codePoint == '\n') {
@@ -454,10 +466,31 @@ public final class TerminalView extends View {
return true;
}
/**
* Get the zero indexed column and row of the terminal view for the
* position of the event.
*
* @param event The event with the position to get the column and row for.
* @param relativeToScroll If true the column number will take the scroll
* position into account. E.g. if scrolled 3 lines up and the event
* position is in the top left, column will be -3 if relativeToScroll is
* true and 0 if relativeToScroll is false.
* @return Array with the column and row.
*/
public int[] getColumnAndRow(MotionEvent event, boolean relativeToScroll) {
int column = (int) (event.getX() / mRenderer.mFontWidth);
int row = (int) ((event.getY() - mRenderer.mFontLineSpacingAndAscent) / mRenderer.mFontLineSpacing);
if (relativeToScroll) {
row += mTopRow;
}
return new int[] { column, row };
}
/** Send a single mouse event code to the terminal. */
void sendMouseEventCode(MotionEvent e, int button, boolean pressed) {
int x = (int) (e.getX() / mRenderer.mFontWidth) + 1;
int y = (int) ((e.getY() - mRenderer.mFontLineSpacingAndAscent) / mRenderer.mFontLineSpacing) + 1;
int[] columnAndRow = getColumnAndRow(e, false);
int x = columnAndRow[0] + 1;
int y = columnAndRow[1] + 1;
if (pressed && (button == TerminalEmulator.MOUSE_WHEELDOWN_BUTTON || button == TerminalEmulator.MOUSE_WHEELUP_BUTTON)) {
if (mMouseStartDownTime == e.getDownTime()) {
x = mMouseScrollStartX;
@@ -533,7 +566,6 @@ public final class TerminalView extends View {
sendMouseEventCode(event, TerminalEmulator.MOUSE_LEFT_BUTTON_MOVED, true);
break;
}
return true;
}
}
@@ -567,6 +599,102 @@ public final class TerminalView extends View {
return super.onKeyPreIme(keyCode, event);
}
/**
* Key presses in software keyboards will generally NOT trigger this listener, although some
* may elect to do so in some situations. Do not rely on this to catch software key presses.
* Gboard calls this when shouldEnforceCharBasedInput() is disabled (InputType.TYPE_NULL) instead
* of calling commitText(), with deviceId=-1. However, Hacker's Keyboard, OpenBoard, LG Keyboard
* call commitText().
*
* This function may also be called directly without android calling it, like by
* `TerminalExtraKeys` which generates a KeyEvent manually which uses {@link KeyCharacterMap#VIRTUAL_KEYBOARD}
* as the device (deviceId=-1), as does Gboard. That would normally use mappings defined in
* `/system/usr/keychars/Virtual.kcm`. You can run `dumpsys input` to find the `KeyCharacterMapFile`
* used by virtual keyboard or hardware keyboard. Note that virtual keyboard device is not the
* same as software keyboard, like Gboard, etc. Its a fake device used for generating events and
* for testing.
*
* We handle shift key in `commitText()` to convert codepoint to uppercase case there with a
* call to {@link Character#toUpperCase(int)}, but here we instead rely on getUnicodeChar() for
* conversion of keyCode, for both hardware keyboard shift key (via effectiveMetaState) and
* `mClient.readShiftKey()`, based on value in kcm files.
* This may result in different behaviour depending on keyboard and android kcm files set for the
* InputDevice for the event passed to this function. This will likely be an issue for non-english
* languages since `Virtual.kcm` in english only by default or at least in AOSP. For both hardware
* shift key (via effectiveMetaState) and `mClient.readShiftKey()`, `getUnicodeChar()` is used
* for shift specific behaviour which usually is to uppercase.
*
* For fn key on hardware keyboard, android checks kcm files for hardware keyboards, which is
* `Generic.kcm` by default, unless a vendor specific one is defined. The event passed will have
* {@link KeyEvent#META_FUNCTION_ON} set. If the kcm file only defines a single character or unicode
* code point `\\uxxxx`, then only one event is passed with that value. However, if kcm defines
* a `fallback` key for fn or others, like `key DPAD_UP { ... fn: fallback PAGE_UP }`, then
* android will first pass an event with original key `DPAD_UP` and {@link KeyEvent#META_FUNCTION_ON}
* set. But this function will not consume it and android will pass another event with `PAGE_UP`
* and {@link KeyEvent#META_FUNCTION_ON} not set, which will be consumed.
*
* Now there are some other issues as well, firstly ctrl and alt flags are not passed to
* `getUnicodeChar()`, so modified key values in kcm are not used. Secondly, if the kcm file
* for other modifiers like shift or fn define a non-alphabet, like { fn: '\u0015' } to act as
* DPAD_LEFT, the `getUnicodeChar()` will correctly return `21` as the code point but action will
* not happen because the `handleKeyCode()` function that transforms DPAD_LEFT to `\033[D`
* escape sequence for the terminal to perform the left action would not be called since its
* called before `getUnicodeChar()` and terminal will instead get `21 0x15 Negative Acknowledgement`.
* The solution to such issues is calling `getUnicodeChar()` before the call to `handleKeyCode()`
* if user has defined a custom kcm file, like done in POC mentioned in #2237. Note that
* Hacker's Keyboard calls `commitText()` so don't test fn/shift with it for this function.
* https://github.com/termux/termux-app/pull/2237
* https://github.com/agnostic-apollo/termux-app/blob/terminal-code-point-custom-mapping/terminal-view/src/main/java/com/termux/view/TerminalView.java
*
* Key Character Map (kcm) and Key Layout (kl) files info:
* https://source.android.com/devices/input/key-character-map-files
* https://source.android.com/devices/input/key-layout-files
* https://source.android.com/devices/input/keyboard-devices
* AOSP kcm and kl files:
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/data/keyboards
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/packages/InputDevices/res/raw
*
* KeyCodes:
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/core/java/android/view/KeyEvent.java
* https://cs.android.com/android/platform/superproject/+/master:frameworks/native/include/android/keycodes.h
*
* `dumpsys input`:
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/services/inputflinger/reader/EventHub.cpp;l=1917
*
* Loading of keymap:
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/services/inputflinger/reader/EventHub.cpp;l=1644
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/libs/input/Keyboard.cpp;l=41
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/libs/input/InputDevice.cpp
* OVERLAY keymaps for hardware keyboards may be combined as well:
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/libs/input/KeyCharacterMap.cpp;l=165
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/libs/input/KeyCharacterMap.cpp;l=831
*
* Parse kcm file:
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/libs/input/KeyCharacterMap.cpp;l=727
* Parse key value:
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/libs/input/KeyCharacterMap.cpp;l=981
*
* `KeyEvent.getUnicodeChar()`
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/core/java/android/view/KeyEvent.java;l=2716
* https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/KeyCharacterMap.java;l=368
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/core/jni/android_view_KeyCharacterMap.cpp;l=117
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/native/libs/input/KeyCharacterMap.cpp;l=231
*
* Keyboard layouts advertised by applications, like for hardware keyboards via #ACTION_QUERY_KEYBOARD_LAYOUTS
* Config is stored in `/data/system/input-manager-state.xml`
* https://github.com/ris58h/custom-keyboard-layout
* Loading from apps:
* https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/input/InputManagerService.java;l=1221
* Set:
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/core/java/android/hardware/input/InputManager.java;l=89
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/core/java/android/hardware/input/InputManager.java;l=543
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:packages/apps/Settings/src/com/android/settings/inputmethod/KeyboardLayoutDialogFragment.java;l=167
* https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/input/InputManagerService.java;l=1385
* https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/input/PersistentDataStore.java
* Get overlay keyboard layout
* https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/input/InputManagerService.java;l=2158
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp;l=616
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED)
@@ -589,13 +717,15 @@ public final class TerminalView extends View {
final int metaState = event.getMetaState();
final boolean controlDown = event.isCtrlPressed() || mClient.readControlKey();
final boolean leftAltDown = (metaState & KeyEvent.META_ALT_LEFT_ON) != 0 || mClient.readAltKey();
final boolean shiftDown = event.isShiftPressed() || mClient.readShiftKey();
final boolean rightAltDownFromEvent = (metaState & KeyEvent.META_ALT_RIGHT_ON) != 0;
int keyMod = 0;
if (controlDown) keyMod |= KeyHandler.KEYMOD_CTRL;
if (event.isAltPressed() || leftAltDown) keyMod |= KeyHandler.KEYMOD_ALT;
if (event.isShiftPressed()) keyMod |= KeyHandler.KEYMOD_SHIFT;
if (shiftDown) keyMod |= KeyHandler.KEYMOD_SHIFT;
if (event.isNumLockOn()) keyMod |= KeyHandler.KEYMOD_NUM_LOCK;
// https://github.com/termux/termux-app/issues/731
if (!event.isFunctionPressed() && handleKeyCode(keyCode, keyMod)) {
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED) mClient.logInfo(LOG_TAG, "handleKeyCode() took key event");
return true;
@@ -611,6 +741,9 @@ public final class TerminalView extends View {
}
int effectiveMetaState = event.getMetaState() & ~bitsToClear;
if (shiftDown) effectiveMetaState |= KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_LEFT_ON;
if (mClient.readFnKey()) effectiveMetaState |= KeyEvent.META_FUNCTION_ON;
int result = event.getUnicodeChar(effectiveMetaState);
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED)
mClient.logInfo(LOG_TAG, "KeyEvent#getUnicodeChar(" + effectiveMetaState + ") returned: " + result);
@@ -646,6 +779,10 @@ public final class TerminalView extends View {
if (mTermSession == null) return;
// Ensure cursor is shown when a key is pressed down like long hold on (arrow) keys
if (mEmulator != null)
mEmulator.setCursorBlinkState(true);
final boolean controlDown = controlDownFromEvent || mClient.readControlKey();
final boolean altDown = leftAltDownFromEvent || mClient.readAltKey();
@@ -720,7 +857,10 @@ public final class TerminalView extends View {
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED)
mClient.logInfo(LOG_TAG, "onKeyUp(keyCode=" + keyCode + ", event=" + event + ")");
if (mEmulator == null) return true;
// Do not return for KEYCODE_BACK and send it to the client since user may be trying
// to exit the activity.
if (mEmulator == null && keyCode != KeyEvent.KEYCODE_BACK) return true;
if (mClient.onKeyUp(keyCode, event)) {
invalidate();
@@ -757,6 +897,10 @@ public final class TerminalView extends View {
mEmulator = mTermSession.getEmulator();
mClient.onEmulatorSet();
// Update mTerminalCursorBlinkerRunnable inner class mEmulator on session change
if (mTerminalCursorBlinkerRunnable != null)
mTerminalCursorBlinkerRunnable.setEmulator(mEmulator);
mTopRow = 0;
scrollTo(0, 0);
invalidate();
@@ -881,13 +1025,16 @@ public final class TerminalView extends View {
* {@link #TERMINAL_CURSOR_BLINK_RATE_MIN} and {@link #TERMINAL_CURSOR_BLINK_RATE_MAX}.
*
* This should be called when the view holding this activity is resumed or stopped so that
* cursor blinker does not run when activity is not visible. Ensure that {@link #mEmulator}
* is set when you call this to start cursor blinking by waiting for {@link TerminalViewClient#onEmulatorSet()}
* event after calling {@link #attachSession(TerminalSession)} for the first session added in the
* activity, otherwise blinking will not start. Do not call this directly after
* {@link #attachSession(TerminalSession)} since {@link #updateSize()} may return without
* setting {@link #mEmulator} since width/height may be 0. Its called again in
* {@link #onSizeChanged(int, int, int, int)}.
* cursor blinker does not run when activity is not visible. If you call this on onResume()
* to start cursor blinking, then ensure that {@link #mEmulator} is set, otherwise wait for the
* {@link TerminalViewClient#onEmulatorSet()} event after calling {@link #attachSession(TerminalSession)}
* for the first session added in the activity since blinking will not start if {@link #mEmulator}
* is not set, like if activity is started again after exiting it with double back press. Do not
* call this directly after {@link #attachSession(TerminalSession)} since {@link #updateSize()}
* may return without setting {@link #mEmulator} since width/height may be 0. Its called again in
* {@link #onSizeChanged(int, int, int, int)}. Calling on onResume() if emulator is already set
* is necessary, since onEmulatorSet() may not be called after activity is started after device
* display timeout with double tap and not power button.
*
* It should also be called on the
* {@link com.termux.terminal.TerminalSessionClient#onTerminalCursorStateChange(boolean)}
@@ -895,6 +1042,10 @@ public final class TerminalView extends View {
* to be shown. It should also be checked if activity is visible if blinker is to be started
* before calling this.
*
* It should also be called after terminal is reset with {@link TerminalSession#reset()} in case
* cursor blinker was disabled before reset due to call to
* {@link com.termux.terminal.TerminalSessionClient#onTerminalCursorStateChange(boolean)}.
*
* How cursor blinker starting works is by registering a {@link Runnable} with the looper of
* the main thread of the app which when run, toggles the cursor blinking state and re-registers
* itself to be called with the delay set by {@link #mTerminalCursorBlinkerRate}. When cursor
@@ -960,7 +1111,7 @@ public final class TerminalView extends View {
private class TerminalCursorBlinkerRunnable implements Runnable {
private final TerminalEmulator mEmulator;
private TerminalEmulator mEmulator;
private final int mBlinkRate;
// Initialize with false so that initial blink state is visible after toggling
@@ -971,6 +1122,10 @@ public final class TerminalView extends View {
mBlinkRate = blinkRate;
}
public void setEmulator(TerminalEmulator emulator) {
mEmulator = emulator;
}
public void run() {
try {
if (mEmulator != null) {

View File

@@ -34,6 +34,8 @@ public interface TerminalViewClient {
boolean shouldUseCtrlSpaceWorkaround();
boolean isTerminalViewSelected();
void copyModeChanged(boolean copyMode);
@@ -52,6 +54,11 @@ public interface TerminalViewClient {
boolean readAltKey();
boolean readShiftKey();
boolean readFnKey();
boolean onCodePoint(int codePoint, boolean ctrlDown, TerminalSession session);

View File

@@ -89,14 +89,9 @@ public class TextSelectionCursorController implements CursorController {
}
public void setInitialTextSelectionPosition(MotionEvent event) {
int cx = (int) (event.getX() / terminalView.mRenderer.getFontWidth());
final boolean eventFromMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
// Offset for finger:
final int SELECT_TEXT_OFFSET_Y = eventFromMouse ? 0 : -40;
int cy = (int) ((event.getY() + SELECT_TEXT_OFFSET_Y) / terminalView.mRenderer.getFontLineSpacing()) + terminalView.getTopRow();
mSelX1 = mSelX2 = cx;
mSelY1 = mSelY2 = cy;
int[] columnAndRow = terminalView.getColumnAndRow(event, true);
mSelX1 = mSelX2 = columnAndRow[0];
mSelY1 = mSelY2 = columnAndRow[1];
TerminalBuffer screen = terminalView.mEmulator.getScreen();
if (!" ".equals(screen.getSelectedText(mSelX1, mSelY1, mSelX1, mSelY1))) {
@@ -138,17 +133,12 @@ public class TextSelectionCursorController implements CursorController {
switch (item.getItemId()) {
case ACTION_COPY:
String selectedText = terminalView.mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2).trim();
terminalView.mTermSession.clipboardText(selectedText);
terminalView.mTermSession.onCopyTextToClipboard(selectedText);
terminalView.stopTextSelectionMode();
break;
case ACTION_PASTE:
terminalView.stopTextSelectionMode();
ClipboardManager clipboard = (ClipboardManager) terminalView.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboard.getPrimaryClip();
if (clipData != null) {
CharSequence paste = clipData.getItemAt(0).coerceToText(terminalView.getContext());
if (!TextUtils.isEmpty(paste)) terminalView.mEmulator.paste(paste.toString());
}
terminalView.mTermSession.onPasteTextFromClipboard();
break;
case ACTION_MORE:
terminalView.stopTextSelectionMode(); //we stop text selection first, otherwise handles will show above popup

65
termux-shared/LICENSE.md Normal file
View File

@@ -0,0 +1,65 @@
The `termux-shared` library is released under [GPLv3 only](https://www.gnu.org/licenses/gpl-3.0.html) license.
### Exceptions
#### [MIT License](https://opensource.org/licenses/MIT)
- [`src/main/java/com/termux/shared/termux/TermuxConstants.java`](src/main/java/com/termux/shared/termux/TermuxConstants.java).
- [`src/main/java/com/termux/shared/settings/properties/TermuxPropertyConstants.java`](src/main/java/com/termux/shared/settings/properties/TermuxPropertyConstants.java).
- [`src/main/java/com/termux/shared/activities/*`](src/main/java/com/termux/shared/activities).
- [`src/main/java/com/termux/shared/crash/CrashHandler.java`](src/main/java/com/termux/shared/crash/CrashHandler.java).
- [`src/main/java/com/termux/shared/data/DataUtils.java`](src/main/java/com/termux/shared/data/DataUtils.java).
- [`src/main/java/com/termux/shared/data/IntentUtils.java`](src/main/java/com/termux/shared/data/IntentUtils.java).
- [`src/main/java/com/termux/shared/file/filesystem/FileType.java`](src/main/java/com/termux/shared/file/filesystem/FileType.java).
- [`src/main/java/com/termux/shared/file/filesystem/FileTypes.java`](src/main/java/com/termux/shared/file/filesystem/FileTypes.java).
- [`src/main/java/com/termux/shared/file/filesystem/NativeDispatcher.java`](src/main/java/com/termux/shared/file/filesystem/NativeDispatcher.java).
- [`src/main/java/com/termux/shared/file/tests/FileUtilsTests.java`](src/main/java/com/termux/shared/file/tests/FileUtilsTests.java).
- [`src/main/java/com/termux/shared/file/FileUtils.java`](src/main/java/com/termux/shared/file/FileUtils.java).
- [`src/main/java/com/termux/shared/interact/ShareUtils.java`](src/main/java/com/termux/shared/interact/ShareUtils.java).
- [`src/main/java/com/termux/shared/interact/MessageDialogUtils.java`](src/main/java/com/termux/shared/interact/MessageDialogUtils.java).
- [`src/main/java/com/termux/shared/logger/Logger.java`](src/main/java/com/termux/shared/logger/Logger.java).
- [`src/main/java/com/termux/shared/markdown/MarkdownUtils.java`](src/main/java/com/termux/shared/markdown/MarkdownUtils.java).
- [`src/main/java/com/termux/shared/models/*`](src/main/java/com/termux/shared/models).
- [`src/main/java/com/termux/shared/notification/NotificationUtils.java`](src/main/java/com/termux/shared/notification/NotificationUtils.java).
- [`src/main/java/com/termux/shared/settings/preferences/SharedPreferenceUtils.java`](src/main/java/com/termux/shared/settings/preferences/SharedPreferenceUtils.java).
- [`src/main/java/com/termux/shared/settings/properties/SharedPropertiesParser.java`](src/main/java/com/termux/shared/settings/properties/SharedPropertiesParser.java).
- [`src/main/java/com/termux/shared/settings/properties/SharedProperties.java`](src/main/java/com/termux/shared/settings/properties/SharedProperties.java).
- [`src/main/java/com/termux/shared/shell/ResultSender.java`](src/main/java/com/termux/shared/shell/ResultSender.java).
- [`src/main/java/com/termux/shared/shell/ShellEnvironmentClient.java`](src/main/java/com/termux/shared/shell/ShellEnvironmentClient.java).
- [`src/main/java/com/termux/shared/shell/ShellUtils.java`](src/main/java/com/termux/shared/shell/ShellUtils.java).
- [`src/main/java/com/termux/shared/shell/TermuxTask.java`](src/main/java/com/termux/shared/shell/TermuxTask.java).
- [`src/main/java/com/termux/shared/termux/AndroidUtils.java`](src/main/java/com/termux/shared/termux/AndroidUtils.java).
- [`src/main/java/com/termux/shared/view/KeyboardUtils.java`](src/main/java/com/termux/shared/view/KeyboardUtils.java).
- [`src/main/java/com/termux/shared/view/ViewUtils.java`](src/main/java/com/termux/shared/view/ViewUtils.java).
- [`src/main/res/drawable/*`](src/main/res/drawable).
- [`src/main/res/layout/*`](src/main/res/layout).
- [`src/main/res/menu/*`](src/main/res/menu).
- [`src/main/res/values/*`](src/main/res/values).
##
#### [GPLv2 only with "Classpath" exception](https://openjdk.java.net/legal/gplv2+ce.html)
- [`src/main/java/com/termux/shared/file/filesystem/*`](src/main/java/com/termux/shared/file/filesystem) files that use code from [libcore/ojluni](https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/).
##
#### [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0)
- [`src/main/java/com/termux/shared/shell/StreamGobbler.java`](src/main/java/com/termux/shared/shell/StreamGobbler.java) uses code from [libsuperuser ](https://github.com/Chainfire/libsuperuser).
##

View File

@@ -5,15 +5,20 @@ android {
compileSdkVersion project.properties.compileSdkVersion.toInteger()
dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation "androidx.annotation:annotation:1.2.0"
implementation "androidx.core:core:1.5.0-rc01"
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation "androidx.annotation:annotation:1.3.0"
implementation "androidx.core:core:1.6.0"
implementation 'com.google.android.material:material:1.4.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"
// Do not increment version higher than 1.0.0-alpha09 since it will break ViewUtils and needs to be looked into
// noinspection GradleDependency
implementation "androidx.window:window:1.0.0-alpha09"
// Do not increment version higher than 2.5 or there
// will be runtime exceptions on android < 8
// due to missing classes like java.nio.file.Path.
@@ -43,8 +48,8 @@ android {
dependencies {
testImplementation "junit:junit:4.13.2"
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
task sourceJar(type: Jar) {
@@ -52,25 +57,16 @@ task sourceJar(type: Jar) {
classifier "sources"
}
publishing {
publications {
bar(MavenPublication) {
groupId 'com.termux'
artifactId 'termux-shared'
version "0.114"
artifact(sourceJar)
artifact("$buildDir/outputs/aar/termux-shared-release.aar")
}
}
repositories {
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/termux/termux-app")
credentials {
username = System.getenv("GH_USERNAME")
password = System.getenv("GH_TOKEN")
afterEvaluate {
publishing {
publications {
// Creates a Maven publication called "release".
release(MavenPublication) {
from components.release
groupId = 'com.termux'
artifactId = 'termux-shared'
version = '0.118.0'
artifact(sourceJar)
}
}
}

View File

@@ -1,3 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.termux.shared">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.termux.shared">
<uses-permission android:name="android.permission.VIBRATE" />
</manifest>

View File

@@ -0,0 +1,472 @@
package com.termux.shared.activities;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import com.termux.shared.R;
import com.termux.shared.data.DataUtils;
import com.termux.shared.file.FileUtils;
import com.termux.shared.file.filesystem.FileType;
import com.termux.shared.logger.Logger;
import com.termux.shared.models.errors.Error;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.interact.ShareUtils;
import com.termux.shared.models.ReportInfo;
import org.commonmark.node.FencedCodeBlock;
import org.jetbrains.annotations.NotNull;
import io.noties.markwon.Markwon;
import io.noties.markwon.recycler.MarkwonAdapter;
import io.noties.markwon.recycler.SimpleEntry;
/**
* An activity to show reports in markdown format as per CommonMark spec based on config passed as {@link ReportInfo}.
* Add Following to `AndroidManifest.xml` to use in an app:
* {@code `<activity android:name="com.termux.shared.activities.ReportActivity" android:theme="@style/Theme.AppCompat.TermuxReportActivity" android:documentLaunchMode="intoExisting" />` }
* and
* {@code `<receiver android:name="com.termux.shared.activities.ReportActivity$ReportActivityBroadcastReceiver" android:exported="false" />` }
* Receiver **must not** be `exported="true"`!!!
*
* Also make an incremental call to {@link #deleteReportInfoFilesOlderThanXDays(Context, int, boolean)}
* in the app to cleanup cached files.
*/
public class ReportActivity extends AppCompatActivity {
private static final String CLASS_NAME = ReportActivity.class.getCanonicalName();
private static final String ACTION_DELETE_REPORT_INFO_OBJECT_FILE = CLASS_NAME + ".ACTION_DELETE_REPORT_INFO_OBJECT_FILE";
private static final String EXTRA_REPORT_INFO_OBJECT = CLASS_NAME + ".EXTRA_REPORT_INFO_OBJECT";
private static final String EXTRA_REPORT_INFO_OBJECT_FILE_PATH = CLASS_NAME + ".EXTRA_REPORT_INFO_OBJECT_FILE_PATH";
private static final String CACHE_DIR_BASENAME = "report_activity";
private static final String CACHE_FILE_BASENAME_PREFIX = "report_info_";
public static final int REQUEST_GRANT_STORAGE_PERMISSION_FOR_SAVE_FILE = 1000;
public static final int ACTIVITY_TEXT_SIZE_LIMIT_IN_BYTES = 1000 * 1024; // 1MB
private ReportInfo mReportInfo;
private String mReportInfoFilePath;
private String mReportActivityMarkdownString;
private Bundle mBundle;
private static final String LOG_TAG = "ReportActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Logger.logVerbose(LOG_TAG, "onCreate");
setContentView(R.layout.activity_report);
Toolbar toolbar = findViewById(R.id.toolbar);
if (toolbar != null) {
setSupportActionBar(toolbar);
}
mBundle = null;
Intent intent = getIntent();
if (intent != null)
mBundle = intent.getExtras();
else if (savedInstanceState != null)
mBundle = savedInstanceState;
updateUI();
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
Logger.logVerbose(LOG_TAG, "onNewIntent");
setIntent(intent);
if (intent != null) {
deleteReportInfoFile(this, mReportInfoFilePath);
mBundle = intent.getExtras();
updateUI();
}
}
private void updateUI() {
if (mBundle == null) {
finish(); return;
}
mReportInfo = null;
mReportInfoFilePath = null;
if (mBundle.containsKey(EXTRA_REPORT_INFO_OBJECT_FILE_PATH)) {
mReportInfoFilePath = mBundle.getString(EXTRA_REPORT_INFO_OBJECT_FILE_PATH);
Logger.logVerbose(LOG_TAG, ReportInfo.class.getSimpleName() + " serialized object will be read from file at path \"" + mReportInfoFilePath + "\"");
if (mReportInfoFilePath != null) {
try {
FileUtils.ReadSerializableObjectResult result = FileUtils.readSerializableObjectFromFile(ReportInfo.class.getSimpleName(), mReportInfoFilePath, ReportInfo.class, false);
if (result.error != null) {
Logger.logErrorExtended(LOG_TAG, result.error.toString());
Logger.showToast(this, Error.getMinimalErrorString(result.error), true);
finish(); return;
} else {
if (result.serializableObject != null)
mReportInfo = (ReportInfo) result.serializableObject;
}
} catch (Exception e) {
Logger.logErrorAndShowToast(this, LOG_TAG, e.getMessage());
Logger.logStackTraceWithMessage(LOG_TAG, "Failure while getting " + ReportInfo.class.getSimpleName() + " serialized object from file at path \"" + mReportInfoFilePath + "\"", e);
}
}
} else {
mReportInfo = (ReportInfo) mBundle.getSerializable(EXTRA_REPORT_INFO_OBJECT);
}
if (mReportInfo == null) {
finish(); return;
}
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
if (mReportInfo.reportTitle != null)
actionBar.setTitle(mReportInfo.reportTitle);
else
actionBar.setTitle(TermuxConstants.TERMUX_APP_NAME + " App Report");
}
RecyclerView recyclerView = findViewById(R.id.recycler_view);
final Markwon markwon = MarkdownUtils.getRecyclerMarkwonBuilder(this);
final MarkwonAdapter adapter = MarkwonAdapter.builderTextViewIsRoot(R.layout.markdown_adapter_node_default)
.include(FencedCodeBlock.class, SimpleEntry.create(R.layout.markdown_adapter_node_code_block, R.id.code_text_view))
.build();
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(adapter);
generateReportActivityMarkdownString();
adapter.setMarkdown(markwon, mReportActivityMarkdownString);
adapter.notifyDataSetChanged();
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
if (mBundle.containsKey(EXTRA_REPORT_INFO_OBJECT_FILE_PATH)) {
outState.putString(EXTRA_REPORT_INFO_OBJECT_FILE_PATH, mReportInfoFilePath);
} else {
outState.putSerializable(EXTRA_REPORT_INFO_OBJECT, mReportInfo);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
Logger.logVerbose(LOG_TAG, "onDestroy");
deleteReportInfoFile(this, mReportInfoFilePath);
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
final MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_report, menu);
if (mReportInfo.reportSaveFilePath == null) {
MenuItem item = menu.findItem(R.id.menu_item_save_report_to_file);
if (item != null)
item.setEnabled(false);
}
return true;
}
@Override
public void onBackPressed() {
// Remove activity from recents menu on back button press
finishAndRemoveTask();
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
int id = item.getItemId();
if (id == R.id.menu_item_share_report) {
ShareUtils.shareText(this, getString(R.string.title_report_text), ReportInfo.getReportInfoMarkdownString(mReportInfo));
} else if (id == R.id.menu_item_copy_report) {
ShareUtils.copyTextToClipboard(this, ReportInfo.getReportInfoMarkdownString(mReportInfo), null);
} else if (id == R.id.menu_item_save_report_to_file) {
ShareUtils.saveTextToFile(this, mReportInfo.reportSaveFileLabel,
mReportInfo.reportSaveFilePath, ReportInfo.getReportInfoMarkdownString(mReportInfo),
true, REQUEST_GRANT_STORAGE_PERMISSION_FOR_SAVE_FILE);
}
return false;
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Logger.logInfo(LOG_TAG, "Storage permission granted by user on request.");
if (requestCode == REQUEST_GRANT_STORAGE_PERMISSION_FOR_SAVE_FILE) {
ShareUtils.saveTextToFile(this, mReportInfo.reportSaveFileLabel,
mReportInfo.reportSaveFilePath, ReportInfo.getReportInfoMarkdownString(mReportInfo),
true, -1);
}
} else {
Logger.logInfo(LOG_TAG, "Storage permission denied by user on request.");
}
}
/**
* Generate the markdown {@link String} to be shown in {@link ReportActivity}.
*/
private void generateReportActivityMarkdownString() {
// We need to reduce chances of OutOfMemoryError happening so reduce new allocations and
// do not keep output of getReportInfoMarkdownString in memory
StringBuilder reportString = new StringBuilder();
if (mReportInfo.reportStringPrefix != null)
reportString.append(mReportInfo.reportStringPrefix);
String reportMarkdownString = ReportInfo.getReportInfoMarkdownString(mReportInfo);
int reportMarkdownStringSize = reportMarkdownString.getBytes().length;
boolean truncated = false;
if (reportMarkdownStringSize > ACTIVITY_TEXT_SIZE_LIMIT_IN_BYTES) {
Logger.logVerbose(LOG_TAG, mReportInfo.reportTitle + " report string size " + reportMarkdownStringSize + " is greater than " + ACTIVITY_TEXT_SIZE_LIMIT_IN_BYTES + " and will be truncated");
reportString.append(DataUtils.getTruncatedCommandOutput(reportMarkdownString, ACTIVITY_TEXT_SIZE_LIMIT_IN_BYTES, true, false, true));
truncated = true;
} else {
reportString.append(reportMarkdownString);
}
// Free reference
reportMarkdownString = null;
if (mReportInfo.reportStringSuffix != null)
reportString.append(mReportInfo.reportStringSuffix);
int reportStringSize = reportString.length();
if (reportStringSize > ACTIVITY_TEXT_SIZE_LIMIT_IN_BYTES) {
// This may break markdown formatting
Logger.logVerbose(LOG_TAG, mReportInfo.reportTitle + " report string total size " + reportStringSize + " is greater than " + ACTIVITY_TEXT_SIZE_LIMIT_IN_BYTES + " and will be truncated");
mReportActivityMarkdownString = this.getString(R.string.msg_report_truncated) +
DataUtils.getTruncatedCommandOutput(reportString.toString(), ACTIVITY_TEXT_SIZE_LIMIT_IN_BYTES, true, false, false);
} else if (truncated) {
mReportActivityMarkdownString = this.getString(R.string.msg_report_truncated) + reportString.toString();
} else {
mReportActivityMarkdownString = reportString.toString();
}
}
public static class NewInstanceResult {
/** An intent that can be used to start the {@link ReportActivity}. */
public Intent contentIntent;
/** An intent that can should be adding as the {@link android.app.Notification#deleteIntent}
* by a call to {@link android.app.PendingIntent#getBroadcast(Context, int, Intent, int)}
* so that {@link ReportActivityBroadcastReceiver} can do cleanup of {@link #EXTRA_REPORT_INFO_OBJECT_FILE_PATH}. */
public Intent deleteIntent;
NewInstanceResult(Intent contentIntent, Intent deleteIntent) {
this.contentIntent = contentIntent;
this.deleteIntent = deleteIntent;
}
}
/**
* Start the {@link ReportActivity}.
*
* @param context The {@link Context} for operations.
* @param reportInfo The {@link ReportInfo} containing info that needs to be displayed.
*/
public static void startReportActivity(@NonNull final Context context, @NonNull ReportInfo reportInfo) {
NewInstanceResult result = newInstance(context, reportInfo);
if (result.contentIntent == null) return;
context.startActivity(result.contentIntent);
}
/**
* Get content and delete intents for the {@link ReportActivity} that can be used to start it
* and do cleanup.
*
* If {@link ReportInfo} size is too large, then a TransactionTooLargeException will be thrown
* so its object may be saved to a file in the {@link Context#getCacheDir()}. Then when activity
* starts, its read back and the file is deleted in {@link #onDestroy()}.
* Note that files may still be left if {@link #onDestroy()} is not called or doesn't finish.
* A separate cleanup routine is implemented from that case by
* {@link #deleteReportInfoFilesOlderThanXDays(Context, int, boolean)} which should be called
* incrementally or at app startup.
*
* @param context The {@link Context} for operations.
* @param reportInfo The {@link ReportInfo} containing info that needs to be displayed.
* @return Returns {@link NewInstanceResult}.
*/
@NonNull
public static NewInstanceResult newInstance(@NonNull final Context context, @NonNull final ReportInfo reportInfo) {
long size = DataUtils.getSerializedSize(reportInfo);
if (size > DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES) {
String reportInfoDirectoryPath = getReportInfoDirectoryPath(context);
String reportInfoFilePath = reportInfoDirectoryPath + "/" + CACHE_FILE_BASENAME_PREFIX + reportInfo.reportTimestamp;
Logger.logVerbose(LOG_TAG, reportInfo.reportTitle + " " + ReportInfo.class.getSimpleName() + " serialized object size " + size + " is greater than " + DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES + " and it will be written to file at path \"" + reportInfoFilePath + "\"");
Error error = FileUtils.writeSerializableObjectToFile(ReportInfo.class.getSimpleName(), reportInfoFilePath, reportInfo);
if (error != null) {
Logger.logErrorExtended(LOG_TAG, error.toString());
Logger.showToast(context, Error.getMinimalErrorString(error), true);
return new NewInstanceResult(null, null);
}
return new NewInstanceResult(createContentIntent(context, null, reportInfoFilePath),
createDeleteIntent(context, reportInfoFilePath));
} else {
return new NewInstanceResult(createContentIntent(context, reportInfo, null),
null);
}
}
private static Intent createContentIntent(@NonNull final Context context, final ReportInfo reportInfo, final String reportInfoFilePath) {
Intent intent = new Intent(context, ReportActivity.class);
Bundle bundle = new Bundle();
if (reportInfoFilePath != null) {
bundle.putString(EXTRA_REPORT_INFO_OBJECT_FILE_PATH, reportInfoFilePath);
} else {
bundle.putSerializable(EXTRA_REPORT_INFO_OBJECT, reportInfo);
}
intent.putExtras(bundle);
// Note that ReportActivity should have `documentLaunchMode="intoExisting"` set in `AndroidManifest.xml`
// which has equivalent behaviour to FLAG_ACTIVITY_NEW_DOCUMENT.
// FLAG_ACTIVITY_SINGLE_TOP must also be passed for onNewIntent to be called.
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
return intent;
}
private static Intent createDeleteIntent(@NonNull final Context context, final String reportInfoFilePath) {
if (reportInfoFilePath == null) return null;
Intent intent = new Intent(context, ReportActivityBroadcastReceiver.class);
intent.setAction(ACTION_DELETE_REPORT_INFO_OBJECT_FILE);
Bundle bundle = new Bundle();
bundle.putString(EXTRA_REPORT_INFO_OBJECT_FILE_PATH, reportInfoFilePath);
intent.putExtras(bundle);
return intent;
}
@NotNull
private static String getReportInfoDirectoryPath(Context context) {
// Canonicalize to solve /data/data and /data/user/0 issues when comparing with reportInfoFilePath
return FileUtils.getCanonicalPath(context.getCacheDir().getAbsolutePath(), null) + "/" + CACHE_DIR_BASENAME;
}
private static void deleteReportInfoFile(Context context, String reportInfoFilePath) {
if (context == null || reportInfoFilePath == null) return;
// Extra protection for mainly if someone set `exported="true"` for ReportActivityBroadcastReceiver
String reportInfoDirectoryPath = getReportInfoDirectoryPath(context);
reportInfoFilePath = FileUtils.getCanonicalPath(reportInfoFilePath, null);
if(!reportInfoFilePath.equals(reportInfoDirectoryPath) && reportInfoFilePath.startsWith(reportInfoDirectoryPath + "/")) {
Logger.logVerbose(LOG_TAG, "Deleting " + ReportInfo.class.getSimpleName() + " serialized object file at path \"" + reportInfoFilePath + "\"");
Error error = FileUtils.deleteRegularFile(ReportInfo.class.getSimpleName(), reportInfoFilePath, true);
if (error != null) {
Logger.logErrorExtended(LOG_TAG, error.toString());
}
} else {
Logger.logError(LOG_TAG, "Not deleting " + ReportInfo.class.getSimpleName() + " serialized object file at path \"" + reportInfoFilePath + "\" since its not under \"" + reportInfoDirectoryPath + "\"");
}
}
/**
* Delete {@link ReportInfo} serialized object files from cache older than x days. If a notification
* has still not been opened after x days that's using a PendingIntent to ReportActivity, then
* opening the notification will throw a file not found error, so choose days value appropriately
* or check if a notification is still active if tracking notification ids.
* The {@link Context} object passed must be of the same package with which {@link #newInstance(Context, ReportInfo)}
* was called since a call to {@link Context#getCacheDir()} is made.
*
* @param context The {@link Context} for operations.
* @param days The x amount of days before which files should be deleted. This must be `>=0`.
* @param isSynchronous If set to {@code true}, then the command will be executed in the
* caller thread and results returned synchronously.
* If set to {@code false}, then a new thread is started run the commands
* asynchronously in the background and control is returned to the caller thread.
* @return Returns the {@code error} if deleting was not successful, otherwise {@code null}.
*/
public static Error deleteReportInfoFilesOlderThanXDays(@NonNull final Context context, int days, final boolean isSynchronous) {
if (isSynchronous) {
return deleteReportInfoFilesOlderThanXDaysInner(context, days);
} else {
new Thread() { public void run() {
Error error = deleteReportInfoFilesOlderThanXDaysInner(context, days);
if (error != null) {
Logger.logErrorExtended(LOG_TAG, error.toString());
}
}}.start();
return null;
}
}
private static Error deleteReportInfoFilesOlderThanXDaysInner(@NonNull final Context context, int days) {
// Only regular files are deleted and subdirectories are not checked
String reportInfoDirectoryPath = getReportInfoDirectoryPath(context);
Logger.logVerbose(LOG_TAG, "Deleting " + ReportInfo.class.getSimpleName() + " serialized object files under directory path \"" + reportInfoDirectoryPath + "\" older than " + days + " days");
return FileUtils.deleteFilesOlderThanXDays(ReportInfo.class.getSimpleName(), reportInfoDirectoryPath, null, days, true, FileType.REGULAR.getValue());
}
/**
* The {@link BroadcastReceiver} for {@link ReportActivity} that currently does cleanup when
* {@link android.app.Notification#deleteIntent} is called. It must be registered in `AndroidManifest.xml`.
*/
public static class ReportActivityBroadcastReceiver extends BroadcastReceiver {
private static final String LOG_TAG = "ReportActivityBroadcastReceiver";
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null) return;
String action = intent.getAction();
Logger.logVerbose(LOG_TAG, "onReceive: \"" + action + "\" action");
if (ACTION_DELETE_REPORT_INFO_OBJECT_FILE.equals(action)) {
Bundle bundle = intent.getExtras();
if (bundle == null) return;
if (bundle.containsKey(EXTRA_REPORT_INFO_OBJECT_FILE_PATH)) {
deleteReportInfoFile(context, bundle.getString(EXTRA_REPORT_INFO_OBJECT_FILE_PATH));
}
}
}
}
}

View File

@@ -0,0 +1,278 @@
package com.termux.shared.activities;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Typeface;
import android.os.Bundle;
import android.text.Editable;
import android.text.InputFilter;
import android.text.TextWatcher;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import com.termux.shared.interact.ShareUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.R;
import com.termux.shared.models.TextIOInfo;
import com.termux.shared.view.KeyboardUtils;
import org.jetbrains.annotations.NotNull;
import java.util.Locale;
/**
* An activity to edit or view text based on config passed as {@link TextIOInfo}.
*
* Add Following to `AndroidManifest.xml` to use in an app:
*
* {@code ` <activity android:name="com.termux.shared.activities.TextIOActivity" android:theme="@style/Theme.AppCompat.TermuxTextIOActivity" />` }
*/
public class TextIOActivity extends AppCompatActivity {
private static final String CLASS_NAME = ReportActivity.class.getCanonicalName();
public static final String EXTRA_TEXT_IO_INFO_OBJECT = CLASS_NAME + ".EXTRA_TEXT_IO_INFO_OBJECT";
private TextView mTextIOLabel;
private View mTextIOLabelSeparator;
private EditText mTextIOText;
private HorizontalScrollView mTextIOHorizontalScrollView;
private LinearLayout mTextIOTextLinearLayout;
private TextView mTextIOTextCharacterUsage;
private TextIOInfo mTextIOInfo;
private Bundle mBundle;
private static final String LOG_TAG = "TextIOActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Logger.logVerbose(LOG_TAG, "onCreate");
setContentView(R.layout.activity_text_io);
mTextIOLabel = findViewById(R.id.text_io_label);
mTextIOLabelSeparator = findViewById(R.id.text_io_label_separator);
mTextIOText = findViewById(R.id.text_io_text);
mTextIOHorizontalScrollView = findViewById(R.id.text_io_horizontal_scroll_view);
mTextIOTextLinearLayout = findViewById(R.id.text_io_text_linear_layout);
mTextIOTextCharacterUsage = findViewById(R.id.text_io_text_character_usage);
Toolbar toolbar = findViewById(R.id.toolbar);
if (toolbar != null) {
setSupportActionBar(toolbar);
}
mBundle = null;
Intent intent = getIntent();
if (intent != null)
mBundle = intent.getExtras();
else if (savedInstanceState != null)
mBundle = savedInstanceState;
updateUI();
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
Logger.logVerbose(LOG_TAG, "onNewIntent");
// Views must be re-created since different configs for isEditingTextDisabled() and
// isHorizontallyScrollable() will not work or at least reliably
finish();
startActivity(intent);
}
@SuppressLint("ClickableViewAccessibility")
private void updateUI() {
if (mBundle == null) {
finish(); return;
}
mTextIOInfo = (TextIOInfo) mBundle.getSerializable(EXTRA_TEXT_IO_INFO_OBJECT);
if (mTextIOInfo == null) {
finish(); return;
}
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
if (mTextIOInfo.getTitle() != null)
actionBar.setTitle(mTextIOInfo.getTitle());
else
actionBar.setTitle("Text Input");
if (mTextIOInfo.shouldShowBackButtonInActionBar()) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setDisplayShowHomeEnabled(true);
}
}
mTextIOLabel.setVisibility(View.GONE);
mTextIOLabelSeparator.setVisibility(View.GONE);
if (mTextIOInfo.isLabelEnabled()) {
mTextIOLabel.setVisibility(View.VISIBLE);
mTextIOLabelSeparator.setVisibility(View.VISIBLE);
mTextIOLabel.setText(mTextIOInfo.getLabel());
mTextIOLabel.setFilters(new InputFilter[] { new InputFilter.LengthFilter(TextIOInfo.LABEL_SIZE_LIMIT_IN_BYTES) });
mTextIOLabel.setTextSize(mTextIOInfo.getLabelSize());
mTextIOLabel.setTextColor(mTextIOInfo.getLabelColor());
mTextIOLabel.setTypeface(Typeface.create(mTextIOInfo.getLabelTypeFaceFamily(), mTextIOInfo.getLabelTypeFaceStyle()));
}
if (mTextIOInfo.isHorizontallyScrollable()) {
mTextIOHorizontalScrollView.setEnabled(true);
mTextIOText.setHorizontallyScrolling(true);
} else {
// Remove mTextIOHorizontalScrollView and add mTextIOText in its place
ViewGroup parent = (ViewGroup) mTextIOHorizontalScrollView.getParent();
if (parent != null && parent.indexOfChild(mTextIOText) < 0) {
ViewGroup.LayoutParams params = mTextIOHorizontalScrollView.getLayoutParams();
int index = parent.indexOfChild(mTextIOHorizontalScrollView);
mTextIOTextLinearLayout.removeAllViews();
mTextIOHorizontalScrollView.removeAllViews();
parent.removeView(mTextIOHorizontalScrollView);
parent.addView(mTextIOText, index, params);
mTextIOText.setHorizontallyScrolling(false);
}
}
mTextIOText.setText(mTextIOInfo.getText());
mTextIOText.setFilters(new InputFilter[] { new InputFilter.LengthFilter(mTextIOInfo.getTextLengthLimit()) });
mTextIOText.setTextSize(mTextIOInfo.getTextSize());
mTextIOText.setTextColor(mTextIOInfo.getTextColor());
mTextIOText.setTypeface(Typeface.create(mTextIOInfo.getTextTypeFaceFamily(), mTextIOInfo.getTextTypeFaceStyle()));
// setTextIsSelectable must be called after changing KeyListener to regain focusability and selectivity
if (mTextIOInfo.isEditingTextDisabled()) {
mTextIOText.setCursorVisible(false);
mTextIOText.setKeyListener(null);
mTextIOText.setTextIsSelectable(true);
}
if (mTextIOInfo.shouldShowTextCharacterUsage()) {
mTextIOTextCharacterUsage.setVisibility(View.VISIBLE);
updateTextIOTextCharacterUsage(mTextIOInfo.getText());
mTextIOText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable editable) {
if (editable != null)
updateTextIOTextCharacterUsage(editable.toString());
}
});
} else {
mTextIOTextCharacterUsage.setVisibility(View.GONE);
mTextIOText.addTextChangedListener(null);
}
}
private void updateTextIOInfoText() {
if (mTextIOText != null)
mTextIOInfo.setText(mTextIOText.getText().toString());
}
private void updateTextIOTextCharacterUsage(String text) {
if (text == null) text = "";
if (mTextIOTextCharacterUsage != null)
mTextIOTextCharacterUsage.setText(String.format(Locale.getDefault(), "%1$d/%2$d", text.length(), mTextIOInfo.getTextLengthLimit()));
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
updateTextIOInfoText();
outState.putSerializable(EXTRA_TEXT_IO_INFO_OBJECT, mTextIOInfo);
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
final MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_text_io, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
String text = "";
if (mTextIOText != null)
text = mTextIOText.getText().toString();
int id = item.getItemId();
if (id == android.R.id.home) {
confirm();
} if (id == R.id.menu_item_cancel) {
cancel();
} else if (id == R.id.menu_item_share_text) {
ShareUtils.shareText(this, mTextIOInfo.getTitle(), text);
} else if (id == R.id.menu_item_copy_text) {
ShareUtils.copyTextToClipboard(this, text, null);
}
return false;
}
@Override
public void onBackPressed() {
confirm();
}
/** Confirm current text and send it back to calling {@link Activity}. */
private void confirm() {
updateTextIOInfoText();
KeyboardUtils.hideSoftKeyboard(this, mTextIOText);
setResult(Activity.RESULT_OK, getResultIntent());
finish();
}
/** Cancel current text and notify calling {@link Activity}. */
private void cancel() {
KeyboardUtils.hideSoftKeyboard(this, mTextIOText);
setResult(Activity.RESULT_CANCELED, getResultIntent());
finish();
}
@NotNull
private Intent getResultIntent() {
Intent intent = new Intent();
Bundle bundle = new Bundle();
bundle.putSerializable(EXTRA_TEXT_IO_INFO_OBJECT, mTextIOInfo);
intent.putExtras(bundle);
return intent;
}
/**
* Get the {@link Intent} that can be used to start the {@link TextIOActivity}.
*
* @param context The {@link Context} for operations.
* @param textIOInfo The {@link TextIOInfo} containing info for the edit text.
*/
public static Intent newInstance(@NonNull final Context context, @NonNull final TextIOInfo textIOInfo) {
Intent intent = new Intent(context, TextIOActivity.class);
Bundle bundle = new Bundle();
bundle.putSerializable(EXTRA_TEXT_IO_INFO_OBJECT, textIOInfo);
intent.putExtras(bundle);
return intent;
}
}

View File

@@ -7,8 +7,8 @@ import androidx.annotation.NonNull;
import com.termux.shared.file.FileUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxUtils;
import com.termux.shared.models.errors.Error;
import com.termux.shared.termux.AndroidUtils;
import java.nio.charset.Charset;
@@ -17,58 +17,85 @@ import java.nio.charset.Charset;
*/
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private final Context context;
private final Context mContext;
private final CrashHandlerClient mCrashHandlerClient;
private final Thread.UncaughtExceptionHandler defaultUEH;
private static final String LOG_TAG = "CrashUtils";
private CrashHandler(final Context context) {
this.context = context;
private CrashHandler(@NonNull final Context context, @NonNull final CrashHandlerClient crashHandlerClient) {
this.mContext = context;
this.mCrashHandlerClient = crashHandlerClient;
this.defaultUEH = Thread.getDefaultUncaughtExceptionHandler();
}
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
logCrash(context,thread, throwable);
logCrash(mContext, mCrashHandlerClient, thread, throwable);
defaultUEH.uncaughtException(thread, throwable);
}
/**
* Set default uncaught crash handler of current thread to {@link CrashHandler}.
*/
public static void setCrashHandler(final Context context) {
public static void setCrashHandler(@NonNull final Context context, @NonNull final CrashHandlerClient crashHandlerClient) {
if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof CrashHandler)) {
Thread.setDefaultUncaughtExceptionHandler(new CrashHandler(context));
Thread.setDefaultUncaughtExceptionHandler(new CrashHandler(context, crashHandlerClient));
}
}
/**
* Log a crash in the crash log file at
* {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}.
* Log a crash in the crash log file at {@code crashlogFilePath}.
*
* @param context The {@link Context} for operations.
* @param crashHandlerClient The {@link CrashHandlerClient} implementation.
* @param thread The {@link Thread} in which the crash happened.
* @param throwable The {@link Throwable} thrown for the crash.
*/
public static void logCrash(final Context context, final Thread thread, final Throwable throwable) {
public static void logCrash(@NonNull final Context context, @NonNull final CrashHandlerClient crashHandlerClient, final Thread thread, final Throwable throwable) {
StringBuilder reportString = new StringBuilder();
reportString.append("## Crash Details\n");
reportString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Crash Thread", thread.toString(), "-"));
reportString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Crash Timestamp", TermuxUtils.getCurrentTimeStamp(), "-"));
reportString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Crash Timestamp", AndroidUtils.getCurrentMilliSecondUTCTimeStamp(), "-"));
reportString.append("\n\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Crash Message", throwable.getMessage(), "-"));
reportString.append("\n\n").append(Logger.getStackTracesMarkdownString("Stacktrace", Logger.getStackTracesStringArray(throwable)));
reportString.append("\n\n").append(Logger.getStackTracesMarkdownString("Stacktrace", Logger.getStackTraceStringArray(throwable)));
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, true));
reportString.append("\n\n").append(TermuxUtils.getDeviceInfoMarkdownString(context));
String appInfoMarkdownString = crashHandlerClient.getAppInfoMarkdownString(context);
if (appInfoMarkdownString != null && !appInfoMarkdownString.isEmpty())
reportString.append("\n\n").append(appInfoMarkdownString);
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context));
// Log report string to logcat
Logger.logError(reportString.toString());
// Write report string to crash log file
String errmsg = FileUtils.writeStringToFile(context, "crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, Charset.defaultCharset(), reportString.toString(), false);
if (errmsg != null) {
Logger.logError(LOG_TAG, errmsg);
Error error = FileUtils.writeStringToFile("crash log", crashHandlerClient.getCrashLogFilePath(context),
Charset.defaultCharset(), reportString.toString(), false);
if (error != null) {
Logger.logErrorExtended(LOG_TAG, error.toString());
}
}
public interface CrashHandlerClient {
/**
* Get crash log file path.
*
* @param context The {@link Context} passed to {@link CrashHandler#CrashHandler(Context, CrashHandlerClient)}.
* @return Should return the crash log file path.
*/
@NonNull
String getCrashLogFilePath(Context context);
/**
* Get app info markdown string to add to crash log.
*
* @param context The {@link Context} passed to {@link CrashHandler#CrashHandler(Context, CrashHandlerClient)}.
* @return Should return app info markdown string.
*/
String getAppInfoMarkdownString(Context context);
}
}

View File

@@ -0,0 +1,31 @@
package com.termux.shared.crash;
import android.content.Context;
import androidx.annotation.NonNull;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxUtils;
public class TermuxCrashUtils implements CrashHandler.CrashHandlerClient {
/**
* Set default uncaught crash handler of current thread to {@link CrashHandler} for Termux app
* and its plugin to log crashes at {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}.
*/
public static void setCrashHandler(@NonNull final Context context) {
CrashHandler.setCrashHandler(context, new TermuxCrashUtils());
}
@NonNull
@Override
public String getCrashLogFilePath(Context context) {
return TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH;
}
@Override
public String getAppInfoMarkdownString(Context context) {
return TermuxUtils.getAppInfoMarkdownString(context, true);
}
}

View File

@@ -2,12 +2,16 @@ package com.termux.shared.data;
import android.os.Bundle;
import java.util.LinkedHashSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import androidx.annotation.Nullable;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class DataUtils {
/** Max safe limit of data size to prevent TransactionTooLargeException when transferring data
* inside or to other apps via transactions. */
public static final int TRANSACTION_SIZE_LIMIT_IN_BYTES = 100 * 1024; // 100KB
private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
@@ -23,7 +27,7 @@ public class DataUtils {
if (maxLength < 0 || text.length() < maxLength) return text;
if (fromEnd) {
text = text.substring(0, Math.min(text.length(), maxLength));
text = text.substring(0, maxLength);
} else {
int cutOffIndex = text.length() - maxLength;
@@ -42,6 +46,21 @@ public class DataUtils {
return text;
}
/**
* Replace a sub string in each item of a {@link String[]}.
*
* @param array The {@link String[]} to replace in.
* @param find The sub string to replace.
* @param replace The sub string to replace with.
*/
public static void replaceSubStringsInStringArrayItems(String[] array, String find, String replace) {
if(array == null || array.length == 0) return;
for (int i = 0; i < array.length; i++) {
array[i] = array[i].replace(find, replace);
}
}
/**
* Get the {@code float} from a {@link String}.
*
@@ -80,6 +99,17 @@ public class DataUtils {
}
}
/**
* Get the {@code String} from an {@link Integer}.
*
* @param value The {@link Integer} value.
* @param def The default {@link String} value.
* @return Returns {@code value} if it is not {@code null}, otherwise returns {@code def}.
*/
public static String getStringFromInteger(Integer value, String def) {
return (value == null) ? def : String.valueOf((int) value);
}
/**
* Get the {@code hex string} from a {@link byte[]}.
*
@@ -139,97 +169,41 @@ public class DataUtils {
* @param def The default {@link Object}.
* @return Returns {@code object} if it is not {@code null}, otherwise returns {@code def}.
*/
public static <T> T getDefaultIfNull(@androidx.annotation.Nullable T object, @androidx.annotation.Nullable T def) {
public static <T> T getDefaultIfNull(@Nullable T object, @Nullable T def) {
return (object == null) ? def : object;
}
/**
* Get the {@link String} itself if it is not {@code null} or empty, otherwise default.
*
* @param value The {@link String} to check.
* @param def The default {@link String}.
* @return Returns {@code value} if it is not {@code null} or empty, otherwise returns {@code def}.
*/
public static String getDefaultIfUnset(@Nullable String value, String def) {
return (value == null || value.isEmpty()) ? def : value;
}
/** Check if a string is null or empty. */
public static boolean isNullOrEmpty(String string) {
return string == null || string.isEmpty();
}
public static LinkedHashSet<CharSequence> extractUrls(String text) {
StringBuilder regex_sb = new StringBuilder();
regex_sb.append("("); // Begin first matching group.
regex_sb.append("(?:"); // Begin scheme group.
regex_sb.append("dav|"); // The DAV proto.
regex_sb.append("dict|"); // The DICT proto.
regex_sb.append("dns|"); // The DNS proto.
regex_sb.append("file|"); // File path.
regex_sb.append("finger|"); // The Finger proto.
regex_sb.append("ftp(?:s?)|"); // The FTP proto.
regex_sb.append("git|"); // The Git proto.
regex_sb.append("gopher|"); // The Gopher proto.
regex_sb.append("http(?:s?)|"); // The HTTP proto.
regex_sb.append("imap(?:s?)|"); // The IMAP proto.
regex_sb.append("irc(?:[6s]?)|"); // The IRC proto.
regex_sb.append("ip[fn]s|"); // The IPFS proto.
regex_sb.append("ldap(?:s?)|"); // The LDAP proto.
regex_sb.append("pop3(?:s?)|"); // The POP3 proto.
regex_sb.append("redis(?:s?)|"); // The Redis proto.
regex_sb.append("rsync|"); // The Rsync proto.
regex_sb.append("rtsp(?:[su]?)|"); // The RTSP proto.
regex_sb.append("sftp|"); // The SFTP proto.
regex_sb.append("smb(?:s?)|"); // The SAMBA proto.
regex_sb.append("smtp(?:s?)|"); // The SMTP proto.
regex_sb.append("svn(?:(?:\\+ssh)?)|"); // The Subversion proto.
regex_sb.append("tcp|"); // The TCP proto.
regex_sb.append("telnet|"); // The Telnet proto.
regex_sb.append("tftp|"); // The TFTP proto.
regex_sb.append("udp|"); // The UDP proto.
regex_sb.append("vnc|"); // The VNC proto.
regex_sb.append("ws(?:s?)"); // The Websocket proto.
regex_sb.append(")://"); // End scheme group.
regex_sb.append(")"); // End first matching group.
// Begin second matching group.
regex_sb.append("(");
// User name and/or password in format 'user:pass@'.
regex_sb.append("(?:\\S+(?::\\S*)?@)?");
// Begin host group.
regex_sb.append("(?:");
// IP address (from http://www.regular-expressions.info/examples.html).
regex_sb.append("(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|");
// Host name or domain.
regex_sb.append("(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)(?:(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))?|");
// Just path. Used in case of 'file://' scheme.
regex_sb.append("/(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)");
// End host group.
regex_sb.append(")");
// Port number.
regex_sb.append("(?::\\d{1,5})?");
// Resource path with optional query string.
regex_sb.append("(?:/[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?");
// Fragment.
regex_sb.append("(?:#[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?");
// End second matching group.
regex_sb.append(")");
final Pattern urlPattern = Pattern.compile(
regex_sb.toString(),
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
LinkedHashSet<CharSequence> urlSet = new LinkedHashSet<>();
Matcher matcher = urlPattern.matcher(text);
while (matcher.find()) {
int matchStart = matcher.start(1);
int matchEnd = matcher.end();
String url = text.substring(matchStart, matchEnd);
urlSet.add(url);
/** Get size of a serializable object. */
public static long getSerializedSize(Serializable object) {
if (object == null) return 0;
try {
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteOutputStream);
objectOutputStream.writeObject(object);
objectOutputStream.flush();
objectOutputStream.close();
return byteOutputStream.toByteArray().length;
} catch (Exception e) {
return -1;
}
return urlSet;
}
}

View File

@@ -0,0 +1,166 @@
package com.termux.shared.data;
import android.content.Intent;
import android.os.Bundle;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import java.util.Arrays;
public class IntentUtils {
private static final String LOG_TAG = "IntentUtils";
/**
* Get a {@link String} extra from an {@link Intent} if its not {@code null} or empty.
*
* @param intent The {@link Intent} to get the extra from.
* @param key The {@link String} key name.
* @param def The default value if extra is not set.
* @param throwExceptionIfNotSet If set to {@code true}, then an exception will be thrown if extra
* is not set.
* @return Returns the {@link String} extra if set, otherwise {@code null}.
*/
public static String getStringExtraIfSet(@NonNull Intent intent, String key, String def, boolean throwExceptionIfNotSet) throws Exception {
String value = getStringExtraIfSet(intent, key, def);
if (value == null && throwExceptionIfNotSet)
throw new Exception("The \"" + key + "\" key string value is null or empty");
return value;
}
/**
* Get a {@link String} extra from an {@link Intent} if its not {@code null} or empty.
*
* @param intent The {@link Intent} to get the extra from.
* @param key The {@link String} key name.
* @param def The default value if extra is not set.
* @return Returns the {@link String} extra if set, otherwise {@code null}.
*/
public static String getStringExtraIfSet(@NonNull Intent intent, String key, String def) {
String value = intent.getStringExtra(key);
if (value == null || value.isEmpty()) {
if (def != null && !def.isEmpty())
return def;
else
return null;
}
return value;
}
/**
* Get an {@link Integer} from an {@link Intent} stored as a {@link String} extra if its not
* {@code null} or empty.
*
* @param intent The {@link Intent} to get the extra from.
* @param key The {@link String} key name.
* @param def The default value if extra is not set.
* @return Returns the {@link Integer} extra if set, otherwise {@code null}.
*/
public static Integer getIntegerExtraIfSet(@NonNull Intent intent, String key, Integer def) {
try {
String value = intent.getStringExtra(key);
if (value == null || value.isEmpty()) {
return def;
}
return Integer.parseInt(value);
}
catch (Exception e) {
return def;
}
}
/**
* Get a {@link String[]} extra from an {@link Intent} if its not {@code null} or empty.
*
* @param intent The {@link Intent} to get the extra from.
* @param key The {@link String} key name.
* @param def The default value if extra is not set.
* @param throwExceptionIfNotSet If set to {@code true}, then an exception will be thrown if extra
* is not set.
* @return Returns the {@link String[]} extra if set, otherwise {@code null}.
*/
public static String[] getStringArrayExtraIfSet(@NonNull Intent intent, String key, String[] def, boolean throwExceptionIfNotSet) throws Exception {
String[] value = getStringArrayExtraIfSet(intent, key, def);
if (value == null && throwExceptionIfNotSet)
throw new Exception("The \"" + key + "\" key string array is null or empty");
return value;
}
/**
* Get a {@link String[]} extra from an {@link Intent} if its not {@code null} or empty.
*
* @param intent The {@link Intent} to get the extra from.
* @param key The {@link String} key name.
* @param def The default value if extra is not set.
* @return Returns the {@link String[]} extra if set, otherwise {@code null}.
*/
public static String[] getStringArrayExtraIfSet(Intent intent, String key, String[] def) {
String[] value = intent.getStringArrayExtra(key);
if (value == null || value.length == 0) {
if (def != null && def.length != 0)
return def;
else
return null;
}
return value;
}
public static String getIntentString(Intent intent) {
if (intent == null) return null;
return intent.toString() + "\n" + getBundleString(intent.getExtras());
}
public static String getBundleString(Bundle bundle) {
if (bundle == null || bundle.size() == 0) return "Bundle[]";
StringBuilder bundleString = new StringBuilder("Bundle[\n");
boolean first = true;
for (String key : bundle.keySet()) {
if (!first)
bundleString.append("\n");
bundleString.append(key).append(": `");
Object value = bundle.get(key);
if (value instanceof int[]) {
bundleString.append(Arrays.toString((int[]) value));
} else if (value instanceof byte[]) {
bundleString.append(Arrays.toString((byte[]) value));
} else if (value instanceof boolean[]) {
bundleString.append(Arrays.toString((boolean[]) value));
} else if (value instanceof short[]) {
bundleString.append(Arrays.toString((short[]) value));
} else if (value instanceof long[]) {
bundleString.append(Arrays.toString((long[]) value));
} else if (value instanceof float[]) {
bundleString.append(Arrays.toString((float[]) value));
} else if (value instanceof double[]) {
bundleString.append(Arrays.toString((double[]) value));
} else if (value instanceof String[]) {
bundleString.append(Arrays.toString((String[]) value));
} else if (value instanceof CharSequence[]) {
bundleString.append(Arrays.toString((CharSequence[]) value));
} else if (value instanceof Parcelable[]) {
bundleString.append(Arrays.toString((Parcelable[]) value));
} else if (value instanceof Bundle) {
bundleString.append(getBundleString((Bundle) value));
} else {
bundleString.append(value);
}
bundleString.append("`");
first = false;
}
bundleString.append("\n]");
return bundleString.toString();
}
}

View File

@@ -0,0 +1,104 @@
package com.termux.shared.data;
import java.util.LinkedHashSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class UrlUtils {
public static Pattern URL_MATCH_REGEX;
public static Pattern getUrlMatchRegex() {
if (URL_MATCH_REGEX != null) return URL_MATCH_REGEX;
StringBuilder regex_sb = new StringBuilder();
regex_sb.append("("); // Begin first matching group.
regex_sb.append("(?:"); // Begin scheme group.
regex_sb.append("dav|"); // The DAV proto.
regex_sb.append("dict|"); // The DICT proto.
regex_sb.append("dns|"); // The DNS proto.
regex_sb.append("file|"); // File path.
regex_sb.append("finger|"); // The Finger proto.
regex_sb.append("ftp(?:s?)|"); // The FTP proto.
regex_sb.append("git|"); // The Git proto.
regex_sb.append("gemini|"); // The Gemini proto.
regex_sb.append("gopher|"); // The Gopher proto.
regex_sb.append("http(?:s?)|"); // The HTTP proto.
regex_sb.append("imap(?:s?)|"); // The IMAP proto.
regex_sb.append("irc(?:[6s]?)|"); // The IRC proto.
regex_sb.append("ip[fn]s|"); // The IPFS proto.
regex_sb.append("ldap(?:s?)|"); // The LDAP proto.
regex_sb.append("pop3(?:s?)|"); // The POP3 proto.
regex_sb.append("redis(?:s?)|"); // The Redis proto.
regex_sb.append("rsync|"); // The Rsync proto.
regex_sb.append("rtsp(?:[su]?)|"); // The RTSP proto.
regex_sb.append("sftp|"); // The SFTP proto.
regex_sb.append("smb(?:s?)|"); // The SAMBA proto.
regex_sb.append("smtp(?:s?)|"); // The SMTP proto.
regex_sb.append("svn(?:(?:\\+ssh)?)|"); // The Subversion proto.
regex_sb.append("tcp|"); // The TCP proto.
regex_sb.append("telnet|"); // The Telnet proto.
regex_sb.append("tftp|"); // The TFTP proto.
regex_sb.append("udp|"); // The UDP proto.
regex_sb.append("vnc|"); // The VNC proto.
regex_sb.append("ws(?:s?)"); // The Websocket proto.
regex_sb.append(")://"); // End scheme group.
regex_sb.append(")"); // End first matching group.
// Begin second matching group.
regex_sb.append("(");
// User name and/or password in format 'user:pass@'.
regex_sb.append("(?:\\S+(?::\\S*)?@)?");
// Begin host group.
regex_sb.append("(?:");
// IP address (from http://www.regular-expressions.info/examples.html).
regex_sb.append("(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|");
// Host name or domain.
regex_sb.append("(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)(?:(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))?|");
// Just path. Used in case of 'file://' scheme.
regex_sb.append("/(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)");
// End host group.
regex_sb.append(")");
// Port number.
regex_sb.append("(?::\\d{1,5})?");
// Resource path with optional query string.
regex_sb.append("(?:/[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?");
// Fragment.
regex_sb.append("(?:#[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?");
// End second matching group.
regex_sb.append(")");
URL_MATCH_REGEX = Pattern.compile(
regex_sb.toString(),
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
return URL_MATCH_REGEX;
}
public static LinkedHashSet<CharSequence> extractUrls(String text) {
LinkedHashSet<CharSequence> urlSet = new LinkedHashSet<>();
Matcher matcher = getUrlMatchRegex().matcher(text);
while (matcher.find()) {
int matchStart = matcher.start(1);
int matchEnd = matcher.end();
String url = text.substring(matchStart, matchEnd);
urlSet.add(url);
}
return urlSet;
}
}

View File

@@ -0,0 +1,377 @@
package com.termux.shared.file;
import android.content.Context;
import android.os.Environment;
import androidx.annotation.NonNull;
import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.models.ExecutionCommand;
import com.termux.shared.models.errors.Error;
import com.termux.shared.models.errors.FileUtilsErrno;
import com.termux.shared.shell.TermuxShellEnvironmentClient;
import com.termux.shared.shell.TermuxTask;
import com.termux.shared.termux.AndroidUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxUtils;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
public class TermuxFileUtils {
private static final String LOG_TAG = "TermuxFileUtils";
/**
* Replace "$PREFIX/" or "~/" prefix with termux absolute paths.
*
* @param paths The {@code paths} to expand.
* @return Returns the {@code expand paths}.
*/
public static List<String> getExpandedTermuxPaths(List<String> paths) {
if (paths == null) return null;
List<String> expandedPaths = new ArrayList<>();
for (int i = 0; i < paths.size(); i++) {
expandedPaths.add(getExpandedTermuxPath(paths.get(i)));
}
return expandedPaths;
}
/**
* Replace "$PREFIX/" or "~/" prefix with termux absolute paths.
*
* @param path The {@code path} to expand.
* @return Returns the {@code expand path}.
*/
public static String getExpandedTermuxPath(String path) {
if (path != null && !path.isEmpty()) {
path = path.replaceAll("^\\$PREFIX$", TermuxConstants.TERMUX_PREFIX_DIR_PATH);
path = path.replaceAll("^\\$PREFIX/", TermuxConstants.TERMUX_PREFIX_DIR_PATH + "/");
path = path.replaceAll("^~/$", TermuxConstants.TERMUX_HOME_DIR_PATH);
path = path.replaceAll("^~/", TermuxConstants.TERMUX_HOME_DIR_PATH + "/");
}
return path;
}
/**
* Replace termux absolute paths with "$PREFIX/" or "~/" prefix.
*
* @param paths The {@code paths} to unexpand.
* @return Returns the {@code unexpand paths}.
*/
public static List<String> getUnExpandedTermuxPaths(List<String> paths) {
if (paths == null) return null;
List<String> unExpandedPaths = new ArrayList<>();
for (int i = 0; i < paths.size(); i++) {
unExpandedPaths.add(getUnExpandedTermuxPath(paths.get(i)));
}
return unExpandedPaths;
}
/**
* Replace termux absolute paths with "$PREFIX/" or "~/" prefix.
*
* @param path The {@code path} to unexpand.
* @return Returns the {@code unexpand path}.
*/
public static String getUnExpandedTermuxPath(String path) {
if (path != null && !path.isEmpty()) {
path = path.replaceAll("^" + Pattern.quote(TermuxConstants.TERMUX_PREFIX_DIR_PATH) + "/", "\\$PREFIX/");
path = path.replaceAll("^" + Pattern.quote(TermuxConstants.TERMUX_HOME_DIR_PATH) + "/", "~/");
}
return path;
}
/**
* Get canonical path.
*
* @param path The {@code path} to convert.
* @param prefixForNonAbsolutePath Optional prefix path to prefix before non-absolute paths. This
* can be set to {@code null} if non-absolute paths should
* be prefixed with "/". The call to {@link File#getCanonicalPath()}
* will automatically do this anyways.
* @param expandPath The {@code boolean} that decides if input path is first attempted to be expanded by calling
* {@link TermuxFileUtils#getExpandedTermuxPath(String)} before its passed to
* {@link FileUtils#getCanonicalPath(String, String)}.
* @return Returns the {@code canonical path}.
*/
public static String getCanonicalPath(String path, final String prefixForNonAbsolutePath, final boolean expandPath) {
if (path == null) path = "";
if (expandPath)
path = getExpandedTermuxPath(path);
return FileUtils.getCanonicalPath(path, prefixForNonAbsolutePath);
}
/**
* Check if {@code path} is under the allowed termux working directory paths. If it is, then
* allowed parent path is returned.
*
* @param path The {@code path} to check.
* @return Returns the allowed path if it {@code path} is under it, otherwise {@link TermuxConstants#TERMUX_FILES_DIR_PATH}.
*/
public static String getMatchedAllowedTermuxWorkingDirectoryParentPathForPath(String path) {
if (path == null || path.isEmpty()) return TermuxConstants.TERMUX_FILES_DIR_PATH;
if (path.startsWith(TermuxConstants.TERMUX_STORAGE_HOME_DIR_PATH + "/")) {
return TermuxConstants.TERMUX_STORAGE_HOME_DIR_PATH;
} if (path.startsWith(Environment.getExternalStorageDirectory().getAbsolutePath() + "/")) {
return Environment.getExternalStorageDirectory().getAbsolutePath();
} else if (path.startsWith("/sdcard/")) {
return "/sdcard";
} else {
return TermuxConstants.TERMUX_FILES_DIR_PATH;
}
}
/**
* Validate the existence and permissions of directory file at path as a working directory for
* termux app.
*
* The creation of missing directory and setting of missing permissions will only be done if
* {@code path} is under paths returned by {@link #getMatchedAllowedTermuxWorkingDirectoryParentPathForPath(String)}.
*
* The permissions set to directory will be {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS}.
*
* @param label The optional label for the directory file. This can optionally be {@code null}.
* @param filePath The {@code path} for file to validate or create. Symlinks will not be followed.
* @param createDirectoryIfMissing The {@code boolean} that decides if directory file
* should be created if its missing.
* @param setPermissions The {@code boolean} that decides if permissions are to be
* automatically set defined by {@code permissionsToCheck}.
* @param setMissingPermissionsOnly The {@code boolean} that decides if only missing permissions
* are to be set or if they should be overridden.
* @param ignoreErrorsIfPathIsInParentDirPath The {@code boolean} that decides if existence
* and permission errors are to be ignored if path is
* in {@code parentDirPath}.
* @param ignoreIfNotExecutable The {@code boolean} that decides if missing executable permission
* error is to be ignored. This allows making an attempt to set
* executable permissions, but ignoring if it fails.
* @return Returns the {@code error} if path is not a directory file, failed to create it,
* or validating permissions failed, otherwise {@code null}.
*/
public static Error validateDirectoryFileExistenceAndPermissions(String label, final String filePath, final boolean createDirectoryIfMissing,
final boolean setPermissions, final boolean setMissingPermissionsOnly,
final boolean ignoreErrorsIfPathIsInParentDirPath, final boolean ignoreIfNotExecutable) {
return FileUtils.validateDirectoryFileExistenceAndPermissions(label, filePath,
TermuxFileUtils.getMatchedAllowedTermuxWorkingDirectoryParentPathForPath(filePath), createDirectoryIfMissing,
FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS, setPermissions, setMissingPermissionsOnly,
ignoreErrorsIfPathIsInParentDirPath, ignoreIfNotExecutable);
}
/**
* Validate if {@link TermuxConstants#TERMUX_FILES_DIR_PATH} exists and has
* {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS} permissions.
*
* This is required because binaries compiled for termux are hard coded with
* {@link TermuxConstants#TERMUX_PREFIX_DIR_PATH} and the path must be accessible.
*
* The permissions set to directory will be {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS}.
*
* This function does not create the directory manually but by calling {@link Context#getFilesDir()}
* so that android itself creates it. However, the call will not create its parent package
* data directory `/data/user/0/[package_name]` if it does not already exist and a `logcat`
* error will be logged by android.
* {@code Failed to ensure /data/user/0/<package_name>/files: mkdir failed: ENOENT (No such file or directory)}
* An android app normally can't create the package data directory since its parent `/data/user/0`
* is owned by `system` user and is normally created at app install or update time and not at app startup.
*
* Note that the path returned by {@link Context#getFilesDir()} may
* be under `/data/user/[id]/[package_name]` instead of `/data/data/[package_name]`
* defined by default by {@link TermuxConstants#TERMUX_FILES_DIR_PATH} where id will be 0 for
* primary user and a higher number for other users/profiles. If app is running under work profile
* or secondary user, then {@link TermuxConstants#TERMUX_FILES_DIR_PATH} will not be accessible
* and will not be automatically created, unless there is a bind mount from `/data/data` to
* `/data/user/[id]`, ideally in the right namespace.
* https://source.android.com/devices/tech/admin/multi-user
*
*
* On Android version `<=10`, the `/data/user/0` is a symlink to `/data/data` directory.
* https://cs.android.com/android/platform/superproject/+/android-10.0.0_r47:system/core/rootdir/init.rc;l=589
* {@code
* symlink /data/data /data/user/0
* }
*
* {@code
* /system/bin/ls -lhd /data/data /data/user/0
* drwxrwx--x 179 system system 8.0K 2021-xx-xx xx:xx /data/data
* lrwxrwxrwx 1 root root 10 2021-xx-xx xx:xx /data/user/0 -> /data/data
* }
*
* On Android version `>=11`, the `/data/data` directory is bind mounted at `/data/user/0`.
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:system/core/rootdir/init.rc;l=705
* https://cs.android.com/android/_/android/platform/system/core/+/3cca270e95ca8d8bc8b800e2b5d7da1825fd7100
* {@code
* # Unlink /data/user/0 if we previously symlink it to /data/data
* rm /data/user/0
*
* # Bind mount /data/user/0 to /data/data
* mkdir /data/user/0 0700 system system encryption=None
* mount none /data/data /data/user/0 bind rec
* }
*
* {@code
* /system/bin/grep -E '( /data )|( /data/data )|( /data/user/[0-9]+ )' /proc/self/mountinfo 2>&1 | /system/bin/grep -v '/data_mirror' 2>&1
* 87 32 253:5 / /data rw,nosuid,nodev,noatime shared:27 - ext4 /dev/block/dm-5 rw,seclabel,resgid=1065,errors=panic
* 91 87 253:5 /data /data/user/0 rw,nosuid,nodev,noatime shared:27 - ext4 /dev/block/dm-5 rw,seclabel,resgid=1065,errors=panic
* }
*
* The column 4 defines the root of the mount within the filesystem.
* Basically, `/dev/block/dm-5/` is mounted at `/data` and `/dev/block/dm-5/data` is mounted at
* `/data/user/0`.
* https://www.kernel.org/doc/Documentation/filesystems/proc.txt (section 3.5)
* https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt
* https://unix.stackexchange.com/a/571959
*
*
* Also note that running `/system/bin/ls -lhd /data/user/0/com.termux` as secondary user will result
* in `ls: /data/user/0/com.termux: Permission denied` where `0` is primary user id but running
* `/system/bin/ls -lhd /data/user/10/com.termux` will result in
* `drwx------ 6 u10_a149 u10_a149 4.0K 2021-xx-xx xx:xx /data/user/10/com.termux` where `10` is
* secondary user id. So can't stat directory (not contents) of primary user from secondary user
* but can the other way around. However, this is happening on android 10 avd, but not on android
* 11 avd.
*
* @param context The {@link Context} for operations.
* @param createDirectoryIfMissing The {@code boolean} that decides if directory file
* should be created if its missing.
* @param setMissingPermissions The {@code boolean} that decides if permissions are to be
* automatically set.
* @return Returns the {@code error} if path is not a directory file, failed to create it,
* or validating permissions failed, otherwise {@code null}.
*/
public static Error isTermuxFilesDirectoryAccessible(@NonNull final Context context, boolean createDirectoryIfMissing, boolean setMissingPermissions) {
if (createDirectoryIfMissing)
context.getFilesDir();
if (!FileUtils.directoryFileExists(TermuxConstants.TERMUX_FILES_DIR_PATH, true))
return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError("termux files directory", TermuxConstants.TERMUX_FILES_DIR_PATH);
if (setMissingPermissions)
FileUtils.setMissingFilePermissions("termux files directory", TermuxConstants.TERMUX_FILES_DIR_PATH,
FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS);
return FileUtils.checkMissingFilePermissions("termux files directory", TermuxConstants.TERMUX_FILES_DIR_PATH,
FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS, false);
}
/**
* Validate if {@link TermuxConstants#TERMUX_PREFIX_DIR_PATH} exists and has
* {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS} permissions.
* .
*
* The {@link TermuxConstants#TERMUX_PREFIX_DIR_PATH} directory would not exist if termux has
* not been installed or the bootstrap setup has not been run or if it was deleted by the user.
*
* @param createDirectoryIfMissing The {@code boolean} that decides if directory file
* should be created if its missing.
* @param setMissingPermissions The {@code boolean} that decides if permissions are to be
* automatically set.
* @return Returns the {@code error} if path is not a directory file, failed to create it,
* or validating permissions failed, otherwise {@code null}.
*/
public static Error isTermuxPrefixDirectoryAccessible(boolean createDirectoryIfMissing, boolean setMissingPermissions) {
return FileUtils.validateDirectoryFileExistenceAndPermissions("termux prefix directory", TermuxConstants.TERMUX_PREFIX_DIR_PATH,
null, createDirectoryIfMissing,
FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS, setMissingPermissions, true,
false, false);
}
/**
* Validate if {@link TermuxConstants#TERMUX_STAGING_PREFIX_DIR_PATH} exists and has
* {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS} permissions.
*
* @param createDirectoryIfMissing The {@code boolean} that decides if directory file
* should be created if its missing.
* @param setMissingPermissions The {@code boolean} that decides if permissions are to be
* automatically set.
* @return Returns the {@code error} if path is not a directory file, failed to create it,
* or validating permissions failed, otherwise {@code null}.
*/
public static Error isTermuxPrefixStagingDirectoryAccessible(boolean createDirectoryIfMissing, boolean setMissingPermissions) {
return FileUtils.validateDirectoryFileExistenceAndPermissions("termux prefix staging directory", TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH,
null, createDirectoryIfMissing,
FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS, setMissingPermissions, true,
false, false);
}
/**
* Get a markdown {@link String} for stat output for various Termux app files paths.
*
* @param context The context for operations.
* @return Returns the markdown {@link String}.
*/
public static String getTermuxFilesStatMarkdownString(@NonNull final Context context) {
Context termuxPackageContext = TermuxUtils.getTermuxPackageContext(context);
if (termuxPackageContext == null) return null;
// Also ensures that termux files directory is created if it does not already exist
String filesDir = termuxPackageContext.getFilesDir().getAbsolutePath();
// Build script
StringBuilder statScript = new StringBuilder();
statScript
.append("echo 'ls info:'\n")
.append("/system/bin/ls -lhdZ")
.append(" '/data/data'")
.append(" '/data/user/0'")
.append(" '" + TermuxConstants.TERMUX_INTERNAL_PRIVATE_APP_DATA_DIR_PATH + "'")
.append(" '/data/user/0/" + TermuxConstants.TERMUX_PACKAGE_NAME + "'")
.append(" '" + TermuxConstants.TERMUX_FILES_DIR_PATH + "'")
.append(" '" + filesDir + "'")
.append(" '/data/user/0/" + TermuxConstants.TERMUX_PACKAGE_NAME + "/files'")
.append(" '/data/user/" + TermuxConstants.TERMUX_PACKAGE_NAME + "/files'")
.append(" '" + TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH + "'")
.append(" '" + TermuxConstants.TERMUX_PREFIX_DIR_PATH + "'")
.append(" '" + TermuxConstants.TERMUX_HOME_DIR_PATH + "'")
.append(" '" + TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/login'")
.append(" 2>&1")
.append("\necho; echo 'mount info:'\n")
.append("/system/bin/grep -E '( /data )|( /data/data )|( /data/user/[0-9]+ )' /proc/self/mountinfo 2>&1 | /system/bin/grep -v '/data_mirror' 2>&1");
// Run script
ExecutionCommand executionCommand = new ExecutionCommand(1, "/system/bin/sh", null, statScript.toString() + "\n", "/", true, true);
executionCommand.commandLabel = TermuxConstants.TERMUX_APP_NAME + " Files Stat Command";
executionCommand.backgroundCustomLogLevel = Logger.LOG_LEVEL_OFF;
TermuxTask termuxTask = TermuxTask.execute(context, executionCommand, null, new TermuxShellEnvironmentClient(), true);
if (termuxTask == null || !executionCommand.isSuccessful()) {
Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
return null;
}
// Build script output
StringBuilder statOutput = new StringBuilder();
statOutput.append("$ ").append(statScript.toString());
statOutput.append("\n\n").append(executionCommand.resultData.stdout.toString());
boolean stderrSet = !executionCommand.resultData.stderr.toString().isEmpty();
if (executionCommand.resultData.exitCode != 0 || stderrSet) {
Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
if (stderrSet)
statOutput.append("\n").append(executionCommand.resultData.stderr.toString());
statOutput.append("\n").append("exit code: ").append(executionCommand.resultData.exitCode.toString());
}
// Build markdown output
StringBuilder markdownString = new StringBuilder();
markdownString.append("## ").append(TermuxConstants.TERMUX_APP_NAME).append(" Files Info\n\n");
AndroidUtils.appendPropertyToMarkdown(markdownString,"TERMUX_REQUIRED_FILES_DIR_PATH ($PREFIX)", TermuxConstants.TERMUX_FILES_DIR_PATH);
AndroidUtils.appendPropertyToMarkdown(markdownString,"ANDROID_ASSIGNED_FILES_DIR_PATH", filesDir);
markdownString.append("\n\n").append(MarkdownUtils.getMarkdownCodeForString(statOutput.toString(), true));
markdownString.append("\n##\n");
return markdownString.toString();
}
}

View File

@@ -90,7 +90,7 @@ public class FileTypes {
return getFileType(fileAttributes);
} catch (Exception e) {
// If not a ENOENT (No such file or directory) exception
if (!e.getMessage().contains("ENOENT"))
if (e.getMessage() != null && !e.getMessage().contains("ENOENT"))
Logger.logError("Failed to get file type for file at path \"" + filePath + "\": " + e.getMessage());
return FileType.NO_EXIST;
}

View File

@@ -6,6 +6,7 @@ import androidx.annotation.NonNull;
import com.termux.shared.file.FileUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.models.errors.Error;
import java.io.File;
import java.nio.charset.Charset;
@@ -31,18 +32,19 @@ public class FileUtilsTests {
Logger.logInfo(LOG_TAG, "Running tests");
Logger.logInfo(LOG_TAG, "testRootDirectoryPath: \"" + testRootDirectoryPath + "\"");
String fileUtilsTestsDirectoryCanonicalPath = FileUtils.getCanonicalPath(testRootDirectoryPath, null, false);
String fileUtilsTestsDirectoryCanonicalPath = FileUtils.getCanonicalPath(testRootDirectoryPath, null);
assertEqual("FileUtilsTests directory path is not a canonical path", testRootDirectoryPath, fileUtilsTestsDirectoryCanonicalPath);
runTestsInner(context, testRootDirectoryPath);
runTestsInner(testRootDirectoryPath);
Logger.logInfo(LOG_TAG, "All tests successful");
} catch (Exception e) {
Logger.logErrorAndShowToast(context, LOG_TAG, e.getMessage());
Logger.logErrorExtended(LOG_TAG, e.getMessage());
Logger.showToast(context, e.getMessage() != null ? e.getMessage().replaceAll("(?s)\nFull Error:\n.*", "") : null, true);
}
}
private static void runTestsInner(@NonNull final Context context, @NonNull final String testRootDirectoryPath) throws Exception {
String errmsg;
private static void runTestsInner(@NonNull final String testRootDirectoryPath) throws Exception {
Error error;
String label;
String path;
@@ -101,20 +103,20 @@ public class FileUtilsTests {
// Create or clear test root directory file
label = "testRootDirectoryPath";
errmsg = FileUtils.clearDirectory(context, label, testRootDirectoryPath);
assertEqual("Failed to create " + label + " directory file", null, errmsg);
error = FileUtils.clearDirectory(label, testRootDirectoryPath);
assertEqual("Failed to create " + label + " directory file", null, error);
if (!FileUtils.directoryFileExists(testRootDirectoryPath, false))
throwException("The " + label + " directory file does not exist as expected after creation");
// Create dir1 directory file
errmsg = FileUtils.createDirectoryFile(context, dir1_label, dir1_path);
assertEqual("Failed to create " + dir1_label + " directory file", null, errmsg);
error = FileUtils.createDirectoryFile(dir1_label, dir1_path);
assertEqual("Failed to create " + dir1_label + " directory file", null, error);
// Create dir2 directory file
errmsg = FileUtils.createDirectoryFile(context, dir2_label, dir2_path);
assertEqual("Failed to create " + dir2_label + " directory file", null, errmsg);
error = FileUtils.createDirectoryFile(dir2_label, dir2_path);
assertEqual("Failed to create " + dir2_label + " directory file", null, error);
@@ -122,29 +124,29 @@ public class FileUtilsTests {
// Create dir1/sub_dir1 directory file
label = dir1__sub_dir1_label; path = dir1__sub_dir1_path;
errmsg = FileUtils.createDirectoryFile(context, label, path);
assertEqual("Failed to create " + label + " directory file", null, errmsg);
error = FileUtils.createDirectoryFile(label, path);
assertEqual("Failed to create " + label + " directory file", null, error);
if (!FileUtils.directoryFileExists(path, false))
throwException("The " + label + " directory file does not exist as expected after creation");
// Create dir1/sub_reg1 regular file
label = dir1__sub_reg1_label; path = dir1__sub_reg1_path;
errmsg = FileUtils.createRegularFile(context, label, path);
assertEqual("Failed to create " + label + " regular file", null, errmsg);
error = FileUtils.createRegularFile(label, path);
assertEqual("Failed to create " + label + " regular file", null, error);
if (!FileUtils.regularFileExists(path, false))
throwException("The " + label + " regular file does not exist as expected after creation");
// Create dir1/sub_sym1 -> dir2 absolute symlink file
label = dir1__sub_sym1_label; path = dir1__sub_sym1_path;
errmsg = FileUtils.createSymlinkFile(context, label, dir2_path, path);
assertEqual("Failed to create " + label + " symlink file", null, errmsg);
error = FileUtils.createSymlinkFile(label, dir2_path, path);
assertEqual("Failed to create " + label + " symlink file", null, error);
if (!FileUtils.symlinkFileExists(path))
throwException("The " + label + " symlink file does not exist as expected after creation");
// Copy dir1/sub_sym1 symlink file to dir1/sub_sym2
label = dir1__sub_sym2_label; path = dir1__sub_sym2_path;
errmsg = FileUtils.copySymlinkFile(context, label, dir1__sub_sym1_path, path, false);
assertEqual("Failed to copy " + dir1__sub_sym1_label + " symlink file to " + label, null, errmsg);
error = FileUtils.copySymlinkFile(label, dir1__sub_sym1_path, path, false);
assertEqual("Failed to copy " + dir1__sub_sym1_label + " symlink file to " + label, null, error);
if (!FileUtils.symlinkFileExists(path))
throwException("The " + label + " symlink file does not exist as expected after copying it from " + dir1__sub_sym1_label);
if (!new File(path).getCanonicalPath().equals(dir2_path))
@@ -156,25 +158,25 @@ public class FileUtilsTests {
// Write "line1" to dir2/sub_reg1 regular file
label = dir2__sub_reg1_label; path = dir2__sub_reg1_path;
errmsg = FileUtils.writeStringToFile(context, label, path, Charset.defaultCharset(), "line1", false);
assertEqual("Failed to write string to " + label + " file with append mode false", null, errmsg);
error = FileUtils.writeStringToFile(label, path, Charset.defaultCharset(), "line1", false);
assertEqual("Failed to write string to " + label + " file with append mode false", null, error);
if (!FileUtils.regularFileExists(path, false))
throwException("The " + label + " file does not exist as expected after writing to it with append mode false");
// Write "line2" to dir2/sub_reg1 regular file
errmsg = FileUtils.writeStringToFile(context, label, path, Charset.defaultCharset(), "\nline2", true);
assertEqual("Failed to write string to " + label + " file with append mode true", null, errmsg);
error = FileUtils.writeStringToFile(label, path, Charset.defaultCharset(), "\nline2", true);
assertEqual("Failed to write string to " + label + " file with append mode true", null, error);
// Read dir2/sub_reg1 regular file
StringBuilder dataStringBuilder = new StringBuilder();
errmsg = FileUtils.readStringFromFile(context, label, path, Charset.defaultCharset(), dataStringBuilder, false);
assertEqual("Failed to read from " + label + " file", null, errmsg);
error = FileUtils.readStringFromFile(label, path, Charset.defaultCharset(), dataStringBuilder, false);
assertEqual("Failed to read from " + label + " file", null, error);
assertEqual("The data read from " + label + " file in not as expected", "line1\nline2", dataStringBuilder.toString());
// Copy dir2/sub_reg1 regular file to dir2/sub_reg2 file
label = dir2__sub_reg2_label; path = dir2__sub_reg2_path;
errmsg = FileUtils.copyRegularFile(context, label, dir2__sub_reg1_path, path, false);
assertEqual("Failed to copy " + dir2__sub_reg1_label + " regular file to " + label, null, errmsg);
error = FileUtils.copyRegularFile(label, dir2__sub_reg1_path, path, false);
assertEqual("Failed to copy " + dir2__sub_reg1_label + " regular file to " + label, null, error);
if (!FileUtils.regularFileExists(path, false))
throwException("The " + label + " regular file does not exist as expected after copying it from " + dir2__sub_reg1_label);
@@ -184,22 +186,22 @@ public class FileUtilsTests {
// Copy dir1 directory file to dir3
label = dir3_label; path = dir3_path;
errmsg = FileUtils.copyDirectoryFile(context, label, dir2_path, path, false);
assertEqual("Failed to copy " + dir2_label + " directory file to " + label, null, errmsg);
error = FileUtils.copyDirectoryFile(label, dir2_path, path, false);
assertEqual("Failed to copy " + dir2_label + " directory file to " + label, null, error);
if (!FileUtils.directoryFileExists(path, false))
throwException("The " + label + " directory file does not exist as expected after copying it from " + dir2_label);
// Copy dir1 directory file to dir3 again to test overwrite
label = dir3_label; path = dir3_path;
errmsg = FileUtils.copyDirectoryFile(context, label, dir2_path, path, false);
assertEqual("Failed to copy " + dir2_label + " directory file to " + label, null, errmsg);
error = FileUtils.copyDirectoryFile(label, dir2_path, path, false);
assertEqual("Failed to copy " + dir2_label + " directory file to " + label, null, error);
if (!FileUtils.directoryFileExists(path, false))
throwException("The " + label + " directory file does not exist as expected after copying it from " + dir2_label);
// Move dir3 directory file to dir4
label = dir4_label; path = dir4_path;
errmsg = FileUtils.moveDirectoryFile(context, label, dir3_path, path, false);
assertEqual("Failed to move " + dir3_label + " directory file to " + label, null, errmsg);
error = FileUtils.moveDirectoryFile(label, dir3_path, path, false);
assertEqual("Failed to move " + dir3_label + " directory file to " + label, null, error);
if (!FileUtils.directoryFileExists(path, false))
throwException("The " + label + " directory file does not exist as expected after copying it from " + dir3_label);
@@ -209,16 +211,16 @@ public class FileUtilsTests {
// Create dir1/sub_sym3 -> dir4 relative symlink file
label = dir1__sub_sym3_label; path = dir1__sub_sym3_path;
errmsg = FileUtils.createSymlinkFile(context, label, "../dir4", path);
assertEqual("Failed to create " + label + " symlink file", null, errmsg);
error = FileUtils.createSymlinkFile(label, "../dir4", path);
assertEqual("Failed to create " + label + " symlink file", null, error);
if (!FileUtils.symlinkFileExists(path))
throwException("The " + label + " symlink file does not exist as expected after creation");
// Create dir1/sub_sym3 -> dirX relative dangling symlink file
// This is to ensure that symlinkFileExists returns true if a symlink file exists but is dangling
label = dir1__sub_sym3_label; path = dir1__sub_sym3_path;
errmsg = FileUtils.createSymlinkFile(context, label, "../dirX", path);
assertEqual("Failed to create " + label + " symlink file", null, errmsg);
error = FileUtils.createSymlinkFile(label, "../dirX", path);
assertEqual("Failed to create " + label + " symlink file", null, error);
if (!FileUtils.symlinkFileExists(path))
throwException("The " + label + " dangling symlink file does not exist as expected after creation");
@@ -228,8 +230,8 @@ public class FileUtilsTests {
// Delete dir1/sub_sym2 symlink file
label = dir1__sub_sym2_label; path = dir1__sub_sym2_path;
errmsg = FileUtils.deleteSymlinkFile(context, label, path, false);
assertEqual("Failed to delete " + label + " symlink file", null, errmsg);
error = FileUtils.deleteSymlinkFile(label, path, false);
assertEqual("Failed to delete " + label + " symlink file", null, error);
if (FileUtils.fileExists(path, false))
throwException("The " + label + " symlink file still exist after deletion");
@@ -245,8 +247,8 @@ public class FileUtilsTests {
// Delete dir1 directory file
label = dir1_label; path = dir1_path;
errmsg = FileUtils.deleteDirectoryFile(context, label, path, false);
assertEqual("Failed to delete " + label + " directory file", null, errmsg);
error = FileUtils.deleteDirectoryFile(label, path, false);
assertEqual("Failed to delete " + label + " directory file", null, error);
if (FileUtils.fileExists(path, false))
throwException("The " + label + " directory file still exist after deletion");
@@ -267,8 +269,8 @@ public class FileUtilsTests {
// Delete dir2/sub_reg1 regular file
label = dir2__sub_reg1_label; path = dir2__sub_reg1_path;
errmsg = FileUtils.deleteRegularFile(context, label, path, false);
assertEqual("Failed to delete " + label + " regular file", null, errmsg);
error = FileUtils.deleteRegularFile(label, path, false);
assertEqual("Failed to delete " + label + " regular file", null, error);
if (FileUtils.fileExists(path, false))
throwException("The " + label + " regular file still exist after deletion");
@@ -276,6 +278,14 @@ public class FileUtilsTests {
FileUtils.getFileType("/dev/null", false);
}
public static void assertEqual(@NonNull final String message, final String expected, final Error actual) throws Exception {
String actualString = actual != null ? actual.getMessage() : null;
if (!equalsRegardingNull(expected, actualString))
throwException(message + "\nexpected: \"" + expected + "\"\nactual: \"" + actualString + "\"\nFull Error:\n" + (actual != null ? actual.toString() : ""));
}
public static void assertEqual(@NonNull final String message, final String expected, final String actual) throws Exception {
if (!equalsRegardingNull(expected, actual))
throwException(message + "\nexpected: \"" + expected + "\"\nactual: \"" + actual + "\"");

View File

@@ -0,0 +1,84 @@
package com.termux.shared.interact;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import com.termux.shared.R;
public class MessageDialogUtils {
/**
* Show a message in a dialog
*
* @param context The {@link Context} to use to start the dialog. An {@link Activity} {@link Context}
* must be passed, otherwise exceptions will be thrown.
* @param titleText The title text of the dialog.
* @param messageText The message text of the dialog.
* @param onDismiss The {@link DialogInterface.OnDismissListener} to run when dialog is dismissed.
*/
public static void showMessage(Context context, String titleText, String messageText, final DialogInterface.OnDismissListener onDismiss) {
showMessage(context, titleText, messageText, null, null, null, null, onDismiss);
}
/**
* Show a message in a dialog
*
* @param context The {@link Context} to use to start the dialog. An {@link Activity} {@link Context}
* must be passed, otherwise exceptions will be thrown.
* @param titleText The title text of the dialog.
* @param messageText The message text of the dialog.
* @param positiveText The positive button text of the dialog.
* @param onPositiveButton The {@link DialogInterface.OnClickListener} to run when positive button
* is pressed.
* @param negativeText The negative button text of the dialog. If this is {@code null}, then
* negative button will not be shown.
* @param onNegativeButton The {@link DialogInterface.OnClickListener} to run when negative button
* is pressed.
* @param onDismiss The {@link DialogInterface.OnDismissListener} to run when dialog is dismissed.
*/
public static void showMessage(Context context, String titleText, String messageText,
String positiveText,
final DialogInterface.OnClickListener onPositiveButton,
String negativeText,
final DialogInterface.OnClickListener onNegativeButton,
final DialogInterface.OnDismissListener onDismiss) {
AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.Theme_AppCompat_Light_Dialog);
LayoutInflater inflater = (LayoutInflater) context.getSystemService( Context.LAYOUT_INFLATER_SERVICE );
View view = inflater.inflate(R.layout.dialog_show_message, null);
if (view != null) {
builder.setView(view);
TextView titleView = view.findViewById(R.id.dialog_title);
if (titleView != null)
titleView.setText(titleText);
TextView messageView = view.findViewById(R.id.dialog_message);
if (messageView != null)
messageView.setText(messageText);
}
if (positiveText == null)
positiveText = context.getString(android.R.string.ok);
builder.setPositiveButton(positiveText, onPositiveButton);
if (negativeText != null)
builder.setNegativeButton(negativeText, onNegativeButton);
if (onDismiss != null)
builder.setOnDismissListener(onDismiss);
builder.show();
}
public static void exitAppWithErrorMessage(Context context, String titleText, String messageText) {
showMessage(context, titleText, messageText, dialog -> System.exit(0));
}
}

View File

@@ -1,16 +1,27 @@
package com.termux.shared.interact;
import android.Manifest;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Environment;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import com.termux.shared.R;
import com.termux.shared.data.DataUtils;
import com.termux.shared.data.IntentUtils;
import com.termux.shared.file.FileUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.models.errors.Error;
import com.termux.shared.packages.PermissionUtils;
import java.nio.charset.Charset;
public class ShareUtils {
@@ -30,7 +41,11 @@ public class ShareUtils {
chooserIntent.putExtra(Intent.EXTRA_INTENT, intent);
chooserIntent.putExtra(Intent.EXTRA_TITLE, title);
chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(chooserIntent);
try {
context.startActivity(chooserIntent);
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to open system chooser for:\n" + IntentUtils.getIntentString(chooserIntent), e);
}
}
/**
@@ -41,12 +56,12 @@ public class ShareUtils {
* @param text The text to share.
*/
public static void shareText(final Context context, final String subject, final String text) {
if (context == null) return;
if (context == null || text == null) return;
final Intent shareTextIntent = new Intent(Intent.ACTION_SEND);
shareTextIntent.setType("text/plain");
shareTextIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
shareTextIntent.putExtra(Intent.EXTRA_TEXT, DataUtils.getTruncatedCommandOutput(text, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false));
shareTextIntent.putExtra(Intent.EXTRA_TEXT, DataUtils.getTruncatedCommandOutput(text, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, true, false, false));
openSystemAppChooser(context, shareTextIntent, context.getString(R.string.title_share_with));
}
@@ -60,12 +75,12 @@ public class ShareUtils {
* clipboard is successful.
*/
public static void copyTextToClipboard(final Context context, final String text, final String toastString) {
if (context == null) return;
if (context == null || text == null) return;
final ClipboardManager clipboardManager = ContextCompat.getSystemService(context, ClipboardManager.class);
if (clipboardManager != null) {
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, text));
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, DataUtils.getTruncatedCommandOutput(text, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, true, false, false)));
if (toastString != null && !toastString.isEmpty())
Logger.showToast(context, toastString, true);
}
@@ -79,12 +94,61 @@ public class ShareUtils {
*/
public static void openURL(final Context context, final String url) {
if (context == null || url == null || url.isEmpty()) return;
Uri uri = Uri.parse(url);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
try {
Uri uri = Uri.parse(url);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
context.startActivity(intent);
} catch (ActivityNotFoundException e) {
// If no activity found to handle intent, show system chooser
openSystemAppChooser(context, intent, context.getString(R.string.title_open_url_with));
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to open the url \"" + url + "\"", e);
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to open url \"" + url + "\"", e);
}
}
/**
* Save a file at the path.
*
* If if path is under {@link Environment#getExternalStorageDirectory()}
* or `/sdcard` and storage permission is missing, it will be requested if {@code context} is an
* instance of {@link Activity} or {@link AppCompatActivity} and {@code storagePermissionRequestCode}
* is `>=0` and the function will automatically return. The caller should call this function again
* if user granted the permission.
*
* @param context The context for operations.
* @param label The label for file.
* @param filePath The path to save the file.
* @param text The text to write to file.
* @param showToast If set to {@code true}, then a toast is shown if saving to file is successful.
* @param storagePermissionRequestCode The request code to use while asking for permission.
*/
public static void saveTextToFile(final Context context, final String label, final String filePath, final String text, final boolean showToast, final int storagePermissionRequestCode) {
if (context == null || filePath == null || filePath.isEmpty() || text == null) return;
// If path is under primary external storage directory, then check for missing permissions.
if ((FileUtils.isPathInDirPath(filePath, Environment.getExternalStorageDirectory().getAbsolutePath(), true) ||
FileUtils.isPathInDirPath(filePath, "/sdcard", true)) &&
!PermissionUtils.checkPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
Logger.logErrorAndShowToast(context, LOG_TAG, context.getString(R.string.msg_storage_permission_not_granted));
if (storagePermissionRequestCode >= 0) {
if (context instanceof AppCompatActivity)
PermissionUtils.requestPermission(((AppCompatActivity) context), Manifest.permission.WRITE_EXTERNAL_STORAGE, storagePermissionRequestCode);
else if (context instanceof Activity)
PermissionUtils.requestPermission(((Activity) context), Manifest.permission.WRITE_EXTERNAL_STORAGE, storagePermissionRequestCode);
}
return;
}
Error error = FileUtils.writeStringToFile(label, filePath,
Charset.defaultCharset(), text, false);
if (error != null) {
Logger.logErrorExtended(LOG_TAG, error.toString());
Logger.showToast(context, Error.getMinimalErrorString(error), true);
} else {
if (showToast)
Logger.showToast(context, context.getString(R.string.msg_file_saved_successfully, label, filePath), true);
}
}

View File

@@ -16,7 +16,7 @@ import android.widget.TextView;
import com.termux.shared.R;
public final class DialogUtils {
public final class TextInputDialogUtils {
public interface TextSetListener {
void onTextSet(String text);
@@ -75,42 +75,4 @@ public final class DialogUtils {
dialogHolder[0].show();
}
/**
* Show a message in a dialog
*
* @param context The {@link Context} to use to start the dialog. An {@link Activity} {@link Context}
* must be passed, otherwise exceptions will be thrown.
* @param titleText The title text of the dialog.
* @param messageText The message text of the dialog.
* @param onDismiss The {@link DialogInterface.OnDismissListener} to run when dialog is dismissed.
*/
public static void showMessage(Context context, String titleText, String messageText, final DialogInterface.OnDismissListener onDismiss) {
AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.Theme_AppCompat_Light_Dialog)
.setPositiveButton(android.R.string.ok, null);
LayoutInflater inflater = (LayoutInflater) context.getSystemService( Context.LAYOUT_INFLATER_SERVICE );
View view = inflater.inflate(R.layout.dialog_show_message, null);
if (view != null) {
builder.setView(view);
TextView titleView = view.findViewById(R.id.dialog_title);
if (titleView != null)
titleView.setText(titleText);
TextView messageView = view.findViewById(R.id.dialog_message);
if (messageView != null)
messageView.setText(messageText);
}
if (onDismiss != null)
builder.setOnDismissListener(onDismiss);
builder.show();
}
public static void exitAppWithErrorMessage(Context context, String titleText, String messageText) {
showMessage(context, titleText, messageText, dialog -> System.exit(0));
}
}

View File

@@ -12,6 +12,7 @@ import com.termux.shared.termux.TermuxConstants;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -25,25 +26,78 @@ public class Logger {
public static final int LOG_LEVEL_VERBOSE = 3; // start logging verbose messages
public static final int DEFAULT_LOG_LEVEL = LOG_LEVEL_NORMAL;
public static final int MAX_LOG_LEVEL = LOG_LEVEL_VERBOSE;
private static int CURRENT_LOG_LEVEL = DEFAULT_LOG_LEVEL;
public static final int LOGGER_ENTRY_SIZE_LIMIT_IN_BYTES = 4 * 1024; // 4KB
/**
* The maximum size of the log entry payload that can be written to the logger. An attempt to
* write more than this amount will result in a truncated log entry.
*
* The limit is 4068 but this includes log tag and log level prefix "D/" before log tag and ": "
* suffix after it.
*
* #define LOGGER_ENTRY_MAX_PAYLOAD 4068
* https://cs.android.com/android/_/android/platform/system/core/+/android10-release:liblog/include/log/log_read.h;l=127
*/
public static final int LOGGER_ENTRY_MAX_PAYLOAD = 4068; // 4068 bytes
/**
* The maximum safe size of the log entry payload that can be written to the logger, based on
* {@link #LOGGER_ENTRY_MAX_PAYLOAD}. Using 4000 as a safe limit to give log tag and its
* prefix/suffix max 68 characters for itself. Use "log*Extended()" functions to use max possible
* limit if tag is already known.
*/
public static final int LOGGER_ENTRY_MAX_SAFE_PAYLOAD = 4000; // 4000 bytes
public static void logMessage(int logLevel, String tag, String message) {
if (logLevel == Log.ERROR && CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL)
public static void logMessage(int logPriority, String tag, String message) {
if (logPriority == Log.ERROR && CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL)
Log.e(getFullTag(tag), message);
else if (logLevel == Log.WARN && CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL)
else if (logPriority == Log.WARN && CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL)
Log.w(getFullTag(tag), message);
else if (logLevel == Log.INFO && CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL)
else if (logPriority == Log.INFO && CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL)
Log.i(getFullTag(tag), message);
else if (logLevel == Log.DEBUG && CURRENT_LOG_LEVEL >= LOG_LEVEL_DEBUG)
else if (logPriority == Log.DEBUG && CURRENT_LOG_LEVEL >= LOG_LEVEL_DEBUG)
Log.d(getFullTag(tag), message);
else if (logLevel == Log.VERBOSE && CURRENT_LOG_LEVEL >= LOG_LEVEL_VERBOSE)
else if (logPriority == Log.VERBOSE && CURRENT_LOG_LEVEL >= LOG_LEVEL_VERBOSE)
Log.v(getFullTag(tag), message);
}
public static void logExtendedMessage(int logLevel, String tag, String message) {
if (message == null) return;
int cutOffIndex;
int nextNewlineIndex;
String prefix = "";
// -8 for prefix "(xx/xx)" (max 99 sections), - log tag length, -4 for log tag prefix "D/" and suffix ": "
int maxEntrySize = LOGGER_ENTRY_MAX_PAYLOAD - 8 - getFullTag(tag).length() - 4;
List<String> messagesList = new ArrayList<>();
while(!message.isEmpty()) {
if (message.length() > maxEntrySize) {
cutOffIndex = maxEntrySize;
nextNewlineIndex = message.lastIndexOf('\n', cutOffIndex);
if (nextNewlineIndex != -1) {
cutOffIndex = nextNewlineIndex + 1;
}
messagesList.add(message.substring(0, cutOffIndex));
message = message.substring(cutOffIndex);
} else {
messagesList.add(message);
break;
}
}
for(int i=0; i<messagesList.size(); i++) {
if (messagesList.size() > 1)
prefix = "(" + (i + 1) + "/" + messagesList.size() + ")\n";
logMessage(logLevel, tag, prefix + messagesList.get(i));
}
}
public static void logError(String tag, String message) {
@@ -54,6 +108,14 @@ public class Logger {
logMessage(Log.ERROR, DEFAULT_LOG_TAG, message);
}
public static void logErrorExtended(String tag, String message) {
logExtendedMessage(Log.ERROR, tag, message);
}
public static void logErrorExtended(String message) {
logExtendedMessage(Log.ERROR, DEFAULT_LOG_TAG, message);
}
public static void logWarn(String tag, String message) {
@@ -64,6 +126,14 @@ public class Logger {
logMessage(Log.WARN, DEFAULT_LOG_TAG, message);
}
public static void logWarnExtended(String tag, String message) {
logExtendedMessage(Log.WARN, tag, message);
}
public static void logWarnExtended(String message) {
logExtendedMessage(Log.WARN, DEFAULT_LOG_TAG, message);
}
public static void logInfo(String tag, String message) {
@@ -74,6 +144,14 @@ public class Logger {
logMessage(Log.INFO, DEFAULT_LOG_TAG, message);
}
public static void logInfoExtended(String tag, String message) {
logExtendedMessage(Log.INFO, tag, message);
}
public static void logInfoExtended(String message) {
logExtendedMessage(Log.INFO, DEFAULT_LOG_TAG, message);
}
public static void logDebug(String tag, String message) {
@@ -84,6 +162,14 @@ public class Logger {
logMessage(Log.DEBUG, DEFAULT_LOG_TAG, message);
}
public static void logDebugExtended(String tag, String message) {
logExtendedMessage(Log.DEBUG, tag, message);
}
public static void logDebugExtended(String message) {
logExtendedMessage(Log.DEBUG, DEFAULT_LOG_TAG, message);
}
public static void logVerbose(String tag, String message) {
@@ -94,6 +180,18 @@ public class Logger {
logMessage(Log.VERBOSE, DEFAULT_LOG_TAG, message);
}
public static void logVerboseExtended(String tag, String message) {
logExtendedMessage(Log.VERBOSE, tag, message);
}
public static void logVerboseExtended(String message) {
logExtendedMessage(Log.VERBOSE, DEFAULT_LOG_TAG, message);
}
public static void logVerboseForce(String tag, String message) {
Log.v(tag, message);
}
public static void logErrorAndShowToast(Context context, String tag, String message) {
@@ -127,8 +225,7 @@ public class Logger {
public static void logStackTraceWithMessage(String tag, String message, Throwable throwable) {
if (CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL)
Log.e(getFullTag(tag), getMessageAndStackTraceString(message, throwable));
Logger.logErrorExtended(tag, getMessageAndStackTraceString(message, throwable));
}
public static void logStackTraceWithMessage(String message, Throwable throwable) {
@@ -143,11 +240,14 @@ public class Logger {
logStackTraceWithMessage(DEFAULT_LOG_TAG, null, throwable);
}
public static void logStackTracesWithMessage(String tag, String message, List<Throwable> throwableList) {
if (CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL)
Log.e(getFullTag(tag), getMessageAndStackTracesString(message, throwableList));
public static void logStackTracesWithMessage(String tag, String message, List<Throwable> throwablesList) {
Logger.logErrorExtended(tag, getMessageAndStackTracesString(message, throwablesList));
}
public static String getMessageAndStackTraceString(String message, Throwable throwable) {
if (message == null && throwable == null)
return null;
@@ -159,17 +259,19 @@ public class Logger {
return getStackTraceString(throwable);
}
public static String getMessageAndStackTracesString(String message, List<Throwable> throwableList) {
if (message == null && (throwableList == null || throwableList.size() == 0))
public static String getMessageAndStackTracesString(String message, List<Throwable> throwablesList) {
if (message == null && (throwablesList == null || throwablesList.size() == 0))
return null;
else if (message != null && (throwableList != null && throwableList.size() != 0))
return message + ":\n" + getStackTracesString(null, getStackTraceStringArray(throwableList));
else if (throwableList == null || throwableList.size() == 0)
else if (message != null && (throwablesList != null && throwablesList.size() != 0))
return message + ":\n" + getStackTracesString(null, getStackTracesStringArray(throwablesList));
else if (throwablesList == null || throwablesList.size() == 0)
return message;
else
return getStackTracesString(null, getStackTraceStringArray(throwableList));
return getStackTracesString(null, getStackTracesStringArray(throwablesList));
}
public static String getStackTraceString(Throwable throwable) {
if (throwable == null) return null;
@@ -182,27 +284,30 @@ public class Logger {
pw.close();
stackTraceString = errors.toString();
errors.close();
} catch (IOException e1) {
e1.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return stackTraceString;
}
public static String[] getStackTraceStringArray(Throwable throwable) {
return getStackTraceStringArray(Collections.singletonList(throwable));
public static String[] getStackTracesStringArray(Throwable throwable) {
return getStackTracesStringArray(Collections.singletonList(throwable));
}
public static String[] getStackTraceStringArray(List<Throwable> throwableList) {
if (throwableList == null) return null;
final String[] stackTraceStringArray = new String[throwableList.size()];
for (int i = 0; i < throwableList.size(); i++) {
stackTraceStringArray[i] = getStackTraceString(throwableList.get(i));
public static String[] getStackTracesStringArray(List<Throwable> throwablesList) {
if (throwablesList == null) return null;
final String[] stackTraceStringArray = new String[throwablesList.size()];
for (int i = 0; i < throwablesList.size(); i++) {
stackTraceStringArray[i] = getStackTraceString(throwablesList.get(i));
}
return stackTraceStringArray;
}
public static String getStackTracesString(String label, String[] stackTraceStringArray) {
if (label == null) label = "StackTraces:";
StringBuilder stackTracesString = new StringBuilder(label);
@@ -254,7 +359,7 @@ public class Logger {
else
return label + ": " + def;
}
public static void showToast(final Context context, final String toastText, boolean longDuration) {
@@ -283,7 +388,7 @@ public class Logger {
logLevelLabels[i] = getLogLevelLabel(context, Integer.parseInt(logLevels[i].toString()), addDefaultTag);
}
return logLevelLabels;
return logLevelLabels;
}
public static String getLogLevelLabel(final Context context, final int logLevel, final boolean addDefaultTag) {
@@ -309,7 +414,7 @@ public class Logger {
}
public static int setLogLevel(Context context, int logLevel) {
if (logLevel >= LOG_LEVEL_OFF && logLevel <= LOG_LEVEL_VERBOSE)
if (isLogLevelValid(logLevel))
CURRENT_LOG_LEVEL = logLevel;
else
CURRENT_LOG_LEVEL = DEFAULT_LOG_LEVEL;
@@ -327,4 +432,16 @@ public class Logger {
return DEFAULT_LOG_TAG + ":" + tag;
}
public static boolean isLogLevelValid(Integer logLevel) {
return (logLevel != null && logLevel >= LOG_LEVEL_OFF && logLevel <= MAX_LOG_LEVEL);
}
/** Check if custom log level is valid and >= {@link #CURRENT_LOG_LEVEL}. If custom log level is
* not valid then {@link #LOG_LEVEL_VERBOSE} must be >= {@link #CURRENT_LOG_LEVEL}. */
public static boolean shouldEnableLoggingForCustomLogLevel(Integer customLogLevel) {
if (customLogLevel == null || CURRENT_LOG_LEVEL <= LOG_LEVEL_OFF || customLogLevel <= LOG_LEVEL_OFF) return false;
customLogLevel = Logger.isLogLevelValid(customLogLevel) ? customLogLevel: Logger.LOG_LEVEL_VERBOSE;
return (customLogLevel >= CURRENT_LOG_LEVEL);
}
}

View File

@@ -90,10 +90,12 @@ public class MarkdownUtils {
int maxCount = 0;
int matchCount;
String match;
Matcher matcher = backticksPattern.matcher(string);
while(matcher.find()) {
matchCount = matcher.group(1).length();
match = matcher.group(1);
matchCount = match != null ? match.length() : 0;
if (matchCount > maxCount)
maxCount = matchCount;
}
@@ -117,9 +119,9 @@ public class MarkdownUtils {
return "**" + label + "**: " + def + "\n";
}
public static String getLinkMarkdownString(String label, Object object) {
if (object != null)
return "[" + label + "](" + object + ")";
public static String getLinkMarkdownString(String label, String url) {
if (url != null)
return "[" + label.replaceAll("]", "\\\\]") + "](" + url.replaceAll("\\)", "\\\\)") + ")";
else
return label;
}

View File

@@ -1,18 +1,17 @@
package com.termux.shared.models;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Intent;
import android.net.Uri;
import androidx.annotation.NonNull;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
import com.termux.shared.data.IntentUtils;
import com.termux.shared.models.errors.Error;
import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.data.DataUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ExecutionCommand {
@@ -52,12 +51,6 @@ public class ExecutionCommand {
}
// Define errCode values
// TODO: Define custom values for different cases
public final static int RESULT_CODE_OK = Activity.RESULT_OK;
public final static int RESULT_CODE_OK_MINOR_FAILURES = Activity.RESULT_FIRST_USER;
public final static int RESULT_CODE_FAILED = Activity.RESULT_FIRST_USER + 1;
public final static int RESULT_CODE_CANCELED = Activity.RESULT_FIRST_USER + 2;
/** The optional unique id for the {@link ExecutionCommand}. */
public Integer id;
@@ -90,6 +83,14 @@ public class ExecutionCommand {
/** If the {@link ExecutionCommand} is meant to start a failsafe terminal session. */
public boolean isFailsafe;
/**
* The {@link ExecutionCommand} custom log level for background {@link com.termux.shared.shell.TermuxTask}
* commands. By default, @link com.termux.shared.shell.StreamGobbler} only logs stdout and
* stderr if {@link Logger} `CURRENT_LOG_LEVEL` is >= {@link Logger#LOG_LEVEL_VERBOSE} and
* {@link com.termux.shared.shell.TermuxTask} only logs stdin if `CURRENT_LOG_LEVEL` is >=
* {@link Logger#LOG_LEVEL_DEBUG}.
*/
public Integer backgroundCustomLogLevel;
/** The session action of foreground commands. */
public String sessionAction;
@@ -110,33 +111,26 @@ public class ExecutionCommand {
public String pluginAPIHelp;
/** Defines the {@link Intent} received which started the command. */
public Intent commandIntent;
/** Defines if {@link ExecutionCommand} was started because of an external plugin request
* like {@link TERMUX_SERVICE#ACTION_SERVICE_EXECUTE} intent or from within Termux app itself. */
* like with an intent or from within Termux app itself. */
public boolean isPluginExecutionCommand;
/** Defines the {@link Intent} received from the external plugin which started the {@link ExecutionCommand}. */
public Intent pluginIntent;
/** Defines {@link PendingIntent} that should be sent if an external plugin requested the execution. */
public PendingIntent pluginPendingIntent;
/** Defines the {@link ResultConfig} for the {@link ExecutionCommand} containing information
* on how to handle the result. */
public final ResultConfig resultConfig = new ResultConfig();
/** The stdout of shell command. */
public String stdout;
/** The stderr of shell command. */
public String stderr;
/** The exit code of shell command. */
public Integer exitCode;
/** Defines the {@link ResultData} for the {@link ExecutionCommand} containing information
* of the result. */
public final ResultData resultData = new ResultData();
/** The internal error code of {@link ExecutionCommand}. */
public Integer errCode = RESULT_CODE_OK;
/** The internal error message of {@link ExecutionCommand}. */
public String errmsg;
/** The internal exceptions of {@link ExecutionCommand}. */
public List<Throwable> throwableList = new ArrayList<>();
/** Defines if processing results already called for this {@link ExecutionCommand}. */
public boolean processingResultsAlreadyCalled;
private static final String LOG_TAG = "ExecutionCommand";
public ExecutionCommand() {
@@ -156,13 +150,101 @@ public class ExecutionCommand {
this.isFailsafe = isFailsafe;
}
public boolean isPluginExecutionCommandWithPendingResult() {
return isPluginExecutionCommand && resultConfig.isCommandWithPendingResult();
}
public synchronized boolean setState(ExecutionState newState) {
// The state transition cannot go back or change if already at {@link ExecutionState#SUCCESS}
if (newState.getValue() < currentState.getValue() || currentState == ExecutionState.SUCCESS) {
Logger.logError(LOG_TAG, "Invalid "+ getCommandIdAndLabelLogString() + " state transition from \"" + currentState.getName() + "\" to " + "\"" + newState.getName() + "\"");
return false;
}
// The {@link ExecutionState#FAILED} can be set again, like to add more errors, but we don't update
// {@link #previousState} with the {@link #currentState} value if its at {@link ExecutionState#FAILED} to
// preserve the last valid state
if (currentState != ExecutionState.FAILED)
previousState = currentState;
currentState = newState;
return true;
}
public synchronized boolean hasExecuted() {
return currentState.getValue() >= ExecutionState.EXECUTED.getValue();
}
public synchronized boolean isExecuting() {
return currentState == ExecutionState.EXECUTING;
}
public synchronized boolean isSuccessful() {
return currentState == ExecutionState.SUCCESS;
}
public synchronized boolean setStateFailed(@NonNull Error error) {
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), null);
}
public synchronized boolean setStateFailed(@NonNull Error error, Throwable throwable) {
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), Collections.singletonList(throwable));
}
public synchronized boolean setStateFailed(@NonNull Error error, List<Throwable> throwablesList) {
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), throwablesList);
}
public synchronized boolean setStateFailed(int code, String message) {
return setStateFailed(null, code, message, null);
}
public synchronized boolean setStateFailed(int code, String message, Throwable throwable) {
return setStateFailed(null, code, message, Collections.singletonList(throwable));
}
public synchronized boolean setStateFailed(int code, String message, List<Throwable> throwablesList) {
return setStateFailed(null, code, message, throwablesList);
}
public synchronized boolean setStateFailed(String type, int code, String message, List<Throwable> throwablesList) {
if (!this.resultData.setStateFailed(type, code, message, throwablesList)) {
Logger.logWarn(LOG_TAG, "setStateFailed for " + getCommandIdAndLabelLogString() + " resultData encountered an error.");
}
return setState(ExecutionState.FAILED);
}
public synchronized boolean shouldNotProcessResults() {
if (processingResultsAlreadyCalled) {
return true;
} else {
processingResultsAlreadyCalled = true;
return false;
}
}
public synchronized boolean isStateFailed() {
if (currentState != ExecutionState.FAILED)
return false;
if (!resultData.isStateFailed()) {
Logger.logWarn(LOG_TAG, "The " + getCommandIdAndLabelLogString() + " has an invalid errCode value set in errors list while having ExecutionState.FAILED state.\n" + resultData.errorsList);
return false;
} else {
return true;
}
}
@NonNull
@Override
public String toString() {
if (!hasExecuted())
return getExecutionInputLogString(this, true);
return getExecutionInputLogString(this, true, true);
else {
return getExecutionOutputLogString(this, true);
return getExecutionOutputLogString(this, true, true, true);
}
}
@@ -171,9 +253,10 @@ public class ExecutionCommand {
*
* @param executionCommand The {@link ExecutionCommand} to convert.
* @param ignoreNull Set to {@code true} if non-critical {@code null} values are to be ignored.
* @param logStdin Set to {@code true} if {@link #stdin} should be logged.
* @return Returns the log friendly {@link String}.
*/
public static String getExecutionInputLogString(final ExecutionCommand executionCommand, boolean ignoreNull) {
public static String getExecutionInputLogString(final ExecutionCommand executionCommand, boolean ignoreNull, boolean logStdin) {
if (executionCommand == null) return "null";
StringBuilder logString = new StringBuilder();
@@ -190,15 +273,23 @@ public class ExecutionCommand {
logString.append("\n").append(executionCommand.getInBackgroundLogString());
logString.append("\n").append(executionCommand.getIsFailsafeLogString());
if (executionCommand.inBackground) {
if (logStdin && (!ignoreNull || !DataUtils.isNullOrEmpty(executionCommand.stdin)))
logString.append("\n").append(executionCommand.getStdinLogString());
if (!ignoreNull || executionCommand.backgroundCustomLogLevel != null)
logString.append("\n").append(executionCommand.getBackgroundCustomLogLevelLogString());
}
if (!ignoreNull || executionCommand.sessionAction != null)
logString.append("\n").append(executionCommand.getSessionActionLogString());
if (!ignoreNull || executionCommand.commandIntent != null)
logString.append("\n").append(executionCommand.getCommandIntentLogString());
logString.append("\n").append(executionCommand.getIsPluginExecutionCommandLogString());
if (!ignoreNull || executionCommand.isPluginExecutionCommand) {
if (!ignoreNull || executionCommand.pluginPendingIntent != null)
logString.append("\n").append(executionCommand.getPendingIntentCreatorLogString());
}
if (executionCommand.isPluginExecutionCommand)
logString.append("\n").append(ResultConfig.getResultConfigLogString(executionCommand.resultConfig, ignoreNull));
return logString.toString();
}
@@ -208,9 +299,11 @@ public class ExecutionCommand {
*
* @param executionCommand The {@link ExecutionCommand} to convert.
* @param ignoreNull Set to {@code true} if non-critical {@code null} values are to be ignored.
* @param logResultData Set to {@code true} if {@link #resultData} should be logged.
* @param logStdoutAndStderr Set to {@code true} if {@link ResultData#stdout} and {@link ResultData#stderr} should be logged.
* @return Returns the log friendly {@link String}.
*/
public static String getExecutionOutputLogString(final ExecutionCommand executionCommand, boolean ignoreNull) {
public static String getExecutionOutputLogString(final ExecutionCommand executionCommand, boolean ignoreNull, boolean logResultData, boolean logStdoutAndStderr) {
if (executionCommand == null) return "null";
StringBuilder logString = new StringBuilder();
@@ -220,32 +313,8 @@ public class ExecutionCommand {
logString.append("\n").append(executionCommand.getPreviousStateLogString());
logString.append("\n").append(executionCommand.getCurrentStateLogString());
logString.append("\n").append(executionCommand.getStdoutLogString());
logString.append("\n").append(executionCommand.getStderrLogString());
logString.append("\n").append(executionCommand.getExitCodeLogString());
logString.append(getExecutionErrLogString(executionCommand, ignoreNull));
return logString.toString();
}
/**
* Get a log friendly {@link String} for {@link ExecutionCommand} execution error parameters.
*
* @param executionCommand The {@link ExecutionCommand} to convert.
* @param ignoreNull Set to {@code true} if non-critical {@code null} values are to be ignored.
* @return Returns the log friendly {@link String}.
*/
public static String getExecutionErrLogString(final ExecutionCommand executionCommand, boolean ignoreNull) {
StringBuilder logString = new StringBuilder();
if (!ignoreNull || (executionCommand.isStateFailed())) {
logString.append("\n").append(executionCommand.getErrCodeLogString());
logString.append("\n").append(executionCommand.getErrmsgLogString());
logString.append("\n").append(executionCommand.geStackTracesLogString());
} else {
logString.append("");
}
if (logResultData)
logString.append("\n").append(ResultData.getResultDataLogString(executionCommand.resultData, logStdoutAndStderr));
return logString.toString();
}
@@ -261,8 +330,8 @@ public class ExecutionCommand {
StringBuilder logString = new StringBuilder();
logString.append(getExecutionInputLogString(executionCommand, false));
logString.append(getExecutionOutputLogString(executionCommand, false));
logString.append(getExecutionInputLogString(executionCommand, false, true));
logString.append(getExecutionOutputLogString(executionCommand, false, true, true));
logString.append("\n").append(executionCommand.getCommandDescriptionLogString());
logString.append("\n").append(executionCommand.getCommandHelpLogString());
@@ -295,22 +364,22 @@ public class ExecutionCommand {
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Working Directory", executionCommand.workingDirectory, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("inBackground", executionCommand.inBackground, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("isFailsafe", executionCommand.isFailsafe, "-"));
if (executionCommand.inBackground) {
if (!DataUtils.isNullOrEmpty(executionCommand.stdin))
markdownString.append("\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Stdin", executionCommand.stdin, "-"));
if (executionCommand.backgroundCustomLogLevel != null)
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Background Custom Log Level", executionCommand.backgroundCustomLogLevel, "-"));
}
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Session Action", executionCommand.sessionAction, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("isPluginExecutionCommand", executionCommand.isPluginExecutionCommand, "-"));
if (executionCommand.pluginPendingIntent != null)
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Pending Intent Creator", executionCommand.pluginPendingIntent.getCreatorPackage(), "-"));
else
markdownString.append("\n").append("**Pending Intent Creator:** - ");
markdownString.append("\n\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Stdout", executionCommand.stdout, "-"));
markdownString.append("\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Stderr", executionCommand.stderr, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Exit Code", executionCommand.exitCode, "-"));
markdownString.append("\n\n").append(ResultConfig.getResultConfigMarkdownString(executionCommand.resultConfig));
markdownString.append("\n\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Err Code", executionCommand.errCode, "-"));
markdownString.append("\n").append("**Errmsg:**\n").append(DataUtils.getDefaultIfNull(executionCommand.errmsg, "-"));
markdownString.append("\n\n").append(executionCommand.geStackTracesMarkdownString());
markdownString.append("\n\n").append(ResultData.getResultDataMarkdownString(executionCommand.resultData));
if (executionCommand.commandDescription != null || executionCommand.commandHelp != null) {
if (executionCommand.commandDescription != null)
@@ -329,7 +398,6 @@ public class ExecutionCommand {
}
public String getIdLogString() {
if (id != null)
return "(" + id + ") ";
@@ -376,21 +444,21 @@ public class ExecutionCommand {
return "isFailsafe: `" + isFailsafe + "`";
}
public String getIsPluginExecutionCommandLogString() {
return "isPluginExecutionCommand: `" + isPluginExecutionCommand + "`";
public String getStdinLogString() {
if (DataUtils.isNullOrEmpty(stdin))
return "Stdin: -";
else
return Logger.getMultiLineLogStringEntry("Stdin", stdin, "-");
}
public String getBackgroundCustomLogLevelLogString() {
return "Background Custom Log Level: `" + backgroundCustomLogLevel + "`";
}
public String getSessionActionLogString() {
return Logger.getSingleLineLogStringEntry("Session Action", sessionAction, "-");
}
public String getPendingIntentCreatorLogString() {
if (pluginPendingIntent != null)
return "Pending Intent Creator: `" + pluginPendingIntent.getCreatorPackage() + "`";
else
return "Pending Intent Creator: -";
}
public String getCommandDescriptionLogString() {
return Logger.getSingleLineLogStringEntry("Command Description", commandDescription, "-");
}
@@ -403,35 +471,49 @@ public class ExecutionCommand {
return Logger.getSingleLineLogStringEntry("Plugin API Help", pluginAPIHelp, "-");
}
public String getStdoutLogString() {
return Logger.getMultiLineLogStringEntry("Stdout", DataUtils.getTruncatedCommandOutput(stdout, Logger.LOGGER_ENTRY_SIZE_LIMIT_IN_BYTES / 5, false, false, true), "-");
public String getCommandIntentLogString() {
if (commandIntent == null)
return "Command Intent: -";
else
return Logger.getMultiLineLogStringEntry("Command Intent", IntentUtils.getIntentString(commandIntent), "-");
}
public String getStderrLogString() {
return Logger.getMultiLineLogStringEntry("Stderr", DataUtils.getTruncatedCommandOutput(stderr, Logger.LOGGER_ENTRY_SIZE_LIMIT_IN_BYTES / 5, false, false, true), "-");
}
public String getExitCodeLogString() {
return Logger.getSingleLineLogStringEntry("Exit Code", exitCode, "-");
}
public String getErrCodeLogString() {
return Logger.getSingleLineLogStringEntry("Err Code", errCode, "-");
}
public String getErrmsgLogString() {
return Logger.getMultiLineLogStringEntry("Errmsg", errmsg, "-");
}
public String geStackTracesLogString() {
return Logger.getStackTracesString("StackTraces:", Logger.getStackTraceStringArray(throwableList));
}
public String geStackTracesMarkdownString() {
return Logger.getStackTracesMarkdownString("StackTraces", Logger.getStackTraceStringArray(throwableList));
public String getIsPluginExecutionCommandLogString() {
return "isPluginExecutionCommand: `" + isPluginExecutionCommand + "`";
}
/**
* Get a log friendly {@link String} for {@link List<String>} argumentsArray.
* If argumentsArray are null or of size 0, then `Arguments: -` is returned. Otherwise
* following format is returned:
*
* Arguments:
* ```
* Arg 1: `value`
* Arg 2: 'value`
* ```
*
* @param argumentsArray The {@link String[]} argumentsArray to convert.
* @return Returns the log friendly {@link String}.
*/
public static String getArgumentsLogString(final String[] argumentsArray) {
StringBuilder argumentsString = new StringBuilder("Arguments:");
if (argumentsArray != null && argumentsArray.length != 0) {
argumentsString.append("\n```\n");
for (int i = 0; i != argumentsArray.length; i++) {
argumentsString.append(Logger.getSingleLineLogStringEntry("Arg " + (i + 1),
DataUtils.getTruncatedCommandOutput(argumentsArray[i], Logger.LOGGER_ENTRY_MAX_SAFE_PAYLOAD / 5, true, false, true),
"-")).append("\n");
}
argumentsString.append("```");
} else{
argumentsString.append(" -");
}
return argumentsString.toString();
}
/**
* Get a markdown {@link String} for {@link String[]} argumentsArray.
@@ -467,110 +549,4 @@ public class ExecutionCommand {
return argumentsString.toString();
}
/**
* Get a log friendly {@link String} for {@link List<String>} argumentsArray.
* If argumentsArray are null or of size 0, then `Arguments: -` is returned. Otherwise
* following format is returned:
*
* Arguments:
* ```
* Arg 1: `value`
* Arg 2: 'value`
* ```
*
* @param argumentsArray The {@link String[]} argumentsArray to convert.
* @return Returns the log friendly {@link String}.
*/
public static String getArgumentsLogString(final String[] argumentsArray) {
StringBuilder argumentsString = new StringBuilder("Arguments:");
if (argumentsArray != null && argumentsArray.length != 0) {
argumentsString.append("\n```\n");
for (int i = 0; i != argumentsArray.length; i++) {
argumentsString.append(Logger.getSingleLineLogStringEntry("Arg " + (i + 1),
DataUtils.getTruncatedCommandOutput(argumentsArray[i], Logger.LOGGER_ENTRY_SIZE_LIMIT_IN_BYTES / 5, true, false, true),
"-")).append("`\n");
}
argumentsString.append("```");
} else{
argumentsString.append(" -");
}
return argumentsString.toString();
}
public synchronized boolean setState(ExecutionState newState) {
// The state transition cannot go back or change if already at {@link ExecutionState#SUCCESS}
if (newState.getValue() < currentState.getValue() || currentState == ExecutionState.SUCCESS) {
Logger.logError("Invalid "+ getCommandIdAndLabelLogString() + " state transition from \"" + currentState.getName() + "\" to " + "\"" + newState.getName() + "\"");
return false;
}
// The {@link ExecutionState#FAILED} can be set again, like to add more errors, but we don't update
// {@link #previousState} with the {@link #currentState} value if its at {@link ExecutionState#FAILED} to
// preserve the last valid state
if (currentState != ExecutionState.FAILED)
previousState = currentState;
currentState = newState;
return true;
}
public synchronized boolean setStateFailed(int errCode, String errmsg, Throwable throwable) {
if (errCode > RESULT_CODE_OK) {
this.errCode = errCode;
} else {
Logger.logWarn("Ignoring invalid " + getCommandIdAndLabelLogString() + " errCode value \"" + errCode + "\". Force setting it to RESULT_CODE_FAILED \"" + RESULT_CODE_FAILED + "\"");
this.errCode = RESULT_CODE_FAILED;
}
this.errmsg = errmsg;
if (!setState(ExecutionState.FAILED))
return false;
if (this.throwableList == null)
this.throwableList = new ArrayList<>();
if (throwable != null)
this.throwableList.add(throwable);
return true;
}
public synchronized boolean shouldNotProcessResults() {
if (processingResultsAlreadyCalled) {
return true;
} else {
processingResultsAlreadyCalled = true;
return false;
}
}
public synchronized boolean isStateFailed() {
if (currentState != ExecutionState.FAILED)
return false;
if (errCode <= RESULT_CODE_OK) {
Logger.logWarn("The " + getCommandIdAndLabelLogString() + " has an invalid errCode value \"" + errCode + "\" while having ExecutionState.FAILED state.");
return false;
} else {
return true;
}
}
public synchronized boolean hasExecuted() {
return currentState.getValue() >= ExecutionState.EXECUTED.getValue();
}
public synchronized boolean isExecuting() {
return currentState == ExecutionState.EXECUTING;
}
public synchronized boolean isSuccessful() {
return currentState == ExecutionState.SUCCESS;
}
}

View File

@@ -1,40 +1,52 @@
package com.termux.app.models;
package com.termux.shared.models;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.termux.TermuxUtils;
import com.termux.shared.termux.AndroidUtils;
import java.io.Serializable;
/**
* An object that stored info for {@link com.termux.shared.activities.ReportActivity}.
*/
public class ReportInfo implements Serializable {
/** The user action that was being processed for which the report was generated. */
public final UserAction userAction;
public final String userAction;
/** The internal app component that sent the report. */
public final String sender;
/** The report title. */
public final String reportTitle;
/** The markdown report text prefix. Will not be part of copy and share operations, etc. */
public final String reportStringPrefix;
public String reportStringPrefix;
/** The markdown report text. */
public final String reportString;
public String reportString;
/** The markdown report text suffix. Will not be part of copy and share operations, etc. */
public final String reportStringSuffix;
public String reportStringSuffix;
/** If set to {@code true}, then report, app and device info will be added to the report when
* markdown is generated.
*/
public final boolean addReportInfoToMarkdown;
public final boolean addReportInfoHeaderToMarkdown;
/** The timestamp for the report. */
public final String reportTimestamp;
public ReportInfo(UserAction userAction, String sender, String reportTitle, String reportStringPrefix, String reportString, String reportStringSuffix, boolean addReportInfoToMarkdown) {
/** The label for the report file to save if user selects menu_item_save_report_to_file. */
public final String reportSaveFileLabel;
/** The path for the report file to save if user selects menu_item_save_report_to_file. */
public final String reportSaveFilePath;
public ReportInfo(String userAction, String sender, String reportTitle, String reportStringPrefix,
String reportString, String reportStringSuffix, boolean addReportInfoHeaderToMarkdown,
String reportSaveFileLabel, String reportSaveFilePath) {
this.userAction = userAction;
this.sender = sender;
this.reportTitle = reportTitle;
this.reportStringPrefix = reportStringPrefix;
this.reportString = reportString;
this.reportStringSuffix = reportStringSuffix;
this.addReportInfoToMarkdown = addReportInfoToMarkdown;
this.reportTimestamp = TermuxUtils.getCurrentTimeStamp();
this.addReportInfoHeaderToMarkdown = addReportInfoHeaderToMarkdown;
this.reportSaveFileLabel = reportSaveFileLabel;
this.reportSaveFilePath = reportSaveFilePath;
this.reportTimestamp = AndroidUtils.getCurrentMilliSecondUTCTimeStamp();
}
/**
@@ -48,7 +60,7 @@ public class ReportInfo implements Serializable {
StringBuilder markdownString = new StringBuilder();
if (reportInfo.addReportInfoToMarkdown) {
if (reportInfo.addReportInfoHeaderToMarkdown) {
markdownString.append("## Report Info\n\n");
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("User Action", reportInfo.userAction, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Sender", reportInfo.sender, "-"));

View File

@@ -0,0 +1,170 @@
package com.termux.shared.models;
import android.app.PendingIntent;
import androidx.annotation.NonNull;
import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import java.util.Formatter;
public class ResultConfig {
/** Defines {@link PendingIntent} that should be sent with the result of the command. We cannot
* implement {@link java.io.Serializable} because {@link PendingIntent} cannot be serialized. */
public PendingIntent resultPendingIntent;
/** The key with which to send result {@link android.os.Bundle} in {@link #resultPendingIntent}. */
public String resultBundleKey;
/** The key with which to send {@link ResultData#stdout} in {@link #resultPendingIntent}. */
public String resultStdoutKey;
/** The key with which to send {@link ResultData#stderr} in {@link #resultPendingIntent}. */
public String resultStderrKey;
/** The key with which to send {@link ResultData#exitCode} in {@link #resultPendingIntent}. */
public String resultExitCodeKey;
/** The key with which to send {@link ResultData#errorsList} errCode in {@link #resultPendingIntent}. */
public String resultErrCodeKey;
/** The key with which to send {@link ResultData#errorsList} errmsg in {@link #resultPendingIntent}. */
public String resultErrmsgKey;
/** The key with which to send original length of {@link ResultData#stdout} in {@link #resultPendingIntent}. */
public String resultStdoutOriginalLengthKey;
/** The key with which to send original length of {@link ResultData#stderr} in {@link #resultPendingIntent}. */
public String resultStderrOriginalLengthKey;
/** Defines the directory path in which to write the result of the command. */
public String resultDirectoryPath;
/** Defines the directory path under which {@link #resultDirectoryPath} can exist. */
public String resultDirectoryAllowedParentPath;
/** Defines whether the result should be written to a single file or multiple files
* (err, error, stdout, stderr, exit_code) in {@link #resultDirectoryPath}. */
public boolean resultSingleFile;
/** Defines the basename of the result file that should be created in {@link #resultDirectoryPath}
* if {@link #resultSingleFile} is {@code true}. */
public String resultFileBasename;
/** Defines the output {@link Formatter} format of the {@link #resultFileBasename} result file. */
public String resultFileOutputFormat;
/** Defines the error {@link Formatter} format of the {@link #resultFileBasename} result file. */
public String resultFileErrorFormat;
/** Defines the suffix of the result files that should be created in {@link #resultDirectoryPath}
* if {@link #resultSingleFile} is {@code true}. */
public String resultFilesSuffix;
public ResultConfig() {
}
public boolean isCommandWithPendingResult() {
return resultPendingIntent != null || resultDirectoryPath != null;
}
@NonNull
@Override
public String toString() {
return getResultConfigLogString(this, true);
}
/**
* Get a log friendly {@link String} for {@link ResultConfig} parameters.
*
* @param resultConfig The {@link ResultConfig} to convert.
* @param ignoreNull Set to {@code true} if non-critical {@code null} values are to be ignored.
* @return Returns the log friendly {@link String}.
*/
public static String getResultConfigLogString(final ResultConfig resultConfig, boolean ignoreNull) {
if (resultConfig == null) return "null";
StringBuilder logString = new StringBuilder();
logString.append("Result Pending: `").append(resultConfig.isCommandWithPendingResult()).append("`\n");
if (resultConfig.resultPendingIntent != null) {
logString.append(resultConfig.getResultPendingIntentVariablesLogString(ignoreNull));
if (resultConfig.resultDirectoryPath != null)
logString.append("\n");
}
if (resultConfig.resultDirectoryPath != null && !resultConfig.resultDirectoryPath.isEmpty())
logString.append(resultConfig.getResultDirectoryVariablesLogString(ignoreNull));
return logString.toString();
}
public String getResultPendingIntentVariablesLogString(boolean ignoreNull) {
if (resultPendingIntent == null) return "Result PendingIntent Creator: -";
StringBuilder resultPendingIntentVariablesString = new StringBuilder();
resultPendingIntentVariablesString.append("Result PendingIntent Creator: `").append(resultPendingIntent.getCreatorPackage()).append("`");
if (!ignoreNull || resultBundleKey != null)
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Bundle Key", resultBundleKey, "-"));
if (!ignoreNull || resultStdoutKey != null)
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Stdout Key", resultStdoutKey, "-"));
if (!ignoreNull || resultStderrKey != null)
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Stderr Key", resultStderrKey, "-"));
if (!ignoreNull || resultExitCodeKey != null)
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Exit Code Key", resultExitCodeKey, "-"));
if (!ignoreNull || resultErrCodeKey != null)
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Err Code Key", resultErrCodeKey, "-"));
if (!ignoreNull || resultErrmsgKey != null)
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Error Key", resultErrmsgKey, "-"));
if (!ignoreNull || resultStdoutOriginalLengthKey != null)
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Stdout Original Length Key", resultStdoutOriginalLengthKey, "-"));
if (!ignoreNull || resultStderrOriginalLengthKey != null)
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Stderr Original Length Key", resultStderrOriginalLengthKey, "-"));
return resultPendingIntentVariablesString.toString();
}
public String getResultDirectoryVariablesLogString(boolean ignoreNull) {
if (resultDirectoryPath == null) return "Result Directory Path: -";
StringBuilder resultDirectoryVariablesString = new StringBuilder();
resultDirectoryVariablesString.append(Logger.getSingleLineLogStringEntry("Result Directory Path", resultDirectoryPath, "-"));
resultDirectoryVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Single File", resultSingleFile, "-"));
if (!ignoreNull || resultFileBasename != null)
resultDirectoryVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result File Basename", resultFileBasename, "-"));
if (!ignoreNull || resultFileOutputFormat != null)
resultDirectoryVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result File Output Format", resultFileOutputFormat, "-"));
if (!ignoreNull || resultFileErrorFormat != null)
resultDirectoryVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result File Error Format", resultFileErrorFormat, "-"));
if (!ignoreNull || resultFilesSuffix != null)
resultDirectoryVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Files Suffix", resultFilesSuffix, "-"));
return resultDirectoryVariablesString.toString();
}
/**
* Get a markdown {@link String} for {@link ResultConfig}.
*
* @param resultConfig The {@link ResultConfig} to convert.
* @return Returns the markdown {@link String}.
*/
public static String getResultConfigMarkdownString(final ResultConfig resultConfig) {
if (resultConfig == null) return "null";
StringBuilder markdownString = new StringBuilder();
if (resultConfig.resultPendingIntent != null)
markdownString.append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result PendingIntent Creator", resultConfig.resultPendingIntent.getCreatorPackage(), "-"));
else
markdownString.append("**Result PendingIntent Creator:** - ");
if (resultConfig.resultDirectoryPath != null) {
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result Directory Path", resultConfig.resultDirectoryPath, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result Single File", resultConfig.resultSingleFile, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result File Basename", resultConfig.resultFileBasename, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result File Output Format", resultConfig.resultFileOutputFormat, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result File Error Format", resultConfig.resultFileErrorFormat, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result Files Suffix", resultConfig.resultFilesSuffix, "-"));
}
return markdownString.toString();
}
}

View File

@@ -0,0 +1,258 @@
package com.termux.shared.models;
import androidx.annotation.NonNull;
import com.termux.shared.data.DataUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.models.errors.Errno;
import com.termux.shared.models.errors.Error;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ResultData implements Serializable {
/** The stdout of command. */
public final StringBuilder stdout = new StringBuilder();
/** The stderr of command. */
public final StringBuilder stderr = new StringBuilder();
/** The exit code of command. */
public Integer exitCode;
/** The internal errors list of command. */
public List<Error> errorsList = new ArrayList<>();
public ResultData() {
}
public void clearStdout() {
stdout.setLength(0);
}
public StringBuilder prependStdout(String message) {
return stdout.insert(0, message);
}
public StringBuilder prependStdoutLn(String message) {
return stdout.insert(0, message + "\n");
}
public StringBuilder appendStdout(String message) {
return stdout.append(message);
}
public StringBuilder appendStdoutLn(String message) {
return stdout.append(message).append("\n");
}
public void clearStderr() {
stderr.setLength(0);
}
public StringBuilder prependStderr(String message) {
return stderr.insert(0, message);
}
public StringBuilder prependStderrLn(String message) {
return stderr.insert(0, message + "\n");
}
public StringBuilder appendStderr(String message) {
return stderr.append(message);
}
public StringBuilder appendStderrLn(String message) {
return stderr.append(message).append("\n");
}
public synchronized boolean setStateFailed(@NonNull Error error) {
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), null);
}
public synchronized boolean setStateFailed(@NonNull Error error, Throwable throwable) {
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), Collections.singletonList(throwable));
}
public synchronized boolean setStateFailed(@NonNull Error error, List<Throwable> throwablesList) {
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), throwablesList);
}
public synchronized boolean setStateFailed(int code, String message) {
return setStateFailed(null, code, message, null);
}
public synchronized boolean setStateFailed(int code, String message, Throwable throwable) {
return setStateFailed(null, code, message, Collections.singletonList(throwable));
}
public synchronized boolean setStateFailed(int code, String message, List<Throwable> throwablesList) {
return setStateFailed(null, code, message, throwablesList);
}
public synchronized boolean setStateFailed(String type, int code, String message, List<Throwable> throwablesList) {
if (errorsList == null)
errorsList = new ArrayList<>();
Error error = new Error();
errorsList.add(error);
return error.setStateFailed(type, code, message, throwablesList);
}
public boolean isStateFailed() {
if (errorsList != null) {
for (Error error : errorsList)
if (error.isStateFailed())
return true;
}
return false;
}
public int getErrCode() {
if (errorsList != null && errorsList.size() > 0)
return errorsList.get(errorsList.size() - 1).getCode();
else
return Errno.ERRNO_SUCCESS.getCode();
}
@NonNull
@Override
public String toString() {
return getResultDataLogString(this, true);
}
/**
* Get a log friendly {@link String} for {@link ResultData} parameters.
*
* @param resultData The {@link ResultData} to convert.
* @param logStdoutAndStderr Set to {@code true} if {@link #stdout} and {@link #stderr} should be logged.
* @return Returns the log friendly {@link String}.
*/
public static String getResultDataLogString(final ResultData resultData, boolean logStdoutAndStderr) {
if (resultData == null) return "null";
StringBuilder logString = new StringBuilder();
if (logStdoutAndStderr) {
logString.append("\n").append(resultData.getStdoutLogString());
logString.append("\n").append(resultData.getStderrLogString());
}
logString.append("\n").append(resultData.getExitCodeLogString());
logString.append("\n\n").append(getErrorsListLogString(resultData));
return logString.toString();
}
public String getStdoutLogString() {
if (stdout.toString().isEmpty())
return Logger.getSingleLineLogStringEntry("Stdout", null, "-");
else
return Logger.getMultiLineLogStringEntry("Stdout", DataUtils.getTruncatedCommandOutput(stdout.toString(), Logger.LOGGER_ENTRY_MAX_SAFE_PAYLOAD / 5, false, false, true), "-");
}
public String getStderrLogString() {
if (stderr.toString().isEmpty())
return Logger.getSingleLineLogStringEntry("Stderr", null, "-");
else
return Logger.getMultiLineLogStringEntry("Stderr", DataUtils.getTruncatedCommandOutput(stderr.toString(), Logger.LOGGER_ENTRY_MAX_SAFE_PAYLOAD / 5, false, false, true), "-");
}
public String getExitCodeLogString() {
return Logger.getSingleLineLogStringEntry("Exit Code", exitCode, "-");
}
public static String getErrorsListLogString(final ResultData resultData) {
if (resultData == null) return "null";
StringBuilder logString = new StringBuilder();
if (resultData.errorsList != null) {
for (Error error : resultData.errorsList) {
if (error.isStateFailed()) {
if (!logString.toString().isEmpty())
logString.append("\n");
logString.append(Error.getErrorLogString(error));
}
}
}
return logString.toString();
}
/**
* Get a markdown {@link String} for {@link ResultData}.
*
* @param resultData The {@link ResultData} to convert.
* @return Returns the markdown {@link String}.
*/
public static String getResultDataMarkdownString(final ResultData resultData) {
if (resultData == null) return "null";
StringBuilder markdownString = new StringBuilder();
if (resultData.stdout.toString().isEmpty())
markdownString.append(MarkdownUtils.getSingleLineMarkdownStringEntry("Stdout", null, "-"));
else
markdownString.append(MarkdownUtils.getMultiLineMarkdownStringEntry("Stdout", resultData.stdout.toString(), "-"));
if (resultData.stderr.toString().isEmpty())
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Stderr", null, "-"));
else
markdownString.append("\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Stderr", resultData.stderr.toString(), "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Exit Code", resultData.exitCode, "-"));
markdownString.append("\n\n").append(getErrorsListMarkdownString(resultData));
return markdownString.toString();
}
public static String getErrorsListMarkdownString(final ResultData resultData) {
if (resultData == null) return "null";
StringBuilder markdownString = new StringBuilder();
if (resultData.errorsList != null) {
for (Error error : resultData.errorsList) {
if (error.isStateFailed()) {
if (!markdownString.toString().isEmpty())
markdownString.append("\n");
markdownString.append(Error.getErrorMarkdownString(error));
}
}
}
return markdownString.toString();
}
public static String getErrorsListMinimalString(final ResultData resultData) {
if (resultData == null) return "null";
StringBuilder minimalString = new StringBuilder();
if (resultData.errorsList != null) {
for (Error error : resultData.errorsList) {
if (error.isStateFailed()) {
if (!minimalString.toString().isEmpty())
minimalString.append("\n");
minimalString.append(Error.getMinimalErrorString(error));
}
}
}
return minimalString.toString();
}
}

View File

@@ -0,0 +1,234 @@
package com.termux.shared.models;
import android.graphics.Color;
import android.graphics.Typeface;
import androidx.annotation.NonNull;
import com.termux.shared.activities.TextIOActivity;
import com.termux.shared.data.DataUtils;
import java.io.Serializable;
/**
* An object that stored info for {@link TextIOActivity}.
* Max text limit is 95KB to prevent TransactionTooLargeException as per
* {@link DataUtils#TRANSACTION_SIZE_LIMIT_IN_BYTES}. Larger size can be supported for in-app
* transactions by storing {@link TextIOInfo} as a serialized object in a file like
* {@link com.termux.shared.activities.ReportActivity} does.
*/
public class TextIOInfo implements Serializable {
public static final int GENERAL_DATA_SIZE_LIMIT_IN_BYTES = 1000;
public static final int LABEL_SIZE_LIMIT_IN_BYTES = 4000;
public static final int TEXT_SIZE_LIMIT_IN_BYTES = 100000 - GENERAL_DATA_SIZE_LIMIT_IN_BYTES - LABEL_SIZE_LIMIT_IN_BYTES; // < 100KB
/** The action for which {@link TextIOActivity} will be started. */
private final String mAction;
/** The internal app component that is will start the {@link TextIOActivity}. */
private final String mSender;
/** The activity title. */
private String mTitle;
/** If back button should be shown in {@link android.app.ActionBar}. */
private boolean mShowBackButtonInActionBar = false;
/** If label is enabled. */
private boolean mLabelEnabled = false;
/**
* The label of text input set in {@link android.widget.TextView} that can be updated by user.
* Max allowed length is {@link #LABEL_SIZE_LIMIT_IN_BYTES}.
*/
private String mLabel;
/** The text size of label. Defaults to 14sp. */
private int mLabelSize = 14;
/** The text color of label. Defaults to {@link Color#BLACK}. */
private int mLabelColor = Color.BLACK;
/** The {@link Typeface} family of label. Defaults to "sans-serif". */
private String mLabelTypeFaceFamily = "sans-serif";
/** The {@link Typeface} style of label. Defaults to {@link Typeface#BOLD}. */
private int mLabelTypeFaceStyle = Typeface.BOLD;
/**
* The text of text input set in {@link android.widget.EditText} that can be updated by user.
* Max allowed length is {@link #TEXT_SIZE_LIMIT_IN_BYTES}.
*/
private String mText;
/** The text size for text. Defaults to 12sp. */
private int mTextSize = 12;
/** The text size for text. Defaults to {@link #TEXT_SIZE_LIMIT_IN_BYTES}. */
private int mTextLengthLimit = TEXT_SIZE_LIMIT_IN_BYTES;
/** The text color of text. Defaults to {@link Color#BLACK}. */
private int mTextColor = Color.BLACK;
/** The {@link Typeface} family for text. Defaults to "sans-serif". */
private String mTextTypeFaceFamily = "sans-serif";
/** The {@link Typeface} style for text. Defaults to {@link Typeface#NORMAL}. */
private int mTextTypeFaceStyle = Typeface.NORMAL;
/** If horizontal scrolling should be enabled for text. */
private boolean mTextHorizontallyScrolling = false;
/** If character usage should be enabled for text. */
private boolean mShowTextCharacterUsage = false;
/** If editing text should be disabled so that text acts like its in a {@link android.widget.TextView}. */
private boolean mEditingTextDisabled = false;
public TextIOInfo(@NonNull String action, @NonNull String sender) {
mAction = action;
mSender = sender;
}
public String getAction() {
return mAction;
}
public String getSender() {
return mSender;
}
public String getTitle() {
return mTitle;
}
public void setTitle(String title) {
mTitle = title;
}
public boolean shouldShowBackButtonInActionBar() {
return mShowBackButtonInActionBar;
}
public void setShowBackButtonInActionBar(boolean showBackButtonInActionBar) {
mShowBackButtonInActionBar = showBackButtonInActionBar;
}
public boolean isLabelEnabled() {
return mLabelEnabled;
}
public void setLabelEnabled(boolean labelEnabled) {
mLabelEnabled = labelEnabled;
}
public String getLabel() {
return mLabel;
}
public void setLabel(String label) {
mLabel = DataUtils.getTruncatedCommandOutput(label, LABEL_SIZE_LIMIT_IN_BYTES, true, false, false);
}
public int getLabelSize() {
return mLabelSize;
}
public void setLabelSize(int labelSize) {
if (labelSize > 0)
mLabelSize = labelSize;
}
public int getLabelColor() {
return mLabelColor;
}
public void setLabelColor(int labelColor) {
mLabelColor = labelColor;
}
public String getLabelTypeFaceFamily() {
return mLabelTypeFaceFamily;
}
public void setLabelTypeFaceFamily(String labelTypeFaceFamily) {
mLabelTypeFaceFamily = labelTypeFaceFamily;
}
public int getLabelTypeFaceStyle() {
return mLabelTypeFaceStyle;
}
public void setLabelTypeFaceStyle(int labelTypeFaceStyle) {
mLabelTypeFaceStyle = labelTypeFaceStyle;
}
public String getText() {
return mText;
}
public void setText(String text) {
mText = DataUtils.getTruncatedCommandOutput(text, TEXT_SIZE_LIMIT_IN_BYTES, true, false, false);
}
public int getTextSize() {
return mTextSize;
}
public void setTextSize(int textSize) {
if (textSize > 0)
mTextSize = textSize;
}
public int getTextLengthLimit() {
return mTextLengthLimit;
}
public void setTextLengthLimit(int textLengthLimit) {
if (textLengthLimit < TEXT_SIZE_LIMIT_IN_BYTES)
mTextLengthLimit = textLengthLimit;
}
public int getTextColor() {
return mTextColor;
}
public void setTextColor(int textColor) {
mTextColor = textColor;
}
public String getTextTypeFaceFamily() {
return mTextTypeFaceFamily;
}
public void setTextTypeFaceFamily(String textTypeFaceFamily) {
mTextTypeFaceFamily = textTypeFaceFamily;
}
public int getTextTypeFaceStyle() {
return mTextTypeFaceStyle;
}
public void setTextTypeFaceStyle(int textTypeFaceStyle) {
mTextTypeFaceStyle = textTypeFaceStyle;
}
public boolean isHorizontallyScrollable() {
return mTextHorizontallyScrolling;
}
public void setTextHorizontallyScrolling(boolean textHorizontallyScrolling) {
mTextHorizontallyScrolling = textHorizontallyScrolling;
}
public boolean shouldShowTextCharacterUsage() {
return mShowTextCharacterUsage;
}
public void setShowTextCharacterUsage(boolean showTextCharacterUsage) {
mShowTextCharacterUsage = showTextCharacterUsage;
}
public boolean isEditingTextDisabled() {
return mEditingTextDisabled;
}
public void setEditingTextDisabled(boolean editingTextDisabled) {
mEditingTextDisabled = editingTextDisabled;
}
}

View File

@@ -0,0 +1,110 @@
package com.termux.shared.models.errors;
import android.app.Activity;
import androidx.annotation.NonNull;
import com.termux.shared.logger.Logger;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
/** The {@link Class} that defines error messages and codes. */
public class Errno {
private static final HashMap<String, Errno> map = new HashMap<>();
public static final String TYPE = "Error";
public static final Errno ERRNO_SUCCESS = new Errno(TYPE, Activity.RESULT_OK, "Success");
public static final Errno ERRNO_CANCELLED = new Errno(TYPE, Activity.RESULT_CANCELED, "Cancelled");
public static final Errno ERRNO_MINOR_FAILURES = new Errno(TYPE, Activity.RESULT_FIRST_USER, "Minor failure");
public static final Errno ERRNO_FAILED = new Errno(TYPE, Activity.RESULT_FIRST_USER + 1, "Failed");
/** The errno type. */
protected String type;
/** The errno code. */
protected final int code;
/** The errno message. */
protected final String message;
private static final String LOG_TAG = "Errno";
public Errno(final String type, final int code, final String message) {
this.type = type;
this.code = code;
this.message = message;
map.put(type + ":" + code, this);
}
@NonNull
@Override
public String toString() {
return "type=" + type + ", code=" + code + ", message=\"" + message + "\"";
}
public String getType() {
return type;
}
public String getMessage() {
return message;
}
public int getCode() {
return code;
}
/**
* Get the {@link Errno} of a specific type and code.
*
* @param type The unique type of the {@link Errno}.
* @param code The unique code of the {@link Errno}.
*/
public static Errno valueOf(String type, Integer code) {
if (type == null || type.isEmpty() || code == null) return null;
return map.get(type + ":" + code);
}
public Error getError() {
return new Error(getType(), getCode(), getMessage());
}
public Error getError(Object... args) {
try {
return new Error(getType(), getCode(), String.format(getMessage(), args));
} catch (Exception e) {
Logger.logWarn(LOG_TAG, "Exception raised while calling String.format() for error message of errno " + this + " with args" + Arrays.toString(args) + "\n" + e.getMessage());
// Return unformatted message as a backup
return new Error(getType(), getCode(), getMessage() + ": " + Arrays.toString(args));
}
}
public Error getError(Throwable throwable, Object... args) {
if (throwable == null)
return getError(args);
else
return getError(Collections.singletonList(throwable), args);
}
public Error getError(List<Throwable> throwablesList, Object... args) {
try {
if (throwablesList == null)
return new Error(getType(), getCode(), String.format(getMessage(), args));
else
return new Error(getType(), getCode(), String.format(getMessage(), args), throwablesList);
} catch (Exception e) {
Logger.logWarn(LOG_TAG, "Exception raised while calling String.format() for error message of errno " + this + " with args" + Arrays.toString(args) + "\n" + e.getMessage());
// Return unformatted message as a backup
return new Error(getType(), getCode(), getMessage() + ": " + Arrays.toString(args), throwablesList);
}
}
}

View File

@@ -0,0 +1,262 @@
package com.termux.shared.models.errors;
import androidx.annotation.NonNull;
import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Error implements Serializable {
/** The optional error label. */
private String label;
/** The error type. */
private String type;
/** The error code. */
private int code;
/** The error message. */
private String message;
/** The error exceptions. */
private List<Throwable> throwablesList = new ArrayList<>();
private static final String LOG_TAG = "Error";
public Error() {
InitError(null, null, null, null);
}
public Error(String type, Integer code, String message, List<Throwable> throwablesList) {
InitError(type, code, message, throwablesList);
}
public Error(String type, Integer code, String message, Throwable throwable) {
InitError(type, code, message, Collections.singletonList(throwable));
}
public Error(String type, Integer code, String message) {
InitError(type, code, message, null);
}
public Error(Integer code, String message, List<Throwable> throwablesList) {
InitError(null, code, message, throwablesList);
}
public Error(Integer code, String message, Throwable throwable) {
InitError(null, code, message, Collections.singletonList(throwable));
}
public Error(Integer code, String message) {
InitError(null, code, message, null);
}
public Error(String message, Throwable throwable) {
InitError(null, null, message, Collections.singletonList(throwable));
}
public Error(String message, List<Throwable> throwablesList) {
InitError(null, null, message, throwablesList);
}
public Error(String message) {
InitError(null, null, message, null);
}
private void InitError(String type, Integer code, String message, List<Throwable> throwablesList) {
if (type != null && !type.isEmpty())
this.type = type;
else
this.type = Errno.TYPE;
if (code != null && code > Errno.ERRNO_SUCCESS.getCode())
this.code = code;
else
this.code = Errno.ERRNO_SUCCESS.getCode();
this.message = message;
if (throwablesList != null)
this.throwablesList = throwablesList;
}
public Error setLabel(String label) {
this.label = label;
return this;
}
public String getLabel() {
return label;
}
public String getType() {
return type;
}
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
public void prependMessage(String message) {
if (message != null && isStateFailed())
this.message = message + this.message;
}
public void appendMessage(String message) {
if (message != null && isStateFailed())
this.message = this.message + message;
}
public List<Throwable> getThrowablesList() {
return Collections.unmodifiableList(throwablesList);
}
public synchronized boolean setStateFailed(@NonNull Error error) {
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), null);
}
public synchronized boolean setStateFailed(@NonNull Error error, Throwable throwable) {
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), Collections.singletonList(throwable));
}
public synchronized boolean setStateFailed(@NonNull Error error, List<Throwable> throwablesList) {
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), throwablesList);
}
public synchronized boolean setStateFailed(int code, String message) {
return setStateFailed(this.type, code, message, null);
}
public synchronized boolean setStateFailed(int code, String message, Throwable throwable) {
return setStateFailed(this.type, code, message, Collections.singletonList(throwable));
}
public synchronized boolean setStateFailed(int code, String message, List<Throwable> throwablesList) {
return setStateFailed(this.type, code, message, throwablesList);
}
public synchronized boolean setStateFailed(String type, int code, String message, List<Throwable> throwablesList) {
this.message = message;
this.throwablesList = throwablesList;
if (type != null && !type.isEmpty())
this.type = type;
if (code > Errno.ERRNO_SUCCESS.getCode()) {
this.code = code;
return true;
} else {
Logger.logWarn(LOG_TAG, "Ignoring invalid error code value \"" + code + "\". Force setting it to RESULT_CODE_FAILED \"" + Errno.ERRNO_FAILED.getCode() + "\"");
this.code = Errno.ERRNO_FAILED.getCode();
return false;
}
}
public boolean isStateFailed() {
return code > Errno.ERRNO_SUCCESS.getCode();
}
@NonNull
@Override
public String toString() {
return getErrorLogString(this);
}
/**
* Get a log friendly {@link String} for {@link Error} error parameters.
*
* @param error The {@link Error} to convert.
* @return Returns the log friendly {@link String}.
*/
public static String getErrorLogString(final Error error) {
if (error == null) return "null";
StringBuilder logString = new StringBuilder();
logString.append(error.getCodeString());
logString.append("\n").append(error.getTypeAndMessageLogString());
if (error.throwablesList != null)
logString.append("\n").append(error.geStackTracesLogString());
return logString.toString();
}
/**
* Get a minimal log friendly {@link String} for {@link Error} error parameters.
*
* @param error The {@link Error} to convert.
* @return Returns the log friendly {@link String}.
*/
public static String getMinimalErrorLogString(final Error error) {
if (error == null) return "null";
StringBuilder logString = new StringBuilder();
logString.append(error.getCodeString());
logString.append(error.getTypeAndMessageLogString());
return logString.toString();
}
/**
* Get a minimal {@link String} for {@link Error} error parameters.
*
* @param error The {@link Error} to convert.
* @return Returns the {@link String}.
*/
public static String getMinimalErrorString(final Error error) {
if (error == null) return "null";
StringBuilder logString = new StringBuilder();
logString.append("(").append(error.getCode()).append(") ");
logString.append(error.getType()).append(": ").append(error.getMessage());
return logString.toString();
}
/**
* Get a markdown {@link String} for {@link Error}.
*
* @param error The {@link Error} to convert.
* @return Returns the markdown {@link String}.
*/
public static String getErrorMarkdownString(final Error error) {
if (error == null) return "null";
StringBuilder markdownString = new StringBuilder();
markdownString.append(MarkdownUtils.getSingleLineMarkdownStringEntry("Error Code", error.getCode(), "-"));
markdownString.append("\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry((Errno.TYPE.equals(error.getType()) ? "Error Message" : "Error Message (" + error.getType() + ")"), error.message, "-"));
markdownString.append("\n\n").append(error.geStackTracesMarkdownString());
return markdownString.toString();
}
public String getCodeString() {
return Logger.getSingleLineLogStringEntry("Error Code", code, "-");
}
public String getTypeAndMessageLogString() {
return Logger.getMultiLineLogStringEntry(Errno.TYPE.equals(type) ? "Error Message" : "Error Message (" + type + ")", message, "-");
}
public String geStackTracesLogString() {
return Logger.getStackTracesString("StackTraces:", Logger.getStackTracesStringArray(throwablesList));
}
public String geStackTracesMarkdownString() {
return Logger.getStackTracesMarkdownString("StackTraces", Logger.getStackTracesStringArray(throwablesList));
}
}

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