Compare commits

..

57 Commits

Author SHA1 Message Date
agnostic-apollo
4dfed3320e Bump to v0.114 2021-06-11 02:57:56 +05:00
agnostic-apollo
7ac62c9840 Allow users to disable terminal session change toast
The user can add `disable-terminal-session-change-toast=true` entry to `termux.properties` file to disable terminal session change toast. The default value is `false`. Running `termux-reload-settings` command will also update the behaviour instantaneously if changed.

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

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

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

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

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

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

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

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

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

Fixes #2077
2021-05-23 21:14:30 +05:00
agnostic-apollo
b711a467c1 Bump to v0.113 2021-05-16 23:44:43 +05:00
agnostic-apollo
d736b1eba5 Implement TermuxActivity callbacks in TermuxTerminalViewClient and TermuxTerminalSessionClient 2021-05-16 23:33:44 +05:00
agnostic-apollo
58d577066a Release terminal beep SoundPool resources on activity stop to attempt to prevent exception
The following exception may be thrown, likely because of unreleased resources.

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

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

Updated javadocs with info on how cursor blinking works

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Updated onSingleTapUp() to also forcefully show keyboard.

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

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

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

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

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

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

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

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

1fe5c6b905 (54c3cc576ac595d35a41ce9e4a69e1c905fd6ea4)
2021-05-03 00:40:03 +05:00
agnostic-apollo
42ad3723fd Fix NullPointerExceptions for cases when TermuxActivity tries to access TermuxService when it doesn't hold a reference
Fixes #2026
2021-05-03 00:39:42 +05:00
agnostic-apollo
b268b6edf7 Disable error flashes when clearing TMPDIR directory on termux app exit
Rooted users were getting `Clearing $TMPDIR directory at path "/data/data/com.termux/files/usr/tmp" failed` flash errors when they exited Termux if directories existed in TMPDIR that only had `root` user ownership, since they would fail to get cleared since clearing was being run as the termux app user instead of as the root user. Now errors will only be logged to logcat.
2021-05-01 23:31:02 +05:00
agnostic-apollo
b84854af92 Update README.md 2021-05-01 19:35:52 +05:00
agnostic-apollo
cfebb3358d Update README 2021-04-27 15:50:57 +05:00
agnostic-apollo
93e1b13278 Update README 2021-04-26 12:43:55 +05:00
Fredrik Fornwall
0d4bfb7bd5 Replace jcenter() with mavenCentral()
This is since JCenter is being shut down.
2021-04-26 01:11:35 +02:00
agnostic-apollo
0aa5a123b7 Bump to v0.112
This only reverts the versioning login change done in a6ae656c since that caused F-Droid bot and Github Packages to fail to pick up new releases. The versions must be bumped directly in `build.gradle` file in future and not through other files like `gradle.properties`.
2021-04-22 19:57:08 +05:00
agnostic-apollo
2e156d4621 Update LICENSE 2021-04-21 17:01:52 +05:00
agnostic-apollo
fdcf6cb6e1 Update LICENSE 2021-04-21 17:00:38 +05:00
agnostic-apollo
01f2ed0892 Update LICENSE 2021-04-21 16:51:20 +05:00
agnostic-apollo
c9abfe5438 Update README 2021-04-21 16:51:08 +05:00
79 changed files with 3287 additions and 691 deletions

8
.gitattributes vendored
View File

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

View File

