Compare commits

...

135 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
132 changed files with 7299 additions and 1457 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

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

153
README.md
View File

@@ -3,6 +3,7 @@
[![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)
@@ -14,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.
@@ -25,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)
##
@@ -46,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.
##
@@ -92,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
@@ -106,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 = 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.6.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 116
versionName "0.116"
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.30-r1"
downloadBootstrap("aarch64", "ce56ce9a4e8845bd1d35cc2695bbdd636c72625ee10ce21c9b98ab38ebbee5ab", version)
downloadBootstrap("arm", "537e81951c7d3d3f3def9ce6778e1032457488e21edb2c037a1e0e680c39e747", version)
downloadBootstrap("i686", "3c2ca858c0225671c00c44ac182e31819ffa93ec624e95e02824e7d6d30ca1b4", version)
downloadBootstrap("x86_64", "93c50d36b45bca42bb014395e8e184e5b540adcad5d4e215f7e64ebf0d655d2b", 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"
@@ -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

@@ -13,6 +13,7 @@ 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;
@@ -72,11 +73,10 @@ public class RunCommandService extends Service {
errmsg = this.getString(R.string.error_run_command_service_invalid_intent_action, intent.getAction());
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return Service.START_NOT_STICKY;
return stopService();
}
executionCommand.executable = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH, null);
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);
/*
@@ -101,6 +101,7 @@ public class RunCommandService extends Service {
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 = 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);
@@ -121,11 +122,11 @@ public class RunCommandService extends Service {
// 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(Errno.ERRNO_FAILED.getCode(), errmsg);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true);
return Service.START_NOT_STICKY;
return stopService();
}
@@ -135,7 +136,7 @@ public class RunCommandService extends Service {
errmsg = this.getString(R.string.error_run_command_service_mandatory_extra_missing, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH);
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return Service.START_NOT_STICKY;
return stopService();
}
// Get canonical path of executable
@@ -147,10 +148,9 @@ public class RunCommandService extends Service {
FileUtils.APP_EXECUTABLE_FILE_PERMISSIONS, true, true,
false);
if (error != null) {
error.appendMessage("\n" + this.getString(R.string.msg_executable_absolute_path, executionCommand.executable));
executionCommand.setStateFailed(error);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return Service.START_NOT_STICKY;
return stopService();
}
@@ -169,18 +169,25 @@ public class RunCommandService extends Service {
true, true, true,
false, true);
if (error != null) {
error.appendMessage("\n" + this.getString(R.string.msg_working_directory_absolute_path, executionCommand.workingDirectory));
executionCommand.setStateFailed(error);
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return Service.START_NOT_STICKY;
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(TermuxFileUtils.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);
@@ -189,6 +196,7 @@ 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);
@@ -211,8 +219,11 @@ public class RunCommandService extends Service {
this.startService(execIntent);
}
runStopForeground();
return stopService();
}
private int stopService() {
runStopForeground();
return Service.START_NOT_STICKY;
}
@@ -234,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

@@ -29,10 +29,12 @@ 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;
@@ -43,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.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;
@@ -182,6 +185,9 @@ public final class TermuxActivity extends Activity implements ServiceConnection
// notification with the crash details if it did
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);
@@ -200,6 +206,8 @@ 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);
@@ -259,7 +267,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
if (mTermuxTerminalViewClient != null)
mTermuxTerminalViewClient.onStart();
if (!mProperties.isTerminalMarginAdjustmentDisabled())
if (mPreferences.isTerminalMarginAdjustmentEnabled())
addTermuxActivityRootViewGlobalLayoutListener();
registerTermuxActivityBroadcastReceiver();
@@ -357,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) {
@@ -372,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());
@@ -412,6 +420,13 @@ public final class TermuxActivity extends Activity implements ServiceConnection
}
}
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() {
@@ -452,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();
@@ -469,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) *
@@ -479,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();
}
@@ -744,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();
}
@@ -850,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

@@ -5,7 +5,6 @@ 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;
@@ -13,10 +12,14 @@ import android.view.WindowManager;
import com.termux.R;
import com.termux.app.utils.CrashUtils;
import com.termux.shared.file.FileUtils;
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;
@@ -28,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/>
@@ -53,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);
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.");
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(PREFIX_FILE_PATH, false)) {
Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" does not exist but another file exists at its destination.");
} 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);
@@ -92,24 +115,35 @@ final class TermuxInstaller {
Error error;
final String STAGING_PREFIX_PATH = TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH;
final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH);
// Delete prefix staging directory or any file at its destination
error = FileUtils.deleteFile("prefix staging directory", STAGING_PREFIX_PATH, true);
error = FileUtils.deleteFile("termux prefix staging directory", TERMUX_STAGING_PREFIX_DIR_PATH, true);
if (error != null) {
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
return;
}
// Delete prefix directory or any file at its destination
error = FileUtils.deleteFile("prefix directory", PREFIX_FILE_PATH, true);
error = FileUtils.deleteFile("termux prefix directory", TERMUX_PREFIX_DIR_PATH, true);
if (error != null) {
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
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);
@@ -126,23 +160,23 @@ 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));
error = ensureDirectoryExists(new File(newPath).getParentFile());
if (error != null) {
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
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();
error = ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile());
if (error != null) {
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
return;
}
@@ -152,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);
}
@@ -167,17 +202,17 @@ 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) {
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)));
showBootstrapErrorDialog(activity, whenDone, Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)));
} finally {
activity.runOnUiThread(() -> {
@@ -192,11 +227,11 @@ final class TermuxInstaller {
}.start();
}
public static void showBootstrapErrorDialog(Activity activity, String PREFIX_FILE_PATH, Runnable whenDone, String message) {
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
CrashUtils.sendCrashReportNotification(activity, LOG_TAG, "## Bootstrap Error\n\n" + message, true);
sendBootstrapCrashReportNotification(activity, message);
activity.runOnUiThread(() -> {
try {
@@ -207,7 +242,7 @@ final class TermuxInstaller {
})
.setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
dialog.dismiss();
FileUtils.deleteFile("prefix directory", PREFIX_FILE_PATH, true);
FileUtils.deleteFile("termux prefix directory", TERMUX_PREFIX_DIR_PATH, true);
TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone);
}).show();
} catch (WindowManager.BadTokenException e1) {
@@ -216,6 +251,13 @@ final class TermuxInstaller {
});
}
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";
@@ -231,7 +273,7 @@ final class TermuxInstaller {
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);
CrashUtils.sendCrashReportNotification(context, LOG_TAG, "## Setup Storage Error\n\n" + Error.getErrorMarkdownString(error), true, true);
return;
}
@@ -270,7 +312,7 @@ final class TermuxInstaller {
} catch (Exception 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);
CrashUtils.sendCrashReportNotification(context, LOG_TAG, "## Setup Storage Error\n\n" + Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)), true, true);
}
}
}.start();

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,6 +19,8 @@ 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;
@@ -47,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.
@@ -364,10 +364,11 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
executionCommand.arguments = IntentUtils.getStringArrayExtraIfSet(intent, TERMUX_SERVICE.EXTRA_ARGUMENTS, null);
if (executionCommand.inBackground)
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 = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_WORKDIR, null);
executionCommand.isFailsafe = intent.getBooleanExtra(TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION, false);
executionCommand.isFailsafe = intent.getBooleanExtra(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false);
executionCommand.sessionAction = intent.getStringExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION);
executionCommand.commandLabel = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_LABEL, "Execution Intent Command");
executionCommand.commandDescription = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, null);
@@ -426,7 +427,7 @@ 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, new TermuxShellEnvironmentClient(), false);
if (newTermuxTask == null) {
@@ -518,7 +519,7 @@ 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,
@@ -707,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
@@ -731,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

@@ -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;
@@ -11,11 +12,15 @@ import androidx.preference.PreferenceFragmentCompat;
import com.termux.R;
import com.termux.shared.activities.ReportActivity;
import com.termux.shared.file.FileUtils;
import com.termux.shared.models.ReportInfo;
import com.termux.app.models.UserAction;
import com.termux.shared.interact.ShareUtils;
import com.termux.shared.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;
@@ -53,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);
}
}
@@ -86,7 +121,13 @@ public class SettingsActivity extends AppCompatActivity {
aboutString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context));
aboutString.append("\n\n").append(TermuxUtils.getImportantLinksMarkdownString(context));
ReportActivity.startReportActivity(context, new ReportInfo(UserAction.ABOUT.getName(), TermuxConstants.TERMUX_APP.TERMUX_SETTINGS_ACTIVITY_NAME, title, null, aboutString.toString(), null, false));
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

@@ -20,7 +20,7 @@ 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;

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;
@@ -22,15 +23,18 @@ 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.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;
@@ -39,6 +43,7 @@ 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;
@@ -72,6 +77,10 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
this.mTermuxTerminalSessionClient = termuxTerminalSessionClient;
}
public TermuxActivity getActivity() {
return mActivity;
}
/**
* Should be called when mActivity.onCreate() is called
*/
@@ -133,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() {
@@ -165,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
@@ -186,6 +211,11 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
return mActivity.getProperties().isUsingCtrlSpaceWorkaround();
}
@Override
public boolean isTerminalViewSelected() {
return mActivity.getTerminalToolbarViewPager() == null || mActivity.isTerminalViewSelected();
}
@Override
@@ -204,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);
@@ -281,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
@@ -484,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,
@@ -492,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
@@ -508,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;
}
@@ -541,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
@@ -628,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;
});
});
@@ -649,20 +703,26 @@ 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(AndroidUtils.getDeviceInfoMarkdownString(mActivity));
@@ -671,7 +731,21 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
if (termuxAptInfo != null)
reportString.append("\n\n").append(termuxAptInfo);
ReportActivity.startReportActivity(mActivity, new ReportInfo(UserAction.REPORT_ISSUE_FROM_TRANSCRIPT.getName(), TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY_NAME, title, null, reportString.toString(), "\n\n" + TermuxUtils.getReportIssueMarkdownString(mActivity), false));
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,7 +4,7 @@ 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;
@@ -20,6 +20,7 @@ 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;
@@ -86,7 +87,7 @@ public class CrashUtils {
Logger.logDebug(logTag, "A crash log file found at \"" + TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH + "\".");
sendCrashReportNotification(context, logTag, reportString, false);
sendCrashReportNotification(context, logTag, reportString, false, false);
}
}.start();
}
@@ -97,13 +98,15 @@ public class CrashUtils {
*
* @param context The {@link Context} for operations.
* @param logTag The log tag to use for logging.
* @param reportString The text for the crash report.
* @param 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 reportString, boolean forceNotification) {
public static void sendCrashReportNotification(final Context context, String logTag, String message, boolean forceNotification, boolean addAppAndDeviceInfo) {
if (context == null) return;
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
@@ -121,18 +124,40 @@ public class CrashUtils {
Logger.logDebug(logTag, "Sending \"" + title + "\" notification.");
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.CRASH_REPORT.getName(), logTag, title, null, reportString, "\n\n" + TermuxUtils.getReportIssueMarkdownString(context), true));
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
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, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
Notification.Builder builder = getCrashReportsNotificationBuilder(context, title, null,
null, contentIntent, deleteIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
if (builder == null) return;
// Send the notification
int nextNotificationId = TermuxNotificationUtils.getNextNotificationId(context);
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
if (notificationManager != null)
notificationManager.notify(nextNotificationId, builder.build());
@@ -146,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

@@ -4,12 +4,13 @@ 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.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;
@@ -64,9 +65,12 @@ public class PluginUtils {
}
boolean isPluginExecutionCommandWithPendingResult = executionCommand.isPluginExecutionCommandWithPendingResult();
boolean isExecutionCommandLoggingEnabled = Logger.shouldEnableLoggingForCustomLogLevel(executionCommand.backgroundCustomLogLevel);
// Log the output. ResultData should not be logged if pending result since ResultSender will do it
Logger.logDebugExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, !isPluginExecutionCommandWithPendingResult));
// or if logging is disabled
Logger.logDebugExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true,
!isPluginExecutionCommandWithPendingResult, isExecutionCommandLoggingEnabled));
// If execution command was started by a plugin which expects the result back
if (isPluginExecutionCommandWithPendingResult) {
@@ -77,11 +81,12 @@ public class PluginUtils {
setPluginResultDirectoryVariables(executionCommand);
// Send result to caller
error = ResultSender.sendCommandResultData(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.resultConfig, executionCommand.resultData);
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));
Logger.logDebugExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, true, isExecutionCommandLoggingEnabled));
// Flash and send notification for the error
Logger.showToast(context, ResultData.getErrorsListMinimalString(resultData), true);
@@ -132,9 +137,11 @@ public class PluginUtils {
}
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));
Logger.logErrorExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true,
!isPluginExecutionCommandWithPendingResult, isExecutionCommandLoggingEnabled));
// If execution command was started by a plugin which expects the result back
if (isPluginExecutionCommandWithPendingResult) {
@@ -145,11 +152,12 @@ public class PluginUtils {
setPluginResultDirectoryVariables(executionCommand);
// Send result to caller
error = ResultSender.sendCommandResultData(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.resultConfig, executionCommand.resultData);
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));
Logger.logErrorExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, true, isExecutionCommandLoggingEnabled));
forceNotification = true;
}
@@ -170,7 +178,7 @@ public class PluginUtils {
}
/** Set variables which will be used by {@link ResultSender#sendCommandResultData(Context, String, String, ResultConfig, ResultData)}
/** 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;
@@ -185,7 +193,7 @@ public class PluginUtils {
resultConfig.resultErrmsgKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG;
}
/** Set variables which will be used by {@link ResultSender#sendCommandResultData(Context, String, String, ResultConfig, ResultData)}
/** 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;
@@ -219,8 +227,23 @@ public class PluginUtils {
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, true));
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context));
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.PLUGIN_EXECUTION_COMMAND.getName(), logTag, title, null, reportString.toString(), null,true));
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
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);
@@ -230,11 +253,11 @@ public class PluginUtils {
//CharSequence notificationTextCharSequence = notificationTextString;
// Build the notification
Notification.Builder builder = getPluginCommandErrorsNotificationBuilder(context, title, notificationTextCharSequence, notificationTextCharSequence, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
Notification.Builder builder = getPluginCommandErrorsNotificationBuilder(context, title,
notificationTextCharSequence, notificationTextCharSequence, contentIntent, deleteIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
if (builder == null) return;
// Send the notification
int nextNotificationId = TermuxNotificationUtils.getNextNotificationId(context);
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
if (notificationManager != null)
notificationManager.notify(nextNotificationId, builder.build());
@@ -248,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;
@@ -295,10 +321,12 @@ public class PluginUtils {
* @param context The {@link Context} to get error string.
* @return Returns the {@code error} if policy is violated, otherwise {@code null}.
*/
public static String checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(final Context context) {
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,6 +8,9 @@ import android.provider.OpenableColumns;
import android.util.Patterns;
import com.termux.R;
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;
@@ -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) {
@@ -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"

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>
@@ -92,24 +95,19 @@
<!-- 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>
<string name="error_display_over_other_apps_permission_not_granted">&TERMUX_APP_NAME; requires
\"Display over other apps\" permission to start terminal sessions from background on Android >= 10.
Grants it from Settings -> Apps -> &TERMUX_APP_NAME; -> Advanced</string>
<!-- Termux RunCommandService -->
<string name="error_run_command_service_invalid_intent_action">Invalid intent action to RunCommandService: `%1$s`</string>
<string name="error_run_command_service_mandatory_extra_missing">Mandatory extra missing to RunCommandService: \"%1$s\"</string>
<string name="error_run_command_service_allow_external_apps_ungranted">RunCommandService require `allow-external-apps` property to be set to `true` in `&TERMUX_PROPERTIES_PRIMARY_PATH_SHORT;` file.</string>
<string name="error_run_command_service_api_help">Visit %1$s for more info on RUN_COMMAND Intent usage.</string>
<!-- Termux Execution Commands -->
<string name="msg_executable_absolute_path">Executable Absolute Path: \"%1$s\"</string>
<string name="msg_working_directory_absolute_path">Working Directory Absolute Path: \"%1$s\"</string>
<!-- Termux File Receiver -->
<string name="title_file_received">Save file in ~/downloads/</string>
<string name="action_file_received_edit">Edit</string>
@@ -117,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>
@@ -137,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>
@@ -164,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

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

@@ -4,7 +4,7 @@ buildscript {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.2.1'
classpath 'com.android.tools.build:gradle:4.2.2'
}
}

