Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bde9d01f76 | ||
|
|
5a511a2ba3 | ||
|
|
5c50964b1f | ||
|
|
dea8c9879e | ||
|
|
2034121798 | ||
|
|
23a900c433 | ||
|
|
93a7525d9b | ||
|
|
5670128236 | ||
|
|
dfd32435af | ||
|
|
49265160f8 | ||
|
|
70e1accafe | ||
|
|
1c7f9166f2 | ||
|
|
553913cde1 | ||
|
|
6bca378cec | ||
|
|
12f910c32d | ||
|
|
94c5f3674a | ||
|
|
28b9f93d13 | ||
|
|
69bebb5916 | ||
|
|
321350256e | ||
|
|
e5a9b99afe | ||
|
|
00f805f7ec | ||
|
|
d3c34ad1f5 | ||
|
|
59877a08d1 | ||
|
|
9c92251595 | ||
|
|
e408fdcc08 | ||
|
|
53c1a49b5b | ||
|
|
2aafcf8435 | ||
|
|
1c1af34374 | ||
|
|
52f18a73fb | ||
|
|
28f81f2cc7 | ||
|
|
4494bc66e4 | ||
|
|
679e0de044 | ||
|
|
80b495e50b | ||
|
|
69e5deedc7 | ||
|
|
7f36d7bbd0 | ||
|
|
b7b12ebe84 | ||
|
|
f77c88633e | ||
|
|
5f2ccca423 | ||
|
|
f0f6927273 | ||
|
|
0fb18c0c8b | ||
|
|
4dfed3320e | ||
|
|
7ac62c9840 | ||
|
|
fd80cdaf23 | ||
|
|
19c690d02b | ||
|
|
e119d34bca | ||
|
|
f545ebf0bd | ||
|
|
0b4bbaf23d | ||
|
|
e7dd0eeebe | ||
|
|
7ef9255437 | ||
|
|
7225e2b379 | ||
|
|
1ad038ece5 | ||
|
|
cb8b0225ca | ||
|
|
7620800cd5 | ||
|
|
6837db0015 | ||
|
|
e08e3b536e | ||
|
|
b711a467c1 | ||
|
|
d736b1eba5 | ||
|
|
58d577066a | ||
|
|
89a1e02713 | ||
|
|
6524a619f6 | ||
|
|
f8ccbb4953 | ||
|
|
31298b8857 | ||
|
|
11f5c0afd1 | ||
|
|
27dc211e2d | ||
|
|
2f828255ee | ||
|
|
339b2a24a2 | ||
|
|
6de3713049 | ||
|
|
79df863b75 | ||
|
|
af115c9966 | ||
|
|
1e30022ce7 | ||
|
|
4629276500 | ||
|
|
d42514d8c9 | ||
|
|
90c9a7b3bc | ||
|
|
e6dac93352 | ||
|
|
e4e638bd31 | ||
|
|
fe8c3ba216 | ||
|
|
4ecea144bb | ||
|
|
116b9b42d8 | ||
|
|
39c69db820 | ||
|
|
4d1851e6be | ||
|
|
596aa56b38 | ||
|
|
4850678d55 | ||
|
|
bc52a4e90c | ||
|
|
3e7b3604a4 | ||
|
|
f3f58c8fc7 | ||
|
|
4711094614 | ||
|
|
42ad3723fd | ||
|
|
b268b6edf7 | ||
|
|
b84854af92 | ||
|
|
cfebb3358d | ||
|
|
93e1b13278 | ||
|
|
0d4bfb7bd5 | ||
|
|
0aa5a123b7 | ||
|
|
2e156d4621 | ||
|
|
fdcf6cb6e1 | ||
|
|
01f2ed0892 | ||
|
|
c9abfe5438 | ||
|
|
8f9771adce | ||
|
|
b34f60b1b0 | ||
|
|
0fe608f91e | ||
|
|
5d911ef93f | ||
|
|
1d06ff9bf0 | ||
|
|
107927f5a1 | ||
|
|
d6eb5e3511 |
8
.gitattributes
vendored
8
.gitattributes
vendored
@@ -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
|
||||
|
||||
26
.github/workflows/publish_libraries.yml
vendored
26
.github/workflows/publish_libraries.yml
vendored
@@ -1,26 +0,0 @@
|
||||
name: Publish library packages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'terminal-emulator/build.gradle'
|
||||
- 'terminal-view/build.gradle'
|
||||
- 'termux-shared/build.gradle'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Perform release build
|
||||
run: |
|
||||
./gradlew assembleRelease
|
||||
- name: Publish libraries on Github Packages
|
||||
env:
|
||||
GH_USERNAME: xeffyr
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
run: |
|
||||
./gradlew publish
|
||||
@@ -1,3 +1,6 @@
|
||||
Released under [the GPLv3 license](https://www.gnu.org/licenses/gpl.html).
|
||||
The `termux/termux-app` repository is released under [GPLv3 only](https://www.gnu.org/licenses/gpl-3.0.html) license.
|
||||
|
||||
Contains code from `Terminal Emulator for Android` by which is released under [the Apache License 2.0](https://www.apache.org/licenses/).
|
||||
### Exceptions
|
||||
|
||||
- [Terminal Emulator for Android](https://github.com/jackpal/Android-Terminal-Emulator) code is used which is released under [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). Check [`terminal-view`](terminal-view) and [`terminal-emulator`](terminal-emulator) libraries.
|
||||
- Check [`termux-shared/LICENSE.md`](termux-shared/LICENSE.md) for `termux-shared` library related exceptions.
|
||||
|
||||
145
README.md
145
README.md
@@ -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.
|
||||
|
||||
@@ -8,7 +8,7 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.annotation:annotation:1.2.0"
|
||||
implementation "androidx.core:core:1.5.0-rc01"
|
||||
implementation "androidx.core:core:1.6.0-rc01"
|
||||
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
|
||||
implementation "androidx.preference:preference:1.1.1"
|
||||
implementation "androidx.viewpager:viewpager:1.0.0"
|
||||
@@ -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 115
|
||||
versionName "0.115"
|
||||
|
||||
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.30-r1"
|
||||
downloadBootstrap("aarch64", "ce56ce9a4e8845bd1d35cc2695bbdd636c72625ee10ce21c9b98ab38ebbee5ab", version)
|
||||
downloadBootstrap("arm", "537e81951c7d3d3f3def9ce6778e1032457488e21edb2c037a1e0e680c39e747", version)
|
||||
downloadBootstrap("i686", "3c2ca858c0225671c00c44ac182e31819ffa93ec624e95e02824e7d6d30ca1b4", version)
|
||||
downloadBootstrap("x86_64", "93c50d36b45bca42bb014395e8e184e5b540adcad5d4e215f7e64ebf0d655d2b", version)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -103,7 +102,7 @@
|
||||
android:theme="@style/Theme.AppCompat.Light.DarkActionBar" />
|
||||
|
||||
<activity
|
||||
android:name=".app.activities.ReportActivity"
|
||||
android:name=".shared.activities.ReportActivity"
|
||||
android:theme="@style/Theme.AppCompat.TermuxReportActivity"
|
||||
android:documentLaunchMode="intoExisting"
|
||||
/>
|
||||
|
||||
@@ -10,6 +10,11 @@ import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.data.IntentUtils;
|
||||
import com.termux.shared.file.TermuxFileUtils;
|
||||
import com.termux.shared.models.errors.Errno;
|
||||
import com.termux.shared.models.errors.Error;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||
@@ -17,7 +22,6 @@ import com.termux.shared.file.FileUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.notification.NotificationUtils;
|
||||
import com.termux.app.utils.PluginUtils;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.models.ExecutionCommand;
|
||||
|
||||
/**
|
||||
@@ -60,29 +64,57 @@ public class RunCommandService extends Service {
|
||||
ExecutionCommand executionCommand = new ExecutionCommand();
|
||||
executionCommand.pluginAPIHelp = this.getString(R.string.error_run_command_service_api_help, RUN_COMMAND_SERVICE.RUN_COMMAND_API_HELP_URL);
|
||||
|
||||
Error error;
|
||||
String errmsg;
|
||||
|
||||
// If invalid action passed, then just return
|
||||
if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) {
|
||||
errmsg = this.getString(R.string.error_run_command_service_invalid_intent_action, intent.getAction());
|
||||
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
||||
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return Service.START_NOT_STICKY;
|
||||
}
|
||||
|
||||
executionCommand.executable = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH);
|
||||
executionCommand.arguments = intent.getStringArrayExtra(RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS);
|
||||
executionCommand.stdin = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_STDIN);
|
||||
executionCommand.workingDirectory = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_WORKDIR);
|
||||
executionCommand.executable = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH, null);
|
||||
|
||||
executionCommand.arguments = IntentUtils.getStringArrayExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS, null);
|
||||
|
||||
/*
|
||||
* If intent was sent with `am` command, then normal comma characters may have been replaced
|
||||
* with alternate characters if a normal comma existed in an argument itself to prevent it
|
||||
* splitting into multiple arguments by `am` command.
|
||||
* If `tudo` or `sudo` are used, then simply using their `-r` and `--comma-alternative` command
|
||||
* options can be used without passing the below extras, but native supports is helpful if
|
||||
* they are not being used.
|
||||
* https://github.com/agnostic-apollo/tudo#passing-arguments-using-run_command-intent
|
||||
* https://android.googlesource.com/platform/frameworks/base/+/21bdaf1/cmds/am/src/com/android/commands/am/Am.java#572
|
||||
*/
|
||||
boolean replaceCommaAlternativeCharsInArguments = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS, false);
|
||||
if (replaceCommaAlternativeCharsInArguments) {
|
||||
String commaAlternativeCharsInArguments = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS, null);
|
||||
if (commaAlternativeCharsInArguments == null)
|
||||
commaAlternativeCharsInArguments = TermuxConstants.COMMA_ALTERNATIVE;
|
||||
// Replace any commaAlternativeCharsInArguments characters with normal commas
|
||||
DataUtils.replaceSubStringsInStringArrayItems(executionCommand.arguments, commaAlternativeCharsInArguments, TermuxConstants.COMMA_NORMAL);
|
||||
}
|
||||
|
||||
executionCommand.stdin = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_STDIN, null);
|
||||
executionCommand.workingDirectory = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_WORKDIR, null);
|
||||
executionCommand.inBackground = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false);
|
||||
executionCommand.sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION);
|
||||
executionCommand.commandLabel = DataUtils.getDefaultIfNull(intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_LABEL), "RUN_COMMAND Execution Intent Command");
|
||||
executionCommand.commandDescription = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION);
|
||||
executionCommand.commandHelp = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_HELP);
|
||||
executionCommand.commandLabel = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_LABEL, "RUN_COMMAND Execution Intent Command");
|
||||
executionCommand.commandDescription = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION, null);
|
||||
executionCommand.commandHelp = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_HELP, null);
|
||||
executionCommand.isPluginExecutionCommand = true;
|
||||
executionCommand.pluginPendingIntent = intent.getParcelableExtra(RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT);
|
||||
|
||||
|
||||
executionCommand.resultConfig.resultPendingIntent = intent.getParcelableExtra(RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT);
|
||||
executionCommand.resultConfig.resultDirectoryPath = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_DIRECTORY, null);
|
||||
if (executionCommand.resultConfig.resultDirectoryPath != null) {
|
||||
executionCommand.resultConfig.resultSingleFile = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_RESULT_SINGLE_FILE, false);
|
||||
executionCommand.resultConfig.resultFileBasename = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_BASENAME, null);
|
||||
executionCommand.resultConfig.resultFileOutputFormat = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, null);
|
||||
executionCommand.resultConfig.resultFileErrorFormat = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, null);
|
||||
executionCommand.resultConfig.resultFilesSuffix = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILES_SUFFIX, null);
|
||||
}
|
||||
|
||||
// If "allow-external-apps" property to not set to "true", then just return
|
||||
// We enable force notifications if "allow-external-apps" policy is violated so that the
|
||||
@@ -91,7 +123,7 @@ public class RunCommandService extends Service {
|
||||
// also sent, then its creator is also logged and shown.
|
||||
errmsg = PluginUtils.checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(this);
|
||||
if (errmsg != null) {
|
||||
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
||||
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true);
|
||||
return Service.START_NOT_STICKY;
|
||||
}
|
||||
@@ -101,22 +133,22 @@ public class RunCommandService extends Service {
|
||||
// If executable is null or empty, then exit here instead of getting canonical path which would expand to "/"
|
||||
if (executionCommand.executable == null || executionCommand.executable.isEmpty()) {
|
||||
errmsg = this.getString(R.string.error_run_command_service_mandatory_extra_missing, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH);
|
||||
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
||||
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return Service.START_NOT_STICKY;
|
||||
}
|
||||
|
||||
// Get canonical path of executable
|
||||
executionCommand.executable = FileUtils.getCanonicalPath(executionCommand.executable, null, true);
|
||||
executionCommand.executable = TermuxFileUtils.getCanonicalPath(executionCommand.executable, null, true);
|
||||
|
||||
// If executable is not a regular file, or is not readable or executable, then just return
|
||||
// Setting of missing read and execute permissions is not done
|
||||
errmsg = FileUtils.validateRegularFileExistenceAndPermissions(this, "executable", executionCommand.executable, null,
|
||||
PluginUtils.PLUGIN_EXECUTABLE_FILE_PERMISSIONS, true, true,
|
||||
error = FileUtils.validateRegularFileExistenceAndPermissions("executable", executionCommand.executable, null,
|
||||
FileUtils.APP_EXECUTABLE_FILE_PERMISSIONS, true, true,
|
||||
false);
|
||||
if (errmsg != null) {
|
||||
errmsg += "\n" + this.getString(R.string.msg_executable_absolute_path, executionCommand.executable);
|
||||
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
||||
if (error != null) {
|
||||
error.appendMessage("\n" + this.getString(R.string.msg_executable_absolute_path, executionCommand.executable));
|
||||
executionCommand.setStateFailed(error);
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return Service.START_NOT_STICKY;
|
||||
}
|
||||
@@ -126,19 +158,19 @@ public class RunCommandService extends Service {
|
||||
// If workingDirectory is not null or empty
|
||||
if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) {
|
||||
// Get canonical path of workingDirectory
|
||||
executionCommand.workingDirectory = FileUtils.getCanonicalPath(executionCommand.workingDirectory, null, true);
|
||||
executionCommand.workingDirectory = TermuxFileUtils.getCanonicalPath(executionCommand.workingDirectory, null, true);
|
||||
|
||||
// If workingDirectory is not a directory, or is not readable or writable, then just return
|
||||
// Creation of missing directory and setting of read, write and execute permissions are only done if workingDirectory is
|
||||
// under {@link TermuxConstants#TERMUX_FILES_DIR_PATH}
|
||||
// under allowed termux working directory paths.
|
||||
// We try to set execute permissions, but ignore if they are missing, since only read and write permissions are required
|
||||
// for working directories.
|
||||
errmsg = FileUtils.validateDirectoryFileExistenceAndPermissions(this, "working", executionCommand.workingDirectory, TermuxConstants.TERMUX_FILES_DIR_PATH, true,
|
||||
PluginUtils.PLUGIN_WORKING_DIRECTORY_PERMISSIONS, true, true,
|
||||
true, true);
|
||||
if (errmsg != null) {
|
||||
errmsg += "\n" + this.getString(R.string.msg_working_directory_absolute_path, executionCommand.workingDirectory);
|
||||
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
||||
error = TermuxFileUtils.validateDirectoryFileExistenceAndPermissions("working", executionCommand.workingDirectory,
|
||||
true, true, true,
|
||||
false, true);
|
||||
if (error != null) {
|
||||
error.appendMessage("\n" + this.getString(R.string.msg_working_directory_absolute_path, executionCommand.workingDirectory));
|
||||
executionCommand.setStateFailed(error);
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return Service.START_NOT_STICKY;
|
||||
}
|
||||
@@ -146,7 +178,7 @@ public class RunCommandService extends Service {
|
||||
|
||||
|
||||
|
||||
executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(FileUtils.getExpandedTermuxPath(executionCommand.executable)).build();
|
||||
executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(TermuxFileUtils.getExpandedTermuxPath(executionCommand.executable)).build();
|
||||
|
||||
Logger.logVerbose(LOG_TAG, executionCommand.toString());
|
||||
|
||||
@@ -162,7 +194,15 @@ public class RunCommandService extends Service {
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, executionCommand.commandDescription);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_HELP, executionCommand.commandHelp);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP, executionCommand.pluginAPIHelp);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT, executionCommand.pluginPendingIntent);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT, executionCommand.resultConfig.resultPendingIntent);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_DIRECTORY, executionCommand.resultConfig.resultDirectoryPath);
|
||||
if (executionCommand.resultConfig.resultDirectoryPath != null) {
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_SINGLE_FILE, executionCommand.resultConfig.resultSingleFile);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_BASENAME, executionCommand.resultConfig.resultFileBasename);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, executionCommand.resultConfig.resultFileOutputFormat);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, executionCommand.resultConfig.resultFileErrorFormat);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILES_SUFFIX, executionCommand.resultConfig.resultFilesSuffix);
|
||||
}
|
||||
|
||||
// Start TERMUX_SERVICE and pass it execution intent
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
@@ -25,12 +26,14 @@ 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.ImageButton;
|
||||
import android.widget.ListView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.terminal.TermuxActivityRootView;
|
||||
import com.termux.shared.packages.PermissionUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
|
||||
import com.termux.app.activities.HelpActivity;
|
||||
@@ -42,7 +45,7 @@ import com.termux.app.terminal.TermuxTerminalSessionClient;
|
||||
import com.termux.app.terminal.TermuxTerminalViewClient;
|
||||
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
|
||||
import com.termux.app.settings.properties.TermuxAppSharedProperties;
|
||||
import com.termux.shared.interact.DialogUtils;
|
||||
import com.termux.shared.interact.TextInputDialogUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
@@ -69,7 +72,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 +80,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 +106,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 +142,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;
|
||||
@@ -145,8 +168,6 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
private static final int CONTEXT_MENU_SETTINGS_ID = 8;
|
||||
private static final int CONTEXT_MENU_REPORT_ID = 9;
|
||||
|
||||
private static final int REQUESTCODE_PERMISSION_STORAGE = 1234;
|
||||
|
||||
private static final String ARG_TERMINAL_TOOLBAR_TEXT_INPUT = "terminal_toolbar_text_input";
|
||||
|
||||
private static final String LOG_TAG = "TermuxActivity";
|
||||
@@ -155,13 +176,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);
|
||||
CrashUtils.notifyAppCrashOnLastRun(this, LOG_TAG);
|
||||
|
||||
// Load termux shared preferences and properties
|
||||
mPreferences = new TermuxAppSharedPreferences(this);
|
||||
// Load termux shared properties
|
||||
mProperties = new TermuxAppSharedProperties(this);
|
||||
|
||||
setActivityTheme();
|
||||
@@ -170,6 +191,20 @@ 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);
|
||||
mTermuxActivityRootView.setOnApplyWindowInsetsListener(new TermuxActivityRootView.WindowInsetsListener());
|
||||
|
||||
View content = findViewById(android.R.id.content);
|
||||
content.setOnApplyWindowInsetsListener((v, insets) -> {
|
||||
mNavBarHeight = insets.getSystemWindowInsetBottom();
|
||||
@@ -186,6 +221,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
|
||||
setTerminalToolbarView(savedInstanceState);
|
||||
|
||||
setSettingsButtonView();
|
||||
|
||||
setNewSessionButtonView();
|
||||
|
||||
setToggleKeyboardView();
|
||||
@@ -212,34 +249,92 @@ 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();
|
||||
|
||||
if (!mProperties.isTerminalMarginAdjustmentDisabled())
|
||||
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 +392,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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -347,14 +408,52 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
if (mProperties.isUsingBlackUI()) {
|
||||
findViewById(R.id.left_drawer).setBackgroundColor(ContextCompat.getColor(this,
|
||||
android.R.color.background_dark));
|
||||
((ImageButton) findViewById(R.id.settings_button)).setColorFilter(Color.WHITE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
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 +473,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);
|
||||
}
|
||||
|
||||
@@ -404,11 +503,18 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
|
||||
|
||||
|
||||
private void setSettingsButtonView() {
|
||||
ImageButton settingsButton = findViewById(R.id.settings_button);
|
||||
settingsButton.setOnClickListener(v -> {
|
||||
startActivity(new Intent(this, SettingsActivity.class));
|
||||
});
|
||||
}
|
||||
|
||||
private void setNewSessionButtonView() {
|
||||
View newSessionButton = findViewById(R.id.new_session_button);
|
||||
newSessionButton.setOnClickListener(v -> mTermuxTerminalSessionClient.addNewSession(false, null));
|
||||
newSessionButton.setOnLongClickListener(v -> {
|
||||
DialogUtils.textInput(TermuxActivity.this, R.string.title_create_named_session, null,
|
||||
TextInputDialogUtils.textInput(TermuxActivity.this, R.string.title_create_named_session, null,
|
||||
R.string.action_create_named_session_confirm, text -> mTermuxTerminalSessionClient.addNewSession(false, text),
|
||||
R.string.action_new_session_failsafe, text -> mTermuxTerminalSessionClient.addNewSession(true, text),
|
||||
-1, null, null);
|
||||
@@ -418,8 +524,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 +534,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 +585,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);
|
||||
@@ -552,7 +613,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
requestAutoFill();
|
||||
return true;
|
||||
case CONTEXT_MENU_RESET_TERMINAL_ID:
|
||||
resetSession(session);
|
||||
onResetTerminalSession(session);
|
||||
return true;
|
||||
case CONTEXT_MENU_KILL_PROCESS_ID:
|
||||
showKillSessionDialog(session);
|
||||
@@ -591,10 +652,13 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
b.show();
|
||||
}
|
||||
|
||||
private void resetSession(TerminalSession session) {
|
||||
private void onResetTerminalSession(TerminalSession session) {
|
||||
if (session != null) {
|
||||
session.reset();
|
||||
showToast(getResources().getString(R.string.msg_terminal_reset), true);
|
||||
|
||||
if (mTermuxTerminalSessionClient != null)
|
||||
mTermuxTerminalSessionClient.onResetTerminalSession();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -635,22 +699,22 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
* For processes to access shared internal storage (/sdcard) we need this permission.
|
||||
*/
|
||||
public boolean ensureStoragePermissionGranted() {
|
||||
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
|
||||
if (PermissionUtils.checkPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||
return true;
|
||||
} else {
|
||||
Logger.logDebug(LOG_TAG, "Storage permission not granted, requesting permission.");
|
||||
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUESTCODE_PERMISSION_STORAGE);
|
||||
Logger.logInfo(LOG_TAG, "Storage permission not granted, requesting permission.");
|
||||
PermissionUtils.requestPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE, PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
if (requestCode == REQUESTCODE_PERMISSION_STORAGE && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
Logger.logDebug(LOG_TAG, "Storage permission granted by user on request.");
|
||||
if (requestCode == PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
Logger.logInfo(LOG_TAG, "Storage permission granted by user on request.");
|
||||
TermuxInstaller.setupStorageSymlinks(this);
|
||||
} else {
|
||||
Logger.logDebug(LOG_TAG, "Storage permission denied by user on request.");
|
||||
Logger.logInfo(LOG_TAG, "Storage permission denied by user on request.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -660,6 +724,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 +752,10 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
return mIsVisible;
|
||||
}
|
||||
|
||||
public boolean isOnResumeAfterOnCreate() {
|
||||
return isOnResumeAfterOnCreate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public TermuxService getTermuxService() {
|
||||
@@ -690,6 +766,10 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
||||
return mTerminalView;
|
||||
}
|
||||
|
||||
public TermuxTerminalViewClient getTermuxTerminalViewClient() {
|
||||
return mTermuxTerminalViewClient;
|
||||
}
|
||||
|
||||
public TermuxTerminalSessionClient getTermuxTerminalSessionClient() {
|
||||
return mTermuxTerminalSessionClient;
|
||||
}
|
||||
@@ -757,7 +837,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 +845,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 +856,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.
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.termux.app;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import com.termux.shared.crash.CrashHandler;
|
||||
import com.termux.shared.crash.TermuxCrashUtils;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.logger.Logger;
|
||||
|
||||
@@ -12,7 +12,7 @@ public class TermuxApplication extends Application {
|
||||
super.onCreate();
|
||||
|
||||
// Set crash handler for the app
|
||||
CrashHandler.setCrashHandler(this);
|
||||
TermuxCrashUtils.setCrashHandler(this);
|
||||
|
||||
// Set log level for the app
|
||||
setLogLevel();
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -11,8 +11,11 @@ import android.util.Pair;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.utils.CrashUtils;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.shared.interact.MessageDialogUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.models.errors.Error;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
@@ -57,15 +60,27 @@ 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();
|
||||
MessageDialogUtils.exitAppWithErrorMessage(activity,
|
||||
activity.getString(R.string.bootstrap_error_title),
|
||||
bootstrapErrorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
final String PREFIX_FILE_PATH = TermuxConstants.TERMUX_PREFIX_DIR_PATH;
|
||||
final File PREFIX_FILE = TermuxConstants.TERMUX_PREFIX_DIR;
|
||||
if (PREFIX_FILE.isDirectory()) {
|
||||
whenDone.run();
|
||||
return;
|
||||
|
||||
// If prefix directory exists, even if its a symlink to a valid directory and symlink is not broken/dangling
|
||||
if (FileUtils.directoryFileExists(PREFIX_FILE_PATH, true)) {
|
||||
File[] PREFIX_FILE_LIST = PREFIX_FILE.listFiles();
|
||||
// If prefix directory is empty or only contains the tmp directory
|
||||
if(PREFIX_FILE_LIST == null || PREFIX_FILE_LIST.length == 0 || (PREFIX_FILE_LIST.length == 1 && TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH.equals(PREFIX_FILE_LIST[0].getAbsolutePath()))) {
|
||||
Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" exists but is empty or only contains the tmp directory.");
|
||||
} else {
|
||||
whenDone.run();
|
||||
return;
|
||||
}
|
||||
} else if (FileUtils.fileExists(PREFIX_FILE_PATH, false)) {
|
||||
Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" does not exist but another file exists at its destination.");
|
||||
}
|
||||
|
||||
final ProgressDialog progress = ProgressDialog.show(activity, null, activity.getString(R.string.bootstrap_installer_body), true, false);
|
||||
@@ -75,17 +90,26 @@ final class TermuxInstaller {
|
||||
try {
|
||||
Logger.logInfo(LOG_TAG, "Installing " + TermuxConstants.TERMUX_APP_NAME + " bootstrap packages.");
|
||||
|
||||
String errmsg;
|
||||
Error error;
|
||||
|
||||
final String STAGING_PREFIX_PATH = TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH;
|
||||
final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH);
|
||||
|
||||
errmsg = FileUtils.clearDirectory(activity, "prefix staging directory", STAGING_PREFIX_PATH);
|
||||
if (errmsg != null) {
|
||||
throw new RuntimeException(errmsg);
|
||||
// Delete prefix staging directory or any file at its destination
|
||||
error = FileUtils.deleteFile("prefix staging directory", STAGING_PREFIX_PATH, true);
|
||||
if (error != null) {
|
||||
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.logInfo(LOG_TAG, "Extracting bootstrap zip to prefix staging directory \"" + TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH + "\".");
|
||||
// Delete prefix directory or any file at its destination
|
||||
error = FileUtils.deleteFile("prefix directory", PREFIX_FILE_PATH, true);
|
||||
if (error != null) {
|
||||
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.logInfo(LOG_TAG, "Extracting bootstrap zip to prefix staging directory \"" + STAGING_PREFIX_PATH + "\".");
|
||||
|
||||
final byte[] buffer = new byte[8096];
|
||||
final List<Pair<String, String>> symlinks = new ArrayList<>(50);
|
||||
@@ -105,14 +129,22 @@ final class TermuxInstaller {
|
||||
String newPath = STAGING_PREFIX_PATH + "/" + parts[1];
|
||||
symlinks.add(Pair.create(oldPath, newPath));
|
||||
|
||||
ensureDirectoryExists(activity, new File(newPath).getParentFile());
|
||||
error = ensureDirectoryExists(new File(newPath).getParentFile());
|
||||
if (error != null) {
|
||||
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
String zipEntryName = zipEntry.getName();
|
||||
File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName);
|
||||
boolean isDirectory = zipEntry.isDirectory();
|
||||
|
||||
ensureDirectoryExists(activity, isDirectory ? targetFile : targetFile.getParentFile());
|
||||
error = ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile());
|
||||
if (error != null) {
|
||||
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDirectory) {
|
||||
try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
|
||||
@@ -143,22 +175,10 @@ final class TermuxInstaller {
|
||||
|
||||
Logger.logInfo(LOG_TAG, "Bootstrap packages installed successfully.");
|
||||
activity.runOnUiThread(whenDone);
|
||||
|
||||
} catch (final Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Bootstrap error", e);
|
||||
activity.runOnUiThread(() -> {
|
||||
try {
|
||||
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
|
||||
.setNegativeButton(R.string.bootstrap_error_abort, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
activity.finish();
|
||||
}).setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone);
|
||||
}).show();
|
||||
} catch (WindowManager.BadTokenException e1) {
|
||||
// Activity already dismissed - ignore.
|
||||
}
|
||||
});
|
||||
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)));
|
||||
|
||||
} finally {
|
||||
activity.runOnUiThread(() -> {
|
||||
try {
|
||||
@@ -172,6 +192,30 @@ final class TermuxInstaller {
|
||||
}.start();
|
||||
}
|
||||
|
||||
public static void showBootstrapErrorDialog(Activity activity, String PREFIX_FILE_PATH, Runnable whenDone, String message) {
|
||||
Logger.logErrorExtended(LOG_TAG, "Bootstrap Error:\n" + message);
|
||||
|
||||
// Send a notification with the exception so that the user knows why bootstrap setup failed
|
||||
CrashUtils.sendCrashReportNotification(activity, LOG_TAG, "## Bootstrap Error\n\n" + message, true);
|
||||
|
||||
activity.runOnUiThread(() -> {
|
||||
try {
|
||||
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
|
||||
.setNegativeButton(R.string.bootstrap_error_abort, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
activity.finish();
|
||||
})
|
||||
.setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
FileUtils.deleteFile("prefix directory", PREFIX_FILE_PATH, true);
|
||||
TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone);
|
||||
}).show();
|
||||
} catch (WindowManager.BadTokenException e1) {
|
||||
// Activity already dismissed - ignore.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void setupStorageSymlinks(final Context context) {
|
||||
final String LOG_TAG = "termux-storage";
|
||||
|
||||
@@ -180,12 +224,14 @@ final class TermuxInstaller {
|
||||
new Thread() {
|
||||
public void run() {
|
||||
try {
|
||||
String errmsg;
|
||||
Error error;
|
||||
File storageDir = TermuxConstants.TERMUX_STORAGE_HOME_DIR;
|
||||
|
||||
errmsg = FileUtils.clearDirectory(context, "~/storage", storageDir.getAbsolutePath());
|
||||
if (errmsg != null) {
|
||||
Logger.logErrorAndShowToast(context, LOG_TAG, errmsg);
|
||||
error = FileUtils.clearDirectory("~/storage", storageDir.getAbsolutePath());
|
||||
if (error != null) {
|
||||
Logger.logErrorAndShowToast(context, LOG_TAG, error.getMessage());
|
||||
Logger.logErrorExtended(LOG_TAG, "Setup Storage Error\n" + error.toString());
|
||||
CrashUtils.sendCrashReportNotification(context, LOG_TAG, "## Setup Storage Error\n\n" + Error.getErrorMarkdownString(error), true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -222,19 +268,16 @@ final class TermuxInstaller {
|
||||
|
||||
Logger.logInfo(LOG_TAG, "Storage symlinks created successfully.");
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Error setting up link", e);
|
||||
Logger.logErrorAndShowToast(context, LOG_TAG, e.getMessage());
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Setup Storage Error: Error setting up link", e);
|
||||
CrashUtils.sendCrashReportNotification(context, LOG_TAG, "## Setup Storage Error\n\n" + Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)), true);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
private static void ensureDirectoryExists(Context context, File directory) {
|
||||
String errmsg;
|
||||
|
||||
errmsg = FileUtils.createDirectoryFile(context, directory.getAbsolutePath());
|
||||
if (errmsg != null) {
|
||||
throw new RuntimeException(errmsg);
|
||||
}
|
||||
private static Error ensureDirectoryExists(File directory) {
|
||||
return FileUtils.createDirectoryFile(directory.getAbsolutePath());
|
||||
}
|
||||
|
||||
public static byte[] loadZipBytes() {
|
||||
|
||||
@@ -20,8 +20,14 @@ 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.data.IntentUtils;
|
||||
import com.termux.shared.models.errors.Errno;
|
||||
import com.termux.shared.shell.ShellUtils;
|
||||
import com.termux.shared.shell.TermuxShellEnvironmentClient;
|
||||
import com.termux.shared.shell.TermuxShellUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||
@@ -31,7 +37,6 @@ import com.termux.shared.terminal.TermuxTerminalSessionClientBase;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.notification.NotificationUtils;
|
||||
import com.termux.shared.packages.PermissionUtils;
|
||||
import com.termux.shared.shell.ShellUtils;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.models.ExecutionCommand;
|
||||
import com.termux.shared.shell.TermuxTask;
|
||||
@@ -106,6 +111,8 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
/** If the user has executed the {@link TERMUX_SERVICE#ACTION_STOP_SERVICE} intent. */
|
||||
boolean mWantsToStop = false;
|
||||
|
||||
public Integer mTerminalTranscriptRows;
|
||||
|
||||
private static final String LOG_TAG = "TermuxService";
|
||||
|
||||
@Override
|
||||
@@ -157,7 +164,7 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
public void onDestroy() {
|
||||
Logger.logVerbose(LOG_TAG, "onDestroy");
|
||||
|
||||
ShellUtils.clearTermuxTMPDIR(this);
|
||||
TermuxShellUtils.clearTermuxTMPDIR(true);
|
||||
|
||||
actionReleaseWakeLock(false);
|
||||
if (!mWantsToStop)
|
||||
@@ -251,22 +258,22 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
List<TermuxSession> termuxSessions = new ArrayList<>(mTermuxSessions);
|
||||
for (int i = 0; i < termuxSessions.size(); i++) {
|
||||
ExecutionCommand executionCommand = termuxSessions.get(i).getExecutionCommand();
|
||||
processResult = mWantsToStop || (executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null);
|
||||
processResult = mWantsToStop || executionCommand.isPluginExecutionCommandWithPendingResult();
|
||||
termuxSessions.get(i).killIfExecuting(this, processResult);
|
||||
}
|
||||
|
||||
List<TermuxTask> termuxTasks = new ArrayList<>(mTermuxTasks);
|
||||
for (int i = 0; i < termuxTasks.size(); i++) {
|
||||
ExecutionCommand executionCommand = termuxTasks.get(i).getExecutionCommand();
|
||||
if (executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null)
|
||||
if (executionCommand.isPluginExecutionCommandWithPendingResult())
|
||||
termuxTasks.get(i).killIfExecuting(this, true);
|
||||
}
|
||||
|
||||
List<ExecutionCommand> pendingPluginExecutionCommands = new ArrayList<>(mPendingPluginExecutionCommands);
|
||||
for (int i = 0; i < pendingPluginExecutionCommands.size(); i++) {
|
||||
ExecutionCommand executionCommand = pendingPluginExecutionCommands.get(i);
|
||||
if (!executionCommand.shouldNotProcessResults() && executionCommand.pluginPendingIntent != null) {
|
||||
if (executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_CANCELED, this.getString(com.termux.shared.R.string.error_execution_cancelled), null)) {
|
||||
if (!executionCommand.shouldNotProcessResults() && executionCommand.isPluginExecutionCommandWithPendingResult()) {
|
||||
if (executionCommand.setStateFailed(Errno.ERRNO_CANCELLED.getCode(), this.getString(com.termux.shared.R.string.error_execution_cancelled))) {
|
||||
PluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
|
||||
}
|
||||
}
|
||||
@@ -354,20 +361,28 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
|
||||
if (executionCommand.executableUri != null) {
|
||||
executionCommand.executable = executionCommand.executableUri.getPath();
|
||||
executionCommand.arguments = intent.getStringArrayExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS);
|
||||
executionCommand.arguments = IntentUtils.getStringArrayExtraIfSet(intent, TERMUX_SERVICE.EXTRA_ARGUMENTS, null);
|
||||
if (executionCommand.inBackground)
|
||||
executionCommand.stdin = intent.getStringExtra(TERMUX_SERVICE.EXTRA_STDIN);
|
||||
executionCommand.stdin = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_STDIN, null);
|
||||
}
|
||||
|
||||
executionCommand.workingDirectory = intent.getStringExtra(TERMUX_SERVICE.EXTRA_WORKDIR);
|
||||
executionCommand.workingDirectory = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_WORKDIR, null);
|
||||
executionCommand.isFailsafe = intent.getBooleanExtra(TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION, false);
|
||||
executionCommand.sessionAction = intent.getStringExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION);
|
||||
executionCommand.commandLabel = DataUtils.getDefaultIfNull(intent.getStringExtra(TERMUX_SERVICE.EXTRA_COMMAND_LABEL), "Execution Intent Command");
|
||||
executionCommand.commandDescription = intent.getStringExtra(TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION);
|
||||
executionCommand.commandHelp = intent.getStringExtra(TERMUX_SERVICE.EXTRA_COMMAND_HELP);
|
||||
executionCommand.pluginAPIHelp = intent.getStringExtra(TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP);
|
||||
executionCommand.commandLabel = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_LABEL, "Execution Intent Command");
|
||||
executionCommand.commandDescription = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, null);
|
||||
executionCommand.commandHelp = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_HELP, null);
|
||||
executionCommand.pluginAPIHelp = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP, null);
|
||||
executionCommand.isPluginExecutionCommand = true;
|
||||
executionCommand.pluginPendingIntent = intent.getParcelableExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT);
|
||||
executionCommand.resultConfig.resultPendingIntent = intent.getParcelableExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT);
|
||||
executionCommand.resultConfig.resultDirectoryPath = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_DIRECTORY, null);
|
||||
if (executionCommand.resultConfig.resultDirectoryPath != null) {
|
||||
executionCommand.resultConfig.resultSingleFile = intent.getBooleanExtra(TERMUX_SERVICE.EXTRA_RESULT_SINGLE_FILE, false);
|
||||
executionCommand.resultConfig.resultFileBasename = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILE_BASENAME, null);
|
||||
executionCommand.resultConfig.resultFileOutputFormat = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, null);
|
||||
executionCommand.resultConfig.resultFileErrorFormat = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, null);
|
||||
executionCommand.resultConfig.resultFilesSuffix = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILES_SUFFIX, null);
|
||||
}
|
||||
|
||||
// Add the execution command to pending plugin execution commands list
|
||||
mPendingPluginExecutionCommands.add(executionCommand);
|
||||
@@ -413,9 +428,14 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
if (Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE)
|
||||
Logger.logVerbose(LOG_TAG, executionCommand.toString());
|
||||
|
||||
TermuxTask newTermuxTask = TermuxTask.execute(this, executionCommand, this, false);
|
||||
TermuxTask newTermuxTask = TermuxTask.execute(this, executionCommand, this, new TermuxShellEnvironmentClient(), false);
|
||||
if (newTermuxTask == null) {
|
||||
Logger.logError(LOG_TAG, "Failed to execute new TermuxTask command for:\n" + executionCommand.getCommandIdAndLabelLogString());
|
||||
// If the execution command was started for a plugin, then process the error
|
||||
if (executionCommand.isPluginExecutionCommand)
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
else
|
||||
Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -503,9 +523,15 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
// If the execution command was started for a plugin, only then will the stdout be set
|
||||
// Otherwise if command was manually started by the user like by adding a new terminal session,
|
||||
// then no need to set stdout
|
||||
TermuxSession newTermuxSession = TermuxSession.execute(this, executionCommand, getTermuxTerminalSessionClient(), this, sessionName, executionCommand.isPluginExecutionCommand);
|
||||
executionCommand.terminalTranscriptRows = getTerminalTranscriptRows();
|
||||
TermuxSession newTermuxSession = TermuxSession.execute(this, executionCommand, getTermuxTerminalSessionClient(), this, new TermuxShellEnvironmentClient(), sessionName, executionCommand.isPluginExecutionCommand);
|
||||
if (newTermuxSession == null) {
|
||||
Logger.logError(LOG_TAG, "Failed to execute new TermuxSession command for:\n" + executionCommand.getCommandIdAndLabelLogString());
|
||||
// If the execution command was started for a plugin, then process the error
|
||||
if (executionCommand.isPluginExecutionCommand)
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
else
|
||||
Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -560,6 +586,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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -601,8 +640,13 @@ public final class TermuxService extends Service implements TermuxTask.TermuxTas
|
||||
// For android >= 10, apps require Display over other apps permission to start foreground activities
|
||||
// from background (services). If it is not granted, then TermuxSessions that are started will
|
||||
// show in Termux notification but will not run until user manually clicks the notification.
|
||||
if (PermissionUtils.validateDisplayOverOtherAppsPermissionForPostAndroid10(this)) {
|
||||
if (PermissionUtils.validateDisplayOverOtherAppsPermissionForPostAndroid10(this, true)) {
|
||||
TermuxActivity.startTermuxActivity(this);
|
||||
} else {
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(this);
|
||||
if (preferences == null) return;
|
||||
if (preferences.arePluginErrorNotificationsEnabled())
|
||||
Logger.showToast(this, this.getString(R.string.error_display_over_other_apps_permission_not_granted), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -743,8 +787,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
package com.termux.app.activities;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.activities.ReportActivity;
|
||||
import com.termux.shared.models.ReportInfo;
|
||||
import com.termux.app.models.UserAction;
|
||||
import com.termux.shared.interact.ShareUtils;
|
||||
import com.termux.shared.packages.PackageUtils;
|
||||
import com.termux.shared.settings.preferences.TermuxTaskerAppSharedPreferences;
|
||||
import com.termux.shared.termux.AndroidUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
|
||||
public class SettingsActivity extends AppCompatActivity {
|
||||
|
||||
@@ -36,7 +48,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(AndroidUtils.getDeviceInfoMarkdownString(context));
|
||||
aboutString.append("\n\n").append(TermuxUtils.getImportantLinksMarkdownString(context));
|
||||
|
||||
ReportActivity.startReportActivity(context, new ReportInfo(UserAction.ABOUT.getName(), TermuxConstants.TERMUX_APP.TERMUX_SETTINGS_ACTIVITY_NAME, title, null, aboutString.toString(), null, false));
|
||||
}
|
||||
}.start();
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void configureDonatePreference(@NonNull Context context) {
|
||||
Preference donatePreference = findPreference("donate");
|
||||
if (donatePreference != null) {
|
||||
String signingCertificateSHA256Digest = PackageUtils.getSigningCertificateSHA256DigestForPackage(context);
|
||||
if (signingCertificateSHA256Digest != null) {
|
||||
// If APK is a Google Playstore release, then do not show the donation link
|
||||
// since Termux isn't exempted from the playstore policy donation links restriction
|
||||
// Check Fund solicitations: https://pay.google.com/intl/en_in/about/policy/
|
||||
String apkRelease = TermuxUtils.getAPKRelease(signingCertificateSHA256Digest);
|
||||
if (apkRelease == null || apkRelease.equals(TermuxConstants.APK_RELEASE_GOOGLE_PLAYSTORE_SIGNING_CERTIFICATE_SHA256_DIGEST)) {
|
||||
donatePreference.setVisible(false);
|
||||
return;
|
||||
} else {
|
||||
donatePreference.setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
donatePreference.setOnPreferenceClickListener(preference -> {
|
||||
ShareUtils.openURL(context, TermuxConstants.TERMUX_DONATE_URL);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
package com.termux.app.terminal;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.inputmethodservice.InputMethodService;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.view.ViewUtils;
|
||||
|
||||
|
||||
/**
|
||||
* The {@link TermuxActivity} relies on {@link android.view.WindowManager.LayoutParams#SOFT_INPUT_ADJUST_RESIZE)}
|
||||
* set by {@link TermuxTerminalViewClient#setSoftKeyboardState(boolean, boolean)} to automatically
|
||||
* resize the view and push the terminal up when soft keyboard is opened. However, this does not
|
||||
* always work properly. When `enforce-char-based-input=true` is set in `termux.properties`
|
||||
* and {@link com.termux.view.TerminalView#onCreateInputConnection(EditorInfo)} sets the inputType
|
||||
* to `InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS`
|
||||
* instead of the default `InputType.TYPE_NULL` for termux, some keyboards may still show suggestions.
|
||||
* Gboard does too, but only when text is copied and clipboard suggestions **and** number keys row
|
||||
* toggles are enabled in its settings. When number keys row toggle is not enabled, Gboard will still
|
||||
* show the row but will switch it with suggestions if needed. If its enabled, then number keys row
|
||||
* is always shown and suggestions are shown in an additional row on top of it. This additional row is likely
|
||||
* part of the candidates view returned by the keyboard app in {@link InputMethodService#onCreateCandidatesView()}.
|
||||
*
|
||||
* With the above configuration, the additional clipboard suggestions row partially covers the
|
||||
* extra keys/terminal. Reopening the keyboard/activity does not fix the issue. This is either a bug
|
||||
* in the Android OS where it does not consider the candidate's view height in its calculation to push
|
||||
* up the view or because Gboard does not include the candidate's view height in the height reported
|
||||
* to android that should be used, hence causing an overlap.
|
||||
*
|
||||
* Gboard logs the following entry to `logcat` when its opened with or without the suggestions bar showing:
|
||||
* I/KeyboardViewUtil: KeyboardViewUtil.calculateMaxKeyboardBodyHeight():62 leave 500 height for app when screen height:2392, header height:176 and isFullscreenMode:false, so the max keyboard body height is:1716
|
||||
* where `keyboard_height = screen_height - height_for_app - header_height` (62 is a hardcoded value in Gboard source code and may be a version number)
|
||||
* So this may in fact be due to Gboard but https://stackoverflow.com/questions/57567272 suggests
|
||||
* otherwise. Another similar report https://stackoverflow.com/questions/66761661.
|
||||
* Also check https://github.com/termux/termux-app/issues/1539.
|
||||
*
|
||||
* This overlap may happen even without `enforce-char-based-input=true` for keyboards with extended layouts
|
||||
* like number row, etc.
|
||||
*
|
||||
* To fix these issues, `activity_termux.xml` has the constant 1sp transparent
|
||||
* `activity_termux_bottom_space_view` View at the bottom. This will appear as a line matching the
|
||||
* activity theme. When {@link TermuxActivity} {@link ViewTreeObserver.OnGlobalLayoutListener} is
|
||||
* called when any of the sub view layouts change, like keyboard opening/closing keyboard,
|
||||
* extra keys/input view switched, etc, we check if the bottom space view is visible or not.
|
||||
* If its not, then we add a margin to the bottom of the root view, so that the keyboard does not
|
||||
* overlap the extra keys/terminal, since the margin will push up the view. By default the margin
|
||||
* added is equal to the height of the hidden part of extra keys/terminal. For Gboard's case, the
|
||||
* hidden part equals the `header_height`. The updates to margins may cause a jitter in some cases
|
||||
* when the view is redrawn if the margin is incorrect, but logic has been implemented to avoid that.
|
||||
*/
|
||||
public class TermuxActivityRootView extends LinearLayout implements ViewTreeObserver.OnGlobalLayoutListener {
|
||||
|
||||
public TermuxActivity mActivity;
|
||||
public Integer marginBottom;
|
||||
public Integer lastMarginBottom;
|
||||
public long lastMarginBottomTime;
|
||||
public long lastMarginBottomExtraTime;
|
||||
|
||||
/** Log root view events. */
|
||||
private boolean ROOT_VIEW_LOGGING_ENABLED = false;
|
||||
|
||||
private static final String LOG_TAG = "TermuxActivityRootView";
|
||||
|
||||
private static int mStatusBarHeight;
|
||||
|
||||
public TermuxActivityRootView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public TermuxActivityRootView(Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public TermuxActivityRootView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public void setActivity(TermuxActivity activity) {
|
||||
mActivity = activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether root view logging is enabled or not.
|
||||
*
|
||||
* @param value The boolean value that defines the state.
|
||||
*/
|
||||
public void setIsRootViewLoggingEnabled(boolean value) {
|
||||
ROOT_VIEW_LOGGING_ENABLED = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
|
||||
if (marginBottom != null) {
|
||||
if (ROOT_VIEW_LOGGING_ENABLED)
|
||||
Logger.logVerbose(LOG_TAG, "onMeasure: Setting bottom margin to " + marginBottom);
|
||||
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) getLayoutParams();
|
||||
params.setMargins(0, 0, 0, marginBottom);
|
||||
setLayoutParams(params);
|
||||
marginBottom = null;
|
||||
requestLayout();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
if (mActivity == null || !mActivity.isVisible()) return;
|
||||
|
||||
View bottomSpaceView = mActivity.getTermuxActivityBottomSpaceView();
|
||||
if (bottomSpaceView == null) return;
|
||||
|
||||
boolean root_view_logging_enabled = ROOT_VIEW_LOGGING_ENABLED;
|
||||
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, ":\nonGlobalLayout:");
|
||||
|
||||
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) getLayoutParams();
|
||||
|
||||
// Get the position Rects of the bottom space view and the main window holding it
|
||||
Rect[] windowAndViewRects = ViewUtils.getWindowAndViewRects(bottomSpaceView, mStatusBarHeight);
|
||||
if (windowAndViewRects == null)
|
||||
return;
|
||||
|
||||
Rect windowAvailableRect = windowAndViewRects[0];
|
||||
Rect bottomSpaceViewRect = windowAndViewRects[1];
|
||||
|
||||
// If the bottomSpaceViewRect is inside the windowAvailableRect, then it must be completely visible
|
||||
//boolean isVisible = windowAvailableRect.contains(bottomSpaceViewRect); // rect.right comparison often fails in landscape
|
||||
boolean isVisible = ViewUtils.isRectAbove(windowAvailableRect, bottomSpaceViewRect);
|
||||
boolean isVisibleBecauseMargin = (windowAvailableRect.bottom == bottomSpaceViewRect.bottom) && params.bottomMargin > 0;
|
||||
boolean isVisibleBecauseExtraMargin = ((bottomSpaceViewRect.bottom - windowAvailableRect.bottom) < 0);
|
||||
|
||||
if (root_view_logging_enabled) {
|
||||
Logger.logVerbose(LOG_TAG, "windowAvailableRect " + ViewUtils.toRectString(windowAvailableRect) + ", bottomSpaceViewRect " + ViewUtils.toRectString(bottomSpaceViewRect));
|
||||
Logger.logVerbose(LOG_TAG, "windowAvailableRect.bottom " + windowAvailableRect.bottom +
|
||||
", bottomSpaceViewRect.bottom " +bottomSpaceViewRect.bottom +
|
||||
", diff " + (bottomSpaceViewRect.bottom - windowAvailableRect.bottom) + ", bottom " + params.bottomMargin +
|
||||
", isVisible " + windowAvailableRect.contains(bottomSpaceViewRect) + ", isRectAbove " + ViewUtils.isRectAbove(windowAvailableRect, bottomSpaceViewRect) +
|
||||
", isVisibleBecauseMargin " + isVisibleBecauseMargin + ", isVisibleBecauseExtraMargin " + isVisibleBecauseExtraMargin);
|
||||
}
|
||||
|
||||
// If the bottomSpaceViewRect is visible, then remove the margin if needed
|
||||
if (isVisible) {
|
||||
// If visible because of margin, i.e the bottom of bottomSpaceViewRect equals that of windowAvailableRect
|
||||
// and a margin has been added
|
||||
// Necessary so that we don't get stuck in an infinite loop since setting margin
|
||||
// will call OnGlobalLayoutListener again and next time bottom space view
|
||||
// will be visible and margin will be set to 0, which again will call
|
||||
// OnGlobalLayoutListener...
|
||||
// Calling addTermuxActivityRootViewGlobalLayoutListener with a delay fails to
|
||||
// set appropriate margins when views are changed quickly since some changes
|
||||
// may be missed.
|
||||
if (isVisibleBecauseMargin) {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Visible due to margin");
|
||||
|
||||
// Once the view has been redrawn with new margin, we set margin back to 0 so that
|
||||
// when next time onMeasure() is called, margin 0 is used. This is necessary for
|
||||
// cases when view has been redrawn with new margin because bottom space view was
|
||||
// hidden by keyboard and then view was redrawn again due to layout change (like
|
||||
// keyboard symbol view is switched to), android will add margin below its new position
|
||||
// if its greater than 0, which was already above the keyboard creating x2x margin.
|
||||
// Adding time check since moving split screen divider in landscape causes jitter
|
||||
// and prevents some infinite loops
|
||||
if ((System.currentTimeMillis() - lastMarginBottomTime) > 40) {
|
||||
lastMarginBottomTime = System.currentTimeMillis();
|
||||
marginBottom = 0;
|
||||
} else {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Ignoring restoring marginBottom to 0 since called to quickly");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
boolean setMargin = params.bottomMargin != 0;
|
||||
|
||||
// If visible because of extra margin, i.e the bottom of bottomSpaceViewRect is above that of windowAvailableRect
|
||||
// onGlobalLayout: windowAvailableRect 1408, bottomSpaceViewRect 1232, diff -176, bottom 0, isVisible true, isVisibleBecauseMargin false, isVisibleBecauseExtraMargin false
|
||||
// onGlobalLayout: Bottom margin already equals 0
|
||||
if (isVisibleBecauseExtraMargin) {
|
||||
// Adding time check since prevents infinite loops, like in landscape mode in freeform mode in Taskbar
|
||||
if ((System.currentTimeMillis() - lastMarginBottomExtraTime) > 40) {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Resetting margin since visible due to extra margin");
|
||||
lastMarginBottomExtraTime = System.currentTimeMillis();
|
||||
// lastMarginBottom must be invalid. May also happen when keyboards are changed.
|
||||
lastMarginBottom = null;
|
||||
setMargin = true;
|
||||
} else {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Ignoring resetting margin since visible due to extra margin since called to quickly");
|
||||
}
|
||||
}
|
||||
|
||||
if (setMargin) {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Setting bottom margin to 0");
|
||||
params.setMargins(0, 0, 0, 0);
|
||||
setLayoutParams(params);
|
||||
} else {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Bottom margin already equals 0");
|
||||
// This is done so that when next time onMeasure() is called, lastMarginBottom is used.
|
||||
// This is done since we **expect** the keyboard to have same dimensions next time layout
|
||||
// changes, so best set margin while view is drawn the first time, otherwise it will
|
||||
// cause a jitter when OnGlobalLayoutListener is called with margin 0 and it sets the
|
||||
// likely same lastMarginBottom again and requesting a redraw. Hopefully, this logic
|
||||
// works fine for all cases.
|
||||
marginBottom = lastMarginBottom;
|
||||
}
|
||||
}
|
||||
// ELse find the part of the extra keys/terminal that is hidden and add a margin accordingly
|
||||
else {
|
||||
int pxHidden = bottomSpaceViewRect.bottom - windowAvailableRect.bottom;
|
||||
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "pxHidden " + pxHidden + ", bottom " + params.bottomMargin);
|
||||
|
||||
boolean setMargin = params.bottomMargin != pxHidden;
|
||||
|
||||
// If invisible despite margin, i.e a margin was added, but the bottom of bottomSpaceViewRect
|
||||
// is still below that of windowAvailableRect, this will trigger OnGlobalLayoutListener
|
||||
// again, so that margins are set properly. May happen when toolbar/extra keys is disabled
|
||||
// and enabled from left drawer, just like case for isVisibleBecauseExtraMargin.
|
||||
// onMeasure: Setting bottom margin to 176
|
||||
// onGlobalLayout: windowAvailableRect 1232, bottomSpaceViewRect 1408, diff 176, bottom 176, isVisible false, isVisibleBecauseMargin false, isVisibleBecauseExtraMargin false
|
||||
// onGlobalLayout: Bottom margin already equals 176
|
||||
if (pxHidden > 0 && params.bottomMargin > 0) {
|
||||
if (pxHidden != params.bottomMargin) {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Force setting margin to 0 since not visible due to wrong margin");
|
||||
pxHidden = 0;
|
||||
} else {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Force setting margin since not visible despite required margin");
|
||||
}
|
||||
setMargin = true;
|
||||
}
|
||||
|
||||
if (pxHidden < 0) {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Force setting margin to 0 since new margin is negative");
|
||||
pxHidden = 0;
|
||||
}
|
||||
|
||||
|
||||
if (setMargin) {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Setting bottom margin to " + pxHidden);
|
||||
params.setMargins(0, 0, 0, pxHidden);
|
||||
setLayoutParams(params);
|
||||
lastMarginBottom = pxHidden;
|
||||
} else {
|
||||
if (root_view_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Bottom margin already equals " + pxHidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class WindowInsetsListener implements View.OnApplyWindowInsetsListener {
|
||||
@Override
|
||||
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
|
||||
mStatusBarHeight = WindowInsetsCompat.toWindowInsetsCompat(insets).getInsets(WindowInsetsCompat.Type.statusBars()).top;
|
||||
// Let view window handle insets however it wants
|
||||
return v.onApplyWindowInsets(insets);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import android.widget.ListView;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.shell.TermuxSession;
|
||||
import com.termux.shared.interact.DialogUtils;
|
||||
import com.termux.shared.interact.TextInputDialogUtils;
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.shared.terminal.TermuxTerminalSessionClientBase;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
@@ -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,58 @@ 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when mActivity.onResetTerminalSession() is called
|
||||
*/
|
||||
public void onResetTerminalSession() {
|
||||
// Ensure blinker starts again after reset if cursor blinking was disabled before reset like
|
||||
// with "tput civis" which would have called onTerminalCursorStateChange()
|
||||
mActivity.getTerminalView().setTerminalCursorBlinkerState(true, true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public Integer getTerminalCursorStyle() {
|
||||
return mActivity.getProperties().getTerminalCursorStyle();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Initialize and get mBellSoundPool */
|
||||
private synchronized SoundPool getBellSoundPool() {
|
||||
if (mBellSoundPool == null) {
|
||||
mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
|
||||
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
|
||||
|
||||
mBellSoundId = mBellSoundPool.load(mActivity, R.raw.bell, 1);
|
||||
}
|
||||
|
||||
return mBellSoundPool;
|
||||
}
|
||||
|
||||
/** Release mBellSoundPool resources */
|
||||
private synchronized void releaseBellSoundPool() {
|
||||
if (mBellSoundPool != null) {
|
||||
mBellSoundPool.release();
|
||||
mBellSoundPool = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Try switching to session. */
|
||||
@@ -155,12 +264,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 +289,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());
|
||||
}
|
||||
@@ -186,14 +301,17 @@ public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase
|
||||
public void renameSession(final TerminalSession sessionToRename) {
|
||||
if (sessionToRename == null) return;
|
||||
|
||||
DialogUtils.textInput(mActivity, R.string.title_rename_session, sessionToRename.mSessionName, R.string.action_rename_session_confirm, text -> {
|
||||
TextInputDialogUtils.textInput(mActivity, R.string.title_rename_session, sessionToRename.mSessionName, R.string.action_rename_session_confirm, text -> {
|
||||
sessionToRename.mSessionName = text;
|
||||
termuxSessionListNotifyUpdated();
|
||||
}, -1, null, -1, null, null);
|
||||
}
|
||||
|
||||
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 +324,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 +344,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 +362,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 +370,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 +403,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 +418,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)) {
|
||||
|
||||
@@ -14,17 +14,20 @@ 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;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.shared.data.UrlUtils;
|
||||
import com.termux.shared.shell.ShellUtils;
|
||||
import com.termux.shared.terminal.TermuxTerminalViewClientBase;
|
||||
import com.termux.shared.termux.AndroidUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.app.activities.ReportActivity;
|
||||
import com.termux.app.models.ReportInfo;
|
||||
import com.termux.shared.activities.ReportActivity;
|
||||
import com.termux.shared.models.ReportInfo;
|
||||
import com.termux.app.models.UserAction;
|
||||
import com.termux.app.terminal.io.KeyboardShortcut;
|
||||
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
|
||||
@@ -33,6 +36,8 @@ import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
import com.termux.shared.view.KeyboardUtils;
|
||||
import com.termux.shared.view.ViewUtils;
|
||||
import com.termux.terminal.KeyHandler;
|
||||
import com.termux.terminal.TerminalEmulator;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
@@ -53,11 +58,99 @@ 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);
|
||||
ViewUtils.setIsViewUtilsLoggingEnabled(isTerminalViewKeyLoggingEnabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when mActivity.onResume() is called
|
||||
*/
|
||||
public void onResume() {
|
||||
// Show the soft keyboard if required
|
||||
setSoftKeyboardState(true, false);
|
||||
|
||||
mTerminalCursorBlinkerStateAlreadySet = false;
|
||||
|
||||
if (mActivity.getTerminalView().mEmulator != null) {
|
||||
// Start terminal cursor blinking if enabled
|
||||
// If emulator is already set, then start blinker now, otherwise wait for onEmulatorSet()
|
||||
// event to start it. This is needed since onEmulatorSet() may not be called after
|
||||
// TermuxActivity is started after device display timeout with double tap and not power button.
|
||||
setTerminalCursorBlinkerState(true);
|
||||
mTerminalCursorBlinkerStateAlreadySet = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when mActivity.onStop() is called
|
||||
*/
|
||||
public void onStop() {
|
||||
// Stop terminal cursor blinking if enabled
|
||||
setTerminalCursorBlinkerState(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when mActivity.reloadActivityStyling() is called
|
||||
*/
|
||||
public void onReload() {
|
||||
// Show the soft keyboard if required
|
||||
setSoftKeyboardState(false, true);
|
||||
|
||||
// Start terminal cursor blinking if enabled
|
||||
setTerminalCursorBlinkerState(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when {@link com.termux.view.TerminalView#mEmulator}
|
||||
*/
|
||||
@Override
|
||||
public void onEmulatorSet() {
|
||||
if (!mTerminalCursorBlinkerStateAlreadySet) {
|
||||
// Start terminal cursor blinking if enabled
|
||||
// We need to wait for the first session to be attached that's set in
|
||||
// TermuxActivity.onServiceConnected() and then the multiple calls to TerminalView.updateSize()
|
||||
// where the final one eventually sets the mEmulator when width/height is not 0. Otherwise
|
||||
// blinker will not start again if TermuxActivity is started again after exiting it with
|
||||
// double back press. Check TerminalView.setTerminalCursorBlinkerState().
|
||||
setTerminalCursorBlinkerState(true);
|
||||
mTerminalCursorBlinkerStateAlreadySet = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public float onScale(float scale) {
|
||||
if (scale < 0.9f || scale > 1.1f) {
|
||||
@@ -72,8 +165,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 +217,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,8 +245,17 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent e) {
|
||||
// If emulator is not set, like if bootstrap installation failed and user dismissed the error
|
||||
// dialog, then just exit the activity, otherwise they will be stuck in a broken state.
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK && mActivity.getTerminalView().mEmulator == null) {
|
||||
mActivity.finishActivityIfNotFinishing();
|
||||
return true;
|
||||
}
|
||||
|
||||
return handleVirtualKeys(keyCode, e, false);
|
||||
}
|
||||
|
||||
@@ -338,6 +441,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 +595,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,7 +605,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
|
||||
String text = ShellUtils.getTerminalSessionTranscriptText(session, true, true);
|
||||
|
||||
LinkedHashSet<CharSequence> urlSet = DataUtils.extractUrls(text);
|
||||
LinkedHashSet<CharSequence> urlSet = UrlUtils.extractUrls(text);
|
||||
if (urlSet.isEmpty()) {
|
||||
new AlertDialog.Builder(mActivity).setMessage(R.string.title_select_url_none_found).show();
|
||||
return;
|
||||
@@ -405,26 +646,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(AndroidUtils.getDeviceInfoMarkdownString(mActivity));
|
||||
|
||||
String termuxAptInfo = TermuxUtils.geAPTInfoMarkdownString(mActivity);
|
||||
if (termuxAptInfo != null)
|
||||
reportString.append("\n\n").append(termuxAptInfo);
|
||||
|
||||
ReportActivity.startReportActivity(mActivity, new ReportInfo(UserAction.REPORT_ISSUE_FROM_TRANSCRIPT.getName(), TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY_NAME, title, null, reportString.toString(), "\n\n" + TermuxUtils.getReportIssueMarkdownString(mActivity), false));
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
public void doPaste() {
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,11 +9,13 @@ import android.content.Intent;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.activities.ReportActivity;
|
||||
import com.termux.shared.activities.ReportActivity;
|
||||
import com.termux.shared.models.errors.Error;
|
||||
import com.termux.shared.notification.NotificationUtils;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.app.models.ReportInfo;
|
||||
import com.termux.shared.models.ReportInfo;
|
||||
import com.termux.app.models.UserAction;
|
||||
import com.termux.shared.notification.TermuxNotificationUtils;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.settings.preferences.TermuxPreferenceConstants;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
@@ -29,8 +31,8 @@ public class CrashUtils {
|
||||
private static final String LOG_TAG = "CrashUtils";
|
||||
|
||||
/**
|
||||
* Notify the user of a previous app crash by reading the crash info from the crash log file at
|
||||
* {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}. The crash log file would have been
|
||||
* Notify the user of an app crash at last run by reading the crash info from the crash log file
|
||||
* at {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}. The crash log file would have been
|
||||
* created by {@link com.termux.shared.crash.CrashHandler}.
|
||||
*
|
||||
* If the crash log file exists and is not empty and
|
||||
@@ -43,13 +45,14 @@ public class CrashUtils {
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param logTagParam The log tag to use for logging.
|
||||
*/
|
||||
public static void notifyCrash(final Context context, final String logTagParam) {
|
||||
public static void notifyAppCrashOnLastRun(final Context context, final String logTagParam) {
|
||||
if (context == null) return;
|
||||
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
||||
if (preferences == null) return;
|
||||
|
||||
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(context);
|
||||
// If user has disabled notifications for crashes
|
||||
if (!preferences.getCrashReportNotificationsEnabled())
|
||||
if (!preferences.areCrashReportNotificationsEnabled())
|
||||
return;
|
||||
|
||||
new Thread() {
|
||||
@@ -60,52 +63,81 @@ public class CrashUtils {
|
||||
if (!FileUtils.regularFileExists(TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, false))
|
||||
return;
|
||||
|
||||
String errmsg;
|
||||
Error error;
|
||||
StringBuilder reportStringBuilder = new StringBuilder();
|
||||
|
||||
// Read report string from crash log file
|
||||
errmsg = FileUtils.readStringFromFile(context, "crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, Charset.defaultCharset(), reportStringBuilder, false);
|
||||
if (errmsg != null) {
|
||||
Logger.logError(logTag, errmsg);
|
||||
error = FileUtils.readStringFromFile("crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, Charset.defaultCharset(), reportStringBuilder, false);
|
||||
if (error != null) {
|
||||
Logger.logErrorExtended(logTag, error.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
// Move crash log file to backup location if it exists
|
||||
FileUtils.moveRegularFile(context, "crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, TermuxConstants.TERMUX_CRASH_LOG_BACKUP_FILE_PATH, true);
|
||||
if (errmsg != null) {
|
||||
Logger.logError(logTag, errmsg);
|
||||
error = FileUtils.moveRegularFile("crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, TermuxConstants.TERMUX_CRASH_LOG_BACKUP_FILE_PATH, true);
|
||||
if (error != null) {
|
||||
Logger.logErrorExtended(logTag, error.toString());
|
||||
}
|
||||
|
||||
String reportString = reportStringBuilder.toString();
|
||||
|
||||
if (reportString == null || reportString.isEmpty())
|
||||
if (reportString.isEmpty())
|
||||
return;
|
||||
|
||||
// Send a notification to show the crash log which when clicked will open the {@link ReportActivity}
|
||||
// to show the details of the crash
|
||||
String title = TermuxConstants.TERMUX_APP_NAME + " Crash Report";
|
||||
Logger.logDebug(logTag, "A crash log file found at \"" + TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH + "\".");
|
||||
|
||||
Logger.logDebug(logTag, "The crash log file at \"" + TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH + "\" found. Sending \"" + title + "\" notification.");
|
||||
|
||||
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.CRASH_REPORT, logTag, title, null, reportString, "\n\n" + TermuxUtils.getReportIssueMarkdownString(context), true));
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
// Setup the notification channel if not already set up
|
||||
setupCrashReportsNotificationChannel(context);
|
||||
|
||||
// Build the notification
|
||||
Notification.Builder builder = getCrashReportsNotificationBuilder(context, title, null, null, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
|
||||
if (builder == null) return;
|
||||
|
||||
// Send the notification
|
||||
int nextNotificationId = NotificationUtils.getNextNotificationId(context);
|
||||
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
|
||||
if (notificationManager != null)
|
||||
notificationManager.notify(nextNotificationId, builder.build());
|
||||
sendCrashReportNotification(context, logTag, reportString, false);
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a crash report notification for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID}
|
||||
* and {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param logTag The log tag to use for logging.
|
||||
* @param reportString The text for the crash report.
|
||||
* @param forceNotification If set to {@code true}, then a notification will be shown
|
||||
* regardless of if pending intent is {@code null} or
|
||||
* {@link TermuxPreferenceConstants.TERMUX_APP#KEY_CRASH_REPORT_NOTIFICATIONS_ENABLED}
|
||||
* is {@code false}.
|
||||
*/
|
||||
public static void sendCrashReportNotification(final Context context, String logTag, String reportString, boolean forceNotification) {
|
||||
if (context == null) return;
|
||||
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
||||
if (preferences == null) return;
|
||||
|
||||
// If user has disabled notifications for crashes
|
||||
if (!preferences.areCrashReportNotificationsEnabled() && !forceNotification)
|
||||
return;
|
||||
|
||||
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||
|
||||
// Send a notification to show the crash log which when clicked will open the {@link ReportActivity}
|
||||
// to show the details of the crash
|
||||
String title = TermuxConstants.TERMUX_APP_NAME + " Crash Report";
|
||||
|
||||
Logger.logDebug(logTag, "Sending \"" + title + "\" notification.");
|
||||
|
||||
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.CRASH_REPORT.getName(), logTag, title, null, reportString, "\n\n" + TermuxUtils.getReportIssueMarkdownString(context), true));
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
// Setup the notification channel if not already set up
|
||||
setupCrashReportsNotificationChannel(context);
|
||||
|
||||
// Build the notification
|
||||
Notification.Builder builder = getCrashReportsNotificationBuilder(context, title, null, null, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
|
||||
if (builder == null) return;
|
||||
|
||||
// Send the notification
|
||||
int nextNotificationId = TermuxNotificationUtils.getNextNotificationId(context);
|
||||
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
|
||||
if (notificationManager != null)
|
||||
notificationManager.notify(nextNotificationId, builder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get {@link Notification.Builder} for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID}
|
||||
* and {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}.
|
||||
|
||||
@@ -1,26 +1,33 @@
|
||||
package com.termux.app.utils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.activities.ReportActivity;
|
||||
import com.termux.shared.file.TermuxFileUtils;
|
||||
import com.termux.shared.models.ResultConfig;
|
||||
import com.termux.shared.models.ResultData;
|
||||
import com.termux.shared.models.errors.Errno;
|
||||
import com.termux.shared.models.errors.Error;
|
||||
import com.termux.shared.notification.NotificationUtils;
|
||||
import com.termux.shared.notification.TermuxNotificationUtils;
|
||||
import com.termux.shared.shell.ResultSender;
|
||||
import com.termux.shared.shell.ShellUtils;
|
||||
import com.termux.shared.termux.AndroidUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||
import com.termux.app.activities.ReportActivity;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_APP;
|
||||
import com.termux.shared.settings.properties.SharedProperties;
|
||||
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.app.models.ReportInfo;
|
||||
import com.termux.shared.models.ReportInfo;
|
||||
import com.termux.shared.models.ExecutionCommand;
|
||||
import com.termux.app.models.UserAction;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
@@ -29,12 +36,6 @@ import com.termux.shared.termux.TermuxUtils;
|
||||
|
||||
public class PluginUtils {
|
||||
|
||||
/** Required file permissions for the executable file of execute intent. Executable file must have read and execute permissions */
|
||||
public static final String PLUGIN_EXECUTABLE_FILE_PERMISSIONS = "r-x"; // Default: "r-x"
|
||||
/** Required file permissions for the working directory of execute intent. Working directory must have read and write permissions.
|
||||
* Execute permissions should be attempted to be set, but ignored if they are missing */
|
||||
public static final String PLUGIN_WORKING_DIRECTORY_PERMISSIONS = "rwx"; // Default: "rwx"
|
||||
|
||||
private static final String LOG_TAG = "PluginUtils";
|
||||
|
||||
/**
|
||||
@@ -43,8 +44,8 @@ public class PluginUtils {
|
||||
* The ExecutionCommand currentState must be greater or equal to
|
||||
* {@link ExecutionCommand.ExecutionState#EXECUTED}.
|
||||
* If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and
|
||||
* {@link ExecutionCommand#pluginPendingIntent} is not {@code null}, then the result of commands
|
||||
* are sent back to the {@link PendingIntent} creator.
|
||||
* {@link ResultConfig#resultPendingIntent} or {@link ResultConfig#resultDirectoryPath}
|
||||
* is not {@code null}, then the result of commands are sent back to the command caller.
|
||||
*
|
||||
* @param context The {@link Context} that will be used to send result intent to the {@link PendingIntent} creator.
|
||||
* @param logTag The log tag to use for logging.
|
||||
@@ -54,31 +55,42 @@ public class PluginUtils {
|
||||
if (executionCommand == null) return;
|
||||
|
||||
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||
Error error = null;
|
||||
ResultData resultData = executionCommand.resultData;
|
||||
|
||||
if (!executionCommand.hasExecuted()) {
|
||||
Logger.logWarn(logTag, "Ignoring call to processPluginExecutionCommandResult() since the execution command state is not higher than the ExecutionState.EXECUTED");
|
||||
Logger.logWarn(logTag, executionCommand.getCommandIdAndLabelLogString() + ": Ignoring call to processPluginExecutionCommandResult() since the execution command state is not higher than the ExecutionState.EXECUTED");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.logDebug(LOG_TAG, executionCommand.toString());
|
||||
boolean isPluginExecutionCommandWithPendingResult = executionCommand.isPluginExecutionCommandWithPendingResult();
|
||||
|
||||
boolean result = true;
|
||||
// Log the output. ResultData should not be logged if pending result since ResultSender will do it
|
||||
Logger.logDebugExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, !isPluginExecutionCommandWithPendingResult));
|
||||
|
||||
// If isPluginExecutionCommand is true and pluginPendingIntent is not null, then
|
||||
// send pluginPendingIntent to its creator with the result
|
||||
if (executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null) {
|
||||
String errmsg = executionCommand.errmsg;
|
||||
// If execution command was started by a plugin which expects the result back
|
||||
if (isPluginExecutionCommandWithPendingResult) {
|
||||
// Set variables which will be used by sendCommandResultData to send back the result
|
||||
if (executionCommand.resultConfig.resultPendingIntent != null)
|
||||
setPluginResultPendingIntentVariables(executionCommand);
|
||||
if (executionCommand.resultConfig.resultDirectoryPath != null)
|
||||
setPluginResultDirectoryVariables(executionCommand);
|
||||
|
||||
//Combine errmsg and stacktraces
|
||||
if (executionCommand.isStateFailed()) {
|
||||
errmsg = Logger.getMessageAndStackTracesString(executionCommand.errmsg, executionCommand.throwableList);
|
||||
// Send result to caller
|
||||
error = ResultSender.sendCommandResultData(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.resultConfig, executionCommand.resultData);
|
||||
if (error != null) {
|
||||
// error will be added to existing Errors
|
||||
resultData.setStateFailed(error);
|
||||
Logger.logDebugExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, true));
|
||||
|
||||
// Flash and send notification for the error
|
||||
Logger.showToast(context, ResultData.getErrorsListMinimalString(resultData), true);
|
||||
sendPluginCommandErrorNotification(context, logTag, executionCommand, ResultData.getErrorsListMinimalString(resultData));
|
||||
}
|
||||
|
||||
// Send pluginPendingIntent to its creator
|
||||
result = sendPluginExecutionCommandResultPendingIntent(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.stdout, executionCommand.stderr, executionCommand.exitCode, executionCommand.errCode, errmsg, executionCommand.pluginPendingIntent);
|
||||
}
|
||||
|
||||
if (!executionCommand.isStateFailed() && result)
|
||||
if (!executionCommand.isStateFailed() && error == null)
|
||||
executionCommand.setState(ExecutionCommand.ExecutionState.SUCCESS);
|
||||
}
|
||||
|
||||
@@ -86,14 +98,13 @@ public class PluginUtils {
|
||||
* Process {@link ExecutionCommand} error.
|
||||
*
|
||||
* The ExecutionCommand currentState must be equal to {@link ExecutionCommand.ExecutionState#FAILED}.
|
||||
* The {@link ExecutionCommand#errCode} must have been set to a value greater than
|
||||
* {@link ExecutionCommand#RESULT_CODE_OK}.
|
||||
* The {@link ExecutionCommand#errmsg} and any {@link ExecutionCommand#throwableList} must also
|
||||
* be set with appropriate error info.
|
||||
* The {@link ResultData#getErrCode()} must have been set to a value greater than
|
||||
* {@link Errno#ERRNO_SUCCESS}.
|
||||
* The {@link ResultData#errorsList} must also be set with appropriate error info.
|
||||
*
|
||||
* If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and
|
||||
* {@link ExecutionCommand#pluginPendingIntent} is not {@code null}, then the errors of commands
|
||||
* are sent back to the {@link PendingIntent} creator.
|
||||
* {@link ResultConfig#resultPendingIntent} or {@link ResultConfig#resultDirectoryPath}
|
||||
* is not {@code null}, then the errors of commands are sent back to the command caller.
|
||||
*
|
||||
* Otherwise if the {@link TERMUX_APP#KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED} is
|
||||
* enabled, then a flash and a notification will be shown for the error as well
|
||||
@@ -112,42 +123,93 @@ public class PluginUtils {
|
||||
if (context == null || executionCommand == null) return;
|
||||
|
||||
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||
Error error;
|
||||
ResultData resultData = executionCommand.resultData;
|
||||
|
||||
if (!executionCommand.isStateFailed()) {
|
||||
Logger.logWarn(logTag, "Ignoring call to processPluginExecutionCommandError() since the execution command is not in ExecutionState.FAILED");
|
||||
Logger.logWarn(logTag, executionCommand.getCommandIdAndLabelLogString() + ": Ignoring call to processPluginExecutionCommandError() since the execution command is not in ExecutionState.FAILED");
|
||||
return;
|
||||
}
|
||||
|
||||
// Log the error and any exception
|
||||
Logger.logStackTracesWithMessage(logTag, "(" + executionCommand.errCode + ") " + executionCommand.errmsg, executionCommand.throwableList);
|
||||
boolean isPluginExecutionCommandWithPendingResult = executionCommand.isPluginExecutionCommandWithPendingResult();
|
||||
|
||||
// Log the error and any exception. ResultData should not be logged if pending result since ResultSender will do it
|
||||
Logger.logErrorExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, !isPluginExecutionCommandWithPendingResult));
|
||||
|
||||
// If isPluginExecutionCommand is true and pluginPendingIntent is not null, then
|
||||
// send pluginPendingIntent to its creator with the errors
|
||||
if (executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null) {
|
||||
String errmsg = executionCommand.errmsg;
|
||||
// If execution command was started by a plugin which expects the result back
|
||||
if (isPluginExecutionCommandWithPendingResult) {
|
||||
// Set variables which will be used by sendCommandResultData to send back the result
|
||||
if (executionCommand.resultConfig.resultPendingIntent != null)
|
||||
setPluginResultPendingIntentVariables(executionCommand);
|
||||
if (executionCommand.resultConfig.resultDirectoryPath != null)
|
||||
setPluginResultDirectoryVariables(executionCommand);
|
||||
|
||||
//Combine errmsg and stacktraces
|
||||
if (executionCommand.isStateFailed()) {
|
||||
errmsg = Logger.getMessageAndStackTracesString(executionCommand.errmsg, executionCommand.throwableList);
|
||||
// Send result to caller
|
||||
error = ResultSender.sendCommandResultData(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.resultConfig, executionCommand.resultData);
|
||||
if (error != null) {
|
||||
// error will be added to existing Errors
|
||||
resultData.setStateFailed(error);
|
||||
Logger.logErrorExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, true));
|
||||
forceNotification = true;
|
||||
}
|
||||
|
||||
sendPluginExecutionCommandResultPendingIntent(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.stdout, executionCommand.stderr, executionCommand.exitCode, executionCommand.errCode, errmsg, executionCommand.pluginPendingIntent);
|
||||
|
||||
// No need to show notifications if a pending intent was sent, let the caller handle the result himself
|
||||
if (!forceNotification) return;
|
||||
}
|
||||
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
||||
if (preferences == null) return;
|
||||
|
||||
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(context);
|
||||
// If user has disabled notifications for plugin, then just return
|
||||
if (!preferences.getPluginErrorNotificationsEnabled() && !forceNotification)
|
||||
// If user has disabled notifications for plugin commands, then just return
|
||||
if (!preferences.arePluginErrorNotificationsEnabled() && !forceNotification)
|
||||
return;
|
||||
|
||||
// Flash the errmsg
|
||||
Logger.showToast(context, executionCommand.errmsg, true);
|
||||
// Flash and send notification for the error
|
||||
Logger.showToast(context, ResultData.getErrorsListMinimalString(resultData), true);
|
||||
sendPluginCommandErrorNotification(context, logTag, executionCommand, ResultData.getErrorsListMinimalString(resultData));
|
||||
|
||||
// Send a notification to show the errmsg which when clicked will open the {@link ReportActivity}
|
||||
}
|
||||
|
||||
/** Set variables which will be used by {@link ResultSender#sendCommandResultData(Context, String, String, ResultConfig, ResultData)}
|
||||
* to send back the result via {@link ResultConfig#resultPendingIntent}. */
|
||||
public static void setPluginResultPendingIntentVariables(ExecutionCommand executionCommand) {
|
||||
ResultConfig resultConfig = executionCommand.resultConfig;
|
||||
|
||||
resultConfig.resultBundleKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE;
|
||||
resultConfig.resultStdoutKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT;
|
||||
resultConfig.resultStdoutOriginalLengthKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH;
|
||||
resultConfig.resultStderrKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR;
|
||||
resultConfig.resultStderrOriginalLengthKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH;
|
||||
resultConfig.resultExitCodeKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE;
|
||||
resultConfig.resultErrCodeKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERR;
|
||||
resultConfig.resultErrmsgKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG;
|
||||
}
|
||||
|
||||
/** Set variables which will be used by {@link ResultSender#sendCommandResultData(Context, String, String, ResultConfig, ResultData)}
|
||||
* to send back the result by writing it to files in {@link ResultConfig#resultDirectoryPath}. */
|
||||
public static void setPluginResultDirectoryVariables(ExecutionCommand executionCommand) {
|
||||
ResultConfig resultConfig = executionCommand.resultConfig;
|
||||
|
||||
resultConfig.resultDirectoryPath = TermuxFileUtils.getCanonicalPath(resultConfig.resultDirectoryPath, null, true);
|
||||
resultConfig.resultDirectoryAllowedParentPath = TermuxFileUtils.getMatchedAllowedTermuxWorkingDirectoryParentPathForPath(resultConfig.resultDirectoryPath);
|
||||
|
||||
// Set default resultFileBasename if resultSingleFile is true to `<executable_basename>-<timestamp>.log`
|
||||
if (resultConfig.resultSingleFile && resultConfig.resultFileBasename == null)
|
||||
resultConfig.resultFileBasename = ShellUtils.getExecutableBasename(executionCommand.executable) + "-" + AndroidUtils.getCurrentMilliSecondLocalTimeStamp() + ".log";
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Send an error notification for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
|
||||
* and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param executionCommand The {@link ExecutionCommand} that failed.
|
||||
* @param notificationTextString The text of the notification.
|
||||
*/
|
||||
public static void sendPluginCommandErrorNotification(Context context, String logTag, ExecutionCommand executionCommand, String notificationTextString) {
|
||||
// Send a notification to show the error which when clicked will open the ReportActivity
|
||||
// to show the details of the error
|
||||
String title = TermuxConstants.TERMUX_APP_NAME + " Plugin Execution Command Error";
|
||||
|
||||
@@ -155,114 +217,29 @@ public class PluginUtils {
|
||||
|
||||
reportString.append(ExecutionCommand.getExecutionCommandMarkdownString(executionCommand));
|
||||
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, true));
|
||||
reportString.append("\n\n").append(TermuxUtils.getDeviceInfoMarkdownString(context));
|
||||
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context));
|
||||
|
||||
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.PLUGIN_EXECUTION_COMMAND, logTag, title, null, reportString.toString(), null,true));
|
||||
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.PLUGIN_EXECUTION_COMMAND.getName(), logTag, title, null, reportString.toString(), null,true));
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
// Setup the notification channel if not already set up
|
||||
setupPluginCommandErrorsNotificationChannel(context);
|
||||
|
||||
// Use markdown in notification
|
||||
CharSequence notificationText = MarkdownUtils.getSpannedMarkdownText(context, executionCommand.errmsg);
|
||||
//CharSequence notificationText = executionCommand.errmsg;
|
||||
CharSequence notificationTextCharSequence = MarkdownUtils.getSpannedMarkdownText(context, notificationTextString);
|
||||
//CharSequence notificationTextCharSequence = notificationTextString;
|
||||
|
||||
// Build the notification
|
||||
Notification.Builder builder = getPluginCommandErrorsNotificationBuilder(context, title, notificationText, notificationText, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
|
||||
if (builder == null) return;
|
||||
Notification.Builder builder = getPluginCommandErrorsNotificationBuilder(context, title, notificationTextCharSequence, notificationTextCharSequence, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
|
||||
if (builder == null) return;
|
||||
|
||||
// Send the notification
|
||||
int nextNotificationId = NotificationUtils.getNextNotificationId(context);
|
||||
int nextNotificationId = TermuxNotificationUtils.getNextNotificationId(context);
|
||||
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
|
||||
if (notificationManager != null)
|
||||
notificationManager.notify(nextNotificationId, builder.build());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Send {@link ExecutionCommand} result {@link PendingIntent} in the
|
||||
* {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE} bundle.
|
||||
*
|
||||
*
|
||||
* @param context The {@link Context} that will be used to send result intent to the {@link PendingIntent} creator.
|
||||
* @param logTag The log tag to use for logging.
|
||||
* @param label The label of {@link ExecutionCommand}.
|
||||
* @param stdout The stdout of {@link ExecutionCommand}.
|
||||
* @param stderr The stderr of {@link ExecutionCommand}.
|
||||
* @param exitCode The exitCode of {@link ExecutionCommand}.
|
||||
* @param errCode The errCode of {@link ExecutionCommand}.
|
||||
* @param errmsg The errmsg of {@link ExecutionCommand}.
|
||||
* @param pluginPendingIntent The pluginPendingIntent of {@link ExecutionCommand}.
|
||||
* @return Returns {@code true} if pluginPendingIntent was successfully send, otherwise [@code false}.
|
||||
*/
|
||||
public static boolean sendPluginExecutionCommandResultPendingIntent(Context context, String logTag, String label, String stdout, String stderr, Integer exitCode, Integer errCode, String errmsg, PendingIntent pluginPendingIntent) {
|
||||
if (context == null || pluginPendingIntent == null) return false;
|
||||
|
||||
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||
|
||||
Logger.logDebug(logTag, "Sending execution result for Execution Command \"" + label + "\" to " + pluginPendingIntent.getCreatorPackage());
|
||||
|
||||
String truncatedStdout = null;
|
||||
String truncatedStderr = null;
|
||||
|
||||
String stdoutOriginalLength = (stdout == null) ? null: String.valueOf(stdout.length());
|
||||
String stderrOriginalLength = (stderr == null) ? null: String.valueOf(stderr.length());
|
||||
|
||||
// Truncate stdout and stdout to max TRANSACTION_SIZE_LIMIT_IN_BYTES
|
||||
if (stderr == null || stderr.isEmpty()) {
|
||||
truncatedStdout = DataUtils.getTruncatedCommandOutput(stdout, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false);
|
||||
} else if (stdout == null || stdout.isEmpty()) {
|
||||
truncatedStderr = DataUtils.getTruncatedCommandOutput(stderr, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false);
|
||||
} else {
|
||||
truncatedStdout = DataUtils.getTruncatedCommandOutput(stdout, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 2, false, false, false);
|
||||
truncatedStderr = DataUtils.getTruncatedCommandOutput(stderr, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 2, false, false, false);
|
||||
}
|
||||
|
||||
if (truncatedStdout != null && truncatedStdout.length() < stdout.length()) {
|
||||
Logger.logWarn(logTag, "Execution Result for Execution Command \"" + label + "\" stdout length truncated from " + stdoutOriginalLength + " to " + truncatedStdout.length());
|
||||
stdout = truncatedStdout;
|
||||
}
|
||||
|
||||
if (truncatedStderr != null && truncatedStderr.length() < stderr.length()) {
|
||||
Logger.logWarn(logTag, "Execution Result for Execution Command \"" + label + "\" stderr length truncated from " + stderrOriginalLength + " to " + truncatedStderr.length());
|
||||
stderr = truncatedStderr;
|
||||
}
|
||||
|
||||
String errmsgOriginalLength = (errmsg == null) ? null: String.valueOf(errmsg.length());
|
||||
|
||||
// Truncate errmsg to max TRANSACTION_SIZE_LIMIT_IN_BYTES / 4
|
||||
// trim from end to preserve start of stacktraces
|
||||
String truncatedErrmsg = DataUtils.getTruncatedCommandOutput(errmsg, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 4, true, false, false);
|
||||
if (truncatedErrmsg != null && truncatedErrmsg.length() < errmsg.length()) {
|
||||
Logger.logWarn(logTag, "Execution Result for Execution Command \"" + label + "\" errmsg length truncated from " + errmsgOriginalLength + " to " + truncatedErrmsg.length());
|
||||
errmsg = truncatedErrmsg;
|
||||
}
|
||||
|
||||
|
||||
final Bundle resultBundle = new Bundle();
|
||||
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT, stdout);
|
||||
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH, stdoutOriginalLength);
|
||||
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR, stderr);
|
||||
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH, stderrOriginalLength);
|
||||
if (exitCode != null) resultBundle.putInt(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE, exitCode);
|
||||
if (errCode != null) resultBundle.putInt(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERR, errCode);
|
||||
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG, errmsg);
|
||||
|
||||
Intent resultIntent = new Intent();
|
||||
resultIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE, resultBundle);
|
||||
|
||||
try {
|
||||
pluginPendingIntent.send(context, Activity.RESULT_OK, resultIntent);
|
||||
} catch (PendingIntent.CanceledException e) {
|
||||
// The caller doesn't want the result? That's fine, just ignore
|
||||
Logger.logDebug(logTag, "The Execution Command \"" + label + "\" creator " + pluginPendingIntent.getCreatorPackage() + " does not want the results anymore");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get {@link Notification.Builder} for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
|
||||
* and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
|
||||
@@ -316,11 +293,11 @@ public class PluginUtils {
|
||||
* Check if {@link TermuxConstants#PROP_ALLOW_EXTERNAL_APPS} property is not set to "true".
|
||||
*
|
||||
* @param context The {@link Context} to get error string.
|
||||
* @return Returns the {@code errmsg} if policy is violated, otherwise {@code null}.
|
||||
* @return Returns the {@code error} if policy is violated, otherwise {@code null}.
|
||||
*/
|
||||
public static String checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(final Context context) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import android.provider.OpenableColumns;
|
||||
import android.util.Patterns;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.interact.DialogUtils;
|
||||
import com.termux.shared.interact.TextInputDialogUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||
import com.termux.app.TermuxService;
|
||||
@@ -118,7 +118,7 @@ public class TermuxFileReceiverActivity extends Activity {
|
||||
}
|
||||
|
||||
void promptNameAndSave(final InputStream in, final String attachmentFileName) {
|
||||
DialogUtils.textInput(this, R.string.title_file_received, attachmentFileName, R.string.action_file_received_edit, text -> {
|
||||
TextInputDialogUtils.textInput(this, R.string.title_file_received, attachmentFileName, R.string.action_file_received_edit, text -> {
|
||||
File outFile = saveStreamWithName(in, text);
|
||||
if (outFile == null) return;
|
||||
|
||||
|
||||
@@ -1,80 +1,109 @@
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<com.termux.app.terminal.TermuxActivityRootView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/activity_termux_root_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_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"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/action_toggle_soft_keyboard" />
|
||||
android:orientation="horizontal">
|
||||
<ImageButton
|
||||
android:id="@+id/settings_button"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:src="@drawable/ic_settings"
|
||||
android:background="@null"
|
||||
android:contentDescription="@string/action_open_settings" />
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/new_session_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_new_session" />
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/toggle_keyboard_button"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/action_toggle_soft_keyboard" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/new_session_button"
|
||||
style="?android:attr/buttonBarButtonStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/action_new_session" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</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>
|
||||
|
||||
20
app/src/main/res/layout/preference_markdown_text.xml
Normal file
20
app/src/main/res/layout/preference_markdown_text.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -89,6 +91,11 @@
|
||||
|
||||
|
||||
|
||||
<!-- TermuxService -->
|
||||
<string name="error_display_over_other_apps_permission_not_granted">&TERMUX_APP_NAME; requires \"Display over other apps\" permission to start terminal sessions from background on Android >= 10. Grants it from Settings -> Apps -> &TERMUX_APP_NAME; -> Advanced</string>
|
||||
|
||||
|
||||
|
||||
<!-- Termux RunCommandService -->
|
||||
<string name="error_run_command_service_invalid_intent_action">Invalid intent action to RunCommandService: `%1$s`</string>
|
||||
<string name="error_run_command_service_mandatory_extra_missing">Mandatory extra missing to RunCommandService: \"%1$s\"</string>
|
||||
@@ -103,15 +110,6 @@
|
||||
|
||||
|
||||
|
||||
<!-- Termux Report And ShareUtils -->
|
||||
<string name="action_copy">Copy</string>
|
||||
<string name="action_share">Share</string>
|
||||
|
||||
<string name="title_share_with">Share With</string>
|
||||
<string name="title_report_text">Report Text</string>
|
||||
|
||||
|
||||
|
||||
<!-- Termux File Receiver -->
|
||||
<string name="title_file_received">Save file in ~/downloads/</string>
|
||||
<string name="action_file_received_edit">Edit</string>
|
||||
@@ -122,37 +120,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>
|
||||
|
||||
@@ -44,15 +44,6 @@
|
||||
</style>
|
||||
|
||||
|
||||
<style name="Theme.AppCompat.TermuxReportActivity" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<item name="colorPrimaryDark">#FF0000</item>
|
||||
</style>
|
||||
|
||||
<style name="Toolbar.Title" parent="TextAppearance.Widget.AppCompat.Toolbar.Title">
|
||||
<item name="android:textSize">14sp</item>
|
||||
</style>
|
||||
|
||||
|
||||
<style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert">
|
||||
<!-- Seen in buttons on alert dialog: -->
|
||||
<item name="android:colorAccent">#212121</item>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
33
app/src/main/res/xml/termux_debugging_preferences.xml
Normal file
33
app/src/main/res/xml/termux_debugging_preferences.xml
Normal 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>
|
||||
13
app/src/main/res/xml/termux_preferences.xml
Normal file
13
app/src/main/res/xml/termux_preferences.xml
Normal 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>
|
||||
15
app/src/main/res/xml/termux_tasker_debugging_preferences.xml
Normal file
15
app/src/main/res/xml/termux_tasker_debugging_preferences.xml
Normal 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>
|
||||
8
app/src/main/res/xml/termux_tasker_preferences.xml
Normal file
8
app/src/main/res/xml/termux_tasker_preferences.xml
Normal 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>
|
||||
21
app/src/main/res/xml/termux_terminal_io_preferences.xml
Normal file
21
app/src/main/res/xml/termux_terminal_io_preferences.xml
Normal 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>
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.termux.app;
|
||||
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.data.UrlUtils;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
@@ -13,7 +13,7 @@ public class TermuxActivityTest {
|
||||
private void assertUrlsAre(String text, String... urls) {
|
||||
LinkedHashSet<String> expected = new LinkedHashSet<>();
|
||||
Collections.addAll(expected, urls);
|
||||
Assert.assertEquals(expected, DataUtils.extractUrls(text));
|
||||
Assert.assertEquals(expected, UrlUtils.extractUrls(text));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.1.3'
|
||||
classpath 'com.android.tools.build:gradle:4.2.1'
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
maven { url "https://jitpack.io" }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,13 +15,10 @@
|
||||
org.gradle.jvmargs=-Xmx2048M
|
||||
android.useAndroidX=true
|
||||
|
||||
termuxVersion=0.110
|
||||
termuxVersionCode=110
|
||||
|
||||
minSdkVersion=24
|
||||
targetSdkVersion=28
|
||||
ndkVersion=22.0.7026061
|
||||
compileSdkVersion=29
|
||||
ndkVersion=22.1.7171670
|
||||
compileSdkVersion=30
|
||||
|
||||
markwonVersion=4.6.2
|
||||
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
178
gradlew.bat
vendored
178
gradlew.bat
vendored
@@ -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
|
||||
|
||||
@@ -58,25 +58,16 @@ task sourceJar(type: Jar) {
|
||||
classifier "sources"
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
bar(MavenPublication) {
|
||||
groupId 'com.termux'
|
||||
artifactId 'terminal-emulator'
|
||||
version project.properties.termuxVersion
|
||||
artifact(sourceJar)
|
||||
artifact("$buildDir/outputs/aar/terminal-emulator-release.aar")
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
name = "GitHubPackages"
|
||||
url = uri("https://maven.pkg.github.com/termux/termux-app")
|
||||
|
||||
credentials {
|
||||
username = System.getenv("GH_USERNAME")
|
||||
password = System.getenv("GH_TOKEN")
|
||||
afterEvaluate {
|
||||
publishing {
|
||||
publications {
|
||||
// Creates a Maven publication called "release".
|
||||
release(MavenPublication) {
|
||||
from components.release
|
||||
groupId = 'com.termux'
|
||||
artifactId = 'terminal-emulator'
|
||||
version = '0.115'
|
||||
artifact(sourceJar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -19,6 +19,13 @@ public interface TerminalSessionClient {
|
||||
|
||||
void onColorsChanged(TerminalSession session);
|
||||
|
||||
void onTerminalCursorStateChange(boolean state);
|
||||
|
||||
|
||||
|
||||
Integer getTerminalCursorStyle();
|
||||
|
||||
|
||||
|
||||
void logError(String tag, String message);
|
||||
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -5,7 +5,7 @@ android {
|
||||
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.annotation:annotation:1.1.0"
|
||||
implementation "androidx.annotation:annotation:1.2.0"
|
||||
api project(":terminal-emulator")
|
||||
}
|
||||
|
||||
@@ -37,25 +37,16 @@ task sourceJar(type: Jar) {
|
||||
classifier "sources"
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
bar(MavenPublication) {
|
||||
groupId 'com.termux'
|
||||
artifactId 'terminal-view'
|
||||
version project.properties.termuxVersion
|
||||
artifact(sourceJar)
|
||||
artifact("$buildDir/outputs/aar/terminal-view-release.aar")
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
name = "GitHubPackages"
|
||||
url = uri("https://maven.pkg.github.com/termux/termux-app")
|
||||
|
||||
credentials {
|
||||
username = System.getenv("GH_USERNAME")
|
||||
password = System.getenv("GH_TOKEN")
|
||||
afterEvaluate {
|
||||
publishing {
|
||||
publications {
|
||||
// Creates a Maven publication called "release".
|
||||
release(MavenPublication) {
|
||||
from components.release
|
||||
groupId = 'com.termux'
|
||||
artifactId = 'terminal-view'
|
||||
version = '0.115'
|
||||
artifact(sourceJar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -703,7 +720,10 @@ public final class TerminalView extends View {
|
||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||
if (TERMINAL_VIEW_KEY_LOGGING_ENABLED)
|
||||
mClient.logInfo(LOG_TAG, "onKeyUp(keyCode=" + keyCode + ", event=" + event + ")");
|
||||
if (mEmulator == null) return true;
|
||||
|
||||
// Do not return for KEYCODE_BACK and send it to the client since user may be trying
|
||||
// to exit the activity.
|
||||
if (mEmulator == null && keyCode != KeyEvent.KEYCODE_BACK) return true;
|
||||
|
||||
if (mClient.onKeyUp(keyCode, event)) {
|
||||
invalidate();
|
||||
@@ -738,6 +758,11 @@ public final class TerminalView extends View {
|
||||
if (mEmulator == null || (newColumns != mEmulator.mColumns || newRows != mEmulator.mRows)) {
|
||||
mTermSession.updateSize(newColumns, newRows);
|
||||
mEmulator = mTermSession.getEmulator();
|
||||
mClient.onEmulatorSet();
|
||||
|
||||
// Update mTerminalCursorBlinkerRunnable inner class mEmulator on session change
|
||||
if (mTerminalCursorBlinkerRunnable != null)
|
||||
mTerminalCursorBlinkerRunnable.setEmulator(mEmulator);
|
||||
|
||||
mTopRow = 0;
|
||||
scrollTo(0, 0);
|
||||
@@ -755,6 +780,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 +825,6 @@ public final class TerminalView extends View {
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Define functions required for AutoFill API
|
||||
*/
|
||||
@@ -825,6 +850,165 @@ 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. If you call this on onResume()
|
||||
* to start cursor blinking, then ensure that {@link #mEmulator} is set, otherwise wait for the
|
||||
* {@link TerminalViewClient#onEmulatorSet()} event after calling {@link #attachSession(TerminalSession)}
|
||||
* for the first session added in the activity since blinking will not start if {@link #mEmulator}
|
||||
* is not set, like if activity is started again after exiting it with double back press. Do not
|
||||
* call this directly after {@link #attachSession(TerminalSession)} since {@link #updateSize()}
|
||||
* may return without setting {@link #mEmulator} since width/height may be 0. Its called again in
|
||||
* {@link #onSizeChanged(int, int, int, int)}. Calling on onResume() if emulator is already set
|
||||
* is necessary, since onEmulatorSet() may not be called after activity is started after device
|
||||
* display timeout with double tap and not power button.
|
||||
*
|
||||
* It should also be called on the
|
||||
* {@link com.termux.terminal.TerminalSessionClient#onTerminalCursorStateChange(boolean)}
|
||||
* 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.
|
||||
*
|
||||
* It should also be called after terminal is reset with {@link TerminalSession#reset()} in case
|
||||
* cursor blinker was disabled before reset due to call to
|
||||
* {@link com.termux.terminal.TerminalSessionClient#onTerminalCursorStateChange(boolean)}.
|
||||
*
|
||||
* How cursor blinker starting works is by registering a {@link Runnable} with the looper of
|
||||
* the main thread of the app which when run, toggles the cursor blinking state and re-registers
|
||||
* itself to be called with the delay set by {@link #mTerminalCursorBlinkerRate}. When cursor
|
||||
* 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 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 setEmulator(TerminalEmulator emulator) {
|
||||
mEmulator = emulator;
|
||||
}
|
||||
|
||||
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 +1104,6 @@ public final class TerminalView extends View {
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Define functions required for long hold toolbar.
|
||||
*/
|
||||
|
||||
@@ -56,6 +56,8 @@ public interface TerminalViewClient {
|
||||
boolean onCodePoint(int codePoint, boolean ctrlDown, TerminalSession session);
|
||||
|
||||
|
||||
void onEmulatorSet();
|
||||
|
||||
|
||||
void logError(String tag, String message);
|
||||
|
||||
|
||||
65
termux-shared/LICENSE.md
Normal file
65
termux-shared/LICENSE.md
Normal file
@@ -0,0 +1,65 @@
|
||||
The `termux-shared` library is released under [GPLv3 only](https://www.gnu.org/licenses/gpl-3.0.html) license.
|
||||
|
||||
### Exceptions
|
||||
|
||||
#### [MIT License](https://opensource.org/licenses/MIT)
|
||||
|
||||
- [`src/main/java/com/termux/shared/termux/TermuxConstants.java`](src/main/java/com/termux/shared/termux/TermuxConstants.java).
|
||||
- [`src/main/java/com/termux/shared/settings/properties/TermuxPropertyConstants.java`](src/main/java/com/termux/shared/settings/properties/TermuxPropertyConstants.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/activities/ReportActivity.java`](src/main/java/com/termux/shared/activities/ReportActivity.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/crash/CrashHandler.java`](src/main/java/com/termux/shared/crash/CrashHandler.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/data/DataUtils.java`](src/main/java/com/termux/shared/data/DataUtils.java).
|
||||
- [`src/main/java/com/termux/shared/data/IntentUtils.java`](src/main/java/com/termux/shared/data/IntentUtils.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/file/filesystem/FileType.java`](src/main/java/com/termux/shared/file/filesystem/FileType.java).
|
||||
- [`src/main/java/com/termux/shared/file/filesystem/FileTypes.java`](src/main/java/com/termux/shared/file/filesystem/FileTypes.java).
|
||||
- [`src/main/java/com/termux/shared/file/filesystem/NativeDispatcher.java`](src/main/java/com/termux/shared/file/filesystem/NativeDispatcher.java).
|
||||
- [`src/main/java/com/termux/shared/file/tests/FileUtilsTests.java`](src/main/java/com/termux/shared/file/tests/FileUtilsTests.java).
|
||||
- [`src/main/java/com/termux/shared/file/FileUtils.java`](src/main/java/com/termux/shared/file/FileUtils.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/interact/ShareUtils.java`](src/main/java/com/termux/shared/interact/ShareUtils.java).
|
||||
- [`src/main/java/com/termux/shared/interact/MessageDialogUtils.java`](src/main/java/com/termux/shared/interact/MessageDialogUtils.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/logger/Logger.java`](src/main/java/com/termux/shared/logger/Logger.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/markdown/MarkdownUtils.java`](src/main/java/com/termux/shared/markdown/MarkdownUtils.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/models/*`](src/main/java/com/termux/shared/models).
|
||||
|
||||
- [`src/main/java/com/termux/shared/notification/NotificationUtils.java`](src/main/java/com/termux/shared/notification/NotificationUtils.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/settings/preferences/SharedPreferenceUtils.java`](src/main/java/com/termux/shared/settings/preferences/SharedPreferenceUtils.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/settings/properties/SharedPropertiesParser.java`](src/main/java/com/termux/shared/settings/properties/SharedPropertiesParser.java).
|
||||
- [`src/main/java/com/termux/shared/settings/properties/SharedProperties.java`](src/main/java/com/termux/shared/settings/properties/SharedProperties.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/shell/ResultSender.java`](src/main/java/com/termux/shared/shell/ResultSender.java).
|
||||
- [`src/main/java/com/termux/shared/shell/ShellEnvironmentClient.java`](src/main/java/com/termux/shared/shell/ShellEnvironmentClient.java).
|
||||
- [`src/main/java/com/termux/shared/shell/ShellUtils.java`](src/main/java/com/termux/shared/shell/ShellUtils.java).
|
||||
- [`src/main/java/com/termux/shared/shell/TermuxTask.java`](src/main/java/com/termux/shared/shell/TermuxTask.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/termux/AndroidUtils.java`](src/main/java/com/termux/shared/termux/AndroidUtils.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/view/KeyboardUtils.java`](src/main/java/com/termux/shared/view/KeyboardUtils.java).
|
||||
- [`src/main/java/com/termux/shared/view/ViewUtils.java`](src/main/java/com/termux/shared/view/ViewUtils.java).
|
||||
|
||||
- [`src/main/res/drawable/*`](src/main/res/drawable).
|
||||
- [`src/main/res/layout/*`](src/main/res/layout).
|
||||
- [`src/main/res/menu/*`](src/main/res/menu).
|
||||
- [`src/main/res/values/*`](src/main/res/values).
|
||||
##
|
||||
|
||||
|
||||
#### [GPLv2 only with "Classpath" exception](https://openjdk.java.net/legal/gplv2+ce.html)
|
||||
|
||||
- [`src/main/java/com/termux/shared/file/filesystem/*`](src/main/java/com/termux/shared/file/filesystem) files that use code from [libcore/ojluni](https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/).
|
||||
##
|
||||
|
||||
|
||||
#### [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
- [`src/main/java/com/termux/shared/shell/StreamGobbler.java`](src/main/java/com/termux/shared/shell/StreamGobbler.java) uses code from [libsuperuser ](https://github.com/Chainfire/libsuperuser).
|
||||
##
|
||||
@@ -5,7 +5,10 @@ android {
|
||||
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.3.0'
|
||||
implementation "androidx.annotation:annotation:1.2.0"
|
||||
implementation "androidx.core:core:1.6.0-rc01"
|
||||
implementation "androidx.window:window:1.0.0-alpha08"
|
||||
implementation "com.google.guava:guava:24.1-jre"
|
||||
implementation "io.noties.markwon:core:$markwonVersion"
|
||||
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
|
||||
@@ -50,25 +53,16 @@ task sourceJar(type: Jar) {
|
||||
classifier "sources"
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
bar(MavenPublication) {
|
||||
groupId 'com.termux'
|
||||
artifactId 'termux-shared'
|
||||
version project.properties.termuxVersion
|
||||
artifact(sourceJar)
|
||||
artifact("$buildDir/outputs/aar/termux-shared-release.aar")
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
name = "GitHubPackages"
|
||||
url = uri("https://maven.pkg.github.com/termux/termux-app")
|
||||
|
||||
credentials {
|
||||
username = System.getenv("GH_USERNAME")
|
||||
password = System.getenv("GH_TOKEN")
|
||||
afterEvaluate {
|
||||
publishing {
|
||||
publications {
|
||||
// Creates a Maven publication called "release".
|
||||
release(MavenPublication) {
|
||||
from components.release
|
||||
groupId = 'com.termux'
|
||||
artifactId = 'termux-shared'
|
||||
version = '0.115'
|
||||
artifact(sourceJar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.termux.app.activities;
|
||||
package com.termux.shared.activities;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
@@ -14,11 +14,11 @@ import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.R;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.interact.ShareUtils;
|
||||
import com.termux.app.models.ReportInfo;
|
||||
import com.termux.shared.models.ReportInfo;
|
||||
|
||||
import org.commonmark.node.FencedCodeBlock;
|
||||
|
||||
@@ -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));
|
||||
@@ -7,8 +7,8 @@ import androidx.annotation.NonNull;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
import com.termux.shared.models.errors.Error;
|
||||
import com.termux.shared.termux.AndroidUtils;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
@@ -17,58 +17,85 @@ import java.nio.charset.Charset;
|
||||
*/
|
||||
public class CrashHandler implements Thread.UncaughtExceptionHandler {
|
||||
|
||||
private final Context context;
|
||||
private final Context mContext;
|
||||
private final CrashHandlerClient mCrashHandlerClient;
|
||||
private final Thread.UncaughtExceptionHandler defaultUEH;
|
||||
|
||||
private static final String LOG_TAG = "CrashUtils";
|
||||
|
||||
private CrashHandler(final Context context) {
|
||||
this.context = context;
|
||||
private CrashHandler(@NonNull final Context context, @NonNull final CrashHandlerClient crashHandlerClient) {
|
||||
this.mContext = context;
|
||||
this.mCrashHandlerClient = crashHandlerClient;
|
||||
this.defaultUEH = Thread.getDefaultUncaughtExceptionHandler();
|
||||
}
|
||||
|
||||
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
|
||||
logCrash(context,thread, throwable);
|
||||
logCrash(mContext, mCrashHandlerClient, thread, throwable);
|
||||
defaultUEH.uncaughtException(thread, throwable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default uncaught crash handler of current thread to {@link CrashHandler}.
|
||||
*/
|
||||
public static void setCrashHandler(final Context context) {
|
||||
public static void setCrashHandler(@NonNull final Context context, @NonNull final CrashHandlerClient crashHandlerClient) {
|
||||
if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof CrashHandler)) {
|
||||
Thread.setDefaultUncaughtExceptionHandler(new CrashHandler(context));
|
||||
Thread.setDefaultUncaughtExceptionHandler(new CrashHandler(context, crashHandlerClient));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a crash in the crash log file at
|
||||
* {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}.
|
||||
* Log a crash in the crash log file at {@code crashlogFilePath}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param crashHandlerClient The {@link CrashHandlerClient} implementation.
|
||||
* @param thread The {@link Thread} in which the crash happened.
|
||||
* @param throwable The {@link Throwable} thrown for the crash.
|
||||
*/
|
||||
public static void logCrash(final Context context, final Thread thread, final Throwable throwable) {
|
||||
|
||||
public static void logCrash(@NonNull final Context context, @NonNull final CrashHandlerClient crashHandlerClient, final Thread thread, final Throwable throwable) {
|
||||
StringBuilder reportString = new StringBuilder();
|
||||
|
||||
reportString.append("## Crash Details\n");
|
||||
reportString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Crash Thread", thread.toString(), "-"));
|
||||
reportString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Crash Timestamp", TermuxUtils.getCurrentTimeStamp(), "-"));
|
||||
reportString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Crash Timestamp", AndroidUtils.getCurrentTimeStamp(), "-"));
|
||||
reportString.append("\n\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Crash Message", throwable.getMessage(), "-"));
|
||||
reportString.append("\n\n").append(Logger.getStackTracesMarkdownString("Stacktrace", Logger.getStackTracesStringArray(throwable)));
|
||||
|
||||
reportString.append("\n\n").append(Logger.getStackTracesMarkdownString("Stacktrace", Logger.getStackTraceStringArray(throwable)));
|
||||
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, true));
|
||||
reportString.append("\n\n").append(TermuxUtils.getDeviceInfoMarkdownString(context));
|
||||
String appInfoMarkdownString = crashHandlerClient.getAppInfoMarkdownString(context);
|
||||
if (appInfoMarkdownString != null && !appInfoMarkdownString.isEmpty())
|
||||
reportString.append("\n\n").append(appInfoMarkdownString);
|
||||
|
||||
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context));
|
||||
|
||||
// Log report string to logcat
|
||||
Logger.logError(reportString.toString());
|
||||
|
||||
// Write report string to crash log file
|
||||
String errmsg = FileUtils.writeStringToFile(context, "crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, Charset.defaultCharset(), reportString.toString(), false);
|
||||
if (errmsg != null) {
|
||||
Logger.logError(LOG_TAG, errmsg);
|
||||
Error error = FileUtils.writeStringToFile("crash log", crashHandlerClient.getCrashLogFilePath(context),
|
||||
Charset.defaultCharset(), reportString.toString(), false);
|
||||
if (error != null) {
|
||||
Logger.logErrorExtended(LOG_TAG, error.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public interface CrashHandlerClient {
|
||||
|
||||
/**
|
||||
* Get crash log file path.
|
||||
*
|
||||
* @param context The {@link Context} passed to {@link CrashHandler#CrashHandler(Context, CrashHandlerClient)}.
|
||||
* @return Should return the crash log file path.
|
||||
*/
|
||||
@NonNull
|
||||
String getCrashLogFilePath(Context context);
|
||||
|
||||
/**
|
||||
* Get app info markdown string to add to crash log.
|
||||
*
|
||||
* @param context The {@link Context} passed to {@link CrashHandler#CrashHandler(Context, CrashHandlerClient)}.
|
||||
* @return Should return app info markdown string.
|
||||
*/
|
||||
String getAppInfoMarkdownString(Context context);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.termux.shared.crash;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
|
||||
public class TermuxCrashUtils implements CrashHandler.CrashHandlerClient {
|
||||
|
||||
/**
|
||||
* Set default uncaught crash handler of current thread to {@link CrashHandler} for Termux app
|
||||
* and its plugin to log crashes at {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}.
|
||||
*/
|
||||
public static void setCrashHandler(@NonNull final Context context) {
|
||||
CrashHandler.setCrashHandler(context, new TermuxCrashUtils());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getCrashLogFilePath(Context context) {
|
||||
return TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAppInfoMarkdownString(Context context) {
|
||||
return TermuxUtils.getAppInfoMarkdownString(context, true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package com.termux.shared.data;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -10,6 +12,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;
|
||||
|
||||
@@ -21,7 +25,7 @@ public class DataUtils {
|
||||
if (maxLength < 0 || text.length() < maxLength) return text;
|
||||
|
||||
if (fromEnd) {
|
||||
text = text.substring(0, Math.min(text.length(), maxLength));
|
||||
text = text.substring(0, maxLength);
|
||||
} else {
|
||||
int cutOffIndex = text.length() - maxLength;
|
||||
|
||||
@@ -40,10 +44,25 @@ public class DataUtils {
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace a sub string in each item of a {@link String[]}.
|
||||
*
|
||||
* @param array The {@link String[]} to replace in.
|
||||
* @param find The sub string to replace.
|
||||
* @param replace The sub string to replace with.
|
||||
*/
|
||||
public static void replaceSubStringsInStringArrayItems(String[] array, String find, String replace) {
|
||||
if(array == null || array.length == 0) return;
|
||||
|
||||
for (int i = 0; i < array.length; i++) {
|
||||
array[i] = array[i].replace(find, replace);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@code float} from a {@link String}.
|
||||
*
|
||||
* @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 +81,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 +97,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}.
|
||||
*
|
||||
@@ -121,97 +156,13 @@ public class DataUtils {
|
||||
* @param def The default {@link Object}.
|
||||
* @return Returns {@code object} if it is not {@code null}, otherwise returns {@code def}.
|
||||
*/
|
||||
public static <T> T getDefaultIfNull(@androidx.annotation.Nullable T object, @androidx.annotation.Nullable T def) {
|
||||
public static <T> T getDefaultIfNull(@Nullable T object, @Nullable T def) {
|
||||
return (object == null) ? def : object;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static LinkedHashSet<CharSequence> extractUrls(String text) {
|
||||
|
||||
StringBuilder regex_sb = new StringBuilder();
|
||||
|
||||
regex_sb.append("("); // Begin first matching group.
|
||||
regex_sb.append("(?:"); // Begin scheme group.
|
||||
regex_sb.append("dav|"); // The DAV proto.
|
||||
regex_sb.append("dict|"); // The DICT proto.
|
||||
regex_sb.append("dns|"); // The DNS proto.
|
||||
regex_sb.append("file|"); // File path.
|
||||
regex_sb.append("finger|"); // The Finger proto.
|
||||
regex_sb.append("ftp(?:s?)|"); // The FTP proto.
|
||||
regex_sb.append("git|"); // The Git proto.
|
||||
regex_sb.append("gopher|"); // The Gopher proto.
|
||||
regex_sb.append("http(?:s?)|"); // The HTTP proto.
|
||||
regex_sb.append("imap(?:s?)|"); // The IMAP proto.
|
||||
regex_sb.append("irc(?:[6s]?)|"); // The IRC proto.
|
||||
regex_sb.append("ip[fn]s|"); // The IPFS proto.
|
||||
regex_sb.append("ldap(?:s?)|"); // The LDAP proto.
|
||||
regex_sb.append("pop3(?:s?)|"); // The POP3 proto.
|
||||
regex_sb.append("redis(?:s?)|"); // The Redis proto.
|
||||
regex_sb.append("rsync|"); // The Rsync proto.
|
||||
regex_sb.append("rtsp(?:[su]?)|"); // The RTSP proto.
|
||||
regex_sb.append("sftp|"); // The SFTP proto.
|
||||
regex_sb.append("smb(?:s?)|"); // The SAMBA proto.
|
||||
regex_sb.append("smtp(?:s?)|"); // The SMTP proto.
|
||||
regex_sb.append("svn(?:(?:\\+ssh)?)|"); // The Subversion proto.
|
||||
regex_sb.append("tcp|"); // The TCP proto.
|
||||
regex_sb.append("telnet|"); // The Telnet proto.
|
||||
regex_sb.append("tftp|"); // The TFTP proto.
|
||||
regex_sb.append("udp|"); // The UDP proto.
|
||||
regex_sb.append("vnc|"); // The VNC proto.
|
||||
regex_sb.append("ws(?:s?)"); // The Websocket proto.
|
||||
regex_sb.append(")://"); // End scheme group.
|
||||
regex_sb.append(")"); // End first matching group.
|
||||
|
||||
|
||||
// Begin second matching group.
|
||||
regex_sb.append("(");
|
||||
|
||||
// User name and/or password in format 'user:pass@'.
|
||||
regex_sb.append("(?:\\S+(?::\\S*)?@)?");
|
||||
|
||||
// Begin host group.
|
||||
regex_sb.append("(?:");
|
||||
|
||||
// IP address (from http://www.regular-expressions.info/examples.html).
|
||||
regex_sb.append("(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|");
|
||||
|
||||
// Host name or domain.
|
||||
regex_sb.append("(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)(?:(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))?|");
|
||||
|
||||
// Just path. Used in case of 'file://' scheme.
|
||||
regex_sb.append("/(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)");
|
||||
|
||||
// End host group.
|
||||
regex_sb.append(")");
|
||||
|
||||
// Port number.
|
||||
regex_sb.append("(?::\\d{1,5})?");
|
||||
|
||||
// Resource path with optional query string.
|
||||
regex_sb.append("(?:/[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?");
|
||||
|
||||
// Fragment.
|
||||
regex_sb.append("(?:#[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?");
|
||||
|
||||
// End second matching group.
|
||||
regex_sb.append(")");
|
||||
|
||||
final Pattern urlPattern = Pattern.compile(
|
||||
regex_sb.toString(),
|
||||
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
|
||||
|
||||
LinkedHashSet<CharSequence> urlSet = new LinkedHashSet<>();
|
||||
Matcher matcher = urlPattern.matcher(text);
|
||||
|
||||
while (matcher.find()) {
|
||||
int matchStart = matcher.start(1);
|
||||
int matchEnd = matcher.end();
|
||||
String url = text.substring(matchStart, matchEnd);
|
||||
urlSet.add(url);
|
||||
}
|
||||
|
||||
return urlSet;
|
||||
/** Check if a string is null or empty. */
|
||||
public static boolean isNullOrEmpty(String string) {
|
||||
return string == null || string.isEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
package com.termux.shared.data;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class IntentUtils {
|
||||
|
||||
private static final String LOG_TAG = "IntentUtils";
|
||||
|
||||
|
||||
/**
|
||||
* Get a {@link String} extra from an {@link Intent} if its not {@code null} or empty.
|
||||
*
|
||||
* @param intent The {@link Intent} to get the extra from.
|
||||
* @param key The {@link String} key name.
|
||||
* @param def The default value if extra is not set.
|
||||
* @param throwExceptionIfNotSet If set to {@code true}, then an exception will be thrown if extra
|
||||
* is not set.
|
||||
* @return Returns the {@link String} extra if set, otherwise {@code null}.
|
||||
*/
|
||||
public static String getStringExtraIfSet(@NonNull Intent intent, String key, String def, boolean throwExceptionIfNotSet) throws Exception {
|
||||
String value = getStringExtraIfSet(intent, key, def);
|
||||
if (value == null && throwExceptionIfNotSet)
|
||||
throw new Exception("The \"" + key + "\" key string value is null or empty");
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link String} extra from an {@link Intent} if its not {@code null} or empty.
|
||||
*
|
||||
* @param intent The {@link Intent} to get the extra from.
|
||||
* @param key The {@link String} key name.
|
||||
* @param def The default value if extra is not set.
|
||||
* @return Returns the {@link String} extra if set, otherwise {@code null}.
|
||||
*/
|
||||
public static String getStringExtraIfSet(@NonNull Intent intent, String key, String def) {
|
||||
String value = intent.getStringExtra(key);
|
||||
if (value == null || value.isEmpty()) {
|
||||
if (def != null && !def.isEmpty())
|
||||
return def;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get a {@link String[]} extra from an {@link Intent} if its not {@code null} or empty.
|
||||
*
|
||||
* @param intent The {@link Intent} to get the extra from.
|
||||
* @param key The {@link String} key name.
|
||||
* @param def The default value if extra is not set.
|
||||
* @param throwExceptionIfNotSet If set to {@code true}, then an exception will be thrown if extra
|
||||
* is not set.
|
||||
* @return Returns the {@link String[]} extra if set, otherwise {@code null}.
|
||||
*/
|
||||
public static String[] getStringArrayExtraIfSet(@NonNull Intent intent, String key, String[] def, boolean throwExceptionIfNotSet) throws Exception {
|
||||
String[] value = getStringArrayExtraIfSet(intent, key, def);
|
||||
if (value == null && throwExceptionIfNotSet)
|
||||
throw new Exception("The \"" + key + "\" key string array is null or empty");
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link String[]} extra from an {@link Intent} if its not {@code null} or empty.
|
||||
*
|
||||
* @param intent The {@link Intent} to get the extra from.
|
||||
* @param key The {@link String} key name.
|
||||
* @param def The default value if extra is not set.
|
||||
* @return Returns the {@link String[]} extra if set, otherwise {@code null}.
|
||||
*/
|
||||
public static String[] getStringArrayExtraIfSet(Intent intent, String key, String[] def) {
|
||||
String[] value = intent.getStringArrayExtra(key);
|
||||
if (value == null || value.length == 0) {
|
||||
if (def != null && def.length != 0)
|
||||
return def;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public static String getIntentString(Intent intent) {
|
||||
if (intent == null) return null;
|
||||
|
||||
return intent.toString() + "\n" + getBundleString(intent.getExtras());
|
||||
}
|
||||
|
||||
public static String getBundleString(Bundle bundle) {
|
||||
if (bundle == null || bundle.size() == 0) return "Bundle[]";
|
||||
|
||||
StringBuilder bundleString = new StringBuilder("Bundle[\n");
|
||||
boolean first = true;
|
||||
for (String key : bundle.keySet()) {
|
||||
if (!first)
|
||||
bundleString.append("\n");
|
||||
|
||||
bundleString.append(key).append(": `");
|
||||
|
||||
Object value = bundle.get(key);
|
||||
if (value instanceof int[]) {
|
||||
bundleString.append(Arrays.toString((int[]) value));
|
||||
} else if (value instanceof byte[]) {
|
||||
bundleString.append(Arrays.toString((byte[]) value));
|
||||
} else if (value instanceof boolean[]) {
|
||||
bundleString.append(Arrays.toString((boolean[]) value));
|
||||
} else if (value instanceof short[]) {
|
||||
bundleString.append(Arrays.toString((short[]) value));
|
||||
} else if (value instanceof long[]) {
|
||||
bundleString.append(Arrays.toString((long[]) value));
|
||||
} else if (value instanceof float[]) {
|
||||
bundleString.append(Arrays.toString((float[]) value));
|
||||
} else if (value instanceof double[]) {
|
||||
bundleString.append(Arrays.toString((double[]) value));
|
||||
} else if (value instanceof String[]) {
|
||||
bundleString.append(Arrays.toString((String[]) value));
|
||||
} else if (value instanceof CharSequence[]) {
|
||||
bundleString.append(Arrays.toString((CharSequence[]) value));
|
||||
} else if (value instanceof Parcelable[]) {
|
||||
bundleString.append(Arrays.toString((Parcelable[]) value));
|
||||
} else if (value instanceof Bundle) {
|
||||
bundleString.append(getBundleString((Bundle) value));
|
||||
} else {
|
||||
bundleString.append(value);
|
||||
}
|
||||
|
||||
bundleString.append("`");
|
||||
|
||||
first = false;
|
||||
}
|
||||
|
||||
bundleString.append("\n]");
|
||||
return bundleString.toString();
|
||||
}
|
||||
|
||||
}
|
||||
103
termux-shared/src/main/java/com/termux/shared/data/UrlUtils.java
Normal file
103
termux-shared/src/main/java/com/termux/shared/data/UrlUtils.java
Normal file
@@ -0,0 +1,103 @@
|
||||
package com.termux.shared.data;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class UrlUtils {
|
||||
|
||||
public static Pattern URL_MATCH_REGEX;
|
||||
|
||||
public static Pattern getUrlMatchRegex() {
|
||||
if (URL_MATCH_REGEX != null) return URL_MATCH_REGEX;
|
||||
|
||||
StringBuilder regex_sb = new StringBuilder();
|
||||
|
||||
regex_sb.append("("); // Begin first matching group.
|
||||
regex_sb.append("(?:"); // Begin scheme group.
|
||||
regex_sb.append("dav|"); // The DAV proto.
|
||||
regex_sb.append("dict|"); // The DICT proto.
|
||||
regex_sb.append("dns|"); // The DNS proto.
|
||||
regex_sb.append("file|"); // File path.
|
||||
regex_sb.append("finger|"); // The Finger proto.
|
||||
regex_sb.append("ftp(?:s?)|"); // The FTP proto.
|
||||
regex_sb.append("git|"); // The Git proto.
|
||||
regex_sb.append("gopher|"); // The Gopher proto.
|
||||
regex_sb.append("http(?:s?)|"); // The HTTP proto.
|
||||
regex_sb.append("imap(?:s?)|"); // The IMAP proto.
|
||||
regex_sb.append("irc(?:[6s]?)|"); // The IRC proto.
|
||||
regex_sb.append("ip[fn]s|"); // The IPFS proto.
|
||||
regex_sb.append("ldap(?:s?)|"); // The LDAP proto.
|
||||
regex_sb.append("pop3(?:s?)|"); // The POP3 proto.
|
||||
regex_sb.append("redis(?:s?)|"); // The Redis proto.
|
||||
regex_sb.append("rsync|"); // The Rsync proto.
|
||||
regex_sb.append("rtsp(?:[su]?)|"); // The RTSP proto.
|
||||
regex_sb.append("sftp|"); // The SFTP proto.
|
||||
regex_sb.append("smb(?:s?)|"); // The SAMBA proto.
|
||||
regex_sb.append("smtp(?:s?)|"); // The SMTP proto.
|
||||
regex_sb.append("svn(?:(?:\\+ssh)?)|"); // The Subversion proto.
|
||||
regex_sb.append("tcp|"); // The TCP proto.
|
||||
regex_sb.append("telnet|"); // The Telnet proto.
|
||||
regex_sb.append("tftp|"); // The TFTP proto.
|
||||
regex_sb.append("udp|"); // The UDP proto.
|
||||
regex_sb.append("vnc|"); // The VNC proto.
|
||||
regex_sb.append("ws(?:s?)"); // The Websocket proto.
|
||||
regex_sb.append(")://"); // End scheme group.
|
||||
regex_sb.append(")"); // End first matching group.
|
||||
|
||||
|
||||
// Begin second matching group.
|
||||
regex_sb.append("(");
|
||||
|
||||
// User name and/or password in format 'user:pass@'.
|
||||
regex_sb.append("(?:\\S+(?::\\S*)?@)?");
|
||||
|
||||
// Begin host group.
|
||||
regex_sb.append("(?:");
|
||||
|
||||
// IP address (from http://www.regular-expressions.info/examples.html).
|
||||
regex_sb.append("(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|");
|
||||
|
||||
// Host name or domain.
|
||||
regex_sb.append("(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)(?:(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))?|");
|
||||
|
||||
// Just path. Used in case of 'file://' scheme.
|
||||
regex_sb.append("/(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)");
|
||||
|
||||
// End host group.
|
||||
regex_sb.append(")");
|
||||
|
||||
// Port number.
|
||||
regex_sb.append("(?::\\d{1,5})?");
|
||||
|
||||
// Resource path with optional query string.
|
||||
regex_sb.append("(?:/[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?");
|
||||
|
||||
// Fragment.
|
||||
regex_sb.append("(?:#[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?");
|
||||
|
||||
// End second matching group.
|
||||
regex_sb.append(")");
|
||||
|
||||
URL_MATCH_REGEX = Pattern.compile(
|
||||
regex_sb.toString(),
|
||||
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
|
||||
|
||||
return URL_MATCH_REGEX;
|
||||
}
|
||||
|
||||
public static LinkedHashSet<CharSequence> extractUrls(String text) {
|
||||
LinkedHashSet<CharSequence> urlSet = new LinkedHashSet<>();
|
||||
Matcher matcher = getUrlMatchRegex().matcher(text);
|
||||
|
||||
while (matcher.find()) {
|
||||
int matchStart = matcher.start(1);
|
||||
int matchEnd = matcher.end();
|
||||
String url = text.substring(matchStart, matchEnd);
|
||||
urlSet.add(url);
|
||||
}
|
||||
|
||||
return urlSet;
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,123 @@
|
||||
package com.termux.shared.file;
|
||||
|
||||
import android.os.Environment;
|
||||
|
||||
import com.termux.shared.models.errors.Error;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class TermuxFileUtils {
|
||||
/**
|
||||
* Replace "$PREFIX/" or "~/" prefix with termux absolute paths.
|
||||
*
|
||||
* @param path The {@code path} to expand.
|
||||
* @return Returns the {@code expand path}.
|
||||
*/
|
||||
public static String getExpandedTermuxPath(String path) {
|
||||
if (path != null && !path.isEmpty()) {
|
||||
path = path.replaceAll("^\\$PREFIX$", TermuxConstants.TERMUX_PREFIX_DIR_PATH);
|
||||
path = path.replaceAll("^\\$PREFIX/", TermuxConstants.TERMUX_PREFIX_DIR_PATH + "/");
|
||||
path = path.replaceAll("^~/$", TermuxConstants.TERMUX_HOME_DIR_PATH);
|
||||
path = path.replaceAll("^~/", TermuxConstants.TERMUX_HOME_DIR_PATH + "/");
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace termux absolute paths with "$PREFIX/" or "~/" prefix.
|
||||
*
|
||||
* @param path The {@code path} to unexpand.
|
||||
* @return Returns the {@code unexpand path}.
|
||||
*/
|
||||
public static String getUnExpandedTermuxPath(String path) {
|
||||
if (path != null && !path.isEmpty()) {
|
||||
path = path.replaceAll("^" + Pattern.quote(TermuxConstants.TERMUX_PREFIX_DIR_PATH) + "/", "\\$PREFIX/");
|
||||
path = path.replaceAll("^" + Pattern.quote(TermuxConstants.TERMUX_HOME_DIR_PATH) + "/", "~/");
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get canonical path.
|
||||
*
|
||||
* @param path The {@code path} to convert.
|
||||
* @param prefixForNonAbsolutePath Optional prefix path to prefix before non-absolute paths. This
|
||||
* can be set to {@code null} if non-absolute paths should
|
||||
* be prefixed with "/". The call to {@link File#getCanonicalPath()}
|
||||
* will automatically do this anyways.
|
||||
* @param expandPath The {@code boolean} that decides if input path is first attempted to be expanded by calling
|
||||
* {@link TermuxFileUtils#getExpandedTermuxPath(String)} before its passed to
|
||||
* {@link FileUtils#getCanonicalPath(String, String)}.
|
||||
|
||||
* @return Returns the {@code canonical path}.
|
||||
*/
|
||||
public static String getCanonicalPath(String path, final String prefixForNonAbsolutePath, final boolean expandPath) {
|
||||
if (path == null) path = "";
|
||||
|
||||
if (expandPath)
|
||||
path = getExpandedTermuxPath(path);
|
||||
|
||||
return FileUtils.getCanonicalPath(path, prefixForNonAbsolutePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if {@code path} is under the allowed termux working directory paths. If it is, then
|
||||
* allowed parent path is returned.
|
||||
*
|
||||
* @param path The {@code path} to check.
|
||||
* @return Returns the allowed path if it {@code path} is under it, otherwise {@link TermuxConstants#TERMUX_FILES_DIR_PATH}.
|
||||
*/
|
||||
public static String getMatchedAllowedTermuxWorkingDirectoryParentPathForPath(String path) {
|
||||
if (path == null || path.isEmpty()) return TermuxConstants.TERMUX_FILES_DIR_PATH;
|
||||
|
||||
if (path.startsWith(TermuxConstants.TERMUX_STORAGE_HOME_DIR_PATH + "/")) {
|
||||
return TermuxConstants.TERMUX_STORAGE_HOME_DIR_PATH;
|
||||
} if (path.startsWith(Environment.getExternalStorageDirectory().getAbsolutePath() + "/")) {
|
||||
return Environment.getExternalStorageDirectory().getAbsolutePath();
|
||||
} else if (path.startsWith("/sdcard/")) {
|
||||
return "/sdcard";
|
||||
} else {
|
||||
return TermuxConstants.TERMUX_FILES_DIR_PATH;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the existence and permissions of directory file at path as a working directory for
|
||||
* termux app.
|
||||
*
|
||||
* The creation of missing directory and setting of missing permissions will only be done if
|
||||
* {@code path} is under paths returned by {@link #getMatchedAllowedTermuxWorkingDirectoryParentPathForPath(String)}.
|
||||
*
|
||||
* The permissions set to directory will be {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS}.
|
||||
*
|
||||
* @param label The optional label for the directory file. This can optionally be {@code null}.
|
||||
* @param filePath The {@code path} for file to validate or create. Symlinks will not be followed.
|
||||
* @param createDirectoryIfMissing The {@code boolean} that decides if directory file
|
||||
* should be created if its missing.
|
||||
* @param setPermissions The {@code boolean} that decides if permissions are to be
|
||||
* automatically set defined by {@code permissionsToCheck}.
|
||||
* @param setMissingPermissionsOnly The {@code boolean} that decides if only missing permissions
|
||||
* are to be set or if they should be overridden.
|
||||
* @param ignoreErrorsIfPathIsInParentDirPath The {@code boolean} that decides if existence
|
||||
* and permission errors are to be ignored if path is
|
||||
* in {@code parentDirPath}.
|
||||
* @param ignoreIfNotExecutable The {@code boolean} that decides if missing executable permission
|
||||
* error is to be ignored. This allows making an attempt to set
|
||||
* executable permissions, but ignoring if it fails.
|
||||
* @return Returns the {@code error} if path is not a directory file, failed to create it,
|
||||
* or validating permissions failed, otherwise {@code null}.
|
||||
*/
|
||||
public static Error validateDirectoryFileExistenceAndPermissions(String label, final String filePath, final boolean createDirectoryIfMissing,
|
||||
final boolean setPermissions, final boolean setMissingPermissionsOnly,
|
||||
final boolean ignoreErrorsIfPathIsInParentDirPath, final boolean ignoreIfNotExecutable) {
|
||||
return FileUtils.validateDirectoryFileExistenceAndPermissions(label, filePath,
|
||||
TermuxFileUtils.getMatchedAllowedTermuxWorkingDirectoryParentPathForPath(filePath), createDirectoryIfMissing,
|
||||
FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS, setPermissions, setMissingPermissionsOnly,
|
||||
ignoreErrorsIfPathIsInParentDirPath, ignoreIfNotExecutable);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -90,7 +90,7 @@ public class FileTypes {
|
||||
return getFileType(fileAttributes);
|
||||
} catch (Exception e) {
|
||||
// If not a ENOENT (No such file or directory) exception
|
||||
if (!e.getMessage().contains("ENOENT"))
|
||||
if (e.getMessage() != null && !e.getMessage().contains("ENOENT"))
|
||||
Logger.logError("Failed to get file type for file at path \"" + filePath + "\": " + e.getMessage());
|
||||
return FileType.NO_EXIST;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.models.errors.Error;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.Charset;
|
||||
@@ -31,18 +32,19 @@ public class FileUtilsTests {
|
||||
Logger.logInfo(LOG_TAG, "Running tests");
|
||||
Logger.logInfo(LOG_TAG, "testRootDirectoryPath: \"" + testRootDirectoryPath + "\"");
|
||||
|
||||
String fileUtilsTestsDirectoryCanonicalPath = FileUtils.getCanonicalPath(testRootDirectoryPath, null, false);
|
||||
String fileUtilsTestsDirectoryCanonicalPath = FileUtils.getCanonicalPath(testRootDirectoryPath, null);
|
||||
assertEqual("FileUtilsTests directory path is not a canonical path", testRootDirectoryPath, fileUtilsTestsDirectoryCanonicalPath);
|
||||
|
||||
runTestsInner(context, testRootDirectoryPath);
|
||||
runTestsInner(testRootDirectoryPath);
|
||||
Logger.logInfo(LOG_TAG, "All tests successful");
|
||||
} catch (Exception e) {
|
||||
Logger.logErrorAndShowToast(context, LOG_TAG, e.getMessage());
|
||||
Logger.logErrorExtended(LOG_TAG, e.getMessage());
|
||||
Logger.showToast(context, e.getMessage() != null ? e.getMessage().replaceAll("(?s)\nFull Error:\n.*", "") : null, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static void runTestsInner(@NonNull final Context context, @NonNull final String testRootDirectoryPath) throws Exception {
|
||||
String errmsg;
|
||||
private static void runTestsInner(@NonNull final String testRootDirectoryPath) throws Exception {
|
||||
Error error;
|
||||
String label;
|
||||
String path;
|
||||
|
||||
@@ -101,20 +103,20 @@ public class FileUtilsTests {
|
||||
|
||||
// Create or clear test root directory file
|
||||
label = "testRootDirectoryPath";
|
||||
errmsg = FileUtils.clearDirectory(context, label, testRootDirectoryPath);
|
||||
assertEqual("Failed to create " + label + " directory file", null, errmsg);
|
||||
error = FileUtils.clearDirectory(label, testRootDirectoryPath);
|
||||
assertEqual("Failed to create " + label + " directory file", null, error);
|
||||
|
||||
if (!FileUtils.directoryFileExists(testRootDirectoryPath, false))
|
||||
throwException("The " + label + " directory file does not exist as expected after creation");
|
||||
|
||||
|
||||
// Create dir1 directory file
|
||||
errmsg = FileUtils.createDirectoryFile(context, dir1_label, dir1_path);
|
||||
assertEqual("Failed to create " + dir1_label + " directory file", null, errmsg);
|
||||
error = FileUtils.createDirectoryFile(dir1_label, dir1_path);
|
||||
assertEqual("Failed to create " + dir1_label + " directory file", null, error);
|
||||
|
||||
// Create dir2 directory file
|
||||
errmsg = FileUtils.createDirectoryFile(context, dir2_label, dir2_path);
|
||||
assertEqual("Failed to create " + dir2_label + " directory file", null, errmsg);
|
||||
error = FileUtils.createDirectoryFile(dir2_label, dir2_path);
|
||||
assertEqual("Failed to create " + dir2_label + " directory file", null, error);
|
||||
|
||||
|
||||
|
||||
@@ -122,29 +124,29 @@ public class FileUtilsTests {
|
||||
|
||||
// Create dir1/sub_dir1 directory file
|
||||
label = dir1__sub_dir1_label; path = dir1__sub_dir1_path;
|
||||
errmsg = FileUtils.createDirectoryFile(context, label, path);
|
||||
assertEqual("Failed to create " + label + " directory file", null, errmsg);
|
||||
error = FileUtils.createDirectoryFile(label, path);
|
||||
assertEqual("Failed to create " + label + " directory file", null, error);
|
||||
if (!FileUtils.directoryFileExists(path, false))
|
||||
throwException("The " + label + " directory file does not exist as expected after creation");
|
||||
|
||||
// Create dir1/sub_reg1 regular file
|
||||
label = dir1__sub_reg1_label; path = dir1__sub_reg1_path;
|
||||
errmsg = FileUtils.createRegularFile(context, label, path);
|
||||
assertEqual("Failed to create " + label + " regular file", null, errmsg);
|
||||
error = FileUtils.createRegularFile(label, path);
|
||||
assertEqual("Failed to create " + label + " regular file", null, error);
|
||||
if (!FileUtils.regularFileExists(path, false))
|
||||
throwException("The " + label + " regular file does not exist as expected after creation");
|
||||
|
||||
// Create dir1/sub_sym1 -> dir2 absolute symlink file
|
||||
label = dir1__sub_sym1_label; path = dir1__sub_sym1_path;
|
||||
errmsg = FileUtils.createSymlinkFile(context, label, dir2_path, path);
|
||||
assertEqual("Failed to create " + label + " symlink file", null, errmsg);
|
||||
error = FileUtils.createSymlinkFile(label, dir2_path, path);
|
||||
assertEqual("Failed to create " + label + " symlink file", null, error);
|
||||
if (!FileUtils.symlinkFileExists(path))
|
||||
throwException("The " + label + " symlink file does not exist as expected after creation");
|
||||
|
||||
// Copy dir1/sub_sym1 symlink file to dir1/sub_sym2
|
||||
label = dir1__sub_sym2_label; path = dir1__sub_sym2_path;
|
||||
errmsg = FileUtils.copySymlinkFile(context, label, dir1__sub_sym1_path, path, false);
|
||||
assertEqual("Failed to copy " + dir1__sub_sym1_label + " symlink file to " + label, null, errmsg);
|
||||
error = FileUtils.copySymlinkFile(label, dir1__sub_sym1_path, path, false);
|
||||
assertEqual("Failed to copy " + dir1__sub_sym1_label + " symlink file to " + label, null, error);
|
||||
if (!FileUtils.symlinkFileExists(path))
|
||||
throwException("The " + label + " symlink file does not exist as expected after copying it from " + dir1__sub_sym1_label);
|
||||
if (!new File(path).getCanonicalPath().equals(dir2_path))
|
||||
@@ -156,25 +158,25 @@ public class FileUtilsTests {
|
||||
|
||||
// Write "line1" to dir2/sub_reg1 regular file
|
||||
label = dir2__sub_reg1_label; path = dir2__sub_reg1_path;
|
||||
errmsg = FileUtils.writeStringToFile(context, label, path, Charset.defaultCharset(), "line1", false);
|
||||
assertEqual("Failed to write string to " + label + " file with append mode false", null, errmsg);
|
||||
error = FileUtils.writeStringToFile(label, path, Charset.defaultCharset(), "line1", false);
|
||||
assertEqual("Failed to write string to " + label + " file with append mode false", null, error);
|
||||
if (!FileUtils.regularFileExists(path, false))
|
||||
throwException("The " + label + " file does not exist as expected after writing to it with append mode false");
|
||||
|
||||
// Write "line2" to dir2/sub_reg1 regular file
|
||||
errmsg = FileUtils.writeStringToFile(context, label, path, Charset.defaultCharset(), "\nline2", true);
|
||||
assertEqual("Failed to write string to " + label + " file with append mode true", null, errmsg);
|
||||
error = FileUtils.writeStringToFile(label, path, Charset.defaultCharset(), "\nline2", true);
|
||||
assertEqual("Failed to write string to " + label + " file with append mode true", null, error);
|
||||
|
||||
// Read dir2/sub_reg1 regular file
|
||||
StringBuilder dataStringBuilder = new StringBuilder();
|
||||
errmsg = FileUtils.readStringFromFile(context, label, path, Charset.defaultCharset(), dataStringBuilder, false);
|
||||
assertEqual("Failed to read from " + label + " file", null, errmsg);
|
||||
error = FileUtils.readStringFromFile(label, path, Charset.defaultCharset(), dataStringBuilder, false);
|
||||
assertEqual("Failed to read from " + label + " file", null, error);
|
||||
assertEqual("The data read from " + label + " file in not as expected", "line1\nline2", dataStringBuilder.toString());
|
||||
|
||||
// Copy dir2/sub_reg1 regular file to dir2/sub_reg2 file
|
||||
label = dir2__sub_reg2_label; path = dir2__sub_reg2_path;
|
||||
errmsg = FileUtils.copyRegularFile(context, label, dir2__sub_reg1_path, path, false);
|
||||
assertEqual("Failed to copy " + dir2__sub_reg1_label + " regular file to " + label, null, errmsg);
|
||||
error = FileUtils.copyRegularFile(label, dir2__sub_reg1_path, path, false);
|
||||
assertEqual("Failed to copy " + dir2__sub_reg1_label + " regular file to " + label, null, error);
|
||||
if (!FileUtils.regularFileExists(path, false))
|
||||
throwException("The " + label + " regular file does not exist as expected after copying it from " + dir2__sub_reg1_label);
|
||||
|
||||
@@ -184,22 +186,22 @@ public class FileUtilsTests {
|
||||
|
||||
// Copy dir1 directory file to dir3
|
||||
label = dir3_label; path = dir3_path;
|
||||
errmsg = FileUtils.copyDirectoryFile(context, label, dir2_path, path, false);
|
||||
assertEqual("Failed to copy " + dir2_label + " directory file to " + label, null, errmsg);
|
||||
error = FileUtils.copyDirectoryFile(label, dir2_path, path, false);
|
||||
assertEqual("Failed to copy " + dir2_label + " directory file to " + label, null, error);
|
||||
if (!FileUtils.directoryFileExists(path, false))
|
||||
throwException("The " + label + " directory file does not exist as expected after copying it from " + dir2_label);
|
||||
|
||||
// Copy dir1 directory file to dir3 again to test overwrite
|
||||
label = dir3_label; path = dir3_path;
|
||||
errmsg = FileUtils.copyDirectoryFile(context, label, dir2_path, path, false);
|
||||
assertEqual("Failed to copy " + dir2_label + " directory file to " + label, null, errmsg);
|
||||
error = FileUtils.copyDirectoryFile(label, dir2_path, path, false);
|
||||
assertEqual("Failed to copy " + dir2_label + " directory file to " + label, null, error);
|
||||
if (!FileUtils.directoryFileExists(path, false))
|
||||
throwException("The " + label + " directory file does not exist as expected after copying it from " + dir2_label);
|
||||
|
||||
// Move dir3 directory file to dir4
|
||||
label = dir4_label; path = dir4_path;
|
||||
errmsg = FileUtils.moveDirectoryFile(context, label, dir3_path, path, false);
|
||||
assertEqual("Failed to move " + dir3_label + " directory file to " + label, null, errmsg);
|
||||
error = FileUtils.moveDirectoryFile(label, dir3_path, path, false);
|
||||
assertEqual("Failed to move " + dir3_label + " directory file to " + label, null, error);
|
||||
if (!FileUtils.directoryFileExists(path, false))
|
||||
throwException("The " + label + " directory file does not exist as expected after copying it from " + dir3_label);
|
||||
|
||||
@@ -209,16 +211,16 @@ public class FileUtilsTests {
|
||||
|
||||
// Create dir1/sub_sym3 -> dir4 relative symlink file
|
||||
label = dir1__sub_sym3_label; path = dir1__sub_sym3_path;
|
||||
errmsg = FileUtils.createSymlinkFile(context, label, "../dir4", path);
|
||||
assertEqual("Failed to create " + label + " symlink file", null, errmsg);
|
||||
error = FileUtils.createSymlinkFile(label, "../dir4", path);
|
||||
assertEqual("Failed to create " + label + " symlink file", null, error);
|
||||
if (!FileUtils.symlinkFileExists(path))
|
||||
throwException("The " + label + " symlink file does not exist as expected after creation");
|
||||
|
||||
// Create dir1/sub_sym3 -> dirX relative dangling symlink file
|
||||
// This is to ensure that symlinkFileExists returns true if a symlink file exists but is dangling
|
||||
label = dir1__sub_sym3_label; path = dir1__sub_sym3_path;
|
||||
errmsg = FileUtils.createSymlinkFile(context, label, "../dirX", path);
|
||||
assertEqual("Failed to create " + label + " symlink file", null, errmsg);
|
||||
error = FileUtils.createSymlinkFile(label, "../dirX", path);
|
||||
assertEqual("Failed to create " + label + " symlink file", null, error);
|
||||
if (!FileUtils.symlinkFileExists(path))
|
||||
throwException("The " + label + " dangling symlink file does not exist as expected after creation");
|
||||
|
||||
@@ -228,8 +230,8 @@ public class FileUtilsTests {
|
||||
|
||||
// Delete dir1/sub_sym2 symlink file
|
||||
label = dir1__sub_sym2_label; path = dir1__sub_sym2_path;
|
||||
errmsg = FileUtils.deleteSymlinkFile(context, label, path, false);
|
||||
assertEqual("Failed to delete " + label + " symlink file", null, errmsg);
|
||||
error = FileUtils.deleteSymlinkFile(label, path, false);
|
||||
assertEqual("Failed to delete " + label + " symlink file", null, error);
|
||||
if (FileUtils.fileExists(path, false))
|
||||
throwException("The " + label + " symlink file still exist after deletion");
|
||||
|
||||
@@ -245,8 +247,8 @@ public class FileUtilsTests {
|
||||
|
||||
// Delete dir1 directory file
|
||||
label = dir1_label; path = dir1_path;
|
||||
errmsg = FileUtils.deleteDirectoryFile(context, label, path, false);
|
||||
assertEqual("Failed to delete " + label + " directory file", null, errmsg);
|
||||
error = FileUtils.deleteDirectoryFile(label, path, false);
|
||||
assertEqual("Failed to delete " + label + " directory file", null, error);
|
||||
if (FileUtils.fileExists(path, false))
|
||||
throwException("The " + label + " directory file still exist after deletion");
|
||||
|
||||
@@ -267,8 +269,8 @@ public class FileUtilsTests {
|
||||
|
||||
// Delete dir2/sub_reg1 regular file
|
||||
label = dir2__sub_reg1_label; path = dir2__sub_reg1_path;
|
||||
errmsg = FileUtils.deleteRegularFile(context, label, path, false);
|
||||
assertEqual("Failed to delete " + label + " regular file", null, errmsg);
|
||||
error = FileUtils.deleteRegularFile(label, path, false);
|
||||
assertEqual("Failed to delete " + label + " regular file", null, error);
|
||||
if (FileUtils.fileExists(path, false))
|
||||
throwException("The " + label + " regular file still exist after deletion");
|
||||
|
||||
@@ -276,6 +278,14 @@ public class FileUtilsTests {
|
||||
FileUtils.getFileType("/dev/null", false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static void assertEqual(@NonNull final String message, final String expected, final Error actual) throws Exception {
|
||||
String actualString = actual != null ? actual.getMessage() : null;
|
||||
if (!equalsRegardingNull(expected, actualString))
|
||||
throwException(message + "\nexpected: \"" + expected + "\"\nactual: \"" + actualString + "\"\nFull Error:\n" + (actual != null ? actual.toString() : ""));
|
||||
}
|
||||
|
||||
public static void assertEqual(@NonNull final String message, final String expected, final String actual) throws Exception {
|
||||
if (!equalsRegardingNull(expected, actual))
|
||||
throwException(message + "\nexpected: \"" + expected + "\"\nactual: \"" + actual + "\"");
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.termux.shared.interact;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.termux.shared.R;
|
||||
|
||||
public class MessageDialogUtils {
|
||||
|
||||
/**
|
||||
* Show a message in a dialog
|
||||
*
|
||||
* @param context The {@link Context} to use to start the dialog. An {@link Activity} {@link Context}
|
||||
* must be passed, otherwise exceptions will be thrown.
|
||||
* @param titleText The title text of the dialog.
|
||||
* @param messageText The message text of the dialog.
|
||||
* @param onDismiss The {@link DialogInterface.OnDismissListener} to run when dialog is dismissed.
|
||||
*/
|
||||
public static void showMessage(Context context, String titleText, String messageText, final DialogInterface.OnDismissListener onDismiss) {
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.Theme_AppCompat_Light_Dialog)
|
||||
.setPositiveButton(android.R.string.ok, null);
|
||||
|
||||
LayoutInflater inflater = (LayoutInflater) context.getSystemService( Context.LAYOUT_INFLATER_SERVICE );
|
||||
View view = inflater.inflate(R.layout.dialog_show_message, null);
|
||||
if (view != null) {
|
||||
builder.setView(view);
|
||||
|
||||
TextView titleView = view.findViewById(R.id.dialog_title);
|
||||
if (titleView != null)
|
||||
titleView.setText(titleText);
|
||||
|
||||
TextView messageView = view.findViewById(R.id.dialog_message);
|
||||
if (messageView != null)
|
||||
messageView.setText(messageText);
|
||||
}
|
||||
|
||||
if (onDismiss != null)
|
||||
builder.setOnDismissListener(onDismiss);
|
||||
|
||||
builder.show();
|
||||
}
|
||||
|
||||
public static void exitAppWithErrorMessage(Context context, String titleText, String messageText) {
|
||||
showMessage(context, titleText, messageText, dialog -> System.exit(0));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,15 +2,21 @@ 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;
|
||||
|
||||
public final class DialogUtils {
|
||||
import com.termux.shared.R;
|
||||
|
||||
public final class TextInputDialogUtils {
|
||||
|
||||
public interface TextSetListener {
|
||||
void onTextSet(String text);
|
||||
@@ -61,7 +67,8 @@ 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);
|
||||
@@ -12,6 +12,7 @@ import com.termux.shared.termux.TermuxConstants;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@@ -27,7 +28,25 @@ public class Logger {
|
||||
public static final int DEFAULT_LOG_LEVEL = LOG_LEVEL_NORMAL;
|
||||
private static int CURRENT_LOG_LEVEL = DEFAULT_LOG_LEVEL;
|
||||
|
||||
public static final int LOGGER_ENTRY_SIZE_LIMIT_IN_BYTES = 4 * 1024; // 4KB
|
||||
/**
|
||||
* The maximum size of the log entry payload that can be written to the logger. An attempt to
|
||||
* write more than this amount will result in a truncated log entry.
|
||||
*
|
||||
* The limit is 4068 but this includes log tag and log level prefix "D/" before log tag and ": "
|
||||
* suffix after it.
|
||||
*
|
||||
* #define LOGGER_ENTRY_MAX_PAYLOAD 4068
|
||||
* https://cs.android.com/android/_/android/platform/system/core/+/android10-release:liblog/include/log/log_read.h;l=127
|
||||
*/
|
||||
public static final int LOGGER_ENTRY_MAX_PAYLOAD = 4068; // 4068 bytes
|
||||
|
||||
/**
|
||||
* The maximum safe size of the log entry payload that can be written to the logger, based on
|
||||
* {@link #LOGGER_ENTRY_MAX_PAYLOAD}. Using 4000 as a safe limit to give log tag and its
|
||||
* prefix/suffix max 68 characters for itself. Use "log*Extended()" functions to use max possible
|
||||
* limit if tag is already known.
|
||||
*/
|
||||
public static final int LOGGER_ENTRY_MAX_SAFE_PAYLOAD = 4000; // 4000 bytes
|
||||
|
||||
|
||||
|
||||
@@ -44,6 +63,40 @@ public class Logger {
|
||||
Log.v(getFullTag(tag), message);
|
||||
}
|
||||
|
||||
public static void logExtendedMessage(int logLevel, String tag, String message) {
|
||||
if (message == null) return;
|
||||
|
||||
int cutOffIndex;
|
||||
int nextNewlineIndex;
|
||||
String prefix = "";
|
||||
|
||||
// -8 for prefix "(xx/xx)" (max 99 sections), - log tag length, -4 for log tag prefix "D/" and suffix ": "
|
||||
int maxEntrySize = LOGGER_ENTRY_MAX_PAYLOAD - 8 - getFullTag(tag).length() - 4;
|
||||
|
||||
List<String> messagesList = new ArrayList<>();
|
||||
|
||||
while(!message.isEmpty()) {
|
||||
if (message.length() > maxEntrySize) {
|
||||
cutOffIndex = maxEntrySize;
|
||||
nextNewlineIndex = message.lastIndexOf('\n', cutOffIndex);
|
||||
if (nextNewlineIndex != -1) {
|
||||
cutOffIndex = nextNewlineIndex + 1;
|
||||
}
|
||||
messagesList.add(message.substring(0, cutOffIndex));
|
||||
message = message.substring(cutOffIndex);
|
||||
} else {
|
||||
messagesList.add(message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for(int i=0; i<messagesList.size(); i++) {
|
||||
if (messagesList.size() > 1)
|
||||
prefix = "(" + (i + 1) + "/" + messagesList.size() + ")\n";
|
||||
logMessage(logLevel, tag, prefix + messagesList.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static void logError(String tag, String message) {
|
||||
@@ -54,6 +107,14 @@ public class Logger {
|
||||
logMessage(Log.ERROR, DEFAULT_LOG_TAG, message);
|
||||
}
|
||||
|
||||
public static void logErrorExtended(String tag, String message) {
|
||||
logExtendedMessage(Log.ERROR, tag, message);
|
||||
}
|
||||
|
||||
public static void logErrorExtended(String message) {
|
||||
logExtendedMessage(Log.ERROR, DEFAULT_LOG_TAG, message);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static void logWarn(String tag, String message) {
|
||||
@@ -64,6 +125,14 @@ public class Logger {
|
||||
logMessage(Log.WARN, DEFAULT_LOG_TAG, message);
|
||||
}
|
||||
|
||||
public static void logWarnExtended(String tag, String message) {
|
||||
logExtendedMessage(Log.WARN, tag, message);
|
||||
}
|
||||
|
||||
public static void logWarnExtended(String message) {
|
||||
logExtendedMessage(Log.WARN, DEFAULT_LOG_TAG, message);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static void logInfo(String tag, String message) {
|
||||
@@ -74,6 +143,14 @@ public class Logger {
|
||||
logMessage(Log.INFO, DEFAULT_LOG_TAG, message);
|
||||
}
|
||||
|
||||
public static void logInfoExtended(String tag, String message) {
|
||||
logExtendedMessage(Log.INFO, tag, message);
|
||||
}
|
||||
|
||||
public static void logInfoExtended(String message) {
|
||||
logExtendedMessage(Log.INFO, DEFAULT_LOG_TAG, message);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static void logDebug(String tag, String message) {
|
||||
@@ -84,6 +161,14 @@ public class Logger {
|
||||
logMessage(Log.DEBUG, DEFAULT_LOG_TAG, message);
|
||||
}
|
||||
|
||||
public static void logDebugExtended(String tag, String message) {
|
||||
logExtendedMessage(Log.DEBUG, tag, message);
|
||||
}
|
||||
|
||||
public static void logDebugExtended(String message) {
|
||||
logExtendedMessage(Log.DEBUG, DEFAULT_LOG_TAG, message);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static void logVerbose(String tag, String message) {
|
||||
@@ -94,6 +179,14 @@ public class Logger {
|
||||
logMessage(Log.VERBOSE, DEFAULT_LOG_TAG, message);
|
||||
}
|
||||
|
||||
public static void logVerboseExtended(String tag, String message) {
|
||||
logExtendedMessage(Log.VERBOSE, tag, message);
|
||||
}
|
||||
|
||||
public static void logVerboseExtended(String message) {
|
||||
logExtendedMessage(Log.VERBOSE, DEFAULT_LOG_TAG, message);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static void logErrorAndShowToast(Context context, String tag, String message) {
|
||||
@@ -127,8 +220,7 @@ public class Logger {
|
||||
|
||||
|
||||
public static void logStackTraceWithMessage(String tag, String message, Throwable throwable) {
|
||||
if (CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL)
|
||||
Log.e(getFullTag(tag), getMessageAndStackTraceString(message, throwable));
|
||||
Logger.logErrorExtended(tag, getMessageAndStackTraceString(message, throwable));
|
||||
}
|
||||
|
||||
public static void logStackTraceWithMessage(String message, Throwable throwable) {
|
||||
@@ -143,11 +235,14 @@ public class Logger {
|
||||
logStackTraceWithMessage(DEFAULT_LOG_TAG, null, throwable);
|
||||
}
|
||||
|
||||
public static void logStackTracesWithMessage(String tag, String message, List<Throwable> throwableList) {
|
||||
if (CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL)
|
||||
Log.e(getFullTag(tag), getMessageAndStackTracesString(message, throwableList));
|
||||
|
||||
|
||||
public static void logStackTracesWithMessage(String tag, String message, List<Throwable> throwablesList) {
|
||||
Logger.logErrorExtended(tag, getMessageAndStackTracesString(message, throwablesList));
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static String getMessageAndStackTraceString(String message, Throwable throwable) {
|
||||
if (message == null && throwable == null)
|
||||
return null;
|
||||
@@ -159,17 +254,19 @@ public class Logger {
|
||||
return getStackTraceString(throwable);
|
||||
}
|
||||
|
||||
public static String getMessageAndStackTracesString(String message, List<Throwable> throwableList) {
|
||||
if (message == null && (throwableList == null || throwableList.size() == 0))
|
||||
public static String getMessageAndStackTracesString(String message, List<Throwable> throwablesList) {
|
||||
if (message == null && (throwablesList == null || throwablesList.size() == 0))
|
||||
return null;
|
||||
else if (message != null && (throwableList != null && throwableList.size() != 0))
|
||||
return message + ":\n" + getStackTracesString(null, getStackTraceStringArray(throwableList));
|
||||
else if (throwableList == null || throwableList.size() == 0)
|
||||
else if (message != null && (throwablesList != null && throwablesList.size() != 0))
|
||||
return message + ":\n" + getStackTracesString(null, getStackTracesStringArray(throwablesList));
|
||||
else if (throwablesList == null || throwablesList.size() == 0)
|
||||
return message;
|
||||
else
|
||||
return getStackTracesString(null, getStackTraceStringArray(throwableList));
|
||||
return getStackTracesString(null, getStackTracesStringArray(throwablesList));
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static String getStackTraceString(Throwable throwable) {
|
||||
if (throwable == null) return null;
|
||||
|
||||
@@ -182,27 +279,30 @@ public class Logger {
|
||||
pw.close();
|
||||
stackTraceString = errors.toString();
|
||||
errors.close();
|
||||
} catch (IOException e1) {
|
||||
e1.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return stackTraceString;
|
||||
}
|
||||
|
||||
public static String[] getStackTraceStringArray(Throwable throwable) {
|
||||
return getStackTraceStringArray(Collections.singletonList(throwable));
|
||||
|
||||
|
||||
public static String[] getStackTracesStringArray(Throwable throwable) {
|
||||
return getStackTracesStringArray(Collections.singletonList(throwable));
|
||||
}
|
||||
|
||||
public static String[] getStackTraceStringArray(List<Throwable> throwableList) {
|
||||
if (throwableList == null) return null;
|
||||
|
||||
final String[] stackTraceStringArray = new String[throwableList.size()];
|
||||
for (int i = 0; i < throwableList.size(); i++) {
|
||||
stackTraceStringArray[i] = getStackTraceString(throwableList.get(i));
|
||||
public static String[] getStackTracesStringArray(List<Throwable> throwablesList) {
|
||||
if (throwablesList == null) return null;
|
||||
final String[] stackTraceStringArray = new String[throwablesList.size()];
|
||||
for (int i = 0; i < throwablesList.size(); i++) {
|
||||
stackTraceStringArray[i] = getStackTraceString(throwablesList.get(i));
|
||||
}
|
||||
return stackTraceStringArray;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static String getStackTracesString(String label, String[] stackTraceStringArray) {
|
||||
if (label == null) label = "StackTraces:";
|
||||
StringBuilder stackTracesString = new StringBuilder(label);
|
||||
@@ -254,7 +354,7 @@ public class Logger {
|
||||
else
|
||||
return label + ": " + def;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public static void showToast(final Context context, final String toastText, boolean longDuration) {
|
||||
@@ -283,7 +383,7 @@ public class Logger {
|
||||
logLevelLabels[i] = getLogLevelLabel(context, Integer.parseInt(logLevels[i].toString()), addDefaultTag);
|
||||
}
|
||||
|
||||
return logLevelLabels;
|
||||
return logLevelLabels;
|
||||
}
|
||||
|
||||
public static String getLogLevelLabel(final Context context, final int logLevel, final boolean addDefaultTag) {
|
||||
|
||||
@@ -90,10 +90,12 @@ public class MarkdownUtils {
|
||||
|
||||
int maxCount = 0;
|
||||
int matchCount;
|
||||
String match;
|
||||
|
||||
Matcher matcher = backticksPattern.matcher(string);
|
||||
while(matcher.find()) {
|
||||
matchCount = matcher.group(1).length();
|
||||
match = matcher.group(1);
|
||||
matchCount = match != null ? match.length() : 0;
|
||||
if (matchCount > maxCount)
|
||||
maxCount = matchCount;
|
||||
}
|
||||
@@ -179,7 +181,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());
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
package com.termux.shared.models;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||
import com.termux.shared.data.IntentUtils;
|
||||
import com.termux.shared.models.errors.Error;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class ExecutionCommand {
|
||||
@@ -51,12 +51,6 @@ public class ExecutionCommand {
|
||||
|
||||
}
|
||||
|
||||
// Define errCode values
|
||||
// TODO: Define custom values for different cases
|
||||
public final static int RESULT_CODE_OK = Activity.RESULT_OK;
|
||||
public final static int RESULT_CODE_OK_MINOR_FAILURES = Activity.RESULT_FIRST_USER;
|
||||
public final static int RESULT_CODE_FAILED = Activity.RESULT_FIRST_USER + 1;
|
||||
public final static int RESULT_CODE_CANCELED = Activity.RESULT_FIRST_USER + 2;
|
||||
|
||||
/** The optional unique id for the {@link ExecutionCommand}. */
|
||||
public Integer id;
|
||||
@@ -80,6 +74,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. */
|
||||
@@ -105,32 +103,26 @@ public class ExecutionCommand {
|
||||
public String pluginAPIHelp;
|
||||
|
||||
|
||||
/** Defines the {@link Intent} received which started the command. */
|
||||
public Intent commandIntent;
|
||||
|
||||
/** Defines if {@link ExecutionCommand} was started because of an external plugin request
|
||||
* like {@link TERMUX_SERVICE#ACTION_SERVICE_EXECUTE} intent or from within Termux app itself.
|
||||
*/
|
||||
* like with an intent or from within Termux app itself. */
|
||||
public boolean isPluginExecutionCommand;
|
||||
/** Defines {@link PendingIntent} that should be sent if an external plugin requested the execution. */
|
||||
public PendingIntent pluginPendingIntent;
|
||||
|
||||
/** Defines the {@link ResultConfig} for the {@link ExecutionCommand} containing information
|
||||
* on how to handle the result. */
|
||||
public final ResultConfig resultConfig = new ResultConfig();
|
||||
|
||||
/** The stdout of shell command. */
|
||||
public String stdout;
|
||||
/** The stderr of shell command. */
|
||||
public String stderr;
|
||||
/** The exit code of shell command. */
|
||||
public Integer exitCode;
|
||||
/** Defines the {@link ResultData} for the {@link ExecutionCommand} containing information
|
||||
* of the result. */
|
||||
public final ResultData resultData = new ResultData();
|
||||
|
||||
|
||||
/** The internal error code of {@link ExecutionCommand}. */
|
||||
public Integer errCode = RESULT_CODE_OK;
|
||||
/** The internal error message of {@link ExecutionCommand}. */
|
||||
public String errmsg;
|
||||
/** The internal exceptions of {@link ExecutionCommand}. */
|
||||
public List<Throwable> throwableList = new ArrayList<>();
|
||||
|
||||
/** Defines if processing results already called for this {@link ExecutionCommand}. */
|
||||
public boolean processingResultsAlreadyCalled;
|
||||
|
||||
private static final String LOG_TAG = "ExecutionCommand";
|
||||
|
||||
|
||||
public ExecutionCommand() {
|
||||
@@ -150,13 +142,101 @@ public class ExecutionCommand {
|
||||
this.isFailsafe = isFailsafe;
|
||||
}
|
||||
|
||||
|
||||
public boolean isPluginExecutionCommandWithPendingResult() {
|
||||
return isPluginExecutionCommand && resultConfig.isCommandWithPendingResult();
|
||||
}
|
||||
|
||||
|
||||
public synchronized boolean setState(ExecutionState newState) {
|
||||
// The state transition cannot go back or change if already at {@link ExecutionState#SUCCESS}
|
||||
if (newState.getValue() < currentState.getValue() || currentState == ExecutionState.SUCCESS) {
|
||||
Logger.logError(LOG_TAG, "Invalid "+ getCommandIdAndLabelLogString() + " state transition from \"" + currentState.getName() + "\" to " + "\"" + newState.getName() + "\"");
|
||||
return false;
|
||||
}
|
||||
|
||||
// The {@link ExecutionState#FAILED} can be set again, like to add more errors, but we don't update
|
||||
// {@link #previousState} with the {@link #currentState} value if its at {@link ExecutionState#FAILED} to
|
||||
// preserve the last valid state
|
||||
if (currentState != ExecutionState.FAILED)
|
||||
previousState = currentState;
|
||||
|
||||
currentState = newState;
|
||||
return true;
|
||||
}
|
||||
|
||||
public synchronized boolean hasExecuted() {
|
||||
return currentState.getValue() >= ExecutionState.EXECUTED.getValue();
|
||||
}
|
||||
|
||||
public synchronized boolean isExecuting() {
|
||||
return currentState == ExecutionState.EXECUTING;
|
||||
}
|
||||
|
||||
public synchronized boolean isSuccessful() {
|
||||
return currentState == ExecutionState.SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
public synchronized boolean setStateFailed(@NonNull Error error) {
|
||||
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), null);
|
||||
}
|
||||
|
||||
public synchronized boolean setStateFailed(@NonNull Error error, Throwable throwable) {
|
||||
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), Collections.singletonList(throwable));
|
||||
}
|
||||
public synchronized boolean setStateFailed(@NonNull Error error, List<Throwable> throwablesList) {
|
||||
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), throwablesList);
|
||||
}
|
||||
|
||||
public synchronized boolean setStateFailed(int code, String message) {
|
||||
return setStateFailed(null, code, message, null);
|
||||
}
|
||||
|
||||
public synchronized boolean setStateFailed(int code, String message, Throwable throwable) {
|
||||
return setStateFailed(null, code, message, Collections.singletonList(throwable));
|
||||
}
|
||||
|
||||
public synchronized boolean setStateFailed(int code, String message, List<Throwable> throwablesList) {
|
||||
return setStateFailed(null, code, message, throwablesList);
|
||||
}
|
||||
public synchronized boolean setStateFailed(String type, int code, String message, List<Throwable> throwablesList) {
|
||||
if (!this.resultData.setStateFailed(type, code, message, throwablesList)) {
|
||||
Logger.logWarn(LOG_TAG, "setStateFailed for " + getCommandIdAndLabelLogString() + " resultData encountered an error.");
|
||||
}
|
||||
|
||||
return setState(ExecutionState.FAILED);
|
||||
}
|
||||
|
||||
public synchronized boolean shouldNotProcessResults() {
|
||||
if (processingResultsAlreadyCalled) {
|
||||
return true;
|
||||
} else {
|
||||
processingResultsAlreadyCalled = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized boolean isStateFailed() {
|
||||
if (currentState != ExecutionState.FAILED)
|
||||
return false;
|
||||
|
||||
if (!resultData.isStateFailed()) {
|
||||
Logger.logWarn(LOG_TAG, "The " + getCommandIdAndLabelLogString() + " has an invalid errCode value set in errors list while having ExecutionState.FAILED state.\n" + resultData.errorsList);
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
if (!hasExecuted())
|
||||
return getExecutionInputLogString(this, true);
|
||||
else {
|
||||
return getExecutionOutputLogString(this, true);
|
||||
return getExecutionOutputLogString(this, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,15 +264,15 @@ public class ExecutionCommand {
|
||||
logString.append("\n").append(executionCommand.getInBackgroundLogString());
|
||||
logString.append("\n").append(executionCommand.getIsFailsafeLogString());
|
||||
|
||||
|
||||
if (!ignoreNull || executionCommand.sessionAction != null)
|
||||
logString.append("\n").append(executionCommand.getSessionActionLogString());
|
||||
|
||||
if (!ignoreNull || executionCommand.commandIntent != null)
|
||||
logString.append("\n").append(executionCommand.getCommandIntentLogString());
|
||||
|
||||
logString.append("\n").append(executionCommand.getIsPluginExecutionCommandLogString());
|
||||
if (!ignoreNull || executionCommand.isPluginExecutionCommand) {
|
||||
if (!ignoreNull || executionCommand.pluginPendingIntent != null)
|
||||
logString.append("\n").append(executionCommand.getPendingIntentCreatorLogString());
|
||||
}
|
||||
if (executionCommand.isPluginExecutionCommand)
|
||||
logString.append("\n").append(ResultConfig.getResultConfigLogString(executionCommand.resultConfig, ignoreNull));
|
||||
|
||||
return logString.toString();
|
||||
}
|
||||
@@ -202,9 +282,10 @@ public class ExecutionCommand {
|
||||
*
|
||||
* @param executionCommand The {@link ExecutionCommand} to convert.
|
||||
* @param ignoreNull Set to {@code true} if non-critical {@code null} values are to be ignored.
|
||||
* @param logResultData Set to {@code true} if {@link #resultData} should be logged.
|
||||
* @return Returns the log friendly {@link String}.
|
||||
*/
|
||||
public static String getExecutionOutputLogString(final ExecutionCommand executionCommand, boolean ignoreNull) {
|
||||
public static String getExecutionOutputLogString(final ExecutionCommand executionCommand, boolean ignoreNull, boolean logResultData) {
|
||||
if (executionCommand == null) return "null";
|
||||
|
||||
StringBuilder logString = new StringBuilder();
|
||||
@@ -214,32 +295,8 @@ public class ExecutionCommand {
|
||||
logString.append("\n").append(executionCommand.getPreviousStateLogString());
|
||||
logString.append("\n").append(executionCommand.getCurrentStateLogString());
|
||||
|
||||
logString.append("\n").append(executionCommand.getStdoutLogString());
|
||||
logString.append("\n").append(executionCommand.getStderrLogString());
|
||||
logString.append("\n").append(executionCommand.getExitCodeLogString());
|
||||
|
||||
logString.append(getExecutionErrLogString(executionCommand, ignoreNull));
|
||||
|
||||
return logString.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a log friendly {@link String} for {@link ExecutionCommand} execution error parameters.
|
||||
*
|
||||
* @param executionCommand The {@link ExecutionCommand} to convert.
|
||||
* @param ignoreNull Set to {@code true} if non-critical {@code null} values are to be ignored.
|
||||
* @return Returns the log friendly {@link String}.
|
||||
*/
|
||||
public static String getExecutionErrLogString(final ExecutionCommand executionCommand, boolean ignoreNull) {
|
||||
StringBuilder logString = new StringBuilder();
|
||||
|
||||
if (!ignoreNull || (executionCommand.isStateFailed())) {
|
||||
logString.append("\n").append(executionCommand.getErrCodeLogString());
|
||||
logString.append("\n").append(executionCommand.getErrmsgLogString());
|
||||
logString.append("\n").append(executionCommand.geStackTracesLogString());
|
||||
} else {
|
||||
logString.append("");
|
||||
}
|
||||
if (logResultData)
|
||||
logString.append("\n").append(ResultData.getResultDataLogString(executionCommand.resultData, ignoreNull));
|
||||
|
||||
return logString.toString();
|
||||
}
|
||||
@@ -256,7 +313,7 @@ public class ExecutionCommand {
|
||||
StringBuilder logString = new StringBuilder();
|
||||
|
||||
logString.append(getExecutionInputLogString(executionCommand, false));
|
||||
logString.append(getExecutionOutputLogString(executionCommand, false));
|
||||
logString.append(getExecutionOutputLogString(executionCommand, false, true));
|
||||
|
||||
logString.append("\n").append(executionCommand.getCommandDescriptionLogString());
|
||||
logString.append("\n").append(executionCommand.getCommandHelpLogString());
|
||||
@@ -293,18 +350,10 @@ public class ExecutionCommand {
|
||||
|
||||
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("isPluginExecutionCommand", executionCommand.isPluginExecutionCommand, "-"));
|
||||
if (executionCommand.pluginPendingIntent != null)
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Pending Intent Creator", executionCommand.pluginPendingIntent.getCreatorPackage(), "-"));
|
||||
else
|
||||
markdownString.append("\n").append("**Pending Intent Creator:** - ");
|
||||
|
||||
markdownString.append("\n\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Stdout", executionCommand.stdout, "-"));
|
||||
markdownString.append("\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Stderr", executionCommand.stderr, "-"));
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Exit Code", executionCommand.exitCode, "-"));
|
||||
markdownString.append("\n\n").append(ResultConfig.getResultConfigMarkdownString(executionCommand.resultConfig));
|
||||
|
||||
markdownString.append("\n\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Err Code", executionCommand.errCode, "-"));
|
||||
markdownString.append("\n").append("**Errmsg:**\n").append(DataUtils.getDefaultIfNull(executionCommand.errmsg, "-"));
|
||||
markdownString.append("\n\n").append(executionCommand.geStackTracesMarkdownString());
|
||||
markdownString.append("\n\n").append(ResultData.getResultDataMarkdownString(executionCommand.resultData));
|
||||
|
||||
if (executionCommand.commandDescription != null || executionCommand.commandHelp != null) {
|
||||
if (executionCommand.commandDescription != null)
|
||||
@@ -323,7 +372,6 @@ public class ExecutionCommand {
|
||||
}
|
||||
|
||||
|
||||
|
||||
public String getIdLogString() {
|
||||
if (id != null)
|
||||
return "(" + id + ") ";
|
||||
@@ -370,21 +418,10 @@ public class ExecutionCommand {
|
||||
return "isFailsafe: `" + isFailsafe + "`";
|
||||
}
|
||||
|
||||
public String getIsPluginExecutionCommandLogString() {
|
||||
return "isPluginExecutionCommand: `" + isPluginExecutionCommand + "`";
|
||||
}
|
||||
|
||||
public String getSessionActionLogString() {
|
||||
return Logger.getSingleLineLogStringEntry("Session Action", sessionAction, "-");
|
||||
}
|
||||
|
||||
public String getPendingIntentCreatorLogString() {
|
||||
if (pluginPendingIntent != null)
|
||||
return "Pending Intent Creator: `" + pluginPendingIntent.getCreatorPackage() + "`";
|
||||
else
|
||||
return "Pending Intent Creator: -";
|
||||
}
|
||||
|
||||
public String getCommandDescriptionLogString() {
|
||||
return Logger.getSingleLineLogStringEntry("Command Description", commandDescription, "-");
|
||||
}
|
||||
@@ -397,35 +434,49 @@ public class ExecutionCommand {
|
||||
return Logger.getSingleLineLogStringEntry("Plugin API Help", pluginAPIHelp, "-");
|
||||
}
|
||||
|
||||
public String getStdoutLogString() {
|
||||
return Logger.getMultiLineLogStringEntry("Stdout", DataUtils.getTruncatedCommandOutput(stdout, Logger.LOGGER_ENTRY_SIZE_LIMIT_IN_BYTES / 5, false, false, true), "-");
|
||||
public String getCommandIntentLogString() {
|
||||
if (commandIntent == null)
|
||||
return "Command Intent: -";
|
||||
else
|
||||
return Logger.getMultiLineLogStringEntry("Command Intent", IntentUtils.getIntentString(commandIntent), "-");
|
||||
}
|
||||
|
||||
public String getStderrLogString() {
|
||||
return Logger.getMultiLineLogStringEntry("Stderr", DataUtils.getTruncatedCommandOutput(stderr, Logger.LOGGER_ENTRY_SIZE_LIMIT_IN_BYTES / 5, false, false, true), "-");
|
||||
}
|
||||
|
||||
public String getExitCodeLogString() {
|
||||
return Logger.getSingleLineLogStringEntry("Exit Code", exitCode, "-");
|
||||
}
|
||||
|
||||
public String getErrCodeLogString() {
|
||||
return Logger.getSingleLineLogStringEntry("Err Code", errCode, "-");
|
||||
}
|
||||
|
||||
public String getErrmsgLogString() {
|
||||
return Logger.getMultiLineLogStringEntry("Errmsg", errmsg, "-");
|
||||
}
|
||||
|
||||
public String geStackTracesLogString() {
|
||||
return Logger.getStackTracesString("StackTraces:", Logger.getStackTraceStringArray(throwableList));
|
||||
}
|
||||
|
||||
public String geStackTracesMarkdownString() {
|
||||
return Logger.getStackTracesMarkdownString("StackTraces", Logger.getStackTraceStringArray(throwableList));
|
||||
public String getIsPluginExecutionCommandLogString() {
|
||||
return "isPluginExecutionCommand: `" + isPluginExecutionCommand + "`";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a log friendly {@link String} for {@link List<String>} argumentsArray.
|
||||
* If argumentsArray are null or of size 0, then `Arguments: -` is returned. Otherwise
|
||||
* following format is returned:
|
||||
*
|
||||
* Arguments:
|
||||
* ```
|
||||
* Arg 1: `value`
|
||||
* Arg 2: 'value`
|
||||
* ```
|
||||
*
|
||||
* @param argumentsArray The {@link String[]} argumentsArray to convert.
|
||||
* @return Returns the log friendly {@link String}.
|
||||
*/
|
||||
public static String getArgumentsLogString(final String[] argumentsArray) {
|
||||
StringBuilder argumentsString = new StringBuilder("Arguments:");
|
||||
|
||||
if (argumentsArray != null && argumentsArray.length != 0) {
|
||||
argumentsString.append("\n```\n");
|
||||
for (int i = 0; i != argumentsArray.length; i++) {
|
||||
argumentsString.append(Logger.getSingleLineLogStringEntry("Arg " + (i + 1),
|
||||
DataUtils.getTruncatedCommandOutput(argumentsArray[i], Logger.LOGGER_ENTRY_MAX_SAFE_PAYLOAD / 5, true, false, true),
|
||||
"-")).append("\n");
|
||||
}
|
||||
argumentsString.append("```");
|
||||
} else{
|
||||
argumentsString.append(" -");
|
||||
}
|
||||
|
||||
return argumentsString.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a markdown {@link String} for {@link String[]} argumentsArray.
|
||||
@@ -461,110 +512,4 @@ public class ExecutionCommand {
|
||||
return argumentsString.toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a log friendly {@link String} for {@link List<String>} argumentsArray.
|
||||
* If argumentsArray are null or of size 0, then `Arguments: -` is returned. Otherwise
|
||||
* following format is returned:
|
||||
*
|
||||
* Arguments:
|
||||
* ```
|
||||
* Arg 1: `value`
|
||||
* Arg 2: 'value`
|
||||
* ```
|
||||
*
|
||||
* @param argumentsArray The {@link String[]} argumentsArray to convert.
|
||||
* @return Returns the log friendly {@link String}.
|
||||
*/
|
||||
public static String getArgumentsLogString(final String[] argumentsArray) {
|
||||
StringBuilder argumentsString = new StringBuilder("Arguments:");
|
||||
|
||||
if (argumentsArray != null && argumentsArray.length != 0) {
|
||||
argumentsString.append("\n```\n");
|
||||
for (int i = 0; i != argumentsArray.length; i++) {
|
||||
argumentsString.append(Logger.getSingleLineLogStringEntry("Arg " + (i + 1),
|
||||
DataUtils.getTruncatedCommandOutput(argumentsArray[i], Logger.LOGGER_ENTRY_SIZE_LIMIT_IN_BYTES / 5, true, false, true),
|
||||
"-")).append("`\n");
|
||||
}
|
||||
argumentsString.append("```");
|
||||
} else{
|
||||
argumentsString.append(" -");
|
||||
}
|
||||
|
||||
return argumentsString.toString();
|
||||
}
|
||||
|
||||
|
||||
public synchronized boolean setState(ExecutionState newState) {
|
||||
// The state transition cannot go back or change if already at {@link ExecutionState#SUCCESS}
|
||||
if (newState.getValue() < currentState.getValue() || currentState == ExecutionState.SUCCESS) {
|
||||
Logger.logError("Invalid "+ getCommandIdAndLabelLogString() + " state transition from \"" + currentState.getName() + "\" to " + "\"" + newState.getName() + "\"");
|
||||
return false;
|
||||
}
|
||||
|
||||
// The {@link ExecutionState#FAILED} can be set again, like to add more errors, but we don't update
|
||||
// {@link #previousState} with the {@link #currentState} value if its at {@link ExecutionState#FAILED} to
|
||||
// preserve the last valid state
|
||||
if (currentState != ExecutionState.FAILED)
|
||||
previousState = currentState;
|
||||
|
||||
currentState = newState;
|
||||
return true;
|
||||
}
|
||||
|
||||
public synchronized boolean setStateFailed(int errCode, String errmsg, Throwable throwable) {
|
||||
if (errCode > RESULT_CODE_OK) {
|
||||
this.errCode = errCode;
|
||||
} else {
|
||||
Logger.logWarn("Ignoring invalid " + getCommandIdAndLabelLogString() + " errCode value \"" + errCode + "\". Force setting it to RESULT_CODE_FAILED \"" + RESULT_CODE_FAILED + "\"");
|
||||
this.errCode = RESULT_CODE_FAILED;
|
||||
}
|
||||
|
||||
this.errmsg = errmsg;
|
||||
|
||||
if (!setState(ExecutionState.FAILED))
|
||||
return false;
|
||||
|
||||
if (this.throwableList == null)
|
||||
this.throwableList = new ArrayList<>();
|
||||
|
||||
if (throwable != null)
|
||||
this.throwableList.add(throwable);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public synchronized boolean shouldNotProcessResults() {
|
||||
if (processingResultsAlreadyCalled) {
|
||||
return true;
|
||||
} else {
|
||||
processingResultsAlreadyCalled = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized boolean isStateFailed() {
|
||||
if (currentState != ExecutionState.FAILED)
|
||||
return false;
|
||||
|
||||
if (errCode <= RESULT_CODE_OK) {
|
||||
Logger.logWarn("The " + getCommandIdAndLabelLogString() + " has an invalid errCode value \"" + errCode + "\" while having ExecutionState.FAILED state.");
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized boolean hasExecuted() {
|
||||
return currentState.getValue() >= ExecutionState.EXECUTED.getValue();
|
||||
}
|
||||
|
||||
public synchronized boolean isExecuting() {
|
||||
return currentState == ExecutionState.EXECUTING;
|
||||
}
|
||||
|
||||
public synchronized boolean isSuccessful() {
|
||||
return currentState == ExecutionState.SUCCESS;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package com.termux.app.models;
|
||||
package com.termux.shared.models;
|
||||
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
import com.termux.shared.termux.AndroidUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class ReportInfo implements Serializable {
|
||||
|
||||
/** The user action that was being processed for which the report was generated. */
|
||||
public final UserAction userAction;
|
||||
public final String userAction;
|
||||
/** The internal app component that sent the report. */
|
||||
public final String sender;
|
||||
/** The report title. */
|
||||
@@ -26,7 +26,7 @@ public class ReportInfo implements Serializable {
|
||||
/** The timestamp for the report. */
|
||||
public final String reportTimestamp;
|
||||
|
||||
public ReportInfo(UserAction userAction, String sender, String reportTitle, String reportStringPrefix, String reportString, String reportStringSuffix, boolean addReportInfoToMarkdown) {
|
||||
public ReportInfo(String userAction, String sender, String reportTitle, String reportStringPrefix, String reportString, String reportStringSuffix, boolean addReportInfoToMarkdown) {
|
||||
this.userAction = userAction;
|
||||
this.sender = sender;
|
||||
this.reportTitle = reportTitle;
|
||||
@@ -34,7 +34,7 @@ public class ReportInfo implements Serializable {
|
||||
this.reportString = reportString;
|
||||
this.reportStringSuffix = reportStringSuffix;
|
||||
this.addReportInfoToMarkdown = addReportInfoToMarkdown;
|
||||
this.reportTimestamp = TermuxUtils.getCurrentTimeStamp();
|
||||
this.reportTimestamp = AndroidUtils.getCurrentTimeStamp();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -0,0 +1,170 @@
|
||||
package com.termux.shared.models;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
|
||||
import java.util.Formatter;
|
||||
|
||||
public class ResultConfig {
|
||||
|
||||
/** Defines {@link PendingIntent} that should be sent with the result of the command. We cannot
|
||||
* implement {@link java.io.Serializable} because {@link PendingIntent} cannot be serialized. */
|
||||
public PendingIntent resultPendingIntent;
|
||||
/** The key with which to send result {@link android.os.Bundle} in {@link #resultPendingIntent}. */
|
||||
public String resultBundleKey;
|
||||
/** The key with which to send {@link ResultData#stdout} in {@link #resultPendingIntent}. */
|
||||
public String resultStdoutKey;
|
||||
/** The key with which to send {@link ResultData#stderr} in {@link #resultPendingIntent}. */
|
||||
public String resultStderrKey;
|
||||
/** The key with which to send {@link ResultData#exitCode} in {@link #resultPendingIntent}. */
|
||||
public String resultExitCodeKey;
|
||||
/** The key with which to send {@link ResultData#errorsList} errCode in {@link #resultPendingIntent}. */
|
||||
public String resultErrCodeKey;
|
||||
/** The key with which to send {@link ResultData#errorsList} errmsg in {@link #resultPendingIntent}. */
|
||||
public String resultErrmsgKey;
|
||||
/** The key with which to send original length of {@link ResultData#stdout} in {@link #resultPendingIntent}. */
|
||||
public String resultStdoutOriginalLengthKey;
|
||||
/** The key with which to send original length of {@link ResultData#stderr} in {@link #resultPendingIntent}. */
|
||||
public String resultStderrOriginalLengthKey;
|
||||
|
||||
|
||||
/** Defines the directory path in which to write the result of the command. */
|
||||
public String resultDirectoryPath;
|
||||
/** Defines the directory path under which {@link #resultDirectoryPath} can exist. */
|
||||
public String resultDirectoryAllowedParentPath;
|
||||
/** Defines whether the result should be written to a single file or multiple files
|
||||
* (err, error, stdout, stderr, exit_code) in {@link #resultDirectoryPath}. */
|
||||
public boolean resultSingleFile;
|
||||
/** Defines the basename of the result file that should be created in {@link #resultDirectoryPath}
|
||||
* if {@link #resultSingleFile} is {@code true}. */
|
||||
public String resultFileBasename;
|
||||
/** Defines the output {@link Formatter} format of the {@link #resultFileBasename} result file. */
|
||||
public String resultFileOutputFormat;
|
||||
/** Defines the error {@link Formatter} format of the {@link #resultFileBasename} result file. */
|
||||
public String resultFileErrorFormat;
|
||||
/** Defines the suffix of the result files that should be created in {@link #resultDirectoryPath}
|
||||
* if {@link #resultSingleFile} is {@code true}. */
|
||||
public String resultFilesSuffix;
|
||||
|
||||
|
||||
public ResultConfig() {
|
||||
}
|
||||
|
||||
|
||||
public boolean isCommandWithPendingResult() {
|
||||
return resultPendingIntent != null || resultDirectoryPath != null;
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return getResultConfigLogString(this, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a log friendly {@link String} for {@link ResultConfig} parameters.
|
||||
*
|
||||
* @param resultConfig The {@link ResultConfig} to convert.
|
||||
* @param ignoreNull Set to {@code true} if non-critical {@code null} values are to be ignored.
|
||||
* @return Returns the log friendly {@link String}.
|
||||
*/
|
||||
public static String getResultConfigLogString(final ResultConfig resultConfig, boolean ignoreNull) {
|
||||
if (resultConfig == null) return "null";
|
||||
|
||||
StringBuilder logString = new StringBuilder();
|
||||
|
||||
logString.append("Result Pending: `").append(resultConfig.isCommandWithPendingResult()).append("`\n");
|
||||
|
||||
if (resultConfig.resultPendingIntent != null) {
|
||||
logString.append(resultConfig.getResultPendingIntentVariablesLogString(ignoreNull));
|
||||
if (resultConfig.resultDirectoryPath != null)
|
||||
logString.append("\n");
|
||||
}
|
||||
|
||||
if (resultConfig.resultDirectoryPath != null && !resultConfig.resultDirectoryPath.isEmpty())
|
||||
logString.append(resultConfig.getResultDirectoryVariablesLogString(ignoreNull));
|
||||
|
||||
return logString.toString();
|
||||
}
|
||||
|
||||
public String getResultPendingIntentVariablesLogString(boolean ignoreNull) {
|
||||
if (resultPendingIntent == null) return "Result PendingIntent Creator: -";
|
||||
|
||||
StringBuilder resultPendingIntentVariablesString = new StringBuilder();
|
||||
|
||||
resultPendingIntentVariablesString.append("Result PendingIntent Creator: `").append(resultPendingIntent.getCreatorPackage()).append("`");
|
||||
|
||||
if (!ignoreNull || resultBundleKey != null)
|
||||
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Bundle Key", resultBundleKey, "-"));
|
||||
if (!ignoreNull || resultStdoutKey != null)
|
||||
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Stdout Key", resultStdoutKey, "-"));
|
||||
if (!ignoreNull || resultStderrKey != null)
|
||||
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Stderr Key", resultStderrKey, "-"));
|
||||
if (!ignoreNull || resultExitCodeKey != null)
|
||||
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Exit Code Key", resultExitCodeKey, "-"));
|
||||
if (!ignoreNull || resultErrCodeKey != null)
|
||||
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Err Code Key", resultErrCodeKey, "-"));
|
||||
if (!ignoreNull || resultErrmsgKey != null)
|
||||
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Error Key", resultErrmsgKey, "-"));
|
||||
if (!ignoreNull || resultStdoutOriginalLengthKey != null)
|
||||
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Stdout Original Length Key", resultStdoutOriginalLengthKey, "-"));
|
||||
if (!ignoreNull || resultStderrOriginalLengthKey != null)
|
||||
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Stderr Original Length Key", resultStderrOriginalLengthKey, "-"));
|
||||
|
||||
return resultPendingIntentVariablesString.toString();
|
||||
}
|
||||
|
||||
public String getResultDirectoryVariablesLogString(boolean ignoreNull) {
|
||||
if (resultDirectoryPath == null) return "Result Directory Path: -";
|
||||
|
||||
StringBuilder resultDirectoryVariablesString = new StringBuilder();
|
||||
|
||||
resultDirectoryVariablesString.append(Logger.getSingleLineLogStringEntry("Result Directory Path", resultDirectoryPath, "-"));
|
||||
|
||||
resultDirectoryVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Single File", resultSingleFile, "-"));
|
||||
if (!ignoreNull || resultFileBasename != null)
|
||||
resultDirectoryVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result File Basename", resultFileBasename, "-"));
|
||||
if (!ignoreNull || resultFileOutputFormat != null)
|
||||
resultDirectoryVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result File Output Format", resultFileOutputFormat, "-"));
|
||||
if (!ignoreNull || resultFileErrorFormat != null)
|
||||
resultDirectoryVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result File Error Format", resultFileErrorFormat, "-"));
|
||||
if (!ignoreNull || resultFilesSuffix != null)
|
||||
resultDirectoryVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Files Suffix", resultFilesSuffix, "-"));
|
||||
|
||||
return resultDirectoryVariablesString.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a markdown {@link String} for {@link ResultConfig}.
|
||||
*
|
||||
* @param resultConfig The {@link ResultConfig} to convert.
|
||||
* @return Returns the markdown {@link String}.
|
||||
*/
|
||||
public static String getResultConfigMarkdownString(final ResultConfig resultConfig) {
|
||||
if (resultConfig == null) return "null";
|
||||
|
||||
StringBuilder markdownString = new StringBuilder();
|
||||
|
||||
if (resultConfig.resultPendingIntent != null)
|
||||
markdownString.append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result PendingIntent Creator", resultConfig.resultPendingIntent.getCreatorPackage(), "-"));
|
||||
else
|
||||
markdownString.append("**Result PendingIntent Creator:** - ");
|
||||
|
||||
if (resultConfig.resultDirectoryPath != null) {
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result Directory Path", resultConfig.resultDirectoryPath, "-"));
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result Single File", resultConfig.resultSingleFile, "-"));
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result File Basename", resultConfig.resultFileBasename, "-"));
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result File Output Format", resultConfig.resultFileOutputFormat, "-"));
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result File Error Format", resultConfig.resultFileErrorFormat, "-"));
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result Files Suffix", resultConfig.resultFilesSuffix, "-"));
|
||||
}
|
||||
|
||||
return markdownString.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
package com.termux.shared.models;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.models.errors.Errno;
|
||||
import com.termux.shared.models.errors.Error;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class ResultData implements Serializable {
|
||||
|
||||
/** The stdout of command. */
|
||||
public final StringBuilder stdout = new StringBuilder();
|
||||
/** The stderr of command. */
|
||||
public final StringBuilder stderr = new StringBuilder();
|
||||
/** The exit code of command. */
|
||||
public Integer exitCode;
|
||||
|
||||
/** The internal errors list of command. */
|
||||
public List<Error> errorsList = new ArrayList<>();
|
||||
|
||||
|
||||
public ResultData() {
|
||||
}
|
||||
|
||||
|
||||
public void clearStdout() {
|
||||
stdout.setLength(0);
|
||||
}
|
||||
|
||||
public StringBuilder prependStdout(String message) {
|
||||
return stdout.insert(0, message);
|
||||
}
|
||||
|
||||
public StringBuilder prependStdoutLn(String message) {
|
||||
return stdout.insert(0, message + "\n");
|
||||
}
|
||||
|
||||
public StringBuilder appendStdout(String message) {
|
||||
return stdout.append(message);
|
||||
}
|
||||
|
||||
public StringBuilder appendStdoutLn(String message) {
|
||||
return stdout.append(message).append("\n");
|
||||
}
|
||||
|
||||
|
||||
public void clearStderr() {
|
||||
stderr.setLength(0);
|
||||
}
|
||||
|
||||
public StringBuilder prependStderr(String message) {
|
||||
return stderr.insert(0, message);
|
||||
}
|
||||
|
||||
public StringBuilder prependStderrLn(String message) {
|
||||
return stderr.insert(0, message + "\n");
|
||||
}
|
||||
|
||||
public StringBuilder appendStderr(String message) {
|
||||
return stderr.append(message);
|
||||
}
|
||||
|
||||
public StringBuilder appendStderrLn(String message) {
|
||||
return stderr.append(message).append("\n");
|
||||
}
|
||||
|
||||
|
||||
public synchronized boolean setStateFailed(@NonNull Error error) {
|
||||
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), null);
|
||||
}
|
||||
|
||||
public synchronized boolean setStateFailed(@NonNull Error error, Throwable throwable) {
|
||||
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), Collections.singletonList(throwable));
|
||||
}
|
||||
public synchronized boolean setStateFailed(@NonNull Error error, List<Throwable> throwablesList) {
|
||||
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), throwablesList);
|
||||
}
|
||||
|
||||
public synchronized boolean setStateFailed(int code, String message) {
|
||||
return setStateFailed(null, code, message, null);
|
||||
}
|
||||
|
||||
public synchronized boolean setStateFailed(int code, String message, Throwable throwable) {
|
||||
return setStateFailed(null, code, message, Collections.singletonList(throwable));
|
||||
}
|
||||
|
||||
public synchronized boolean setStateFailed(int code, String message, List<Throwable> throwablesList) {
|
||||
return setStateFailed(null, code, message, throwablesList);
|
||||
}
|
||||
|
||||
public synchronized boolean setStateFailed(String type, int code, String message, List<Throwable> throwablesList) {
|
||||
if (errorsList == null)
|
||||
errorsList = new ArrayList<>();
|
||||
|
||||
Error error = new Error();
|
||||
errorsList.add(error);
|
||||
|
||||
return error.setStateFailed(type, code, message, throwablesList);
|
||||
}
|
||||
|
||||
public boolean isStateFailed() {
|
||||
if (errorsList != null) {
|
||||
for (Error error : errorsList)
|
||||
if (error.isStateFailed())
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public int getErrCode() {
|
||||
if (errorsList != null && errorsList.size() > 0)
|
||||
return errorsList.get(errorsList.size() - 1).getCode();
|
||||
else
|
||||
return Errno.ERRNO_SUCCESS.getCode();
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return getResultDataLogString(this, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a log friendly {@link String} for {@link ResultData} parameters.
|
||||
*
|
||||
* @param resultData The {@link ResultData} to convert.
|
||||
* @param ignoreNull Set to {@code true} if non-critical {@code null} values are to be ignored.
|
||||
* @return Returns the log friendly {@link String}.
|
||||
*/
|
||||
public static String getResultDataLogString(final ResultData resultData, boolean ignoreNull) {
|
||||
if (resultData == null) return "null";
|
||||
|
||||
StringBuilder logString = new StringBuilder();
|
||||
|
||||
logString.append("\n").append(resultData.getStdoutLogString());
|
||||
logString.append("\n").append(resultData.getStderrLogString());
|
||||
logString.append("\n").append(resultData.getExitCodeLogString());
|
||||
|
||||
logString.append("\n\n").append(getErrorsListLogString(resultData));
|
||||
|
||||
return logString.toString();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public String getStdoutLogString() {
|
||||
if (stdout.toString().isEmpty())
|
||||
return Logger.getSingleLineLogStringEntry("Stdout", null, "-");
|
||||
else
|
||||
return Logger.getMultiLineLogStringEntry("Stdout", DataUtils.getTruncatedCommandOutput(stdout.toString(), Logger.LOGGER_ENTRY_MAX_SAFE_PAYLOAD / 5, false, false, true), "-");
|
||||
}
|
||||
|
||||
public String getStderrLogString() {
|
||||
if (stderr.toString().isEmpty())
|
||||
return Logger.getSingleLineLogStringEntry("Stderr", null, "-");
|
||||
else
|
||||
return Logger.getMultiLineLogStringEntry("Stderr", DataUtils.getTruncatedCommandOutput(stderr.toString(), Logger.LOGGER_ENTRY_MAX_SAFE_PAYLOAD / 5, false, false, true), "-");
|
||||
}
|
||||
|
||||
public String getExitCodeLogString() {
|
||||
return Logger.getSingleLineLogStringEntry("Exit Code", exitCode, "-");
|
||||
}
|
||||
|
||||
public static String getErrorsListLogString(final ResultData resultData) {
|
||||
if (resultData == null) return "null";
|
||||
|
||||
StringBuilder logString = new StringBuilder();
|
||||
|
||||
if (resultData.errorsList != null) {
|
||||
for (Error error : resultData.errorsList) {
|
||||
if (error.isStateFailed()) {
|
||||
if (!logString.toString().isEmpty())
|
||||
logString.append("\n");
|
||||
logString.append(Error.getErrorLogString(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return logString.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a markdown {@link String} for {@link ResultData}.
|
||||
*
|
||||
* @param resultData The {@link ResultData} to convert.
|
||||
* @return Returns the markdown {@link String}.
|
||||
*/
|
||||
public static String getResultDataMarkdownString(final ResultData resultData) {
|
||||
if (resultData == null) return "null";
|
||||
|
||||
StringBuilder markdownString = new StringBuilder();
|
||||
|
||||
if (resultData.stdout.toString().isEmpty())
|
||||
markdownString.append(MarkdownUtils.getSingleLineMarkdownStringEntry("Stdout", null, "-"));
|
||||
else
|
||||
markdownString.append(MarkdownUtils.getMultiLineMarkdownStringEntry("Stdout", resultData.stdout.toString(), "-"));
|
||||
|
||||
if (resultData.stderr.toString().isEmpty())
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Stderr", null, "-"));
|
||||
else
|
||||
markdownString.append("\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Stderr", resultData.stderr.toString(), "-"));
|
||||
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Exit Code", resultData.exitCode, "-"));
|
||||
|
||||
markdownString.append("\n\n").append(getErrorsListMarkdownString(resultData));
|
||||
|
||||
|
||||
return markdownString.toString();
|
||||
}
|
||||
|
||||
public static String getErrorsListMarkdownString(final ResultData resultData) {
|
||||
if (resultData == null) return "null";
|
||||
|
||||
StringBuilder markdownString = new StringBuilder();
|
||||
|
||||
if (resultData.errorsList != null) {
|
||||
for (Error error : resultData.errorsList) {
|
||||
if (error.isStateFailed()) {
|
||||
if (!markdownString.toString().isEmpty())
|
||||
markdownString.append("\n");
|
||||
markdownString.append(Error.getErrorMarkdownString(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return markdownString.toString();
|
||||
}
|
||||
|
||||
public static String getErrorsListMinimalString(final ResultData resultData) {
|
||||
if (resultData == null) return "null";
|
||||
|
||||
StringBuilder minimalString = new StringBuilder();
|
||||
|
||||
if (resultData.errorsList != null) {
|
||||
for (Error error : resultData.errorsList) {
|
||||
if (error.isStateFailed()) {
|
||||
if (!minimalString.toString().isEmpty())
|
||||
minimalString.append("\n");
|
||||
minimalString.append(Error.getMinimalErrorString(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return minimalString.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.termux.shared.models.errors;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/** The {@link Class} that defines error messages and codes. */
|
||||
public class Errno {
|
||||
|
||||
public static final String TYPE = "Error";
|
||||
|
||||
|
||||
public static final Errno ERRNO_SUCCESS = new Errno(TYPE, Activity.RESULT_OK, "Success");
|
||||
public static final Errno ERRNO_CANCELLED = new Errno(TYPE, Activity.RESULT_CANCELED, "Cancelled");
|
||||
public static final Errno ERRNO_MINOR_FAILURES = new Errno(TYPE, Activity.RESULT_FIRST_USER, "Minor failure");
|
||||
public static final Errno ERRNO_FAILED = new Errno(TYPE, Activity.RESULT_FIRST_USER + 1, "Failed");
|
||||
|
||||
/** The errno type. */
|
||||
protected String type;
|
||||
/** The errno code. */
|
||||
protected final int code;
|
||||
/** The errno message. */
|
||||
protected final String message;
|
||||
|
||||
private static final String LOG_TAG = "Errno";
|
||||
|
||||
|
||||
public Errno(final String type, final int code, final String message) {
|
||||
this.type = type;
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "type=" + type + ", code=" + code + ", message=\"" + message + "\"";
|
||||
}
|
||||
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public Error getError() {
|
||||
return new Error(getType(), getCode(), getMessage());
|
||||
}
|
||||
|
||||
public Error getError(Object... args) {
|
||||
try {
|
||||
return new Error(getType(), getCode(), String.format(getMessage(), args));
|
||||
} catch (Exception e) {
|
||||
Logger.logWarn(LOG_TAG, "Exception raised while calling String.format() for error message of errno " + this + " with args" + Arrays.toString(args) + "\n" + e.getMessage());
|
||||
// Return unformatted message as a backup
|
||||
return new Error(getType(), getCode(), getMessage() + ": " + Arrays.toString(args));
|
||||
}
|
||||
}
|
||||
|
||||
public Error getError(Throwable throwable, Object... args) {
|
||||
return getError(Collections.singletonList(throwable), args);
|
||||
}
|
||||
|
||||
public Error getError(List<Throwable> throwablesList, Object... args) {
|
||||
try {
|
||||
return new Error(getType(), getCode(), String.format(getMessage(), args), throwablesList);
|
||||
} catch (Exception e) {
|
||||
Logger.logWarn(LOG_TAG, "Exception raised while calling String.format() for error message of errno " + this + " with args" + Arrays.toString(args) + "\n" + e.getMessage());
|
||||
// Return unformatted message as a backup
|
||||
return new Error(getType(), getCode(), getMessage() + ": " + Arrays.toString(args), throwablesList);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package com.termux.shared.models.errors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class Error implements Serializable {
|
||||
|
||||
/** The error type. */
|
||||
private String type;
|
||||
/** The error code. */
|
||||
private int code;
|
||||
/** The error message. */
|
||||
private String message;
|
||||
/** The error exceptions. */
|
||||
private List<Throwable> throwablesList = new ArrayList<>();
|
||||
|
||||
private static final String LOG_TAG = "Error";
|
||||
|
||||
|
||||
public Error() {
|
||||
InitError(null, null, null, null);
|
||||
}
|
||||
|
||||
public Error(String type, Integer code, String message, List<Throwable> throwablesList) {
|
||||
InitError(type, code, message, throwablesList);
|
||||
}
|
||||
|
||||
public Error(String type, Integer code, String message, Throwable throwable) {
|
||||
InitError(type, code, message, Collections.singletonList(throwable));
|
||||
}
|
||||
|
||||
public Error(String type, Integer code, String message) {
|
||||
InitError(type, code, message, null);
|
||||
}
|
||||
|
||||
public Error(Integer code, String message, List<Throwable> throwablesList) {
|
||||
InitError(null, code, message, throwablesList);
|
||||
}
|
||||
|
||||
public Error(Integer code, String message, Throwable throwable) {
|
||||
InitError(null, code, message, Collections.singletonList(throwable));
|
||||
}
|
||||
|
||||
public Error(Integer code, String message) {
|
||||
InitError(null, code, message, null);
|
||||
}
|
||||
|
||||
public Error(String message, Throwable throwable) {
|
||||
InitError(null, null, message, Collections.singletonList(throwable));
|
||||
}
|
||||
|
||||
public Error(String message, List<Throwable> throwablesList) {
|
||||
InitError(null, null, message, throwablesList);
|
||||
}
|
||||
|
||||
public Error(String message) {
|
||||
InitError(null, null, message, null);
|
||||
}
|
||||
|
||||
private void InitError(String type, Integer code, String message, List<Throwable> throwablesList) {
|
||||
if (type != null && !type.isEmpty())
|
||||
this.type = type;
|
||||
else
|
||||
this.type = Errno.TYPE;
|
||||
|
||||
if (code != null && code > Errno.ERRNO_SUCCESS.getCode())
|
||||
this.code = code;
|
||||
else
|
||||
this.code = Errno.ERRNO_SUCCESS.getCode();
|
||||
|
||||
this.message = message;
|
||||
this.throwablesList = throwablesList;
|
||||
}
|
||||
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public Integer getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void prependMessage(String message) {
|
||||
if (message != null && isStateFailed())
|
||||
this.message = message + this.message;
|
||||
}
|
||||
|
||||
public void appendMessage(String message) {
|
||||
if (message != null && isStateFailed())
|
||||
this.message = this.message + message;
|
||||
}
|
||||
|
||||
public List<Throwable> getThrowablesList() {
|
||||
return Collections.unmodifiableList(throwablesList);
|
||||
}
|
||||
|
||||
|
||||
public synchronized boolean setStateFailed(@NonNull Error error) {
|
||||
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), null);
|
||||
}
|
||||
|
||||
public synchronized boolean setStateFailed(@NonNull Error error, Throwable throwable) {
|
||||
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), Collections.singletonList(throwable));
|
||||
}
|
||||
public synchronized boolean setStateFailed(@NonNull Error error, List<Throwable> throwablesList) {
|
||||
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), throwablesList);
|
||||
}
|
||||
|
||||
public synchronized boolean setStateFailed(int code, String message) {
|
||||
return setStateFailed(this.type, code, message, null);
|
||||
}
|
||||
|
||||
public synchronized boolean setStateFailed(int code, String message, Throwable throwable) {
|
||||
return setStateFailed(this.type, code, message, Collections.singletonList(throwable));
|
||||
}
|
||||
|
||||
public synchronized boolean setStateFailed(int code, String message, List<Throwable> throwablesList) {
|
||||
return setStateFailed(this.type, code, message, throwablesList);
|
||||
}
|
||||
|
||||
public synchronized boolean setStateFailed(String type, int code, String message, List<Throwable> throwablesList) {
|
||||
this.message = message;
|
||||
this.throwablesList = throwablesList;
|
||||
|
||||
if (type != null && !type.isEmpty())
|
||||
this.type = type;
|
||||
|
||||
if (code > Errno.ERRNO_SUCCESS.getCode()) {
|
||||
this.code = code;
|
||||
return true;
|
||||
} else {
|
||||
Logger.logWarn(LOG_TAG, "Ignoring invalid error code value \"" + code + "\". Force setting it to RESULT_CODE_FAILED \"" + Errno.ERRNO_FAILED.getCode() + "\"");
|
||||
this.code = Errno.ERRNO_FAILED.getCode();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isStateFailed() {
|
||||
return code > Errno.ERRNO_SUCCESS.getCode();
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return getErrorLogString(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a log friendly {@link String} for {@link Error} error parameters.
|
||||
*
|
||||
* @param error The {@link Error} to convert.
|
||||
* @return Returns the log friendly {@link String}.
|
||||
*/
|
||||
public static String getErrorLogString(final Error error) {
|
||||
if (error == null) return "null";
|
||||
|
||||
StringBuilder logString = new StringBuilder();
|
||||
|
||||
logString.append(error.getCodeString());
|
||||
logString.append("\n").append(error.getTypeAndMessageLogString());
|
||||
if (error.throwablesList != null)
|
||||
logString.append("\n").append(error.geStackTracesLogString());
|
||||
|
||||
return logString.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a minimal log friendly {@link String} for {@link Error} error parameters.
|
||||
*
|
||||
* @param error The {@link Error} to convert.
|
||||
* @return Returns the log friendly {@link String}.
|
||||
*/
|
||||
public static String getMinimalErrorLogString(final Error error) {
|
||||
if (error == null) return "null";
|
||||
|
||||
StringBuilder logString = new StringBuilder();
|
||||
|
||||
logString.append(error.getCodeString());
|
||||
logString.append(error.getTypeAndMessageLogString());
|
||||
|
||||
return logString.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a minimal {@link String} for {@link Error} error parameters.
|
||||
*
|
||||
* @param error The {@link Error} to convert.
|
||||
* @return Returns the {@link String}.
|
||||
*/
|
||||
public static String getMinimalErrorString(final Error error) {
|
||||
if (error == null) return "null";
|
||||
|
||||
StringBuilder logString = new StringBuilder();
|
||||
|
||||
logString.append("(").append(error.getCode()).append(") ");
|
||||
logString.append(error.getType()).append(": ").append(error.getMessage());
|
||||
|
||||
return logString.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a markdown {@link String} for {@link Error}.
|
||||
*
|
||||
* @param error The {@link Error} to convert.
|
||||
* @return Returns the markdown {@link String}.
|
||||
*/
|
||||
public static String getErrorMarkdownString(final Error error) {
|
||||
if (error == null) return "null";
|
||||
|
||||
StringBuilder markdownString = new StringBuilder();
|
||||
|
||||
markdownString.append(MarkdownUtils.getSingleLineMarkdownStringEntry("Error Code", error.getCode(), "-"));
|
||||
markdownString.append("\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry((Errno.TYPE.equals(error.getType()) ? "Error Message" : "Error Message (" + error.getType() + ")"), error.message, "-"));
|
||||
markdownString.append("\n\n").append(error.geStackTracesMarkdownString());
|
||||
|
||||
return markdownString.toString();
|
||||
}
|
||||
|
||||
|
||||
public String getCodeString() {
|
||||
return Logger.getSingleLineLogStringEntry("Error Code", code, "-");
|
||||
}
|
||||
|
||||
public String getTypeAndMessageLogString() {
|
||||
return Logger.getMultiLineLogStringEntry(Errno.TYPE.equals(type) ? "Error Message" : "Error Message (" + type + ")", message, "-");
|
||||
}
|
||||
|
||||
public String geStackTracesLogString() {
|
||||
return Logger.getStackTracesString("StackTraces:", Logger.getStackTracesStringArray(throwablesList));
|
||||
}
|
||||
|
||||
public String geStackTracesMarkdownString() {
|
||||
return Logger.getStackTracesMarkdownString("StackTraces", Logger.getStackTracesStringArray(throwablesList));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.termux.shared.models.errors;
|
||||
|
||||
/** The {@link Class} that defines FileUtils error messages and codes. */
|
||||
public class FileUtilsErrno extends Errno {
|
||||
|
||||
public static final String TYPE = "FileUtils Error";
|
||||
|
||||
|
||||
/* Errors for null or empty paths (100-150) */
|
||||
public static final Errno ERRNO_EXECUTABLE_REQUIRED = new Errno(TYPE, 100, "Executable required.");
|
||||
public static final Errno ERRNO_NULL_OR_EMPTY_REGULAR_FILE_PATH = new Errno(TYPE, 101, "The regular file path is null or empty.");
|
||||
public static final Errno ERRNO_NULL_OR_EMPTY_REGULAR_FILE = new Errno(TYPE, 102, "The regular file is null or empty.");
|
||||
public static final Errno ERRNO_NULL_OR_EMPTY_EXECUTABLE_FILE_PATH = new Errno(TYPE, 103, "The executable file path is null or empty.");
|
||||
public static final Errno ERRNO_NULL_OR_EMPTY_EXECUTABLE_FILE = new Errno(TYPE, 104, "The executable file is null or empty.");
|
||||
public static final Errno ERRNO_NULL_OR_EMPTY_DIRECTORY_FILE_PATH = new Errno(TYPE, 105, "The directory file path is null or empty.");
|
||||
public static final Errno ERRNO_NULL_OR_EMPTY_DIRECTORY_FILE = new Errno(TYPE, 106, "The directory file is null or empty.");
|
||||
|
||||
|
||||
|
||||
/* Errors for invalid or not found files at path (150-200) */
|
||||
public static final Errno ERRNO_FILE_NOT_FOUND_AT_PATH = new Errno(TYPE, 150, "The %1$s is not found at path \"%2$s\".");
|
||||
|
||||
public static final Errno ERRNO_NO_REGULAR_FILE_FOUND = new Errno(TYPE, 151, "Regular file not found at %1$s path.");
|
||||
public static final Errno ERRNO_NOT_A_REGULAR_FILE = new Errno(TYPE, 152, "The %1$s at path \"%2$s\" is not a regular file.");
|
||||
|
||||
public static final Errno ERRNO_NON_REGULAR_FILE_FOUND = new Errno(TYPE, 153, "Non-regular file found at %1$s path.");
|
||||
public static final Errno ERRNO_NON_DIRECTORY_FILE_FOUND = new Errno(TYPE, 154, "Non-directory file found at %1$s path.");
|
||||
public static final Errno ERRNO_NON_SYMLINK_FILE_FOUND = new Errno(TYPE, 155, "Non-symlink file found at %1$s path.");
|
||||
|
||||
public static final Errno ERRNO_FILE_NOT_AN_ALLOWED_FILE_TYPE = new Errno(TYPE, 156, "The %1$s found at path \"%2$s\" is not one of allowed file types \"%3$s\".");
|
||||
|
||||
public static final Errno ERRNO_VALIDATE_FILE_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 157, "Validating file existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s");
|
||||
public static final Errno ERRNO_VALIDATE_DIRECTORY_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 158, "Validating directory existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s");
|
||||
|
||||
|
||||
|
||||
/* Errors for file creation (200-250) */
|
||||
public static final Errno ERRNO_CREATING_FILE_FAILED = new Errno(TYPE, 200, "Creating %1$s at path \"%2$s\" failed.");
|
||||
public static final Errno ERRNO_CREATING_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 201, "Creating %1$s at path \"%2$s\" failed.\nException: %3$s");
|
||||
|
||||
public static final Errno ERRNO_CANNOT_OVERWRITE_A_NON_SYMLINK_FILE_TYPE = new Errno(TYPE, 202, "Cannot overwrite %1$s while creating symlink at \"%2$s\" to \"%3$s\" since destination file type \"%4$s\" is not a symlink.");
|
||||
public static final Errno ERRNO_CREATING_SYMLINK_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 203, "Creating %1$s at path \"%2$s\" to \"%3$s\" failed.\nException: %4$s");
|
||||
|
||||
|
||||
|
||||
/* Errors for file copying and moving (250-300) */
|
||||
public static final Errno ERRNO_COPYING_OR_MOVING_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 250, "%1$s from \"%2$s\" to \"%3$s\" failed.\nException: %4$s");
|
||||
public static final Errno ERRNO_COPYING_OR_MOVING_FILE_TO_SAME_PATH = new Errno(TYPE, 251, "%1$s from \"%2$s\" to \"%3$s\" cannot be done since they point to the same path.");
|
||||
public static final Errno ERRNO_CANNOT_OVERWRITE_A_DIFFERENT_FILE_TYPE = new Errno(TYPE, 252, "Cannot overwrite %1$s while %2$s it from \"%3$s\" to \"%4$s\" since destination file type \"%5$s\" is different from source file type \"%6$s\".");
|
||||
public static final Errno ERRNO_CANNOT_MOVE_DIRECTORY_TO_SUB_DIRECTORY_OF_ITSELF = new Errno(TYPE, 253, "Cannot move %1$s from \"%2$s\" to \"%3$s\" since destination is a subdirectory of the source.");
|
||||
|
||||
|
||||
|
||||
/* Errors for file deletion (300-350) */
|
||||
public static final Errno ERRNO_DELETING_FILE_FAILED = new Errno(TYPE, 300, "Deleting %1$s at path \"%2$s\" failed.");
|
||||
public static final Errno ERRNO_DELETING_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 301, "Deleting %1$s at path \"%2$s\" failed.\nException: %3$s");
|
||||
public static final Errno ERRNO_CLEARING_DIRECTORY_FAILED_WITH_EXCEPTION = new Errno(TYPE, 302, "Clearing %1$s at path \"%2$s\" failed.\nException: %3$s");
|
||||
public static final Errno ERRNO_FILE_STILL_EXISTS_AFTER_DELETING = new Errno(TYPE, 303, "The %1$s still exists after deleting it from \"%2$s\".");
|
||||
|
||||
|
||||
|
||||
/* Errors for file reading and writing (350-400) */
|
||||
public static final Errno ERRNO_READING_STRING_TO_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 350, "Reading string from %1$s at path \"%2$s\" failed.\nException: %3$s");
|
||||
public static final Errno ERRNO_WRITING_STRING_TO_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 351, "Writing string to %1$s at path \"%2$s\" failed.\nException: %3$s");
|
||||
public static final Errno ERRNO_UNSUPPORTED_CHARSET = new Errno(TYPE, 352, "Unsupported charset \"%1$s\"");
|
||||
public static final Errno ERRNO_CHECKING_IF_CHARSET_SUPPORTED_FAILED = new Errno(TYPE, 353, "Checking if charset \"%1$s\" is supported failed.\nException: %2$s");
|
||||
|
||||
|
||||
|
||||
/* Errors for invalid file permissions (400-450) */
|
||||
public static final Errno ERRNO_INVALID_FILE_PERMISSIONS_STRING_TO_CHECK = new Errno(TYPE, 400, "The file permission string to check is invalid.");
|
||||
public static final Errno ERRNO_FILE_NOT_READABLE = new Errno(TYPE, 401, "The %1$s at path is not readable. Permission Denied.");
|
||||
public static final Errno ERRNO_FILE_NOT_WRITABLE = new Errno(TYPE, 402, "The %1$s at path is not writable. Permission Denied.");
|
||||
public static final Errno ERRNO_FILE_NOT_EXECUTABLE = new Errno(TYPE, 403, "The %1$s at path is not executable. Permission Denied.");
|
||||
|
||||
|
||||
FileUtilsErrno(final String type, final int code, final String message) {
|
||||
super(type, code, message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.termux.shared.models.errors;
|
||||
|
||||
/** The {@link Class} that defines function error messages and codes. */
|
||||
public class FunctionErrno extends Errno {
|
||||
|
||||
public static final String TYPE = "Function Error";
|
||||
|
||||
|
||||
/* Errors for null or empty parameters (100-150) */
|
||||
public static final Errno ERRNO_NULL_OR_EMPTY_PARAMETER = new Errno(TYPE, 100, "The %1$s parameter passed to \"%2$s\" is null or empty.");
|
||||
public static final Errno ERRNO_NULL_OR_EMPTY_PARAMETERS = new Errno(TYPE, 101, "The %1$s parameters passed to \"%2$s\" are null or empty.");
|
||||
public static final Errno ERRNO_UNSET_PARAMETER = new Errno(TYPE, 102, "The %1$s parameter passed to \"%2$s\" must be set.");
|
||||
public static final Errno ERRNO_UNSET_PARAMETERS = new Errno(TYPE, 103, "The %1$s parameters passed to \"%2$s\" must be set.");
|
||||
|
||||
|
||||
FunctionErrno(final String type, final int code, final String message) {
|
||||
super(type, code, message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.termux.shared.models.errors;
|
||||
|
||||
/** The {@link Class} that defines ResultSender error messages and codes. */
|
||||
public class ResultSenderErrno extends Errno {
|
||||
|
||||
public static final String TYPE = "ResultSender Error";
|
||||
|
||||
|
||||
/* Errors for null or empty parameters (100-150) */
|
||||
public static final Errno ERROR_RESULT_FILE_BASENAME_NULL_OR_INVALID = new Errno(TYPE, 100, "The result file basename \"%1$s\" is null, empty or contains forward slashes \"/\".");
|
||||
public static final Errno ERROR_RESULT_FILES_SUFFIX_INVALID = new Errno(TYPE, 101, "The result files suffix \"%1$s\" contains forward slashes \"/\".");
|
||||
public static final Errno ERROR_FORMAT_RESULT_ERROR_FAILED_WITH_EXCEPTION = new Errno(TYPE, 102, "Formatting result error failed.\nException: %1$s");
|
||||
public static final Errno ERROR_FORMAT_RESULT_OUTPUT_FAILED_WITH_EXCEPTION = new Errno(TYPE, 103, "Formatting result output failed.\nException: %1$s");
|
||||
|
||||
|
||||
ResultSenderErrno(final String type, final int code, final String message) {
|
||||
super(type, code, message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,9 +10,6 @@ import android.os.Build;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.settings.preferences.TermuxPreferenceConstants;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
|
||||
public class NotificationUtils {
|
||||
|
||||
@@ -49,39 +46,12 @@ public class NotificationUtils {
|
||||
return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to get the next unique notification id that isn't already being used by the app.
|
||||
*
|
||||
* Termux app and its plugin must use unique notification ids from the same pool due to usage of android:sharedUserId.
|
||||
* https://commonsware.com/blog/2017/06/07/jobscheduler-job-ids-libraries.html
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @return Returns the notification id that should be safe to use.
|
||||
*/
|
||||
public synchronized static int getNextNotificationId(final Context context) {
|
||||
if (context == null) return TermuxPreferenceConstants.TERMUX_APP.DEFAULT_VALUE_KEY_LAST_NOTIFICATION_ID;
|
||||
|
||||
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(context);
|
||||
int lastNotificationId = preferences.getLastNotificationId();
|
||||
|
||||
int nextNotificationId = lastNotificationId + 1;
|
||||
while(nextNotificationId == TermuxConstants.TERMUX_APP_NOTIFICATION_ID || nextNotificationId == TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_ID) {
|
||||
nextNotificationId++;
|
||||
}
|
||||
|
||||
if (nextNotificationId == Integer.MAX_VALUE || nextNotificationId < 0)
|
||||
nextNotificationId = TermuxPreferenceConstants.TERMUX_APP.DEFAULT_VALUE_KEY_LAST_NOTIFICATION_ID;
|
||||
|
||||
preferences.setLastNotificationId(nextNotificationId);
|
||||
return nextNotificationId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get {@link Notification.Builder}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param title The title for the notification.
|
||||
* @param notifiationText The second line text of the notification.
|
||||
* @param notificationText The second line text of the notification.
|
||||
* @param notificationBigText The full text of the notification that may optionally be styled.
|
||||
* @param pendingIntent The {@link PendingIntent} which should be sent when notification is clicked.
|
||||
* @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}.
|
||||
@@ -90,11 +60,11 @@ public class NotificationUtils {
|
||||
* @return Returns the {@link Notification.Builder}.
|
||||
*/
|
||||
@Nullable
|
||||
public static Notification.Builder geNotificationBuilder(final Context context, final String channelId, final int priority, final CharSequence title, final CharSequence notifiationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) {
|
||||
public static Notification.Builder geNotificationBuilder(final Context context, final String channelId, final int priority, final CharSequence title, final CharSequence notificationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) {
|
||||
if (context == null) return null;
|
||||
Notification.Builder builder = new Notification.Builder(context);
|
||||
builder.setContentTitle(title);
|
||||
builder.setContentText(notifiationText);
|
||||
builder.setContentText(notificationText);
|
||||
builder.setStyle(new Notification.BigTextStyle().bigText(notificationBigText));
|
||||
builder.setContentIntent(pendingIntent);
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.termux.shared.notification;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.settings.preferences.TermuxPreferenceConstants;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
|
||||
public class TermuxNotificationUtils {
|
||||
/**
|
||||
* Try to get the next unique notification id that isn't already being used by the app.
|
||||
*
|
||||
* Termux app and its plugin must use unique notification ids from the same pool due to usage of android:sharedUserId.
|
||||
* https://commonsware.com/blog/2017/06/07/jobscheduler-job-ids-libraries.html
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @return Returns the notification id that should be safe to use.
|
||||
*/
|
||||
public synchronized static int getNextNotificationId(final Context context) {
|
||||
if (context == null) return TermuxPreferenceConstants.TERMUX_APP.DEFAULT_VALUE_KEY_LAST_NOTIFICATION_ID;
|
||||
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
||||
if (preferences == null) return TermuxPreferenceConstants.TERMUX_APP.DEFAULT_VALUE_KEY_LAST_NOTIFICATION_ID;
|
||||
|
||||
int lastNotificationId = preferences.getLastNotificationId();
|
||||
|
||||
int nextNotificationId = lastNotificationId + 1;
|
||||
while(nextNotificationId == TermuxConstants.TERMUX_APP_NOTIFICATION_ID || nextNotificationId == TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_ID) {
|
||||
nextNotificationId++;
|
||||
}
|
||||
|
||||
if (nextNotificationId == Integer.MAX_VALUE || nextNotificationId < 0)
|
||||
nextNotificationId = TermuxPreferenceConstants.TERMUX_APP.DEFAULT_VALUE_KEY_LAST_NOTIFICATION_ID;
|
||||
|
||||
preferences.setLastNotificationId(nextNotificationId);
|
||||
return nextNotificationId;
|
||||
}
|
||||
}
|
||||
@@ -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.MessageDialogUtils;
|
||||
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);
|
||||
MessageDialogUtils.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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,31 +1,41 @@
|
||||
package com.termux.shared.packages;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.PowerManager;
|
||||
import android.provider.Settings;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.termux.shared.R;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class PermissionUtils {
|
||||
|
||||
public static final int ACTION_MANAGE_OVERLAY_PERMISSION_REQUEST_CODE = 0;
|
||||
public static final int REQUEST_GRANT_STORAGE_PERMISSION = 1000;
|
||||
|
||||
public static final int REQUEST_DISABLE_BATTERY_OPTIMIZATIONS = 2000;
|
||||
public static final int REQUEST_GRANT_DISPLAY_OVER_OTHER_APPS_PERMISSION = 2001;
|
||||
|
||||
private static final String LOG_TAG = "PermissionUtils";
|
||||
|
||||
public static boolean checkPermissions(Context context, String[] permissions) {
|
||||
int result;
|
||||
|
||||
public static boolean checkPermission(Context context, String permission) {
|
||||
if (permission == null) return false;
|
||||
return checkPermissions(context, new String[]{permission});
|
||||
}
|
||||
|
||||
public static boolean checkPermissions(Context context, String[] permissions) {
|
||||
if (permissions == null) return false;
|
||||
|
||||
int result;
|
||||
for (String p:permissions) {
|
||||
result = ContextCompat.checkSelfPermission(context,p);
|
||||
if (result != PackageManager.PERMISSION_GRANTED) {
|
||||
@@ -35,18 +45,25 @@ public class PermissionUtils {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void askPermissions(Activity context, String[] permissions) {
|
||||
if (context == null || permissions == null) return;
|
||||
|
||||
|
||||
public static void requestPermission(Activity activity, String permission, int requestCode) {
|
||||
if (permission == null) return;
|
||||
requestPermissions(activity, new String[]{permission}, requestCode);
|
||||
}
|
||||
|
||||
public static void requestPermissions(Activity activity, String[] permissions, int requestCode) {
|
||||
if (activity == null || permissions == null) return;
|
||||
|
||||
int result;
|
||||
Logger.showToast(context, context.getString(R.string.message_sudo_please_grant_permissions), true);
|
||||
Logger.showToast(activity, activity.getString(R.string.message_sudo_please_grant_permissions), true);
|
||||
try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}
|
||||
|
||||
for (String permission:permissions) {
|
||||
result = ContextCompat.checkSelfPermission(context, permission);
|
||||
result = ContextCompat.checkSelfPermission(activity, permission);
|
||||
if (result != PackageManager.PERMISSION_GRANTED) {
|
||||
Logger.logDebug(LOG_TAG, "Requesting Permissions: " + Arrays.toString(permissions));
|
||||
context.requestPermissions(new String[]{permission}, 0);
|
||||
activity.requestPermissions(new String[]{permission}, requestCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,34 +71,40 @@ public class PermissionUtils {
|
||||
|
||||
|
||||
public static boolean checkDisplayOverOtherAppsPermission(Context context) {
|
||||
boolean permissionGranted;
|
||||
|
||||
permissionGranted = Settings.canDrawOverlays(context);
|
||||
if (!permissionGranted) {
|
||||
Logger.logWarn(LOG_TAG, TermuxConstants.TERMUX_APP_NAME + " App does not have Display over other apps (SYSTEM_ALERT_WINDOW) permission");
|
||||
return false;
|
||||
} else {
|
||||
Logger.logDebug(LOG_TAG, TermuxConstants.TERMUX_APP_NAME + " App already has Display over other apps (SYSTEM_ALERT_WINDOW) permission");
|
||||
return true;
|
||||
}
|
||||
return Settings.canDrawOverlays(context);
|
||||
}
|
||||
|
||||
public static void askDisplayOverOtherAppsPermission(Activity context) {
|
||||
public static void requestDisplayOverOtherAppsPermission(Activity context, int requestCode) {
|
||||
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + context.getPackageName()));
|
||||
context.startActivityForResult(intent, ACTION_MANAGE_OVERLAY_PERMISSION_REQUEST_CODE);
|
||||
context.startActivityForResult(intent, requestCode);
|
||||
}
|
||||
|
||||
public static boolean validateDisplayOverOtherAppsPermissionForPostAndroid10(Context context) {
|
||||
public static boolean validateDisplayOverOtherAppsPermissionForPostAndroid10(Context context, boolean logResults) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return true;
|
||||
|
||||
if (!PermissionUtils.checkDisplayOverOtherAppsPermission(context)) {
|
||||
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(context);
|
||||
if (preferences.getPluginErrorNotificationsEnabled())
|
||||
Logger.showToast(context, context.getString(R.string.error_display_over_other_apps_permission_not_granted), true);
|
||||
if (logResults)
|
||||
Logger.logWarn(LOG_TAG, context.getPackageName() + " does not have Display over other apps (SYSTEM_ALERT_WINDOW) permission");
|
||||
return false;
|
||||
} else {
|
||||
if (logResults)
|
||||
Logger.logDebug(LOG_TAG, context.getPackageName() + " already has Display over other apps (SYSTEM_ALERT_WINDOW) permission");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static boolean checkIfBatteryOptimizationsDisabled(Context context) {
|
||||
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
|
||||
return powerManager.isIgnoringBatteryOptimizations(context.getPackageName());
|
||||
}
|
||||
|
||||
@SuppressLint("BatteryLife")
|
||||
public static void requestDisableBatteryOptimizations(Activity activity, int requestCode) {
|
||||
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
||||
intent.setData(Uri.parse("package:" + activity.getPackageName()));
|
||||
activity.startActivityForResult(intent, requestCode);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,17 @@ 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 terminal view margin adjustment that is done to prevent soft
|
||||
* keyboard from covering bottom part of terminal view on some devices is disabled or not.
|
||||
* Margin adjustment may cause screen flickering on some devices and so should be disabled. */
|
||||
public static final String KEY_DISABLE_TERMINAL_MARGIN_ADJUSTMENT = "disable-terminal-margin-adjustment"; // Default: "disable-terminal-margin-adjustment"
|
||||
|
||||
|
||||
|
||||
/** Defines the key for whether a toast will be shown when user changes the terminal session */
|
||||
public static final String KEY_DISABLE_TERMINAL_SESSION_CHANGE_TOAST = "disable-terminal-session-change-toast"; // Default: "disable-terminal-session-change-toast"
|
||||
|
||||
|
||||
|
||||
@@ -66,6 +94,11 @@ public final class TermuxPropertyConstants {
|
||||
|
||||
|
||||
|
||||
/** Defines the key for whether url links in terminal transcript will automatically open on click or on tap */
|
||||
public static final String KEY_TERMINAL_ONCLICK_URL_OPEN = "terminal-onclick-url-open"; // Default: "terminal-onclick-url-open"
|
||||
|
||||
|
||||
|
||||
/** Defines the key for whether to use black UI */
|
||||
public static final String KEY_USE_BLACK_UI = "use-black-ui"; // Default: "use-black-ui"
|
||||
|
||||
@@ -86,13 +119,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 +146,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 +196,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 +225,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 +254,88 @@ 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_MARGIN_ADJUSTMENT,
|
||||
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_TERMINAL_ONCLICK_URL_OPEN,
|
||||
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,8 +343,11 @@ public final class TermuxPropertyConstants {
|
||||
* default: false
|
||||
* */
|
||||
public static final Set<String> TERMUX_DEFAULT_BOOLEAN_BEHAVIOUR_PROPERTIES_LIST = new HashSet<>(Arrays.asList(
|
||||
KEY_DISABLE_TERMINAL_MARGIN_ADJUSTMENT,
|
||||
KEY_DISABLE_TERMINAL_SESSION_CHANGE_TOAST,
|
||||
KEY_ENFORCE_CHAR_BASED_INPUT,
|
||||
KEY_HIDE_SOFT_KEYBOARD_ON_STARTUP,
|
||||
KEY_TERMINAL_ONCLICK_URL_OPEN,
|
||||
KEY_USE_CTRL_SPACE_WORKAROUND,
|
||||
KEY_USE_FULLSCREEN,
|
||||
KEY_USE_FULLSCREEN_WORKAROUND,
|
||||
|
||||
@@ -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,38 @@ 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 isTerminalMarginAdjustmentDisabled() {
|
||||
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_DISABLE_TERMINAL_MARGIN_ADJUSTMENT, true);
|
||||
}
|
||||
|
||||
public boolean areTerminalSessionChangeToastsDisabled() {
|
||||
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_DISABLE_TERMINAL_SESSION_CHANGE_TOAST, true);
|
||||
}
|
||||
|
||||
public boolean isEnforcingCharBasedInput() {
|
||||
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_ENFORCE_CHAR_BASED_INPUT, true);
|
||||
}
|
||||
@@ -385,8 +476,8 @@ 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 shouldOpenTerminalTranscriptURLOnClick() {
|
||||
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_TERMINAL_ONCLICK_URL_OPEN, true);
|
||||
}
|
||||
|
||||
public boolean isUsingBlackUI() {
|
||||
@@ -405,22 +496,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));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
package com.termux.shared.shell;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.termux.shared.R;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.models.errors.Error;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.models.ResultConfig;
|
||||
import com.termux.shared.models.ResultData;
|
||||
import com.termux.shared.models.errors.FunctionErrno;
|
||||
import com.termux.shared.models.errors.ResultSenderErrno;
|
||||
import com.termux.shared.termux.AndroidUtils;
|
||||
import com.termux.shared.termux.TermuxConstants.RESULT_SENDER;
|
||||
|
||||
public class ResultSender {
|
||||
|
||||
private static final String LOG_TAG = "ResultSender";
|
||||
|
||||
/**
|
||||
* Send result stored in {@link ResultConfig} to command caller via
|
||||
* {@link ResultConfig#resultPendingIntent} and/or by writing it to files in
|
||||
* {@link ResultConfig#resultDirectoryPath}. If both are not {@code null}, then result will be
|
||||
* sent via both.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param logTag The log tag to use for logging.
|
||||
* @param label The label for the command.
|
||||
* @param resultConfig The {@link ResultConfig} object containing information on how to send the result.
|
||||
* @param resultData The {@link ResultData} object containing result data.
|
||||
* @return Returns the {@link Error} if failed to send the result, otherwise {@code null}.
|
||||
*/
|
||||
public static Error sendCommandResultData(Context context, String logTag, String label, ResultConfig resultConfig, ResultData resultData) {
|
||||
if (context == null || resultConfig == null || resultData == null)
|
||||
return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETERS.getError("context, resultConfig or resultData", "sendCommandResultData");
|
||||
|
||||
Error error;
|
||||
|
||||
if (resultConfig.resultPendingIntent != null) {
|
||||
error = sendCommandResultDataWithPendingIntent(context, logTag, label, resultConfig, resultData);
|
||||
if (error != null || resultConfig.resultDirectoryPath == null)
|
||||
return error;
|
||||
}
|
||||
|
||||
if (resultConfig.resultDirectoryPath != null) {
|
||||
return sendCommandResultDataToDirectory(context, logTag, label, resultConfig, resultData);
|
||||
} else {
|
||||
return FunctionErrno.ERRNO_UNSET_PARAMETERS.getError("resultConfig.resultPendingIntent or resultConfig.resultDirectoryPath", "sendCommandResultData");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send result stored in {@link ResultConfig} to command caller via {@link ResultConfig#resultPendingIntent}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param logTag The log tag to use for logging.
|
||||
* @param label The label for the command.
|
||||
* @param resultConfig The {@link ResultConfig} object containing information on how to send the result.
|
||||
* @param resultData The {@link ResultData} object containing result data.
|
||||
* @return Returns the {@link Error} if failed to send the result, otherwise {@code null}.
|
||||
*/
|
||||
public static Error sendCommandResultDataWithPendingIntent(Context context, String logTag, String label, ResultConfig resultConfig, ResultData resultData) {
|
||||
if (context == null || resultConfig == null || resultData == null || resultConfig.resultPendingIntent == null || resultConfig.resultBundleKey == null)
|
||||
return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError("context, resultConfig, resultData, resultConfig.resultPendingIntent or resultConfig.resultBundleKey", "sendCommandResultDataWithPendingIntent");
|
||||
|
||||
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||
|
||||
Logger.logDebugExtended(logTag, "Sending result for command \"" + label + "\":\n" + resultConfig.toString() + "\n" + resultData.toString());
|
||||
|
||||
String resultDataStdout = resultData.stdout.toString();
|
||||
String resultDataStderr = resultData.stderr.toString();
|
||||
|
||||
String truncatedStdout = null;
|
||||
String truncatedStderr = null;
|
||||
|
||||
String stdoutOriginalLength = String.valueOf(resultDataStdout.length());
|
||||
String stderrOriginalLength = String.valueOf(resultDataStderr.length());
|
||||
|
||||
// Truncate stdout and stdout to max TRANSACTION_SIZE_LIMIT_IN_BYTES
|
||||
if (resultDataStderr.isEmpty()) {
|
||||
truncatedStdout = DataUtils.getTruncatedCommandOutput(resultDataStdout, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false);
|
||||
} else if (resultDataStdout.isEmpty()) {
|
||||
truncatedStderr = DataUtils.getTruncatedCommandOutput(resultDataStderr, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false);
|
||||
} else {
|
||||
truncatedStdout = DataUtils.getTruncatedCommandOutput(resultDataStdout, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 2, false, false, false);
|
||||
truncatedStderr = DataUtils.getTruncatedCommandOutput(resultDataStderr, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 2, false, false, false);
|
||||
}
|
||||
|
||||
if (truncatedStdout != null && truncatedStdout.length() < resultDataStdout.length()) {
|
||||
Logger.logWarn(logTag, "The result for command \"" + label + "\" stdout length truncated from " + stdoutOriginalLength + " to " + truncatedStdout.length());
|
||||
resultDataStdout = truncatedStdout;
|
||||
}
|
||||
|
||||
if (truncatedStderr != null && truncatedStderr.length() < resultDataStderr.length()) {
|
||||
Logger.logWarn(logTag, "The result for command \"" + label + "\" stderr length truncated from " + stderrOriginalLength + " to " + truncatedStderr.length());
|
||||
resultDataStderr = truncatedStderr;
|
||||
}
|
||||
|
||||
String resultDataErrmsg = null;
|
||||
if (resultData.isStateFailed()) {
|
||||
resultDataErrmsg = ResultData.getErrorsListLogString(resultData);
|
||||
if (resultDataErrmsg.isEmpty()) resultDataErrmsg = null;
|
||||
}
|
||||
|
||||
String errmsgOriginalLength = (resultDataErrmsg == null) ? null : String.valueOf(resultDataErrmsg.length());
|
||||
|
||||
// Truncate error to max TRANSACTION_SIZE_LIMIT_IN_BYTES / 4
|
||||
// trim from end to preserve start of stacktraces
|
||||
String truncatedErrmsg = DataUtils.getTruncatedCommandOutput(resultDataErrmsg, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 4, true, false, false);
|
||||
if (truncatedErrmsg != null && truncatedErrmsg.length() < resultDataErrmsg.length()) {
|
||||
Logger.logWarn(logTag, "The result for command \"" + label + "\" error length truncated from " + errmsgOriginalLength + " to " + truncatedErrmsg.length());
|
||||
resultDataErrmsg = truncatedErrmsg;
|
||||
}
|
||||
|
||||
|
||||
final Bundle resultBundle = new Bundle();
|
||||
resultBundle.putString(resultConfig.resultStdoutKey, resultDataStdout);
|
||||
resultBundle.putString(resultConfig.resultStdoutOriginalLengthKey, stdoutOriginalLength);
|
||||
resultBundle.putString(resultConfig.resultStderrKey, resultDataStderr);
|
||||
resultBundle.putString(resultConfig.resultStderrOriginalLengthKey, stderrOriginalLength);
|
||||
if (resultData.exitCode != null)
|
||||
resultBundle.putInt(resultConfig.resultExitCodeKey, resultData.exitCode);
|
||||
resultBundle.putInt(resultConfig.resultErrCodeKey, resultData.getErrCode());
|
||||
resultBundle.putString(resultConfig.resultErrmsgKey, resultDataErrmsg);
|
||||
|
||||
Intent resultIntent = new Intent();
|
||||
resultIntent.putExtra(resultConfig.resultBundleKey, resultBundle);
|
||||
|
||||
try {
|
||||
resultConfig.resultPendingIntent.send(context, Activity.RESULT_OK, resultIntent);
|
||||
} catch (PendingIntent.CanceledException e) {
|
||||
// The caller doesn't want the result? That's fine, just ignore
|
||||
Logger.logDebug(logTag, "The command \"" + label + "\" creator " + resultConfig.resultPendingIntent.getCreatorPackage() + " does not want the results anymore");
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Send result stored in {@link ResultConfig} to command caller by writing it to files in
|
||||
* {@link ResultConfig#resultDirectoryPath}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param logTag The log tag to use for logging.
|
||||
* @param label The label for the command.
|
||||
* @param resultConfig The {@link ResultConfig} object containing information on how to send the result.
|
||||
* @param resultData The {@link ResultData} object containing result data.
|
||||
* @return Returns the {@link Error} if failed to send the result, otherwise {@code null}.
|
||||
*/
|
||||
public static Error sendCommandResultDataToDirectory(Context context, String logTag, String label, ResultConfig resultConfig, ResultData resultData) {
|
||||
if (context == null || resultConfig == null || resultData == null || DataUtils.isNullOrEmpty(resultConfig.resultDirectoryPath))
|
||||
return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError("context, resultConfig, resultData or resultConfig.resultDirectoryPath", "sendCommandResultDataToDirectory");
|
||||
|
||||
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||
|
||||
Error error;
|
||||
|
||||
String resultDataStdout = resultData.stdout.toString();
|
||||
String resultDataStderr = resultData.stderr.toString();
|
||||
|
||||
String resultDataExitCode = "";
|
||||
if (resultData.exitCode != null)
|
||||
resultDataExitCode = String.valueOf(resultData.exitCode);
|
||||
|
||||
String resultDataErrmsg = null;
|
||||
if (resultData.isStateFailed()) {
|
||||
resultDataErrmsg = ResultData.getErrorsListLogString(resultData);
|
||||
}
|
||||
resultDataErrmsg = DataUtils.getDefaultIfNull(resultDataErrmsg, "");
|
||||
|
||||
resultConfig.resultDirectoryPath = FileUtils.getCanonicalPath(resultConfig.resultDirectoryPath, null);
|
||||
|
||||
Logger.logDebugExtended(logTag, "Writing result for command \"" + label + "\":\n" + resultConfig.toString() + "\n" + resultData.toString());
|
||||
|
||||
// If resultDirectoryPath is not a directory, or is not readable or writable, then just return
|
||||
// Creation of missing directory and setting of read, write and execute permissions are
|
||||
// only done if resultDirectoryPath is under resultDirectoryAllowedParentPath.
|
||||
// We try to set execute permissions, but ignore if they are missing, since only read and write
|
||||
// permissions are required for working directories.
|
||||
error = FileUtils.validateDirectoryFileExistenceAndPermissions("result", resultConfig.resultDirectoryPath,
|
||||
resultConfig.resultDirectoryAllowedParentPath, true,
|
||||
FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS, true, true,
|
||||
true, true);
|
||||
if (error != null) {
|
||||
error.appendMessage("\n" + context.getString(R.string.msg_directory_absolute_path, "Result", resultConfig.resultDirectoryPath));
|
||||
return error;
|
||||
}
|
||||
|
||||
if (resultConfig.resultSingleFile) {
|
||||
// If resultFileBasename is null, empty or contains forward slashes "/"
|
||||
if (DataUtils.isNullOrEmpty(resultConfig.resultFileBasename) ||
|
||||
resultConfig.resultFileBasename.contains("/")) {
|
||||
error = ResultSenderErrno.ERROR_RESULT_FILE_BASENAME_NULL_OR_INVALID.getError(resultConfig.resultFileBasename);
|
||||
return error;
|
||||
}
|
||||
|
||||
String error_or_output;
|
||||
|
||||
if (resultData.isStateFailed()) {
|
||||
try {
|
||||
if (DataUtils.isNullOrEmpty(resultConfig.resultFileErrorFormat)) {
|
||||
error_or_output = String.format(RESULT_SENDER.FORMAT_FAILED_ERR__ERRMSG__STDOUT__STDERR__EXIT_CODE,
|
||||
resultData.getErrCode(), resultDataErrmsg, resultDataStdout, resultDataStderr, resultDataExitCode);
|
||||
} else {
|
||||
error_or_output = String.format(resultConfig.resultFileErrorFormat,
|
||||
resultData.getErrCode(), resultDataErrmsg, resultDataStdout, resultDataStderr, resultDataExitCode);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
error = ResultSenderErrno.ERROR_FORMAT_RESULT_ERROR_FAILED_WITH_EXCEPTION.getError(e.getMessage());
|
||||
return error;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
if (DataUtils.isNullOrEmpty(resultConfig.resultFileOutputFormat)) {
|
||||
if (resultDataStderr.isEmpty() && resultDataExitCode.equals("0"))
|
||||
error_or_output = String.format(RESULT_SENDER.FORMAT_SUCCESS_STDOUT, resultDataStdout);
|
||||
else if (resultDataStderr.isEmpty())
|
||||
error_or_output = String.format(RESULT_SENDER.FORMAT_SUCCESS_STDOUT__EXIT_CODE, resultDataStdout, resultDataExitCode);
|
||||
else
|
||||
error_or_output = String.format(RESULT_SENDER.FORMAT_SUCCESS_STDOUT__STDERR__EXIT_CODE, resultDataStdout, resultDataStderr, resultDataExitCode);
|
||||
} else {
|
||||
error_or_output = String.format(resultConfig.resultFileOutputFormat, resultDataStdout, resultDataStderr, resultDataExitCode);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
error = ResultSenderErrno.ERROR_FORMAT_RESULT_OUTPUT_FAILED_WITH_EXCEPTION.getError(e.getMessage());
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
// Write error or output to temp file
|
||||
// Check errCode file creation below for explanation for why temp file is used
|
||||
String temp_filename = resultConfig.resultFileBasename + "-" + AndroidUtils.getCurrentMilliSecondLocalTimeStamp();
|
||||
error = FileUtils.writeStringToFile(temp_filename, resultConfig.resultDirectoryPath + "/" + temp_filename,
|
||||
null, error_or_output, false);
|
||||
if (error != null) {
|
||||
return error;
|
||||
}
|
||||
|
||||
// Move error or output temp file to final destination
|
||||
error = FileUtils.moveRegularFile("error or output temp file", resultConfig.resultDirectoryPath + "/" + temp_filename,
|
||||
resultConfig.resultDirectoryPath + "/" + resultConfig.resultFileBasename, false);
|
||||
if (error != null) {
|
||||
return error;
|
||||
}
|
||||
} else {
|
||||
String filename;
|
||||
|
||||
// Default to no suffix, useful if user expects result in an empty directory, like created with mktemp
|
||||
if (resultConfig.resultFilesSuffix == null)
|
||||
resultConfig.resultFilesSuffix = "";
|
||||
|
||||
// If resultFilesSuffix contains forward slashes "/"
|
||||
if (resultConfig.resultFilesSuffix.contains("/")) {
|
||||
error = ResultSenderErrno.ERROR_RESULT_FILES_SUFFIX_INVALID.getError(resultConfig.resultFilesSuffix);
|
||||
return error;
|
||||
}
|
||||
|
||||
// Write result to result files under resultDirectoryPath
|
||||
|
||||
// Write stdout to file
|
||||
if (!resultDataStdout.isEmpty()) {
|
||||
filename = RESULT_SENDER.RESULT_FILE_STDOUT_PREFIX + resultConfig.resultFilesSuffix;
|
||||
error = FileUtils.writeStringToFile(filename, resultConfig.resultDirectoryPath + "/" + filename,
|
||||
null, resultDataStdout, false);
|
||||
if (error != null) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
// Write stderr to file
|
||||
if (!resultDataStderr.isEmpty()) {
|
||||
filename = RESULT_SENDER.RESULT_FILE_STDERR_PREFIX + resultConfig.resultFilesSuffix;
|
||||
error = FileUtils.writeStringToFile(filename, resultConfig.resultDirectoryPath + "/" + filename,
|
||||
null, resultDataStderr, false);
|
||||
if (error != null) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
// Write exitCode to file
|
||||
if (!resultDataExitCode.isEmpty()) {
|
||||
filename = RESULT_SENDER.RESULT_FILE_EXIT_CODE_PREFIX + resultConfig.resultFilesSuffix;
|
||||
error = FileUtils.writeStringToFile(filename, resultConfig.resultDirectoryPath + "/" + filename,
|
||||
null, resultDataExitCode, false);
|
||||
if (error != null) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
// Write errmsg to file
|
||||
if (resultData.isStateFailed() && !resultDataErrmsg.isEmpty()) {
|
||||
filename = RESULT_SENDER.RESULT_FILE_ERRMSG_PREFIX + resultConfig.resultFilesSuffix;
|
||||
error = FileUtils.writeStringToFile(filename, resultConfig.resultDirectoryPath + "/" + filename,
|
||||
null, resultDataErrmsg, false);
|
||||
if (error != null) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
// Write errCode to file
|
||||
// This must be created after writing to other result files has already finished since
|
||||
// caller should wait for this file to be created to be notified that the command has
|
||||
// finished and should then start reading from the rest of the result files if they exist.
|
||||
// Since there may be a delay between creation of errCode file and writing to it or flushing
|
||||
// to disk, we create a temp file first and then move it to the final destination, since
|
||||
// caller may otherwise read from an empty file in some cases.
|
||||
|
||||
// Write errCode to temp file
|
||||
String temp_filename = RESULT_SENDER.RESULT_FILE_ERR_PREFIX + "-" + AndroidUtils.getCurrentMilliSecondLocalTimeStamp();
|
||||
if (!resultConfig.resultFilesSuffix.isEmpty()) temp_filename = temp_filename + "-" + resultConfig.resultFilesSuffix;
|
||||
error = FileUtils.writeStringToFile(temp_filename, resultConfig.resultDirectoryPath + "/" + temp_filename,
|
||||
null, String.valueOf(resultData.getErrCode()), false);
|
||||
if (error != null) {
|
||||
return error;
|
||||
}
|
||||
|
||||
// Move errCode temp file to final destination
|
||||
filename = RESULT_SENDER.RESULT_FILE_ERR_PREFIX + resultConfig.resultFilesSuffix;
|
||||
error = FileUtils.moveRegularFile(RESULT_SENDER.RESULT_FILE_ERR_PREFIX + " temp file", resultConfig.resultDirectoryPath + "/" + temp_filename,
|
||||
resultConfig.resultDirectoryPath + "/" + filename, false);
|
||||
if (error != null) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.termux.shared.shell;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public interface ShellEnvironmentClient {
|
||||
|
||||
/**
|
||||
* Get the default working directory path for the environment in case the path that was passed
|
||||
* was {@code null} or empty.
|
||||
*
|
||||
* @return Should return the default working directory path.
|
||||
*/
|
||||
@NonNull
|
||||
String getDefaultWorkingDirectoryPath();
|
||||
|
||||
/**
|
||||
* Get the default "/bin" path, likely $PREFIX/bin.
|
||||
*
|
||||
* @return Should return the "/bin" path.
|
||||
*/
|
||||
@NonNull
|
||||
String getDefaultBinPath();
|
||||
|
||||
/**
|
||||
* Build the shell environment to be used for commands.
|
||||
*
|
||||
* @param currentPackageContext The {@link Context} for the current package.
|
||||
* @param isFailSafe If running a failsafe session.
|
||||
* @param workingDirectory The working directory for the environment.
|
||||
* @return Should return the build environment.
|
||||
*/
|
||||
@NonNull
|
||||
String[] buildEnvironment(Context currentPackageContext, boolean isFailSafe, String workingDirectory);
|
||||
|
||||
/**
|
||||
* Setup process arguments for the file to execute, like interpreter, etc.
|
||||
*
|
||||
* @param fileToExecute The file to execute.
|
||||
* @param arguments The arguments to pass to the executable.
|
||||
* @return Should return the final process arguments.
|
||||
*/
|
||||
@NonNull
|
||||
String[] setupProcessArgs(@NonNull String fileToExecute, String[] arguments);
|
||||
|
||||
}
|
||||
@@ -1,81 +1,13 @@
|
||||
package com.termux.shared.shell;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.packages.PackageUtils;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
import com.termux.terminal.TerminalBuffer;
|
||||
import com.termux.terminal.TerminalEmulator;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class ShellUtils {
|
||||
|
||||
public static String[] buildEnvironment(Context currentPackageContext, boolean isFailSafe, String workingDirectory) {
|
||||
TermuxConstants.TERMUX_HOME_DIR.mkdirs();
|
||||
|
||||
if (workingDirectory == null || workingDirectory.isEmpty()) workingDirectory = TermuxConstants.TERMUX_HOME_DIR_PATH;
|
||||
|
||||
List<String> environment = new ArrayList<>();
|
||||
|
||||
// This function may be called by a different package like a plugin, so we get version for Termux package via its context
|
||||
Context termuxPackageContext = TermuxUtils.getTermuxPackageContext(currentPackageContext);
|
||||
if (termuxPackageContext != null) {
|
||||
String termuxVersionName = PackageUtils.getVersionNameForPackage(termuxPackageContext);
|
||||
if (termuxVersionName != null)
|
||||
environment.add("TERMUX_VERSION=" + termuxVersionName);
|
||||
}
|
||||
|
||||
environment.add("TERM=xterm-256color");
|
||||
environment.add("COLORTERM=truecolor");
|
||||
environment.add("HOME=" + TermuxConstants.TERMUX_HOME_DIR_PATH);
|
||||
environment.add("PREFIX=" + TermuxConstants.TERMUX_PREFIX_DIR_PATH);
|
||||
environment.add("BOOTCLASSPATH=" + System.getenv("BOOTCLASSPATH"));
|
||||
environment.add("ANDROID_ROOT=" + System.getenv("ANDROID_ROOT"));
|
||||
environment.add("ANDROID_DATA=" + System.getenv("ANDROID_DATA"));
|
||||
// EXTERNAL_STORAGE is needed for /system/bin/am to work on at least
|
||||
// Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3.
|
||||
environment.add("EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE"));
|
||||
|
||||
// These variables are needed if running on Android 10 and higher.
|
||||
addToEnvIfPresent(environment, "ANDROID_ART_ROOT");
|
||||
addToEnvIfPresent(environment, "DEX2OATBOOTCLASSPATH");
|
||||
addToEnvIfPresent(environment, "ANDROID_I18N_ROOT");
|
||||
addToEnvIfPresent(environment, "ANDROID_RUNTIME_ROOT");
|
||||
addToEnvIfPresent(environment, "ANDROID_TZDATA_ROOT");
|
||||
|
||||
if (isFailSafe) {
|
||||
// Keep the default path so that system binaries can be used in the failsafe session.
|
||||
environment.add("PATH= " + System.getenv("PATH"));
|
||||
} else {
|
||||
environment.add("LANG=en_US.UTF-8");
|
||||
environment.add("PATH=" + TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH);
|
||||
environment.add("PWD=" + workingDirectory);
|
||||
environment.add("TMPDIR=" + TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH);
|
||||
}
|
||||
|
||||
return environment.toArray(new String[0]);
|
||||
}
|
||||
|
||||
public static void addToEnvIfPresent(List<String> environment, String name) {
|
||||
String value = System.getenv(name);
|
||||
if (value != null) {
|
||||
environment.add(name + "=" + value);
|
||||
}
|
||||
}
|
||||
|
||||
public static int getPid(Process p) {
|
||||
try {
|
||||
Field f = p.getClass().getDeclaredField("pid");
|
||||
@@ -90,74 +22,12 @@ public class ShellUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static String[] setupProcessArgs(@NonNull String fileToExecute, String[] arguments) {
|
||||
// The file to execute may either be:
|
||||
// - An elf file, in which we execute it directly.
|
||||
// - A script file without shebang, which we execute with our standard shell $PREFIX/bin/sh instead of the
|
||||
// system /system/bin/sh. The system shell may vary and may not work at all due to LD_LIBRARY_PATH.
|
||||
// - A file with shebang, which we try to handle with e.g. /bin/foo -> $PREFIX/bin/foo.
|
||||
String interpreter = null;
|
||||
try {
|
||||
File file = new File(fileToExecute);
|
||||
try (FileInputStream in = new FileInputStream(file)) {
|
||||
byte[] buffer = new byte[256];
|
||||
int bytesRead = in.read(buffer);
|
||||
if (bytesRead > 4) {
|
||||
if (buffer[0] == 0x7F && buffer[1] == 'E' && buffer[2] == 'L' && buffer[3] == 'F') {
|
||||
// Elf file, do nothing.
|
||||
} else if (buffer[0] == '#' && buffer[1] == '!') {
|
||||
// Try to parse shebang.
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = 2; i < bytesRead; i++) {
|
||||
char c = (char) buffer[i];
|
||||
if (c == ' ' || c == '\n') {
|
||||
if (builder.length() == 0) {
|
||||
// Skip whitespace after shebang.
|
||||
} else {
|
||||
// End of shebang.
|
||||
String executable = builder.toString();
|
||||
if (executable.startsWith("/usr") || executable.startsWith("/bin")) {
|
||||
String[] parts = executable.split("/");
|
||||
String binary = parts[parts.length - 1];
|
||||
interpreter = TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/" + binary;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
builder.append(c);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No shebang and no ELF, use standard shell.
|
||||
interpreter = TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/sh";
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Ignore.
|
||||
}
|
||||
|
||||
List<String> result = new ArrayList<>();
|
||||
if (interpreter != null) result.add(interpreter);
|
||||
result.add(fileToExecute);
|
||||
if (arguments != null) Collections.addAll(result, arguments);
|
||||
return result.toArray(new String[0]);
|
||||
}
|
||||
|
||||
public static String getExecutableBasename(String executable) {
|
||||
if (executable == null) return null;
|
||||
int lastSlash = executable.lastIndexOf('/');
|
||||
return (lastSlash == -1) ? executable : executable.substring(lastSlash + 1);
|
||||
}
|
||||
|
||||
public static void clearTermuxTMPDIR(Context context) {
|
||||
String errmsg;
|
||||
errmsg = FileUtils.clearDirectory(context, "$TMPDIR", FileUtils.getCanonicalPath(TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH, null, false));
|
||||
if (errmsg != null) {
|
||||
Logger.logErrorAndShowToast(context, errmsg);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getTerminalSessionTranscriptText(TerminalSession terminalSession, boolean linesJoined, boolean trim) {
|
||||
if (terminalSession == null) return null;
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.shared.R;
|
||||
import com.termux.shared.models.ExecutionCommand;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.models.ResultData;
|
||||
import com.termux.shared.models.errors.Errno;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
import com.termux.terminal.TerminalSessionClient;
|
||||
@@ -50,8 +51,9 @@ public class TermuxSession {
|
||||
* @param executionCommand The {@link ExecutionCommand} containing the information for execution command.
|
||||
* @param terminalSessionClient The {@link TerminalSessionClient} interface implementation.
|
||||
* @param termuxSessionClient The {@link TermuxSessionClient} interface implementation.
|
||||
* @param shellEnvironmentClient The {@link ShellEnvironmentClient} interface implementation.
|
||||
* @param sessionName The optional {@link TerminalSession} name.
|
||||
* @param setStdoutOnExit If set to {@code true}, then the {@link ExecutionCommand#stdout}
|
||||
* @param setStdoutOnExit If set to {@code true}, then the {@link ResultData#stdout}
|
||||
* available in the {@link TermuxSessionClient#onTermuxSessionExited(TermuxSession)}
|
||||
* callback will be set to the {@link TerminalSession} transcript. The session
|
||||
* transcript will contain both stdout and stderr combined, basically
|
||||
@@ -62,16 +64,24 @@ public class TermuxSession {
|
||||
*/
|
||||
public static TermuxSession execute(@NonNull final Context context, @NonNull ExecutionCommand executionCommand,
|
||||
@NonNull final TerminalSessionClient terminalSessionClient, final TermuxSessionClient termuxSessionClient,
|
||||
@NonNull final ShellEnvironmentClient shellEnvironmentClient,
|
||||
final String sessionName, final boolean setStdoutOnExit) {
|
||||
if (executionCommand.workingDirectory == null || executionCommand.workingDirectory.isEmpty()) executionCommand.workingDirectory = TermuxConstants.TERMUX_HOME_DIR_PATH;
|
||||
if (executionCommand.workingDirectory == null || executionCommand.workingDirectory.isEmpty())
|
||||
executionCommand.workingDirectory = shellEnvironmentClient.getDefaultWorkingDirectoryPath();
|
||||
if (executionCommand.workingDirectory.isEmpty())
|
||||
executionCommand.workingDirectory = "/";
|
||||
|
||||
String[] environment = ShellUtils.buildEnvironment(context, executionCommand.isFailsafe, executionCommand.workingDirectory);
|
||||
String[] environment = shellEnvironmentClient.buildEnvironment(context, executionCommand.isFailsafe, executionCommand.workingDirectory);
|
||||
|
||||
String defaultBinPath = shellEnvironmentClient.getDefaultBinPath();
|
||||
if (defaultBinPath.isEmpty())
|
||||
defaultBinPath = "/system/bin";
|
||||
|
||||
boolean isLoginShell = false;
|
||||
if (executionCommand.executable == null) {
|
||||
if (!executionCommand.isFailsafe) {
|
||||
for (String shellBinary : new String[]{"login", "bash", "zsh"}) {
|
||||
File shellFile = new File(TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH, shellBinary);
|
||||
File shellFile = new File(defaultBinPath, shellBinary);
|
||||
if (shellFile.canExecute()) {
|
||||
executionCommand.executable = shellFile.getAbsolutePath();
|
||||
break;
|
||||
@@ -81,12 +91,21 @@ public class TermuxSession {
|
||||
|
||||
if (executionCommand.executable == null) {
|
||||
// Fall back to system shell as last resort:
|
||||
// Do not start a login shell since ~/.profile may cause startup failure if its invalid.
|
||||
// /system/bin/sh is provided by mksh (not toybox) and does load .mkshrc but for android its set
|
||||
// to /system/etc/mkshrc even though its default is ~/.mkshrc.
|
||||
// So /system/etc/mkshrc must still be valid for failsafe session to start properly.
|
||||
// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:external/mksh/src/main.c;l=663
|
||||
// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:external/mksh/src/main.c;l=41
|
||||
// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:external/mksh/Android.bp;l=114
|
||||
executionCommand.executable = "/system/bin/sh";
|
||||
} else {
|
||||
isLoginShell = true;
|
||||
}
|
||||
isLoginShell = true;
|
||||
|
||||
}
|
||||
|
||||
String[] processArgs = ShellUtils.setupProcessArgs(executionCommand.executable, executionCommand.arguments);
|
||||
String[] processArgs = shellEnvironmentClient.setupProcessArgs(executionCommand.executable, executionCommand.arguments);
|
||||
|
||||
executionCommand.executable = processArgs[0];
|
||||
String processName = (isLoginShell ? "-" : "") + ShellUtils.getExecutableBasename(executionCommand.executable);
|
||||
@@ -101,7 +120,7 @@ public class TermuxSession {
|
||||
executionCommand.commandLabel = processName;
|
||||
|
||||
if (!executionCommand.setState(ExecutionCommand.ExecutionState.EXECUTING)) {
|
||||
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, context.getString(R.string.error_failed_to_execute_termux_session_command, executionCommand.getCommandIdAndLabelLogString()), null);
|
||||
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_failed_to_execute_termux_session_command, executionCommand.getCommandIdAndLabelLogString()));
|
||||
TermuxSession.processTermuxSessionResult(null, executionCommand);
|
||||
return null;
|
||||
}
|
||||
@@ -109,7 +128,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;
|
||||
@@ -122,8 +141,8 @@ public class TermuxSession {
|
||||
* Signal that this {@link TermuxSession} has finished. This should be called when
|
||||
* {@link TerminalSessionClient#onSessionFinished(TerminalSession)} callback is received by the caller.
|
||||
*
|
||||
* If the processes has finished, then sets {@link ExecutionCommand#stdout}, {@link ExecutionCommand#stderr}
|
||||
* and {@link ExecutionCommand#exitCode} for the {@link #mExecutionCommand} of the {@code termuxTask}
|
||||
* If the processes has finished, then sets {@link ResultData#stdout}, {@link ResultData#stderr}
|
||||
* and {@link ResultData#exitCode} for the {@link #mExecutionCommand} of the {@code termuxTask}
|
||||
* and then calls {@link #processTermuxSessionResult(TermuxSession, ExecutionCommand)} to process the result}.
|
||||
*
|
||||
*/
|
||||
@@ -134,9 +153,9 @@ public class TermuxSession {
|
||||
int exitCode = mTerminalSession.getExitStatus();
|
||||
|
||||
if (exitCode == 0)
|
||||
Logger.logDebug(LOG_TAG, "The \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession with exited normally");
|
||||
Logger.logDebug(LOG_TAG, "The \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession exited normally");
|
||||
else
|
||||
Logger.logDebug(LOG_TAG, "The \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession with exited with code: " + exitCode);
|
||||
Logger.logDebug(LOG_TAG, "The \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession exited with code: " + exitCode);
|
||||
|
||||
// If the execution command has already failed, like SIGKILL was sent, then don't continue
|
||||
if (mExecutionCommand.isStateFailed()) {
|
||||
@@ -144,13 +163,10 @@ public class TermuxSession {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.mSetStdoutOnExit)
|
||||
mExecutionCommand.stdout = ShellUtils.getTerminalSessionTranscriptText(mTerminalSession, true, false);
|
||||
else
|
||||
mExecutionCommand.stdout = null;
|
||||
mExecutionCommand.resultData.exitCode = exitCode;
|
||||
|
||||
mExecutionCommand.stderr = null;
|
||||
mExecutionCommand.exitCode = exitCode;
|
||||
if (this.mSetStdoutOnExit)
|
||||
mExecutionCommand.resultData.stdout.append(ShellUtils.getTerminalSessionTranscriptText(mTerminalSession, true, false));
|
||||
|
||||
if (!mExecutionCommand.setState(ExecutionCommand.ExecutionState.EXECUTED))
|
||||
return;
|
||||
@@ -162,8 +178,6 @@ public class TermuxSession {
|
||||
* Kill this {@link TermuxSession} by sending a {@link OsConstants#SIGILL} to its {@link #mTerminalSession}
|
||||
* if its still executing.
|
||||
*
|
||||
* We process the results even if
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param processResult If set to {@code true}, then the {@link #processTermuxSessionResult(TermuxSession, ExecutionCommand)}
|
||||
* will be called to process the failure.
|
||||
@@ -176,16 +190,13 @@ public class TermuxSession {
|
||||
}
|
||||
|
||||
Logger.logDebug(LOG_TAG, "Send SIGKILL to \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession");
|
||||
if (mExecutionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, context.getString(R.string.error_sending_sigkill_to_process), null)) {
|
||||
if (mExecutionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_sending_sigkill_to_process))) {
|
||||
if (processResult) {
|
||||
mExecutionCommand.resultData.exitCode = 137; // SIGKILL
|
||||
|
||||
// Get whatever output has been set till now in case its needed
|
||||
if (this.mSetStdoutOnExit)
|
||||
mExecutionCommand.stdout = ShellUtils.getTerminalSessionTranscriptText(mTerminalSession, true, false);
|
||||
else
|
||||
mExecutionCommand.stdout = null;
|
||||
|
||||
mExecutionCommand.stderr = null;
|
||||
mExecutionCommand.exitCode = 137; // SIGKILL
|
||||
mExecutionCommand.resultData.stdout.append(ShellUtils.getTerminalSessionTranscriptText(mTerminalSession, true, false));
|
||||
|
||||
TermuxSession.processTermuxSessionResult(this, null);
|
||||
}
|
||||
@@ -205,10 +216,10 @@ public class TermuxSession {
|
||||
* callback will be called.
|
||||
*
|
||||
* @param termuxSession The {@link TermuxSession}, which should be set if
|
||||
* {@link #execute(Context, ExecutionCommand, TerminalSessionClient, TermuxSessionClient, String, boolean)}
|
||||
* {@link #execute(Context, ExecutionCommand, TerminalSessionClient, TermuxSessionClient, ShellEnvironmentClient, String, boolean)}
|
||||
* successfully started the process.
|
||||
* @param executionCommand The {@link ExecutionCommand}, which should be set if
|
||||
* {@link #execute(Context, ExecutionCommand, TerminalSessionClient, TermuxSessionClient, String, boolean)}
|
||||
* {@link #execute(Context, ExecutionCommand, TerminalSessionClient, TermuxSessionClient, ShellEnvironmentClient, String, boolean)}
|
||||
* failed to start the process.
|
||||
*/
|
||||
private static void processTermuxSessionResult(final TermuxSession termuxSession, ExecutionCommand executionCommand) {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.termux.shared.shell;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class TermuxShellEnvironmentClient implements ShellEnvironmentClient {
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getDefaultWorkingDirectoryPath() {
|
||||
return TermuxShellUtils.getDefaultWorkingDirectoryPath();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getDefaultBinPath() {
|
||||
return TermuxShellUtils.getDefaultBinPath();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String[] buildEnvironment(Context currentPackageContext, boolean isFailSafe, String workingDirectory) {
|
||||
return TermuxShellUtils.buildEnvironment(currentPackageContext, isFailSafe, workingDirectory);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String[] setupProcessArgs(@NonNull String fileToExecute, String[] arguments) {
|
||||
return TermuxShellUtils.setupProcessArgs(fileToExecute, arguments);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package com.termux.shared.shell;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.shared.models.errors.Error;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.packages.PackageUtils;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class TermuxShellUtils {
|
||||
|
||||
public static String getDefaultWorkingDirectoryPath() {
|
||||
return TermuxConstants.TERMUX_HOME_DIR_PATH;
|
||||
}
|
||||
|
||||
public static String getDefaultBinPath() {
|
||||
return TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH;
|
||||
}
|
||||
|
||||
public static String[] buildEnvironment(Context currentPackageContext, boolean isFailSafe, String workingDirectory) {
|
||||
TermuxConstants.TERMUX_HOME_DIR.mkdirs();
|
||||
|
||||
if (workingDirectory == null || workingDirectory.isEmpty())
|
||||
workingDirectory = getDefaultWorkingDirectoryPath();
|
||||
|
||||
List<String> environment = new ArrayList<>();
|
||||
|
||||
// This function may be called by a different package like a plugin, so we get version for Termux package via its context
|
||||
Context termuxPackageContext = TermuxUtils.getTermuxPackageContext(currentPackageContext);
|
||||
if (termuxPackageContext != null) {
|
||||
String termuxVersionName = PackageUtils.getVersionNameForPackage(termuxPackageContext);
|
||||
if (termuxVersionName != null)
|
||||
environment.add("TERMUX_VERSION=" + termuxVersionName);
|
||||
}
|
||||
|
||||
environment.add("TERM=xterm-256color");
|
||||
environment.add("COLORTERM=truecolor");
|
||||
environment.add("HOME=" + TermuxConstants.TERMUX_HOME_DIR_PATH);
|
||||
environment.add("PREFIX=" + TermuxConstants.TERMUX_PREFIX_DIR_PATH);
|
||||
environment.add("BOOTCLASSPATH=" + System.getenv("BOOTCLASSPATH"));
|
||||
environment.add("ANDROID_ROOT=" + System.getenv("ANDROID_ROOT"));
|
||||
environment.add("ANDROID_DATA=" + System.getenv("ANDROID_DATA"));
|
||||
// EXTERNAL_STORAGE is needed for /system/bin/am to work on at least
|
||||
// Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3.
|
||||
environment.add("EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE"));
|
||||
|
||||
// These variables are needed if running on Android 10 and higher.
|
||||
addToEnvIfPresent(environment, "ANDROID_ART_ROOT");
|
||||
addToEnvIfPresent(environment, "DEX2OATBOOTCLASSPATH");
|
||||
addToEnvIfPresent(environment, "ANDROID_I18N_ROOT");
|
||||
addToEnvIfPresent(environment, "ANDROID_RUNTIME_ROOT");
|
||||
addToEnvIfPresent(environment, "ANDROID_TZDATA_ROOT");
|
||||
|
||||
if (isFailSafe) {
|
||||
// Keep the default path so that system binaries can be used in the failsafe session.
|
||||
environment.add("PATH= " + System.getenv("PATH"));
|
||||
} else {
|
||||
environment.add("LANG=en_US.UTF-8");
|
||||
environment.add("PATH=" + TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH);
|
||||
environment.add("PWD=" + workingDirectory);
|
||||
environment.add("TMPDIR=" + TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH);
|
||||
}
|
||||
|
||||
return environment.toArray(new String[0]);
|
||||
}
|
||||
|
||||
public static void addToEnvIfPresent(List<String> environment, String name) {
|
||||
String value = System.getenv(name);
|
||||
if (value != null) {
|
||||
environment.add(name + "=" + value);
|
||||
}
|
||||
}
|
||||
|
||||
public static String[] setupProcessArgs(@NonNull String fileToExecute, String[] arguments) {
|
||||
// The file to execute may either be:
|
||||
// - An elf file, in which we execute it directly.
|
||||
// - A script file without shebang, which we execute with our standard shell $PREFIX/bin/sh instead of the
|
||||
// system /system/bin/sh. The system shell may vary and may not work at all due to LD_LIBRARY_PATH.
|
||||
// - A file with shebang, which we try to handle with e.g. /bin/foo -> $PREFIX/bin/foo.
|
||||
String interpreter = null;
|
||||
try {
|
||||
File file = new File(fileToExecute);
|
||||
try (FileInputStream in = new FileInputStream(file)) {
|
||||
byte[] buffer = new byte[256];
|
||||
int bytesRead = in.read(buffer);
|
||||
if (bytesRead > 4) {
|
||||
if (buffer[0] == 0x7F && buffer[1] == 'E' && buffer[2] == 'L' && buffer[3] == 'F') {
|
||||
// Elf file, do nothing.
|
||||
} else if (buffer[0] == '#' && buffer[1] == '!') {
|
||||
// Try to parse shebang.
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = 2; i < bytesRead; i++) {
|
||||
char c = (char) buffer[i];
|
||||
if (c == ' ' || c == '\n') {
|
||||
if (builder.length() == 0) {
|
||||
// Skip whitespace after shebang.
|
||||
} else {
|
||||
// End of shebang.
|
||||
String executable = builder.toString();
|
||||
if (executable.startsWith("/usr") || executable.startsWith("/bin")) {
|
||||
String[] parts = executable.split("/");
|
||||
String binary = parts[parts.length - 1];
|
||||
interpreter = TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/" + binary;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
builder.append(c);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No shebang and no ELF, use standard shell.
|
||||
interpreter = TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/sh";
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Ignore.
|
||||
}
|
||||
|
||||
List<String> result = new ArrayList<>();
|
||||
if (interpreter != null) result.add(interpreter);
|
||||
result.add(fileToExecute);
|
||||
if (arguments != null) Collections.addAll(result, arguments);
|
||||
return result.toArray(new String[0]);
|
||||
}
|
||||
|
||||
public static void clearTermuxTMPDIR(boolean onlyIfExists) {
|
||||
if(onlyIfExists && !FileUtils.directoryFileExists(TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH, false))
|
||||
return;
|
||||
|
||||
Error error;
|
||||
error = FileUtils.clearDirectory("$TMPDIR", FileUtils.getCanonicalPath(TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH, null));
|
||||
if (error != null) {
|
||||
Logger.logErrorExtended(error.toString());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,7 +9,8 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.shared.R;
|
||||
import com.termux.shared.models.ExecutionCommand;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.models.ResultData;
|
||||
import com.termux.shared.models.errors.Errno;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.models.ExecutionCommand.ExecutionState;
|
||||
|
||||
@@ -29,9 +30,6 @@ public final class TermuxTask {
|
||||
private final ExecutionCommand mExecutionCommand;
|
||||
private final TermuxTaskClient mTermuxTaskClient;
|
||||
|
||||
private final StringBuilder mStdout = new StringBuilder();
|
||||
private final StringBuilder mStderr = new StringBuilder();
|
||||
|
||||
private static final String LOG_TAG = "TermuxTask";
|
||||
|
||||
private TermuxTask(@NonNull final Process process, @NonNull final ExecutionCommand executionCommand,
|
||||
@@ -55,6 +53,7 @@ public final class TermuxTask {
|
||||
* be called regardless of {@code isSynchronous} value but not if
|
||||
* {@code null} is returned by this method. This can
|
||||
* optionally be {@code null}.
|
||||
* @param shellEnvironmentClient The {@link ShellEnvironmentClient} interface implementation.
|
||||
* @param isSynchronous If set to {@code true}, then the command will be executed in the
|
||||
* caller thread and results returned synchronously in the {@link ExecutionCommand}
|
||||
* sub object of the {@link TermuxTask} returned.
|
||||
@@ -63,15 +62,23 @@ public final class TermuxTask {
|
||||
* @return Returns the {@link TermuxTask}. This will be {@code null} if failed to start the execution command.
|
||||
*/
|
||||
public static TermuxTask execute(@NonNull final Context context, @NonNull ExecutionCommand executionCommand,
|
||||
final TermuxTaskClient termuxTaskClient, final boolean isSynchronous) {
|
||||
if (executionCommand.workingDirectory == null || executionCommand.workingDirectory.isEmpty()) executionCommand.workingDirectory = TermuxConstants.TERMUX_HOME_DIR_PATH;
|
||||
final TermuxTaskClient termuxTaskClient,
|
||||
@NonNull final ShellEnvironmentClient shellEnvironmentClient,
|
||||
final boolean isSynchronous) {
|
||||
if (executionCommand.workingDirectory == null || executionCommand.workingDirectory.isEmpty())
|
||||
executionCommand.workingDirectory = shellEnvironmentClient.getDefaultWorkingDirectoryPath();
|
||||
if (executionCommand.workingDirectory.isEmpty())
|
||||
executionCommand.workingDirectory = "/";
|
||||
|
||||
String[] env = ShellUtils.buildEnvironment(context, false, executionCommand.workingDirectory);
|
||||
String[] env = shellEnvironmentClient.buildEnvironment(context, false, executionCommand.workingDirectory);
|
||||
|
||||
final String[] commandArray = ShellUtils.setupProcessArgs(executionCommand.executable, executionCommand.arguments);
|
||||
final String[] commandArray = shellEnvironmentClient.setupProcessArgs(executionCommand.executable, executionCommand.arguments);
|
||||
|
||||
if (!executionCommand.setState(ExecutionState.EXECUTING))
|
||||
if (!executionCommand.setState(ExecutionState.EXECUTING)) {
|
||||
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_failed_to_execute_termux_task_command, executionCommand.getCommandIdAndLabelLogString()));
|
||||
TermuxTask.processTermuxTaskResult(null, executionCommand);
|
||||
return null;
|
||||
}
|
||||
|
||||
Logger.logDebug(LOG_TAG, executionCommand.toString());
|
||||
|
||||
@@ -85,7 +92,7 @@ public final class TermuxTask {
|
||||
try {
|
||||
process = Runtime.getRuntime().exec(commandArray, env, new File(executionCommand.workingDirectory));
|
||||
} catch (IOException e) {
|
||||
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, context.getString(R.string.error_failed_to_execute_termux_task_command, executionCommand.getCommandIdAndLabelLogString()), e);
|
||||
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_failed_to_execute_termux_task_command, executionCommand.getCommandIdAndLabelLogString()), e);
|
||||
TermuxTask.processTermuxTaskResult(null, executionCommand);
|
||||
return null;
|
||||
}
|
||||
@@ -117,8 +124,8 @@ public final class TermuxTask {
|
||||
/**
|
||||
* Sets up stdout and stderr readers for the {@link #mProcess} and waits for the process to end.
|
||||
*
|
||||
* If the processes finishes, then sets {@link ExecutionCommand#stdout}, {@link ExecutionCommand#stderr}
|
||||
* and {@link ExecutionCommand#exitCode} for the {@link #mExecutionCommand} of the {@code termuxTask}
|
||||
* If the processes finishes, then sets {@link ResultData#stdout}, {@link ResultData#stderr}
|
||||
* and {@link ResultData#exitCode} for the {@link #mExecutionCommand} of the {@code termuxTask}
|
||||
* and then calls {@link #processTermuxTaskResult(TermuxTask, ExecutionCommand) to process the result}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
@@ -128,15 +135,12 @@ public final class TermuxTask {
|
||||
|
||||
Logger.logDebug(LOG_TAG, "Running \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask with pid " + pid);
|
||||
|
||||
mExecutionCommand.stdout = null;
|
||||
mExecutionCommand.stderr = null;
|
||||
mExecutionCommand.exitCode = null;
|
||||
|
||||
mExecutionCommand.resultData.exitCode = null;
|
||||
|
||||
// setup stdin, and stdout and stderr gobblers
|
||||
DataOutputStream STDIN = new DataOutputStream(mProcess.getOutputStream());
|
||||
StreamGobbler STDOUT = new StreamGobbler(pid + "-stdout", mProcess.getInputStream(), mStdout);
|
||||
StreamGobbler STDERR = new StreamGobbler(pid + "-stderr", mProcess.getErrorStream(), mStderr);
|
||||
StreamGobbler STDOUT = new StreamGobbler(pid + "-stdout", mProcess.getInputStream(), mExecutionCommand.resultData.stdout);
|
||||
StreamGobbler STDERR = new StreamGobbler(pid + "-stderr", mProcess.getErrorStream(), mExecutionCommand.resultData.stderr);
|
||||
|
||||
// start gobbling
|
||||
STDOUT.start();
|
||||
@@ -150,7 +154,7 @@ public final class TermuxTask {
|
||||
//STDIN.write("exit\n".getBytes(StandardCharsets.UTF_8));
|
||||
//STDIN.flush();
|
||||
} catch(IOException e) {
|
||||
if (e.getMessage().contains("EPIPE") || e.getMessage().contains("Stream closed")) {
|
||||
if (e.getMessage() != null && (e.getMessage().contains("EPIPE") || e.getMessage().contains("Stream closed"))) {
|
||||
// Method most horrid to catch broken pipe, in which case we
|
||||
// do nothing. The command is not a shell, the shell closed
|
||||
// STDIN, the script already contained the exit command, etc.
|
||||
@@ -158,10 +162,8 @@ public final class TermuxTask {
|
||||
} else {
|
||||
// other issues we don't know how to handle, leads to
|
||||
// returning null
|
||||
mExecutionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, context.getString(R.string.error_exception_received_while_executing_termux_task_command, mExecutionCommand.getCommandIdAndLabelLogString(), e.getMessage()), e);
|
||||
mExecutionCommand.stdout = mStdout.toString();
|
||||
mExecutionCommand.stderr = mStderr.toString();
|
||||
mExecutionCommand.exitCode = -1;
|
||||
mExecutionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_exception_received_while_executing_termux_task_command, mExecutionCommand.getCommandIdAndLabelLogString(), e.getMessage()), e);
|
||||
mExecutionCommand.resultData.exitCode = 1;
|
||||
TermuxTask.processTermuxTaskResult(this, null);
|
||||
kill();
|
||||
return;
|
||||
@@ -198,9 +200,7 @@ public final class TermuxTask {
|
||||
return;
|
||||
}
|
||||
|
||||
mExecutionCommand.stdout = mStdout.toString();
|
||||
mExecutionCommand.stderr = mStderr.toString();
|
||||
mExecutionCommand.exitCode = exitCode;
|
||||
mExecutionCommand.resultData.exitCode = exitCode;
|
||||
|
||||
if (!mExecutionCommand.setState(ExecutionState.EXECUTED))
|
||||
return;
|
||||
@@ -225,13 +225,9 @@ public final class TermuxTask {
|
||||
|
||||
Logger.logDebug(LOG_TAG, "Send SIGKILL to \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask");
|
||||
|
||||
if (mExecutionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, context.getString(R.string.error_sending_sigkill_to_process), null)) {
|
||||
if (mExecutionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_sending_sigkill_to_process))) {
|
||||
if (processResult) {
|
||||
// Get whatever output has been set till now in case its needed
|
||||
mExecutionCommand.stdout = mStdout.toString();
|
||||
mExecutionCommand.stderr = mStderr.toString();
|
||||
mExecutionCommand.exitCode = 137; // SIGKILL
|
||||
|
||||
mExecutionCommand.resultData.exitCode = 137; // SIGKILL
|
||||
TermuxTask.processTermuxTaskResult(this, null);
|
||||
}
|
||||
}
|
||||
@@ -263,10 +259,10 @@ public final class TermuxTask {
|
||||
* then the {@link TermuxTaskClient#onTermuxTaskExited(TermuxTask)} callback will be called.
|
||||
*
|
||||
* @param termuxTask The {@link TermuxTask}, which should be set if
|
||||
* {@link #execute(Context, ExecutionCommand, TermuxTaskClient, boolean)}
|
||||
* {@link #execute(Context, ExecutionCommand, TermuxTaskClient, ShellEnvironmentClient, boolean)}
|
||||
* successfully started the process.
|
||||
* @param executionCommand The {@link ExecutionCommand}, which should be set if
|
||||
* {@link #execute(Context, ExecutionCommand, TermuxTaskClient, boolean)}
|
||||
* {@link #execute(Context, ExecutionCommand, TermuxTaskClient, ShellEnvironmentClient, boolean)}
|
||||
* failed to start the process.
|
||||
*/
|
||||
private static void processTermuxTaskResult(final TermuxTask termuxTask, ExecutionCommand executionCommand) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user