@@ -1,3 +1,7 @@
Released under [the GPLv3 license](https://www.gnu.org/licenses/gpl.html).
The `termux/termux-app` repository is released under [GPLv3 only](https://www.gnu.org/licenses/gpl-3.0.html) license.
Contains code from `Terminal Emulator for Android` by which is released under [the Apache License 2.0](https://www.apache.org/licenses/).
### Exceptions
- [Terminal Emulator for Android](https://github.com/jackpal/Android-Terminal-Emulator) code is used which is released under [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). Check [terminal-view](terminal-view) and [terminal-emulator](terminal-emulator) modules.
- [libcore/ojluni](https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/) code is used which is released under [GPLv2 only with "Classpath" exception](https://openjdk.java.net/legal/gplv2+ce.html). Check `com.termux.shared.file` package under [termux-shared](termux-shared) module.
- [libsuperuser ](https://github.com/Chainfire/libsuperuser) code is used which is released under [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). Check `com.termux.shared.shell.StreamGobbler` class under [termux-shared](termux-shared) module.

145
README.md
View File

@@ -6,65 +6,140 @@
[Termux](https://termux.com) is an Android terminal application and Linux environment.
- [Termux Reddit community](https://reddit.com/r/termux)
- [Termux Wiki](https://wiki.termux.com/wiki/)
- [Termux Twitter](http://twitter.com/termux/)
Note that this repository is for the app itself (the user interface and the terminal emulation). For the packages installable inside the app, see [termux/termux-packages](https://github.com/termux/termux-packages).
Note that this repository is for the app itself (the user interface and the
terminal emulation). For the packages installable inside the app, see
[termux/termux-packages](https://github.com/termux/termux-packages)
Quick how-to about Termux package management is available at [Package Management](https://github.com/termux/termux-packages/wiki/Package-Management). It also has info on how to fix **`repository is under maintenance or down`** errors when running `apt` or `pkg` commands.
***
**@termux is looking for Termux Application maintainer 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 current one (@fornwall) is inactive.**
Issue https://github.com/termux/termux-app/issues/1072 needs extra attention.
***
### Contents
- [Termux App and Plugins](#Termux-App-and-Plugins)
- [Installation](#Installation)
- [Uninstallation](#Uninstallation)
- [Important Links](#Important-Links)
- [For Devs and Contributors](#For-Devs-and-Contributors)
##
## Termux App and Plugins
The core [Termux](https://github.com/termux/termux-app) app comes with the following optional plugin apps.
- [Termux:API](https://github.com/termux/termux-api)
- [Termux:Boot](https://github.com/termux/termux-boot)
- [Termux:Float](https://github.com/termux/termux-float)
- [Termux:Styling](https://github.com/termux/termux-styling)
- [Termux:Tasker](https://github.com/termux/termux-tasker)
- [Termux:Widget](https://github.com/termux/termux-widget)
##
## Installation
Termux application can be obtained from [F-Droid](https://f-droid.org/en/packages/com.termux/).
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).
Additionally we provide per-commit debug builds for those who want to try
out the latest features or test their pull request. This build can be obtained
from one of the workflow runs listed on [Github Actions](https://github.com/termux/termux-app/actions)
page.
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.
Signature keys of all offered builds are different. Before you switch the
installation source, you will have to uninstall the Termux application and
all currently installed plugins.
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.
## Terminal resources
### F-Droid
Termux application can be obtained from F-Droid [here](https://f-droid.org/en/packages/com.termux/). It usually takes a few days (or even a week or more) for updates to be available on F-Droid once an update has been released on Github. F-Droid releases are built and published by F-Droid once they detect a new Github release. The Termux maintainers **do not** have any control over building and publishing of Termux app on F-Droid. Moreover, the Termux maintainers also do not have access to the APK signing keys of F-Droid releases, so we cannot release an APK ourselves on Github that would be compatible with F-Droid releases.
### Debug Builds
For users who don't want to wait for F-Droid releases and want to try out the latest features immediately or want to test their pull requests can get the APKs from [Github Actions](https://github.com/termux/termux-app/actions) page from the workflow runs labeled `Build`. The APK will be listed under `Artifacts` section. These are published for each commit done to the repository. These APKs are [debuggable](https://developer.android.com/studio/debug) and are also not compatible with other sources.
### Google Playstore **(Deprecated)**
**Termux and its plugins are no longer updated on [Google playstore](https://play.google.com/store/apps/details?id=com.termux) due to [android 10 issues](https://github.com/termux/termux-packages/wiki/Termux-and-Android-10).** The last version released for Android `>= 7` was `v0.101`. There are currently no immediate plans to resume updates on Google playstore. **It is highly recommended to not install Termux from playstore for now.** Any current users **should switch** to a different source like F-Droid.
If for some reason you don't want to switch, then at least check [Package Management](https://github.com/termux/termux-packages/wiki/Package-Management) to **change your mirror**, otherwise you will get **`repository is under maintenance or down`** errors when running `apt` or `pkg` commands. After that, it is also **highly advisable** to run `pkg upgrade` command to update all packages to the latest available versions, or at least update `termux-tools` package with `pkg install termux-tools` command.
##
## Uninstallation
Uninstallation may be required if a user doesn't want Termux installed in their device anymore or is switching to a different [install source](#Installation). You may also want to consider [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux) before uninstallation.
To uninstall Termux completely, you must uninstall **any and all existing Termux or its plugin app APKs** listed in [Termux App and Plugins](#Termux-App-and-Plugins).
Go to `Android Settings` -> `Applications` and then look for those apps. You can also use the search feature if its available on your device and search `termux` in the applications list.
Even if you think you have not installed any of the plugins, its strongly suggesting to go through the application list in Android settings and double check.
##
## Important Links
### Community
All community links are available [here](https://wiki.termux.com/wiki/Community).
The main ones are the following.
- [Termux Reddit community](https://reddit.com/r/termux)
- [Termux Matrix Channel](https://matrix.to/#termux_termux:gitter.im)
- [Termux Dev Matrix Channel](https://matrix.to/#termux_dev:gitter.im)
- [Termux Twitter](http://twitter.com/termux/)
- [Termux Reports Email](mailto:termuxreports@groups.io)
### Wikis
- [Termux Wiki](https://wiki.termux.com/wiki/)
- [Termux App Wiki](https://github.com/termux/termux-app/wiki)
- [Termux Packages Wiki](https://github.com/termux/termux-packages/wiki)
### Miscellaneous
- [FAQ](https://wiki.termux.com/wiki/FAQ)
- [Termux File System Layout](https://github.com/termux/termux-packages/wiki/Termux-file-system-layout)
- [Differences From Linux](https://wiki.termux.com/wiki/Differences_from_Linux)
- [Package Management](https://wiki.termux.com/wiki/Package_Management)
- [Remote_Access](https://wiki.termux.com/wiki/Remote_Access)
- [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux)
- [Terminal Settings](https://wiki.termux.com/wiki/Terminal_Settings)
- [Touch Keyboard](https://wiki.termux.com/wiki/Touch_Keyboard)
- [Android Storage and Sharing Data with Other Apps](https://wiki.termux.com/wiki/Internal_and_external_storage)
- [Android APIs](https://wiki.termux.com/wiki/Termux:API)
- [Moved Termux Packages Hosting From Bintray to IPFS](https://github.com/termux/termux-packages/issues/6348)
- [Termux and Android 10](https://github.com/termux/termux-packages/wiki/Termux-and-Android-10)
### Terminal resources
- [XTerm control sequences](http://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
- [vt100.net](http://vt100.net/)
- [Terminal codes (ANSI and terminfo equivalents)](http://wiki.bash-hackers.org/scripting/terminalcodes)
## Terminal emulators
### 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).
- 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](http://www.iterm2.com/documentation.html) (which includes [iTerm2 proprietary escape codes](http://www.iterm2.com/documentation-escape-codes.html)).
- Konsole: KDE terminal application. [Source](https://projects.kde.org/projects/kde/applications/konsole/repository),
in particular [tests](https://projects.kde.org/projects/kde/applications/konsole/repository/revisions/master/show/tests),
[Bugs](https://bugs.kde.org/buglist.cgi?bug_severity=critical&bug_severity=grave&bug_severity=major&bug_severity=crash&bug_severity=normal&bug_severity=minor&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole)
and [Wishes](https://bugs.kde.org/buglist.cgi?bug_severity=wishlist&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&product=konsole).
- 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).
- 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](http://invisible-island.net/datafiles/release/xterm.tar.gz).
- Connectbot: Android SSH client. [Source](https://github.com/connectbot/connectbot)
- Android Terminal Emulator: Android terminal app which Termux terminal handling
is based on. Inactive. [Source](https://github.com/jackpal/Android-Terminal-Emulator).
- Android Terminal Emulator: Android terminal app which Termux terminal handling is based on. Inactive. [Source](https://github.com/jackpal/Android-Terminal-Emulator).
##
## For Devs and Contributors
The [termux-shared](termux-shared) library was added in [`v0.109`](https://github.com/termux/termux-app/releases/tag/v0.109). It defines shared constants and utils of Termux app and its plugins. It was created to allow for removal of all hardcoded paths in Termux app. The termux plugins will hopefully use this in future as well. If you are contributing code that is using a constant or a util that may be shared, then define it in `termux-shared` library if it currently doesn't exist and reference it from there. Update the relevant changelogs as well. Pull requests using hardcoded values **will/should not** be accepted.
The main Termux constants are defined by [`TermuxConstants`](https://github.com/termux/termux-app/blob/master/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java) class. It also contains information on how to fork Termux or build it with your own package name. Changing the package name will require building the bootstrap zip packages and other packages with the new `$PREFIX`, check [Building Packages](https://github.com/termux/termux-packages/wiki/Building-packages) for more info.

View File

@@ -26,8 +26,8 @@ android {
applicationId "com.termux"
minSdkVersion project.properties.minSdkVersion.toInteger()
targetSdkVersion project.properties.targetSdkVersion.toInteger()
versionCode project.properties.termuxVersionCode.toInteger()
versionName project.properties.termuxVersion
versionCode 114
versionName "0.114"
manifestPlaceholders.TERMUX_PACKAGE_NAME = "com.termux"
manifestPlaceholders.TERMUX_APP_NAME = "Termux"
@@ -155,11 +155,11 @@ clean {
task downloadBootstraps() {
doLast {
def version = "2021.04.13-r1"
downloadBootstrap("aarch64", "ff82e5755d947cd1f3e0b30916d125c6ddd8ba3254801ca7499d73653417e158", version)
downloadBootstrap("arm", "53a7df2d6d0a36a8c9ab5259c8b5457c93b8bae8aec2321a470236b6da54e59a", version)
downloadBootstrap("i686", "f0e1399a13ebed6c5229fde161f9848d9f5eeae7b8cd82f31250a813b52e371", version)
downloadBootstrap("x86_64", "e36c4d8c933dc12b3f48937b7747c7a4dcfaa70f0dd89ad5e8b4465930075ae9", version)
def version = "2021.06.04-r1"
downloadBootstrap("aarch64", "115409ec8abe096080f9e6d9b2765e009c818b3e5aff0133792182009388b81e", version)
downloadBootstrap("arm", "c289688d4f9aabd86240a4d3e25bd89c1a96641e0d085d9ee5a471e9f019e16f", version)
downloadBootstrap("i686", "63cfb8e39cac20cecbe9e9890f49a30d7454d4c4dfa2b22b5d0cc6de1dc59726", version)
downloadBootstrap("x86_64", "db47007825e65f8ed15fc46d6b8e3119b01ca4a9856d7c7e5604cf804e753ba2", version)
}
}

View File

@@ -57,8 +57,7 @@
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|uiMode|keyboard|keyboardHidden|navigation"
android:label="@string/application_name"
android:launchMode="singleTask"
android:resizeableActivity="true"
android:windowSoftInputMode="adjustResize|stateAlwaysVisible">
android:resizeableActivity="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@@ -25,12 +25,12 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.autofill.AutofillManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.Toast;
import com.termux.R;
import com.termux.app.terminal.TermuxActivityRootView;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
import com.termux.app.activities.HelpActivity;
@@ -69,7 +69,6 @@ import androidx.viewpager.widget.ViewPager;
*/
public final class TermuxActivity extends Activity implements ServiceConnection {
/**
* The connection to the {@link TermuxService}. Requested in {@link #onCreate(Bundle)} with a call to
* {@link #bindService(Intent, ServiceConnection, int)}, and obtained and stored in
@@ -78,7 +77,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
TermuxService mTermuxService;
/**
* The main view of the activity showing the terminal. Initialized in onCreate().
* The {@link TerminalView} shown in {@link TermuxActivity} that displays the terminal.
*/
TerminalView mTerminalView;
@@ -104,6 +103,16 @@ public final class TermuxActivity extends Activity implements ServiceConnection
*/
private TermuxAppSharedProperties mProperties;
/**
* The root view of the {@link TermuxActivity}.
*/
TermuxActivityRootView mTermuxActivityRootView;
/**
* The space at the bottom of {@link @mTermuxActivityRootView} of the {@link TermuxActivity}.
*/
View mTermuxActivityBottomSpaceView;
/**
* The terminal extra keys view.
*/
@@ -130,10 +139,21 @@ public final class TermuxActivity extends Activity implements ServiceConnection
*/
private boolean mIsVisible;
/**
* If onResume() was called after onCreate().
*/
private boolean isOnResumeAfterOnCreate = false;
/**
* The {@link TermuxActivity} is in an invalid state and must not be run.
*/
private boolean mIsInvalidState;
private int mNavBarHeight;
private int mTerminalToolbarDefaultHeight;
private static final int CONTEXT_MENU_SELECT_URL_ID = 0;
private static final int CONTEXT_MENU_SHARE_TRANSCRIPT_ID = 1;
private static final int CONTEXT_MENU_AUTOFILL_ID = 2;
@@ -155,13 +175,13 @@ public final class TermuxActivity extends Activity implements ServiceConnection
public void onCreate(Bundle savedInstanceState) {
Logger.logDebug(LOG_TAG, "onCreate");
isOnResumeAfterOnCreate = true;
// Check if a crash happened on last run of the app and show a
// notification with the crash details if it did
CrashUtils.notifyCrash(this, LOG_TAG);
// Load termux shared preferences and properties
mPreferences = new TermuxAppSharedPreferences(this);
// Load termux shared properties
mProperties = new TermuxAppSharedProperties(this);
setActivityTheme();
@@ -170,6 +190,19 @@ public final class TermuxActivity extends Activity implements ServiceConnection
setContentView(R.layout.activity_termux);
// Load termux shared preferences
// This will also fail if TermuxConstants.TERMUX_PACKAGE_NAME does not equal applicationId
mPreferences = TermuxAppSharedPreferences.build(this, true);
if (mPreferences == null) {
// An AlertDialog should have shown to kill the app, so we don't continue running activity code
mIsInvalidState = true;
return;
}
mTermuxActivityRootView = findViewById(R.id.activity_termux_root_view);
mTermuxActivityRootView.setActivity(this);
mTermuxActivityBottomSpaceView = findViewById(R.id.activity_termux_bottom_space_view);
View content = findViewById(android.R.id.content);
content.setOnApplyWindowInsetsListener((v, insets) -> {
mNavBarHeight = insets.getSystemWindowInsetBottom();
@@ -212,34 +245,91 @@ public final class TermuxActivity extends Activity implements ServiceConnection
Logger.logDebug(LOG_TAG, "onStart");
if (mIsInvalidState) return;
mIsVisible = true;
if (mTermuxService != null) {
// The service has connected, but data may have changed since we were last in the foreground.
// Get the session stored in shared preferences stored by {@link #onStop} if its valid,
// otherwise get the last session currently running.
mTermuxTerminalSessionClient.setCurrentSession(mTermuxTerminalSessionClient.getCurrentStoredSessionOrLast());
termuxSessionListNotifyUpdated();
}
if (mTermuxTerminalSessionClient != null)
mTermuxTerminalSessionClient.onStart();
if (mTermuxTerminalViewClient != null)
mTermuxTerminalViewClient.onStart();
addTermuxActivityRootViewGlobalLayoutListener();
registerTermuxActivityBroadcastReceiver();
// If user changed the preference from {@link TermuxSettings} activity and returns, then
// update the {@link TerminalView#TERMINAL_VIEW_KEY_LOGGING_ENABLED} value.
mTerminalView.setIsTerminalViewKeyLoggingEnabled(mPreferences.getTerminalViewKeyLoggingEnabled());
// The current terminal session may have changed while being away, force
// a refresh of the displayed terminal.
mTerminalView.onScreenUpdated();
}
@Override
public void onResume() {
super.onResume();
setSoftKeyboardState();
Logger.logVerbose(LOG_TAG, "onResume");
if (mIsInvalidState) return;
if (mTermuxTerminalSessionClient != null)
mTermuxTerminalSessionClient.onResume();
if (mTermuxTerminalViewClient != null)
mTermuxTerminalViewClient.onResume();
isOnResumeAfterOnCreate = false;
}
@Override
protected void onStop() {
super.onStop();
Logger.logDebug(LOG_TAG, "onStop");
if (mIsInvalidState) return;
mIsVisible = false;
if (mTermuxTerminalSessionClient != null)
mTermuxTerminalSessionClient.onStop();
if (mTermuxTerminalViewClient != null)
mTermuxTerminalViewClient.onStop();
removeTermuxActivityRootViewGlobalLayoutListener();
unregisterTermuxActivityBroadcastReceiever();
getDrawer().closeDrawers();
}
@Override
public void onDestroy() {
super.onDestroy();
Logger.logDebug(LOG_TAG, "onDestroy");
if (mIsInvalidState) return;
if (mTermuxService != null) {
// Do not leave service and session clients with references to activity.
mTermuxService.unsetTermuxTerminalSessionClient();
mTermuxService = null;
}
try {
unbindService(this);
} catch (Exception e) {
// ignore.
}
}
@Override
public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
super.onSaveInstanceState(savedInstanceState);
saveTerminalToolbarTextInput(savedInstanceState);
}
/**
* Part of the {@link ServiceConnection} interface. The service is bound with
* {@link #bindService(Intent, ServiceConnection, int)} in {@link #onCreate(Bundle)} which will cause a call to this
@@ -297,41 +387,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
finishActivityIfNotFinishing();
}
@Override
protected void onStop() {
super.onStop();
Logger.logDebug(LOG_TAG, "onStop");
mIsVisible = false;
// Store current session in shared preferences so that it can be restored later in
// {@link #onStart} if needed.
mTermuxTerminalSessionClient.setCurrentStoredSession();
unregisterTermuxActivityBroadcastReceiever();
getDrawer().closeDrawers();
}
@Override
public void onDestroy() {
super.onDestroy();
Logger.logDebug(LOG_TAG, "onDestroy");
if (mTermuxService != null) {
// Do not leave service and session clients with references to activity.
mTermuxService.unsetTermuxTerminalSessionClient();
mTermuxService = null;
}
unbindService(this);
}
@Override
public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
super.onSaveInstanceState(savedInstanceState);
saveTerminalToolbarTextInput(savedInstanceState);
}
@@ -352,9 +408,46 @@ public final class TermuxActivity extends Activity implements ServiceConnection
public void addTermuxActivityRootViewGlobalLayoutListener() {
getTermuxActivityRootView().getViewTreeObserver().addOnGlobalLayoutListener(getTermuxActivityRootView());
}
public void removeTermuxActivityRootViewGlobalLayoutListener() {
if (getTermuxActivityRootView() != null)
getTermuxActivityRootView().getViewTreeObserver().removeOnGlobalLayoutListener(getTermuxActivityRootView());
}
private void setTermuxTerminalViewAndClients() {
// Set termux terminal view and session clients
mTermuxTerminalSessionClient = new TermuxTerminalSessionClient(this);
mTermuxTerminalViewClient = new TermuxTerminalViewClient(this, mTermuxTerminalSessionClient);
// Set termux terminal view
mTerminalView = findViewById(R.id.terminal_view);
mTerminalView.setTerminalViewClient(mTermuxTerminalViewClient);
if (mTermuxTerminalViewClient != null)
mTermuxTerminalViewClient.onCreate();
if (mTermuxTerminalSessionClient != null)
mTermuxTerminalSessionClient.onCreate();
}
private void setTermuxSessionsListView() {
ListView termuxSessionsListView = findViewById(R.id.terminal_sessions_list);
mTermuxSessionListViewController = new TermuxSessionsListViewController(this, mTermuxService.getTermuxSessions());
termuxSessionsListView.setAdapter(mTermuxSessionListViewController);
termuxSessionsListView.setOnItemClickListener(mTermuxSessionListViewController);
termuxSessionsListView.setOnItemLongClickListener(mTermuxSessionListViewController);
}
private void setTerminalToolbarView(Bundle savedInstanceState) {
final ViewPager terminalToolbarViewPager = findViewById(R.id.terminal_toolbar_view_pager);
if (mPreferences.getShowTerminalToolbar()) terminalToolbarViewPager.setVisibility(View.VISIBLE);
if (mPreferences.shouldShowTerminalToolbar()) terminalToolbarViewPager.setVisibility(View.VISIBLE);
ViewGroup.LayoutParams layoutParams = terminalToolbarViewPager.getLayoutParams();
mTerminalToolbarDefaultHeight = layoutParams.height;
@@ -374,8 +467,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
if (terminalToolbarViewPager == null) return;
ViewGroup.LayoutParams layoutParams = terminalToolbarViewPager.getLayoutParams();
layoutParams.height = (int) Math.round(mTerminalToolbarDefaultHeight *
(mProperties.getExtraKeysInfo() == null ? 0 : mProperties.getExtraKeysInfo().getMatrix().length) *
mProperties.getTerminalToolbarHeightScaleFactor());
(mProperties.getExtraKeysInfo() == null ? 0 : mProperties.getExtraKeysInfo().getMatrix().length) *
mProperties.getTerminalToolbarHeightScaleFactor());
terminalToolbarViewPager.setLayoutParams(layoutParams);
}
@@ -418,8 +511,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
private void setToggleKeyboardView() {
findViewById(R.id.toggle_keyboard_button).setOnClickListener(v -> {
InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0);
mTermuxTerminalViewClient.onToggleSoftKeyboardRequest();
getDrawer().closeDrawers();
});
@@ -429,50 +521,6 @@ public final class TermuxActivity extends Activity implements ServiceConnection
});
}
private void setSoftKeyboardState() {
// If soft keyboard is to disabled
if (!mPreferences.getSoftKeyboardEnabled()) {
getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
} else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
}
// If soft keyboard is to be hidden on startup
if (mProperties.shouldSoftKeyboardBeHiddenOnStartup()) {
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
}
}
private void setTermuxTerminalViewAndClients() {
// Set termux terminal view and session clients
mTermuxTerminalSessionClient = new TermuxTerminalSessionClient(this);
mTermuxTerminalViewClient = new TermuxTerminalViewClient(this, mTermuxTerminalSessionClient);
// Set termux terminal view
mTerminalView = findViewById(R.id.terminal_view);
mTerminalView.setTerminalViewClient(mTermuxTerminalViewClient);
mTerminalView.setTextSize(mPreferences.getFontSize());
mTerminalView.setKeepScreenOn(mPreferences.getKeepScreenOn());
// Set {@link TerminalView#TERMINAL_VIEW_KEY_LOGGING_ENABLED} value
mTerminalView.setIsTerminalViewKeyLoggingEnabled(mPreferences.getTerminalViewKeyLoggingEnabled());
mTerminalView.requestFocus();
mTermuxTerminalSessionClient.checkForFontAndColors();
}
private void setTermuxSessionsListView() {
ListView termuxSessionsListView = findViewById(R.id.terminal_sessions_list);
mTermuxSessionListViewController = new TermuxSessionsListViewController(this, mTermuxService.getTermuxSessions());
termuxSessionsListView.setAdapter(mTermuxSessionListViewController);
termuxSessionsListView.setOnItemClickListener(mTermuxSessionListViewController);
termuxSessionsListView.setOnItemLongClickListener(mTermuxSessionListViewController);
}
@@ -524,7 +572,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
menu.add(Menu.NONE, CONTEXT_MENU_RESET_TERMINAL_ID, Menu.NONE, R.string.action_reset_terminal);
menu.add(Menu.NONE, CONTEXT_MENU_KILL_PROCESS_ID, Menu.NONE, getResources().getString(R.string.action_kill_process, getCurrentSession().getPid())).setEnabled(currentSession.isRunning());
menu.add(Menu.NONE, CONTEXT_MENU_STYLING_ID, Menu.NONE, R.string.action_style_terminal);
menu.add(Menu.NONE, CONTEXT_MENU_TOGGLE_KEEP_SCREEN_ON, Menu.NONE, R.string.action_toggle_keep_screen_on).setCheckable(true).setChecked(mPreferences.getKeepScreenOn());
menu.add(Menu.NONE, CONTEXT_MENU_TOGGLE_KEEP_SCREEN_ON, Menu.NONE, R.string.action_toggle_keep_screen_on).setCheckable(true).setChecked(mPreferences.shouldKeepScreenOn());
menu.add(Menu.NONE, CONTEXT_MENU_HELP_ID, Menu.NONE, R.string.action_open_help);
menu.add(Menu.NONE, CONTEXT_MENU_SETTINGS_ID, Menu.NONE, R.string.action_open_settings);
menu.add(Menu.NONE, CONTEXT_MENU_REPORT_ID, Menu.NONE, R.string.action_report_issue);
@@ -660,6 +708,14 @@ public final class TermuxActivity extends Activity implements ServiceConnection
return mNavBarHeight;
}
public TermuxActivityRootView getTermuxActivityRootView() {
return mTermuxActivityRootView;
}
public View getTermuxActivityBottomSpaceView() {
return mTermuxActivityBottomSpaceView;
}
public ExtraKeysView getExtraKeysView() {
return mExtraKeysView;
}
@@ -680,6 +736,10 @@ public final class TermuxActivity extends Activity implements ServiceConnection
return mIsVisible;
}
public boolean isOnResumeAfterOnCreate() {
return isOnResumeAfterOnCreate;
}
public TermuxService getTermuxService() {
@@ -690,6 +750,10 @@ public final class TermuxActivity extends Activity implements ServiceConnection
return mTerminalView;
}
public TermuxTerminalViewClient getTermuxTerminalViewClient() {
return mTermuxTerminalViewClient;
}
public TermuxTerminalSessionClient getTermuxTerminalSessionClient() {
return mTermuxTerminalSessionClient;
}
@@ -757,7 +821,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
return;
case TERMUX_ACTIVITY.ACTION_RELOAD_STYLE:
Logger.logDebug(LOG_TAG, "Received intent to reload styling");
reloadTermuxActivityStyling();
reloadActivityStyling();
return;
default:
}
@@ -765,11 +829,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
}
}
private void reloadTermuxActivityStyling() {
if (mTermuxTerminalSessionClient != null) {
mTermuxTerminalSessionClient.checkForFontAndColors();
}
private void reloadActivityStyling() {
if (mProperties!= null) {
mProperties.loadTermuxPropertiesFromDisk();
@@ -780,7 +840,14 @@ public final class TermuxActivity extends Activity implements ServiceConnection
setTerminalToolbarHeight();
setSoftKeyboardState();
if (mTermuxTerminalSessionClient != null)
mTermuxTerminalSessionClient.onReload();
if (mTermuxTerminalViewClient != null)
mTermuxTerminalViewClient.onReload();
if (mTermuxService != null)
mTermuxService.setTerminalTranscriptRows();
// To change the activity and drawer theme, activity needs to be recreated.
// But this will destroy the activity, and will call the onCreate() again.

View File

@@ -20,7 +20,8 @@ public class TermuxApplication extends Application {
private void setLogLevel() {
// Load the log level from shared preferences and set it to the {@link Logger.CURRENT_LOG_LEVEL}
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(getApplicationContext());
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(getApplicationContext());
if (preferences == null) return;
preferences.setLogLevel(null, preferences.getLogLevel());
Logger.logDebug("Starting Application");
}

View File

@@ -12,6 +12,7 @@ import android.view.WindowManager;
import com.termux.R;
import com.termux.shared.file.FileUtils;
import com.termux.shared.interact.DialogUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.termux.TermuxConstants;
@@ -57,8 +58,9 @@ final class TermuxInstaller {
if (!isPrimaryUser) {
String bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message, TermuxConstants.TERMUX_PREFIX_DIR_PATH);
Logger.logError(LOG_TAG, bootstrapErrorMessage);
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(bootstrapErrorMessage)
.setOnDismissListener(dialog -> System.exit(0)).setPositiveButton(android.R.string.ok, null).show();
DialogUtils.exitAppWithErrorMessage(activity,
activity.getString(R.string.bootstrap_error_title),
bootstrapErrorMessage);
return;
}
@@ -171,6 +173,7 @@ final class TermuxInstaller {
activity.finish();
}).setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
dialog.dismiss();
FileUtils.deleteFile(activity, "prefix directory", PREFIX_FILE_PATH, true);
TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone);
}).show();
} catch (WindowManager.BadTokenException e1) {

View File

@@ -20,6 +20,7 @@ import android.provider.Settings;
import android.widget.ArrayAdapter;
import com.termux.R;
import com.termux.app.settings.properties.TermuxAppSharedProperties;
import com.termux.app.terminal.TermuxTerminalSessionClient;
import com.termux.app.utils.PluginUtils;
import com.termux.shared.termux.TermuxConstants;
@@ -106,6 +107,8 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
/** If the user has executed the {@link TERMUX_SERVICE#ACTION_STOP_SERVICE} intent. */
boolean mWantsToStop = false;
public Integer mTerminalTranscriptRows;
private static final String LOG_TAG = "TermuxService";
@Override
@@ -503,6 +506,7 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
// If the execution command was started for a plugin, only then will the stdout be set
// Otherwise if command was manually started by the user like by adding a new terminal session,
// then no need to set stdout
executionCommand.terminalTranscriptRows = getTerminalTranscriptRows();
TermuxSession newTermuxSession = TermuxSession.execute(this, executionCommand, getTermuxTerminalSessionClient(), this, sessionName, executionCommand.isPluginExecutionCommand);
if (newTermuxSession == null) {
Logger.logError(LOG_TAG, "Failed to execute new TermuxSession command for:\n" + executionCommand.getCommandIdAndLabelLogString());
@@ -560,6 +564,19 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
updateNotification();
}
/** Get the terminal transcript rows to be used for new {@link TermuxSession}. */
public Integer getTerminalTranscriptRows() {
if (mTerminalTranscriptRows == null)
setTerminalTranscriptRows();
return mTerminalTranscriptRows;
}
public void setTerminalTranscriptRows() {
// TermuxService only uses this termux property currently, so no need to load them all into
// an internal values map like TermuxActivity does
mTerminalTranscriptRows = TermuxAppSharedProperties.getTerminalTranscriptRows(this);
}
@@ -743,8 +760,9 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
private void setCurrentStoredTerminalSession(TerminalSession session) {
if (session == null) return;
// Make the newly created session the current one to be displayed:
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(this);
// Make the newly created session the current one to be displayed
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(this);
if (preferences == null) return;
preferences.setCurrentSession(session.mHandle);
}

View File

@@ -12,6 +12,8 @@ import android.webkit.WebViewClient;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import com.termux.shared.termux.TermuxConstants;
/** Basic embedded browser for viewing help pages. */
public final class HelpActivity extends Activity {
@@ -39,7 +41,7 @@ public final class HelpActivity extends Activity {
mWebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.startsWith("https://wiki.termux.com")) {
if (url.equals(TermuxConstants.TERMUX_WIKI_URL) || url.startsWith(TermuxConstants.TERMUX_WIKI_URL + "/")) {
// Inline help.
setContentView(progressLayout);
return false;
@@ -60,7 +62,7 @@ public final class HelpActivity extends Activity {
setContentView(mWebView);
}
});
mWebView.loadUrl("https://wiki.termux.com/wiki/Main_Page");
mWebView.loadUrl(TermuxConstants.TERMUX_WIKI_URL);
}
@Override

View File

@@ -92,8 +92,8 @@ public class ReportActivity extends AppCompatActivity {
final Markwon markwon = MarkdownUtils.getRecyclerMarkwonBuilder(this);
final MarkwonAdapter adapter = MarkwonAdapter.builderTextViewIsRoot(R.layout.activity_report_adapter_node_default)
.include(FencedCodeBlock.class, SimpleEntry.create(R.layout.activity_report_adapter_node_code_block, R.id.code_text_view))
final MarkwonAdapter adapter = MarkwonAdapter.builderTextViewIsRoot(R.layout.markdown_adapter_node_default)
.include(FencedCodeBlock.class, SimpleEntry.create(R.layout.markdown_adapter_node_code_block, R.id.code_text_view))
.build();
recyclerView.setLayoutManager(new LinearLayoutManager(this));

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,10 @@
package com.termux.app.fragments.settings;
package com.termux.app.fragments.settings.termux;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.ListPreference;
import androidx.preference.PreferenceCategory;
@@ -20,20 +21,32 @@ 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(getContext()));
preferenceManager.setPreferenceDataStore(DebuggingPreferencesDataStore.getInstance(context));
setPreferencesFromResource(R.xml.debugging_preferences, rootKey);
setPreferencesFromResource(R.xml.termux_debugging_preferences, rootKey);
configureLoggingPreferences(context);
}
private void configureLoggingPreferences(@NonNull Context context) {
PreferenceCategory loggingCategory = findPreference("logging");
if (loggingCategory == null) return;
if (loggingCategory != null) {
final ListPreference logLevelListPreference = setLogLevelListPreferenceData(findPreference("log_level"), getActivity());
ListPreference logLevelListPreference = findPreference("log_level");
if (logLevelListPreference != null) {
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context, true);
if (preferences == null) return;
setLogLevelListPreferenceData(logLevelListPreference, context, preferences.getLogLevel());
loggingCategory.addPreference(logLevelListPreference);
}
}
protected ListPreference setLogLevelListPreferenceData(ListPreference logLevelListPreference, Context context) {
public static ListPreference setLogLevelListPreferenceData(ListPreference logLevelListPreference, Context context, int logLevel) {
if (logLevelListPreference == null)
logLevelListPreference = new ListPreference(context);
@@ -43,8 +56,8 @@ public class DebuggingPreferencesFragment extends PreferenceFragmentCompat {
logLevelListPreference.setEntryValues(logLevels);
logLevelListPreference.setEntries(logLevelLabels);
logLevelListPreference.setValue(String.valueOf(Logger.getLogLevel()));
logLevelListPreference.setDefaultValue(Logger.getLogLevel());
logLevelListPreference.setValue(String.valueOf(logLevel));
logLevelListPreference.setDefaultValue(Logger.DEFAULT_LOG_LEVEL);
return logLevelListPreference;
}
@@ -60,12 +73,12 @@ class DebuggingPreferencesDataStore extends PreferenceDataStore {
private DebuggingPreferencesDataStore(Context context) {
mContext = context;
mPreferences = new TermuxAppSharedPreferences(context);
mPreferences = TermuxAppSharedPreferences.build(context, true);
}
public static synchronized DebuggingPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new DebuggingPreferencesDataStore(context.getApplicationContext());
mInstance = new DebuggingPreferencesDataStore(context);
}
return mInstance;
}
@@ -75,6 +88,7 @@ class DebuggingPreferencesDataStore extends PreferenceDataStore {
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
if (mPreferences == null) return null;
if (key == null) return null;
switch (key) {
@@ -87,6 +101,7 @@ class DebuggingPreferencesDataStore extends PreferenceDataStore {
@Override
public void putString(String key, @Nullable String value) {
if (mPreferences == null) return;
if (key == null) return;
switch (key) {
@@ -104,6 +119,7 @@ class DebuggingPreferencesDataStore extends PreferenceDataStore {
@Override
public void putBoolean(String key, boolean value) {
if (mPreferences == null) return;
if (key == null) return;
switch (key) {
@@ -123,13 +139,14 @@ class DebuggingPreferencesDataStore extends PreferenceDataStore {
@Override
public boolean getBoolean(String key, boolean defValue) {
if (mPreferences == null) return false;
switch (key) {
case "terminal_view_key_logging_enabled":
return mPreferences.getTerminalViewKeyLoggingEnabled();
return mPreferences.isTerminalViewKeyLoggingEnabled();
case "plugin_error_notifications_enabled":
return mPreferences.getPluginErrorNotificationsEnabled();
return mPreferences.arePluginErrorNotificationsEnabled();
case "crash_report_notifications_enabled":
return mPreferences.getCrashReportNotificationsEnabled();
return mPreferences.areCrashReportNotificationsEnabled();
default:
return false;
}

View File

@@ -1,4 +1,4 @@
package com.termux.app.fragments.settings;
package com.termux.app.fragments.settings.termux;
import android.content.Context;
import android.os.Bundle;
@@ -16,10 +16,13 @@ public class TerminalIOPreferencesFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(TerminalIOPreferencesDataStore.getInstance(getContext()));
Context context = getContext();
if (context == null) return;
setPreferencesFromResource(R.xml.terminal_io_preferences, rootKey);
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setPreferenceDataStore(TerminalIOPreferencesDataStore.getInstance(context));
setPreferencesFromResource(R.xml.termux_terminal_io_preferences, rootKey);
}
}
@@ -33,12 +36,12 @@ class TerminalIOPreferencesDataStore extends PreferenceDataStore {
private TerminalIOPreferencesDataStore(Context context) {
mContext = context;
mPreferences = new TermuxAppSharedPreferences(context);
mPreferences = TermuxAppSharedPreferences.build(context, true);
}
public static synchronized TerminalIOPreferencesDataStore getInstance(Context context) {
if (mInstance == null) {
mInstance = new TerminalIOPreferencesDataStore(context.getApplicationContext());
mInstance = new TerminalIOPreferencesDataStore(context);
}
return mInstance;
}
@@ -47,12 +50,16 @@ class TerminalIOPreferencesDataStore extends PreferenceDataStore {
@Override
public void putBoolean(String key, boolean value) {
if (mPreferences == null) return;
if (key == null) return;
switch (key) {
case "soft_keyboard_enabled":
mPreferences.setSoftKeyboardEnabled(value);
break;
case "soft_keyboard_enabled_only_if_no_hardware":
mPreferences.setSoftKeyboardEnabledOnlyIfNoHardware(value);
break;
default:
break;
}
@@ -60,9 +67,13 @@ class TerminalIOPreferencesDataStore extends PreferenceDataStore {
@Override
public boolean getBoolean(String key, boolean defValue) {
if (mPreferences == null) return false;
switch (key) {
case "soft_keyboard_enabled":
return mPreferences.getSoftKeyboardEnabled();
return mPreferences.isSoftKeyboardEnabled();
case "soft_keyboard_enabled_only_if_no_hardware":
return mPreferences.isSoftKeyboardEnabledOnlyIfNoHardware();
default:
return false;
}

View File

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

View File

@@ -2,8 +2,9 @@ package com.termux.app.models;
public enum UserAction {
PLUGIN_EXECUTION_COMMAND("plugin execution command"),
ABOUT("about"),
CRASH_REPORT("crash report"),
PLUGIN_EXECUTION_COMMAND("plugin execution command"),
REPORT_ISSUE_FROM_TRANSCRIPT("report issue from transcript");
private final String name;

View File

@@ -5,7 +5,6 @@ import android.content.Context;
import com.termux.app.terminal.io.KeyboardShortcut;
import com.termux.app.terminal.io.extrakeys.ExtraKeysInfo;
import com.termux.shared.logger.Logger;
import com.termux.shared.settings.properties.SharedPropertiesParser;
import com.termux.shared.settings.properties.TermuxPropertyConstants;
import com.termux.shared.settings.properties.TermuxSharedProperties;
@@ -17,7 +16,7 @@ import java.util.Map;
import javax.annotation.Nonnull;
public class TermuxAppSharedProperties extends TermuxSharedProperties implements SharedPropertiesParser {
public class TermuxAppSharedProperties extends TermuxSharedProperties {
private ExtraKeysInfo mExtraKeysInfo;
private List<KeyboardShortcut> mSessionShortcuts = new ArrayList<>();
@@ -96,4 +95,13 @@ public class TermuxAppSharedProperties extends TermuxSharedProperties implements
return mExtraKeysInfo;
}
/**
* Load the {@link TermuxPropertyConstants#KEY_TERMINAL_TRANSCRIPT_ROWS} value from termux properties file on disk.
*/
public static int getTerminalTranscriptRows(Context context) {
return (int) TermuxSharedProperties.getInternalPropertyValue(context, TermuxPropertyConstants.KEY_TERMINAL_TRANSCRIPT_ROWS);
}
}

View File

@@ -0,0 +1,227 @@
package com.termux.app.terminal;
import android.content.Context;
import android.graphics.Rect;
import android.inputmethodservice.InputMethodService;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.inputmethod.EditorInfo;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import com.termux.app.TermuxActivity;
import com.termux.shared.logger.Logger;
import com.termux.shared.view.ViewUtils;
/**
* The {@link TermuxActivity} relies on {@link android.view.WindowManager.LayoutParams#SOFT_INPUT_ADJUST_RESIZE)}
* set by {@link TermuxTerminalViewClient#setSoftKeyboardState(boolean, boolean)} to automatically
* resize the view and push the terminal up when soft keyboard is opened. However, this does not
* always work properly. When `enforce-char-based-input=true` is set in `termux.properties`
* and {@link com.termux.view.TerminalView#onCreateInputConnection(EditorInfo)} sets the inputType
* to `InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS`
* instead of the default `InputType.TYPE_NULL` for termux, some keyboards may still show suggestions.
* Gboard does too, but only when text is copied and clipboard suggestions **and** number keys row
* toggles are enabled in its settings. When number keys row toggle is not enabled, Gboard will still
* show the row but will switch it with suggestions if needed. If its enabled, then number keys row
* is always shown and suggestions are shown in an additional row on top of it. This additional row is likely
* part of the candidates view returned by the keyboard app in {@link InputMethodService#onCreateCandidatesView()}.
*
* With the above configuration, the additional clipboard suggestions row partially covers the
* extra keys/terminal. Reopening the keyboard/activity does not fix the issue. This is either a bug
* in the Android OS where it does not consider the candidate's view height in its calculation to push
* up the view or because Gboard does not include the candidate's view height in the height reported
* to android that should be used, hence causing an overlap.
*
* Gboard logs the following entry to `logcat` when its opened with or without the suggestions bar showing:
* I/KeyboardViewUtil: KeyboardViewUtil.calculateMaxKeyboardBodyHeight():62 leave 500 height for app when screen height:2392, header height:176 and isFullscreenMode:false, so the max keyboard body height is:1716
* where `keyboard_height = screen_height - height_for_app - header_height` (62 is a hardcoded value in Gboard source code and may be a version number)
* So this may in fact be due to Gboard but https://stackoverflow.com/questions/57567272 suggests
* otherwise. Another similar report https://stackoverflow.com/questions/66761661.
* Also check https://github.com/termux/termux-app/issues/1539.
*
* This overlap may happen even without `enforce-char-based-input=true` for keyboards with extended layouts
* like number row, etc.
*
* To fix these issues, `activity_termux.xml` has the constant 1sp transparent
* `activity_termux_bottom_space_view` View at the bottom. This will appear as a line matching the
* activity theme. When {@link TermuxActivity} {@link ViewTreeObserver.OnGlobalLayoutListener} is
* called when any of the sub view layouts change, like keyboard opening/closing keyboard,
* extra keys/input view switched, etc, we check if the bottom space view is visible or not.
* If its not, then we add a margin to the bottom of the root view, so that the keyboard does not
* overlap the extra keys/terminal, since the margin will push up the view. By default the margin
* added is equal to the height of the hidden part of extra keys/terminal. For Gboard's case, the
* hidden part equals the `header_height`. The updates to margins may cause a jitter in some cases
* when the view is redrawn if the margin is incorrect, but logic has been implemented to avoid that.
*/
public class TermuxActivityRootView extends LinearLayout implements ViewTreeObserver.OnGlobalLayoutListener {
public TermuxActivity mActivity;
public Integer marginBottom;
public Integer lastMarginBottom;
/** Log root view events. */
private boolean ROOT_VIEW_LOGGING_ENABLED = false;
private static final String LOG_TAG = "TermuxActivityRootView";
public TermuxActivityRootView(Context context) {
super(context);
}
public TermuxActivityRootView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public TermuxActivityRootView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setActivity(TermuxActivity activity) {
mActivity = activity;
}
/**
* Sets whether root view logging is enabled or not.
*
* @param value The boolean value that defines the state.
*/
public void setIsRootViewLoggingEnabled(boolean value) {
ROOT_VIEW_LOGGING_ENABLED = value;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (marginBottom != null) {
if (ROOT_VIEW_LOGGING_ENABLED)
Logger.logVerbose(LOG_TAG, "onMeasure: Setting bottom margin to " + marginBottom);
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) getLayoutParams();
params.setMargins(0, 0, 0, marginBottom);
setLayoutParams(params);
marginBottom = null;
requestLayout();
}
}
@Override
public void onGlobalLayout() {
if (mActivity == null || !mActivity.isVisible()) return;
View bottomSpaceView = mActivity.getTermuxActivityBottomSpaceView();
if (bottomSpaceView == null) return;
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) getLayoutParams();
// Get the position Rects of the bottom space view and the main window holding it
Rect[] windowAndViewRects = ViewUtils.getWindowAndViewRects(bottomSpaceView);
if (windowAndViewRects == null)
return;
Rect windowAvailableRect = windowAndViewRects[0];
Rect bottomSpaceViewRect = windowAndViewRects[1];
// If the bottomSpaceViewRect is inside the windowAvailableRect, then it must be completely visible
boolean isVisible = windowAvailableRect.contains(bottomSpaceViewRect);
boolean isVisibleBecauseMargin = (windowAvailableRect.bottom == bottomSpaceViewRect.bottom) && params.bottomMargin > 0;
boolean isVisibleBecauseExtraMargin = (bottomSpaceViewRect.bottom - windowAvailableRect.bottom) < 0;
if (ROOT_VIEW_LOGGING_ENABLED)
Logger.logVerbose(LOG_TAG, "onGlobalLayout: windowAvailableRect " + windowAvailableRect.bottom + ", bottomSpaceViewRect " + bottomSpaceViewRect.bottom + ", diff " + (bottomSpaceViewRect.bottom - windowAvailableRect.bottom) + ", bottom " + params.bottomMargin + ", isVisible " + isVisible + ", isVisibleBecauseMargin " + isVisibleBecauseMargin + ", isVisibleBecauseExtraMargin " + isVisibleBecauseExtraMargin);
// If the bottomSpaceViewRect is visible, then remove the margin if needed
if (isVisible) {
// If visible because of margin, i.e the bottom of bottomSpaceViewRect equals that of windowAvailableRect
// and a margin has been added
// Necessary so that we don't get stuck in an infinite loop since setting margin
// will call OnGlobalLayoutListener again and next time bottom space view
// will be visible and margin will be set to 0, which again will call
// OnGlobalLayoutListener...
// Calling addTermuxActivityRootViewGlobalLayoutListener with a delay fails to
// set appropriate margins when views are changed quickly since some changes
// may be missed.
if (isVisibleBecauseMargin) {
if (ROOT_VIEW_LOGGING_ENABLED)
Logger.logVerbose(LOG_TAG, "onGlobalLayout: Visible due to margin");
// Once the view has been redrawn with new margin, we set margin back to 0 so that
// when next time onMeasure() is called, margin 0 is used. This is necessary for
// cases when view has been redrawn with new margin because bottom space view was
// hidden by keyboard and then view was redrawn again due to layout change (like
// keyboard symbol view is switched to), android will add margin below its new position
// if its greater than 0, which was already above the keyboard creating x2x margin.
marginBottom = 0;
return;
}
boolean setMargin = params.bottomMargin != 0;
// If visible because of extra margin, i.e the bottom of bottomSpaceViewRect is above that of windowAvailableRect
// onGlobalLayout: windowAvailableRect 1408, bottomSpaceViewRect 1232, diff -176, bottom 0, isVisible true, isVisibleBecauseMargin false, isVisibleBecauseExtraMargin false
// onGlobalLayout: Bottom margin already equals 0
if (isVisibleBecauseExtraMargin) {
if (ROOT_VIEW_LOGGING_ENABLED)
Logger.logVerbose(LOG_TAG, "onGlobalLayout: Resetting margin since visible due to extra margin");
setMargin = true;
// lastMarginBottom must be invalid. May also happen when keyboards are changed.
lastMarginBottom = null;
}
if (setMargin) {
if (ROOT_VIEW_LOGGING_ENABLED)
Logger.logVerbose(LOG_TAG, "onGlobalLayout: Setting bottom margin to 0");
params.setMargins(0, 0, 0, 0);
setLayoutParams(params);
} else {
if (ROOT_VIEW_LOGGING_ENABLED)
Logger.logVerbose(LOG_TAG, "onGlobalLayout: Bottom margin already equals 0");
// This is done so that when next time onMeasure() is called, lastMarginBottom is used.
// This is done since we **expect** the keyboard to have same dimensions next time layout
// changes, so best set margin while view is drawn the first time, otherwise it will
// cause a jitter when OnGlobalLayoutListener is called with margin 0 and it sets the
// likely same lastMarginBottom again and requesting a redraw. Hopefully, this logic
// works fine for all cases.
marginBottom = lastMarginBottom;
}
}
// ELse find the part of the extra keys/terminal that is hidden and add a margin accordingly
else {
int pxHidden = bottomSpaceViewRect.bottom - windowAvailableRect.bottom;
if (ROOT_VIEW_LOGGING_ENABLED)
Logger.logVerbose(LOG_TAG, "onGlobalLayout: pxHidden " + pxHidden + ", bottom " + params.bottomMargin);
boolean setMargin = params.bottomMargin != pxHidden;
// If invisible despite margin, i.e a margin was added, but the bottom of bottomSpaceViewRect
// is still below that of windowAvailableRect, this will trigger OnGlobalLayoutListener
// again, so that margins are set properly. May happen when toolbar/extra keys is disabled
// and enabled from left drawer, just like case for isVisibleBecauseExtraMargin.
// onMeasure: Setting bottom margin to 176
// onGlobalLayout: windowAvailableRect 1232, bottomSpaceViewRect 1408, diff 176, bottom 176, isVisible false, isVisibleBecauseMargin false, isVisibleBecauseExtraMargin false
// onGlobalLayout: Bottom margin already equals 176
if ((bottomSpaceViewRect.bottom - windowAvailableRect.bottom) > 0) {
if (ROOT_VIEW_LOGGING_ENABLED)
Logger.logVerbose(LOG_TAG, "onGlobalLayout: Force setting margin since not visible despite margin");
setMargin = true;
}
if (setMargin) {
if (ROOT_VIEW_LOGGING_ENABLED)
Logger.logVerbose(LOG_TAG, "onGlobalLayout: Setting bottom margin to " + pxHidden);
params.setMargins(0, 0, 0, pxHidden);
setLayoutParams(params);
lastMarginBottom = pxHidden;
} else {
if (ROOT_VIEW_LOGGING_ENABLED)
Logger.logVerbose(LOG_TAG, "onGlobalLayout: Bottom margin already equals " + pxHidden);
}
}
}
}

View File

@@ -37,20 +37,76 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
private static final int MAX_SESSIONS = 8;
private final SoundPool mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
private SoundPool mBellSoundPool;
private final int mBellSoundId;
private int mBellSoundId;
private static final String LOG_TAG = "TermuxTerminalSessionClient";
public TermuxTerminalSessionClient(TermuxActivity activity) {
this.mActivity = activity;
mBellSoundId = mBellSoundPool.load(activity, R.raw.bell, 1);
}
/**
* Should be called when mActivity.onCreate() is called
*/
public void onCreate() {
// Set terminal fonts and colors
checkForFontAndColors();
}
/**
* Should be called when mActivity.onStart() is called
*/
public void onStart() {
// The service has connected, but data may have changed since we were last in the foreground.
// Get the session stored in shared preferences stored by {@link #onStop} if its valid,
// otherwise get the last session currently running.
if (mActivity.getTermuxService() != null) {
setCurrentSession(getCurrentStoredSessionOrLast());
termuxSessionListNotifyUpdated();
}
// The current terminal session may have changed while being away, force
// a refresh of the displayed terminal.
mActivity.getTerminalView().onScreenUpdated();
}
/**
* Should be called when mActivity.onResume() is called
*/
public void onResume() {
// Just initialize the mBellSoundPool and load the sound, otherwise bell might not run
// the first time bell key is pressed and play() is called, since sound may not be loaded
// quickly enough before the call to play(). https://stackoverflow.com/questions/35435625
getBellSoundPool();
}
/**
* Should be called when mActivity.onStop() is called
*/
public void onStop() {
// Store current session in shared preferences so that it can be restored later in
// {@link #onStart} if needed.
setCurrentStoredSession();
// Release mBellSoundPool resources, specially to prevent exceptions like the following to be thrown
// java.util.concurrent.TimeoutException: android.media.SoundPool.finalize() timed out after 10 seconds
// Bell is not played in background anyways
// Related: https://stackoverflow.com/a/28708351/14686958
releaseBellSoundPool();
}
/**
* Should be called when mActivity.reloadActivityStyling() is called
*/
public void onReload() {
// Set terminal fonts and colors
checkForFontAndColors();
}
@Override
public void onTextChanged(TerminalSession changedSession) {
if (!mActivity.isVisible()) return;
@@ -74,7 +130,9 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
@Override
public void onSessionFinished(final TerminalSession finishedSession) {
if (mActivity.getTermuxService().wantsToStop()) {
TermuxService service = mActivity.getTermuxService();
if (service == null || service.wantsToStop()) {
// The service wants to stop as soon as possible.
mActivity.finishActivityIfNotFinishing();
return;
@@ -82,7 +140,7 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
if (mActivity.isVisible() && finishedSession != mActivity.getCurrentSession()) {
// Show toast for non-current sessions that exit.
int indexOfSession = mActivity.getTermuxService().getIndexOfSession(finishedSession);
int indexOfSession = service.getIndexOfSession(finishedSession);
// Verify that session was not removed before we got told about it finishing:
if (indexOfSession >= 0)
mActivity.showToast(toToastTitle(finishedSession) + " - exited", true);
@@ -91,7 +149,7 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
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 (mActivity.getTermuxService().getTermuxSessionsSize() > 1) {
if (service.getTermuxSessionsSize() > 1) {
removeFinishedSession(finishedSession);
}
} else {
@@ -120,13 +178,12 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
BellHandler.getInstance(mActivity).doBell();
break;
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_BEEP:
mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
getBellSoundPool().play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
break;
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_IGNORE:
// Ignore the bell character.
break;
}
}
@Override
@@ -135,6 +192,49 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
updateBackgroundColor();
}
@Override
public void onTerminalCursorStateChange(boolean enabled) {
// Do not start cursor blinking thread if activity is not visible
if (enabled && !mActivity.isVisible()) {
Logger.logVerbose(LOG_TAG, "Ignoring call to start cursor blinking since activity is not visible");
return;
}
// If cursor is to enabled now, then start cursor blinking if blinking is enabled
// otherwise stop cursor blinking
mActivity.getTerminalView().setTerminalCursorBlinkerState(enabled, false);
}
@Override
public Integer getTerminalCursorStyle() {
return mActivity.getProperties().getTerminalCursorStyle();
}
/** Initialize and get mBellSoundPool */
private synchronized SoundPool getBellSoundPool() {
if (mBellSoundPool == null) {
mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
mBellSoundId = mBellSoundPool.load(mActivity, R.raw.bell, 1);
}
return mBellSoundPool;
}
/** Release mBellSoundPool resources */
private synchronized void releaseBellSoundPool() {
if (mBellSoundPool != null) {
mBellSoundPool.release();
mBellSoundPool = null;
}
}
/** Try switching to session. */
@@ -155,12 +255,15 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
void notifyOfSessionChange() {
if (!mActivity.isVisible()) return;
TerminalSession session = mActivity.getCurrentSession();
mActivity.showToast(toToastTitle(session), false);
if (!mActivity.getProperties().areTerminalSessionChangeToastsDisabled()) {
TerminalSession session = mActivity.getCurrentSession();
mActivity.showToast(toToastTitle(session), false);
}
}
public void switchToSession(boolean forward) {
TermuxService service = mActivity.getTermuxService();
if (service == null) return;
TerminalSession currentTerminalSession = mActivity.getCurrentSession();
int index = service.getIndexOfSession(currentTerminalSession);
@@ -177,7 +280,10 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
}
public void switchToSession(int index) {
TermuxSession termuxSession = mActivity.getTermuxService().getTermuxSession(index);
TermuxService service = mActivity.getTermuxService();
if (service == null) return;
TermuxSession termuxSession = service.getTermuxSession(index);
if (termuxSession != null)
setCurrentSession(termuxSession.getTerminalSession());
}
@@ -193,7 +299,10 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
}
public void addNewSession(boolean isFailSafe, String sessionName) {
if (mActivity.getTermuxService().getTermuxSessionsSize() >= MAX_SESSIONS) {
TermuxService service = mActivity.getTermuxService();
if (service == null) return;
if (service.getTermuxSessionsSize() >= MAX_SESSIONS) {
new AlertDialog.Builder(mActivity).setTitle(R.string.title_max_terminals_reached).setMessage(R.string.msg_max_terminals_reached)
.setPositiveButton(android.R.string.ok, null).show();
} else {
@@ -206,7 +315,7 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
workingDirectory = currentSession.getCwd();
}
TermuxSession newTermuxSession = mActivity.getTermuxService().createTermuxSession(null, null, null, workingDirectory, isFailSafe, sessionName);
TermuxSession newTermuxSession = service.createTermuxSession(null, null, null, workingDirectory, isFailSafe, sessionName);
if (newTermuxSession == null) return;
TerminalSession newTerminalSession = newTermuxSession.getTerminalSession();
@@ -226,14 +335,17 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
/** The current session as stored or the last one if that does not exist. */
public TerminalSession getCurrentStoredSessionOrLast() {
TerminalSession stored = getCurrentStoredSession(mActivity);
TerminalSession stored = getCurrentStoredSession();
if (stored != null) {
// If a stored session is in the list of currently running sessions, then return it
return stored;
} else {
// Else return the last session currently running
TermuxSession termuxSession = mActivity.getTermuxService().getLastTermuxSession();
TermuxService service = mActivity.getTermuxService();
if (service == null) return null;
TermuxSession termuxSession = service.getLastTermuxSession();
if (termuxSession != null)
return termuxSession.getTerminalSession();
else
@@ -241,7 +353,7 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
}
}
private TerminalSession getCurrentStoredSession(TermuxActivity context) {
private TerminalSession getCurrentStoredSession() {
String sessionHandle = mActivity.getPreferences().getCurrentSession();
// If no session is stored in shared preferences
@@ -249,16 +361,20 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
return null;
// Check if the session handle found matches one of the currently running sessions
return context.getTermuxService().getTerminalSessionForHandle(sessionHandle);
TermuxService service = mActivity.getTermuxService();
if (service == null) return null;
return service.getTerminalSessionForHandle(sessionHandle);
}
public void removeFinishedSession(TerminalSession finishedSession) {
// Return pressed with finished session - remove it.
TermuxService service = mActivity.getTermuxService();
if (service == null) return;
int index = service.removeTermuxSession(finishedSession);
int size = mActivity.getTermuxService().getTermuxSessionsSize();
int size = service.getTermuxSessionsSize();
if (size == 0) {
// There are no sessions to show, so finish the activity.
mActivity.finishActivityIfNotFinishing();
@@ -278,7 +394,10 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
public void checkAndScrollToSession(TerminalSession session) {
if (!mActivity.isVisible()) return;
final int indexOfSession = mActivity.getTermuxService().getIndexOfSession(session);
TermuxService service = mActivity.getTermuxService();
if (service == null) return;
final int indexOfSession = service.getIndexOfSession(session);
if (indexOfSession < 0) return;
final ListView termuxSessionsListView = mActivity.findViewById(R.id.terminal_sessions_list);
if (termuxSessionsListView == null) return;
@@ -290,7 +409,10 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
String toToastTitle(TerminalSession session) {
final int indexOfSession = mActivity.getTermuxService().getIndexOfSession(session);
TermuxService service = mActivity.getTermuxService();
if (service == null) return null;
final int indexOfSession = service.getIndexOfSession(session);
if (indexOfSession < 0) return null;
StringBuilder toastTitle = new StringBuilder("[" + (indexOfSession + 1) + "]");
if (!TextUtils.isEmpty(session.mSessionName)) {

View File

@@ -14,7 +14,8 @@ import android.view.Gravity;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.inputmethod.InputMethodManager;
import android.view.View;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.Toast;
@@ -33,6 +34,7 @@ import com.termux.shared.data.DataUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.termux.TermuxUtils;
import com.termux.shared.view.KeyboardUtils;
import com.termux.terminal.KeyHandler;
import com.termux.terminal.TerminalEmulator;
import com.termux.terminal.TerminalSession;
@@ -53,11 +55,89 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
private Runnable mShowSoftKeyboardRunnable;
private boolean mShowSoftKeyboardIgnoreOnce;
private boolean mShowSoftKeyboardWithDelayOnce;
private boolean mTerminalCursorBlinkerStateAlreadySet;
private static final String LOG_TAG = "TermuxTerminalViewClient";
public TermuxTerminalViewClient(TermuxActivity activity, TermuxTerminalSessionClient termuxTerminalSessionClient) {
this.mActivity = activity;
this.mTermuxTerminalSessionClient = termuxTerminalSessionClient;
}
/**
* Should be called when mActivity.onCreate() is called
*/
public void onCreate() {
mActivity.getTerminalView().setTextSize(mActivity.getPreferences().getFontSize());
mActivity.getTerminalView().setKeepScreenOn(mActivity.getPreferences().shouldKeepScreenOn());
}
/**
* Should be called when mActivity.onStart() is called
*/
public void onStart() {
// Set {@link TerminalView#TERMINAL_VIEW_KEY_LOGGING_ENABLED} value
// Also required if user changed the preference from {@link TermuxSettings} activity and returns
boolean isTerminalViewKeyLoggingEnabled = mActivity.getPreferences().isTerminalViewKeyLoggingEnabled();
mActivity.getTerminalView().setIsTerminalViewKeyLoggingEnabled(isTerminalViewKeyLoggingEnabled);
// Piggyback on the terminal view key logging toggle for now, should add a separate toggle in future
mActivity.getTermuxActivityRootView().setIsRootViewLoggingEnabled(isTerminalViewKeyLoggingEnabled);
}
/**
* Should be called when mActivity.onResume() is called
*/
public void onResume() {
// Show the soft keyboard if required
setSoftKeyboardState(true, false);
mTerminalCursorBlinkerStateAlreadySet = false;
}
/**
* Should be called when mActivity.onStop() is called
*/
public void onStop() {
// Stop terminal cursor blinking if enabled
setTerminalCursorBlinkerState(false);
}
/**
* Should be called when mActivity.reloadActivityStyling() is called
*/
public void onReload() {
// Show the soft keyboard if required
setSoftKeyboardState(false, true);
// Start terminal cursor blinking if enabled
setTerminalCursorBlinkerState(true);
}
/**
* Should be called when {@link com.termux.view.TerminalView#mEmulator}
*/
@Override
public void onEmulatorSet() {
if (!mTerminalCursorBlinkerStateAlreadySet) {
// Start terminal cursor blinking if enabled
// We need to wait for the first session to be attached that's set in
// TermuxActivity.onServiceConnected() and then the multiple calls to TerminalView.updateSize()
// where the final one eventually sets the mEmulator when width/height is not 0. Otherwise
// blinker will not start again if TermuxActivity is started again after exiting it with
// double back press. Check TerminalView.setTerminalCursorBlinkerState().
setTerminalCursorBlinkerState(true);
mTerminalCursorBlinkerStateAlreadySet = true;
}
}
@Override
public float onScale(float scale) {
if (scale < 0.9f || scale > 1.1f) {
@@ -72,8 +152,10 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
@Override
public void onSingleTapUp(MotionEvent e) {
InputMethodManager mgr = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
mgr.showSoftInput(mActivity.getTerminalView(), InputMethodManager.SHOW_IMPLICIT);
if (!KeyboardUtils.areDisableSoftKeyboardFlagsSet(mActivity))
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
else
Logger.logVerbose(LOG_TAG, "Not showing soft keyboard onSingleTapUp since its disabled");
}
@Override
@@ -122,8 +204,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
mActivity.getDrawer().closeDrawers();
} else if (unicodeChar == 'k'/* keyboard */) {
InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
onToggleSoftKeyboardRequest();
} else if (unicodeChar == 'm'/* menu */) {
mActivity.getTerminalView().showContextMenu();
} else if (unicodeChar == 'r'/* rename */) {
@@ -151,6 +232,8 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent e) {
return handleVirtualKeys(keyCode, e, false);
@@ -338,6 +421,144 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
/**
* Called when user requests the soft keyboard to be toggled via "KEYBOARD" toggle button in
* drawer or extra keys, or with ctrl+alt+k hardware keyboard shortcut.
*/
public void onToggleSoftKeyboardRequest() {
// If soft keyboard toggle behaviour is enable/disabled
if (mActivity.getProperties().shouldEnableDisableSoftKeyboardOnToggle()) {
// If soft keyboard is visible
if (!KeyboardUtils.areDisableSoftKeyboardFlagsSet(mActivity)) {
Logger.logVerbose(LOG_TAG, "Disabling soft keyboard on toggle");
mActivity.getPreferences().setSoftKeyboardEnabled(false);
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
} else {
// Show with a delay, otherwise pressing keyboard toggle won't show the keyboard after
// switching back from another app if keyboard was previously disabled by user.
// Also request focus, since it wouldn't have been requested at startup by
// setSoftKeyboardState if keyboard was disabled. #2112
Logger.logVerbose(LOG_TAG, "Enabling soft keyboard on toggle");
mActivity.getPreferences().setSoftKeyboardEnabled(true);
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
if(mShowSoftKeyboardWithDelayOnce) {
mShowSoftKeyboardWithDelayOnce = false;
mActivity.getTerminalView().postDelayed(getShowSoftKeyboardRunnable(), 500);
mActivity.getTerminalView().requestFocus();
} else
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
}
}
// If soft keyboard toggle behaviour is show/hide
else {
// If soft keyboard is disabled by user for Termux
if (!mActivity.getPreferences().isSoftKeyboardEnabled()) {
Logger.logVerbose(LOG_TAG, "Maintaining disabled soft keyboard on toggle");
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
} else {
Logger.logVerbose(LOG_TAG, "Showing/Hiding soft keyboard on toggle");
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
KeyboardUtils.toggleSoftKeyboard(mActivity);
}
}
}
public void setSoftKeyboardState(boolean isStartup, boolean isReloadTermuxProperties) {
boolean noRequestFocus = false;
// If soft keyboard is disabled by user for Termux (check function docs for Termux behaviour info)
if (KeyboardUtils.shouldSoftKeyboardBeDisabled(mActivity,
mActivity.getPreferences().isSoftKeyboardEnabled(),
mActivity.getPreferences().isSoftKeyboardEnabledOnlyIfNoHardware())) {
Logger.logVerbose(LOG_TAG, "Maintaining disabled soft keyboard");
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
noRequestFocus = true;
// Delay is only required if onCreate() is called like when Termux app is exited with
// double back press, not when Termux app is switched back from another app and keyboard
// toggle is pressed to enable keyboard
if (isStartup && mActivity.isOnResumeAfterOnCreate())
mShowSoftKeyboardWithDelayOnce = true;
} else {
// Set flag to automatically push up TerminalView when keyboard is opened instead of showing over it
KeyboardUtils.setSoftInputModeAdjustResize(mActivity);
// Clear any previous flags to disable soft keyboard in case setting updated
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
// If soft keyboard is to be hidden on startup
if (isStartup && mActivity.getProperties().shouldSoftKeyboardBeHiddenOnStartup()) {
Logger.logVerbose(LOG_TAG, "Hiding soft keyboard on startup");
KeyboardUtils.hideSoftKeyboard(mActivity, mActivity.getTerminalView());
// Required to keep keyboard hidden when Termux app is switched back from another app
KeyboardUtils.setSoftKeyboardAlwaysHiddenFlags(mActivity);
noRequestFocus = true;
// Required to keep keyboard hidden on app startup
mShowSoftKeyboardIgnoreOnce = true;
}
}
mActivity.getTerminalView().setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View view, boolean hasFocus) {
// Force show soft keyboard if TerminalView or toolbar text input view has
// focus and close it if they don't
boolean textInputViewHasFocus = false;
final EditText textInputView = mActivity.findViewById(R.id.terminal_toolbar_text_input);
if (textInputView != null) textInputViewHasFocus = textInputView.hasFocus();
if (hasFocus || textInputViewHasFocus) {
if (mShowSoftKeyboardIgnoreOnce) {
mShowSoftKeyboardIgnoreOnce = false; return;
}
Logger.logVerbose(LOG_TAG, "Showing soft keyboard on focus change");
} else {
Logger.logVerbose(LOG_TAG, "Hiding soft keyboard on focus change");
}
KeyboardUtils.setSoftKeyboardVisibility(getShowSoftKeyboardRunnable(), mActivity, mActivity.getTerminalView(), hasFocus || textInputViewHasFocus);
}
});
// Do not force show soft keyboard if termux-reload-settings command was run with hardware keyboard
// or soft keyboard is to be hidden or is disabled
if (!isReloadTermuxProperties && !noRequestFocus) {
// Request focus for TerminalView
// Also show the keyboard, since onFocusChange will not be called if TerminalView already
// had focus on startup to show the keyboard, like when opening url with context menu
// "Select URL" long press and returning to Termux app with back button. This
// will also show keyboard even if it was closed before opening url. #2111
Logger.logVerbose(LOG_TAG, "Requesting TerminalView focus and showing soft keyboard");
mActivity.getTerminalView().requestFocus();
mActivity.getTerminalView().postDelayed(getShowSoftKeyboardRunnable(), 300);
}
}
private Runnable getShowSoftKeyboardRunnable() {
if (mShowSoftKeyboardRunnable == null) {
mShowSoftKeyboardRunnable = () -> {
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
};
}
return mShowSoftKeyboardRunnable;
}
public void setTerminalCursorBlinkerState(boolean start) {
if (start) {
// If set/update the cursor blinking rate is successful, then enable cursor blinker
if (mActivity.getTerminalView().setTerminalCursorBlinkerRate(mActivity.getProperties().getTerminalCursorBlinkRate()))
mActivity.getTerminalView().setTerminalCursorBlinkerState(true, true);
else
Logger.logError(LOG_TAG,"Failed to start cursor blinker");
} else {
// Disable cursor blinker
mActivity.getTerminalView().setTerminalCursorBlinkerState(false, true);
}
}
public void shareSessionTranscript() {
TerminalSession session = mActivity.getCurrentSession();
if (session == null) return;
@@ -354,7 +575,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
intent.putExtra(Intent.EXTRA_SUBJECT, mActivity.getString(R.string.title_share_transcript));
mActivity.startActivity(Intent.createChooser(intent, mActivity.getString(R.string.title_share_transcript_with)));
} catch (Exception e) {
Logger.logStackTraceWithMessage("Failed to get share session transcript of length " + transcriptText.length(), e);
Logger.logStackTraceWithMessage(LOG_TAG,"Failed to get share session transcript of length " + transcriptText.length(), e);
}
}
@@ -405,26 +626,34 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
TerminalSession session = mActivity.getCurrentSession();
if (session == null) return;
String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
final String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
if (transcriptText == null) return;
transcriptText = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
Logger.showToast(mActivity, mActivity.getString(R.string.msg_generating_report), true);
StringBuilder reportString = new StringBuilder();
new Thread() {
@Override
public void run() {
String title = TermuxConstants.TERMUX_APP_NAME + " Report Issue";
String transcriptTextTruncated = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
reportString.append("## Transcript\n");
reportString.append("\n").append(MarkdownUtils.getMarkdownCodeForString(transcriptText, true));
StringBuilder reportString = new StringBuilder();
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, true));
reportString.append("\n\n").append(TermuxUtils.getDeviceInfoMarkdownString(mActivity));
String title = TermuxConstants.TERMUX_APP_NAME + " Report Issue";
String termuxAptInfo = TermuxUtils.geAPTInfoMarkdownString(mActivity);
if (termuxAptInfo != null)
reportString.append("\n\n").append(termuxAptInfo);
reportString.append("## Transcript\n");
reportString.append("\n").append(MarkdownUtils.getMarkdownCodeForString(transcriptTextTruncated, true));
ReportActivity.startReportActivity(mActivity, new ReportInfo(UserAction.REPORT_ISSUE_FROM_TRANSCRIPT, TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY_NAME, title, null, reportString.toString(), "\n\n" + TermuxUtils.getReportIssueMarkdownString(mActivity), false));
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, true));
reportString.append("\n\n").append(TermuxUtils.getDeviceInfoMarkdownString(mActivity));
String termuxAptInfo = TermuxUtils.geAPTInfoMarkdownString(mActivity);
if (termuxAptInfo != null)
reportString.append("\n\n").append(termuxAptInfo);
ReportActivity.startReportActivity(mActivity, new ReportInfo(UserAction.REPORT_ISSUE_FROM_TRANSCRIPT, TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY_NAME, title, null, reportString.toString(), "\n\n" + TermuxUtils.getReportIssueMarkdownString(mActivity), false));
}
}.start();
}
public void doPaste() {

View File

@@ -44,6 +44,7 @@ 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());
mActivity.setExtraKeysView(extraKeysView);
extraKeysView.reload(mActivity.getProperties().getExtraKeysInfo());

View File

@@ -1,5 +1,9 @@
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;
@@ -238,6 +242,8 @@ public class ExtraKeysInfo {
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;
}
}

View File

@@ -23,12 +23,12 @@ import android.view.HapticFeedbackConstants;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
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;
@@ -44,6 +44,8 @@ public final class ExtraKeysView extends GridLayout {
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);
}
@@ -82,8 +84,8 @@ public final class ExtraKeysView extends GridLayout {
private void sendKey(View view, String keyName, boolean forceCtrlDown, boolean forceLeftAltDown) {
TerminalView terminalView = view.findViewById(R.id.terminal_view);
if ("KEYBOARD".equals(keyName)) {
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(0, 0);
if(mTermuxTerminalViewClient != null)
mTermuxTerminalViewClient.onToggleSoftKeyboardRequest();
} else if ("DRAWER".equals(keyName)) {
DrawerLayout drawer = view.findViewById(R.id.drawer_layout);
drawer.openDrawer(Gravity.LEFT);
@@ -379,4 +381,8 @@ public final class ExtraKeysView extends GridLayout {
}
}
public void setTermuxTerminalViewClient(TermuxTerminalViewClient termuxTerminalViewClient) {
this.mTermuxTerminalViewClient = termuxTerminalViewClient;
}
}

View File

@@ -47,9 +47,11 @@ public class CrashUtils {
if (context == null) return;
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(context);
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
if (preferences == null) return;
// If user has disabled notifications for crashes
if (!preferences.getCrashReportNotificationsEnabled())
if (!preferences.areCrashReportNotificationsEnabled())
return;
new Thread() {

View File

@@ -139,9 +139,11 @@ public class PluginUtils {
}
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(context);
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
if (preferences == null) return;
// If user has disabled notifications for plugin, then just return
if (!preferences.getPluginErrorNotificationsEnabled() && !forceNotification)
if (!preferences.arePluginErrorNotificationsEnabled() && !forceNotification)
return;
// Flash the errmsg
@@ -320,7 +322,7 @@ public class PluginUtils {
*/
public static String checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(final Context context) {
String errmsg = null;
if (!SharedProperties.isPropertyValueTrue(context, TermuxPropertyConstants.getTermuxPropertiesFile(), TermuxConstants.PROP_ALLOW_EXTERNAL_APPS)) {
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);
}

View File

@@ -1,80 +1,96 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<com.termux.app.terminal.TermuxActivityRootView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_termux_root_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:fitsSystemWindows="true">
<androidx.drawerlayout.widget.DrawerLayout
android:id="@+id/drawer_layout"
<RelativeLayout
android:layout_width="match_parent"
android:layout_alignParentTop="true"
android:layout_above="@+id/terminal_toolbar_view_pager"
android:layout_height="match_parent">
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical">
<com.termux.view.TerminalView
android:id="@+id/terminal_view"
<androidx.drawerlayout.widget.DrawerLayout
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginRight="3dp"
android:layout_marginLeft="3dp"
android:focusableInTouchMode="true"
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
android:scrollbars="vertical"
android:importantForAutofill="no"
android:autofillHints="password" />
android:layout_alignParentTop="true"
android:layout_above="@+id/terminal_toolbar_view_pager"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/left_drawer"
android:layout_width="240dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="@android:color/white"
android:choiceMode="singleChoice"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"
android:descendantFocusability="blocksDescendants"
android:orientation="vertical">
<ListView
android:id="@+id/terminal_sessions_list"
<com.termux.view.TerminalView
android:id="@+id/terminal_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_gravity="top"
android:layout_weight="1"
android:choiceMode="singleChoice"
android:longClickable="true" />
android:layout_height="match_parent"
android:layout_marginRight="3dp"
android:layout_marginLeft="3dp"
android:focusableInTouchMode="true"
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
android:scrollbars="vertical"
android:importantForAutofill="no"
android:autofillHints="password" />
<LinearLayout
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
android:id="@+id/left_drawer"
android:layout_width="240dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="@android:color/white"
android:choiceMode="singleChoice"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"
android:descendantFocusability="blocksDescendants"
android:orientation="vertical">
<Button
android:id="@+id/toggle_keyboard_button"
style="?android:attr/buttonBarButtonStyle"
<ListView
android:id="@+id/terminal_sessions_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_gravity="top"
android:layout_weight="1"
android:choiceMode="singleChoice"
android:longClickable="true" />
<LinearLayout
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/action_toggle_soft_keyboard" />
android:orientation="horizontal">
<Button
android:id="@+id/new_session_button"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/action_new_session" />
<Button
android:id="@+id/toggle_keyboard_button"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/action_toggle_soft_keyboard" />
<Button
android:id="@+id/new_session_button"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/action_new_session" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.drawerlayout.widget.DrawerLayout>
</androidx.drawerlayout.widget.DrawerLayout>
<androidx.viewpager.widget.ViewPager
android:id="@+id/terminal_toolbar_view_pager"
android:visibility="gone"
<androidx.viewpager.widget.ViewPager
android:id="@+id/terminal_toolbar_view_pager"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="37.5dp"
android:background="@android:drawable/screen_background_dark_transparent"
android:layout_alignParentBottom="true" />
</RelativeLayout>
<View
android:id="@+id/activity_termux_bottom_space_view"
android:layout_width="match_parent"
android:layout_height="37.5dp"
android:background="@android:drawable/screen_background_dark_transparent"
android:layout_alignParentBottom="true" />
</RelativeLayout>
android:layout_height="1dp"
android:background="@android:color/transparent" />
</com.termux.app.terminal.TermuxActivityRootView>

View File

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

View File

@@ -66,7 +66,7 @@
<string name="action_autofill_password">Autofill password</string>
<string name="action_reset_terminal">Reset</string>
<string name="msg_terminal_reset">Terminal reset.</string>
<string name="msg_terminal_reset">Terminal reset</string>
<string name="action_kill_process">Kill process (%d)</string>
<string name="title_confirm_kill_process">Really kill this session?</string>
@@ -75,7 +75,9 @@
<string name="action_toggle_keep_screen_on">Keep screen on</string>
<string name="action_open_help">Help</string>
<string name="action_open_settings">Settings</string>
<string name="action_report_issue">Report Issue</string>
<string name="msg_generating_report">Generating Report</string>
<string name="error_styling_not_installed">The &TERMUX_STYLING_APP_NAME; Plugin App is not installed.</string>
<string name="action_styling_install">Install</string>
@@ -122,37 +124,63 @@
<!-- Termux Settings -->
<string name="title_activity_termux_settings">&TERMUX_APP_NAME; Settings</string>
<!-- Debugging Preferences -->
<string name="debugging_preferences">Debugging</string>
<!-- Termux App Preferences -->
<string name="termux_preferences_title">&TERMUX_APP_NAME;</string>
<string name="termux_preferences_summary">Preferences for &TERMUX_APP_NAME; app</string>
<!-- Logging Category -->
<string name="logging_header">Logging</string>
<!-- Debugging Preferences -->
<string name="termux_debugging_preferences_title">Debugging</string>
<string name="termux_debugging_preferences_summary">Preferences for debugging</string>
<!-- Terminal View Key Logging -->
<string name="terminal_view_key_logging_title">Terminal View Key Logging</string>
<string name="terminal_view_key_logging_off">Logs will not have entries for terminal view keys. (Default)</string>
<string name="terminal_view_key_logging_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>
<!-- Logging Category -->
<string name="termux_logging_header">Logging</string>
<!-- Plugin Error Notifications -->
<string name="plugin_error_notifications_title">Plugin Error Notifications</string>
<string name="plugin_error_notifications_off">Disable flashes and notifications for plugin errors.</string>
<string name="plugin_error_notifications_on">Show flashes and notifications for plugin errors. (Default)</string>
<!-- Log Level -->
<string name="termux_log_level_title">Log Level</string>
<!-- Crash Report Notifications -->
<string name="crash_report_notifications_title">Crash Report Notifications</string>
<string name="crash_report_notifications_off">Disable notifications for crash reports.</string>
<string name="crash_report_notifications_on">Show notifications for crash reports. (Default)</string>
<!-- Terminal View Key Logging -->
<string name="termux_terminal_view_key_logging_enabled_title">Terminal View Key Logging</string>
<string name="termux_terminal_view_key_logging_enabled_off">Logs will not have entries for terminal view keys. (Default)</string>
<string name="termux_terminal_view_key_logging_enabled_on">Logcat logs will have entries for terminal view keys. These are very verbose and should be disabled under normal circumstances or will cause performance issues.</string>
<!-- Plugin Error Notifications -->
<string name="termux_plugin_error_notifications_enabled_title">Plugin Error Notifications</string>
<string name="termux_plugin_error_notifications_enabled_off">Disable flashes and notifications for plugin errors.</string>
<string name="termux_plugin_error_notifications_enabled_on">Show flashes and notifications for plugin errors. (Default)</string>
<!-- Crash Report Notifications -->
<string name="termux_crash_report_notifications_enabled_title">Crash Report Notifications</string>
<string name="termux_crash_report_notifications_enabled_off">Disable notifications for crash reports.</string>
<string name="termux_crash_report_notifications_enabled_on">Show notifications for crash reports. (Default)</string>
<!-- Terminal IO Preferences -->
<string name="terminal_io_preferences">Terminal I/O</string>
<!-- Terminal IO Preferences -->
<string name="termux_terminal_io_preferences_title">Terminal I/O</string>
<string name="termux_terminal_io_preferences_summary">Preferences for terminal I/O</string>
<!-- Keyboard Category -->
<string name="keyboard_header">Keyboard</string>
<!-- Keyboard Category -->
<string name="termux_keyboard_header">Keyboard</string>
<!-- Soft Keyboard -->
<string name="soft_keyboard_title">Soft Keyboard</string>
<string name="soft_keyboard_off">Soft keyboard will be disabled.</string>
<string name="soft_keyboard_on">Soft keyboard will be enabled. (Default)</string>
<!-- Soft Keyboard -->
<string name="termux_soft_keyboard_enabled_title">Soft Keyboard Enabled</string>
<string name="termux_soft_keyboard_enabled_off">Soft keyboard will be disabled.</string>
<string name="termux_soft_keyboard_enabled_on">Soft keyboard will be enabled. (Default)</string>
<!-- Soft Keyboard Only If No Hardware-->
<string name="termux_soft_keyboard_enabled_only_if_no_hardware_title">Soft Keyboard Only If No Hardware</string>
<string name="termux_soft_keyboard_enabled_only_if_no_hardware_off">Soft keyboard will be enabled even if hardware keyboard is connected. (Default)</string>
<string name="termux_soft_keyboard_enabled_only_if_no_hardware_on">Soft keyboard will be enabled only if no hardware keyboard is connected.</string>
<!-- Termux Tasker App Preferences -->
<string name="termux_tasker_preferences_title">&TERMUX_TASKER_APP_NAME;</string>
<string name="termux_tasker_preferences_summary">Preferences for &TERMUX_TASKER_APP_NAME; app</string>
<!-- About Preference -->
<string name="about_preference_title">About</string>
<!-- Donate Preference -->
<string name="donate_preference_title">Donate</string>
</resources>

View File

@@ -1,33 +0,0 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:key="logging"
app:title="@string/logging_header">
<ListPreference
app:defaultValue="1"
app:key="log_level"
app:title="@string/log_level_title"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
app:key="terminal_view_key_logging_enabled"
app:summaryOff="@string/terminal_view_key_logging_off"
app:summaryOn="@string/terminal_view_key_logging_on"
app:title="@string/terminal_view_key_logging_title" />
<SwitchPreferenceCompat
app:key="plugin_error_notifications_enabled"
app:summaryOff="@string/plugin_error_notifications_off"
app:summaryOn="@string/plugin_error_notifications_on"
app:title="@string/plugin_error_notifications_title" />
<SwitchPreferenceCompat
app:key="crash_report_notifications_enabled"
app:summaryOff="@string/crash_report_notifications_off"
app:summaryOn="@string/crash_report_notifications_on"
app:title="@string/crash_report_notifications_title" />
</PreferenceCategory>
</PreferenceScreen>

View File

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

View File

@@ -1,15 +0,0 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:key="keyboard"
app:title="@string/keyboard_header">
<SwitchPreferenceCompat
app:key="soft_keyboard_enabled"
app:summaryOff="@string/soft_keyboard_off"
app:summaryOn="@string/soft_keyboard_on"
app:title="@string/soft_keyboard_title" />
</PreferenceCategory>
</PreferenceScreen>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
buildscript {
repositories {
jcenter()
mavenCentral()
google()
}
dependencies {
@@ -11,7 +11,7 @@ buildscript {
allprojects {
repositories {
google()
jcenter()
mavenCentral()
}
}

View File

@@ -20,8 +20,8 @@ termuxVersionCode=111
minSdkVersion=24
targetSdkVersion=28
ndkVersion=22.0.7026061
compileSdkVersion=29
ndkVersion=22.1.7171670
compileSdkVersion=30
markwonVersion=4.6.2

178
gradlew.bat vendored
View File

@@ -1,89 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -63,7 +63,7 @@ publishing {
bar(MavenPublication) {
groupId 'com.termux'
artifactId 'terminal-emulator'
version project.properties.termuxVersion
version "0.114"
artifact(sourceJar)
artifact("$buildDir/outputs/aar/terminal-emulator-release.aar")
}

View File

@@ -38,10 +38,6 @@ public final class TerminalEmulator {
public static final int MOUSE_WHEELUP_BUTTON = 64;
public static final int MOUSE_WHEELDOWN_BUTTON = 65;
public static final int CURSOR_STYLE_BLOCK = 0;
public static final int CURSOR_STYLE_UNDERLINE = 1;
public static final int CURSOR_STYLE_BAR = 2;
/** Used for invalid data - http://en.wikipedia.org/wiki/Replacement_character#Replacement_character */
public static final int UNICODE_REPLACEMENT_CHAR = 0xFFFD;
@@ -108,8 +104,8 @@ public final class TerminalEmulator {
* characters received when the cursor is at the right border of the page replace characters already on the page."
*/
private static final int DECSET_BIT_AUTOWRAP = 1 << 3;
/** DECSET 25 - if the cursor should be visible, {@link #isShowingCursor()}. */
private static final int DECSET_BIT_SHOWING_CURSOR = 1 << 4;
/** DECSET 25 - if the cursor should be enabled, {@link #isCursorEnabled()}. */
private static final int DECSET_BIT_CURSOR_ENABLED = 1 << 4;
private static final int DECSET_BIT_APPLICATION_KEYPAD = 1 << 5;
/** DECSET 1000 - if to report mouse press&release events. */
private static final int DECSET_BIT_MOUSE_TRACKING_PRESS_RELEASE = 1 << 6;
@@ -126,17 +122,35 @@ public final class TerminalEmulator {
/** Not really DECSET bit... - http://www.vt100.net/docs/vt510-rm/DECSACE */
private static final int DECSET_BIT_RECTANGULAR_CHANGEATTRIBUTE = 1 << 12;
private String mTitle;
private final Stack<String> mTitleStack = new Stack<>();
/** The cursor position. Between (0,0) and (mRows-1, mColumns-1). */
private int mCursorRow, mCursorCol;
private int mCursorStyle = CURSOR_STYLE_BLOCK;
/** The number of character rows and columns in the terminal screen. */
public int mRows, mColumns;
/** The number of terminal transcript rows that can be scrolled back to. */
public static final int TERMINAL_TRANSCRIPT_ROWS_MIN = 100;
public static final int TERMINAL_TRANSCRIPT_ROWS_MAX = 50000;
public static final int DEFAULT_TERMINAL_TRANSCRIPT_ROWS = 2000;
/* The supported terminal cursor styles. */
public static final int TERMINAL_CURSOR_STYLE_BLOCK = 0;
public static final int TERMINAL_CURSOR_STYLE_UNDERLINE = 1;
public static final int TERMINAL_CURSOR_STYLE_BAR = 2;
public static final int DEFAULT_TERMINAL_CURSOR_STYLE = TERMINAL_CURSOR_STYLE_BLOCK;
public static final Integer[] TERMINAL_CURSOR_STYLES_LIST = new Integer[]{TERMINAL_CURSOR_STYLE_BLOCK, TERMINAL_CURSOR_STYLE_UNDERLINE, TERMINAL_CURSOR_STYLE_BAR};
/** The terminal cursor styles. */
private int mCursorStyle = DEFAULT_TERMINAL_CURSOR_STYLE;
/** The normal screen buffer. Stores the characters that appear on the screen of the emulated terminal. */
private final TerminalBuffer mMainBuffer;
/**
@@ -205,6 +219,18 @@ public final class TerminalEmulator {
*/
private boolean mAboutToAutoWrap;
/**
* If the cursor blinking is enabled. It requires cursor itself to be enabled, which is controlled
* byt whether {@link #DECSET_BIT_CURSOR_ENABLED} bit is set or not.
*/
private boolean mCursorBlinkingEnabled;
/**
* If currently cursor should be in a visible state or not if {@link #mCursorBlinkingEnabled}
* is {@code true}.
*/
private boolean mCursorBlinkState;
/**
* Current foreground and background colors. Can either be a color index in [0,259] or a truecolor (24-bit) value.
* For a 24-bit value the top byte (0xff000000) is set.
@@ -261,7 +287,7 @@ public final class TerminalEmulator {
case 7:
return DECSET_BIT_AUTOWRAP;
case 25:
return DECSET_BIT_SHOWING_CURSOR;
return DECSET_BIT_CURSOR_ENABLED;
case 66:
return DECSET_BIT_APPLICATION_KEYPAD;
case 69:
@@ -282,9 +308,9 @@ public final class TerminalEmulator {
}
}
public TerminalEmulator(TerminalOutput session, int columns, int rows, int transcriptRows, TerminalSessionClient client) {
public TerminalEmulator(TerminalOutput session, int columns, int rows, Integer transcriptRows, TerminalSessionClient client) {
mSession = session;
mScreen = mMainBuffer = new TerminalBuffer(columns, transcriptRows, rows);
mScreen = mMainBuffer = new TerminalBuffer(columns, getTerminalTranscriptRows(transcriptRows), rows);
mAltBuffer = new TerminalBuffer(columns, rows, rows);
mClient = client;
mRows = rows;
@@ -295,6 +321,8 @@ public final class TerminalEmulator {
public void updateTerminalSessionClient(TerminalSessionClient client) {
mClient = client;
setCursorStyle();
setCursorBlinkState(true);
}
public TerminalBuffer getScreen() {
@@ -305,6 +333,13 @@ public final class TerminalEmulator {
return mScreen == mAltBuffer;
}
private int getTerminalTranscriptRows(Integer transcriptRows) {
if (transcriptRows == null || transcriptRows < TERMINAL_TRANSCRIPT_ROWS_MIN || transcriptRows > TERMINAL_TRANSCRIPT_ROWS_MAX)
return DEFAULT_TERMINAL_TRANSCRIPT_ROWS;
else
return transcriptRows;
}
/**
* @param mouseButton one of the MOUSE_* constants of this class.
*/
@@ -372,18 +407,49 @@ public final class TerminalEmulator {
return mCursorCol;
}
/** {@link #CURSOR_STYLE_BAR}, {@link #CURSOR_STYLE_BLOCK} or {@link #CURSOR_STYLE_UNDERLINE} */
/** Get the terminal cursor style. It will be one of {@link #TERMINAL_CURSOR_STYLES_LIST} */
public int getCursorStyle() {
return mCursorStyle;
}
/** Set the terminal cursor style. */
public void setCursorStyle() {
Integer cursorStyle = null;
if (mClient != null)
cursorStyle = mClient.getTerminalCursorStyle();
if (cursorStyle == null || !Arrays.asList(TERMINAL_CURSOR_STYLES_LIST).contains(cursorStyle))
mCursorStyle = DEFAULT_TERMINAL_CURSOR_STYLE;
else
mCursorStyle = cursorStyle;
}
public boolean isReverseVideo() {
return isDecsetInternalBitSet(DECSET_BIT_REVERSE_VIDEO);
}
public boolean isShowingCursor() {
return isDecsetInternalBitSet(DECSET_BIT_SHOWING_CURSOR);
public boolean isCursorEnabled() {
return isDecsetInternalBitSet(DECSET_BIT_CURSOR_ENABLED);
}
public boolean shouldCursorBeVisible() {
if (!isCursorEnabled())
return false;
else
return mCursorBlinkingEnabled ? mCursorBlinkState : true;
}
public void setCursorBlinkingEnabled(boolean cursorBlinkingEnabled) {
this.mCursorBlinkingEnabled = cursorBlinkingEnabled;
}
public void setCursorBlinkState(boolean cursorBlinkState) {
this.mCursorBlinkState = cursorBlinkState;
}
public boolean isKeypadApplicationMode() {
return isDecsetInternalBitSet(DECSET_BIT_APPLICATION_KEYPAD);
@@ -776,15 +842,15 @@ public final class TerminalEmulator {
case 0: // Blinking block.
case 1: // Blinking block.
case 2: // Steady block.
mCursorStyle = CURSOR_STYLE_BLOCK;
mCursorStyle = TERMINAL_CURSOR_STYLE_BLOCK;
break;
case 3: // Blinking underline.
case 4: // Steady underline.
mCursorStyle = CURSOR_STYLE_UNDERLINE;
mCursorStyle = TERMINAL_CURSOR_STYLE_UNDERLINE;
break;
case 5: // Blinking bar (xterm addition).
case 6: // Steady bar (xterm addition).
mCursorStyle = CURSOR_STYLE_BAR;
mCursorStyle = TERMINAL_CURSOR_STYLE_BAR;
break;
}
break;
@@ -1054,7 +1120,10 @@ public final class TerminalEmulator {
case 8: // Auto-repeat Keys (DECARM). Do not implement.
case 9: // X10 mouse reporting - outdated. Do not implement.
case 12: // Control cursor blinking - ignore.
case 25: // Hide/show cursor - no action needed, renderer will check with isShowingCursor().
case 25: // Hide/show cursor - no action needed, renderer will check with shouldCursorBeVisible().
if (mClient != null)
mClient.onTerminalCursorStateChange(setting);
break;
case 40: // Allow 80 => 132 Mode, ignore.
case 45: // TODO: Reverse wrap-around. Implement???
case 66: // Application keypad (DECNKM).
@@ -2297,7 +2366,7 @@ public final class TerminalEmulator {
/** Reset terminal state so user can interact with it regardless of present state. */
public void reset() {
mCursorStyle = CURSOR_STYLE_BLOCK;
setCursorStyle();
mArgIndex = 0;
mContinueSequence = false;
mEscapeState = ESC_NONE;
@@ -2318,7 +2387,7 @@ public final class TerminalEmulator {
mCurrentDecSetFlags = 0;
// Initial wrap-around is not accurate but makes terminal more useful, especially on a small screen:
setDecsetinternalBit(DECSET_BIT_AUTOWRAP, true);
setDecsetinternalBit(DECSET_BIT_SHOWING_CURSOR, true);
setDecsetinternalBit(DECSET_BIT_CURSOR_ENABLED, true);
mSavedDecSetFlags = mSavedStateMain.mSavedDecFlags = mSavedStateAlt.mSavedDecFlags = mCurrentDecSetFlags;
// XXX: Should we set terminal driver back to IUTF8 with termios?

View File

@@ -74,14 +74,17 @@ public final class TerminalSession extends TerminalOutput {
private final String mCwd;
private final String[] mArgs;
private final String[] mEnv;
private final Integer mTranscriptRows;
private static final String LOG_TAG = "TerminalSession";
public TerminalSession(String shellPath, String cwd, String[] args, String[] env, TerminalSessionClient client) {
public TerminalSession(String shellPath, String cwd, String[] args, String[] env, Integer transcriptRows, TerminalSessionClient client) {
this.mShellPath = shellPath;
this.mCwd = cwd;
this.mArgs = args;
this.mEnv = env;
this.mTranscriptRows = transcriptRows;
this.mClient = client;
}
@@ -118,7 +121,7 @@ public final class TerminalSession extends TerminalOutput {
* @param rows The number of rows in the terminal window.
*/
public void initializeEmulator(int columns, int rows) {
mEmulator = new TerminalEmulator(this, columns, rows, /* transcript= */2000, mClient);
mEmulator = new TerminalEmulator(this, columns, rows, mTranscriptRows, mClient);
int[] processId = new int[1];
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns);

View File

@@ -19,6 +19,13 @@ public interface TerminalSessionClient {
void onColorsChanged(TerminalSession session);
void onTerminalCursorStateChange(boolean state);
Integer getTerminalCursorStyle();
void logError(String tag, String message);

View File

@@ -16,23 +16,23 @@ package com.termux.terminal;
public class DecSetTest extends TerminalTestCase {
/** DECSET 25, DECTCEM, controls visibility of the cursor. */
public void testShowHideCursor() {
public void testEnableDisableCursor() {
withTerminalSized(3, 3);
assertTrue("Initially the cursor should be visible", mTerminal.isShowingCursor());
enterString("\033[?25l"); // Hide Cursor (DECTCEM).
assertFalse(mTerminal.isShowingCursor());
enterString("\033[?25h"); // Show Cursor (DECTCEM).
assertTrue(mTerminal.isShowingCursor());
assertTrue("Initially the cursor should be enabled", mTerminal.isCursorEnabled());
enterString("\033[?25l"); // Disable Cursor (DECTCEM).
assertFalse(mTerminal.isCursorEnabled());
enterString("\033[?25h"); // Enable Cursor (DECTCEM).
assertTrue(mTerminal.isCursorEnabled());
enterString("\033[?25l"); // Hide Cursor (DECTCEM), again.
assertFalse(mTerminal.isShowingCursor());
enterString("\033[?25l"); // Disable Cursor (DECTCEM), again.
assertFalse(mTerminal.isCursorEnabled());
mTerminal.reset();
assertTrue("Resetting the terminal should show the cursor", mTerminal.isShowingCursor());
assertTrue("Resetting the terminal should enable the cursor", mTerminal.isCursorEnabled());
enterString("\033[?25l");
assertFalse(mTerminal.isShowingCursor());
enterString("\033c"); // RIS resetting should reveal cursor.
assertTrue(mTerminal.isShowingCursor());
assertFalse(mTerminal.isCursorEnabled());
enterString("\033c"); // RIS resetting should enabled cursor.
assertTrue(mTerminal.isCursorEnabled());
}
/** DECSET 2004, controls bracketed paste mode. */

View File

@@ -103,23 +103,23 @@ public class TerminalTest extends TerminalTestCase {
/** Test the cursor shape changes using DECSCUSR. */
public void testSetCursorStyle() throws Exception {
withTerminalSized(5, 5);
assertEquals(TerminalEmulator.CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
enterString("\033[3 q");
assertEquals(TerminalEmulator.CURSOR_STYLE_UNDERLINE, mTerminal.getCursorStyle());
assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_UNDERLINE, mTerminal.getCursorStyle());
enterString("\033[5 q");
assertEquals(TerminalEmulator.CURSOR_STYLE_BAR, mTerminal.getCursorStyle());
assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_BAR, mTerminal.getCursorStyle());
enterString("\033[0 q");
assertEquals(TerminalEmulator.CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
enterString("\033[6 q");
assertEquals(TerminalEmulator.CURSOR_STYLE_BAR, mTerminal.getCursorStyle());
assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_BAR, mTerminal.getCursorStyle());
enterString("\033[4 q");
assertEquals(TerminalEmulator.CURSOR_STYLE_UNDERLINE, mTerminal.getCursorStyle());
assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_UNDERLINE, mTerminal.getCursorStyle());
enterString("\033[1 q");
assertEquals(TerminalEmulator.CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
enterString("\033[4 q");
assertEquals(TerminalEmulator.CURSOR_STYLE_UNDERLINE, mTerminal.getCursorStyle());
assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_UNDERLINE, mTerminal.getCursorStyle());
enterString("\033[2 q");
assertEquals(TerminalEmulator.CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
assertEquals(TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK, mTerminal.getCursorStyle());
}
public void testPaste() {

View File

@@ -42,7 +42,7 @@ publishing {
bar(MavenPublication) {
groupId 'com.termux'
artifactId 'terminal-view'
version project.properties.termuxVersion
version "0.114"
artifact(sourceJar)
artifact("$buildDir/outputs/aar/terminal-view-release.aar")
}

View File

@@ -61,7 +61,7 @@ public final class TerminalRenderer {
final int columns = mEmulator.mColumns;
final int cursorCol = mEmulator.getCursorCol();
final int cursorRow = mEmulator.getCursorRow();
final boolean cursorVisible = mEmulator.isShowingCursor();
final boolean cursorVisible = mEmulator.shouldCursorBeVisible();
final TerminalBuffer screen = mEmulator.getScreen();
final int[] palette = mEmulator.mColors.mCurrentColors;
final int cursorShape = mEmulator.getCursorStyle();
@@ -200,8 +200,8 @@ public final class TerminalRenderer {
if (cursor != 0) {
mTextPaint.setColor(cursor);
float cursorHeight = mFontLineSpacingAndAscent - mFontAscent;
if (cursorStyle == TerminalEmulator.CURSOR_STYLE_UNDERLINE) cursorHeight /= 4.;
else if (cursorStyle == TerminalEmulator.CURSOR_STYLE_BAR) right -= ((right - left) * 3) / 4.;
if (cursorStyle == TerminalEmulator.TERMINAL_CURSOR_STYLE_UNDERLINE) cursorHeight /= 4.;
else if (cursorStyle == TerminalEmulator.TERMINAL_CURSOR_STYLE_BAR) right -= ((right - left) * 3) / 4.;
canvas.drawRect(left, y - cursorHeight, right, y, mTextPaint);
}

View File

@@ -8,6 +8,8 @@ import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Typeface;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.text.Editable;
import android.text.InputType;
import android.text.TextUtils;
@@ -52,6 +54,13 @@ public final class TerminalView extends View {
private TextSelectionCursorController mTextSelectionCursorController;
private Handler mTerminalCursorBlinkerHandler;
private TerminalCursorBlinkerRunnable mTerminalCursorBlinkerRunnable;
private int mTerminalCursorBlinkerRate;
private boolean mCursorInvisibleIgnoreOnce;
public static final int TERMINAL_CURSOR_BLINK_RATE_MIN = 100;
public static final int TERMINAL_CURSOR_BLINK_RATE_MAX = 2000;
/** The top row of text to display. Ranges from -activeTranscriptRows to 0. */
int mTopRow;
int[] mDefaultSelectors = new int[]{-1,-1,-1,-1};
@@ -209,6 +218,8 @@ public final class TerminalView extends View {
mAccessibilityEnabled = am.isEnabled();
}
/**
* @param client The {@link TerminalViewClient} interface implementation to allow
* for communication between {@link TerminalView} and its client.
@@ -218,7 +229,7 @@ public final class TerminalView extends View {
}
/**
* Sets terminal view key logging is enabled or not.
* Sets whether terminal view key logging is enabled or not.
*
* @param value The boolean value that defines the state.
*/
@@ -226,6 +237,8 @@ public final class TerminalView extends View {
TERMINAL_VIEW_KEY_LOGGING_ENABLED = value;
}
/**
* Attach a {@link TerminalSession} to this view.
*
@@ -685,6 +698,10 @@ public final class TerminalView extends View {
/** Input the specified keyCode if applicable and return if the input was consumed. */
public boolean handleKeyCode(int keyCode, int keyMod) {
// Ensure cursor is shown when a key is pressed down like long hold on (arrow) keys
if (mEmulator != null)
mEmulator.setCursorBlinkState(true);
TerminalEmulator term = mTermSession.getEmulator();
String code = KeyHandler.getCode(keyCode, keyMod, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode());
if (code == null) return false;
@@ -738,6 +755,7 @@ public final class TerminalView extends View {
if (mEmulator == null || (newColumns != mEmulator.mColumns || newRows != mEmulator.mRows)) {
mTermSession.updateSize(newColumns, newRows);
mEmulator = mTermSession.getEmulator();
mClient.onEmulatorSet();
mTopRow = 0;
scrollTo(0, 0);
@@ -755,6 +773,7 @@ public final class TerminalView extends View {
if (mTextSelectionCursorController != null) {
mTextSelectionCursorController.getSelectors(sel);
}
mRenderer.render(mEmulator, canvas, mTopRow, sel[0], sel[1], sel[2], sel[3]);
// render the text selection handles
@@ -799,7 +818,6 @@ public final class TerminalView extends View {
/**
* Define functions required for AutoFill API
*/
@@ -825,6 +843,154 @@ public final class TerminalView extends View {
/**
* Set terminal cursor blinker rate. It must be between {@link #TERMINAL_CURSOR_BLINK_RATE_MIN}
* and {@link #TERMINAL_CURSOR_BLINK_RATE_MAX}, otherwise it will be disabled.
*
* The {@link #setTerminalCursorBlinkerState(boolean, boolean)} must be called after this
* for changes to take effect if not disabling.
*
* @param blinkRate The value to set.
* @return Returns {@code true} if setting blinker rate was successfully set, otherwise [@code false}.
*/
public synchronized boolean setTerminalCursorBlinkerRate(int blinkRate) {
boolean result;
// If cursor blinking rate is not valid
if (blinkRate != 0 && (blinkRate < TERMINAL_CURSOR_BLINK_RATE_MIN || blinkRate > TERMINAL_CURSOR_BLINK_RATE_MAX)) {
mClient.logError(LOG_TAG, "The cursor blink rate must be in between " + TERMINAL_CURSOR_BLINK_RATE_MIN + "-" + TERMINAL_CURSOR_BLINK_RATE_MAX + ": " + blinkRate);
mTerminalCursorBlinkerRate = 0;
result = false;
} else {
mClient.logVerbose(LOG_TAG, "Setting cursor blinker rate to " + blinkRate);
mTerminalCursorBlinkerRate = blinkRate;
result = true;
}
if (mTerminalCursorBlinkerRate == 0) {
mClient.logVerbose(LOG_TAG, "Cursor blinker disabled");
stopTerminalCursorBlinker();
}
return result;
}
/**
* Sets whether cursor blinker should be started or stopped. Cursor blinker will only be
* started if {@link #mTerminalCursorBlinkerRate} does not equal 0 and is between
* {@link #TERMINAL_CURSOR_BLINK_RATE_MIN} and {@link #TERMINAL_CURSOR_BLINK_RATE_MAX}.
*
* This should be called when the view holding this activity is resumed or stopped so that
* cursor blinker does not run when activity is not visible. Ensure that {@link #mEmulator}
* is set when you call this to start cursor blinking by waiting for {@link TerminalViewClient#onEmulatorSet()}
* event after calling {@link #attachSession(TerminalSession)} for the first session added in the
* activity, otherwise blinking will not start. Do not call this directly after
* {@link #attachSession(TerminalSession)} since {@link #updateSize()} may return without
* setting {@link #mEmulator} since width/height may be 0. Its called again in
* {@link #onSizeChanged(int, int, int, int)}.
*
* It should also be called on the
* {@link com.termux.terminal.TerminalSessionClient#onTerminalCursorStateChange(boolean)}
* callback when cursor is enabled or disabled so that blinker is disabled if cursor is not
* to be shown. It should also be checked if activity is visible if blinker is to be started
* before calling this.
*
* How cursor blinker starting works is by registering a {@link Runnable} with the looper of
* the main thread of the app which when run, toggles the cursor blinking state and re-registers
* itself to be called with the delay set by {@link #mTerminalCursorBlinkerRate}. When cursor
* blinking needs to be disabled, we just cancel any callbacks registered. We don't run our own
* "thread" and let the thread for the main looper do the work for us, whose usage is also
* required to update the UI, since it also handles other calls to update the UI as well based
* on a queue.
*
* Note that when moving cursor in text editors like nano, the cursor state is quickly
* toggled `-> off -> on`, which would call this very quickly sequentially. So that if cursor
* is moved 2 or more times quickly, like long hold on arrow keys, it would trigger
* `-> off -> on -> off -> on -> ...`, and the "on" callback at index 2 is automatically
* cancelled by next "off" callback at index 3 before getting a chance to be run. For this case
* we log only if {@link #TERMINAL_VIEW_KEY_LOGGING_ENABLED} is enabled, otherwise would clutter
* the log. We don't start the blinking with a delay to immediately show cursor in case it was
* previously not visible.
*
* @param start If cursor blinker should be started or stopped.
* @param startOnlyIfCursorEnabled If set to {@code true}, then it will also be checked if the
* cursor is even enabled by {@link TerminalEmulator} before
* starting the cursor blinker.
*/
public synchronized void setTerminalCursorBlinkerState(boolean start, boolean startOnlyIfCursorEnabled) {
// Stop any existing cursor blinker callbacks
stopTerminalCursorBlinker();
if (mEmulator == null) return;
mEmulator.setCursorBlinkingEnabled(false);
if (start) {
// If cursor blinker is not enabled or is not valid
if (mTerminalCursorBlinkerRate < TERMINAL_CURSOR_BLINK_RATE_MIN || mTerminalCursorBlinkerRate > TERMINAL_CURSOR_BLINK_RATE_MAX)
return;
// If cursor blinder is to be started only if cursor is enabled
else if (startOnlyIfCursorEnabled && ! mEmulator.isCursorEnabled()) {
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED)
mClient.logVerbose(LOG_TAG, "Ignoring call to start cursor blinker since cursor is not enabled");
return;
}
// Start cursor blinker runnable
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED)
mClient.logVerbose(LOG_TAG, "Starting cursor blinker with the blink rate " + mTerminalCursorBlinkerRate);
if (mTerminalCursorBlinkerHandler == null)
mTerminalCursorBlinkerHandler = new Handler(Looper.getMainLooper());
mTerminalCursorBlinkerRunnable = new TerminalCursorBlinkerRunnable(mEmulator, mTerminalCursorBlinkerRate);
mEmulator.setCursorBlinkingEnabled(true);
mTerminalCursorBlinkerRunnable.run();
}
}
/**
* Cancel the terminal cursor blinker callbacks
*/
private void stopTerminalCursorBlinker() {
if (mTerminalCursorBlinkerHandler != null && mTerminalCursorBlinkerRunnable != null) {
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED)
mClient.logVerbose(LOG_TAG, "Stopping cursor blinker");
mTerminalCursorBlinkerHandler.removeCallbacks(mTerminalCursorBlinkerRunnable);
}
}
private class TerminalCursorBlinkerRunnable implements Runnable {
private final TerminalEmulator mEmulator;
private final int mBlinkRate;
// Initialize with false so that initial blink state is visible after toggling
boolean mCursorVisible = false;
public TerminalCursorBlinkerRunnable(TerminalEmulator emulator, int blinkRate) {
mEmulator = emulator;
mBlinkRate = blinkRate;
}
public void run() {
try {
if (mEmulator != null) {
// Toggle the blink state and then invalidate() the view so
// that onDraw() is called, which then calls TerminalRenderer.render()
// which checks with TerminalEmulator.shouldCursorBeVisible() to decide whether
// to draw the cursor or not
mCursorVisible = !mCursorVisible;
//mClient.logVerbose(LOG_TAG, "Toggling cursor blink state to " + mCursorVisible);
mEmulator.setCursorBlinkState(mCursorVisible);
invalidate();
}
} finally {
// Recall the Runnable after mBlinkRate milliseconds to toggle the blink state
mTerminalCursorBlinkerHandler.postDelayed(this, mBlinkRate);
}
}
}
/**
* Define functions required for text selection and its handles.
@@ -920,7 +1086,6 @@ public final class TerminalView extends View {
/**
* Define functions required for long hold toolbar.
*/

View File

@@ -56,6 +56,8 @@ public interface TerminalViewClient {
boolean onCodePoint(int codePoint, boolean ctrlDown, TerminalSession session);
void onEmulatorSet();
void logError(String tag, String message);

View File

@@ -5,7 +5,9 @@ android {
compileSdkVersion project.properties.compileSdkVersion.toInteger()
dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation "androidx.annotation:annotation:1.2.0"
implementation "androidx.core:core:1.5.0-rc01"
implementation "com.google.guava:guava:24.1-jre"
implementation "io.noties.markwon:core:$markwonVersion"
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
@@ -55,7 +57,7 @@ publishing {
bar(MavenPublication) {
groupId 'com.termux'
artifactId 'termux-shared'
version project.properties.termuxVersion
version "0.114"
artifact(sourceJar)
artifact("$buildDir/outputs/aar/termux-shared-release.aar")
}

View File

@@ -10,6 +10,8 @@ public class DataUtils {
public static final int TRANSACTION_SIZE_LIMIT_IN_BYTES = 100 * 1024; // 100KB
private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
public static String getTruncatedCommandOutput(String text, int maxLength, boolean fromEnd, boolean onNewline, boolean addPrefix) {
if (text == null) return null;
@@ -43,7 +45,7 @@ public class DataUtils {
/**
* Get the {@code float} from a {@link String}.
*
* @param value The {@link String value.
* @param value The {@link String} value.
* @param def The default value if failed to read a valid value.
* @return Returns the {@code float} value after parsing the {@link String} value, otherwise
* returns default if failed to read a valid value, like in case of an exception.
@@ -62,7 +64,7 @@ public class DataUtils {
/**
* Get the {@code int} from a {@link String}.
*
* @param value The {@link String value.
* @param value The {@link String} value.
* @param def The default value if failed to read a valid value.
* @return Returns the {@code int} value after parsing the {@link String} value, otherwise
* returns default if failed to read a valid value, like in case of an exception.
@@ -78,6 +80,22 @@ public class DataUtils {
}
}
/**
* Get the {@code hex string} from a {@link byte[]}.
*
* @param bytes The {@link byte[]} value.
* @return Returns the {@code hex string} value.
*/
public static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
}
return new String(hexChars);
}
/**
* Get an {@code int} from {@link Bundle} that is stored as a {@link String}.
*

View File

@@ -2,13 +2,19 @@ package com.termux.shared.interact;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.text.Selection;
import android.util.TypedValue;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.termux.shared.R;
public final class DialogUtils {
@@ -61,11 +67,50 @@ public final class DialogUtils {
builder.setNegativeButton(negativeButtonText, (dialog, which) -> onNegative.onTextSet(input.getText().toString()));
}
if (onDismiss != null) builder.setOnDismissListener(onDismiss);
if (onDismiss != null)
builder.setOnDismissListener(onDismiss);
dialogHolder[0] = builder.create();
dialogHolder[0].setCanceledOnTouchOutside(false);
dialogHolder[0].show();
}
/**
* Show a message in a dialog
*
* @param context The {@link Context} to use to start the dialog. An {@link Activity} {@link Context}
* must be passed, otherwise exceptions will be thrown.
* @param titleText The title text of the dialog.
* @param messageText The message text of the dialog.
* @param onDismiss The {@link DialogInterface.OnDismissListener} to run when dialog is dismissed.
*/
public static void showMessage(Context context, String titleText, String messageText, final DialogInterface.OnDismissListener onDismiss) {
AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.Theme_AppCompat_Light_Dialog)
.setPositiveButton(android.R.string.ok, null);
LayoutInflater inflater = (LayoutInflater) context.getSystemService( Context.LAYOUT_INFLATER_SERVICE );
View view = inflater.inflate(R.layout.dialog_show_message, null);
if (view != null) {
builder.setView(view);
TextView titleView = view.findViewById(R.id.dialog_title);
if (titleView != null)
titleView.setText(titleText);
TextView messageView = view.findViewById(R.id.dialog_message);
if (messageView != null)
messageView.setText(messageText);
}
if (onDismiss != null)
builder.setOnDismissListener(onDismiss);
builder.show();
}
public static void exitAppWithErrorMessage(Context context, String titleText, String messageText) {
showMessage(context, titleText, messageText, dialog -> System.exit(0));
}
}

View File

@@ -4,6 +4,7 @@ import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import androidx.core.content.ContextCompat;
@@ -13,6 +14,8 @@ import com.termux.shared.logger.Logger;
public class ShareUtils {
private static final String LOG_TAG = "ShareUtils";
/**
* Open the system app chooser that allows the user to select which app to send the intent.
*
@@ -68,4 +71,21 @@ public class ShareUtils {
}
}
/**
* Open a url.
*
* @param context The context for operations.
* @param url The url to open.
*/
public static void openURL(final Context context, final String url) {
if (context == null || url == null || url.isEmpty()) return;
try {
Uri uri = Uri.parse(url);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
context.startActivity(intent);
} catch (Exception e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to open the url \"" + url + "\"", e);
}
}
}

View File

@@ -179,7 +179,7 @@ public class MarkdownUtils {
.setFactory(Code.class, (configuration, props) -> new Object[]{
new BackgroundColorSpan(ContextCompat.getColor(context, R.color.background_markdown_code_inline)),
new TypefaceSpan("monospace"),
new AbsoluteSizeSpan(8)
new AbsoluteSizeSpan(48)
})
// NB! both ordered and bullet list items
.setFactory(ListItem.class, (configuration, props) -> new BulletSpan());

View File

@@ -2,6 +2,7 @@ package com.termux.shared.models;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Intent;
import android.net.Uri;
import androidx.annotation.NonNull;
@@ -80,6 +81,10 @@ public class ExecutionCommand {
public String workingDirectory;
/** The terminal transcript rows for the {@link ExecutionCommand}. */
public Integer terminalTranscriptRows;
/** If the {@link ExecutionCommand} is a background or a foreground terminal session command. */
public boolean inBackground;
/** If the {@link ExecutionCommand} is meant to start a failsafe terminal session. */
@@ -106,9 +111,10 @@ public class ExecutionCommand {
/** Defines if {@link ExecutionCommand} was started because of an external plugin request
* like {@link TERMUX_SERVICE#ACTION_SERVICE_EXECUTE} intent or from within Termux app itself.
*/
* like {@link TERMUX_SERVICE#ACTION_SERVICE_EXECUTE} intent or from within Termux app itself. */
public boolean isPluginExecutionCommand;
/** Defines the {@link Intent} received from the external plugin which started the {@link ExecutionCommand}. */
public Intent pluginIntent;
/** Defines {@link PendingIntent} that should be sent if an external plugin requested the execution. */
public PendingIntent pluginPendingIntent;

View File

@@ -61,7 +61,9 @@ public class NotificationUtils {
public synchronized static int getNextNotificationId(final Context context) {
if (context == null) return TermuxPreferenceConstants.TERMUX_APP.DEFAULT_VALUE_KEY_LAST_NOTIFICATION_ID;
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(context);
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
if (preferences == null) return TermuxPreferenceConstants.TERMUX_APP.DEFAULT_VALUE_KEY_LAST_NOTIFICATION_ID;
int lastNotificationId = preferences.getLastNotificationId();
int nextNotificationId = lastNotificationId + 1;

View File

@@ -3,28 +3,66 @@ package com.termux.shared.packages;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import androidx.annotation.NonNull;
import com.termux.shared.R;
import com.termux.shared.data.DataUtils;
import com.termux.shared.interact.DialogUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.termux.TermuxConstants;
import java.security.MessageDigest;
import javax.annotation.Nullable;
public class PackageUtils {
private static final String LOG_TAG = "PackageUtils";
/**
* Get the {@link Context} for the package name.
*
* @param context The {@link Context} to use to get the {@link Context} of the {@code packageName}.
* @param packageName The package name whose {@link Context} to get.
* @return Returns the {@link Context}. This will {@code null} if an exception is raised.
*/
@Nullable
public static Context getContextForPackage(@NonNull final Context context, String packageName) {
try {
return context.createPackageContext(packageName, Context.CONTEXT_RESTRICTED);
} catch (Exception e) {
Logger.logStackTraceWithMessage("Failed to get \"" + packageName + "\" package context.", e);
Logger.logVerbose(LOG_TAG, "Failed to get \"" + packageName + "\" package context: " + e.getMessage());
return null;
}
}
/**
* Get the {@link Context} for a package name.
*
* @param context The {@link Context} to use to get the {@link Context} of the {@code packageName}.
* @param packageName The package name whose {@link Context} to get.
* @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 Context}. This will {@code null} if an exception is raised.
*/
@Nullable
public static Context getContextForPackageOrExitApp(@NonNull Context context, String packageName, final boolean exitAppOnError) {
Context packageContext = getContextForPackage(context, packageName);
if (packageContext == null && exitAppOnError) {
String errorMessage = context.getString(R.string.error_get_package_context_failed_message,
packageName, TermuxConstants.TERMUX_GITHUB_REPO_URL);
Logger.logError(LOG_TAG, errorMessage);
DialogUtils.exitAppWithErrorMessage(context,
context.getString(R.string.error_get_package_context_failed_title),
errorMessage);
}
return packageContext;
}
/**
* Get the {@link PackageInfo} for the package associated with the {@code context}.
*
@@ -32,8 +70,20 @@ public class PackageUtils {
* @return Returns the {@link PackageInfo}. This will be {@code null} if an exception is raised.
*/
public static PackageInfo getPackageInfoForPackage(@NonNull final Context context) {
return getPackageInfoForPackage(context, 0);
}
/**
* Get the {@link PackageInfo} for the package associated with the {@code context}.
*
* @param context The {@link Context} for the package.
* @param flags The flags to pass to {@link PackageManager#getPackageInfo(String, int)}.
* @return Returns the {@link PackageInfo}. This will be {@code null} if an exception is raised.
*/
@Nullable
public static PackageInfo getPackageInfoForPackage(@NonNull final Context context, final int flags) {
try {
return context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
return context.getPackageManager().getPackageInfo(context.getPackageName(), flags);
} catch (final Exception e) {
return null;
}
@@ -85,6 +135,7 @@ public class PackageUtils {
* @param context The {@link Context} for the package.
* @return Returns the {@code versionCode}. This will be {@code null} if an exception is raised.
*/
@Nullable
public static Integer getVersionCodeForPackage(@NonNull final Context context) {
try {
return getPackageInfoForPackage(context).versionCode;
@@ -99,6 +150,7 @@ public class PackageUtils {
* @param context The {@link Context} for the package.
* @return Returns the {@code versionName}. This will be {@code null} if an exception is raised.
*/
@Nullable
public static String getVersionNameForPackage(@NonNull final Context context) {
try {
return getPackageInfoForPackage(context).versionName;
@@ -107,4 +159,29 @@ 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.
*/
@Nullable
public static String getSigningCertificateSHA256DigestForPackage(@NonNull final Context context) {
try {
/*
* Todo: We may need AndroidManifest queries entries if package is installed but with a different signature on android 11
* https://developer.android.com/training/package-visibility
* Need a device that allows (manual) installation of apk with mismatched signature of
* sharedUserId apps to test. Currently, if its done, PackageManager just doesn't load
* the package and removes its apk automatically if its installed as a user app instead of system app
* W/PackageManager: Failed to parse /path/to/com.termux.tasker.apk: Signature mismatch for shared user: SharedUserSetting{xxxxxxx com.termux/10xxx}
*/
PackageInfo packageInfo = getPackageInfoForPackage(context, PackageManager.GET_SIGNATURES);
if (packageInfo == null) return null;
return DataUtils.bytesToHex(MessageDigest.getInstance("SHA-256").digest(packageInfo.signatures[0].toByteArray()));
} catch (final Exception e) {
return null;
}
}
}

View File

@@ -75,8 +75,10 @@ public class PermissionUtils {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return true;
if (!PermissionUtils.checkDisplayOverOtherAppsPermission(context)) {
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(context);
if (preferences.getPluginErrorNotificationsEnabled())
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
if (preferences == null) return false;
if (preferences.arePluginErrorNotificationsEnabled())
Logger.showToast(context, context.getString(R.string.error_display_over_other_apps_permission_not_granted), true);
return false;
} else {

View File

@@ -1,16 +1,20 @@
package com.termux.shared.settings.preferences;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.TypedValue;
import androidx.annotation.NonNull;
import com.termux.shared.packages.PackageUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.logger.Logger;
import com.termux.shared.termux.TermuxUtils;
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 {
@@ -24,21 +28,54 @@ public class TermuxAppSharedPreferences {
private static final String LOG_TAG = "TermuxAppSharedPreferences";
public TermuxAppSharedPreferences(@Nonnull Context context) {
// We use the default context if failed to get termux package context
mContext = DataUtils.getDefaultIfNull(TermuxUtils.getTermuxPackageContext(context), context);
private TermuxAppSharedPreferences(@Nonnull Context context) {
mContext = context;
mSharedPreferences = getPrivateSharedPreferences(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_PACKAGE_NAME}.
* @return Returns the {@link TermuxAppSharedPreferences}. This will {@code null} if an exception is raised.
*/
@Nullable
public static TermuxAppSharedPreferences build(@NonNull final Context context) {
Context termuxPackageContext = PackageUtils.getContextForPackage(context, TermuxConstants.TERMUX_PACKAGE_NAME);
if (termuxPackageContext == null)
return null;
else
return new TermuxAppSharedPreferences(termuxPackageContext);
}
/**
* 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_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.
*/
public static TermuxAppSharedPreferences build(@NonNull final Context context, final boolean exitAppOnError) {
Context termuxPackageContext = PackageUtils.getContextForPackageOrExitApp(context, TermuxConstants.TERMUX_PACKAGE_NAME, exitAppOnError);
if (termuxPackageContext == null)
return null;
else
return new TermuxAppSharedPreferences(termuxPackageContext);
}
private static SharedPreferences getPrivateSharedPreferences(Context context) {
if (context == null) return null;
return SharedPreferenceUtils.getPrivateSharedPreferences(context, TermuxConstants.TERMUX_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
}
public boolean getShowTerminalToolbar() {
public boolean shouldShowTerminalToolbar() {
return SharedPreferenceUtils.getBoolean(mSharedPreferences, TERMUX_APP.KEY_SHOW_TERMINAL_TOOLBAR, TERMUX_APP.DEFAULT_VALUE_SHOW_TERMINAL_TOOLBAR);
}
@@ -47,14 +84,14 @@ public class TermuxAppSharedPreferences {
}
public boolean toogleShowTerminalToolbar() {
boolean currentValue = getShowTerminalToolbar();
boolean currentValue = shouldShowTerminalToolbar();
setShowTerminalToolbar(!currentValue);
return !currentValue;
}
public boolean getSoftKeyboardEnabled() {
public boolean isSoftKeyboardEnabled() {
return SharedPreferenceUtils.getBoolean(mSharedPreferences, TERMUX_APP.KEY_SOFT_KEYBOARD_ENABLED, TERMUX_APP.DEFAULT_VALUE_KEY_SOFT_KEYBOARD_ENABLED);
}
@@ -62,9 +99,17 @@ public class TermuxAppSharedPreferences {
SharedPreferenceUtils.setBoolean(mSharedPreferences, TERMUX_APP.KEY_SOFT_KEYBOARD_ENABLED, value, false);
}
public boolean isSoftKeyboardEnabledOnlyIfNoHardware() {
return SharedPreferenceUtils.getBoolean(mSharedPreferences, TERMUX_APP.KEY_SOFT_KEYBOARD_ENABLED_ONLY_IF_NO_HARDWARE, TERMUX_APP.DEFAULT_VALUE_KEY_SOFT_KEYBOARD_ENABLED_ONLY_IF_NO_HARDWARE);
}
public void setSoftKeyboardEnabledOnlyIfNoHardware(boolean value) {
SharedPreferenceUtils.setBoolean(mSharedPreferences, TERMUX_APP.KEY_SOFT_KEYBOARD_ENABLED_ONLY_IF_NO_HARDWARE, value, false);
}
public boolean getKeepScreenOn() {
public boolean shouldKeepScreenOn() {
return SharedPreferenceUtils.getBoolean(mSharedPreferences, TERMUX_APP.KEY_KEEP_SCREEN_ON, TERMUX_APP.DEFAULT_VALUE_KEEP_SCREEN_ON);
}
@@ -143,7 +188,7 @@ public class TermuxAppSharedPreferences {
public boolean getTerminalViewKeyLoggingEnabled() {
public boolean isTerminalViewKeyLoggingEnabled() {
return SharedPreferenceUtils.getBoolean(mSharedPreferences, TERMUX_APP.KEY_TERMINAL_VIEW_KEY_LOGGING_ENABLED, TERMUX_APP.DEFAULT_VALUE_TERMINAL_VIEW_KEY_LOGGING_ENABLED);
}
@@ -153,7 +198,7 @@ public class TermuxAppSharedPreferences {
public boolean getPluginErrorNotificationsEnabled() {
public boolean arePluginErrorNotificationsEnabled() {
return SharedPreferenceUtils.getBoolean(mSharedPreferences, TERMUX_APP.KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED, TERMUX_APP.DEFAULT_VALUE_PLUGIN_ERROR_NOTIFICATIONS_ENABLED);
}
@@ -163,7 +208,7 @@ public class TermuxAppSharedPreferences {
public boolean getCrashReportNotificationsEnabled() {
public boolean areCrashReportNotificationsEnabled() {
return SharedPreferenceUtils.getBoolean(mSharedPreferences, TERMUX_APP.KEY_CRASH_REPORT_NOTIFICATIONS_ENABLED, TERMUX_APP.DEFAULT_VALUE_CRASH_REPORT_NOTIFICATIONS_ENABLED);
}

View File

@@ -1,7 +1,7 @@
package com.termux.shared.settings.preferences;
/*
* Version: v0.9.0
* Version: v0.10.0
*
* Changelog
*
@@ -40,6 +40,10 @@ package com.termux.shared.settings.preferences;
*
* - 0.9.0 (2021-04-07)
* - Updated javadocs.
*
* - 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`.
*/
/**
@@ -70,6 +74,13 @@ public final class TermuxPreferenceConstants {
public static final String KEY_SOFT_KEYBOARD_ENABLED = "soft_keyboard_enabled";
public static final boolean DEFAULT_VALUE_KEY_SOFT_KEYBOARD_ENABLED = true;
/**
* Defines the key for whether the soft keyboard will be enabled only if no hardware keyboard
* attached, for cases where users want to use a hardware keyboard instead.
*/
public static final String KEY_SOFT_KEYBOARD_ENABLED_ONLY_IF_NO_HARDWARE = "soft_keyboard_enabled_only_if_no_hardware";
public static final boolean DEFAULT_VALUE_KEY_SOFT_KEYBOARD_ENABLED_ONLY_IF_NO_HARDWARE = false;
/**
* Defines the key for whether to always keep screen on.

View File

@@ -1,15 +1,18 @@
package com.termux.shared.settings.preferences;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
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.data.DataUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.termux.TermuxUtils;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class TermuxTaskerAppSharedPreferences {
@@ -20,18 +23,52 @@ public class TermuxTaskerAppSharedPreferences {
private static final String LOG_TAG = "TermuxTaskerAppSharedPreferences";
public TermuxTaskerAppSharedPreferences(@Nonnull Context context) {
// We use the default context if failed to get termux-tasker package context
mContext = DataUtils.getDefaultIfNull(TermuxUtils.getTermuxTaskerPackageContext(context), context);
private TermuxTaskerAppSharedPreferences(@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_TASKER_PACKAGE_NAME}.
* @return Returns the {@link TermuxTaskerAppSharedPreferences}. This will {@code null} if an exception is raised.
*/
@Nullable
public static TermuxTaskerAppSharedPreferences build(@NonNull final Context context) {
Context termuxTaskerPackageContext = PackageUtils.getContextForPackage(context, TermuxConstants.TERMUX_TASKER_PACKAGE_NAME);
if (termuxTaskerPackageContext == null)
return null;
else
return new TermuxTaskerAppSharedPreferences(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_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.
*/
public static TermuxTaskerAppSharedPreferences build(@NonNull final Context context, final boolean exitAppOnError) {
Context termuxTaskerPackageContext = PackageUtils.getContextForPackageOrExitApp(context, TermuxConstants.TERMUX_TASKER_PACKAGE_NAME, exitAppOnError);
if (termuxTaskerPackageContext == null)
return null;
else
return new TermuxTaskerAppSharedPreferences(termuxTaskerPackageContext);
}
private static SharedPreferences getPrivateSharedPreferences(Context context) {
if (context == null) return null;
return SharedPreferenceUtils.getPrivateSharedPreferences(context, TermuxConstants.TERMUX_TASKER_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
}
private static SharedPreferences getPrivateAndMultiProcessSharedPreferences(Context context) {
if (context == null) return null;
return SharedPreferenceUtils.getPrivateAndMultiProcessSharedPreferences(context, TermuxConstants.TERMUX_TASKER_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
}

View File

@@ -3,6 +3,7 @@ package com.termux.shared.settings.properties;
import android.content.Context;
import android.widget.Toast;
import com.google.common.collect.BiMap;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.primitives.Primitives;
import com.termux.shared.logger.Logger;
@@ -289,12 +290,14 @@ public class SharedProperties {
* @param context The {@link Context} for the {@link #getPropertiesFromFile(Context,File)}call.
* @param propertiesFile The {@link File} to read the {@link Properties} from.
* @param key The key to read.
* @param logErrorOnInvalidValue If {@code true}, then an error will be logged if key value
* was found in {@link Properties} but was invalid.
* @return Returns the {@code true} if the {@link Properties} key {@link String} value equals "true",
* regardless of case. If the key does not exist in the file or does not equal "true", then
* {@code false} will be returned.
*/
public static boolean isPropertyValueTrue(Context context, File propertiesFile, String key) {
return (boolean) getBooleanValueForStringValue((String) getProperty(context, propertiesFile, key, null), false);
public static boolean isPropertyValueTrue(Context context, File propertiesFile, String key, boolean logErrorOnInvalidValue) {
return (boolean) getBooleanValueForStringValue(key, (String) getProperty(context, propertiesFile, key, null), false, logErrorOnInvalidValue, LOG_TAG);
}
/**
@@ -304,12 +307,14 @@ public class SharedProperties {
* @param context The {@link Context} for the {@link #getPropertiesFromFile(Context,File)} call.
* @param propertiesFile The {@link File} to read the {@link Properties} from.
* @param key The key to read.
* @param logErrorOnInvalidValue If {@code true}, then an error will be logged if key value
* was found in {@link Properties} but was invalid.
* @return Returns the {@code true} if the {@link Properties} key {@link String} value equals "false",
* regardless of case. If the key does not exist in the file or does not equal "false", then
* {@code true} will be returned.
*/
public static boolean isPropertyValueFalse(Context context, File propertiesFile, String key) {
return (boolean) getInvertedBooleanValueForStringValue((String) getProperty(context, propertiesFile, key, null), true);
public static boolean isPropertyValueFalse(Context context, File propertiesFile, String key, boolean logErrorOnInvalidValue) {
return (boolean) getInvertedBooleanValueForStringValue(key, (String) getProperty(context, propertiesFile, key, null), true, logErrorOnInvalidValue, LOG_TAG);
}
@@ -413,16 +418,20 @@ public class SharedProperties {
/**
* Get the boolean value for the {@link String} value.
*
* @param value The {@link String} value to convert.
* @param def The default {@link boolean} value to return.
* @param logErrorOnInvalidValue If {@code true}, then an error will be logged if {@code value}
* was not {@code null} and was invalid.
* @param logTag If log tag to use for logging errors.
* @return Returns {@code true} or {@code false} if value is the literal string "true" or "false" respectively,
* regardless of case. Otherwise returns default value.
*/
public static boolean getBooleanValueForStringValue(String value, boolean def) {
return (boolean) getDefaultIfNull(MAP_GENERIC_BOOLEAN.get(toLowerCase(value)), def);
public static boolean getBooleanValueForStringValue(String key, String value, boolean def, boolean logErrorOnInvalidValue, String logTag) {
return (boolean) getDefaultIfNotInMap(key, MAP_GENERIC_BOOLEAN, toLowerCase(value), def, logErrorOnInvalidValue, logTag);
}
/**
@@ -430,11 +439,107 @@ public class SharedProperties {
*
* @param value The {@link String} value to convert.
* @param def The default {@link boolean} value to return.
* @param logErrorOnInvalidValue If {@code true}, then an error will be logged if {@code value}
* was not {@code null} and was invalid.
* @param logTag If log tag to use for logging errors.
* @return Returns {@code true} or {@code false} if value is the literal string "false" or "true" respectively,
* regardless of case. Otherwise returns default value.
*/
public static boolean getInvertedBooleanValueForStringValue(String value, boolean def) {
return (boolean) getDefaultIfNull(MAP_GENERIC_INVERTED_BOOLEAN.get(toLowerCase(value)), def);
public static boolean getInvertedBooleanValueForStringValue(String key, String value, boolean def, boolean logErrorOnInvalidValue, String logTag) {
return (boolean) getDefaultIfNotInMap(key, MAP_GENERIC_INVERTED_BOOLEAN, toLowerCase(value), def, logErrorOnInvalidValue, logTag);
}
/**
* Get the value for the {@code inputValue} {@link Object} key from a {@link BiMap<>}, otherwise
* default value if key not found in {@code map}.
*
* @param key The shared properties {@link String} key value for which the value is being returned.
* @param map The {@link BiMap<>} value to get the value from.
* @param inputValue The {@link Object} key value of the map.
* @param defaultOutputValue The default {@link boolean} value to return if {@code inputValue} not found in map.
* The default value must exist as a value in the {@link BiMap<>} passed.
* @param logErrorOnInvalidValue If {@code true}, then an error will be logged if {@code inputValue}
* was not {@code null} and was not found in the map.
* @param logTag If log tag to use for logging errors.
* @return Returns the value for the {@code inputValue} key from the map if it exists. Otherwise
* returns default value.
*/
public static Object getDefaultIfNotInMap(String key, @Nonnull BiMap<?, ?> map, Object inputValue, Object defaultOutputValue, boolean logErrorOnInvalidValue, String logTag) {
Object outputValue = map.get(inputValue);
if (outputValue == null) {
Object defaultInputValue = map.inverse().get(defaultOutputValue);
if (defaultInputValue == null)
Logger.logError(LOG_TAG, "The default output value \"" + defaultOutputValue + "\" for the key \"" + key + "\" does not exist as a value in the BiMap passed to getDefaultIfNotInMap(): " + map.values());
if (logErrorOnInvalidValue && inputValue != null) {
if (key != null)
Logger.logError(logTag, "The value \"" + inputValue + "\" for the key \"" + key + "\" is invalid. Using default value \"" + defaultInputValue + "\" instead.");
else
Logger.logError(logTag, "The value \"" + inputValue + "\" is invalid. Using default value \"" + defaultInputValue + "\" instead.");
}
return defaultOutputValue;
} else {
return outputValue;
}
}
/**
* Get the {@code int} {@code value} as is if between {@code min} and {@code max} (inclusive), otherwise
* return default value.
*
* @param key The shared properties {@link String} key value for which the value is being returned.
* @param value The {@code int} value to check.
* @param def The default {@code int} value if {@code value} not in range.
* @param min The min allowed {@code int} value.
* @param max The max allowed {@code int} value.
* @param logErrorOnInvalidValue If {@code true}, then an error will be logged if {@code value}
* not in range.
* @param ignoreErrorIfValueZero If logging error should be ignored if value equals 0.
* @param logTag If log tag to use for logging errors.
* @return Returns the {@code value} as is if within range. Otherwise returns default value.
*/
public static int getDefaultIfNotInRange(String key, int value, int def, int min, int max, boolean logErrorOnInvalidValue, boolean ignoreErrorIfValueZero, String logTag) {
if (value < min || value > max) {
if (logErrorOnInvalidValue && (!ignoreErrorIfValueZero || value != 0)) {
if (key != null)
Logger.logError(logTag, "The value \"" + value + "\" for the key \"" + key + "\" is not within the range " + min + "-" + max + " (inclusive). Using default value \"" + def + "\" instead.");
else
Logger.logError(logTag, "The value \"" + value + "\" is not within the range " + min + "-" + max + " (inclusive). Using default value \"" + def + "\" instead.");
}
return def;
} else {
return value;
}
}
/**
* Get the {@code float} {@code value} as is if between {@code min} and {@code max} (inclusive), otherwise
* return default value.
*
* @param key The shared properties {@link String} key value for which the value is being returned.
* @param value The {@code float} value to check.
* @param def The default {@code float} value if {@code value} not in range.
* @param min The min allowed {@code float} value.
* @param max The max allowed {@code float} value.
* @param logErrorOnInvalidValue If {@code true}, then an error will be logged if {@code value}
* not in range.
* @param ignoreErrorIfValueZero If logging error should be ignored if value equals 0.
* @param logTag If log tag to use for logging errors.
* @return Returns the {@code value} as is if within range. Otherwise returns default value.
*/
public static float getDefaultIfNotInRange(String key, float value, float def, float min, float max, boolean logErrorOnInvalidValue, boolean ignoreErrorIfValueZero, String logTag) {
if (value < min || value > max) {
if (logErrorOnInvalidValue && (!ignoreErrorIfValueZero || value != 0)) {
if (key != null)
Logger.logError(logTag, "The value \"" + value + "\" for the key \"" + key + "\" is not within the range " + min + "-" + max + " (inclusive). Using default value \"" + def + "\" instead.");
else
Logger.logError(logTag, "The value \"" + value + "\" is not within the range " + min + "-" + max + " (inclusive). Using default value \"" + def + "\" instead.");
}
return def;
} else {
return value;
}
}
/**

View File

@@ -3,6 +3,8 @@ package com.termux.shared.settings.properties;
import com.google.common.collect.ImmutableBiMap;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.logger.Logger;
import com.termux.terminal.TerminalEmulator;
import com.termux.view.TerminalView;
import java.io.File;
import java.util.Arrays;
@@ -10,7 +12,7 @@ import java.util.HashSet;
import java.util.Set;
/*
* Version: v0.6.0
* Version: v0.12.0
*
* Changelog
*
@@ -33,6 +35,26 @@ import java.util.Set;
*
* - 0.6.0 (2021-04-07)
* - Updated javadocs.
*
* - 0.7.0 (2021-05-09)
* - Add `*SOFT_KEYBOARD_TOGGLE_BEHAVIOUR*`.
*
* - 0.8.0 (2021-05-10)
* - Change the `KEY_USE_BACK_KEY_AS_ESCAPE_KEY` and `KEY_VIRTUAL_VOLUME_KEYS_DISABLED` booleans
* to `KEY_BACK_KEY_BEHAVIOUR` and `KEY_VOLUME_KEYS_BEHAVIOUR` String internal values.
* - Renamed `SOFT_KEYBOARD_TOGGLE_BEHAVIOUR` to `KEY_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR`.
*
* - 0.9.0 (2021-05-14)
* - Add `*KEY_TERMINAL_CURSOR_BLINK_RATE*`.
*
* - 0.10.0 (2021-05-15)
* - Add `MAP_BACK_KEY_BEHAVIOUR`, `MAP_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR`, `MAP_VOLUME_KEYS_BEHAVIOUR`.
*
* - 0.11.0 (2021-06-10)
* - Add `*KEY_TERMINAL_TRANSCRIPT_ROWS*`.
*
* - 0.12.0 (2021-06-10)
* - Add `*KEY_TERMINAL_CURSOR_STYLE*`.
*/
/**
@@ -48,11 +70,10 @@ import java.util.Set;
*/
public final class TermuxPropertyConstants {
/** Defines the key for whether to use back key as the escape key */
public static final String KEY_USE_BACK_KEY_AS_ESCAPE_KEY = "back-key"; // Default: "back-key"
/* boolean */
public static final String VALUE_BACK_KEY_BEHAVIOUR_BACK = "back";
public static final String VALUE_BACK_KEY_BEHAVIOUR_ESCAPE = "escape";
/** Defines the key for whether a toast will be shown when user changes the terminal session */
public static final String KEY_DISABLE_TERMINAL_SESSION_CHANGE_TOAST = "disable-terminal-session-change-toast"; // Default: "disable-terminal-session-change-toast"
@@ -86,13 +107,9 @@ public final class TermuxPropertyConstants {
/** Defines the key for whether virtual volume keys are disabled */
public static final String KEY_VIRTUAL_VOLUME_KEYS_DISABLED = "volume-keys"; // Default: "volume-keys"
public static final String VALUE_VOLUME_KEY_BEHAVIOUR_VOLUME = "volume";
public static final String VALUE_VOLUME_KEY_BEHAVIOUR_VIRTUAL = "virtual";
/* int */
/** Defines the key for the bell behaviour */
public static final String KEY_BELL_BEHAVIOUR = "bell-character"; // Default: "bell-character"
@@ -117,7 +134,49 @@ public final class TermuxPropertyConstants {
/** Defines the key for the bell behaviour */
/** Defines the key for the terminal cursor blink rate */
public static final String KEY_TERMINAL_CURSOR_BLINK_RATE = "terminal-cursor-blink-rate"; // Default: "terminal-cursor-blink-rate"
public static final int IVALUE_TERMINAL_CURSOR_BLINK_RATE_MIN = TerminalView.TERMINAL_CURSOR_BLINK_RATE_MIN;
public static final int IVALUE_TERMINAL_CURSOR_BLINK_RATE_MAX = TerminalView.TERMINAL_CURSOR_BLINK_RATE_MAX;
public static final int DEFAULT_IVALUE_TERMINAL_CURSOR_BLINK_RATE = 0;
/** Defines the key for the terminal cursor style */
public static final String KEY_TERMINAL_CURSOR_STYLE = "terminal-cursor-style"; // Default: "terminal-cursor-style"
public static final String VALUE_TERMINAL_CURSOR_STYLE_BLOCK = "block";
public static final String VALUE_TERMINAL_CURSOR_STYLE_UNDERLINE = "underline";
public static final String VALUE_TERMINAL_CURSOR_STYLE_BAR = "bar";
public static final int IVALUE_TERMINAL_CURSOR_STYLE_BLOCK = TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK;
public static final int IVALUE_TERMINAL_CURSOR_STYLE_UNDERLINE = TerminalEmulator.TERMINAL_CURSOR_STYLE_UNDERLINE;
public static final int IVALUE_TERMINAL_CURSOR_STYLE_BAR = TerminalEmulator.TERMINAL_CURSOR_STYLE_BAR;
public static final int DEFAULT_IVALUE_TERMINAL_CURSOR_STYLE = TerminalEmulator.DEFAULT_TERMINAL_CURSOR_STYLE;
/** Defines the bidirectional map for terminal cursor styles and their internal values */
public static final ImmutableBiMap<String, Integer> MAP_TERMINAL_CURSOR_STYLE =
new ImmutableBiMap.Builder<String, Integer>()
.put(VALUE_TERMINAL_CURSOR_STYLE_BLOCK, IVALUE_TERMINAL_CURSOR_STYLE_BLOCK)
.put(VALUE_TERMINAL_CURSOR_STYLE_UNDERLINE, IVALUE_TERMINAL_CURSOR_STYLE_UNDERLINE)
.put(VALUE_TERMINAL_CURSOR_STYLE_BAR, IVALUE_TERMINAL_CURSOR_STYLE_BAR)
.build();
/** Defines the key for the terminal transcript rows */
public static final String KEY_TERMINAL_TRANSCRIPT_ROWS = "terminal-transcript-rows"; // Default: "terminal-transcript-rows"
public static final int IVALUE_TERMINAL_TRANSCRIPT_ROWS_MIN = TerminalEmulator.TERMINAL_TRANSCRIPT_ROWS_MIN;
public static final int IVALUE_TERMINAL_TRANSCRIPT_ROWS_MAX = TerminalEmulator.TERMINAL_TRANSCRIPT_ROWS_MAX;
public static final int DEFAULT_IVALUE_TERMINAL_TRANSCRIPT_ROWS = TerminalEmulator.DEFAULT_TERMINAL_TRANSCRIPT_ROWS;
/* float */
/** Defines the key for the terminal toolbar height */
public static final String KEY_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR = "terminal-toolbar-height"; // Default: "terminal-toolbar-height"
public static final float IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR_MIN = 0.4f;
public static final float IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR_MAX = 3;
@@ -125,6 +184,10 @@ public final class TermuxPropertyConstants {
/* Integer */
/** Defines the key for create session shortcut */
public static final String KEY_SHORTCUT_CREATE_SESSION = "shortcut.create-session"; // Default: "shortcut.create-session"
/** Defines the key for next session shortcut */
@@ -150,6 +213,26 @@ public final class TermuxPropertyConstants {
/* String */
/** Defines the key for whether back key will behave as escape key or literal back key */
public static final String KEY_BACK_KEY_BEHAVIOUR = "back-key"; // Default: "back-key"
public static final String IVALUE_BACK_KEY_BEHAVIOUR_BACK = "back";
public static final String IVALUE_BACK_KEY_BEHAVIOUR_ESCAPE = "escape";
public static final String DEFAULT_IVALUE_BACK_KEY_BEHAVIOUR = IVALUE_BACK_KEY_BEHAVIOUR_BACK;
/** Defines the bidirectional map for back key behaviour values and their internal values */
public static final ImmutableBiMap<String, String> MAP_BACK_KEY_BEHAVIOUR =
new ImmutableBiMap.Builder<String, String>()
.put(IVALUE_BACK_KEY_BEHAVIOUR_BACK, IVALUE_BACK_KEY_BEHAVIOUR_BACK)
.put(IVALUE_BACK_KEY_BEHAVIOUR_ESCAPE, IVALUE_BACK_KEY_BEHAVIOUR_ESCAPE)
.build();
/** Defines the key for the default working directory */
public static final String KEY_DEFAULT_WORKING_DIRECTORY = "default-working-directory"; // Default: "default-working-directory"
/** Defines the default working directory */
@@ -159,47 +242,86 @@ public final class TermuxPropertyConstants {
/** Defines the key for extra keys */
public static final String KEY_EXTRA_KEYS = "extra-keys"; // Default: "extra-keys"
//public static final String DEFAULT_IVALUE_EXTRA_KEYS = "[[ESC, TAB, CTRL, ALT, {key: '-', popup: '|'}, DOWN, UP]]"; // Single row
public static final String DEFAULT_IVALUE_EXTRA_KEYS = "[['ESC','/',{key: '-', popup: '|'},'HOME','UP','END','PGUP'], ['TAB','CTRL','ALT','LEFT','DOWN','RIGHT','PGDN']]"; // Double row
/** Defines the key for extra keys style */
public static final String KEY_EXTRA_KEYS_STYLE = "extra-keys-style"; // Default: "extra-keys-style"
public static final String DEFAULT_IVALUE_EXTRA_KEYS = "[[ESC, TAB, CTRL, ALT, {key: '-', popup: '|'}, DOWN, UP]]";
public static final String DEFAULT_IVALUE_EXTRA_KEYS_STYLE = "default";
/** Defines the key for whether toggle soft keyboard request will show/hide or enable/disable keyboard */
public static final String KEY_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR = "soft-keyboard-toggle-behaviour"; // Default: "soft-keyboard-toggle-behaviour"
public static final String IVALUE_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR_SHOW_HIDE = "show/hide";
public static final String IVALUE_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR_ENABLE_DISABLE = "enable/disable";
public static final String DEFAULT_IVALUE_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR = IVALUE_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR_SHOW_HIDE;
/** Defines the bidirectional map for toggle soft keyboard behaviour values and their internal values */
public static final ImmutableBiMap<String, String> MAP_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR =
new ImmutableBiMap.Builder<String, String>()
.put(IVALUE_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR_SHOW_HIDE, IVALUE_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR_SHOW_HIDE)
.put(IVALUE_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR_ENABLE_DISABLE, IVALUE_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR_ENABLE_DISABLE)
.build();
/** Defines the key for whether volume keys will behave as virtual or literal volume keys */
public static final String KEY_VOLUME_KEYS_BEHAVIOUR = "volume-keys"; // Default: "volume-keys"
public static final String IVALUE_VOLUME_KEY_BEHAVIOUR_VIRTUAL = "virtual";
public static final String IVALUE_VOLUME_KEY_BEHAVIOUR_VOLUME = "volume";
public static final String DEFAULT_IVALUE_VOLUME_KEYS_BEHAVIOUR = IVALUE_VOLUME_KEY_BEHAVIOUR_VIRTUAL;
/** Defines the bidirectional map for volume keys behaviour values and their internal values */
public static final ImmutableBiMap<String, String> MAP_VOLUME_KEYS_BEHAVIOUR =
new ImmutableBiMap.Builder<String, String>()
.put(IVALUE_VOLUME_KEY_BEHAVIOUR_VIRTUAL, IVALUE_VOLUME_KEY_BEHAVIOUR_VIRTUAL)
.put(IVALUE_VOLUME_KEY_BEHAVIOUR_VOLUME, IVALUE_VOLUME_KEY_BEHAVIOUR_VOLUME)
.build();
/** Defines the set for keys loaded by termux
* Setting this to {@code null} will make {@link SharedProperties} throw an exception.
* */
public static final Set<String> TERMUX_PROPERTIES_LIST = new HashSet<>(Arrays.asList(
// boolean
/* boolean */
KEY_DISABLE_TERMINAL_SESSION_CHANGE_TOAST,
KEY_ENFORCE_CHAR_BASED_INPUT,
KEY_HIDE_SOFT_KEYBOARD_ON_STARTUP,
KEY_USE_BACK_KEY_AS_ESCAPE_KEY,
KEY_USE_BLACK_UI,
KEY_USE_CTRL_SPACE_WORKAROUND,
KEY_USE_FULLSCREEN,
KEY_USE_FULLSCREEN_WORKAROUND,
KEY_VIRTUAL_VOLUME_KEYS_DISABLED,
TermuxConstants.PROP_ALLOW_EXTERNAL_APPS,
// int
/* int */
KEY_BELL_BEHAVIOUR,
KEY_TERMINAL_CURSOR_BLINK_RATE,
KEY_TERMINAL_CURSOR_STYLE,
KEY_TERMINAL_TRANSCRIPT_ROWS,
// float
/* float */
KEY_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR,
// Integer
/* Integer */
KEY_SHORTCUT_CREATE_SESSION,
KEY_SHORTCUT_NEXT_SESSION,
KEY_SHORTCUT_PREVIOUS_SESSION,
KEY_SHORTCUT_RENAME_SESSION,
// String
/* String */
KEY_BACK_KEY_BEHAVIOUR,
KEY_DEFAULT_WORKING_DIRECTORY,
KEY_EXTRA_KEYS,
KEY_EXTRA_KEYS_STYLE
));
KEY_EXTRA_KEYS_STYLE,
KEY_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR,
KEY_VOLUME_KEYS_BEHAVIOUR
));
/** Defines the set for keys loaded by termux that have default boolean behaviour
* "true" -> true
@@ -207,6 +329,7 @@ public final class TermuxPropertyConstants {
* default: false
* */
public static final Set<String> TERMUX_DEFAULT_BOOLEAN_BEHAVIOUR_PROPERTIES_LIST = new HashSet<>(Arrays.asList(
KEY_DISABLE_TERMINAL_SESSION_CHANGE_TOAST,
KEY_ENFORCE_CHAR_BASED_INPUT,
KEY_HIDE_SOFT_KEYBOARD_ON_STARTUP,
KEY_USE_CTRL_SPACE_WORKAROUND,

View File

@@ -13,18 +13,18 @@ import java.util.Properties;
import javax.annotation.Nonnull;
public class TermuxSharedProperties implements SharedPropertiesParser {
public class TermuxSharedProperties {
protected final Context mContext;
protected final SharedProperties mSharedProperties;
protected final File mPropertiesFile;
private static final String LOG_TAG = "TermuxSharedProperties";
public static final String LOG_TAG = "TermuxSharedProperties";
public TermuxSharedProperties(@Nonnull Context context) {
mContext = context;
mPropertiesFile = TermuxPropertyConstants.getTermuxPropertiesFile();
mSharedProperties = new SharedProperties(context, mPropertiesFile, TermuxPropertyConstants.TERMUX_PROPERTIES_LIST, this);
mSharedProperties = new SharedProperties(context, mPropertiesFile, TermuxPropertyConstants.TERMUX_PROPERTIES_LIST, new SharedPropertiesParserClient());
loadTermuxPropertiesFromDisk();
}
@@ -76,12 +76,14 @@ public class TermuxSharedProperties implements SharedPropertiesParser {
* @param cached If {@code true}, then the value is checked from the the {@link Properties} in-memory cache.
* Otherwise the {@link Properties} object is read directly from the file
* and value is checked from it.
* @param logErrorOnInvalidValue If {@code true}, then an error will be logged if key value
* was found in {@link Properties} but was invalid.
* @return Returns the {@code true} if the {@link Properties} key {@link String} value equals "true",
* regardless of case. If the key does not exist in the file or does not equal "true", then
* {@code false} will be returned.
*/
public boolean isPropertyValueTrue(String key, boolean cached) {
return (boolean) SharedProperties.getBooleanValueForStringValue((String) getPropertyValue(key, null, cached), false);
public boolean isPropertyValueTrue(String key, boolean cached, boolean logErrorOnInvalidValue) {
return (boolean) SharedProperties.getBooleanValueForStringValue(key, (String) getPropertyValue(key, null, cached), false, logErrorOnInvalidValue, LOG_TAG);
}
/**
@@ -92,12 +94,14 @@ public class TermuxSharedProperties implements SharedPropertiesParser {
* @param cached If {@code true}, then the value is checked from the the {@link Properties} in-memory cache.
* Otherwise the {@link Properties} object is read directly from the file
* and value is checked from it.
* @param logErrorOnInvalidValue If {@code true}, then an error will be logged if key value
* was found in {@link Properties} but was invalid.
* @return Returns {@code true} if the {@link Properties} key {@link String} value equals "false",
* regardless of case. If the key does not exist in the file or does not equal "false", then
* {@code true} will be returned.
*/
public boolean isPropertyValueFalse(String key, boolean cached) {
return (boolean) SharedProperties.getInvertedBooleanValueForStringValue((String) getPropertyValue(key, null, cached), true);
public boolean isPropertyValueFalse(String key, boolean cached, boolean logErrorOnInvalidValue) {
return (boolean) SharedProperties.getInvertedBooleanValueForStringValue(key, (String) getPropertyValue(key, null, cached), true, logErrorOnInvalidValue, LOG_TAG);
}
@@ -142,24 +146,47 @@ public class TermuxSharedProperties implements SharedPropertiesParser {
// {@link #loadTermuxPropertiesFromDisk()} call
// A null value can still be returned by
// {@link #getInternalPropertyValueFromValue(Context,String,String)} for some keys
value = getInternalPropertyValueFromValue(mContext, key, null);
Logger.logWarn(LOG_TAG, "The value for \"" + key + "\" not found in SharedProperties cahce, force returning default value: `" + value + "`");
value = getInternalTermuxPropertyValueFromValue(mContext, key, null);
Logger.logWarn(LOG_TAG, "The value for \"" + key + "\" not found in SharedProperties cache, force returning default value: `" + value + "`");
return value;
}
} else {
// We get the property value directly from file and return its internal value
return getInternalPropertyValueFromValue(mContext, key, mSharedProperties.getProperty(key, false));
return getInternalTermuxPropertyValueFromValue(mContext, key, mSharedProperties.getProperty(key, false));
}
}
/**
* Override the
* {@link SharedPropertiesParser#getInternalPropertyValueFromValue(Context,String,String)}
* interface function.
* Get the internal {@link Object} value for the key passed from the file returned by
* {@link TermuxPropertyConstants#getTermuxPropertiesFile()}. The {@link Properties} object is
* read directly from the file and internal value is returned for the property value against the key.
*
* @param context The context for operations.
* @param key The key for which the internal object is required.
* @return Returns the {@link Object} object. This will be {@code null} if key is not found or
* the object stored against the key is {@code null}.
*/
@Override
public Object getInternalPropertyValueFromValue(Context context, String key, String value) {
return getInternalTermuxPropertyValueFromValue(context, key, value);
public static Object getInternalPropertyValue(Context context, String key) {
return SharedProperties.getInternalProperty(context, TermuxPropertyConstants.getTermuxPropertiesFile(), key, new SharedPropertiesParserClient());
}
/**
* The class that implements the {@link SharedPropertiesParser} interface.
*/
public static class SharedPropertiesParserClient implements SharedPropertiesParser {
/**
* Override the
* {@link SharedPropertiesParser#getInternalPropertyValueFromValue(Context,String,String)}
* interface function.
*/
@Override
public Object getInternalPropertyValueFromValue(Context context, String key, String value) {
return getInternalTermuxPropertyValueFromValue(context, key, value);
}
}
/**
@@ -181,43 +208,52 @@ public class TermuxSharedProperties implements SharedPropertiesParser {
- If the value is not null and does exist in MAP_*, then internal value returned by map will be used.
*/
switch (key) {
// boolean
case TermuxPropertyConstants.KEY_USE_BACK_KEY_AS_ESCAPE_KEY:
return (boolean) getUseBackKeyAsEscapeKeyInternalPropertyValueFromValue(value);
/* boolean */
case TermuxPropertyConstants.KEY_USE_BLACK_UI:
return (boolean) getUseBlackUIInternalPropertyValueFromValue(context, value);
case TermuxPropertyConstants.KEY_VIRTUAL_VOLUME_KEYS_DISABLED:
return (boolean) getVolumeKeysDisabledInternalPropertyValueFromValue(value);
// int
/* int */
case TermuxPropertyConstants.KEY_BELL_BEHAVIOUR:
return (int) getBellBehaviourInternalPropertyValueFromValue(value);
case TermuxPropertyConstants.KEY_TERMINAL_CURSOR_BLINK_RATE:
return (int) getTerminalCursorBlinkRateInternalPropertyValueFromValue(value);
case TermuxPropertyConstants.KEY_TERMINAL_CURSOR_STYLE:
return (int) getTerminalCursorStyleInternalPropertyValueFromValue(value);
case TermuxPropertyConstants.KEY_TERMINAL_TRANSCRIPT_ROWS:
return (int) getTerminalTranscriptRowsInternalPropertyValueFromValue(value);
// float
/* float */
case TermuxPropertyConstants.KEY_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR:
return (float) getTerminalToolbarHeightScaleFactorInternalPropertyValueFromValue(value);
// Integer (may be null)
/* Integer (may be null) */
case TermuxPropertyConstants.KEY_SHORTCUT_CREATE_SESSION:
case TermuxPropertyConstants.KEY_SHORTCUT_NEXT_SESSION:
case TermuxPropertyConstants.KEY_SHORTCUT_PREVIOUS_SESSION:
case TermuxPropertyConstants.KEY_SHORTCUT_RENAME_SESSION:
return (Integer) getCodePointForSessionShortcuts(key, value);
// String (may be null)
/* String (may be null) */
case TermuxPropertyConstants.KEY_BACK_KEY_BEHAVIOUR:
return (String) getBackKeyBehaviourInternalPropertyValueFromValue(value);
case TermuxPropertyConstants.KEY_DEFAULT_WORKING_DIRECTORY:
return (String) getDefaultWorkingDirectoryInternalPropertyValueFromValue(value);
case TermuxPropertyConstants.KEY_EXTRA_KEYS:
return (String) getExtraKeysInternalPropertyValueFromValue(value);
case TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE:
return (String) getExtraKeysStyleInternalPropertyValueFromValue(value);
case TermuxPropertyConstants.KEY_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR:
return (String) getSoftKeyboardToggleBehaviourInternalPropertyValueFromValue(value);
case TermuxPropertyConstants.KEY_VOLUME_KEYS_BEHAVIOUR:
return (String) getVolumeKeysBehaviourInternalPropertyValueFromValue(value);
default:
// default boolean behaviour
if (TermuxPropertyConstants.TERMUX_DEFAULT_BOOLEAN_BEHAVIOUR_PROPERTIES_LIST.contains(key))
return (boolean) SharedProperties.getBooleanValueForStringValue(value, false);
return (boolean) SharedProperties.getBooleanValueForStringValue(key, value, false, true, LOG_TAG);
// default inverted boolean behaviour
else if (TermuxPropertyConstants.TERMUX_DEFAULT_INVERETED_BOOLEAN_BEHAVIOUR_PROPERTIES_LIST.contains(key))
return (boolean) SharedProperties.getInvertedBooleanValueForStringValue(value, true);
return (boolean) SharedProperties.getInvertedBooleanValueForStringValue(key, value, true, true, LOG_TAG);
// just use String object as is (may be null)
else
return value;
@@ -228,16 +264,6 @@ public class TermuxSharedProperties implements SharedPropertiesParser {
/**
* Returns {@code true} if value is not {@code null} and equals {@link TermuxPropertyConstants#VALUE_BACK_KEY_BEHAVIOUR_ESCAPE}, otherwise false.
*
* @param value The {@link String} value to convert.
* @return Returns the internal value for value.
*/
public static boolean getUseBackKeyAsEscapeKeyInternalPropertyValueFromValue(String value) {
return SharedProperties.getDefaultIfNull(value, TermuxPropertyConstants.VALUE_BACK_KEY_BEHAVIOUR_BACK).equals(TermuxPropertyConstants.VALUE_BACK_KEY_BEHAVIOUR_ESCAPE);
}
/**
* Returns {@code true} or {@code false} if value is the literal string "true" or "false" respectively regardless of case.
* Otherwise returns {@code true} if the night mode is currently enabled in the system.
@@ -247,59 +273,85 @@ public class TermuxSharedProperties implements SharedPropertiesParser {
*/
public static boolean getUseBlackUIInternalPropertyValueFromValue(Context context, String value) {
int nightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
return SharedProperties.getBooleanValueForStringValue(value, nightMode == Configuration.UI_MODE_NIGHT_YES);
}
/**
* Returns {@code true} if value is not {@code null} and equals
* {@link TermuxPropertyConstants#VALUE_VOLUME_KEY_BEHAVIOUR_VOLUME}, otherwise {@code false}.
*
* @param value The {@link String} value to convert.
* @return Returns the internal value for value.
*/
public static boolean getVolumeKeysDisabledInternalPropertyValueFromValue(String value) {
return SharedProperties.getDefaultIfNull(value, TermuxPropertyConstants.VALUE_VOLUME_KEY_BEHAVIOUR_VIRTUAL).equals(TermuxPropertyConstants.VALUE_VOLUME_KEY_BEHAVIOUR_VOLUME);
return SharedProperties.getBooleanValueForStringValue(TermuxPropertyConstants.KEY_USE_BLACK_UI, value, nightMode == Configuration.UI_MODE_NIGHT_YES, true, LOG_TAG);
}
/**
* Returns the internal value after mapping it based on
* {@code TermuxPropertyConstants#MAP_BELL_BEHAVIOUR} if the value is not {@code null}
* and is valid, otherwise returns {@code TermuxPropertyConstants#DEFAULT_IVALUE_BELL_BEHAVIOUR}.
* and is valid, otherwise returns {@link TermuxPropertyConstants#DEFAULT_IVALUE_BELL_BEHAVIOUR}.
*
* @param value The {@link String} value to convert.
* @return Returns the internal value for value.
*/
public static int getBellBehaviourInternalPropertyValueFromValue(String value) {
return SharedProperties.getDefaultIfNull(TermuxPropertyConstants.MAP_BELL_BEHAVIOUR.get(SharedProperties.toLowerCase(value)), TermuxPropertyConstants.DEFAULT_IVALUE_BELL_BEHAVIOUR);
return (int) SharedProperties.getDefaultIfNotInMap(TermuxPropertyConstants.KEY_BELL_BEHAVIOUR, TermuxPropertyConstants.MAP_BELL_BEHAVIOUR, SharedProperties.toLowerCase(value), TermuxPropertyConstants.DEFAULT_IVALUE_BELL_BEHAVIOUR, true, LOG_TAG);
}
/**
* Returns the int for the value if its not null and is between
* {@code TermuxPropertyConstants#IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR_MIN} and
* {@code TermuxPropertyConstants#IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR_MAX},
* otherwise returns {@code TermuxPropertyConstants#DEFAULT_IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR}.
* {@link TermuxPropertyConstants#IVALUE_TERMINAL_CURSOR_BLINK_RATE_MIN} and
* {@link TermuxPropertyConstants#IVALUE_TERMINAL_CURSOR_BLINK_RATE_MAX},
* otherwise returns {@link TermuxPropertyConstants#DEFAULT_IVALUE_TERMINAL_CURSOR_BLINK_RATE}.
*
* @param value The {@link String} value to convert.
* @return Returns the internal value for value.
*/
public static int getTerminalCursorBlinkRateInternalPropertyValueFromValue(String value) {
return SharedProperties.getDefaultIfNotInRange(TermuxPropertyConstants.KEY_TERMINAL_CURSOR_BLINK_RATE,
DataUtils.getIntFromString(value, TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_CURSOR_BLINK_RATE),
TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_CURSOR_BLINK_RATE,
TermuxPropertyConstants.IVALUE_TERMINAL_CURSOR_BLINK_RATE_MIN,
TermuxPropertyConstants.IVALUE_TERMINAL_CURSOR_BLINK_RATE_MAX,
true, true, LOG_TAG);
}
/**
* Returns the internal value after mapping it based on
* {@link TermuxPropertyConstants#MAP_TERMINAL_CURSOR_STYLE} if the value is not {@code null}
* and is valid, otherwise returns {@link TermuxPropertyConstants#DEFAULT_IVALUE_TERMINAL_CURSOR_STYLE}.
*
* @param value The {@link String} value to convert.
* @return Returns the internal value for value.
*/
public static int getTerminalCursorStyleInternalPropertyValueFromValue(String value) {
return (int) SharedProperties.getDefaultIfNotInMap(TermuxPropertyConstants.KEY_TERMINAL_CURSOR_STYLE, TermuxPropertyConstants.MAP_TERMINAL_CURSOR_STYLE, SharedProperties.toLowerCase(value), TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_CURSOR_STYLE, true, LOG_TAG);
}
/**
* Returns the int for the value if its not null and is between
* {@link TermuxPropertyConstants#IVALUE_TERMINAL_TRANSCRIPT_ROWS_MIN} and
* {@link TermuxPropertyConstants#IVALUE_TERMINAL_TRANSCRIPT_ROWS_MAX},
* otherwise returns {@link TermuxPropertyConstants#DEFAULT_IVALUE_TERMINAL_TRANSCRIPT_ROWS}.
*
* @param value The {@link String} value to convert.
* @return Returns the internal value for value.
*/
public static int getTerminalTranscriptRowsInternalPropertyValueFromValue(String value) {
return SharedProperties.getDefaultIfNotInRange(TermuxPropertyConstants.KEY_TERMINAL_TRANSCRIPT_ROWS,
DataUtils.getIntFromString(value, TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_TRANSCRIPT_ROWS),
TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_TRANSCRIPT_ROWS,
TermuxPropertyConstants.IVALUE_TERMINAL_TRANSCRIPT_ROWS_MIN,
TermuxPropertyConstants.IVALUE_TERMINAL_TRANSCRIPT_ROWS_MAX,
true, true, LOG_TAG);
}
/**
* Returns the int for the value if its not null and is between
* {@link TermuxPropertyConstants#IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR_MIN} and
* {@link TermuxPropertyConstants#IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR_MAX},
* otherwise returns {@link TermuxPropertyConstants#DEFAULT_IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR}.
*
* @param value The {@link String} value to convert.
* @return Returns the internal value for value.
*/
public static float getTerminalToolbarHeightScaleFactorInternalPropertyValueFromValue(String value) {
return rangeTerminalToolbarHeightScaleFactorValue(DataUtils.getFloatFromString(value, TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR));
}
/**
* Returns the value itself if it is between
* {@code TermuxPropertyConstants#IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR_MIN} and
* {@code TermuxPropertyConstants#IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR_MAX},
* otherwise returns {@code TermuxPropertyConstants#DEFAULT_IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR}.
*
* @param value The value to clamp.
* @return Returns the clamped value.
*/
public static float rangeTerminalToolbarHeightScaleFactorValue(float value) {
return DataUtils.rangedOrDefault(value,
return SharedProperties.getDefaultIfNotInRange(TermuxPropertyConstants.KEY_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR,
DataUtils.getFloatFromString(value, TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR),
TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR,
TermuxPropertyConstants.IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR_MIN,
TermuxPropertyConstants.IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR_MAX);
TermuxPropertyConstants.IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR_MAX,
true, true, LOG_TAG);
}
/**
@@ -334,6 +386,16 @@ public class TermuxSharedProperties implements SharedPropertiesParser {
return codePoint;
}
/**
* Returns the value itself if it is not {@code null}, otherwise returns {@link TermuxPropertyConstants#DEFAULT_IVALUE_BACK_KEY_BEHAVIOUR}.
*
* @param value {@link String} value to convert.
* @return Returns the internal value for value.
*/
public static String getBackKeyBehaviourInternalPropertyValueFromValue(String value) {
return (String) SharedProperties.getDefaultIfNotInMap(TermuxPropertyConstants.KEY_BACK_KEY_BEHAVIOUR, TermuxPropertyConstants.MAP_BACK_KEY_BEHAVIOUR, SharedProperties.toLowerCase(value), TermuxPropertyConstants.DEFAULT_IVALUE_BACK_KEY_BEHAVIOUR, true, LOG_TAG);
}
/**
* Returns the path itself if a directory exists at it and is readable, otherwise returns
* {@link TermuxPropertyConstants#DEFAULT_IVALUE_DEFAULT_WORKING_DIRECTORY}.
@@ -345,8 +407,9 @@ public class TermuxSharedProperties implements SharedPropertiesParser {
if (path == null || path.isEmpty()) return TermuxPropertyConstants.DEFAULT_IVALUE_DEFAULT_WORKING_DIRECTORY;
File workDir = new File(path);
if (!workDir.exists() || !workDir.isDirectory() || !workDir.canRead()) {
// Fallback to default directory if user configured working directory does not exist
// or is not a directory or is not readable.
// Fallback to default directory if user configured working directory does not exist,
// is not a directory or is not readable.
Logger.logError(LOG_TAG, "The path \"" + path + "\" for the key \"" + TermuxPropertyConstants.KEY_DEFAULT_WORKING_DIRECTORY + "\" does not exist, is not a directory or is not readable. Using default value \"" + TermuxPropertyConstants.DEFAULT_IVALUE_DEFAULT_WORKING_DIRECTORY + "\" instead.");
return TermuxPropertyConstants.DEFAULT_IVALUE_DEFAULT_WORKING_DIRECTORY;
} else {
return path;
@@ -373,10 +436,34 @@ public class TermuxSharedProperties implements SharedPropertiesParser {
return SharedProperties.getDefaultIfNull(value, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE);
}
/**
* Returns the value itself if it is not {@code null}, otherwise returns {@link TermuxPropertyConstants#DEFAULT_IVALUE_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR}.
*
* @param value {@link String} value to convert.
* @return Returns the internal value for value.
*/
public static String getSoftKeyboardToggleBehaviourInternalPropertyValueFromValue(String value) {
return (String) SharedProperties.getDefaultIfNotInMap(TermuxPropertyConstants.KEY_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR, TermuxPropertyConstants.MAP_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR, SharedProperties.toLowerCase(value), TermuxPropertyConstants.DEFAULT_IVALUE_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR, true, LOG_TAG);
}
/**
* Returns the value itself if it is not {@code null}, otherwise returns {@link TermuxPropertyConstants#DEFAULT_IVALUE_VOLUME_KEYS_BEHAVIOUR}.
*
* @param value {@link String} value to convert.
* @return Returns the internal value for value.
*/
public static String getVolumeKeysBehaviourInternalPropertyValueFromValue(String value) {
return (String) SharedProperties.getDefaultIfNotInMap(TermuxPropertyConstants.KEY_VOLUME_KEYS_BEHAVIOUR, TermuxPropertyConstants.MAP_VOLUME_KEYS_BEHAVIOUR, SharedProperties.toLowerCase(value), TermuxPropertyConstants.DEFAULT_IVALUE_VOLUME_KEYS_BEHAVIOUR, true, LOG_TAG);
}
public boolean areTerminalSessionChangeToastsDisabled() {
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_DISABLE_TERMINAL_SESSION_CHANGE_TOAST, true);
}
public boolean isEnforcingCharBasedInput() {
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_ENFORCE_CHAR_BASED_INPUT, true);
}
@@ -385,10 +472,6 @@ public class TermuxSharedProperties implements SharedPropertiesParser {
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_HIDE_SOFT_KEYBOARD_ON_STARTUP, true);
}
public boolean isBackKeyTheEscapeKey() {
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_USE_BACK_KEY_AS_ESCAPE_KEY, true);
}
public boolean isUsingBlackUI() {
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_USE_BLACK_UI, true);
}
@@ -405,22 +488,42 @@ public class TermuxSharedProperties implements SharedPropertiesParser {
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_USE_FULLSCREEN_WORKAROUND, true);
}
public boolean areVirtualVolumeKeysDisabled() {
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_VIRTUAL_VOLUME_KEYS_DISABLED, true);
}
public int getBellBehaviour() {
return (int) getInternalPropertyValue(TermuxPropertyConstants.KEY_BELL_BEHAVIOUR, true);
}
public int getTerminalCursorBlinkRate() {
return (int) getInternalPropertyValue(TermuxPropertyConstants.KEY_TERMINAL_CURSOR_BLINK_RATE, true);
}
public int getTerminalCursorStyle() {
return (int) getInternalPropertyValue(TermuxPropertyConstants.KEY_TERMINAL_CURSOR_STYLE, true);
}
public int getTerminalTranscriptRows() {
return (int) getInternalPropertyValue(TermuxPropertyConstants.KEY_TERMINAL_TRANSCRIPT_ROWS, true);
}
public float getTerminalToolbarHeightScaleFactor() {
return rangeTerminalToolbarHeightScaleFactorValue((float) getInternalPropertyValue(TermuxPropertyConstants.KEY_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR, true));
return (float) getInternalPropertyValue(TermuxPropertyConstants.KEY_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR, true);
}
public boolean isBackKeyTheEscapeKey() {
return (boolean) TermuxPropertyConstants.IVALUE_BACK_KEY_BEHAVIOUR_ESCAPE.equals(getInternalPropertyValue(TermuxPropertyConstants.KEY_BACK_KEY_BEHAVIOUR, true));
}
public String getDefaultWorkingDirectory() {
return (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_DEFAULT_WORKING_DIRECTORY, true);
}
public boolean shouldEnableDisableSoftKeyboardOnToggle() {
return (boolean) TermuxPropertyConstants.IVALUE_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR_ENABLE_DISABLE.equals(getInternalPropertyValue(TermuxPropertyConstants.KEY_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR, true));
}
public boolean areVirtualVolumeKeysDisabled() {
return (boolean) TermuxPropertyConstants.IVALUE_VOLUME_KEY_BEHAVIOUR_VOLUME.equals(getInternalPropertyValue(TermuxPropertyConstants.KEY_VOLUME_KEYS_BEHAVIOUR, true));
}

View File

@@ -157,7 +157,7 @@ public class ShellUtils {
String errmsg;
errmsg = FileUtils.clearDirectory(context, "$TMPDIR", FileUtils.getCanonicalPath(TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH, null, false));
if (errmsg != null) {
Logger.logErrorAndShowToast(context, errmsg);
Logger.logError(errmsg);
}
}

View File

@@ -109,7 +109,7 @@ public class TermuxSession {
Logger.logDebug(LOG_TAG, executionCommand.toString());
Logger.logDebug(LOG_TAG, "Running \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession");
TerminalSession terminalSession = new TerminalSession(executionCommand.executable, executionCommand.workingDirectory, executionCommand.arguments, environment, terminalSessionClient);
TerminalSession terminalSession = new TerminalSession(executionCommand.executable, executionCommand.workingDirectory, executionCommand.arguments, environment, executionCommand.terminalTranscriptRows, terminalSessionClient);
if (sessionName != null) {
terminalSession.mSessionName = sessionName;

View File

@@ -33,6 +33,19 @@ public class TermuxTerminalSessionClientBase implements TerminalSessionClient {
public void onColorsChanged(TerminalSession changedSession) {
}
@Override
public void onTerminalCursorStateChange(boolean state) {
}
@Override
public Integer getTerminalCursorStyle() {
return null;
}
@Override
public void logError(String tag, String message) {
Logger.logError(tag, message);

View File

@@ -67,6 +67,11 @@ public class TermuxTerminalViewClientBase implements TerminalViewClient {
return false;
}
@Override
public void onEmulatorSet() {
}
@Override
public void logError(String tag, String message) {
Logger.logError(tag, message);

View File

@@ -3,9 +3,11 @@ package com.termux.shared.termux;
import android.annotation.SuppressLint;
import java.io.File;
import java.util.Arrays;
import java.util.List;
/*
* Version: v0.19.0
* Version: v0.21.0
*
* Changelog
*
@@ -135,6 +137,19 @@ import java.io.File;
* - Added `TERMUX_SERVICE.EXTRA_STDIN`.
* - Added `RUN_COMMAND_SERVICE.EXTRA_STDIN`.
* - Deprecated `TERMUX_ACTIVITY.EXTRA_RELOAD_STYLE`.
*
* - 0.20.0 (2021-05-13)
* - Added `TERMUX_WIKI`, `TERMUX_WIKI_URL`, `TERMUX_PLUGIN_APP_NAMES_LIST`, `TERMUX_PLUGIN_APP_PACKAGE_NAMES_LIST`.
* - Added `TERMUX_SETTINGS_ACTIVITY_NAME`.
*
* - 0.21.0 (2021-05-13)
* - Added `APK_RELEASE_FDROID`, `APK_RELEASE_FDROID_SIGNING_CERTIFICATE_SHA256_DIGEST`,
* `APK_RELEASE_GITHUB_DEBUG_BUILD`, `APK_RELEASE_GITHUB_DEBUG_BUILD_SIGNING_CERTIFICATE_SHA256_DIGEST`,
* `APK_RELEASE_GOOGLE_PLAYSTORE`, `APK_RELEASE_GOOGLE_PLAYSTORE_SIGNING_CERTIFICATE_SHA256_DIGEST`.
*
* - 0.22.0 (2021-05-13)
* - Added `TERMUX_DONATE_URL`.
*
*/
/**
@@ -188,18 +203,6 @@ public final class TermuxConstants {
/** Termux Github organization url */
public static final String TERMUX_GITHUB_ORGANIZATION_URL = "https://github.com" + "/" + TERMUX_GITHUB_ORGANIZATION_NAME; // Default: "https://github.com/termux"
/** Termux support email url */
public static final String TERMUX_SUPPORT_EMAIL_URL = "termuxreports@groups.io"; // Default: "termuxreports@groups.io"
/** Termux support email mailto url */
public static final String TERMUX_SUPPORT_EMAIL_MAILTO_URL = "mailto:" + TERMUX_SUPPORT_EMAIL_URL; // Default: "mailto:termuxreports@groups.io"
/** Termux Reddit subreddit */
public static final String TERMUX_REDDIT_SUBREDDIT = "r/termux"; // Default: "r/termux"
/** Termux Reddit subreddit url */
public static final String TERMUX_REDDIT_SUBREDDIT_URL = "https://www.reddit.com/r/termux"; // Default: "https://www.reddit.com/r/termux"
/** F-Droid packages base url */
public static final String FDROID_PACKAGES_BASE_URL = "https://f-droid.org/en/packages"; // Default: "https://f-droid.org/en/packages"
@@ -221,8 +224,6 @@ public final class TermuxConstants {
public static final String TERMUX_GITHUB_REPO_URL = TERMUX_GITHUB_ORGANIZATION_URL + "/" + TERMUX_GITHUB_REPO_NAME; // Default: "https://github.com/termux/termux-app"
/** Termux Github issues repo url */
public static final String TERMUX_GITHUB_ISSUES_REPO_URL = TERMUX_GITHUB_REPO_URL + "/issues"; // Default: "https://github.com/termux/termux-app/issues"
/** Termux Github wiki repo url */
public static final String TERMUX_GITHUB_WIKI_REPO_URL = TERMUX_GITHUB_REPO_URL + "/wiki"; // Default: "https://github.com/termux/termux-app/wiki"
/** Termux F-Droid package url */
public static final String TERMUX_FDROID_PACKAGE_URL = FDROID_PACKAGES_BASE_URL + "/" + TERMUX_PACKAGE_NAME; // Default: "https://f-droid.org/en/packages/com.termux"
@@ -314,6 +315,56 @@ public final class TermuxConstants {
/*
* Termux plugin apps lists.
*/
public static final List<String> TERMUX_PLUGIN_APP_NAMES_LIST = Arrays.asList(
TERMUX_API_APP_NAME,
TERMUX_BOOT_APP_NAME,
TERMUX_FLOAT_APP_NAME,
TERMUX_STYLING_APP_NAME,
TERMUX_TASKER_APP_NAME,
TERMUX_WIDGET_APP_NAME);
public static final List<String> TERMUX_PLUGIN_APP_PACKAGE_NAMES_LIST = Arrays.asList(
TERMUX_API_PACKAGE_NAME,
TERMUX_BOOT_PACKAGE_NAME,
TERMUX_FLOAT_PACKAGE_NAME,
TERMUX_STYLING_PACKAGE_NAME,
TERMUX_TASKER_PACKAGE_NAME,
TERMUX_WIDGET_PACKAGE_NAME);
/*
* Termux APK releases.
*/
/** F-Droid APK release */
public static final String APK_RELEASE_FDROID = "F-Droid"; // Default: "F-Droid"
/** F-Droid APK release signing certificate SHA-256 digest */
public static final String APK_RELEASE_FDROID_SIGNING_CERTIFICATE_SHA256_DIGEST = "228FB2CFE90831C1499EC3CCAF61E96E8E1CE70766B9474672CE427334D41C42"; // Default: "228FB2CFE90831C1499EC3CCAF61E96E8E1CE70766B9474672CE427334D41C42"
/** Github Debug Build APK release */
public static final String APK_RELEASE_GITHUB_DEBUG_BUILD = "Github Debug Build"; // Default: "Github Debug Build"
/** Github Debug Build APK release signing certificate SHA-256 digest */
public static final String APK_RELEASE_GITHUB_DEBUG_BUILD_SIGNING_CERTIFICATE_SHA256_DIGEST = "B6DA01480EEFD5FBF2CD3771B8D1021EC791304BDD6C4BF41D3FAABAD48EE5E1"; // Default: "B6DA01480EEFD5FBF2CD3771B8D1021EC791304BDD6C4BF41D3FAABAD48EE5E1"
/** Google Play Store APK release */
public static final String APK_RELEASE_GOOGLE_PLAYSTORE = "Google Play Store"; // Default: "Google Play Store"
/** Google Play Store APK release signing certificate SHA-256 digest */
public static final String APK_RELEASE_GOOGLE_PLAYSTORE_SIGNING_CERTIFICATE_SHA256_DIGEST = "738F0A30A04D3C8A1BE304AF18D0779BCF3EA88FB60808F657A3521861C2EBF9"; // Default: "738F0A30A04D3C8A1BE304AF18D0779BCF3EA88FB60808F657A3521861C2EBF9"
/*
* Termux packages urls.
*/
@@ -324,8 +375,6 @@ public final class TermuxConstants {
public static final String TERMUX_PACKAGES_GITHUB_REPO_URL = TERMUX_GITHUB_ORGANIZATION_URL + "/" + TERMUX_PACKAGES_GITHUB_REPO_NAME; // Default: "https://github.com/termux/termux-packages"
/** Termux Packages Github issues repo url */
public static final String TERMUX_PACKAGES_GITHUB_ISSUES_REPO_URL = TERMUX_PACKAGES_GITHUB_REPO_URL + "/issues"; // Default: "https://github.com/termux/termux-packages/issues"
/** Termux Packages wiki repo url */
public static final String TERMUX_PACKAGES_GITHUB_WIKI_REPO_URL = TERMUX_PACKAGES_GITHUB_REPO_URL + "/wiki"; // Default: "https://github.com/termux/termux-packages/wiki"
/** Termux Game Packages Github repo name */
@@ -371,6 +420,44 @@ public final class TermuxConstants {
/*
* Termux miscellaneous urls.
*/
/** Termux Wiki */
public static final String TERMUX_WIKI = TERMUX_APP_NAME + " Wiki"; // Default: "Termux Wiki"
/** Termux Wiki url */
public static final String TERMUX_WIKI_URL = "https://wiki.termux.com"; // Default: "https://wiki.termux.com"
/** Termux Github wiki repo url */
public static final String TERMUX_GITHUB_WIKI_REPO_URL = TERMUX_GITHUB_REPO_URL + "/wiki"; // Default: "https://github.com/termux/termux-app/wiki"
/** Termux Packages wiki repo url */
public static final String TERMUX_PACKAGES_GITHUB_WIKI_REPO_URL = TERMUX_PACKAGES_GITHUB_REPO_URL + "/wiki"; // Default: "https://github.com/termux/termux-packages/wiki"
/** Termux support email url */
public static final String TERMUX_SUPPORT_EMAIL_URL = "termuxreports@groups.io"; // Default: "termuxreports@groups.io"
/** Termux support email mailto url */
public static final String TERMUX_SUPPORT_EMAIL_MAILTO_URL = "mailto:" + TERMUX_SUPPORT_EMAIL_URL; // Default: "mailto:termuxreports@groups.io"
/** Termux Reddit subreddit */
public static final String TERMUX_REDDIT_SUBREDDIT = "r/termux"; // Default: "r/termux"
/** Termux Reddit subreddit url */
public static final String TERMUX_REDDIT_SUBREDDIT_URL = "https://www.reddit.com/r/termux"; // Default: "https://www.reddit.com/r/termux"
/** Termux donate url */
public static final String TERMUX_DONATE_URL = TERMUX_PACKAGES_GITHUB_REPO_URL + "/wiki/Donate"; // Default: "https://github.com/termux/termux-packages/wiki/Donate"
/*
* Termux app core directory paths.
*/
@@ -654,6 +741,13 @@ public final class TermuxConstants {
/** Termux app settings activity name. */
public static final String TERMUX_SETTINGS_ACTIVITY_NAME = TERMUX_PACKAGE_NAME + ".app.activities.SettingsActivity"; // Default: "com.termux.app.activities.SettingsActivity"
/** Termux app core service name. */
public static final String TERMUX_SERVICE_NAME = TERMUX_PACKAGE_NAME + ".app.TermuxService"; // Default: "com.termux.app.TermuxService"

View File

@@ -132,6 +132,38 @@ public class TermuxUtils {
}
}
/**
* Get a markdown {@link String} for the apps info of all/any termux plugin apps installed.
*
* @param currentPackageContext The context of current package.
* @return Returns the markdown {@link String}.
*/
public static String getTermuxPluginAppsInfoMarkdownString(@NonNull final Context currentPackageContext) {
if (currentPackageContext == null) return "null";
StringBuilder markdownString = new StringBuilder();
List<String> termuxPluginAppPackageNamesList = TermuxConstants.TERMUX_PLUGIN_APP_PACKAGE_NAMES_LIST;
if (termuxPluginAppPackageNamesList != null) {
for (int i = 0; i < termuxPluginAppPackageNamesList.size(); i++) {
String termuxPluginAppPackageName = termuxPluginAppPackageNamesList.get(i);
Context termuxPluginAppContext = PackageUtils.getContextForPackage(currentPackageContext, termuxPluginAppPackageName);
// If the package context for the plugin app is not null, then assume its installed and get its info
if (termuxPluginAppContext != null) {
if (i != 0)
markdownString.append("\n\n");
markdownString.append(TermuxUtils.getAppInfoMarkdownString(termuxPluginAppContext, false));
}
}
}
if (markdownString.toString().isEmpty())
return null;
return markdownString.toString();
}
/**
* Get a markdown {@link String} for the app info. If the {@code context} passed is different
* from the {@link TermuxConstants#TERMUX_PACKAGE_NAME} package context, then this function
@@ -195,6 +227,12 @@ public class TermuxUtils {
appendPropertyToMarkdown(markdownString,"TARGET_SDK", PackageUtils.getTargetSDKForPackage(context));
appendPropertyToMarkdown(markdownString,"IS_DEBUG_BUILD", PackageUtils.isAppForPackageADebugBuild(context));
String signingCertificateSHA256Digest = PackageUtils.getSigningCertificateSHA256DigestForPackage(context);
if (signingCertificateSHA256Digest != null) {
appendPropertyToMarkdown(markdownString,"APK_RELEASE", getAPKRelease(signingCertificateSHA256Digest));
appendPropertyToMarkdown(markdownString,"SIGNING_CERTIFICATE_SHA256_DIGEST", signingCertificateSHA256Digest);
}
return markdownString.toString();
}
@@ -223,6 +261,8 @@ public class TermuxUtils {
appendPropertyToMarkdown(markdownString, "RELEASE", Build.VERSION.RELEASE);
else
appendPropertyToMarkdown(markdownString, "CODENAME", Build.VERSION.CODENAME);
appendPropertyToMarkdown(markdownString, "ID", Build.ID);
appendPropertyToMarkdown(markdownString, "DISPLAY", Build.DISPLAY);
appendPropertyToMarkdown(markdownString, "INCREMENTAL", Build.VERSION.INCREMENTAL);
appendPropertyToMarkdownIfSet(markdownString, "SECURITY_PATCH", systemProperties.getProperty("ro.build.version.security_patch"));
appendPropertyToMarkdownIfSet(markdownString, "IS_DEBUGGABLE", systemProperties.getProperty("ro.debuggable"));
@@ -236,8 +276,6 @@ public class TermuxUtils {
appendPropertyToMarkdown(markdownString, "BRAND", Build.BRAND);
appendPropertyToMarkdown(markdownString, "MODEL", Build.MODEL);
appendPropertyToMarkdown(markdownString, "PRODUCT", Build.PRODUCT);
appendPropertyToMarkdown(markdownString, "DISPLAY", Build.DISPLAY);
appendPropertyToMarkdown(markdownString, "ID", Build.ID);
appendPropertyToMarkdown(markdownString, "BOARD", Build.BOARD);
appendPropertyToMarkdown(markdownString, "HARDWARE", Build.HARDWARE);
appendPropertyToMarkdown(markdownString, "DEVICE", Build.DEVICE);
@@ -291,6 +329,45 @@ public class TermuxUtils {
return markdownString.toString();
}
/**
* Get a markdown {@link String} for important links.
*
* @param context The context for operations.
* @return Returns the markdown {@link String}.
*/
public static String getImportantLinksMarkdownString(@NonNull final Context context) {
if (context == null) return "null";
StringBuilder markdownString = new StringBuilder();
markdownString.append("## Important Links");
markdownString.append("\n\n### Github\n");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_APP_NAME, TermuxConstants.TERMUX_GITHUB_REPO_URL)).append(" ");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_API_APP_NAME, TermuxConstants.TERMUX_API_GITHUB_REPO_URL)).append(" ");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_BOOT_APP_NAME, TermuxConstants.TERMUX_BOOT_GITHUB_REPO_URL)).append(" ");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_FLOAT_APP_NAME, TermuxConstants.TERMUX_FLOAT_GITHUB_REPO_URL)).append(" ");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_STYLING_APP_NAME, TermuxConstants.TERMUX_STYLING_GITHUB_REPO_URL)).append(" ");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_TASKER_APP_NAME, TermuxConstants.TERMUX_TASKER_GITHUB_REPO_URL)).append(" ");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_WIDGET_APP_NAME, TermuxConstants.TERMUX_WIDGET_GITHUB_REPO_URL)).append(" ");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_PACKAGES_GITHUB_REPO_NAME, TermuxConstants.TERMUX_PACKAGES_GITHUB_REPO_URL)).append(" ");
markdownString.append("\n\n### Email\n");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_SUPPORT_EMAIL_URL, TermuxConstants.TERMUX_SUPPORT_EMAIL_MAILTO_URL)).append(" ");
markdownString.append("\n\n### Reddit\n");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_REDDIT_SUBREDDIT, TermuxConstants.TERMUX_REDDIT_SUBREDDIT_URL)).append(" ");
markdownString.append("\n\n### Wiki\n");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_WIKI, TermuxConstants.TERMUX_WIKI_URL)).append(" ");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_APP_NAME, TermuxConstants.TERMUX_GITHUB_WIKI_REPO_URL)).append(" ");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_PACKAGES_GITHUB_REPO_NAME, TermuxConstants.TERMUX_PACKAGES_GITHUB_WIKI_REPO_URL)).append(" ");
markdownString.append("\n##\n");
return markdownString.toString();
}
/**
@@ -303,7 +380,7 @@ public class TermuxUtils {
*/
public static String geAPTInfoMarkdownString(@NonNull final Context context) {
String aptInfoScript = null;
String aptInfoScript;
InputStream inputStream = context.getResources().openRawResource(com.termux.shared.R.raw.apt_info_script);
try {
aptInfoScript = IOUtils.toString(inputStream, Charset.defaultCharset());
@@ -419,4 +496,19 @@ public class TermuxUtils {
return df.format(new Date());
}
public static String getAPKRelease(String signingCertificateSHA256Digest) {
if (signingCertificateSHA256Digest == null) return "null";
switch (signingCertificateSHA256Digest.toUpperCase()) {
case TermuxConstants.APK_RELEASE_FDROID_SIGNING_CERTIFICATE_SHA256_DIGEST:
return TermuxConstants.APK_RELEASE_FDROID;
case TermuxConstants.APK_RELEASE_GITHUB_DEBUG_BUILD_SIGNING_CERTIFICATE_SHA256_DIGEST:
return TermuxConstants.APK_RELEASE_GITHUB_DEBUG_BUILD;
case TermuxConstants.APK_RELEASE_GOOGLE_PLAYSTORE_SIGNING_CERTIFICATE_SHA256_DIGEST:
return TermuxConstants.APK_RELEASE_GOOGLE_PLAYSTORE;
default:
return "Unknown";
}
}
}

View File

@@ -0,0 +1,195 @@
package com.termux.shared.view;
import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.inputmethodservice.InputMethodService;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import androidx.annotation.NonNull;
import androidx.core.view.WindowInsetsCompat;
import com.termux.shared.logger.Logger;
public class KeyboardUtils {
private static final String LOG_TAG = "KeyboardUtils";
public static void setSoftKeyboardVisibility(@NonNull final Runnable showSoftKeyboardRunnable, final Activity activity, final View view, final boolean visible) {
if (visible) {
// A Runnable with a delay is used, otherwise soft keyboard may not automatically open
// on some devices, but still may fail
view.postDelayed(showSoftKeyboardRunnable, 500);
} else {
view.removeCallbacks(showSoftKeyboardRunnable);
hideSoftKeyboard(activity, view);
}
}
/**
* Toggle the soft keyboard. The {@link InputMethodManager#SHOW_FORCED} is passed as
* {@code showFlags} so that keyboard is forcefully shown if it needs to be enabled.
*
* This is also important for soft keyboard to be shown when a hardware keyboard is connected, and
* user has disabled the {@code Show on-screen keyboard while hardware keyboard is connected} toggle
* in Android "Language and Input" settings but the current soft keyboard app overrides the
* default implementation of {@link InputMethodService#onEvaluateInputViewShown()} and returns
* {@code true}.
*/
public static void toggleSoftKeyboard(final Context context) {
if (context == null) return;
InputMethodManager inputMethodManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
if (inputMethodManager != null)
inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
}
/**
* Show the soft keyboard. The {@code 0} value is passed as {@code flags} so that keyboard is
* forcefully shown.
*
* This is also important for soft keyboard to be shown on app startup when a hardware keyboard
* is connected, and user has disabled the {@code Show on-screen keyboard while hardware keyboard
* is connected} toggle in Android "Language and Input" settings but the current soft keyboard app
* overrides the default implementation of {@link InputMethodService#onEvaluateInputViewShown()}
* and returns {@code true}.
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:frameworks/base/core/java/android/inputmethodservice/InputMethodService.java;l=1751
*
* Also check {@link InputMethodService#onShowInputRequested(int, boolean)} which must return
* {@code true}, which can be done by failing its {@code ((flags&InputMethod.SHOW_EXPLICIT) == 0)}
* check by passing {@code 0} as {@code flags}.
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:frameworks/base/core/java/android/inputmethodservice/InputMethodService.java;l=2022
*/
public static void showSoftKeyboard(final Context context, final View view) {
if (context == null || view == null) return;
InputMethodManager inputMethodManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
if (inputMethodManager != null)
inputMethodManager.showSoftInput(view, 0);
}
public static void hideSoftKeyboard(final Context context, final View view) {
if (context == null || view == null) return;
InputMethodManager inputMethodManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
if (inputMethodManager != null)
inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
public static void disableSoftKeyboard(final Activity activity, final View view) {
if (activity == null || view == null) return;
hideSoftKeyboard(activity, view);
setDisableSoftKeyboardFlags(activity);
}
public static void setDisableSoftKeyboardFlags(final Activity activity) {
if (activity != null && activity.getWindow() != null)
activity.getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
}
public static void clearDisableSoftKeyboardFlags(final Activity activity) {
if (activity != null && activity.getWindow() != null)
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
}
public static boolean areDisableSoftKeyboardFlagsSet(final Activity activity) {
if (activity == null || activity.getWindow() == null) return false;
return (activity.getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) != 0;
}
public static void setSoftKeyboardAlwaysHiddenFlags(final Activity activity) {
if (activity != null && activity.getWindow() != null)
activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
}
public static void setSoftInputModeAdjustResize(final Activity activity) {
// TODO: The flag is deprecated for API 30 and WindowInset API should be used
// https://developer.android.com/reference/android/view/WindowManager.LayoutParams#SOFT_INPUT_ADJUST_RESIZE
// https://medium.com/androiddevelopers/animating-your-keyboard-fb776a8fb66d
// https://stackoverflow.com/a/65194077/14686958
if (activity != null && activity.getWindow() != null)
activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
}
/**
* Check if soft keyboard is visible.
* Does not work on android 7 but does on android 11 avd.
*
* @param activity The Activity of the root view for which the visibility should be checked.
* @return Returns {@code true} if soft keyboard is visible, otherwise {@code false}.
*/
public static boolean isSoftKeyboardVisible(final Activity activity) {
if (activity != null && activity.getWindow() != null) {
WindowInsets insets = activity.getWindow().getDecorView().getRootWindowInsets();
if (insets != null) {
WindowInsetsCompat insetsCompat = WindowInsetsCompat.toWindowInsetsCompat(insets);
if (insetsCompat.isVisible(WindowInsetsCompat.Type.ime())) {
Logger.logVerbose(LOG_TAG, "Soft keyboard visible");
return true;
}
}
}
Logger.logVerbose(LOG_TAG, "Soft keyboard not visible");
return false;
}
/**
* Check if hardware keyboard is connected.
* Based on default implementation of {@link InputMethodService#onEvaluateInputViewShown()}.
*
* https://developer.android.com/guide/topics/resources/providing-resources#ImeQualifier
*
* @param context The Context for operations.
* @return Returns {@code true} if device has hardware keys for text input or an external hardware
* keyboard is connected, otherwise {@code false}.
*/
public static boolean isHardKeyboardConnected(final Context context) {
if (context == null) return false;
Configuration config = context.getResources().getConfiguration();
return config.keyboard != Configuration.KEYBOARD_NOKEYS
|| config.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO;
}
/**
* Check if soft keyboard should be disabled based on user configuration.
*
* @param context The Context for operations.
* @return Returns {@code true} if device has soft keyboard should be disabled, otherwise {@code false}.
*/
public static boolean shouldSoftKeyboardBeDisabled(final Context context, final boolean isSoftKeyboardEnabled, final boolean isSoftKeyboardEnabledOnlyIfNoHardware) {
// If soft keyboard is disabled by user regardless of hardware keyboard
if (!isSoftKeyboardEnabled) {
return true;
} else {
/*
* Currently, for this case, soft keyboard will be disabled on Termux app startup and
* when switching back from another app. Soft keyboard can be temporarily enabled in
* show/hide soft keyboard toggle behaviour with keyboard toggle buttons and will continue
* to work when tapping on terminal view for opening and back button for closing, until
* Termux app is switched to another app. After returning back, keyboard will be disabled
* until toggle is pressed again.
* This may also be helpful for the Lineage OS bug where if "Show soft keyboard" toggle
* in "Language and Input" is disabled and Termux is started without a hardware keyboard
* in landscape mode, and then the keyboard is connected and phone is rotated to portrait
* mode and then keyboard is toggled with Termux keyboard toggle buttons, then a blank
* space is shown in-place of the soft keyboard. Its likely related to
* WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE which pushes up the view when
* keyboard is opened instead of the keyboard opening on top of the view (hiding stuff).
* If the "Show soft keyboard" toggle was disabled, then this resizing shouldn't happen.
* But it seems resizing does happen, but keyboard is never opened since its not supposed to.
* https://github.com/termux/termux-app/issues/1995#issuecomment-837080079
*/
// If soft keyboard is disabled by user only if hardware keyboard is connected
if(isSoftKeyboardEnabledOnlyIfNoHardware) {
boolean isHardKeyboardConnected = KeyboardUtils.isHardKeyboardConnected(context);
Logger.logVerbose(LOG_TAG, "Hardware keyboard connected=" + isHardKeyboardConnected);
return isHardKeyboardConnected;
} else {
return false;
}
}
}
}

View File

@@ -0,0 +1,98 @@
package com.termux.shared.view;
import android.app.Activity;
import android.content.Context;
import android.content.ContextWrapper;
import android.graphics.Rect;
import android.util.TypedValue;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
public class ViewUtils {
/**
* Check if a {@link View} is fully visible and not hidden or partially covered by another view.
*
* https://stackoverflow.com/a/51078418/14686958
*
* @param view The {@link View} to check.
* @return Returns {@code true} if view is fully visible.
*/
public static boolean isViewFullyVisible(View view) {
Rect[] windowAndViewRects = getWindowAndViewRects(view);
if (windowAndViewRects == null)
return false;
return windowAndViewRects[0].contains(windowAndViewRects[1]);
}
/**
* Get the {@link Rect} of a {@link View} and the {@link Rect} of the window inside which it
* exists.
*
* https://stackoverflow.com/a/51078418/14686958
*
* @param view The {@link View} inside the window whose {@link Rect} to get.
* @return Returns {@link Rect[]} if view is visible where Rect[0] will contain window
* {@link Rect} and Rect[1] will contain view {@link Rect}. This will be {@code null}
* if view is not visible.
*/
@Nullable
public static Rect[] getWindowAndViewRects(View view) {
if (view == null || !view.isShown())
return null;
// windowRect - will hold available area where content remain visible to users
// Takes into account screen decorations (e.g. statusbar)
Rect windowRect = new Rect();
view.getWindowVisibleDisplayFrame(windowRect);
// If there is actionbar, get his height
int actionBarHeight = 0;
Context context = view.getContext();
if (context instanceof AppCompatActivity) {
androidx.appcompat.app.ActionBar actionBar = ((AppCompatActivity) context).getSupportActionBar();
if (actionBar != null) actionBarHeight = actionBar.getHeight();
} else if (context instanceof Activity) {
android.app.ActionBar actionBar = ((Activity) context).getActionBar();
if (actionBar != null) actionBarHeight = actionBar.getHeight();
}
// windowAvailableRect - takes into account actionbar and statusbar height
Rect windowAvailableRect;
windowAvailableRect = new Rect(windowRect.left, windowRect.top + actionBarHeight, windowRect.right, windowRect.bottom);
// viewRect - holds position of the view in window
// (methods as getGlobalVisibleRect, getHitRect, getDrawingRect can return different result,
// when partialy visible)
Rect viewRect;
final int[] viewsLocationInWindow = new int[2];
view.getLocationInWindow(viewsLocationInWindow);
int viewLeft = viewsLocationInWindow[0];
int viewTop = viewsLocationInWindow[1];
int viewRight = viewLeft + view.getWidth();
int viewBottom = viewTop + view.getHeight();
viewRect = new Rect(viewLeft, viewTop, viewRight, viewBottom);
return new Rect[]{windowAvailableRect, viewRect};
}
/** Get the {@link Activity} associated with the {@link Context} if available. */
@Nullable
public static Activity getActivity(Context context) {
while (context instanceof ContextWrapper) {
if (context instanceof Activity) {
return (Activity)context;
}
context = ((ContextWrapper)context).getBaseContext();
}
return null;
}
/** Convert value in device independent pixels (dp) to pixels (px) units. */
public static int dpToPx(Context context, int dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics());
}
}

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/dark_red"
android:paddingStart="24dp"
android:paddingTop="16dp"
android:paddingEnd="24dp"
android:paddingBottom="16dp">
<TextView
android:id="@+id/dialog_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Title"
android:textColor="@android:color/white"/>
</LinearLayout>
<ScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="24dp"
android:paddingTop="16dp"
android:paddingEnd="24dp"
android:paddingBottom="16dp"
android:scrollbars="vertical">
<TextView
android:id="@+id/dialog_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="web|email"
android:textSize="14sp"
android:textColor="@android:color/tab_indicator_text"
android:textColorLink="@android:color/black"/>
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@@ -2,4 +2,5 @@
<resources>
<color name="background_markdown_code_inline">#1F000000</color>
<color name="background_markdown_code_block">#0F000000</color>
<color name="dark_red">#FF0000</color>
</resources>

View File

@@ -61,6 +61,13 @@
<string name="error_file_not_executable">The %1$s at path is not executable. Permission Denied.</string>
<!-- PackageUtils -->
<string name="error_get_package_context_failed_title">Failed To Get Package Context</string>
<string name="error_get_package_context_failed_message">Failed to get package context for the \"%1$s\" package. This may be because the app package is not installed or it has different APK signature from the current app. Check install instruction at %2$s for more details.</string>
<!-- PermissionUtils -->
<string name="message_sudo_please_grant_permissions">Please grant permissions on next screen</string>
<string name="error_display_over_other_apps_permission_not_granted">&TERMUX_APP_NAME; requires \"Display over other apps\" permission to start terminal sessions from background on Android >= 10. Grants it from Settings -> Apps -> &TERMUX_APP_NAME; -> Advanced</string>
@@ -72,11 +79,6 @@
<!-- TermuxUtils -->
<string name="msg_report_issue">If you want to report this issue, then copy its text from the options menu (3-dots on top right) and post an issue on one of the following links. If you are posting on Github, then post it in the repository at which the report belongs at. You may optionally remove any device specific info that you consider private or don\'t want to share or that is not relevant to the issue.</string>
<!-- ShellUtils -->
<string name="error_sending_sigkill_to_process">Sending SIGKILL to process on user request or because android is killing the execution service</string>
<string name="error_execution_cancelled">Execution has been cancelled since execution service is being killed</string>
@@ -87,6 +89,11 @@
<!-- TermuxUtils -->
<string name="msg_report_issue">If you want to report this issue, then copy its text from the options menu (3-dots on top right) and post an issue on one of the following links.\n\nIf you are posting a Termux app crash report, then please provide details on what you were doing that caused the crash and how to reproduce it, if possible.\n\nIf you are posting an issue on Github, then post it in the repository at which the report belongs at. You may optionally remove any device specific info that you consider private or don\'t want to share or that is not relevant to the issue.</string>
<!-- Log Level -->
<string name="log_level_title">Log Level</string>
<string name="log_level_off">"Off"</string>