View File

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

View File

@@ -66,7 +66,7 @@ afterEvaluate {
from components.release
groupId = 'com.termux'
artifactId = 'terminal-emulator'
version = '0.116'
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.2.0"
implementation "androidx.annotation:annotation:1.3.0"
api project(":terminal-emulator")
}
@@ -45,7 +45,7 @@ afterEvaluate {
from components.release
groupId = 'com.termux'
artifactId = 'terminal-view'
version = '0.116'
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();

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

View File

@@ -7,7 +7,7 @@ The `termux-shared` library is released under [GPLv3 only](https://www.gnu.org/l
- [`src/main/java/com/termux/shared/termux/TermuxConstants.java`](src/main/java/com/termux/shared/termux/TermuxConstants.java).
- [`src/main/java/com/termux/shared/settings/properties/TermuxPropertyConstants.java`](src/main/java/com/termux/shared/settings/properties/TermuxPropertyConstants.java).
- [`src/main/java/com/termux/shared/activities/ReportActivity.java`](src/main/java/com/termux/shared/activities/ReportActivity.java).
- [`src/main/java/com/termux/shared/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).

View File

@@ -5,16 +5,20 @@ android {
compileSdkVersion project.properties.compileSdkVersion.toInteger()
dependencies {
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation "androidx.annotation:annotation:1.2.0"
implementation "androidx.core:core:1.6.0-rc01"
implementation "androidx.window:window:1.0.0-alpha08"
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.
@@ -44,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) {
@@ -61,7 +65,7 @@ afterEvaluate {
from components.release
groupId = 'com.termux'
artifactId = 'termux-shared'
version = '0.116'
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

@@ -7,36 +7,71 @@ 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 EXTRA_REPORT_INFO = "report_info";
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";
ReportInfo mReportInfo;
String mReportMarkdownString;
String mReportActivityMarkdownString;
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);
@@ -44,38 +79,65 @@ public class ReportActivity extends AppCompatActivity {
setSupportActionBar(toolbar);
}
Bundle bundle = null;
mBundle = null;
Intent intent = getIntent();
if (intent != null)
bundle = intent.getExtras();
mBundle = intent.getExtras();
else if (savedInstanceState != null)
bundle = savedInstanceState;
mBundle = savedInstanceState;
updateUI(bundle);
updateUI();
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
Logger.logVerbose(LOG_TAG, "onNewIntent");
setIntent(intent);
if (intent != null)
updateUI(intent.getExtras());
if (intent != null) {
deleteReportInfoFile(this, mReportInfoFilePath);
mBundle = intent.getExtras();
updateUI();
}
}
private void updateUI(Bundle bundle) {
private void updateUI() {
if (bundle == null) {
finish();
return;
if (mBundle == null) {
finish(); return;
}
mReportInfo = (ReportInfo) bundle.getSerializable(EXTRA_REPORT_INFO);
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;
finish(); return;
}
@@ -99,25 +161,41 @@ public class ReportActivity extends AppCompatActivity {
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);
}
}
outState.putSerializable(EXTRA_REPORT_INFO, 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;
}
@@ -131,50 +209,264 @@ public class ReportActivity extends AppCompatActivity {
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);
ShareUtils.shareText(this, getString(R.string.title_report_text), ReportInfo.getReportInfoMarkdownString(mReportInfo));
} else if (id == R.id.menu_item_copy_report) {
if (mReportMarkdownString != null)
ShareUtils.copyTextToClipboard(this, mReportMarkdownString, null);
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() {
mReportMarkdownString = ReportInfo.getReportInfoMarkdownString(mReportInfo);
// 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();
mReportActivityMarkdownString = "";
if (mReportInfo.reportStringPrefix != null)
mReportActivityMarkdownString += mReportInfo.reportStringPrefix;
reportString.append(mReportInfo.reportStringPrefix);
mReportActivityMarkdownString += mReportMarkdownString;
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)
mReportActivityMarkdownString += mReportInfo.reportStringSuffix;
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 void startReportActivity(@NonNull final Context context, @NonNull final ReportInfo reportInfo) {
context.startActivity(newInstance(context, reportInfo));
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;
}
}
public static Intent newInstance(@NonNull final Context context, @NonNull final ReportInfo reportInfo) {
/**
* 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();
bundle.putSerializable(EXTRA_REPORT_INFO, reportInfo);
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 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);
// 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

@@ -56,7 +56,7 @@ public class CrashHandler implements Thread.UncaughtExceptionHandler {
reportString.append("## Crash Details\n");
reportString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Crash Thread", thread.toString(), "-"));
reportString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Crash Timestamp", AndroidUtils.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)));

View File

@@ -4,12 +4,14 @@ import android.os.Bundle;
import androidx.annotation.Nullable;
import java.util.LinkedHashSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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();
@@ -97,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[]}.
*
@@ -160,9 +173,37 @@ public class DataUtils {
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();
}
/** 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;
}
}
}

View File

@@ -49,6 +49,29 @@ public class IntentUtils {
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;
}
}
/**

View File

@@ -22,6 +22,7 @@ public class UrlUtils {
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.

View File

@@ -10,10 +10,14 @@ import com.termux.shared.file.filesystem.FileType;
import com.termux.shared.file.filesystem.FileTypes;
import com.termux.shared.data.DataUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.models.errors.Errno;
import com.termux.shared.models.errors.Error;
import com.termux.shared.models.errors.FileUtilsErrno;
import com.termux.shared.models.errors.FunctionErrno;
import org.apache.commons.io.filefilter.AgeFileFilter;
import org.apache.commons.io.filefilter.IOFileFilter;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.Closeable;
@@ -22,10 +26,17 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStreamWriter;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.nio.file.LinkOption;
import java.nio.file.StandardCopyOption;
import java.util.Calendar;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Pattern;
public class FileUtils {
@@ -100,6 +111,29 @@ public class FileUtils {
return path;
}
/**
* Convert special characters `\/:*?"<>|` to underscore.
*
* @param fileName The name to sanitize.
* @param sanitizeWhitespaces If set to {@code true}, then white space characters ` \t\n` will be
* converted.
* @param sanitizeWhitespaces If set to {@code true}, then file name will be converted to lowe case.
* @return Returns the {@code sanitized name}.
*/
public static String sanitizeFileName(String fileName, boolean sanitizeWhitespaces, boolean toLower) {
if (fileName == null) return null;
if (sanitizeWhitespaces)
fileName = fileName.replaceAll("[\\\\/:*?\"<>| \t\n]", "_");
else
fileName = fileName.replaceAll("[\\\\/:*?\"<>|]", "_");
if (toLower)
return fileName.toLowerCase();
else
return fileName;
}
/**
* Determines whether path is in {@code dirPath}. The {@code dirPath} is not canonicalized and
* only normalized.
@@ -111,7 +145,21 @@ public class FileUtils {
* @return Returns {@code true} if path in {@code dirPath}, otherwise returns {@code false}.
*/
public static boolean isPathInDirPath(String path, final String dirPath, final boolean ensureUnder) {
if (path == null || dirPath == null) return false;
return isPathInDirPaths(path, Collections.singletonList(dirPath), ensureUnder);
}
/**
* Determines whether path is in one of the {@code dirPaths}. The {@code dirPaths} are not
* canonicalized and only normalized.
*
* @param path The {@code path} to check.
* @param dirPaths The {@code directory paths} to check in.
* @param ensureUnder If set to {@code true}, then it will be ensured that {@code path} is
* under the directories and does not equal it.
* @return Returns {@code true} if path in {@code dirPaths}, otherwise returns {@code false}.
*/
public static boolean isPathInDirPaths(String path, final List<String> dirPaths, final boolean ensureUnder) {
if (path == null || path.isEmpty() || dirPaths == null || dirPaths.size() < 1) return false;
try {
path = new File(path).getCanonicalPath();
@@ -119,12 +167,20 @@ public class FileUtils {
return false;
}
String normalizedDirPath = normalizePath(dirPath);
boolean isPathInDirPaths;
if (ensureUnder)
return !path.equals(normalizedDirPath) && path.startsWith(normalizedDirPath + "/");
else
return path.startsWith(normalizedDirPath + "/");
for (String dirPath : dirPaths) {
String normalizedDirPath = normalizePath(dirPath);
if (ensureUnder)
isPathInDirPaths = !path.equals(normalizedDirPath) && path.startsWith(normalizedDirPath + "/");
else
isPathInDirPaths = path.startsWith(normalizedDirPath + "/");
if (isPathInDirPaths) return true;
}
return false;
}
@@ -229,7 +285,7 @@ public class FileUtils {
// If file exists but not a regular file
if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) {
return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file");
return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file", filePath).setLabel(label + "file");
}
boolean isPathUnderParentDirPath = false;
@@ -252,7 +308,8 @@ public class FileUtils {
// If path is not a regular file
// Regular files cannot be automatically created so we do not ignore if missing
if (fileType != FileType.REGULAR) {
return FileUtilsErrno.ERRNO_NO_REGULAR_FILE_FOUND.getError(label + "file");
label += "regular file";
return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, filePath).setLabel(label);
}
// If there is not parentDirPath restriction or path is not under parentDirPath or
@@ -310,7 +367,7 @@ public class FileUtils {
// If file exists but not a directory file
if (fileType != FileType.NO_EXIST && fileType != FileType.DIRECTORY) {
return FileUtilsErrno.ERRNO_NON_DIRECTORY_FILE_FOUND.getError(label + "directory");
return FileUtilsErrno.ERRNO_NON_DIRECTORY_FILE_FOUND.getError(label + "directory", filePath).setLabel(label + "directory");
}
boolean isPathInParentDirPath = false;
@@ -326,9 +383,10 @@ public class FileUtils {
if (createDirectoryIfMissing && fileType == FileType.NO_EXIST) {
Logger.logVerbose(LOG_TAG, "Creating " + label + "directory file at path \"" + filePath + "\"");
// Create directory and update fileType if successful, otherwise return with error
if (file.mkdirs())
fileType = getFileType(filePath, false);
else
// It "might" be possible that mkdirs returns false even though directory was created
boolean result = file.mkdirs();
fileType = getFileType(filePath, false);
if (!result && fileType != FileType.DIRECTORY)
return FileUtilsErrno.ERRNO_CREATING_FILE_FAILED.getError(label + "directory file", filePath);
}
@@ -348,7 +406,8 @@ public class FileUtils {
// If path is not a directory
// Directories can be automatically created so we can ignore if missing with above check
if (fileType != FileType.DIRECTORY) {
return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label + "directory", filePath);
label += "directory";
return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, filePath).setLabel(label);
}
if (permissionsToCheck != null) {
@@ -423,7 +482,7 @@ public class FileUtils {
// If file exists but not a regular file
if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) {
return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file");
return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file", filePath).setLabel(label + "file");
}
// If regular file already exists
@@ -613,8 +672,10 @@ public class FileUtils {
// If target file does not exist
if (targetFileType == FileType.NO_EXIST) {
// If dangling symlink should not be allowed, then return with error
if (!allowDangling)
return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label + "symlink target file", targetFileAbsolutePath);
if (!allowDangling) {
label += "symlink target file";
return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, targetFileAbsolutePath).setLabel(label);
}
}
// If destination exists
@@ -888,8 +949,10 @@ public class FileUtils {
if (ignoreNonExistentSrcFile)
return null;
// Else return with error
else
return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label + "source file", srcFilePath);
else {
label += "source file";
return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, srcFilePath).setLabel(label);
}
}
// If the file type of the source file does not exist in the allowedFileTypeFlags, then return with error
@@ -995,7 +1058,7 @@ public class FileUtils {
/**
* Delete regular file at path.
*
* This function is a wrapper for {@link #deleteFile(String, String, boolean, int)}.
* This function is a wrapper for {@link #deleteFile(String, String, boolean, boolean, int)}.
*
* @param label The optional label for file to delete. This can optionally be {@code null}.
* @param filePath The {@code path} for file to delete.
@@ -1004,13 +1067,13 @@ public class FileUtils {
* @return Returns the {@code error} if deletion was not successful, otherwise {@code null}.
*/
public static Error deleteRegularFile(String label, final String filePath, final boolean ignoreNonExistentFile) {
return deleteFile(label, filePath, ignoreNonExistentFile, FileType.REGULAR.getValue());
return deleteFile(label, filePath, ignoreNonExistentFile, false, FileType.REGULAR.getValue());
}
/**
* Delete directory file at path.
*
* This function is a wrapper for {@link #deleteFile(String, String, boolean, int)}.
* This function is a wrapper for {@link #deleteFile(String, String, boolean, boolean, int)}.
*
* @param label The optional label for file to delete. This can optionally be {@code null}.
* @param filePath The {@code path} for file to delete.
@@ -1019,13 +1082,13 @@ public class FileUtils {
* @return Returns the {@code error} if deletion was not successful, otherwise {@code null}.
*/
public static Error deleteDirectoryFile(String label, final String filePath, final boolean ignoreNonExistentFile) {
return deleteFile(label, filePath, ignoreNonExistentFile, FileType.DIRECTORY.getValue());
return deleteFile(label, filePath, ignoreNonExistentFile, false, FileType.DIRECTORY.getValue());
}
/**
* Delete symlink file at path.
*
* This function is a wrapper for {@link #deleteFile(String, String, boolean, int)}.
* This function is a wrapper for {@link #deleteFile(String, String, boolean, boolean, int)}.
*
* @param label The optional label for file to delete. This can optionally be {@code null}.
* @param filePath The {@code path} for file to delete.
@@ -1034,13 +1097,13 @@ public class FileUtils {
* @return Returns the {@code error} if deletion was not successful, otherwise {@code null}.
*/
public static Error deleteSymlinkFile(String label, final String filePath, final boolean ignoreNonExistentFile) {
return deleteFile(label, filePath, ignoreNonExistentFile, FileType.SYMLINK.getValue());
return deleteFile(label, filePath, ignoreNonExistentFile, false, FileType.SYMLINK.getValue());
}
/**
* Delete regular, directory or symlink file at path.
*
* This function is a wrapper for {@link #deleteFile(String, String, boolean, int)}.
* This function is a wrapper for {@link #deleteFile(String, String, boolean, boolean, int)}.
*
* @param label The optional label for file to delete. This can optionally be {@code null}.
* @param filePath The {@code path} for file to delete.
@@ -1049,7 +1112,7 @@ public class FileUtils {
* @return Returns the {@code error} if deletion was not successful, otherwise {@code null}.
*/
public static Error deleteFile(String label, final String filePath, final boolean ignoreNonExistentFile) {
return deleteFile(label, filePath, ignoreNonExistentFile, FileTypes.FILE_TYPE_NORMAL_FLAGS);
return deleteFile(label, filePath, ignoreNonExistentFile, false, FileTypes.FILE_TYPE_NORMAL_FLAGS);
}
/**
@@ -1064,6 +1127,8 @@ public class FileUtils {
* @param filePath The {@code path} for file to delete.
* @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
* error if file to deleted doesn't exist.
* @param ignoreWrongFileType The {@code boolean} that decides if it should be considered an
* error if file type is not one from {@code allowedFileTypeFlags}.
* @param allowedFileTypeFlags The flags that are matched against the file's {@link FileType} to
* see if it should be deleted or not. This is a safety measure to
* prevent accidental deletion of the wrong type of file, like a
@@ -1071,29 +1136,41 @@ public class FileUtils {
* {@link FileTypes#FILE_TYPE_ANY_FLAGS} to allow deletion of any file type.
* @return Returns the {@code error} if deletion was not successful, otherwise {@code null}.
*/
public static Error deleteFile(String label, final String filePath, final boolean ignoreNonExistentFile, int allowedFileTypeFlags) {
public static Error deleteFile(String label, final String filePath, final boolean ignoreNonExistentFile, final boolean ignoreWrongFileType, int allowedFileTypeFlags) {
label = (label == null ? "" : label + " ");
if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "deleteFile");
try {
Logger.logVerbose(LOG_TAG, "Deleting " + label + "file at path \"" + filePath + "\"");
File file = new File(filePath);
FileType fileType = getFileType(filePath, false);
Logger.logVerbose(LOG_TAG, "Processing delete of " + label + "file at path \"" + filePath + "\" of type \"" + fileType.getName() + "\"");
// If file does not exist
if (fileType == FileType.NO_EXIST) {
// If delete is to be ignored if file does not exist
if (ignoreNonExistentFile)
return null;
// Else return with error
else
return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label + "file meant to be deleted", filePath);
// Else return with error
else {
label += "file meant to be deleted";
return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, filePath).setLabel(label);
}
}
// If the file type of the file does not exist in the allowedFileTypeFlags, then return with error
if ((allowedFileTypeFlags & fileType.getValue()) <= 0)
// If the file type of the file does not exist in the allowedFileTypeFlags
if ((allowedFileTypeFlags & fileType.getValue()) <= 0) {
// If wrong file type is to be ignored
if (ignoreWrongFileType) {
Logger.logVerbose(LOG_TAG, "Ignoring deletion of " + label + "file at path \"" + filePath + "\" not matching allowed file types: " + FileTypes.convertFileTypeFlagsToNamesString(allowedFileTypeFlags));
return null;
}
// Else return with error
return FileUtilsErrno.ERRNO_FILE_NOT_AN_ALLOWED_FILE_TYPE.getError(label + "file meant to be deleted", filePath, FileTypes.convertFileTypeFlagsToNamesString(allowedFileTypeFlags));
}
Logger.logVerbose(LOG_TAG, "Deleting " + label + "file at path \"" + filePath + "\"");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
/*
@@ -1180,7 +1257,7 @@ public class FileUtils {
// If file exists but not a directory file
if (fileType != FileType.NO_EXIST && fileType != FileType.DIRECTORY) {
return FileUtilsErrno.ERRNO_NON_DIRECTORY_FILE_FOUND.getError(label + "directory");
return FileUtilsErrno.ERRNO_NON_DIRECTORY_FILE_FOUND.getError(label + "directory", filePath).setLabel(label + "directory");
}
// If directory exists, clear its contents
@@ -1208,6 +1285,79 @@ public class FileUtils {
return null;
}
/**
* Delete files under a directory older than x days.
*
* The {@code filePath} must be the canonical path to a directory since symlinks will not be followed.
* Any symlink files found under the directory will be deleted, but not their targets.
*
* @param label The optional label for directory to clear. This can optionally be {@code null}.
* @param filePath The {@code path} for directory to clear.
* @param dirFilter The optional filter to apply when finding subdirectories.
* If this parameter is {@code null}, subdirectories will not be included in the
* search. Use TrueFileFilter.INSTANCE to match all directories.
* @param days The x amount of days before which files should be deleted. This must be `>=0`.
* @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
* error if file to deleted doesn't exist.
* @param allowedFileTypeFlags The flags that are matched against the file's {@link FileType} to
* see if it should be deleted or not. This is a safety measure to
* prevent accidental deletion of the wrong type of file, like a
* directory instead of a regular file. You can pass
* {@link FileTypes#FILE_TYPE_ANY_FLAGS} to allow deletion of any file type.
* @return Returns the {@code error} if deleting was not successful, otherwise {@code null}.
*/
public static Error deleteFilesOlderThanXDays(String label, final String filePath, final IOFileFilter dirFilter, int days, final boolean ignoreNonExistentFile, int allowedFileTypeFlags) {
label = (label == null ? "" : label + " ");
if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "deleteFilesOlderThanXDays");
if (days < 0) return FunctionErrno.ERRNO_INVALID_PARAMETER.getError(label + "days", "deleteFilesOlderThanXDays", " It must be >= 0.");
Error error;
try {
Logger.logVerbose(LOG_TAG, "Deleting files under " + label + "directory at path \"" + filePath + "\" older than " + days + " days");
File file = new File(filePath);
FileType fileType = getFileType(filePath, false);
// If file exists but not a directory file
if (fileType != FileType.NO_EXIST && fileType != FileType.DIRECTORY) {
return FileUtilsErrno.ERRNO_NON_DIRECTORY_FILE_FOUND.getError(label + "directory", filePath).setLabel(label + "directory");
}
// If file does not exist
if (fileType == FileType.NO_EXIST) {
// If delete is to be ignored if file does not exist
if (ignoreNonExistentFile)
return null;
// Else return with error
else {
label += "directory under which files had to be deleted";
return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, filePath).setLabel(label);
}
}
// If directory exists, delete its contents
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DATE, -(days));
// AgeFileFilter seems to apply to symlink destination timestamp instead of symlink file itself
Iterator<File> filesToDelete =
org.apache.commons.io.FileUtils.iterateFiles(file, new AgeFileFilter(calendar.getTime()), dirFilter);
while (filesToDelete.hasNext()) {
File subFile = filesToDelete.next();
error = deleteFile(label + " directory sub", subFile.getAbsolutePath(), true, true, allowedFileTypeFlags);
if (error != null)
return error;
}
} catch (Exception e) {
return FileUtilsErrno.ERRNO_DELETING_FILES_OLDER_THAN_X_DAYS_FAILED_WITH_EXCEPTION.getError(e, label + "directory", filePath, days, e.getMessage());
}
return null;
}
/**
@@ -1234,7 +1384,7 @@ public class FileUtils {
// If file exists but not a regular file
if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) {
return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file");
return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file", filePath).setLabel(label + "file");
}
// If file does not exist
@@ -1243,8 +1393,10 @@ public class FileUtils {
if (ignoreNonExistentFile)
return null;
// Else return with error
else
return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label + "file meant to be read", filePath);
else {
label += "file meant to be read";
return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, filePath).setLabel(label);
}
}
if (charset == null) charset = Charset.defaultCharset();
@@ -1280,6 +1432,74 @@ public class FileUtils {
return null;
}
public static class ReadSerializableObjectResult {
public Error error;
public Serializable serializableObject;
ReadSerializableObjectResult(Error error, Serializable serializableObject) {
this.error = error;
this.serializableObject = serializableObject;
}
}
/**
* Read a {@link Serializable} object from file at path.
*
* @param label The optional label for file to read. This can optionally be {@code null}.
* @param filePath The {@code path} for file to read.
* @param readObjectType The {@link Class} of the object.
* @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
* error if file to read doesn't exist.
* @return Returns the {@code error} if reading was not successful, otherwise {@code null}.
*/
@NonNull
public static <T extends Serializable> ReadSerializableObjectResult readSerializableObjectFromFile(String label, final String filePath, Class<T> readObjectType, final boolean ignoreNonExistentFile) {
label = (label == null ? "" : label + " ");
if (filePath == null || filePath.isEmpty()) return new ReadSerializableObjectResult(FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "readSerializableObjectFromFile"), null);
Logger.logVerbose(LOG_TAG, "Reading serializable object from " + label + "file at path \"" + filePath + "\"");
T serializableObject;
FileType fileType = getFileType(filePath, false);
// If file exists but not a regular file
if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) {
return new ReadSerializableObjectResult(FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file", filePath).setLabel(label + "file"), null);
}
// If file does not exist
if (fileType == FileType.NO_EXIST) {
// If reading is to be ignored if file does not exist
if (ignoreNonExistentFile)
return new ReadSerializableObjectResult(null, null);
// Else return with error
else {
label += "file meant to be read";
return new ReadSerializableObjectResult(FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError(label, filePath).setLabel(label), null);
}
}
FileInputStream fileInputStream = null;
ObjectInputStream objectInputStream = null;
try {
// Read string from file
fileInputStream = new FileInputStream(filePath);
objectInputStream = new ObjectInputStream(fileInputStream);
//serializableObject = (T) objectInputStream.readObject();
serializableObject = readObjectType.cast(objectInputStream.readObject());
//Logger.logVerbose(LOG_TAG, Logger.getMultiLineLogStringEntry("String", DataUtils.getTruncatedCommandOutput(dataStringBuilder.toString(), Logger.LOGGER_ENTRY_MAX_SAFE_PAYLOAD, true, false, true), "-"));
} catch (Exception e) {
return new ReadSerializableObjectResult(FileUtilsErrno.ERRNO_READING_SERIALIZABLE_OBJECT_TO_FILE_FAILED_WITH_EXCEPTION.getError(e, label + "file", filePath, e.getMessage()), null);
} finally {
closeCloseable(fileInputStream);
closeCloseable(objectInputStream);
}
return new ReadSerializableObjectResult(null, serializableObject);
}
/**
* Write the {@link String} {@code dataString} with a specific {@link Charset} to file at path.
*
@@ -1287,6 +1507,7 @@ public class FileUtils {
* @param filePath The {@code path} for file to write.
* @param charset The {@link Charset} of the {@code dataString}. If this is {@code null},
* then default {@link Charset} will be used.
* @param dataString The data to write to file.
* @param append The {@code boolean} that decides if file should be appended to or not.
* @return Returns the {@code error} if writing was not successful, otherwise {@code null}.
*/
@@ -1298,15 +1519,7 @@ public class FileUtils {
Error error;
FileType fileType = getFileType(filePath, false);
// If file exists but not a regular file
if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) {
return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file");
}
// Create the file parent directory
error = createParentDirectoryFile(label + "file parent", filePath);
error = preWriteToFile(label, filePath);
if (error != null)
return error;
@@ -1336,6 +1549,63 @@ public class FileUtils {
return null;
}
/**
* Write the {@link Serializable} {@code serializableObject} to file at path.
*
* @param label The optional label for file to write. This can optionally be {@code null}.
* @param filePath The {@code path} for file to write.
* @param serializableObject The object to write to file.
* @return Returns the {@code error} if writing was not successful, otherwise {@code null}.
*/
public static <T extends Serializable> Error writeSerializableObjectToFile(String label, final String filePath, final T serializableObject) {
label = (label == null ? "" : label + " ");
if (filePath == null || filePath.isEmpty()) return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError(label + "file path", "writeSerializableObjectToFile");
Logger.logVerbose(LOG_TAG, "Writing serializable object to " + label + "file at path \"" + filePath + "\"");
Error error;
error = preWriteToFile(label, filePath);
if (error != null)
return error;
FileOutputStream fileOutputStream = null;
ObjectOutputStream objectOutputStream = null;
try {
// Write object to file
fileOutputStream = new FileOutputStream(filePath);
objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(serializableObject);
objectOutputStream.flush();
} catch (Exception e) {
return FileUtilsErrno.ERRNO_WRITING_SERIALIZABLE_OBJECT_TO_FILE_FAILED_WITH_EXCEPTION.getError(e, label + "file", filePath, e.getMessage());
} finally {
closeCloseable(fileOutputStream);
closeCloseable(objectOutputStream);
}
return null;
}
private static Error preWriteToFile(String label, String filePath) {
Error error;
FileType fileType = getFileType(filePath, false);
// If file exists but not a regular file
if (fileType != FileType.NO_EXIST && fileType != FileType.REGULAR) {
return FileUtilsErrno.ERRNO_NON_REGULAR_FILE_FOUND.getError(label + "file", filePath).setLabel(label + "file");
}
// Create the file parent directory
error = createParentDirectoryFile(label + "file parent", filePath);
if (error != null)
return error;
return null;
}
/**
@@ -1533,17 +1803,17 @@ public class FileUtils {
// If file is not readable
if (permissionsToCheck.contains("r") && !file.canRead()) {
return FileUtilsErrno.ERRNO_FILE_NOT_READABLE.getError(label + "file");
return FileUtilsErrno.ERRNO_FILE_NOT_READABLE.getError(label + "file", filePath).setLabel(label + "file");
}
// If file is not writable
if (permissionsToCheck.contains("w") && !file.canWrite()) {
return FileUtilsErrno.ERRNO_FILE_NOT_WRITABLE.getError(label + "file");
return FileUtilsErrno.ERRNO_FILE_NOT_WRITABLE.getError(label + "file", filePath).setLabel(label + "file");
}
// If file is not executable
// This canExecute() will give "avc: granted { execute }" warnings for target sdk 29
else if (permissionsToCheck.contains("x") && !file.canExecute() && !ignoreIfNotExecutable) {
return FileUtilsErrno.ERRNO_FILE_NOT_EXECUTABLE.getError(label + "file");
return FileUtilsErrno.ERRNO_FILE_NOT_EXECUTABLE.getError(label + "file", filePath).setLabel(label + "file");
}
return null;
@@ -1563,4 +1833,26 @@ public class FileUtils {
return Pattern.compile("^([r-])[w-][x-]$", 0).matcher(string).matches();
}
/**
* Get a {@link Error} that contains a shorter version of {@link Errno} message.
*
* @param error The original {@link Error} returned by one of the {@link FileUtils} functions.
* @return Returns the shorter {@link Error} if one exists, otherwise original {@code error}.
*/
public static Error getShortFileUtilsError(final Error error) {
String type = error.getType();
if (!FileUtilsErrno.TYPE.equals(type)) return error;
Errno shortErrno = FileUtilsErrno.ERRNO_SHORT_MAPPING.get(Errno.valueOf(type, error.getCode()));
if (shortErrno == null) return error;
List<Throwable> throwables = error.getThrowablesList();
if (throwables.isEmpty())
return shortErrno.getError(DataUtils.getDefaultIfNull(error.getLabel(), "file"));
else
return shortErrno.getError(throwables, error.getLabel(), "file");
}
}

View File

@@ -1,14 +1,47 @@
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.
*
@@ -26,6 +59,23 @@ public class TermuxFileUtils {
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.
*
@@ -120,4 +170,208 @@ public class TermuxFileUtils {
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

@@ -22,9 +22,33 @@ public class MessageDialogUtils {
* @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);
}
AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.Theme_AppCompat_Light_Dialog)
.setPositiveButton(android.R.string.ok, null);
/**
* 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);
@@ -40,6 +64,13 @@ public class MessageDialogUtils {
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);

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

@@ -26,6 +26,7 @@ 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;
/**
@@ -50,16 +51,16 @@ public class Logger {
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);
}
@@ -187,6 +188,10 @@ public class Logger {
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) {
@@ -409,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;
@@ -427,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

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

@@ -83,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;
@@ -234,9 +242,9 @@ public class ExecutionCommand {
@Override
public String toString() {
if (!hasExecuted())
return getExecutionInputLogString(this, true);
return getExecutionInputLogString(this, true, true);
else {
return getExecutionOutputLogString(this, true, true);
return getExecutionOutputLogString(this, true, true, true);
}
}
@@ -245,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();
@@ -264,6 +273,14 @@ 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());
@@ -283,9 +300,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 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, boolean logResultData) {
public static String getExecutionOutputLogString(final ExecutionCommand executionCommand, boolean ignoreNull, boolean logResultData, boolean logStdoutAndStderr) {
if (executionCommand == null) return "null";
StringBuilder logString = new StringBuilder();
@@ -296,7 +314,7 @@ public class ExecutionCommand {
logString.append("\n").append(executionCommand.getCurrentStateLogString());
if (logResultData)
logString.append("\n").append(ResultData.getResultDataLogString(executionCommand.resultData, ignoreNull));
logString.append("\n").append(ResultData.getResultDataLogString(executionCommand.resultData, logStdoutAndStderr));
return logString.toString();
}
@@ -312,8 +330,8 @@ public class ExecutionCommand {
StringBuilder logString = new StringBuilder();
logString.append(getExecutionInputLogString(executionCommand, false));
logString.append(getExecutionOutputLogString(executionCommand, false, true));
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());
@@ -346,6 +364,14 @@ 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, "-"));
@@ -418,6 +444,17 @@ public class ExecutionCommand {
return "isFailsafe: `" + isFailsafe + "`";
}
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, "-");
}

View File

@@ -5,6 +5,9 @@ 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. */
@@ -14,27 +17,36 @@ public class ReportInfo implements Serializable {
/** 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(String 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 = AndroidUtils.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

@@ -133,16 +133,18 @@ public class ResultData implements Serializable {
* Get a log friendly {@link String} for {@link ResultData} parameters.
*
* @param resultData The {@link ResultData} to convert.
* @param ignoreNull Set to {@code true} if non-critical {@code null} values are to be ignored.
* @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 ignoreNull) {
public static String getResultDataLogString(final ResultData resultData, boolean logStdoutAndStderr) {
if (resultData == null) return "null";
StringBuilder logString = new StringBuilder();
logString.append("\n").append(resultData.getStdoutLogString());
logString.append("\n").append(resultData.getStderrLogString());
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));

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

@@ -8,11 +8,14 @@ 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";
@@ -35,6 +38,7 @@ public class Errno {
this.type = type;
this.code = code;
this.message = message;
map.put(type + ":" + code, this);
}
@NonNull
@@ -56,6 +60,17 @@ public class Errno {
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() {
@@ -73,12 +88,18 @@ public class Errno {
}
public Error getError(Throwable throwable, Object... args) {
return getError(Collections.singletonList(throwable), args);
if (throwable == null)
return getError(args);
else
return getError(Collections.singletonList(throwable), args);
}
public Error getError(List<Throwable> throwablesList, Object... args) {
try {
return new Error(getType(), getCode(), String.format(getMessage(), args), throwablesList);
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

View File

@@ -12,6 +12,8 @@ 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. */
@@ -76,7 +78,18 @@ public class Error implements Serializable {
this.code = Errno.ERRNO_SUCCESS.getCode();
this.message = message;
this.throwablesList = throwablesList;
if (throwablesList != null)
this.throwablesList = throwablesList;
}
public Error setLabel(String label) {
this.label = label;
return this;
}
public String getLabel() {
return label;
}

View File

@@ -1,5 +1,8 @@
package com.termux.shared.models.errors;
import java.util.HashMap;
import java.util.Map;
/** The {@link Class} that defines FileUtils error messages and codes. */
public class FileUtilsErrno extends Errno {
@@ -18,19 +21,20 @@ public class FileUtilsErrno extends Errno {
/* Errors for invalid or not found files at path (150-200) */
public static final Errno ERRNO_FILE_NOT_FOUND_AT_PATH = new Errno(TYPE, 150, "The %1$s is not found at path \"%2$s\".");
public static final Errno ERRNO_FILE_NOT_FOUND_AT_PATH = new Errno(TYPE, 150, "The %1$s not found at path \"%2$s\".");
public static final Errno ERRNO_FILE_NOT_FOUND_AT_PATH_SHORT = new Errno(TYPE, 151, "The %1$s not found at path.");
public static final Errno ERRNO_NO_REGULAR_FILE_FOUND = new Errno(TYPE, 151, "Regular file not found at %1$s path.");
public static final Errno ERRNO_NOT_A_REGULAR_FILE = new Errno(TYPE, 152, "The %1$s at path \"%2$s\" is not a regular file.");
public static final Errno ERRNO_NON_REGULAR_FILE_FOUND = new Errno(TYPE, 152, "Non-regular file found at %1$s path \"%2$s\".");
public static final Errno ERRNO_NON_REGULAR_FILE_FOUND_SHORT = new Errno(TYPE, 153, "Non-regular file found at %1$s path.");
public static final Errno ERRNO_NON_DIRECTORY_FILE_FOUND = new Errno(TYPE, 154, "Non-directory file found at %1$s path \"%2$s\".");
public static final Errno ERRNO_NON_DIRECTORY_FILE_FOUND_SHORT = new Errno(TYPE, 155, "Non-directory file found at %1$s path.");
public static final Errno ERRNO_NON_SYMLINK_FILE_FOUND = new Errno(TYPE, 156, "Non-symlink file found at %1$s path \"%2$s\".");
public static final Errno ERRNO_NON_SYMLINK_FILE_FOUND_SHORT = new Errno(TYPE, 157, "Non-symlink file found at %1$s path.");
public static final Errno ERRNO_NON_REGULAR_FILE_FOUND = new Errno(TYPE, 153, "Non-regular file found at %1$s path.");
public static final Errno ERRNO_NON_DIRECTORY_FILE_FOUND = new Errno(TYPE, 154, "Non-directory file found at %1$s path.");
public static final Errno ERRNO_NON_SYMLINK_FILE_FOUND = new Errno(TYPE, 155, "Non-symlink file found at %1$s path.");
public static final Errno ERRNO_FILE_NOT_AN_ALLOWED_FILE_TYPE = new Errno(TYPE, 158, "The %1$s found at path \"%2$s\" is not one of allowed file types \"%3$s\".");
public static final Errno ERRNO_FILE_NOT_AN_ALLOWED_FILE_TYPE = new Errno(TYPE, 156, "The %1$s found at path \"%2$s\" is not one of allowed file types \"%3$s\".");
public static final Errno ERRNO_VALIDATE_FILE_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 157, "Validating file existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s");
public static final Errno ERRNO_VALIDATE_DIRECTORY_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 158, "Validating directory existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s");
public static final Errno ERRNO_VALIDATE_FILE_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 159, "Validating file existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s");
public static final Errno ERRNO_VALIDATE_DIRECTORY_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 160, "Validating directory existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s");
@@ -56,6 +60,7 @@ public class FileUtilsErrno extends Errno {
public static final Errno ERRNO_DELETING_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 301, "Deleting %1$s at path \"%2$s\" failed.\nException: %3$s");
public static final Errno ERRNO_CLEARING_DIRECTORY_FAILED_WITH_EXCEPTION = new Errno(TYPE, 302, "Clearing %1$s at path \"%2$s\" failed.\nException: %3$s");
public static final Errno ERRNO_FILE_STILL_EXISTS_AFTER_DELETING = new Errno(TYPE, 303, "The %1$s still exists after deleting it from \"%2$s\".");
public static final Errno ERRNO_DELETING_FILES_OLDER_THAN_X_DAYS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 304, "Deleting %1$s under directory at path \"%2$s\" old than %3$s days failed.\nException: %4$s");
@@ -64,18 +69,38 @@ public class FileUtilsErrno extends Errno {
public static final Errno ERRNO_WRITING_STRING_TO_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 351, "Writing string to %1$s at path \"%2$s\" failed.\nException: %3$s");
public static final Errno ERRNO_UNSUPPORTED_CHARSET = new Errno(TYPE, 352, "Unsupported charset \"%1$s\"");
public static final Errno ERRNO_CHECKING_IF_CHARSET_SUPPORTED_FAILED = new Errno(TYPE, 353, "Checking if charset \"%1$s\" is supported failed.\nException: %2$s");
public static final Errno ERRNO_READING_SERIALIZABLE_OBJECT_TO_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 354, "Reading serializable object from %1$s at path \"%2$s\" failed.\nException: %3$s");
public static final Errno ERRNO_WRITING_SERIALIZABLE_OBJECT_TO_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 355, "Writing serializable object to %1$s at path \"%2$s\" failed.\nException: %3$s");
/* Errors for invalid file permissions (400-450) */
public static final Errno ERRNO_INVALID_FILE_PERMISSIONS_STRING_TO_CHECK = new Errno(TYPE, 400, "The file permission string to check is invalid.");
public static final Errno ERRNO_FILE_NOT_READABLE = new Errno(TYPE, 401, "The %1$s at path is not readable. Permission Denied.");
public static final Errno ERRNO_FILE_NOT_WRITABLE = new Errno(TYPE, 402, "The %1$s at path is not writable. Permission Denied.");
public static final Errno ERRNO_FILE_NOT_EXECUTABLE = new Errno(TYPE, 403, "The %1$s at path is not executable. Permission Denied.");
public static final Errno ERRNO_FILE_NOT_READABLE = new Errno(TYPE, 401, "The %1$s at path \"%2$s\" is not readable. Permission Denied.");
public static final Errno ERRNO_FILE_NOT_READABLE_SHORT = new Errno(TYPE, 402, "The %1$s at path is not readable. Permission Denied.");
public static final Errno ERRNO_FILE_NOT_WRITABLE = new Errno(TYPE, 403, "The %1$s at path \"%2$s\" is not writable. Permission Denied.");
public static final Errno ERRNO_FILE_NOT_WRITABLE_SHORT = new Errno(TYPE, 404, "The %1$s at path is not writable. Permission Denied.");
public static final Errno ERRNO_FILE_NOT_EXECUTABLE = new Errno(TYPE, 405, "The %1$s at path \"%2$s\" is not executable. Permission Denied.");
public static final Errno ERRNO_FILE_NOT_EXECUTABLE_SHORT = new Errno(TYPE, 406, "The %1$s at path is not executable. Permission Denied.");
FileUtilsErrno(final String type, final int code, final String message) {
super(type, code, message);
}
/** Defines the {@link Errno} mapping to get a shorter version of {@link FileUtilsErrno}. */
public static Map<Errno, Errno> ERRNO_SHORT_MAPPING = new HashMap<Errno, Errno>() {{
put(ERRNO_FILE_NOT_FOUND_AT_PATH, ERRNO_FILE_NOT_FOUND_AT_PATH_SHORT);
put(ERRNO_NON_REGULAR_FILE_FOUND, ERRNO_NON_REGULAR_FILE_FOUND_SHORT);
put(ERRNO_NON_DIRECTORY_FILE_FOUND, ERRNO_NON_DIRECTORY_FILE_FOUND_SHORT);
put(ERRNO_NON_SYMLINK_FILE_FOUND, ERRNO_NON_SYMLINK_FILE_FOUND_SHORT);
put(ERRNO_FILE_NOT_READABLE, ERRNO_FILE_NOT_READABLE_SHORT);
put(ERRNO_FILE_NOT_WRITABLE, ERRNO_FILE_NOT_WRITABLE_SHORT);
put(ERRNO_FILE_NOT_EXECUTABLE, ERRNO_FILE_NOT_EXECUTABLE_SHORT);
}};
}

View File

@@ -11,6 +11,7 @@ public class FunctionErrno extends Errno {
public static final Errno ERRNO_NULL_OR_EMPTY_PARAMETERS = new Errno(TYPE, 101, "The %1$s parameters passed to \"%2$s\" are null or empty.");
public static final Errno ERRNO_UNSET_PARAMETER = new Errno(TYPE, 102, "The %1$s parameter passed to \"%2$s\" must be set.");
public static final Errno ERRNO_UNSET_PARAMETERS = new Errno(TYPE, 103, "The %1$s parameters passed to \"%2$s\" must be set.");
public static final Errno ERRNO_INVALID_PARAMETER = new Errno(TYPE, 104, "The %1$s parameter passed to \"%2$s\" is invalid.\"%3$s\"");
FunctionErrno(final String type, final int code, final String message) {

View File

@@ -53,20 +53,25 @@ public class NotificationUtils {
* @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_*}.
* The builder returned will be {@code null} if {@link #NOTIFICATION_MODE_NONE}
* is passed. That case should ideally be handled before calling this function.
* @return Returns the {@link Notification.Builder}.
*/
@Nullable
public static Notification.Builder geNotificationBuilder(final Context context, final String channelId, final int priority, final CharSequence title, final CharSequence notificationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) {
public static Notification.Builder geNotificationBuilder(
final Context context, final String channelId, final int priority, final CharSequence title,
final CharSequence notificationText, final CharSequence notificationBigText,
final PendingIntent contentIntent, final PendingIntent deleteIntent, final int notificationMode) {
if (context == null) return null;
Notification.Builder builder = new Notification.Builder(context);
builder.setContentTitle(title);
builder.setContentText(notificationText);
builder.setStyle(new Notification.BigTextStyle().bigText(notificationBigText));
builder.setContentIntent(pendingIntent);
builder.setContentIntent(contentIntent);
builder.setDeleteIntent(deleteIntent);
builder.setPriority(priority);

View File

@@ -1,11 +1,17 @@
package com.termux.shared.packages;
import android.app.ActivityManager;
import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.UserManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.termux.shared.R;
import com.termux.shared.data.DataUtils;
@@ -14,8 +20,7 @@ import com.termux.shared.logger.Logger;
import com.termux.shared.termux.TermuxConstants;
import java.security.MessageDigest;
import javax.annotation.Nullable;
import java.util.List;
public class PackageUtils {
@@ -120,15 +125,27 @@ public class PackageUtils {
}
/**
* Get the {@code versionName} for the package associated with the {@code context}.
* Check if the app associated with the {@code context} has {@link ApplicationInfo#FLAG_DEBUGGABLE}
* set.
*
* @param context The {@link Context} for the package.
* @return Returns the {@code versionName}. This will be {@code null} if an exception is raised.
*/
public static Boolean isAppForPackageADebugBuild(@NonNull final Context context) {
public static Boolean isAppForPackageADebuggableBuild(@NonNull final Context context) {
return ( 0 != ( context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE ) );
}
/**
* Check if the app associated with the {@code context} has {@link ApplicationInfo#FLAG_EXTERNAL_STORAGE}
* set.
*
* @param context The {@link Context} for the package.
* @return Returns the {@code versionName}. This will be {@code null} if an exception is raised.
*/
public static Boolean isAppInstalledOnExternalStorage(@NonNull final Context context) {
return ( 0 != ( context.getApplicationInfo().flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE ) );
}
/**
* Get the {@code versionCode} for the package associated with the {@code context}.
*
@@ -163,7 +180,7 @@ public class PackageUtils {
* Get the {@code SHA-256 digest} of signing certificate for the package associated with the {@code context}.
*
* @param context The {@link Context} for the package.
* @return Returns the{@code SHA-256 digest}. This will be {@code null} if an exception is raised.
* @return Returns the {@code SHA-256 digest}. This will be {@code null} if an exception is raised.
*/
@Nullable
public static String getSigningCertificateSHA256DigestForPackage(@NonNull final Context context) {
@@ -184,4 +201,219 @@ public class PackageUtils {
}
}
/**
* Get the serial number for the current user.
*
* @param context The {@link Context} for operations.
* @return Returns the serial number. This will be {@code null} if failed to get it.
*/
@Nullable
public static Long getSerialNumberForCurrentUser(@NonNull Context context) {
UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
if (userManager == null) return null;
return userManager.getSerialNumberForUser(android.os.Process.myUserHandle());
}
/**
* Check if the current user is the primary user. This is done by checking if the the serial
* number for the current user equals 0.
*
* @param context The {@link Context} for operations.
* @return Returns {@code true} if the current user is the primary user, otherwise [@code false}.
*/
public static boolean isCurrentUserThePrimaryUser(@NonNull Context context) {
Long userId = getSerialNumberForCurrentUser(context);
return userId != null && userId == 0;
}
/**
* Get the profile owner package name for the current user.
*
* @param context The {@link Context} for operations.
* @return Returns the profile owner package name. This will be {@code null} if failed to get it
* or no profile owner for the current user.
*/
@Nullable
public static String getProfileOwnerPackageNameForUser(@NonNull Context context) {
DevicePolicyManager devicePolicyManager = (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
if (devicePolicyManager == null) return null;
List<ComponentName> activeAdmins = devicePolicyManager.getActiveAdmins();
if (activeAdmins != null){
for (ComponentName admin:activeAdmins){
String packageName = admin.getPackageName();
if(devicePolicyManager.isProfileOwnerApp(packageName))
return packageName;
}
}
return null;
}
/**
* Get the process id of the main app process of a package. This will work for sharedUserId. Note
* that some apps have multiple processes for the app like with `android:process=":background"`
* attribute in AndroidManifest.xml.
*
* @param context The {@link Context} for operations.
* @param packageName The package name of the process.
* @return Returns the process if found and running, otherwise {@code null}.
*/
@Nullable
public static String getPackagePID(final Context context, String packageName) {
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (activityManager != null) {
List<ActivityManager.RunningAppProcessInfo> processInfos = activityManager.getRunningAppProcesses();
if (processInfos != null) {
ActivityManager.RunningAppProcessInfo processInfo;
for (int i = 0; i < processInfos.size(); i++) {
processInfo = processInfos.get(i);
if (processInfo.processName.equals(packageName))
return String.valueOf(processInfo.pid);
}
}
}
return null;
}
/**
* Check if app is installed and enabled. This can be used by external apps that don't
* share `sharedUserId` with the an app.
*
* If your third-party app is targeting sdk `30` (android `11`), then it needs to add package
* name to the `queries` element or request `QUERY_ALL_PACKAGES` permission in its
* `AndroidManifest.xml`. Otherwise it will get `PackageSetting{...... package_name/......} BLOCKED`
* errors in `logcat` and `RUN_COMMAND` won't work.
* Check [package-visibility](https://developer.android.com/training/basics/intents/package-visibility#package-name),
* `QUERY_ALL_PACKAGES` [googleplay policy](https://support.google.com/googleplay/android-developer/answer/10158779
* and this [article](https://medium.com/androiddevelopers/working-with-package-visibility-dc252829de2d) for more info.
*
* {@code
* <manifest
* <queries>
* <package android:name="package_name" />
* </queries>
* </manifest>
* }
*
* @param context The context for operations.
* @return Returns {@code errmsg} if {@code packageName} is not installed or disabled, otherwise {@code null}.
*/
public static String isAppInstalled(@NonNull final Context context, String appName, String packageName) {
String errmsg = null;
PackageManager packageManager = context.getPackageManager();
ApplicationInfo applicationInfo;
try {
applicationInfo = packageManager.getApplicationInfo(packageName, 0);
} catch (final PackageManager.NameNotFoundException e) {
applicationInfo = null;
}
boolean isAppEnabled = (applicationInfo != null && applicationInfo.enabled);
// If app is not installed or is disabled
if (!isAppEnabled)
errmsg = context.getString(R.string.error_app_not_installed_or_disabled_warning, appName, packageName);
return errmsg;
}
/**
* Enable or disable a {@link ComponentName} with a call to
* {@link PackageManager#setComponentEnabledSetting(ComponentName, int, int)}.
*
* @param context The {@link Context} for operations.
* @param packageName The package name of the component.
* @param className The {@link Class} name of the component.
* @param state If component should be enabled or disabled.
* @param toastString If this is not {@code null} or empty, then a toast before setting state.
* @param showErrorMessage If an error message toast should be shown.
* @return Returns the errmsg if failed to set state, otherwise {@code null}.
*/
@Nullable
public static String setComponentState(@NonNull final Context context, @NonNull String packageName,
@NonNull String className, boolean state, String toastString,
boolean showErrorMessage) {
try {
PackageManager packageManager = context.getPackageManager();
if (packageManager != null) {
ComponentName componentName = new ComponentName(packageName, className);
if (toastString != null) Logger.showToast(context, toastString, true);
packageManager.setComponentEnabledSetting(componentName,
state ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
}
return null;
} catch (final Exception e) {
String errmsg = context.getString(
state ? R.string.error_enable_component_failed : R.string.error_disable_component_failed,
packageName, className) + ": " + e.getMessage();
if (showErrorMessage)
Logger.showToast(context, errmsg, true);
return errmsg;
}
}
/**
* Check if state of a {@link ComponentName} is {@link PackageManager#COMPONENT_ENABLED_STATE_DISABLED}
* with a call to {@link PackageManager#getComponentEnabledSetting(ComponentName)}.
*
* @param context The {@link Context} for operations.
* @param packageName The package name of the component.
* @param className The {@link Class} name of the component.
* @param logErrorMessage If an error message should be logged.
* @return Returns {@code true} if disabled, {@code false} if not and {@code null} if failed to
* get the state.
*/
public static Boolean isComponentDisabled(@NonNull final Context context, @NonNull String packageName,
@NonNull String className, boolean logErrorMessage) {
try {
PackageManager packageManager = context.getPackageManager();
if (packageManager != null) {
ComponentName componentName = new ComponentName(packageName, className);
// Will throw IllegalArgumentException: Unknown component: ComponentInfo{} if app
// for context is not installed or component does not exist.
return packageManager.getComponentEnabledSetting(componentName) == PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
}
} catch (final Exception e) {
if (logErrorMessage)
Logger.logStackTraceWithMessage(LOG_TAG, context.getString(R.string.error_get_component_state_failed, packageName, className), e);
}
return null;
}
/**
* Check if an {@link android.app.Activity} {@link ComponentName} can be called by calling
* {@link PackageManager#queryIntentActivities(Intent, int)}.
*
* @param context The {@link Context} for operations.
* @param packageName The package name of the component.
* @param className The {@link Class} name of the component.
* @param flags The flags to filter results.
* @return Returns {@code true} if it exists, otherwise {@code false}.
*/
public static boolean doesActivityComponentExist(@NonNull final Context context, @NonNull String packageName,
@NonNull String className, int flags) {
try {
PackageManager packageManager = context.getPackageManager();
if (packageManager != null) {
Intent intent = new Intent();
intent.setClassName(packageName, className);
return packageManager.queryIntentActivities(intent, flags).size() > 0;
}
} catch (final Exception e) {
// ignore
}
return false;
}
}

View File

@@ -63,7 +63,11 @@ public class PermissionUtils {
result = ContextCompat.checkSelfPermission(activity, permission);
if (result != PackageManager.PERMISSION_GRANTED) {
Logger.logDebug(LOG_TAG, "Requesting Permissions: " + Arrays.toString(permissions));
activity.requestPermissions(new String[]{permission}, requestCode);
try {
activity.requestPermissions(new String[]{permission}, requestCode);
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to request permissions with request code " + requestCode + ": " + Arrays.toString(permissions), e);
}
}
}
}

View File

@@ -245,17 +245,22 @@ public class SharedPreferenceUtils {
* @param sharedPreferences The {@link SharedPreferences} to get the value from.
* @param key The key for the value.
* @param def The default value if failed to read a valid value.
* @param defIfEmpty If set to {@code true}, then {@code def} will be returned if value is empty.
* @return Returns the {@code String} value stored in {@link SharedPreferences}, otherwise returns
* default if failed to read a valid value, like in case of an exception.
*/
public static String getString(SharedPreferences sharedPreferences, String key, String def) {
public static String getString(SharedPreferences sharedPreferences, String key, String def, boolean defIfEmpty) {
if (sharedPreferences == null) {
Logger.logError(LOG_TAG, "Error getting String value for the \"" + key + "\" key from null shared preferences. Returning default value \"" + def + "\".");
return def;
}
try {
return sharedPreferences.getString(key, def);
String value = sharedPreferences.getString(key, def);
if (defIfEmpty && (value == null || value.isEmpty()))
return def;
else
return value;
}
catch (ClassCastException e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Error getting String value for the \"" + key + "\" key from shared preferences. Returning default value \"" + def + "\".", e);

View File

@@ -0,0 +1,87 @@
package com.termux.shared.settings.preferences;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.termux.shared.logger.Logger;
import com.termux.shared.packages.PackageUtils;
import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_API_APP;
import com.termux.shared.termux.TermuxConstants;
public class TermuxAPIAppSharedPreferences {
private final Context mContext;
private final SharedPreferences mSharedPreferences;
private final SharedPreferences mMultiProcessSharedPreferences;
private static final String LOG_TAG = "TermuxAPIAppSharedPreferences";
private TermuxAPIAppSharedPreferences(@NonNull Context context) {
mContext = context;
mSharedPreferences = getPrivateSharedPreferences(mContext);
mMultiProcessSharedPreferences = getPrivateAndMultiProcessSharedPreferences(mContext);
}
/**
* Get the {@link Context} for a package name.
*
* @param context The {@link Context} to use to get the {@link Context} of the
* {@link TermuxConstants#TERMUX_API_PACKAGE_NAME}.
* @return Returns the {@link TermuxAPIAppSharedPreferences}. This will {@code null} if an exception is raised.
*/
@Nullable
public static TermuxAPIAppSharedPreferences build(@NonNull final Context context) {
Context termuxTaskerPackageContext = PackageUtils.getContextForPackage(context, TermuxConstants.TERMUX_API_PACKAGE_NAME);
if (termuxTaskerPackageContext == null)
return null;
else
return new TermuxAPIAppSharedPreferences(termuxTaskerPackageContext);
}
/**
* Get the {@link Context} for a package name.
*
* @param context The {@link Activity} to use to get the {@link Context} of the
* {@link TermuxConstants#TERMUX_API_PACKAGE_NAME}.
* @param exitAppOnError If {@code true} and failed to get package context, then a dialog will
* be shown which when dismissed will exit the app.
* @return Returns the {@link TermuxAPIAppSharedPreferences}. This will {@code null} if an exception is raised.
*/
public static TermuxAPIAppSharedPreferences build(@NonNull final Context context, final boolean exitAppOnError) {
Context termuxTaskerPackageContext = PackageUtils.getContextForPackageOrExitApp(context, TermuxConstants.TERMUX_API_PACKAGE_NAME, exitAppOnError);
if (termuxTaskerPackageContext == null)
return null;
else
return new TermuxAPIAppSharedPreferences(termuxTaskerPackageContext);
}
private static SharedPreferences getPrivateSharedPreferences(Context context) {
if (context == null) return null;
return SharedPreferenceUtils.getPrivateSharedPreferences(context, TermuxConstants.TERMUX_API_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
}
private static SharedPreferences getPrivateAndMultiProcessSharedPreferences(Context context) {
if (context == null) return null;
return SharedPreferenceUtils.getPrivateAndMultiProcessSharedPreferences(context, TermuxConstants.TERMUX_API_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
}
public int getLogLevel(boolean readFromFile) {
if (readFromFile)
return SharedPreferenceUtils.getInt(mMultiProcessSharedPreferences, TERMUX_API_APP.KEY_LOG_LEVEL, Logger.DEFAULT_LOG_LEVEL);
else
return SharedPreferenceUtils.getInt(mSharedPreferences, TERMUX_API_APP.KEY_LOG_LEVEL, Logger.DEFAULT_LOG_LEVEL);
}
public void setLogLevel(Context context, int logLevel, boolean commitToFile) {
logLevel = Logger.setLogLevel(context, logLevel);
SharedPreferenceUtils.setInt(mSharedPreferences, TERMUX_API_APP.KEY_LOG_LEVEL, logLevel, commitToFile);
}
}

View File

@@ -6,6 +6,7 @@ import android.content.SharedPreferences;
import android.util.TypedValue;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.termux.shared.packages.PackageUtils;
import com.termux.shared.termux.TermuxConstants;
@@ -13,22 +14,18 @@ import com.termux.shared.logger.Logger;
import com.termux.shared.data.DataUtils;
import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_APP;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class TermuxAppSharedPreferences {
private final Context mContext;
private final SharedPreferences mSharedPreferences;
private int MIN_FONTSIZE;
private int MAX_FONTSIZE;
private int DEFAULT_FONTSIZE;
private static final String LOG_TAG = "TermuxAppSharedPreferences";
private TermuxAppSharedPreferences(@Nonnull Context context) {
private TermuxAppSharedPreferences(@NonNull Context context) {
mContext = context;
mSharedPreferences = getPrivateSharedPreferences(mContext);
@@ -91,6 +88,16 @@ public class TermuxAppSharedPreferences {
public boolean isTerminalMarginAdjustmentEnabled() {
return SharedPreferenceUtils.getBoolean(mSharedPreferences, TERMUX_APP.KEY_TERMINAL_MARGIN_ADJUSTMENT, TERMUX_APP.DEFAULT_TERMINAL_MARGIN_ADJUSTMENT);
}
public void setTerminalMarginAdjustment(boolean value) {
SharedPreferenceUtils.setBoolean(mSharedPreferences, TERMUX_APP.KEY_TERMINAL_MARGIN_ADJUSTMENT, value, false);
}
public boolean isSoftKeyboardEnabled() {
return SharedPreferenceUtils.getBoolean(mSharedPreferences, TERMUX_APP.KEY_SOFT_KEYBOARD_ENABLED, TERMUX_APP.DEFAULT_VALUE_KEY_SOFT_KEYBOARD_ENABLED);
}
@@ -119,21 +126,33 @@ public class TermuxAppSharedPreferences {
private void setFontVariables(Context context) {
public static int[] getDefaultFontSizes(Context context) {
float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, context.getResources().getDisplayMetrics());
int[] sizes = new int[3];
// This is a bit arbitrary and sub-optimal. We want to give a sensible default for minimum font size
// to prevent invisible text due to zoom be mistake:
MIN_FONTSIZE = (int) (4f * dipInPixels);
sizes[1] = (int) (4f * dipInPixels); // min
// http://www.google.com/design/spec/style/typography.html#typography-line-height
int defaultFontSize = Math.round(12 * dipInPixels);
// Make it divisible by 2 since that is the minimal adjustment step:
if (defaultFontSize % 2 == 1) defaultFontSize--;
DEFAULT_FONTSIZE = defaultFontSize;
sizes[0] = defaultFontSize; // default
MAX_FONTSIZE = 256;
sizes[2] = 256; // max
return sizes;
}
public void setFontVariables(Context context) {
int[] sizes = getDefaultFontSizes(context);
DEFAULT_FONTSIZE = sizes[0];
MIN_FONTSIZE = sizes[1];
MAX_FONTSIZE = sizes[2];
}
public int getFontSize() {
@@ -146,7 +165,6 @@ public class TermuxAppSharedPreferences {
}
public void changeFontSize(boolean increase) {
int fontSize = getFontSize();
fontSize += (increase ? 1 : -1) * 2;
@@ -158,7 +176,7 @@ public class TermuxAppSharedPreferences {
public String getCurrentSession() {
return SharedPreferenceUtils.getString(mSharedPreferences, TERMUX_APP.KEY_CURRENT_SESSION, null);
return SharedPreferenceUtils.getString(mSharedPreferences, TERMUX_APP.KEY_CURRENT_SESSION, null, true);
}
public void setCurrentSession(String value) {

View File

@@ -0,0 +1,87 @@
package com.termux.shared.settings.preferences;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.termux.shared.logger.Logger;
import com.termux.shared.packages.PackageUtils;
import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_BOOT_APP;
import com.termux.shared.termux.TermuxConstants;
public class TermuxBootAppSharedPreferences {
private final Context mContext;
private final SharedPreferences mSharedPreferences;
private final SharedPreferences mMultiProcessSharedPreferences;
private static final String LOG_TAG = "TermuxBootAppSharedPreferences";
private TermuxBootAppSharedPreferences(@NonNull Context context) {
mContext = context;
mSharedPreferences = getPrivateSharedPreferences(mContext);
mMultiProcessSharedPreferences = getPrivateAndMultiProcessSharedPreferences(mContext);
}
/**
* Get the {@link Context} for a package name.
*
* @param context The {@link Context} to use to get the {@link Context} of the
* {@link TermuxConstants#TERMUX_BOOT_PACKAGE_NAME}.
* @return Returns the {@link TermuxBootAppSharedPreferences}. This will {@code null} if an exception is raised.
*/
@Nullable
public static TermuxBootAppSharedPreferences build(@NonNull final Context context) {
Context termuxTaskerPackageContext = PackageUtils.getContextForPackage(context, TermuxConstants.TERMUX_BOOT_PACKAGE_NAME);
if (termuxTaskerPackageContext == null)
return null;
else
return new TermuxBootAppSharedPreferences(termuxTaskerPackageContext);
}
/**
* Get the {@link Context} for a package name.
*
* @param context The {@link Activity} to use to get the {@link Context} of the
* {@link TermuxConstants#TERMUX_BOOT_PACKAGE_NAME}.
* @param exitAppOnError If {@code true} and failed to get package context, then a dialog will
* be shown which when dismissed will exit the app.
* @return Returns the {@link TermuxBootAppSharedPreferences}. This will {@code null} if an exception is raised.
*/
public static TermuxBootAppSharedPreferences build(@NonNull final Context context, final boolean exitAppOnError) {
Context termuxTaskerPackageContext = PackageUtils.getContextForPackageOrExitApp(context, TermuxConstants.TERMUX_BOOT_PACKAGE_NAME, exitAppOnError);
if (termuxTaskerPackageContext == null)
return null;
else
return new TermuxBootAppSharedPreferences(termuxTaskerPackageContext);
}
private static SharedPreferences getPrivateSharedPreferences(Context context) {
if (context == null) return null;
return SharedPreferenceUtils.getPrivateSharedPreferences(context, TermuxConstants.TERMUX_BOOT_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
}
private static SharedPreferences getPrivateAndMultiProcessSharedPreferences(Context context) {
if (context == null) return null;
return SharedPreferenceUtils.getPrivateAndMultiProcessSharedPreferences(context, TermuxConstants.TERMUX_BOOT_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
}
public int getLogLevel(boolean readFromFile) {
if (readFromFile)
return SharedPreferenceUtils.getInt(mMultiProcessSharedPreferences, TERMUX_BOOT_APP.KEY_LOG_LEVEL, Logger.DEFAULT_LOG_LEVEL);
else
return SharedPreferenceUtils.getInt(mSharedPreferences, TERMUX_BOOT_APP.KEY_LOG_LEVEL, Logger.DEFAULT_LOG_LEVEL);
}
public void setLogLevel(Context context, int logLevel, boolean commitToFile) {
logLevel = Logger.setLogLevel(context, logLevel);
SharedPreferenceUtils.setInt(mSharedPreferences, TERMUX_BOOT_APP.KEY_LOG_LEVEL, logLevel, commitToFile);
}
}

View File

@@ -0,0 +1,172 @@
package com.termux.shared.settings.preferences;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.termux.shared.data.DataUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.packages.PackageUtils;
import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_FLOAT_APP;
import com.termux.shared.termux.TermuxConstants;
public class TermuxFloatAppSharedPreferences {
private final Context mContext;
private final SharedPreferences mSharedPreferences;
private final SharedPreferences mMultiProcessSharedPreferences;
private int MIN_FONTSIZE;
private int MAX_FONTSIZE;
private int DEFAULT_FONTSIZE;
private static final String LOG_TAG = "TermuxFloatAppSharedPreferences";
private TermuxFloatAppSharedPreferences(@NonNull Context context) {
mContext = context;
mSharedPreferences = getPrivateSharedPreferences(mContext);
mMultiProcessSharedPreferences = getPrivateAndMultiProcessSharedPreferences(mContext);
setFontVariables(context);
}
/**
* Get the {@link Context} for a package name.
*
* @param context The {@link Context} to use to get the {@link Context} of the
* {@link TermuxConstants#TERMUX_FLOAT_PACKAGE_NAME}.
* @return Returns the {@link TermuxFloatAppSharedPreferences}. This will {@code null} if an exception is raised.
*/
@Nullable
public static TermuxFloatAppSharedPreferences build(@NonNull final Context context) {
Context termuxFloatPackageContext = PackageUtils.getContextForPackage(context, TermuxConstants.TERMUX_FLOAT_PACKAGE_NAME);
if (termuxFloatPackageContext == null)
return null;
else
return new TermuxFloatAppSharedPreferences(termuxFloatPackageContext);
}
/**
* Get the {@link Context} for a package name.
*
* @param context The {@link Activity} to use to get the {@link Context} of the
* {@link TermuxConstants#TERMUX_FLOAT_PACKAGE_NAME}.
* @param exitAppOnError If {@code true} and failed to get package context, then a dialog will
* be shown which when dismissed will exit the app.
* @return Returns the {@link TermuxFloatAppSharedPreferences}. This will {@code null} if an exception is raised.
*/
public static TermuxFloatAppSharedPreferences build(@NonNull final Context context, final boolean exitAppOnError) {
Context termuxFloatPackageContext = PackageUtils.getContextForPackageOrExitApp(context, TermuxConstants.TERMUX_FLOAT_PACKAGE_NAME, exitAppOnError);
if (termuxFloatPackageContext == null)
return null;
else
return new TermuxFloatAppSharedPreferences(termuxFloatPackageContext);
}
private static SharedPreferences getPrivateSharedPreferences(Context context) {
if (context == null) return null;
return SharedPreferenceUtils.getPrivateSharedPreferences(context, TermuxConstants.TERMUX_FLOAT_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
}
private static SharedPreferences getPrivateAndMultiProcessSharedPreferences(Context context) {
if (context == null) return null;
return SharedPreferenceUtils.getPrivateAndMultiProcessSharedPreferences(context, TermuxConstants.TERMUX_FLOAT_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
}
public int getWindowX() {
return SharedPreferenceUtils.getInt(mSharedPreferences, TERMUX_FLOAT_APP.KEY_WINDOW_X, 200);
}
public void setWindowX(int value) {
SharedPreferenceUtils.setInt(mSharedPreferences, TERMUX_FLOAT_APP.KEY_WINDOW_X, value, false);
}
public int getWindowY() {
return SharedPreferenceUtils.getInt(mSharedPreferences, TERMUX_FLOAT_APP.KEY_WINDOW_Y, 200);
}
public void setWindowY(int value) {
SharedPreferenceUtils.setInt(mSharedPreferences, TERMUX_FLOAT_APP.KEY_WINDOW_Y, value, false);
}
public int getWindowWidth() {
return SharedPreferenceUtils.getInt(mSharedPreferences, TERMUX_FLOAT_APP.KEY_WINDOW_WIDTH, 500);
}
public void setWindowWidth(int value) {
SharedPreferenceUtils.setInt(mSharedPreferences, TERMUX_FLOAT_APP.KEY_WINDOW_WIDTH, value, false);
}
public int getWindowHeight() {
return SharedPreferenceUtils.getInt(mSharedPreferences, TERMUX_FLOAT_APP.KEY_WINDOW_HEIGHT, 500);
}
public void setWindowHeight(int value) {
SharedPreferenceUtils.setInt(mSharedPreferences, TERMUX_FLOAT_APP.KEY_WINDOW_HEIGHT, value, false);
}
public void setFontVariables(Context context) {
int[] sizes = TermuxAppSharedPreferences.getDefaultFontSizes(context);
DEFAULT_FONTSIZE = sizes[0];
MIN_FONTSIZE = sizes[1];
MAX_FONTSIZE = sizes[2];
}
public int getFontSize() {
int fontSize = SharedPreferenceUtils.getIntStoredAsString(mSharedPreferences, TERMUX_FLOAT_APP.KEY_FONTSIZE, DEFAULT_FONTSIZE);
return DataUtils.clamp(fontSize, MIN_FONTSIZE, MAX_FONTSIZE);
}
public void setFontSize(int value) {
SharedPreferenceUtils.setIntStoredAsString(mSharedPreferences, TERMUX_FLOAT_APP.KEY_FONTSIZE, value, false);
}
public void changeFontSize(boolean increase) {
int fontSize = getFontSize();
fontSize += (increase ? 1 : -1) * 2;
fontSize = Math.max(MIN_FONTSIZE, Math.min(fontSize, MAX_FONTSIZE));
setFontSize(fontSize);
}
public int getLogLevel(boolean readFromFile) {
if (readFromFile)
return SharedPreferenceUtils.getInt(mMultiProcessSharedPreferences, TERMUX_FLOAT_APP.KEY_LOG_LEVEL, Logger.DEFAULT_LOG_LEVEL);
else
return SharedPreferenceUtils.getInt(mSharedPreferences, TERMUX_FLOAT_APP.KEY_LOG_LEVEL, Logger.DEFAULT_LOG_LEVEL);
}
public void setLogLevel(Context context, int logLevel, boolean commitToFile) {
logLevel = Logger.setLogLevel(context, logLevel);
SharedPreferenceUtils.setInt(mSharedPreferences, TERMUX_FLOAT_APP.KEY_LOG_LEVEL, logLevel, commitToFile);
}
public boolean isTerminalViewKeyLoggingEnabled(boolean readFromFile) {
if (readFromFile)
return SharedPreferenceUtils.getBoolean(mMultiProcessSharedPreferences, TERMUX_FLOAT_APP.KEY_TERMINAL_VIEW_KEY_LOGGING_ENABLED, TERMUX_FLOAT_APP.DEFAULT_VALUE_TERMINAL_VIEW_KEY_LOGGING_ENABLED);
else
return SharedPreferenceUtils.getBoolean(mSharedPreferences, TERMUX_FLOAT_APP.KEY_TERMINAL_VIEW_KEY_LOGGING_ENABLED, TERMUX_FLOAT_APP.DEFAULT_VALUE_TERMINAL_VIEW_KEY_LOGGING_ENABLED);
}
public void setTerminalViewKeyLoggingEnabled(boolean value, boolean commitToFile) {
SharedPreferenceUtils.setBoolean(mSharedPreferences, TERMUX_FLOAT_APP.KEY_TERMINAL_VIEW_KEY_LOGGING_ENABLED, value, commitToFile);
}
}

View File

@@ -1,7 +1,7 @@
package com.termux.shared.settings.preferences;
/*
* Version: v0.10.0
* Version: v0.15.0
*
* Changelog
*
@@ -44,6 +44,27 @@ package com.termux.shared.settings.preferences;
* - 0.10.0 (2021-05-12)
* - Added following to `TERMUX_APP`:
* `KEY_SOFT_KEYBOARD_ENABLED_ONLY_IF_NO_HARDWARE` and `DEFAULT_VALUE_KEY_SOFT_KEYBOARD_ENABLED_ONLY_IF_NO_HARDWARE`.
*
* - 0.11.0 (2021-07-08)
* - Added following to `TERMUX_APP`:
* `KEY_DISABLE_TERMINAL_MARGIN_ADJUSTMENT`.
*
* - 0.12.0 (2021-08-27)
* - Added `TERMUX_API_APP.KEY_LOG_LEVEL`, `TERMUX_BOOT_APP.KEY_LOG_LEVEL`,
* `TERMUX_FLOAT_APP.KEY_LOG_LEVEL`, `TERMUX_STYLING_APP.KEY_LOG_LEVEL`,
* `TERMUX_Widget_APP.KEY_LOG_LEVEL`.
*
* - 0.13.0 (2021-09-02)
* - Added following to `TERMUX_FLOAT_APP`:
* `KEY_WINDOW_X`, `KEY_WINDOW_Y`, `KEY_WINDOW_WIDTH`, `KEY_WINDOW_HEIGHT`, `KEY_FONTSIZE`,
* `KEY_TERMINAL_VIEW_KEY_LOGGING_ENABLED`.
*
* - 0.14.0 (2021-09-04)
* - Added `TERMUX_WIDGET_APP.KEY_TOKEN`.
*
* - 0.15.0 (2021-09-05)
* - Added following to `TERMUX_TASKER_APP`:
* `KEY_LAST_PENDING_INTENT_REQUEST_CODE` and `DEFAULT_VALUE_KEY_LAST_PENDING_INTENT_REQUEST_CODE`.
*/
/**
@@ -60,6 +81,15 @@ public final class TermuxPreferenceConstants {
*/
public static final class TERMUX_APP {
/**
* Defines the key for whether terminal view margin adjustment that is done to prevent soft
* keyboard from covering bottom part of terminal view on some devices is enabled or not.
* Margin adjustment may cause screen flickering on some devices and so should be disabled.
*/
public static final String KEY_TERMINAL_MARGIN_ADJUSTMENT = "terminal_margin_adjustment";
public static final boolean DEFAULT_TERMINAL_MARGIN_ADJUSTMENT = true;
/**
* Defines the key for whether to show terminal toolbar containing extra keys and text input field.
*/
@@ -102,7 +132,7 @@ public final class TermuxPreferenceConstants {
/**
* Defines the key for current termux log level.
* Defines the key for current log level.
*/
public static final String KEY_LOG_LEVEL = "log_level";
@@ -134,16 +164,131 @@ public final class TermuxPreferenceConstants {
}
/**
* Termux Tasker app constants.
* Termux:API app constants.
*/
public static final class TERMUX_TASKER_APP {
public static final class TERMUX_API_APP {
/**
* Defines the key for current termux log level.
* Defines the key for current log level.
*/
public static final String KEY_LOG_LEVEL = "log_level";
}
/**
* Termux:Boot app constants.
*/
public static final class TERMUX_BOOT_APP {
/**
* Defines the key for current log level.
*/
public static final String KEY_LOG_LEVEL = "log_level";
}
/**
* Termux:Float app constants.
*/
public static final class TERMUX_FLOAT_APP {
/**
* The float window x coordinate.
*/
public static final String KEY_WINDOW_X = "window_x";
/**
* The float window y coordinate.
*/
public static final String KEY_WINDOW_Y = "window_y";
/**
* The float window width.
*/
public static final String KEY_WINDOW_WIDTH = "window_width";
/**
* The float window height.
*/
public static final String KEY_WINDOW_HEIGHT = "window_height";
/**
* Defines the key for font size of termux terminal view.
*/
public static final String KEY_FONTSIZE = "fontsize";
/**
* Defines the key for current log level.
*/
public static final String KEY_LOG_LEVEL = "log_level";
/**
* Defines the key for whether termux terminal view key logging is enabled or not
*/
public static final String KEY_TERMINAL_VIEW_KEY_LOGGING_ENABLED = "terminal_view_key_logging_enabled";
public static final boolean DEFAULT_VALUE_TERMINAL_VIEW_KEY_LOGGING_ENABLED = false;
}
/**
* Termux:Styling app constants.
*/
public static final class TERMUX_STYLING_APP {
/**
* Defines the key for current log level.
*/
public static final String KEY_LOG_LEVEL = "log_level";
}
/**
* Termux:Tasker app constants.
*/
public static final class TERMUX_TASKER_APP {
/**
* Defines the key for current log level.
*/
public static final String KEY_LOG_LEVEL = "log_level";
/**
* Defines the key for last used PendingIntent request code.
*/
public static final String KEY_LAST_PENDING_INTENT_REQUEST_CODE = "last_pending_intent_request_code";
public static final int DEFAULT_VALUE_KEY_LAST_PENDING_INTENT_REQUEST_CODE = 0;
}
/**
* Termux:Widget app constants.
*/
public static final class TERMUX_WIDGET_APP {
/**
* Defines the key for current log level.
*/
public static final String KEY_LOG_LEVEL = "log_level";
/**
* Defines the key for current token for shortcuts.
*/
public static final String KEY_TOKEN = "token";
}
}

View File

@@ -0,0 +1,87 @@
package com.termux.shared.settings.preferences;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.termux.shared.logger.Logger;
import com.termux.shared.packages.PackageUtils;
import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_STYLING_APP;
import com.termux.shared.termux.TermuxConstants;
public class TermuxStylingAppSharedPreferences {
private final Context mContext;
private final SharedPreferences mSharedPreferences;
private final SharedPreferences mMultiProcessSharedPreferences;
private static final String LOG_TAG = "TermuxStylingAppSharedPreferences";
private TermuxStylingAppSharedPreferences(@NonNull Context context) {
mContext = context;
mSharedPreferences = getPrivateSharedPreferences(mContext);
mMultiProcessSharedPreferences = getPrivateAndMultiProcessSharedPreferences(mContext);
}
/**
* Get the {@link Context} for a package name.
*
* @param context The {@link Context} to use to get the {@link Context} of the
* {@link TermuxConstants#TERMUX_STYLING_PACKAGE_NAME}.
* @return Returns the {@link TermuxStylingAppSharedPreferences}. This will {@code null} if an exception is raised.
*/
@Nullable
public static TermuxStylingAppSharedPreferences build(@NonNull final Context context) {
Context termuxTaskerPackageContext = PackageUtils.getContextForPackage(context, TermuxConstants.TERMUX_STYLING_PACKAGE_NAME);
if (termuxTaskerPackageContext == null)
return null;
else
return new TermuxStylingAppSharedPreferences(termuxTaskerPackageContext);
}
/**
* Get the {@link Context} for a package name.
*
* @param context The {@link Activity} to use to get the {@link Context} of the
* {@link TermuxConstants#TERMUX_STYLING_PACKAGE_NAME}.
* @param exitAppOnError If {@code true} and failed to get package context, then a dialog will
* be shown which when dismissed will exit the app.
* @return Returns the {@link TermuxStylingAppSharedPreferences}. This will {@code null} if an exception is raised.
*/
public static TermuxStylingAppSharedPreferences build(@NonNull final Context context, final boolean exitAppOnError) {
Context termuxTaskerPackageContext = PackageUtils.getContextForPackageOrExitApp(context, TermuxConstants.TERMUX_STYLING_PACKAGE_NAME, exitAppOnError);
if (termuxTaskerPackageContext == null)
return null;
else
return new TermuxStylingAppSharedPreferences(termuxTaskerPackageContext);
}
private static SharedPreferences getPrivateSharedPreferences(Context context) {
if (context == null) return null;
return SharedPreferenceUtils.getPrivateSharedPreferences(context, TermuxConstants.TERMUX_STYLING_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
}
private static SharedPreferences getPrivateAndMultiProcessSharedPreferences(Context context) {
if (context == null) return null;
return SharedPreferenceUtils.getPrivateAndMultiProcessSharedPreferences(context, TermuxConstants.TERMUX_STYLING_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
}
public int getLogLevel(boolean readFromFile) {
if (readFromFile)
return SharedPreferenceUtils.getInt(mMultiProcessSharedPreferences, TERMUX_STYLING_APP.KEY_LOG_LEVEL, Logger.DEFAULT_LOG_LEVEL);
else
return SharedPreferenceUtils.getInt(mSharedPreferences, TERMUX_STYLING_APP.KEY_LOG_LEVEL, Logger.DEFAULT_LOG_LEVEL);
}
public void setLogLevel(Context context, int logLevel, boolean commitToFile) {
logLevel = Logger.setLogLevel(context, logLevel);
SharedPreferenceUtils.setInt(mSharedPreferences, TERMUX_STYLING_APP.KEY_LOG_LEVEL, logLevel, commitToFile);
}
}

View File

@@ -5,15 +5,13 @@ import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.termux.shared.packages.PackageUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_TASKER_APP;
import com.termux.shared.logger.Logger;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class TermuxTaskerAppSharedPreferences {
private final Context mContext;
@@ -23,7 +21,7 @@ public class TermuxTaskerAppSharedPreferences {
private static final String LOG_TAG = "TermuxTaskerAppSharedPreferences";
private TermuxTaskerAppSharedPreferences(@Nonnull Context context) {
private TermuxTaskerAppSharedPreferences(@NonNull Context context) {
mContext = context;
mSharedPreferences = getPrivateSharedPreferences(mContext);
mMultiProcessSharedPreferences = getPrivateAndMultiProcessSharedPreferences(mContext);
@@ -52,7 +50,7 @@ public class TermuxTaskerAppSharedPreferences {
* {@link TermuxConstants#TERMUX_TASKER_PACKAGE_NAME}.
* @param exitAppOnError If {@code true} and failed to get package context, then a dialog will
* be shown which when dismissed will exit the app.
* @return Returns the {@link TermuxAppSharedPreferences}. This will {@code null} if an exception is raised.
* @return Returns the {@link TermuxTaskerAppSharedPreferences}. This will {@code null} if an exception is raised.
*/
public static TermuxTaskerAppSharedPreferences build(@NonNull final Context context, final boolean exitAppOnError) {
Context termuxTaskerPackageContext = PackageUtils.getContextForPackageOrExitApp(context, TermuxConstants.TERMUX_TASKER_PACKAGE_NAME, exitAppOnError);
@@ -74,8 +72,8 @@ public class TermuxTaskerAppSharedPreferences {
public int getLogLevel(boolean readFromFfile) {
if (readFromFfile)
public int getLogLevel(boolean readFromFile) {
if (readFromFile)
return SharedPreferenceUtils.getInt(mMultiProcessSharedPreferences, TERMUX_TASKER_APP.KEY_LOG_LEVEL, Logger.DEFAULT_LOG_LEVEL);
else
return SharedPreferenceUtils.getInt(mSharedPreferences, TERMUX_TASKER_APP.KEY_LOG_LEVEL, Logger.DEFAULT_LOG_LEVEL);
@@ -86,4 +84,14 @@ public class TermuxTaskerAppSharedPreferences {
SharedPreferenceUtils.setInt(mSharedPreferences, TERMUX_TASKER_APP.KEY_LOG_LEVEL, logLevel, commitToFile);
}
public int getLastPendingIntentRequestCode() {
return SharedPreferenceUtils.getInt(mSharedPreferences, TERMUX_TASKER_APP.KEY_LAST_PENDING_INTENT_REQUEST_CODE, TERMUX_TASKER_APP.DEFAULT_VALUE_KEY_LAST_PENDING_INTENT_REQUEST_CODE);
}
public void setLastPendingIntentRequestCode(int lastPendingIntentRequestCode) {
SharedPreferenceUtils.setInt(mSharedPreferences, TERMUX_TASKER_APP.KEY_LAST_PENDING_INTENT_REQUEST_CODE, lastPendingIntentRequestCode, false);
}
}

View File

@@ -0,0 +1,106 @@
package com.termux.shared.settings.preferences;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.termux.shared.logger.Logger;
import com.termux.shared.packages.PackageUtils;
import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_WIDGET_APP;
import com.termux.shared.termux.TermuxConstants;
import java.util.UUID;
public class TermuxWidgetAppSharedPreferences {
private final Context mContext;
private final SharedPreferences mSharedPreferences;
private final SharedPreferences mMultiProcessSharedPreferences;
private static final String LOG_TAG = "TermuxWidgetAppSharedPreferences";
private TermuxWidgetAppSharedPreferences(@NonNull Context context) {
mContext = context;
mSharedPreferences = getPrivateSharedPreferences(mContext);
mMultiProcessSharedPreferences = getPrivateAndMultiProcessSharedPreferences(mContext);
}
/**
* Get the {@link Context} for a package name.
*
* @param context The {@link Context} to use to get the {@link Context} of the
* {@link TermuxConstants#TERMUX_WIDGET_PACKAGE_NAME}.
* @return Returns the {@link TermuxWidgetAppSharedPreferences}. This will {@code null} if an exception is raised.
*/
@Nullable
public static TermuxWidgetAppSharedPreferences build(@NonNull final Context context) {
Context termuxTaskerPackageContext = PackageUtils.getContextForPackage(context, TermuxConstants.TERMUX_WIDGET_PACKAGE_NAME);
if (termuxTaskerPackageContext == null)
return null;
else
return new TermuxWidgetAppSharedPreferences(termuxTaskerPackageContext);
}
/**
* Get the {@link Context} for a package name.
*
* @param context The {@link Activity} to use to get the {@link Context} of the
* {@link TermuxConstants#TERMUX_WIDGET_PACKAGE_NAME}.
* @param exitAppOnError If {@code true} and failed to get package context, then a dialog will
* be shown which when dismissed will exit the app.
* @return Returns the {@link TermuxWidgetAppSharedPreferences}. This will {@code null} if an exception is raised.
*/
public static TermuxWidgetAppSharedPreferences build(@NonNull final Context context, final boolean exitAppOnError) {
Context termuxTaskerPackageContext = PackageUtils.getContextForPackageOrExitApp(context, TermuxConstants.TERMUX_WIDGET_PACKAGE_NAME, exitAppOnError);
if (termuxTaskerPackageContext == null)
return null;
else
return new TermuxWidgetAppSharedPreferences(termuxTaskerPackageContext);
}
private static SharedPreferences getPrivateSharedPreferences(Context context) {
if (context == null) return null;
return SharedPreferenceUtils.getPrivateSharedPreferences(context, TermuxConstants.TERMUX_WIDGET_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
}
private static SharedPreferences getPrivateAndMultiProcessSharedPreferences(Context context) {
if (context == null) return null;
return SharedPreferenceUtils.getPrivateAndMultiProcessSharedPreferences(context, TermuxConstants.TERMUX_WIDGET_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
}
public static String getGeneratedToken(@NonNull Context context) {
TermuxWidgetAppSharedPreferences preferences = TermuxWidgetAppSharedPreferences.build(context, true);
if (preferences == null) return null;
return preferences.getGeneratedToken();
}
public String getGeneratedToken() {
String token = SharedPreferenceUtils.getString(mSharedPreferences, TERMUX_WIDGET_APP.KEY_TOKEN, null, true);
if (token == null) {
token = UUID.randomUUID().toString();
SharedPreferenceUtils.setString(mSharedPreferences, TERMUX_WIDGET_APP.KEY_TOKEN, token, true);
}
return token;
}
public int getLogLevel(boolean readFromFile) {
if (readFromFile)
return SharedPreferenceUtils.getInt(mMultiProcessSharedPreferences, TERMUX_WIDGET_APP.KEY_LOG_LEVEL, Logger.DEFAULT_LOG_LEVEL);
else
return SharedPreferenceUtils.getInt(mSharedPreferences, TERMUX_WIDGET_APP.KEY_LOG_LEVEL, Logger.DEFAULT_LOG_LEVEL);
}
public void setLogLevel(Context context, int logLevel, boolean commitToFile) {
logLevel = Logger.setLogLevel(context, logLevel);
SharedPreferenceUtils.setInt(mSharedPreferences, TERMUX_WIDGET_APP.KEY_LOG_LEVEL, logLevel, commitToFile);
}
}

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