Compare commits
197 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
a6ae656c9f | ||
|
|
3af5730354 | ||
|
|
3306c3c2a2 | ||
|
|
cde0bd2246 | ||
|
|
354fe1948e | ||
|
|
ae1c9bacd6 | ||
|
|
d19cf435be | ||
|
|
1132028bd2 | ||
|
|
b7b4a0a8a2 | ||
|
|
ffd8687b4c | ||
|
|
bbb6f4471f | ||
|
|
b33b906784 | ||
|
|
567eeca782 | ||
|
|
3d46849673 | ||
|
|
6293f5f170 | ||
|
|
e5c5174f6f | ||
|
|
f1034c2e79 | ||
|
|
bbf03a0507 | ||
|
|
153818f7fb | ||
|
|
192b208883 | ||
|
|
824b3e657f | ||
|
|
a95e187b25 | ||
|
|
f888f35e35 | ||
|
|
24a5493ea5 | ||
|
|
0cd7cb8162 | ||
|
|
df4d8ac7e5 | ||
|
|
64fb2ce49b | ||
|
|
0c9b85a4f9 | ||
|
|
62a2104adc | ||
|
|
71dfefd4b7 | ||
|
|
682ce08314 | ||
|
|
c9a476caf7 | ||
|
|
9cee71004f | ||
|
|
81d97c3584 | ||
|
|
939338aaac | ||
|
|
067709bf4b | ||
|
|
69e4b575a8 | ||
|
|
07e6ecd3c3 | ||
|
|
325a6f7d66 | ||
|
|
cf5bb69fc8 | ||
|
|
18b004a2ba | ||
|
|
38323b1c2a | ||
|
|
8a5442f80d | ||
|
|
8598b92dea | ||
|
|
b2cd20c035 | ||
|
|
d4fc34ca2d | ||
|
|
c0323fe816 | ||
|
|
a32309827f | ||
|
|
ada678dfe2 | ||
|
|
cdbd38faaa | ||
|
|
d4653d0590 | ||
|
|
49f53f55f3 | ||
|
|
15eb56d4dd | ||
|
|
d7ea770d47 | ||
|
|
2a8d5e292d | ||
|
|
04da4b2268 | ||
|
|
34bacfd5b1 | ||
|
|
006d5abb78 | ||
|
|
480bad181b | ||
|
|
2afa4b4351 | ||
|
|
a2209ddd5e | ||
|
|
2cc6285a81 | ||
|
|
5dee839230 | ||
|
|
1ef8eb9219 | ||
|
|
d3ddb21716 | ||
|
|
8e80e889f0 | ||
|
|
4e5d14e4a2 | ||
|
|
0230698494 | ||
|
|
977cb34fc7 | ||
|
|
f62febbfb7 | ||
|
|
7ca20fdeb3 | ||
|
|
922f4f4ae5 | ||
|
|
df03f0b7d6 | ||
|
|
249f7c6b7c | ||
|
|
78a99fddfd | ||
|
|
dff2794501 | ||
|
|
3e0f74a894 | ||
|
|
2b3f681723 | ||
|
|
d3d6731d6f | ||
|
|
9bbcc08ff3 | ||
|
|
7aa957c684 | ||
|
|
eeb8554535 | ||
|
|
4eced52c5f | ||
|
|
b856e16998 | ||
|
|
1bdf9bf2e3 | ||
|
|
92b804dc9c | ||
|
|
1b5e5b56cb | ||
|
|
31371b5e3d | ||
|
|
ef1ab197b6 | ||
|
|
bccc35bc3f | ||
|
|
20d20f42c0 | ||
|
|
9d36e9adde | ||
|
|
3491956385 | ||
|
|
134e21765c | ||
|
|
c28990a176 | ||
|
|
131f481750 | ||
|
|
f393e9b2cf | ||
|
|
8612a1d0f8 | ||
|
|
7d53a147d8 | ||
|
|
5d6a98452a | ||
|
|
b4995ef9a7 | ||
|
|
ec7568d28e | ||
|
|
607ba3ee56 | ||
|
|
ae260fad9c | ||
|
|
d9b5344b24 | ||
|
|
bc02508102 | ||
|
|
f969c01f7e | ||
|
|
fb6e9b69ab | ||
|
|
c1a377d15d | ||
|
|
8c1c7ce727 | ||
|
|
2a940dfca6 | ||
|
|
2e7fd480f4 | ||
|
|
9e82561804 | ||
|
|
4427c1675d | ||
|
|
6f6d11fbb8 | ||
|
|
ff0440d7d2 | ||
|
|
c9e18e5b93 | ||
|
|
5e0b29bb6d | ||
|
|
73af5c2326 | ||
|
|
7da847a485 | ||
|
|
11a236a172 | ||
|
|
3b5d3114a6 | ||
|
|
66f15d2a08 | ||
|
|
0225a8b1fc | ||
|
|
d39972b3bf | ||
|
|
93b506a001 | ||
|
|
ebf2e472b3 | ||
|
|
e72841ba27 | ||
|
|
5fd91b4f92 | ||
|
|
10d6eaa5d1 | ||
|
|
319446fc15 | ||
|
|
36be41d0de | ||
|
|
ef9e406300 | ||
|
|
7b4acb53c9 | ||
|
|
dbf84773d4 | ||
|
|
324a69f7fa | ||
|
|
14c49867f7 | ||
|
|
395759cc0a | ||
|
|
ada5087f67 | ||
|
|
93a5bf8d29 | ||
|
|
356a442c6f | ||
|
|
9fd2cf9834 | ||
|
|
5a96075025 | ||
|
|
85b2c44ac7 | ||
|
|
108e4cb391 | ||
|
|
80858bab6b | ||
|
|
00194ebb90 | ||
|
|
f50d15d353 |
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
|
||||
|
||||
1
.github/workflows/publish_libraries.yml
vendored
1
.github/workflows/publish_libraries.yml
vendored
@@ -7,6 +7,7 @@ on:
|
||||
paths:
|
||||
- 'terminal-emulator/build.gradle'
|
||||
- 'terminal-view/build.gradle'
|
||||
- 'termux-shared/build.gradle'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
Released under [the GPLv3 license](https://www.gnu.org/licenses/gpl.html).
|
||||
The `termux/termux-app` repository is released under [GPLv3 only](https://www.gnu.org/licenses/gpl-3.0.html) license.
|
||||
|
||||
Contains code from `Terminal Emulator for Android` by which is released under [the Apache License 2.0](https://www.apache.org/licenses/).
|
||||
### Exceptions
|
||||
|
||||
- [Terminal Emulator for Android](https://github.com/jackpal/Android-Terminal-Emulator) code is used which is released under [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). Check [terminal-view](terminal-view) and [terminal-emulator](terminal-emulator) modules.
|
||||
- [libcore/ojluni](https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/) code is used which is released under [GPLv2 only with "Classpath" exception](https://openjdk.java.net/legal/gplv2+ce.html). Check `com.termux.shared.file` package under [termux-shared](termux-shared) module.
|
||||
- [libsuperuser ](https://github.com/Chainfire/libsuperuser) code is used which is released under [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). Check `com.termux.shared.shell.StreamGobbler` class under [termux-shared](termux-shared) module.
|
||||
|
||||
145
README.md
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.
|
||||
|
||||
@@ -7,18 +7,36 @@ android {
|
||||
ndkVersion project.properties.ndkVersion
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.annotation:annotation:1.1.0"
|
||||
implementation "androidx.viewpager:viewpager:1.0.0"
|
||||
implementation "androidx.annotation:annotation:1.2.0"
|
||||
implementation "androidx.core:core:1.5.0-rc01"
|
||||
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
|
||||
implementation "androidx.preference:preference:1.1.1"
|
||||
implementation "androidx.viewpager:viewpager:1.0.0"
|
||||
implementation "com.google.guava:guava:24.1-jre"
|
||||
implementation "io.noties.markwon:core:$markwonVersion"
|
||||
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
|
||||
implementation "io.noties.markwon:linkify:$markwonVersion"
|
||||
implementation "io.noties.markwon:recycler:$markwonVersion"
|
||||
|
||||
implementation project(":terminal-view")
|
||||
implementation project(":termux-shared")
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.termux"
|
||||
minSdkVersion project.properties.minSdkVersion.toInteger()
|
||||
targetSdkVersion project.properties.targetSdkVersion.toInteger()
|
||||
versionCode 108
|
||||
versionName "0.108"
|
||||
versionCode 113
|
||||
versionName "0.113"
|
||||
|
||||
manifestPlaceholders.TERMUX_PACKAGE_NAME = "com.termux"
|
||||
manifestPlaceholders.TERMUX_APP_NAME = "Termux"
|
||||
manifestPlaceholders.TERMUX_API_APP_NAME = "Termux:API"
|
||||
manifestPlaceholders.TERMUX_BOOT_APP_NAME = "Termux:Boot"
|
||||
manifestPlaceholders.TERMUX_FLOAT_APP_NAME = "Termux:Float"
|
||||
manifestPlaceholders.TERMUX_STYLING_APP_NAME = "Termux:Styling"
|
||||
manifestPlaceholders.TERMUX_TASKER_APP_NAME = "Termux:Tasker"
|
||||
manifestPlaceholders.TERMUX_WIDGET_APP_NAME = "Termux:Widget"
|
||||
|
||||
externalNativeBuild {
|
||||
ndkBuild {
|
||||
@@ -76,8 +94,8 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'junit:junit:4.13.1'
|
||||
testImplementation 'org.robolectric:robolectric:4.4'
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
testImplementation "org.robolectric:robolectric:4.4"
|
||||
}
|
||||
|
||||
task versionName {
|
||||
@@ -135,13 +153,13 @@ clean {
|
||||
}
|
||||
}
|
||||
|
||||
task downloadBootstraps(){
|
||||
task downloadBootstraps() {
|
||||
doLast {
|
||||
def version = "2021.02.19-r1"
|
||||
downloadBootstrap("aarch64", "1e3d80bd8cc8771715845ab4a1e67fc125d84c4deda3a1a435116fe4d1f86160", version)
|
||||
downloadBootstrap("arm", "317a987cab3d4da6f9d42f024a5f25e3aaf8557b8ec68eaf80411fde6b8ea78d", version)
|
||||
downloadBootstrap("i686", "dd4632350474058fe5dec14b7bd882798bb3e9fe738b2de8c84a51a0b6395f4b", version)
|
||||
downloadBootstrap("x86_64", "5458119186c3ea631420833e59730461dc2a5faf17b8fad239823c88aa4569ae", version)
|
||||
def version = "2021.05.16-r1"
|
||||
downloadBootstrap("aarch64", "6e340d8ab11d1225b89ee920e0884cbbd944d37765d81c5b06ef34579564fd9a", version)
|
||||
downloadBootstrap("arm", "3f02bc2b5bd45c2ec5170527e39ee0413246698f11be4799c7bde6d364cfd780", version)
|
||||
downloadBootstrap("i686", "36a3733fb2d8531d7f8abd989b711919872b9e8a79d7eb2e8b00bef467199187", version)
|
||||
downloadBootstrap("x86_64", "3885376cc514220c0803e38f70b25f837854029fff2b7fda7a81452623cd9074", version)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
5
app/proguard-rules.pro
vendored
5
app/proguard-rules.pro
vendored
@@ -7,5 +7,6 @@
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
-renamesourcefileattribute SourceFile
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
-dontobfuscate
|
||||
#-renamesourcefileattribute SourceFile
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.termux"
|
||||
android:installLocation="internalOnly"
|
||||
android:sharedUserId="com.termux"
|
||||
android:sharedUserLabel="@string/shared_user_label" >
|
||||
android:sharedUserId="${TERMUX_PACKAGE_NAME}"
|
||||
android:sharedUserLabel="@string/shared_user_label">
|
||||
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
|
||||
<permission android:name="com.termux.permission.RUN_COMMAND"
|
||||
android:label="@string/run_command_permission_label"
|
||||
android:description="@string/run_command_permission_description"
|
||||
<permission
|
||||
android:name="${TERMUX_PACKAGE_NAME}.permission.RUN_COMMAND"
|
||||
android:description="@string/permission_run_command_description"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/permission_run_command_label"
|
||||
android:protectionLevel="dangerous" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
@@ -20,62 +26,101 @@
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<uses-permission android:name="android.permission.READ_LOGS" />
|
||||
<uses-permission android:name="android.permission.DUMP" />
|
||||
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
||||
<application
|
||||
android:extractNativeLibs="true"
|
||||
android:name=".app.TermuxApplication"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:banner="@drawable/banner"
|
||||
android:extractNativeLibs="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/application_name"
|
||||
android:theme="@style/Theme.Termux"
|
||||
android:supportsRtl="false" >
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="false"
|
||||
android:theme="@style/Theme.Termux">
|
||||
|
||||
<!-- This (or rather, value 2.1 or higher) is needed to make the Samsung Galaxy S8
|
||||
mark the app with "This app is optimized to run in full screen." -->
|
||||
<meta-data android:name="android.max_aspect" android:value="10.0" />
|
||||
<!--
|
||||
This (or rather, value 2.1 or higher) is needed to make the Samsung Galaxy S8
|
||||
mark the app with "This app is optimized to run in full screen."
|
||||
-->
|
||||
<meta-data
|
||||
android:name="android.max_aspect"
|
||||
android:value="10.0" />
|
||||
|
||||
<activity
|
||||
android:name="com.termux.app.TermuxActivity"
|
||||
android:label="@string/application_name"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|uiMode|keyboard|keyboardHidden|navigation"
|
||||
android:name=".app.TermuxActivity"
|
||||
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" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts" />
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="com.termux.app.TermuxHelpActivity"
|
||||
android:exported="false"
|
||||
android:theme="@android:style/Theme.Material.Light.DarkActionBar"
|
||||
android:parentActivityName=".app.TermuxActivity"
|
||||
android:resizeableActivity="true"
|
||||
android:label="@string/application_name" />
|
||||
<activity-alias
|
||||
android:name=".HomeActivity"
|
||||
android:targetActivity=".app.TermuxActivity">
|
||||
|
||||
<!-- Launch activity automatically on boot on Android Things devices -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.IOT_LAUNCHER" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<activity
|
||||
android:name="com.termux.filepicker.TermuxFileReceiverActivity"
|
||||
android:name=".app.activities.HelpActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/application_name"
|
||||
android:taskAffinity="com.termux.filereceiver"
|
||||
android:excludeFromRecents="true"
|
||||
android:parentActivityName=".app.TermuxActivity"
|
||||
android:resizeableActivity="true"
|
||||
android:noHistory="true">
|
||||
android:theme="@android:style/Theme.Material.Light.DarkActionBar" />
|
||||
|
||||
<activity
|
||||
android:name=".app.activities.SettingsActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/title_activity_termux_settings"
|
||||
android:theme="@style/Theme.AppCompat.Light.DarkActionBar" />
|
||||
|
||||
<activity
|
||||
android:name=".app.activities.ReportActivity"
|
||||
android:theme="@style/Theme.AppCompat.TermuxReportActivity"
|
||||
android:documentLaunchMode="intoExisting"
|
||||
/>
|
||||
|
||||
<activity
|
||||
android:name=".filepicker.TermuxFileReceiverActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:label="@string/application_name"
|
||||
android:noHistory="true"
|
||||
android:resizeableActivity="true"
|
||||
android:taskAffinity="${TERMUX_PACKAGE_NAME}.filereceiver">
|
||||
|
||||
<!-- Accept multiple file types when sending. -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="application/*" />
|
||||
<data android:mimeType="audio/*" />
|
||||
<data android:mimeType="image/*" />
|
||||
@@ -86,8 +131,10 @@
|
||||
</intent-filter>
|
||||
<!-- Accept multiple file types to let Termux be usable as generic file viewer. -->
|
||||
<intent-filter tools:ignore="AppLinkUrlError">
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="application/*" />
|
||||
<data android:mimeType="audio/*" />
|
||||
<data android:mimeType="image/*" />
|
||||
@@ -96,23 +143,11 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity-alias
|
||||
android:name=".HomeActivity"
|
||||
android:targetActivity="com.termux.app.TermuxActivity">
|
||||
|
||||
<!-- Launch activity automatically on boot on Android Things devices -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.IOT_LAUNCHER"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<provider
|
||||
android:name=".filepicker.TermuxDocumentsProvider"
|
||||
android:authorities="com.termux.documents"
|
||||
android:grantUriPermissions="true"
|
||||
android:authorities="${TERMUX_PACKAGE_NAME}.documents"
|
||||
android:exported="true"
|
||||
android:grantUriPermissions="true"
|
||||
android:permission="android.permission.MANAGE_DOCUMENTS">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
||||
@@ -120,27 +155,32 @@
|
||||
</provider>
|
||||
|
||||
<service
|
||||
android:name="com.termux.app.TermuxService"
|
||||
android:name=".app.TermuxService"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".app.RunCommandService"
|
||||
android:exported="true"
|
||||
android:permission="com.termux.permission.RUN_COMMAND" >
|
||||
android:permission="${TERMUX_PACKAGE_NAME}.permission.RUN_COMMAND">
|
||||
<intent-filter>
|
||||
<action android:name="com.termux.RUN_COMMAND" />
|
||||
<action android:name="${TERMUX_PACKAGE_NAME}.RUN_COMMAND" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver android:name=".app.TermuxOpenReceiver" />
|
||||
|
||||
<provider android:authorities="com.termux.files"
|
||||
android:readPermission="android.permission.permRead"
|
||||
android:exported="true"
|
||||
android:grantUriPermissions="true"
|
||||
android:name="com.termux.app.TermuxOpenReceiver$ContentProvider" />
|
||||
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
||||
<meta-data android:name="com.samsung.android.multidisplay.keep_process_alive" android:value="true"/>
|
||||
<provider
|
||||
android:name=".app.TermuxOpenReceiver$ContentProvider"
|
||||
android:authorities="${TERMUX_PACKAGE_NAME}.files"
|
||||
android:exported="true"
|
||||
android:grantUriPermissions="true"
|
||||
android:readPermission="android.permission.permRead" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.sec.android.support.multiwindow"
|
||||
android:value="true" />
|
||||
<meta-data
|
||||
android:name="com.samsung.android.multidisplay.keep_process_alive"
|
||||
android:value="true" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -1,243 +0,0 @@
|
||||
package com.termux.app;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import com.termux.BuildConfig;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A background job launched by Termux.
|
||||
*/
|
||||
public final class BackgroundJob {
|
||||
|
||||
private static final String LOG_TAG = "termux-task";
|
||||
|
||||
final Process mProcess;
|
||||
|
||||
public BackgroundJob(String cwd, String fileToExecute, final String[] args, final TermuxService service){
|
||||
this(cwd, fileToExecute, args, service, null);
|
||||
}
|
||||
|
||||
public BackgroundJob(String cwd, String fileToExecute, final String[] args, final TermuxService service, PendingIntent pendingIntent) {
|
||||
String[] env = buildEnvironment(false, cwd);
|
||||
if (cwd == null) cwd = TermuxService.HOME_PATH;
|
||||
|
||||
final String[] progArray = setupProcessArgs(fileToExecute, args);
|
||||
final String processDescription = Arrays.toString(progArray);
|
||||
|
||||
Process process;
|
||||
try {
|
||||
process = Runtime.getRuntime().exec(progArray, env, new File(cwd));
|
||||
} catch (IOException e) {
|
||||
mProcess = null;
|
||||
// TODO: Visible error message?
|
||||
Log.e(LOG_TAG, "Failed running background job: " + processDescription, e);
|
||||
return;
|
||||
}
|
||||
|
||||
mProcess = process;
|
||||
final int pid = getPid(mProcess);
|
||||
final Bundle result = new Bundle();
|
||||
final StringBuilder outResult = new StringBuilder();
|
||||
final StringBuilder errResult = new StringBuilder();
|
||||
|
||||
Thread errThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
InputStream stderr = mProcess.getErrorStream();
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(stderr, StandardCharsets.UTF_8));
|
||||
String line;
|
||||
try {
|
||||
// FIXME: Long lines.
|
||||
while ((line = reader.readLine()) != null) {
|
||||
errResult.append(line).append('\n');
|
||||
Log.i(LOG_TAG, "[" + pid + "] stderr: " + line);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Ignore.
|
||||
}
|
||||
}
|
||||
};
|
||||
errThread.start();
|
||||
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
Log.i(LOG_TAG, "[" + pid + "] starting: " + processDescription);
|
||||
InputStream stdout = mProcess.getInputStream();
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(stdout, StandardCharsets.UTF_8));
|
||||
|
||||
String line;
|
||||
try {
|
||||
// FIXME: Long lines.
|
||||
while ((line = reader.readLine()) != null) {
|
||||
Log.i(LOG_TAG, "[" + pid + "] stdout: " + line);
|
||||
outResult.append(line).append('\n');
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(LOG_TAG, "Error reading output", e);
|
||||
}
|
||||
|
||||
try {
|
||||
int exitCode = mProcess.waitFor();
|
||||
service.onBackgroundJobExited(BackgroundJob.this);
|
||||
if (exitCode == 0) {
|
||||
Log.i(LOG_TAG, "[" + pid + "] exited normally");
|
||||
} else {
|
||||
Log.w(LOG_TAG, "[" + pid + "] exited with code: " + exitCode);
|
||||
}
|
||||
|
||||
result.putString("stdout", outResult.toString());
|
||||
result.putInt("exitCode", exitCode);
|
||||
|
||||
errThread.join();
|
||||
result.putString("stderr", errResult.toString());
|
||||
|
||||
Intent data = new Intent();
|
||||
data.putExtra("result", result);
|
||||
|
||||
if(pendingIntent != null) {
|
||||
try {
|
||||
pendingIntent.send(service.getApplicationContext(), Activity.RESULT_OK, data);
|
||||
} catch (PendingIntent.CanceledException e) {
|
||||
// The caller doesn't want the result? That's fine, just ignore
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
private static void addToEnvIfPresent(List<String> environment, String name) {
|
||||
String value = System.getenv(name);
|
||||
if (value != null) {
|
||||
environment.add(name + "=" + value);
|
||||
}
|
||||
}
|
||||
|
||||
static String[] buildEnvironment(boolean failSafe, String cwd) {
|
||||
new File(TermuxService.HOME_PATH).mkdirs();
|
||||
|
||||
if (cwd == null) cwd = TermuxService.HOME_PATH;
|
||||
|
||||
List<String> environment = new ArrayList<>();
|
||||
|
||||
environment.add("TERMUX_VERSION=" + BuildConfig.VERSION_NAME);
|
||||
environment.add("TERM=xterm-256color");
|
||||
environment.add("COLORTERM=truecolor");
|
||||
environment.add("HOME=" + TermuxService.HOME_PATH);
|
||||
environment.add("PREFIX=" + TermuxService.PREFIX_PATH);
|
||||
environment.add("BOOTCLASSPATH=" + System.getenv("BOOTCLASSPATH"));
|
||||
environment.add("ANDROID_ROOT=" + System.getenv("ANDROID_ROOT"));
|
||||
environment.add("ANDROID_DATA=" + System.getenv("ANDROID_DATA"));
|
||||
// EXTERNAL_STORAGE is needed for /system/bin/am to work on at least
|
||||
// Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3.
|
||||
environment.add("EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE"));
|
||||
|
||||
// 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 (failSafe) {
|
||||
// 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=" + TermuxService.PREFIX_PATH + "/bin");
|
||||
environment.add("PWD=" + cwd);
|
||||
environment.add("TMPDIR=" + TermuxService.PREFIX_PATH + "/tmp");
|
||||
}
|
||||
|
||||
return environment.toArray(new String[0]);
|
||||
}
|
||||
|
||||
public static int getPid(Process p) {
|
||||
try {
|
||||
Field f = p.getClass().getDeclaredField("pid");
|
||||
f.setAccessible(true);
|
||||
try {
|
||||
return f.getInt(p);
|
||||
} finally {
|
||||
f.setAccessible(false);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
static String[] setupProcessArgs(String fileToExecute, String[] args) {
|
||||
// The file to execute may either be:
|
||||
// - An elf file, in which we execute it directly.
|
||||
// - A script file without shebang, which we execute with our standard shell $PREFIX/bin/sh instead of the
|
||||
// system /system/bin/sh. The system shell may vary and may not work at all due to LD_LIBRARY_PATH.
|
||||
// - A file with shebang, which we try to handle with e.g. /bin/foo -> $PREFIX/bin/foo.
|
||||
String interpreter = null;
|
||||
try {
|
||||
File file = new File(fileToExecute);
|
||||
try (FileInputStream in = new FileInputStream(file)) {
|
||||
byte[] buffer = new byte[256];
|
||||
int bytesRead = in.read(buffer);
|
||||
if (bytesRead > 4) {
|
||||
if (buffer[0] == 0x7F && buffer[1] == 'E' && buffer[2] == 'L' && buffer[3] == 'F') {
|
||||
// Elf file, do nothing.
|
||||
} else if (buffer[0] == '#' && buffer[1] == '!') {
|
||||
// Try to parse shebang.
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = 2; i < bytesRead; i++) {
|
||||
char c = (char) buffer[i];
|
||||
if (c == ' ' || c == '\n') {
|
||||
if (builder.length() == 0) {
|
||||
// Skip whitespace after shebang.
|
||||
} else {
|
||||
// End of shebang.
|
||||
String executable = builder.toString();
|
||||
if (executable.startsWith("/usr") || executable.startsWith("/bin")) {
|
||||
String[] parts = executable.split("/");
|
||||
String binary = parts[parts.length - 1];
|
||||
interpreter = TermuxService.PREFIX_PATH + "/bin/" + binary;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
builder.append(c);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No shebang and no ELF, use standard shell.
|
||||
interpreter = TermuxService.PREFIX_PATH + "/bin/sh";
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Ignore.
|
||||
}
|
||||
|
||||
List<String> result = new ArrayList<>();
|
||||
if (interpreter != null) result.add(interpreter);
|
||||
result.add(fileToExecute);
|
||||
if (args != null) Collections.addAll(result, args);
|
||||
return result.toArray(new String[0]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,75 +1,35 @@
|
||||
package com.termux.app;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
|
||||
import com.termux.R;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Properties;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.notification.NotificationUtils;
|
||||
import com.termux.app.utils.PluginUtils;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.models.ExecutionCommand;
|
||||
|
||||
/**
|
||||
* When allow-external-apps property is set to "true" in ~/.termux/termux.properties, Termux
|
||||
* is able to process execute intents sent by third-party applications.
|
||||
* A service that receives {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent from third party apps and
|
||||
* plugins that contains info on command execution and forwards the extras to {@link TermuxService}
|
||||
* for the actual execution.
|
||||
*
|
||||
* Third-party program must declare com.termux.permission.RUN_COMMAND permission and it should be
|
||||
* granted by user.
|
||||
*
|
||||
* Absolute path of command or script must be given in "RUN_COMMAND_PATH" extra.
|
||||
* The "RUN_COMMAND_ARGUMENTS", "RUN_COMMAND_WORKDIR" and "RUN_COMMAND_BACKGROUND" extras are
|
||||
* optional. The workdir defaults to termux home. The background mode defaults to "false".
|
||||
* The command path and workdir can optionally be prefixed with "$PREFIX/" or "~/" if an absolute
|
||||
* path is not to be given.
|
||||
*
|
||||
* To automatically bring to foreground and start termux commands that were started with
|
||||
* background mode "false" in android >= 10 without user having to click the notification manually,
|
||||
* requires termux to be granted draw over apps permission due to new restrictions
|
||||
* of starting activities from the background, this also applies to Termux:Tasker plugin.
|
||||
*
|
||||
* To reduce the chance of termux being killed by android even further due to violation of not
|
||||
* being able to call startForeground() within ~5s of service start in android >= 8, the user
|
||||
* may disable battery optimizations for termux.
|
||||
*
|
||||
* Sample code to run command "top" with java:
|
||||
* Intent intent = new Intent();
|
||||
* intent.setClassName("com.termux", "com.termux.app.RunCommandService");
|
||||
* intent.setAction("com.termux.RUN_COMMAND");
|
||||
* intent.putExtra("com.termux.RUN_COMMAND_PATH", "/data/data/com.termux/files/usr/bin/top");
|
||||
* intent.putExtra("com.termux.RUN_COMMAND_ARGUMENTS", new String[]{"-n", "5"});
|
||||
* intent.putExtra("com.termux.RUN_COMMAND_WORKDIR", "/data/data/com.termux/files/home");
|
||||
* intent.putExtra("com.termux.RUN_COMMAND_BACKGROUND", false);
|
||||
* startService(intent);
|
||||
*
|
||||
* Sample code to run command "top" with "am startservice" command:
|
||||
* am startservice --user 0 -n com.termux/com.termux.app.RunCommandService
|
||||
* -a com.termux.RUN_COMMAND
|
||||
* --es com.termux.RUN_COMMAND_PATH '/data/data/com.termux/files/usr/bin/top'
|
||||
* --esa com.termux.RUN_COMMAND_ARGUMENTS '-n,5'
|
||||
* --es com.termux.RUN_COMMAND_WORKDIR '/data/data/com.termux/files/home'
|
||||
* --ez com.termux.RUN_COMMAND_BACKGROUND 'false'
|
||||
* Check https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent for more info.
|
||||
*/
|
||||
public class RunCommandService extends Service {
|
||||
|
||||
public static final String RUN_COMMAND_ACTION = "com.termux.RUN_COMMAND";
|
||||
public static final String RUN_COMMAND_PATH = "com.termux.RUN_COMMAND_PATH";
|
||||
public static final String RUN_COMMAND_ARGUMENTS = "com.termux.RUN_COMMAND_ARGUMENTS";
|
||||
public static final String RUN_COMMAND_WORKDIR = "com.termux.RUN_COMMAND_WORKDIR";
|
||||
public static final String RUN_COMMAND_BACKGROUND = "com.termux.RUN_COMMAND_BACKGROUND";
|
||||
|
||||
private static final String NOTIFICATION_CHANNEL_ID = "termux_run_command_notification_channel";
|
||||
private static final int NOTIFICATION_ID = 1338;
|
||||
private static final String LOG_TAG = "RunCommandService";
|
||||
|
||||
class LocalBinder extends Binder {
|
||||
public final RunCommandService service = RunCommandService.this;
|
||||
@@ -84,30 +44,133 @@ public class RunCommandService extends Service {
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
Logger.logVerbose(LOG_TAG, "onCreate");
|
||||
runStartForeground();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
Logger.logDebug(LOG_TAG, "onStartCommand");
|
||||
|
||||
if (intent == null) return Service.START_NOT_STICKY;
|
||||
|
||||
// Run again in case service is already started and onCreate() is not called
|
||||
runStartForeground();
|
||||
|
||||
if (allowExternalApps() && RUN_COMMAND_ACTION.equals(intent.getAction())) {
|
||||
Uri programUri = new Uri.Builder().scheme("com.termux.file").path(parsePath(intent.getStringExtra(RUN_COMMAND_PATH))).build();
|
||||
ExecutionCommand executionCommand = new ExecutionCommand();
|
||||
executionCommand.pluginAPIHelp = this.getString(R.string.error_run_command_service_api_help, RUN_COMMAND_SERVICE.RUN_COMMAND_API_HELP_URL);
|
||||
|
||||
Intent execIntent = new Intent(TermuxService.ACTION_EXECUTE, programUri);
|
||||
execIntent.setClass(this, TermuxService.class);
|
||||
execIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, intent.getStringArrayExtra(RUN_COMMAND_ARGUMENTS));
|
||||
execIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, parsePath(intent.getStringExtra(RUN_COMMAND_WORKDIR)));
|
||||
execIntent.putExtra(TermuxService.EXTRA_EXECUTE_IN_BACKGROUND, intent.getBooleanExtra(RUN_COMMAND_BACKGROUND, false));
|
||||
String errmsg;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
this.startForegroundService(execIntent);
|
||||
} else {
|
||||
this.startService(execIntent);
|
||||
// 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);
|
||||
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.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.isPluginExecutionCommand = true;
|
||||
executionCommand.pluginPendingIntent = intent.getParcelableExtra(RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT);
|
||||
|
||||
|
||||
|
||||
// If "allow-external-apps" property to not set to "true", then just return
|
||||
// We enable force notifications if "allow-external-apps" policy is violated so that the
|
||||
// user knows someone tried to run a command in termux context, since it may be malicious
|
||||
// app or imported (tasker) plugin project and not the user himself. If a pending intent is
|
||||
// also sent, then its creator is also logged and shown.
|
||||
errmsg = PluginUtils.checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(this);
|
||||
if (errmsg != null) {
|
||||
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true);
|
||||
return Service.START_NOT_STICKY;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// If executable is null or empty, then exit here instead of getting canonical path which would expand to "/"
|
||||
if (executionCommand.executable == null || executionCommand.executable.isEmpty()) {
|
||||
errmsg = this.getString(R.string.error_run_command_service_mandatory_extra_missing, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH);
|
||||
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
||||
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);
|
||||
|
||||
// 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,
|
||||
false);
|
||||
if (errmsg != null) {
|
||||
errmsg += "\n" + this.getString(R.string.msg_executable_absolute_path, executionCommand.executable);
|
||||
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return Service.START_NOT_STICKY;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// If workingDirectory is not null or empty
|
||||
if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) {
|
||||
// Get canonical path of workingDirectory
|
||||
executionCommand.workingDirectory = FileUtils.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}
|
||||
// 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);
|
||||
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||
return Service.START_NOT_STICKY;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(FileUtils.getExpandedTermuxPath(executionCommand.executable)).build();
|
||||
|
||||
Logger.logVerbose(LOG_TAG, executionCommand.toString());
|
||||
|
||||
// Create execution intent with the action TERMUX_SERVICE#ACTION_SERVICE_EXECUTE to be sent to the TERMUX_SERVICE
|
||||
Intent execIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, executionCommand.executableUri);
|
||||
execIntent.setClass(this, TermuxService.class);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, executionCommand.arguments);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_STDIN, executionCommand.stdin);
|
||||
if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) execIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, executionCommand.workingDirectory);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, executionCommand.inBackground);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION, executionCommand.sessionAction);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_LABEL, executionCommand.commandLabel);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, executionCommand.commandDescription);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_HELP, executionCommand.commandHelp);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP, executionCommand.pluginAPIHelp);
|
||||
execIntent.putExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT, executionCommand.pluginPendingIntent);
|
||||
|
||||
// Start TERMUX_SERVICE and pass it execution intent
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
this.startForegroundService(execIntent);
|
||||
} else {
|
||||
this.startService(execIntent);
|
||||
}
|
||||
|
||||
runStopForeground();
|
||||
|
||||
return Service.START_NOT_STICKY;
|
||||
@@ -116,7 +179,7 @@ public class RunCommandService extends Service {
|
||||
private void runStartForeground() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
setupNotificationChannel();
|
||||
startForeground(NOTIFICATION_ID, buildNotification());
|
||||
startForeground(TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_ID, buildNotification());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,22 +190,21 @@ public class RunCommandService extends Service {
|
||||
}
|
||||
|
||||
private Notification buildNotification() {
|
||||
Notification.Builder builder = new Notification.Builder(this);
|
||||
builder.setContentTitle(getText(R.string.application_name) + " Run Command");
|
||||
builder.setSmallIcon(R.drawable.ic_service_notification);
|
||||
|
||||
// Use a low priority:
|
||||
builder.setPriority(Notification.PRIORITY_LOW);
|
||||
// Build the notification
|
||||
Notification.Builder builder = NotificationUtils.geNotificationBuilder(this,
|
||||
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_LOW,
|
||||
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_NAME, null, null,
|
||||
null, NotificationUtils.NOTIFICATION_MODE_SILENT);
|
||||
if (builder == null) return null;
|
||||
|
||||
// No need to show a timestamp:
|
||||
builder.setShowWhen(false);
|
||||
|
||||
// Background color for small notification icon:
|
||||
builder.setColor(0xFF607D8B);
|
||||
// Set notification icon
|
||||
builder.setSmallIcon(R.drawable.ic_service_notification);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
builder.setChannelId(NOTIFICATION_CHANNEL_ID);
|
||||
}
|
||||
// Set background color for small notification icon
|
||||
builder.setColor(0xFF607D8B);
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
@@ -150,40 +212,8 @@ public class RunCommandService extends Service {
|
||||
private void setupNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
|
||||
|
||||
String channelName = "Termux Run Command";
|
||||
int importance = NotificationManager.IMPORTANCE_LOW;
|
||||
|
||||
NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, importance);
|
||||
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
manager.createNotificationChannel(channel);
|
||||
NotificationUtils.setupNotificationChannel(this, TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_ID,
|
||||
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW);
|
||||
}
|
||||
|
||||
private boolean allowExternalApps() {
|
||||
File propsFile = new File(TermuxService.HOME_PATH + "/.termux/termux.properties");
|
||||
if (!propsFile.exists())
|
||||
propsFile = new File(TermuxService.HOME_PATH + "/.config/termux/termux.properties");
|
||||
|
||||
Properties props = new Properties();
|
||||
try {
|
||||
if (propsFile.isFile() && propsFile.canRead()) {
|
||||
try (FileInputStream in = new FileInputStream(propsFile)) {
|
||||
props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("termux", "Error loading props", e);
|
||||
}
|
||||
|
||||
return props.getProperty("allow-external-apps", "false").equals("true");
|
||||
}
|
||||
|
||||
/** Replace "$PREFIX/" or "~/" prefix with termux absolute paths */
|
||||
private String parsePath(String path) {
|
||||
if(path != null && !path.isEmpty()) {
|
||||
path = path.replaceAll("^\\$PREFIX\\/", TermuxService.PREFIX_PATH + "/");
|
||||
path = path.replaceAll("^~\\/", TermuxService.HOME_PATH + "/");
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
29
app/src/main/java/com/termux/app/TermuxApplication.java
Normal file
29
app/src/main/java/com/termux/app/TermuxApplication.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.termux.app;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import com.termux.shared.crash.CrashHandler;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.logger.Logger;
|
||||
|
||||
|
||||
public class TermuxApplication extends Application {
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
// Set crash handler for the app
|
||||
CrashHandler.setCrashHandler(this);
|
||||
|
||||
// Set log level for the app
|
||||
setLogLevel();
|
||||
}
|
||||
|
||||
private void setLogLevel() {
|
||||
// Load the log level from shared preferences and set it to the {@link Logger.CURRENT_LOG_LEVEL}
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(getApplicationContext());
|
||||
if (preferences == null) return;
|
||||
preferences.setLogLevel(null, preferences.getLogLevel());
|
||||
Logger.logDebug("Starting Application");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,18 +7,19 @@ import android.content.Context;
|
||||
import android.os.Environment;
|
||||
import android.os.UserManager;
|
||||
import android.system.Os;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.terminal.EmulatorDebug;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.shared.interact.DialogUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -29,11 +30,11 @@ import java.util.zip.ZipInputStream;
|
||||
* Install the Termux bootstrap packages if necessary by following the below steps:
|
||||
* <p/>
|
||||
* (1) If $PREFIX already exist, assume that it is correct and be done. Note that this relies on that we do not create a
|
||||
* broken $PREFIX folder below.
|
||||
* broken $PREFIX directory below.
|
||||
* <p/>
|
||||
* (2) A progress dialog is shown with "Installing..." message and a spinner.
|
||||
* <p/>
|
||||
* (3) A staging folder, $STAGING_PREFIX, is {@link #deleteFolder(File)} if left over from broken installation below.
|
||||
* (3) A staging directory, $STAGING_PREFIX, is cleared if left over from broken installation below.
|
||||
* <p/>
|
||||
* (4) The zip file is loaded from a shared library.
|
||||
* <p/>
|
||||
@@ -46,22 +47,38 @@ import java.util.zip.ZipInputStream;
|
||||
*/
|
||||
final class TermuxInstaller {
|
||||
|
||||
/** Performs setup if necessary. */
|
||||
static void setupIfNeeded(final Activity activity, final Runnable whenDone) {
|
||||
private static final String LOG_TAG = "TermuxInstaller";
|
||||
|
||||
/** Performs bootstrap setup if necessary. */
|
||||
static void setupBootstrapIfNeeded(final Activity activity, final Runnable whenDone) {
|
||||
// Termux can only be run as the primary user (device owner) since only that
|
||||
// account has the expected file system paths. Verify that:
|
||||
UserManager um = (UserManager) activity.getSystemService(Context.USER_SERVICE);
|
||||
boolean isPrimaryUser = um.getSerialNumberForUser(android.os.Process.myUserHandle()) == 0;
|
||||
if (!isPrimaryUser) {
|
||||
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_not_primary_user_message)
|
||||
.setOnDismissListener(dialog -> System.exit(0)).setPositiveButton(android.R.string.ok, null).show();
|
||||
String bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message, TermuxConstants.TERMUX_PREFIX_DIR_PATH);
|
||||
Logger.logError(LOG_TAG, bootstrapErrorMessage);
|
||||
DialogUtils.exitAppWithErrorMessage(activity,
|
||||
activity.getString(R.string.bootstrap_error_title),
|
||||
bootstrapErrorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
final File PREFIX_FILE = new File(TermuxService.PREFIX_PATH);
|
||||
if (PREFIX_FILE.isDirectory()) {
|
||||
whenDone.run();
|
||||
return;
|
||||
final String PREFIX_FILE_PATH = TermuxConstants.TERMUX_PREFIX_DIR_PATH;
|
||||
final File PREFIX_FILE = TermuxConstants.TERMUX_PREFIX_DIR;
|
||||
|
||||
// If prefix directory exists, even if its a symlink to a valid directory and symlink is not broken/dangling
|
||||
if (FileUtils.directoryFileExists(PREFIX_FILE_PATH, true)) {
|
||||
File[] PREFIX_FILE_LIST = PREFIX_FILE.listFiles();
|
||||
// If prefix directory is empty or only contains the tmp directory
|
||||
if(PREFIX_FILE_LIST == null || PREFIX_FILE_LIST.length == 0 || (PREFIX_FILE_LIST.length == 1 && TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH.equals(PREFIX_FILE_LIST[0].getAbsolutePath()))) {
|
||||
Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" exists but is empty or only contains the tmp directory.");
|
||||
} else {
|
||||
whenDone.run();
|
||||
return;
|
||||
}
|
||||
} else if (FileUtils.fileExists(PREFIX_FILE_PATH, false)) {
|
||||
Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" does not exist but another file exists at its destination.");
|
||||
}
|
||||
|
||||
final ProgressDialog progress = ProgressDialog.show(activity, null, activity.getString(R.string.bootstrap_installer_body), true, false);
|
||||
@@ -69,13 +86,27 @@ final class TermuxInstaller {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
final String STAGING_PREFIX_PATH = TermuxService.FILES_PATH + "/usr-staging";
|
||||
Logger.logInfo(LOG_TAG, "Installing " + TermuxConstants.TERMUX_APP_NAME + " bootstrap packages.");
|
||||
|
||||
String errmsg;
|
||||
|
||||
final String STAGING_PREFIX_PATH = TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH;
|
||||
final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH);
|
||||
|
||||
if (STAGING_PREFIX_FILE.exists()) {
|
||||
deleteFolder(STAGING_PREFIX_FILE);
|
||||
// Delete prefix staging directory or any file at its destination
|
||||
errmsg = FileUtils.deleteFile(activity, "prefix staging directory", STAGING_PREFIX_PATH, true);
|
||||
if (errmsg != null) {
|
||||
throw new RuntimeException(errmsg);
|
||||
}
|
||||
|
||||
// Delete prefix directory or any file at its destination
|
||||
errmsg = FileUtils.deleteFile(activity, "prefix directory", PREFIX_FILE_PATH, true);
|
||||
if (errmsg != null) {
|
||||
throw new RuntimeException(errmsg);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -94,14 +125,14 @@ final class TermuxInstaller {
|
||||
String newPath = STAGING_PREFIX_PATH + "/" + parts[1];
|
||||
symlinks.add(Pair.create(oldPath, newPath));
|
||||
|
||||
ensureDirectoryExists(new File(newPath).getParentFile());
|
||||
ensureDirectoryExists(activity, new File(newPath).getParentFile());
|
||||
}
|
||||
} else {
|
||||
String zipEntryName = zipEntry.getName();
|
||||
File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName);
|
||||
boolean isDirectory = zipEntry.isDirectory();
|
||||
|
||||
ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile());
|
||||
ensureDirectoryExists(activity, isDirectory ? targetFile : targetFile.getParentFile());
|
||||
|
||||
if (!isDirectory) {
|
||||
try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
|
||||
@@ -124,13 +155,16 @@ final class TermuxInstaller {
|
||||
Os.symlink(symlink.first, symlink.second);
|
||||
}
|
||||
|
||||
Logger.logInfo(LOG_TAG, "Moving prefix staging to prefix directory.");
|
||||
|
||||
if (!STAGING_PREFIX_FILE.renameTo(PREFIX_FILE)) {
|
||||
throw new RuntimeException("Unable to rename staging folder");
|
||||
throw new RuntimeException("Moving prefix staging to prefix directory failed");
|
||||
}
|
||||
|
||||
Logger.logInfo(LOG_TAG, "Bootstrap packages installed successfully.");
|
||||
activity.runOnUiThread(whenDone);
|
||||
} catch (final Exception e) {
|
||||
Log.e(EmulatorDebug.LOG_TAG, "Bootstrap error", e);
|
||||
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)
|
||||
@@ -138,9 +172,9 @@ final class TermuxInstaller {
|
||||
dialog.dismiss();
|
||||
activity.finish();
|
||||
}).setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
TermuxInstaller.setupIfNeeded(activity, whenDone);
|
||||
}).show();
|
||||
dialog.dismiss();
|
||||
TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone);
|
||||
}).show();
|
||||
} catch (WindowManager.BadTokenException e1) {
|
||||
// Activity already dismissed - ignore.
|
||||
}
|
||||
@@ -158,58 +192,25 @@ final class TermuxInstaller {
|
||||
}.start();
|
||||
}
|
||||
|
||||
private static void ensureDirectoryExists(File directory) {
|
||||
if (!directory.isDirectory() && !directory.mkdirs()) {
|
||||
throw new RuntimeException("Unable to create directory: " + directory.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] loadZipBytes() {
|
||||
// Only load the shared library when necessary to save memory usage.
|
||||
System.loadLibrary("termux-bootstrap");
|
||||
return getZip();
|
||||
}
|
||||
|
||||
public static native byte[] getZip();
|
||||
|
||||
/** Delete a folder and all its content or throw. Don't follow symlinks. */
|
||||
static void deleteFolder(File fileOrDirectory) throws IOException {
|
||||
if (fileOrDirectory.getCanonicalPath().equals(fileOrDirectory.getAbsolutePath()) && fileOrDirectory.isDirectory()) {
|
||||
File[] children = fileOrDirectory.listFiles();
|
||||
|
||||
if (children != null) {
|
||||
for (File child : children) {
|
||||
deleteFolder(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!fileOrDirectory.delete()) {
|
||||
throw new RuntimeException("Unable to delete " + (fileOrDirectory.isDirectory() ? "directory " : "file ") + fileOrDirectory.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
static void setupStorageSymlinks(final Context context) {
|
||||
final String LOG_TAG = "termux-storage";
|
||||
|
||||
Logger.logInfo(LOG_TAG, "Setting up storage symlinks.");
|
||||
|
||||
new Thread() {
|
||||
public void run() {
|
||||
try {
|
||||
File storageDir = new File(TermuxService.HOME_PATH, "storage");
|
||||
String errmsg;
|
||||
File storageDir = TermuxConstants.TERMUX_STORAGE_HOME_DIR;
|
||||
|
||||
if (storageDir.exists()) {
|
||||
try {
|
||||
deleteFolder(storageDir);
|
||||
} catch (IOException e) {
|
||||
Log.e(LOG_TAG, "Could not delete old $HOME/storage, " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!storageDir.mkdirs()) {
|
||||
Log.e(LOG_TAG, "Unable to mkdirs() for $HOME/storage");
|
||||
errmsg = FileUtils.clearDirectory(context, "~/storage", storageDir.getAbsolutePath());
|
||||
if (errmsg != null) {
|
||||
Logger.logErrorAndShowToast(context, LOG_TAG, errmsg);
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.logInfo(LOG_TAG, "Setting up storage symlinks at ~/storage/shared, ~/storage/downloads, ~/storage/dcim, ~/storage/pictures, ~/storage/music and ~/storage/movies for directories in \"" + Environment.getExternalStorageDirectory().getAbsolutePath() + "\".");
|
||||
|
||||
File sharedDir = Environment.getExternalStorageDirectory();
|
||||
Os.symlink(sharedDir.getAbsolutePath(), new File(storageDir, "shared").getAbsolutePath());
|
||||
|
||||
@@ -234,14 +235,34 @@ final class TermuxInstaller {
|
||||
File dir = dirs[i];
|
||||
if (dir == null) continue;
|
||||
String symlinkName = "external-" + i;
|
||||
Logger.logInfo(LOG_TAG, "Setting up storage symlinks at ~/storage/" + symlinkName + " for \"" + dir.getAbsolutePath() + "\".");
|
||||
Os.symlink(dir.getAbsolutePath(), new File(storageDir, symlinkName).getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
Logger.logInfo(LOG_TAG, "Storage symlinks created successfully.");
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "Error setting up link", e);
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Error setting up link", e);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
private static void ensureDirectoryExists(Context context, File directory) {
|
||||
String errmsg;
|
||||
|
||||
errmsg = FileUtils.createDirectoryFile(context, directory.getAbsolutePath());
|
||||
if (errmsg != null) {
|
||||
throw new RuntimeException(errmsg);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] loadZipBytes() {
|
||||
// Only load the shared library when necessary to save memory usage.
|
||||
System.loadLibrary("termux-bootstrap");
|
||||
return getZip();
|
||||
}
|
||||
|
||||
public static native byte[] getZip();
|
||||
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@ import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.Log;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import com.termux.terminal.EmulatorDebug;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
@@ -24,11 +24,13 @@ import androidx.annotation.NonNull;
|
||||
|
||||
public class TermuxOpenReceiver extends BroadcastReceiver {
|
||||
|
||||
private static final String LOG_TAG = "TermuxOpenReceiver";
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
final Uri data = intent.getData();
|
||||
if (data == null) {
|
||||
Log.e(EmulatorDebug.LOG_TAG, "termux-open: Called without intent data");
|
||||
Logger.logError(LOG_TAG, "termux-open: Called without intent data");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -42,7 +44,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
||||
// Ok.
|
||||
break;
|
||||
default:
|
||||
Log.e(EmulatorDebug.LOG_TAG, "Invalid action '" + intentAction + "', using 'view'");
|
||||
Logger.logError(LOG_TAG, "Invalid action '" + intentAction + "', using 'view'");
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -59,14 +61,14 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
||||
try {
|
||||
context.startActivity(urlIntent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Log.e(EmulatorDebug.LOG_TAG, "termux-open: No app handles the url " + data);
|
||||
Logger.logError(LOG_TAG, "termux-open: No app handles the url " + data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final File fileToShare = new File(filePath);
|
||||
if (!(fileToShare.isFile() && fileToShare.canRead())) {
|
||||
Log.e(EmulatorDebug.LOG_TAG, "termux-open: Not a readable file: '" + fileToShare.getAbsolutePath() + "'");
|
||||
Logger.logError(LOG_TAG, "termux-open: Not a readable file: '" + fileToShare.getAbsolutePath() + "'");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -87,7 +89,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
||||
contentTypeToUse = contentTypeExtra;
|
||||
}
|
||||
|
||||
Uri uriToShare = Uri.parse("content://com.termux.files" + fileToShare.getAbsolutePath());
|
||||
Uri uriToShare = Uri.parse("content://" + TermuxConstants.TERMUX_FILE_SHARE_URI_AUTHORITY + fileToShare.getAbsolutePath());
|
||||
|
||||
if (Intent.ACTION_SEND.equals(intentAction)) {
|
||||
sendIntent.putExtra(Intent.EXTRA_STREAM, uriToShare);
|
||||
@@ -103,7 +105,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
||||
try {
|
||||
context.startActivity(sendIntent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Log.e(EmulatorDebug.LOG_TAG, "termux-open: No app handles the url " + data);
|
||||
Logger.logError(LOG_TAG, "termux-open: No app handles the url " + data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +180,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
||||
String path = file.getCanonicalPath();
|
||||
String storagePath = Environment.getExternalStorageDirectory().getCanonicalPath();
|
||||
// See https://support.google.com/faqs/answer/7496913:
|
||||
if (!(path.startsWith(TermuxService.FILES_PATH) || path.startsWith(storagePath))) {
|
||||
if (!(path.startsWith(TermuxConstants.TERMUX_FILES_DIR_PATH) || path.startsWith(storagePath))) {
|
||||
throw new IllegalArgumentException("Invalid path: " + path);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -1,276 +0,0 @@
|
||||
package com.termux.app;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.widget.Toast;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
|
||||
import static com.termux.terminal.EmulatorDebug.LOG_TAG;
|
||||
|
||||
final class TermuxPreferences {
|
||||
|
||||
@IntDef({BELL_VIBRATE, BELL_BEEP, BELL_IGNORE})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@interface AsciiBellBehaviour {
|
||||
}
|
||||
|
||||
final static class KeyboardShortcut {
|
||||
|
||||
KeyboardShortcut(int codePoint, int shortcutAction) {
|
||||
this.codePoint = codePoint;
|
||||
this.shortcutAction = shortcutAction;
|
||||
}
|
||||
|
||||
final int codePoint;
|
||||
final int shortcutAction;
|
||||
}
|
||||
|
||||
static final int SHORTCUT_ACTION_CREATE_SESSION = 1;
|
||||
static final int SHORTCUT_ACTION_NEXT_SESSION = 2;
|
||||
static final int SHORTCUT_ACTION_PREVIOUS_SESSION = 3;
|
||||
static final int SHORTCUT_ACTION_RENAME_SESSION = 4;
|
||||
|
||||
static final int BELL_VIBRATE = 1;
|
||||
static final int BELL_BEEP = 2;
|
||||
static final int BELL_IGNORE = 3;
|
||||
|
||||
private final int MIN_FONTSIZE;
|
||||
private static final int MAX_FONTSIZE = 256;
|
||||
|
||||
private static final String SHOW_EXTRA_KEYS_KEY = "show_extra_keys";
|
||||
private static final String FONTSIZE_KEY = "fontsize";
|
||||
private static final String CURRENT_SESSION_KEY = "current_session";
|
||||
private static final String SCREEN_ALWAYS_ON_KEY = "screen_always_on";
|
||||
|
||||
private boolean mUseDarkUI;
|
||||
private boolean mScreenAlwaysOn;
|
||||
private int mFontSize;
|
||||
|
||||
private boolean mUseFullScreen;
|
||||
private boolean mUseFullScreenWorkAround;
|
||||
|
||||
@AsciiBellBehaviour
|
||||
int mBellBehaviour = BELL_VIBRATE;
|
||||
|
||||
boolean mBackIsEscape;
|
||||
boolean mDisableVolumeVirtualKeys;
|
||||
boolean mShowExtraKeys;
|
||||
String mDefaultWorkingDir;
|
||||
|
||||
ExtraKeysInfos mExtraKeys;
|
||||
|
||||
final List<KeyboardShortcut> shortcuts = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* If value is not in the range [min, max], set it to either min or max.
|
||||
*/
|
||||
static int clamp(int value, int min, int max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
TermuxPreferences(Context context) {
|
||||
reloadFromProperties(context);
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
||||
float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, context.getResources().getDisplayMetrics());
|
||||
|
||||
// This is a bit arbitrary and sub-optimal. We want to give a sensible default for minimum font size
|
||||
// to prevent invisible text due to zoom be mistake:
|
||||
MIN_FONTSIZE = (int) (4f * dipInPixels);
|
||||
|
||||
mShowExtraKeys = prefs.getBoolean(SHOW_EXTRA_KEYS_KEY, true);
|
||||
mScreenAlwaysOn = prefs.getBoolean(SCREEN_ALWAYS_ON_KEY, false);
|
||||
|
||||
// http://www.google.com/design/spec/style/typography.html#typography-line-height
|
||||
int defaultFontSize = Math.round(12 * dipInPixels);
|
||||
// Make it divisible by 2 since that is the minimal adjustment step:
|
||||
if (defaultFontSize % 2 == 1) defaultFontSize--;
|
||||
|
||||
try {
|
||||
mFontSize = Integer.parseInt(prefs.getString(FONTSIZE_KEY, Integer.toString(defaultFontSize)));
|
||||
} catch (NumberFormatException | ClassCastException e) {
|
||||
mFontSize = defaultFontSize;
|
||||
}
|
||||
mFontSize = clamp(mFontSize, MIN_FONTSIZE, MAX_FONTSIZE);
|
||||
}
|
||||
|
||||
boolean toggleShowExtraKeys(Context context) {
|
||||
mShowExtraKeys = !mShowExtraKeys;
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SHOW_EXTRA_KEYS_KEY, mShowExtraKeys).apply();
|
||||
return mShowExtraKeys;
|
||||
}
|
||||
|
||||
int getFontSize() {
|
||||
return mFontSize;
|
||||
}
|
||||
|
||||
void changeFontSize(Context context, boolean increase) {
|
||||
mFontSize += (increase ? 1 : -1) * 2;
|
||||
mFontSize = Math.max(MIN_FONTSIZE, Math.min(mFontSize, MAX_FONTSIZE));
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
prefs.edit().putString(FONTSIZE_KEY, Integer.toString(mFontSize)).apply();
|
||||
}
|
||||
|
||||
boolean isScreenAlwaysOn() {
|
||||
return mScreenAlwaysOn;
|
||||
}
|
||||
|
||||
boolean isUsingBlackUI() {
|
||||
return mUseDarkUI;
|
||||
}
|
||||
|
||||
boolean isUsingFullScreen() {
|
||||
return mUseFullScreen;
|
||||
}
|
||||
|
||||
boolean isUsingFullScreenWorkAround() {
|
||||
return mUseFullScreenWorkAround;
|
||||
}
|
||||
|
||||
void setScreenAlwaysOn(Context context, boolean newValue) {
|
||||
mScreenAlwaysOn = newValue;
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SCREEN_ALWAYS_ON_KEY, newValue).apply();
|
||||
}
|
||||
|
||||
static void storeCurrentSession(Context context, TerminalSession session) {
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putString(TermuxPreferences.CURRENT_SESSION_KEY, session.mHandle).apply();
|
||||
}
|
||||
|
||||
static TerminalSession getCurrentSession(TermuxActivity context) {
|
||||
String sessionHandle = PreferenceManager.getDefaultSharedPreferences(context).getString(TermuxPreferences.CURRENT_SESSION_KEY, "");
|
||||
for (int i = 0, len = context.mTermService.getSessions().size(); i < len; i++) {
|
||||
TerminalSession session = context.mTermService.getSessions().get(i);
|
||||
if (session.mHandle.equals(sessionHandle)) return session;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void reloadFromProperties(Context context) {
|
||||
File propsFile = new File(TermuxService.HOME_PATH + "/.termux/termux.properties");
|
||||
if (!propsFile.exists())
|
||||
propsFile = new File(TermuxService.HOME_PATH + "/.config/termux/termux.properties");
|
||||
|
||||
Properties props = new Properties();
|
||||
try {
|
||||
if (propsFile.isFile() && propsFile.canRead()) {
|
||||
try (FileInputStream in = new FileInputStream(propsFile)) {
|
||||
props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(context, "Could not open properties file termux.properties: " + e.getMessage(), Toast.LENGTH_LONG).show();
|
||||
Log.e("termux", "Error loading props", e);
|
||||
}
|
||||
|
||||
switch (props.getProperty("bell-character", "vibrate")) {
|
||||
case "beep":
|
||||
mBellBehaviour = BELL_BEEP;
|
||||
break;
|
||||
case "ignore":
|
||||
mBellBehaviour = BELL_IGNORE;
|
||||
break;
|
||||
default: // "vibrate".
|
||||
mBellBehaviour = BELL_VIBRATE;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (props.getProperty("use-black-ui", "").toLowerCase()) {
|
||||
case "true":
|
||||
mUseDarkUI = true;
|
||||
break;
|
||||
case "false":
|
||||
mUseDarkUI = false;
|
||||
break;
|
||||
default:
|
||||
int nightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||
mUseDarkUI = nightMode == Configuration.UI_MODE_NIGHT_YES;
|
||||
}
|
||||
|
||||
mUseFullScreen = "true".equals(props.getProperty("fullscreen", "false").toLowerCase());
|
||||
mUseFullScreenWorkAround = "true".equals(props.getProperty("use-fullscreen-workaround", "false").toLowerCase());
|
||||
|
||||
mDefaultWorkingDir = props.getProperty("default-working-directory", TermuxService.HOME_PATH);
|
||||
File workDir = new File(mDefaultWorkingDir);
|
||||
if (!workDir.exists() || !workDir.isDirectory()) {
|
||||
// Fallback to home directory if user configured working directory is not exist
|
||||
// or is a regular file.
|
||||
mDefaultWorkingDir = TermuxService.HOME_PATH;
|
||||
}
|
||||
|
||||
String defaultExtraKeys = "[[ESC, TAB, CTRL, ALT, {key: '-', popup: '|'}, DOWN, UP]]";
|
||||
|
||||
try {
|
||||
String extrakeyProp = props.getProperty("extra-keys", defaultExtraKeys);
|
||||
String extraKeysStyle = props.getProperty("extra-keys-style", "default");
|
||||
mExtraKeys = new ExtraKeysInfos(extrakeyProp, extraKeysStyle);
|
||||
} catch (JSONException e) {
|
||||
Toast.makeText(context, "Could not load the extra-keys property from the config: " + e.toString(), Toast.LENGTH_LONG).show();
|
||||
Log.e("termux", "Error loading props", e);
|
||||
|
||||
try {
|
||||
mExtraKeys = new ExtraKeysInfos(defaultExtraKeys, "default");
|
||||
} catch (JSONException e2) {
|
||||
e2.printStackTrace();
|
||||
Toast.makeText(context, "Can't create default extra keys", Toast.LENGTH_LONG).show();
|
||||
mExtraKeys = null;
|
||||
}
|
||||
}
|
||||
|
||||
mBackIsEscape = "escape".equals(props.getProperty("back-key", "back"));
|
||||
mDisableVolumeVirtualKeys = "volume".equals(props.getProperty("volume-keys", "virtual"));
|
||||
|
||||
shortcuts.clear();
|
||||
parseAction("shortcut.create-session", SHORTCUT_ACTION_CREATE_SESSION, props);
|
||||
parseAction("shortcut.next-session", SHORTCUT_ACTION_NEXT_SESSION, props);
|
||||
parseAction("shortcut.previous-session", SHORTCUT_ACTION_PREVIOUS_SESSION, props);
|
||||
parseAction("shortcut.rename-session", SHORTCUT_ACTION_RENAME_SESSION, props);
|
||||
}
|
||||
|
||||
private void parseAction(String name, int shortcutAction, Properties props) {
|
||||
String value = props.getProperty(name);
|
||||
if (value == null) return;
|
||||
String[] parts = value.toLowerCase().trim().split("\\+");
|
||||
String input = parts.length == 2 ? parts[1].trim() : null;
|
||||
if (!(parts.length == 2 && parts[0].trim().equals("ctrl")) || input.isEmpty() || input.length() > 2) {
|
||||
Log.e("termux", "Keyboard shortcut '" + name + "' is not Ctrl+<something>");
|
||||
return;
|
||||
}
|
||||
|
||||
char c = input.charAt(0);
|
||||
int codePoint = c;
|
||||
if (Character.isLowSurrogate(c)) {
|
||||
if (input.length() != 2 || Character.isHighSurrogate(input.charAt(1))) {
|
||||
Log.e("termux", "Keyboard shortcut '" + name + "' is not Ctrl+<something>");
|
||||
return;
|
||||
} else {
|
||||
codePoint = Character.toCodePoint(input.charAt(1), c);
|
||||
}
|
||||
}
|
||||
shortcuts.add(new KeyboardShortcut(codePoint, shortcutAction));
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,283 +0,0 @@
|
||||
package com.termux.app;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.view.Gravity;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
|
||||
import com.termux.terminal.KeyHandler;
|
||||
import com.termux.terminal.TerminalEmulator;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
import com.termux.view.TerminalViewClient;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
|
||||
public final class TermuxViewClient implements TerminalViewClient {
|
||||
|
||||
final TermuxActivity mActivity;
|
||||
|
||||
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
|
||||
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
|
||||
|
||||
public TermuxViewClient(TermuxActivity activity) {
|
||||
this.mActivity = activity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float onScale(float scale) {
|
||||
if (scale < 0.9f || scale > 1.1f) {
|
||||
boolean increase = scale > 1.f;
|
||||
mActivity.changeFontSize(increase);
|
||||
return 1.0f;
|
||||
}
|
||||
return scale;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSingleTapUp(MotionEvent e) {
|
||||
InputMethodManager mgr = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
mgr.showSoftInput(mActivity.mTerminalView, InputMethodManager.SHOW_IMPLICIT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldBackButtonBeMappedToEscape() {
|
||||
return mActivity.mSettings.mBackIsEscape;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copyModeChanged(boolean copyMode) {
|
||||
// Disable drawer while copying.
|
||||
mActivity.getDrawer().setDrawerLockMode(copyMode ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED : DrawerLayout.LOCK_MODE_UNLOCKED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession currentSession) {
|
||||
if (handleVirtualKeys(keyCode, e, true)) return true;
|
||||
|
||||
if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) {
|
||||
mActivity.removeFinishedSession(currentSession);
|
||||
return true;
|
||||
} else if (e.isCtrlPressed() && e.isAltPressed()) {
|
||||
// Get the unmodified code point:
|
||||
int unicodeChar = e.getUnicodeChar(0);
|
||||
|
||||
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || unicodeChar == 'n'/* next */) {
|
||||
mActivity.switchToSession(true);
|
||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_UP || unicodeChar == 'p' /* previous */) {
|
||||
mActivity.switchToSession(false);
|
||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
|
||||
mActivity.getDrawer().openDrawer(Gravity.LEFT);
|
||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
|
||||
mActivity.getDrawer().closeDrawers();
|
||||
} else if (unicodeChar == 'k'/* keyboard */) {
|
||||
InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
|
||||
} else if (unicodeChar == 'm'/* menu */) {
|
||||
mActivity.mTerminalView.showContextMenu();
|
||||
} else if (unicodeChar == 'r'/* rename */) {
|
||||
mActivity.renameSession(currentSession);
|
||||
} else if (unicodeChar == 'c'/* create */) {
|
||||
mActivity.addNewSession(false, null);
|
||||
} else if (unicodeChar == 'u' /* urls */) {
|
||||
mActivity.showUrlSelection();
|
||||
} else if (unicodeChar == 'v') {
|
||||
mActivity.doPaste();
|
||||
} else if (unicodeChar == '+' || e.getUnicodeChar(KeyEvent.META_SHIFT_ON) == '+') {
|
||||
// We also check for the shifted char here since shift may be required to produce '+',
|
||||
// see https://github.com/termux/termux-api/issues/2
|
||||
mActivity.changeFontSize(true);
|
||||
} else if (unicodeChar == '-') {
|
||||
mActivity.changeFontSize(false);
|
||||
} else if (unicodeChar >= '1' && unicodeChar <= '9') {
|
||||
int num = unicodeChar - '1';
|
||||
TermuxService service = mActivity.mTermService;
|
||||
if (service.getSessions().size() > num)
|
||||
mActivity.switchToSession(service.getSessions().get(num));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent e) {
|
||||
return handleVirtualKeys(keyCode, e, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readControlKey() {
|
||||
return (mActivity.mExtraKeysView != null && mActivity.mExtraKeysView.readSpecialButton(ExtraKeysView.SpecialButton.CTRL)) || mVirtualControlKeyDown;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readAltKey() {
|
||||
return (mActivity.mExtraKeysView != null && mActivity.mExtraKeysView.readSpecialButton(ExtraKeysView.SpecialButton.ALT));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCodePoint(final int codePoint, boolean ctrlDown, TerminalSession session) {
|
||||
if (mVirtualFnKeyDown) {
|
||||
int resultingKeyCode = -1;
|
||||
int resultingCodePoint = -1;
|
||||
boolean altDown = false;
|
||||
int lowerCase = Character.toLowerCase(codePoint);
|
||||
switch (lowerCase) {
|
||||
// Arrow keys.
|
||||
case 'w':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_DPAD_UP;
|
||||
break;
|
||||
case 'a':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_DPAD_LEFT;
|
||||
break;
|
||||
case 's':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_DPAD_DOWN;
|
||||
break;
|
||||
case 'd':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
|
||||
break;
|
||||
|
||||
// Page up and down.
|
||||
case 'p':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_PAGE_UP;
|
||||
break;
|
||||
case 'n':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_PAGE_DOWN;
|
||||
break;
|
||||
|
||||
// Some special keys:
|
||||
case 't':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_TAB;
|
||||
break;
|
||||
case 'i':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_INSERT;
|
||||
break;
|
||||
case 'h':
|
||||
resultingCodePoint = '~';
|
||||
break;
|
||||
|
||||
// Special characters to input.
|
||||
case 'u':
|
||||
resultingCodePoint = '_';
|
||||
break;
|
||||
case 'l':
|
||||
resultingCodePoint = '|';
|
||||
break;
|
||||
|
||||
// Function keys.
|
||||
case '1':
|
||||
case '2':
|
||||
case '3':
|
||||
case '4':
|
||||
case '5':
|
||||
case '6':
|
||||
case '7':
|
||||
case '8':
|
||||
case '9':
|
||||
resultingKeyCode = (codePoint - '1') + KeyEvent.KEYCODE_F1;
|
||||
break;
|
||||
case '0':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_F10;
|
||||
break;
|
||||
|
||||
// Other special keys.
|
||||
case 'e':
|
||||
resultingCodePoint = /*Escape*/ 27;
|
||||
break;
|
||||
case '.':
|
||||
resultingCodePoint = /*^.*/ 28;
|
||||
break;
|
||||
|
||||
case 'b': // alt+b, jumping backward in readline.
|
||||
case 'f': // alf+f, jumping forward in readline.
|
||||
case 'x': // alt+x, common in emacs.
|
||||
resultingCodePoint = lowerCase;
|
||||
altDown = true;
|
||||
break;
|
||||
|
||||
// Volume control.
|
||||
case 'v':
|
||||
resultingCodePoint = -1;
|
||||
AudioManager audio = (AudioManager) mActivity.getSystemService(Context.AUDIO_SERVICE);
|
||||
audio.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME, AudioManager.USE_DEFAULT_STREAM_TYPE, AudioManager.FLAG_SHOW_UI);
|
||||
break;
|
||||
|
||||
// Writing mode:
|
||||
case 'q':
|
||||
case 'k':
|
||||
mActivity.toggleShowExtraKeys();
|
||||
break;
|
||||
}
|
||||
|
||||
if (resultingKeyCode != -1) {
|
||||
TerminalEmulator term = session.getEmulator();
|
||||
session.write(KeyHandler.getCode(resultingKeyCode, 0, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode()));
|
||||
} else if (resultingCodePoint != -1) {
|
||||
session.writeCodePoint(altDown, resultingCodePoint);
|
||||
}
|
||||
return true;
|
||||
} else if (ctrlDown) {
|
||||
if (codePoint == 106 /* Ctrl+j or \n */ && !session.isRunning()) {
|
||||
mActivity.removeFinishedSession(session);
|
||||
return true;
|
||||
}
|
||||
|
||||
List<TermuxPreferences.KeyboardShortcut> shortcuts = mActivity.mSettings.shortcuts;
|
||||
if (!shortcuts.isEmpty()) {
|
||||
int codePointLowerCase = Character.toLowerCase(codePoint);
|
||||
for (int i = shortcuts.size() - 1; i >= 0; i--) {
|
||||
TermuxPreferences.KeyboardShortcut shortcut = shortcuts.get(i);
|
||||
if (codePointLowerCase == shortcut.codePoint) {
|
||||
switch (shortcut.shortcutAction) {
|
||||
case TermuxPreferences.SHORTCUT_ACTION_CREATE_SESSION:
|
||||
mActivity.addNewSession(false, null);
|
||||
return true;
|
||||
case TermuxPreferences.SHORTCUT_ACTION_PREVIOUS_SESSION:
|
||||
mActivity.switchToSession(false);
|
||||
return true;
|
||||
case TermuxPreferences.SHORTCUT_ACTION_NEXT_SESSION:
|
||||
mActivity.switchToSession(true);
|
||||
return true;
|
||||
case TermuxPreferences.SHORTCUT_ACTION_RENAME_SESSION:
|
||||
mActivity.renameSession(mActivity.getCurrentTermSession());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongPress(MotionEvent event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Handle dedicated volume buttons as virtual keys if applicable. */
|
||||
private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) {
|
||||
InputDevice inputDevice = event.getDevice();
|
||||
if (mActivity.mSettings.mDisableVolumeVirtualKeys) {
|
||||
return false;
|
||||
} else if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
|
||||
// Do not steal dedicated buttons from a full external keyboard.
|
||||
return false;
|
||||
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
|
||||
mVirtualControlKeyDown = down;
|
||||
return true;
|
||||
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
||||
mVirtualFnKeyDown = down;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.termux.app;
|
||||
package com.termux.app.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
@@ -13,7 +13,7 @@ import android.widget.ProgressBar;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
/** Basic embedded browser for viewing help pages. */
|
||||
public final class TermuxHelpActivity extends Activity {
|
||||
public final class HelpActivity extends Activity {
|
||||
|
||||
WebView mWebView;
|
||||
|
||||
180
app/src/main/java/com/termux/app/activities/ReportActivity.java
Normal file
180
app/src/main/java/com/termux/app/activities/ReportActivity.java
Normal file
@@ -0,0 +1,180 @@
|
||||
package com.termux.app.activities;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.interact.ShareUtils;
|
||||
import com.termux.app.models.ReportInfo;
|
||||
|
||||
import org.commonmark.node.FencedCodeBlock;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.recycler.MarkwonAdapter;
|
||||
import io.noties.markwon.recycler.SimpleEntry;
|
||||
|
||||
public class ReportActivity extends AppCompatActivity {
|
||||
|
||||
private static final String EXTRA_REPORT_INFO = "report_info";
|
||||
|
||||
ReportInfo mReportInfo;
|
||||
String mReportMarkdownString;
|
||||
String mReportActivityMarkdownString;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_report);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
if (toolbar != null) {
|
||||
setSupportActionBar(toolbar);
|
||||
}
|
||||
|
||||
Bundle bundle = null;
|
||||
Intent intent = getIntent();
|
||||
if (intent != null)
|
||||
bundle = intent.getExtras();
|
||||
else if (savedInstanceState != null)
|
||||
bundle = savedInstanceState;
|
||||
|
||||
updateUI(bundle);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
|
||||
if (intent != null)
|
||||
updateUI(intent.getExtras());
|
||||
}
|
||||
|
||||
private void updateUI(Bundle bundle) {
|
||||
|
||||
if (bundle == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
mReportInfo = (ReportInfo) bundle.getSerializable(EXTRA_REPORT_INFO);
|
||||
|
||||
if (mReportInfo == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
final ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
if (mReportInfo.reportTitle != null)
|
||||
actionBar.setTitle(mReportInfo.reportTitle);
|
||||
else
|
||||
actionBar.setTitle(TermuxConstants.TERMUX_APP_NAME + " App Report");
|
||||
}
|
||||
|
||||
|
||||
RecyclerView recyclerView = findViewById(R.id.recycler_view);
|
||||
|
||||
final Markwon markwon = MarkdownUtils.getRecyclerMarkwonBuilder(this);
|
||||
|
||||
final MarkwonAdapter adapter = MarkwonAdapter.builderTextViewIsRoot(R.layout.markdown_adapter_node_default)
|
||||
.include(FencedCodeBlock.class, SimpleEntry.create(R.layout.markdown_adapter_node_code_block, R.id.code_text_view))
|
||||
.build();
|
||||
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
|
||||
generateReportActivityMarkdownString();
|
||||
adapter.setMarkdown(markwon, mReportActivityMarkdownString);
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
|
||||
outState.putSerializable(EXTRA_REPORT_INFO, mReportInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||
final MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.menu_report, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
// Remove activity from recents menu on back button press
|
||||
finishAndRemoveTask();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
if (id == R.id.menu_item_share_report) {
|
||||
if (mReportMarkdownString != null)
|
||||
ShareUtils.shareText(this, getString(R.string.title_report_text), mReportMarkdownString);
|
||||
} else if (id == R.id.menu_item_copy_report) {
|
||||
if (mReportMarkdownString != null)
|
||||
ShareUtils.copyTextToClipboard(this, mReportMarkdownString, null);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the markdown {@link String} to be shown in {@link ReportActivity}.
|
||||
*/
|
||||
private void generateReportActivityMarkdownString() {
|
||||
mReportMarkdownString = ReportInfo.getReportInfoMarkdownString(mReportInfo);
|
||||
|
||||
mReportActivityMarkdownString = "";
|
||||
if (mReportInfo.reportStringPrefix != null)
|
||||
mReportActivityMarkdownString += mReportInfo.reportStringPrefix;
|
||||
|
||||
mReportActivityMarkdownString += mReportMarkdownString;
|
||||
|
||||
if (mReportInfo.reportStringSuffix != null)
|
||||
mReportActivityMarkdownString += mReportInfo.reportStringSuffix;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static void startReportActivity(@NonNull final Context context, @NonNull final ReportInfo reportInfo) {
|
||||
context.startActivity(newInstance(context, reportInfo));
|
||||
}
|
||||
|
||||
public static Intent newInstance(@NonNull final Context context, @NonNull final ReportInfo reportInfo) {
|
||||
Intent intent = new Intent(context, ReportActivity.class);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putSerializable(EXTRA_REPORT_INFO, reportInfo);
|
||||
intent.putExtras(bundle);
|
||||
|
||||
// Note that ReportActivity task has documentLaunchMode="intoExisting" set in AndroidManifest.xml
|
||||
// which has equivalent behaviour to the following. The following dynamic way doesn't seem to
|
||||
// work for notification pending intent, i.e separate task isn't created and activity is
|
||||
// launched in the same task as TermuxActivity.
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
|
||||
return intent;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.termux.app.activities;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.models.ReportInfo;
|
||||
import com.termux.app.models.UserAction;
|
||||
import com.termux.shared.interact.ShareUtils;
|
||||
import com.termux.shared.packages.PackageUtils;
|
||||
import com.termux.shared.settings.preferences.TermuxTaskerAppSharedPreferences;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
|
||||
public class SettingsActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_settings);
|
||||
if (savedInstanceState == null) {
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, new RootPreferencesFragment())
|
||||
.commit();
|
||||
}
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSupportNavigateUp() {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
public static class RootPreferencesFragment extends PreferenceFragmentCompat {
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
Context context = getContext();
|
||||
if (context == null) return;
|
||||
|
||||
setPreferencesFromResource(R.xml.root_preferences, rootKey);
|
||||
|
||||
configureTermuxTaskerPreference(context);
|
||||
configureAboutPreference(context);
|
||||
configureDonatePreference(context);
|
||||
}
|
||||
|
||||
private void configureTermuxTaskerPreference(@NonNull Context context) {
|
||||
Preference termuxTaskerPrefernce = findPreference("termux_tasker");
|
||||
if (termuxTaskerPrefernce != null) {
|
||||
TermuxTaskerAppSharedPreferences preferences = TermuxTaskerAppSharedPreferences.build(context, false);
|
||||
// If failed to get app preferences, then likely app is not installed, so do not show its preference
|
||||
termuxTaskerPrefernce.setVisible(preferences != null);
|
||||
}
|
||||
}
|
||||
|
||||
private void configureAboutPreference(@NonNull Context context) {
|
||||
Preference aboutPreference = findPreference("about");
|
||||
if (aboutPreference != null) {
|
||||
aboutPreference.setOnPreferenceClickListener(preference -> {
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
String title = "About";
|
||||
|
||||
StringBuilder aboutString = new StringBuilder();
|
||||
aboutString.append(TermuxUtils.getAppInfoMarkdownString(context, false));
|
||||
|
||||
String termuxPluginAppsInfo = TermuxUtils.getTermuxPluginAppsInfoMarkdownString(context);
|
||||
if (termuxPluginAppsInfo != null)
|
||||
aboutString.append("\n\n").append(termuxPluginAppsInfo);
|
||||
|
||||
aboutString.append("\n\n").append(TermuxUtils.getDeviceInfoMarkdownString(context));
|
||||
aboutString.append("\n\n").append(TermuxUtils.getImportantLinksMarkdownString(context));
|
||||
|
||||
ReportActivity.startReportActivity(context, new ReportInfo(UserAction.ABOUT, TermuxConstants.TERMUX_APP.TERMUX_SETTINGS_ACTIVITY_NAME, title, null, aboutString.toString(), null, false));
|
||||
}
|
||||
}.start();
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void configureDonatePreference(@NonNull Context context) {
|
||||
Preference donatePreference = findPreference("donate");
|
||||
if (donatePreference != null) {
|
||||
String signingCertificateSHA256Digest = PackageUtils.getSigningCertificateSHA256DigestForPackage(context);
|
||||
if (signingCertificateSHA256Digest != null) {
|
||||
// If APK is a Google Playstore release, then do not show the donation link
|
||||
// since Termux isn't exempted from the playstore policy donation links restriction
|
||||
// Check Fund solicitations: https://pay.google.com/intl/en_in/about/policy/
|
||||
String apkRelease = TermuxUtils.getAPKRelease(signingCertificateSHA256Digest);
|
||||
if (apkRelease == null || apkRelease.equals(TermuxConstants.APK_RELEASE_GOOGLE_PLAYSTORE_SIGNING_CERTIFICATE_SHA256_DIGEST)) {
|
||||
donatePreference.setVisible(false);
|
||||
return;
|
||||
} else {
|
||||
donatePreference.setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
donatePreference.setOnPreferenceClickListener(preference -> {
|
||||
ShareUtils.openURL(context, TermuxConstants.TERMUX_DONATE_URL);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package com.termux.app.fragments.settings.termux;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.PreferenceCategory;
|
||||
import androidx.preference.PreferenceDataStore;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.logger.Logger;
|
||||
|
||||
@Keep
|
||||
public class DebuggingPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
Context context = getContext();
|
||||
if (context == null) return;
|
||||
|
||||
PreferenceManager preferenceManager = getPreferenceManager();
|
||||
preferenceManager.setPreferenceDataStore(DebuggingPreferencesDataStore.getInstance(context));
|
||||
|
||||
setPreferencesFromResource(R.xml.termux_debugging_preferences, rootKey);
|
||||
|
||||
configureLoggingPreferences(context);
|
||||
}
|
||||
|
||||
private void configureLoggingPreferences(@NonNull Context context) {
|
||||
PreferenceCategory loggingCategory = findPreference("logging");
|
||||
if (loggingCategory == null) return;
|
||||
|
||||
ListPreference logLevelListPreference = findPreference("log_level");
|
||||
if (logLevelListPreference != null) {
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context, true);
|
||||
if (preferences == null) return;
|
||||
|
||||
setLogLevelListPreferenceData(logLevelListPreference, context, preferences.getLogLevel());
|
||||
loggingCategory.addPreference(logLevelListPreference);
|
||||
}
|
||||
}
|
||||
|
||||
public static ListPreference setLogLevelListPreferenceData(ListPreference logLevelListPreference, Context context, int logLevel) {
|
||||
if (logLevelListPreference == null)
|
||||
logLevelListPreference = new ListPreference(context);
|
||||
|
||||
CharSequence[] logLevels = Logger.getLogLevelsArray();
|
||||
CharSequence[] logLevelLabels = Logger.getLogLevelLabelsArray(context, logLevels, true);
|
||||
|
||||
logLevelListPreference.setEntryValues(logLevels);
|
||||
logLevelListPreference.setEntries(logLevelLabels);
|
||||
|
||||
logLevelListPreference.setValue(String.valueOf(logLevel));
|
||||
logLevelListPreference.setDefaultValue(Logger.DEFAULT_LOG_LEVEL);
|
||||
|
||||
return logLevelListPreference;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class DebuggingPreferencesDataStore extends PreferenceDataStore {
|
||||
|
||||
private final Context mContext;
|
||||
private final TermuxAppSharedPreferences mPreferences;
|
||||
|
||||
private static DebuggingPreferencesDataStore mInstance;
|
||||
|
||||
private DebuggingPreferencesDataStore(Context context) {
|
||||
mContext = context;
|
||||
mPreferences = TermuxAppSharedPreferences.build(context, true);
|
||||
}
|
||||
|
||||
public static synchronized DebuggingPreferencesDataStore getInstance(Context context) {
|
||||
if (mInstance == null) {
|
||||
mInstance = new DebuggingPreferencesDataStore(context);
|
||||
}
|
||||
return mInstance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String getString(String key, @Nullable String defValue) {
|
||||
if (mPreferences == null) return null;
|
||||
if (key == null) return null;
|
||||
|
||||
switch (key) {
|
||||
case "log_level":
|
||||
return String.valueOf(mPreferences.getLogLevel());
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putString(String key, @Nullable String value) {
|
||||
if (mPreferences == null) return;
|
||||
if (key == null) return;
|
||||
|
||||
switch (key) {
|
||||
case "log_level":
|
||||
if (value != null) {
|
||||
mPreferences.setLogLevel(mContext, Integer.parseInt(value));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void putBoolean(String key, boolean value) {
|
||||
if (mPreferences == null) return;
|
||||
if (key == null) return;
|
||||
|
||||
switch (key) {
|
||||
case "terminal_view_key_logging_enabled":
|
||||
mPreferences.setTerminalViewKeyLoggingEnabled(value);
|
||||
break;
|
||||
case "plugin_error_notifications_enabled":
|
||||
mPreferences.setPluginErrorNotificationsEnabled(value);
|
||||
break;
|
||||
case "crash_report_notifications_enabled":
|
||||
mPreferences.setCrashReportNotificationsEnabled(value);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getBoolean(String key, boolean defValue) {
|
||||
if (mPreferences == null) return false;
|
||||
switch (key) {
|
||||
case "terminal_view_key_logging_enabled":
|
||||
return mPreferences.isTerminalViewKeyLoggingEnabled();
|
||||
case "plugin_error_notifications_enabled":
|
||||
return mPreferences.arePluginErrorNotificationsEnabled();
|
||||
case "crash_report_notifications_enabled":
|
||||
return mPreferences.areCrashReportNotificationsEnabled();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.termux.app.fragments.settings.termux;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.preference.PreferenceDataStore;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
|
||||
@Keep
|
||||
public class TerminalIOPreferencesFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
Context context = getContext();
|
||||
if (context == null) return;
|
||||
|
||||
PreferenceManager preferenceManager = getPreferenceManager();
|
||||
preferenceManager.setPreferenceDataStore(TerminalIOPreferencesDataStore.getInstance(context));
|
||||
|
||||
setPreferencesFromResource(R.xml.termux_terminal_io_preferences, rootKey);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TerminalIOPreferencesDataStore extends PreferenceDataStore {
|
||||
|
||||
private final Context mContext;
|
||||
private final TermuxAppSharedPreferences mPreferences;
|
||||
|
||||
private static TerminalIOPreferencesDataStore mInstance;
|
||||
|
||||
private TerminalIOPreferencesDataStore(Context context) {
|
||||
mContext = context;
|
||||
mPreferences = TermuxAppSharedPreferences.build(context, true);
|
||||
}
|
||||
|
||||
public static synchronized TerminalIOPreferencesDataStore getInstance(Context context) {
|
||||
if (mInstance == null) {
|
||||
mInstance = new TerminalIOPreferencesDataStore(context);
|
||||
}
|
||||
return mInstance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void putBoolean(String key, boolean value) {
|
||||
if (mPreferences == null) return;
|
||||
if (key == null) return;
|
||||
|
||||
switch (key) {
|
||||
case "soft_keyboard_enabled":
|
||||
mPreferences.setSoftKeyboardEnabled(value);
|
||||
break;
|
||||
case "soft_keyboard_enabled_only_if_no_hardware":
|
||||
mPreferences.setSoftKeyboardEnabledOnlyIfNoHardware(value);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getBoolean(String key, boolean defValue) {
|
||||
if (mPreferences == null) return false;
|
||||
|
||||
switch (key) {
|
||||
case "soft_keyboard_enabled":
|
||||
return mPreferences.isSoftKeyboardEnabled();
|
||||
case "soft_keyboard_enabled_only_if_no_hardware":
|
||||
return mPreferences.isSoftKeyboardEnabledOnlyIfNoHardware();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
64
app/src/main/java/com/termux/app/models/ReportInfo.java
Normal file
64
app/src/main/java/com/termux/app/models/ReportInfo.java
Normal file
@@ -0,0 +1,64 @@
|
||||
package com.termux.app.models;
|
||||
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
|
||||
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;
|
||||
/** The internal app component that sent the report. */
|
||||
public final String sender;
|
||||
/** The report title. */
|
||||
public final String reportTitle;
|
||||
/** The markdown report text prefix. Will not be part of copy and share operations, etc. */
|
||||
public final String reportStringPrefix;
|
||||
/** The markdown report text. */
|
||||
public final String reportString;
|
||||
/** The markdown report text suffix. Will not be part of copy and share operations, etc. */
|
||||
public final String reportStringSuffix;
|
||||
/** If set to {@code true}, then report, app and device info will be added to the report when
|
||||
* markdown is generated.
|
||||
*/
|
||||
public final boolean addReportInfoToMarkdown;
|
||||
/** 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) {
|
||||
this.userAction = userAction;
|
||||
this.sender = sender;
|
||||
this.reportTitle = reportTitle;
|
||||
this.reportStringPrefix = reportStringPrefix;
|
||||
this.reportString = reportString;
|
||||
this.reportStringSuffix = reportStringSuffix;
|
||||
this.addReportInfoToMarkdown = addReportInfoToMarkdown;
|
||||
this.reportTimestamp = TermuxUtils.getCurrentTimeStamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a markdown {@link String} for {@link ReportInfo}.
|
||||
*
|
||||
* @param reportInfo The {@link ReportInfo} to convert.
|
||||
* @return Returns the markdown {@link String}.
|
||||
*/
|
||||
public static String getReportInfoMarkdownString(final ReportInfo reportInfo) {
|
||||
if (reportInfo == null) return "null";
|
||||
|
||||
StringBuilder markdownString = new StringBuilder();
|
||||
|
||||
if (reportInfo.addReportInfoToMarkdown) {
|
||||
markdownString.append("## Report Info\n\n");
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("User Action", reportInfo.userAction, "-"));
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Sender", reportInfo.sender, "-"));
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Report Timestamp", reportInfo.reportTimestamp, "-"));
|
||||
markdownString.append("\n##\n\n");
|
||||
}
|
||||
|
||||
markdownString.append(reportInfo.reportString);
|
||||
|
||||
return markdownString.toString();
|
||||
}
|
||||
|
||||
}
|
||||
20
app/src/main/java/com/termux/app/models/UserAction.java
Normal file
20
app/src/main/java/com/termux/app/models/UserAction.java
Normal file
@@ -0,0 +1,20 @@
|
||||
package com.termux.app.models;
|
||||
|
||||
public enum UserAction {
|
||||
|
||||
ABOUT("about"),
|
||||
CRASH_REPORT("crash report"),
|
||||
PLUGIN_EXECUTION_COMMAND("plugin execution command"),
|
||||
REPORT_ISSUE_FROM_TRANSCRIPT("report issue from transcript");
|
||||
|
||||
private final String name;
|
||||
|
||||
UserAction(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.termux.app.settings.properties;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.termux.app.terminal.io.KeyboardShortcut;
|
||||
import com.termux.app.terminal.io.extrakeys.ExtraKeysInfo;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.settings.properties.SharedPropertiesParser;
|
||||
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.shared.settings.properties.TermuxSharedProperties;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class TermuxAppSharedProperties extends TermuxSharedProperties implements SharedPropertiesParser {
|
||||
|
||||
private ExtraKeysInfo mExtraKeysInfo;
|
||||
private List<KeyboardShortcut> mSessionShortcuts = new ArrayList<>();
|
||||
|
||||
private static final String LOG_TAG = "TermuxAppSharedProperties";
|
||||
|
||||
public TermuxAppSharedProperties(@Nonnull Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload the termux properties from disk into an in-memory cache.
|
||||
*/
|
||||
@Override
|
||||
public void loadTermuxPropertiesFromDisk() {
|
||||
super.loadTermuxPropertiesFromDisk();
|
||||
|
||||
setExtraKeys();
|
||||
setSessionShortcuts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the terminal extra keys and style.
|
||||
*/
|
||||
private void setExtraKeys() {
|
||||
mExtraKeysInfo = null;
|
||||
|
||||
try {
|
||||
// The mMap stores the extra key and style string values while loading properties
|
||||
// Check {@link #getExtraKeysInternalPropertyValueFromValue(String)} and
|
||||
// {@link #getExtraKeysStyleInternalPropertyValueFromValue(String)}
|
||||
String extrakeys = (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS, true);
|
||||
String extraKeysStyle = (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE, true);
|
||||
mExtraKeysInfo = new ExtraKeysInfo(extrakeys, extraKeysStyle);
|
||||
} catch (JSONException e) {
|
||||
Logger.showToast(mContext, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: " + e.toString(), true);
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: ", e);
|
||||
|
||||
try {
|
||||
mExtraKeysInfo = new ExtraKeysInfo(TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE);
|
||||
} catch (JSONException e2) {
|
||||
Logger.showToast(mContext, "Can't create default extra keys",true);
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Could create default extra keys: ", e);
|
||||
mExtraKeysInfo = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the terminal sessions shortcuts.
|
||||
*/
|
||||
private void setSessionShortcuts() {
|
||||
if (mSessionShortcuts == null)
|
||||
mSessionShortcuts = new ArrayList<>();
|
||||
else
|
||||
mSessionShortcuts.clear();
|
||||
|
||||
// The {@link TermuxPropertyConstants#MAP_SESSION_SHORTCUTS} stores the session shortcut key and action pair
|
||||
for (Map.Entry<String, Integer> entry : TermuxPropertyConstants.MAP_SESSION_SHORTCUTS.entrySet()) {
|
||||
// The mMap stores the code points for the session shortcuts while loading properties
|
||||
Integer codePoint = (Integer) getInternalPropertyValue(entry.getKey(), true);
|
||||
// If codePoint is null, then session shortcut did not exist in properties or was invalid
|
||||
// as parsed by {@link #getCodePointForSessionShortcuts(String,String)}
|
||||
// If codePoint is not null, then get the action for the MAP_SESSION_SHORTCUTS key and
|
||||
// add the code point to sessionShortcuts
|
||||
if (codePoint != null)
|
||||
mSessionShortcuts.add(new KeyboardShortcut(codePoint, entry.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
public List<KeyboardShortcut> getSessionShortcuts() {
|
||||
return mSessionShortcuts;
|
||||
}
|
||||
|
||||
public ExtraKeysInfo getExtraKeysInfo() {
|
||||
return mExtraKeysInfo;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.termux.app.terminal;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Typeface;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.shared.shell.TermuxSession;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TermuxSessionsListViewController extends ArrayAdapter<TermuxSession> implements AdapterView.OnItemClickListener, AdapterView.OnItemLongClickListener {
|
||||
|
||||
final TermuxActivity mActivity;
|
||||
|
||||
final StyleSpan boldSpan = new StyleSpan(Typeface.BOLD);
|
||||
final StyleSpan italicSpan = new StyleSpan(Typeface.ITALIC);
|
||||
|
||||
public TermuxSessionsListViewController(TermuxActivity activity, List<TermuxSession> sessionList) {
|
||||
super(activity.getApplicationContext(), R.layout.item_terminal_sessions_list, sessionList);
|
||||
this.mActivity = activity;
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@NonNull
|
||||
@Override
|
||||
public View getView(int position, View convertView, @NonNull ViewGroup parent) {
|
||||
View sessionRowView = convertView;
|
||||
if (sessionRowView == null) {
|
||||
LayoutInflater inflater = mActivity.getLayoutInflater();
|
||||
sessionRowView = inflater.inflate(R.layout.item_terminal_sessions_list, parent, false);
|
||||
}
|
||||
|
||||
TextView sessionTitleView = sessionRowView.findViewById(R.id.session_title);
|
||||
|
||||
TerminalSession sessionAtRow = getItem(position).getTerminalSession();
|
||||
if (sessionAtRow == null) {
|
||||
sessionTitleView.setText("null session");
|
||||
return sessionRowView;
|
||||
}
|
||||
|
||||
boolean isUsingBlackUI = mActivity.getProperties().isUsingBlackUI();
|
||||
|
||||
if (isUsingBlackUI) {
|
||||
sessionTitleView.setBackground(
|
||||
ContextCompat.getDrawable(mActivity, R.drawable.session_background_black_selected)
|
||||
);
|
||||
}
|
||||
|
||||
String name = sessionAtRow.mSessionName;
|
||||
String sessionTitle = sessionAtRow.getTitle();
|
||||
|
||||
String numberPart = "[" + (position + 1) + "] ";
|
||||
String sessionNamePart = (TextUtils.isEmpty(name) ? "" : name);
|
||||
String sessionTitlePart = (TextUtils.isEmpty(sessionTitle) ? "" : ((sessionNamePart.isEmpty() ? "" : "\n") + sessionTitle));
|
||||
|
||||
String fullSessionTitle = numberPart + sessionNamePart + sessionTitlePart;
|
||||
SpannableString fullSessionTitleStyled = new SpannableString(fullSessionTitle);
|
||||
fullSessionTitleStyled.setSpan(boldSpan, 0, numberPart.length() + sessionNamePart.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
fullSessionTitleStyled.setSpan(italicSpan, numberPart.length() + sessionNamePart.length(), fullSessionTitle.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
sessionTitleView.setText(fullSessionTitleStyled);
|
||||
|
||||
boolean sessionRunning = sessionAtRow.isRunning();
|
||||
|
||||
if (sessionRunning) {
|
||||
sessionTitleView.setPaintFlags(sessionTitleView.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
|
||||
} else {
|
||||
sessionTitleView.setPaintFlags(sessionTitleView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
|
||||
}
|
||||
int defaultColor = isUsingBlackUI ? Color.WHITE : Color.BLACK;
|
||||
int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? defaultColor : Color.RED;
|
||||
sessionTitleView.setTextColor(color);
|
||||
return sessionRowView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
TermuxSession clickedSession = getItem(position);
|
||||
mActivity.getTermuxTerminalSessionClient().setCurrentSession(clickedSession.getTerminalSession());
|
||||
mActivity.getDrawer().closeDrawers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
final TermuxSession selectedSession = getItem(position);
|
||||
mActivity.getTermuxTerminalSessionClient().renameSession(selectedSession.getTerminalSession());
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
package com.termux.app.terminal;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Typeface;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.SoundPool;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.ListView;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.shared.shell.TermuxSession;
|
||||
import com.termux.shared.interact.DialogUtils;
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.shared.terminal.TermuxTerminalSessionClientBase;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.app.TermuxService;
|
||||
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.app.terminal.io.BellHandler;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.terminal.TerminalColors;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
import com.termux.terminal.TextStyle;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.Properties;
|
||||
|
||||
public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase {
|
||||
|
||||
private final TermuxActivity mActivity;
|
||||
|
||||
private static final int MAX_SESSIONS = 8;
|
||||
|
||||
private SoundPool mBellSoundPool;
|
||||
|
||||
private int mBellSoundId;
|
||||
|
||||
private static final String LOG_TAG = "TermuxTerminalSessionClient";
|
||||
|
||||
public TermuxTerminalSessionClient(TermuxActivity activity) {
|
||||
this.mActivity = activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when mActivity.onCreate() is called
|
||||
*/
|
||||
public void onCreate() {
|
||||
// Set terminal fonts and colors
|
||||
checkForFontAndColors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when mActivity.onStart() is called
|
||||
*/
|
||||
public void onStart() {
|
||||
// The service has connected, but data may have changed since we were last in the foreground.
|
||||
// Get the session stored in shared preferences stored by {@link #onStop} if its valid,
|
||||
// otherwise get the last session currently running.
|
||||
if (mActivity.getTermuxService() != null) {
|
||||
setCurrentSession(getCurrentStoredSessionOrLast());
|
||||
termuxSessionListNotifyUpdated();
|
||||
}
|
||||
|
||||
// The current terminal session may have changed while being away, force
|
||||
// a refresh of the displayed terminal.
|
||||
mActivity.getTerminalView().onScreenUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when mActivity.onResume() is called
|
||||
*/
|
||||
public void onResume() {
|
||||
// Just initialize the mBellSoundPool and load the sound, otherwise bell might not run
|
||||
// the first time bell key is pressed and play() is called, since sound may not be loaded
|
||||
// quickly enough before the call to play(). https://stackoverflow.com/questions/35435625
|
||||
getBellSoundPool();
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when mActivity.onStop() is called
|
||||
*/
|
||||
public void onStop() {
|
||||
// Store current session in shared preferences so that it can be restored later in
|
||||
// {@link #onStart} if needed.
|
||||
setCurrentStoredSession();
|
||||
|
||||
// Release mBellSoundPool resources, specially to prevent exceptions like the following to be thrown
|
||||
// java.util.concurrent.TimeoutException: android.media.SoundPool.finalize() timed out after 10 seconds
|
||||
// Bell is not played in background anyways
|
||||
// Related: https://stackoverflow.com/a/28708351/14686958
|
||||
releaseBellSoundPool();
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when mActivity.reloadActivityStyling() is called
|
||||
*/
|
||||
public void onReload() {
|
||||
// Set terminal fonts and colors
|
||||
checkForFontAndColors();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void onTextChanged(TerminalSession changedSession) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
if (mActivity.getCurrentSession() == changedSession) mActivity.getTerminalView().onScreenUpdated();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTitleChanged(TerminalSession updatedSession) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
if (updatedSession != mActivity.getCurrentSession()) {
|
||||
// Only show toast for other sessions than the current one, since the user
|
||||
// probably consciously caused the title change to change in the current session
|
||||
// and don't want an annoying toast for that.
|
||||
mActivity.showToast(toToastTitle(updatedSession), true);
|
||||
}
|
||||
|
||||
termuxSessionListNotifyUpdated();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionFinished(final TerminalSession finishedSession) {
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
|
||||
if (service == null || service.wantsToStop()) {
|
||||
// The service wants to stop as soon as possible.
|
||||
mActivity.finishActivityIfNotFinishing();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mActivity.isVisible() && finishedSession != mActivity.getCurrentSession()) {
|
||||
// Show toast for non-current sessions that exit.
|
||||
int indexOfSession = service.getIndexOfSession(finishedSession);
|
||||
// Verify that session was not removed before we got told about it finishing:
|
||||
if (indexOfSession >= 0)
|
||||
mActivity.showToast(toToastTitle(finishedSession) + " - exited", true);
|
||||
}
|
||||
|
||||
if (mActivity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
|
||||
// On Android TV devices we need to use older behaviour because we may
|
||||
// not be able to have multiple launcher icons.
|
||||
if (service.getTermuxSessionsSize() > 1) {
|
||||
removeFinishedSession(finishedSession);
|
||||
}
|
||||
} else {
|
||||
// Once we have a separate launcher icon for the failsafe session, it
|
||||
// should be safe to auto-close session on exit code '0' or '130'.
|
||||
if (finishedSession.getExitStatus() == 0 || finishedSession.getExitStatus() == 130) {
|
||||
removeFinishedSession(finishedSession);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClipboardText(TerminalSession session, String text) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(text)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBell(TerminalSession session) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
switch (mActivity.getProperties().getBellBehaviour()) {
|
||||
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_VIBRATE:
|
||||
BellHandler.getInstance(mActivity).doBell();
|
||||
break;
|
||||
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_BEEP:
|
||||
getBellSoundPool().play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f);
|
||||
break;
|
||||
case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_IGNORE:
|
||||
// Ignore the bell character.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onColorsChanged(TerminalSession changedSession) {
|
||||
if (mActivity.getCurrentSession() == changedSession)
|
||||
updateBackgroundColor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminalCursorStateChange(boolean enabled) {
|
||||
// Do not start cursor blinking thread if activity is not visible
|
||||
if (enabled && !mActivity.isVisible()) {
|
||||
Logger.logVerbose(LOG_TAG, "Ignoring call to start cursor blinking since activity is not visible");
|
||||
return;
|
||||
}
|
||||
|
||||
// If cursor is to enabled now, then start cursor blinking if blinking is enabled
|
||||
// otherwise stop cursor blinking
|
||||
mActivity.getTerminalView().setTerminalCursorBlinkerState(enabled, false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Initialize and get mBellSoundPool */
|
||||
private synchronized SoundPool getBellSoundPool() {
|
||||
if (mBellSoundPool == null) {
|
||||
mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
|
||||
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
|
||||
|
||||
mBellSoundId = mBellSoundPool.load(mActivity, R.raw.bell, 1);
|
||||
}
|
||||
|
||||
return mBellSoundPool;
|
||||
}
|
||||
|
||||
/** Release mBellSoundPool resources */
|
||||
private synchronized void releaseBellSoundPool() {
|
||||
if (mBellSoundPool != null) {
|
||||
mBellSoundPool.release();
|
||||
mBellSoundPool = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Try switching to session. */
|
||||
public void setCurrentSession(TerminalSession session) {
|
||||
if (session == null) return;
|
||||
|
||||
if (mActivity.getTerminalView().attachSession(session)) {
|
||||
// notify about switched session if not already displaying the session
|
||||
notifyOfSessionChange();
|
||||
}
|
||||
|
||||
// We call the following even when the session is already being displayed since config may
|
||||
// be stale, like current session not selected or scrolled to.
|
||||
checkAndScrollToSession(session);
|
||||
updateBackgroundColor();
|
||||
}
|
||||
|
||||
void notifyOfSessionChange() {
|
||||
if (!mActivity.isVisible()) return;
|
||||
|
||||
TerminalSession session = mActivity.getCurrentSession();
|
||||
mActivity.showToast(toToastTitle(session), false);
|
||||
}
|
||||
|
||||
public void switchToSession(boolean forward) {
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
if (service == null) return;
|
||||
|
||||
TerminalSession currentTerminalSession = mActivity.getCurrentSession();
|
||||
int index = service.getIndexOfSession(currentTerminalSession);
|
||||
int size = service.getTermuxSessionsSize();
|
||||
if (forward) {
|
||||
if (++index >= size) index = 0;
|
||||
} else {
|
||||
if (--index < 0) index = size - 1;
|
||||
}
|
||||
|
||||
TermuxSession termuxSession = service.getTermuxSession(index);
|
||||
if (termuxSession != null)
|
||||
setCurrentSession(termuxSession.getTerminalSession());
|
||||
}
|
||||
|
||||
public void switchToSession(int index) {
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
if (service == null) return;
|
||||
|
||||
TermuxSession termuxSession = service.getTermuxSession(index);
|
||||
if (termuxSession != null)
|
||||
setCurrentSession(termuxSession.getTerminalSession());
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
public void renameSession(final TerminalSession sessionToRename) {
|
||||
if (sessionToRename == null) return;
|
||||
|
||||
DialogUtils.textInput(mActivity, R.string.title_rename_session, sessionToRename.mSessionName, R.string.action_rename_session_confirm, text -> {
|
||||
sessionToRename.mSessionName = text;
|
||||
termuxSessionListNotifyUpdated();
|
||||
}, -1, null, -1, null, null);
|
||||
}
|
||||
|
||||
public void addNewSession(boolean isFailSafe, String sessionName) {
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
if (service == null) return;
|
||||
|
||||
if (service.getTermuxSessionsSize() >= MAX_SESSIONS) {
|
||||
new AlertDialog.Builder(mActivity).setTitle(R.string.title_max_terminals_reached).setMessage(R.string.msg_max_terminals_reached)
|
||||
.setPositiveButton(android.R.string.ok, null).show();
|
||||
} else {
|
||||
TerminalSession currentSession = mActivity.getCurrentSession();
|
||||
|
||||
String workingDirectory;
|
||||
if (currentSession == null) {
|
||||
workingDirectory = mActivity.getProperties().getDefaultWorkingDirectory();
|
||||
} else {
|
||||
workingDirectory = currentSession.getCwd();
|
||||
}
|
||||
|
||||
TermuxSession newTermuxSession = service.createTermuxSession(null, null, null, workingDirectory, isFailSafe, sessionName);
|
||||
if (newTermuxSession == null) return;
|
||||
|
||||
TerminalSession newTerminalSession = newTermuxSession.getTerminalSession();
|
||||
setCurrentSession(newTerminalSession);
|
||||
|
||||
mActivity.getDrawer().closeDrawers();
|
||||
}
|
||||
}
|
||||
|
||||
public void setCurrentStoredSession() {
|
||||
TerminalSession currentSession = mActivity.getCurrentSession();
|
||||
if (currentSession != null)
|
||||
mActivity.getPreferences().setCurrentSession(currentSession.mHandle);
|
||||
else
|
||||
mActivity.getPreferences().setCurrentSession(null);
|
||||
}
|
||||
|
||||
/** The current session as stored or the last one if that does not exist. */
|
||||
public TerminalSession getCurrentStoredSessionOrLast() {
|
||||
TerminalSession stored = getCurrentStoredSession();
|
||||
|
||||
if (stored != null) {
|
||||
// If a stored session is in the list of currently running sessions, then return it
|
||||
return stored;
|
||||
} else {
|
||||
// Else return the last session currently running
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
if (service == null) return null;
|
||||
|
||||
TermuxSession termuxSession = service.getLastTermuxSession();
|
||||
if (termuxSession != null)
|
||||
return termuxSession.getTerminalSession();
|
||||
else
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private TerminalSession getCurrentStoredSession() {
|
||||
String sessionHandle = mActivity.getPreferences().getCurrentSession();
|
||||
|
||||
// If no session is stored in shared preferences
|
||||
if (sessionHandle == null)
|
||||
return null;
|
||||
|
||||
// Check if the session handle found matches one of the currently running sessions
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
if (service == null) return null;
|
||||
|
||||
return service.getTerminalSessionForHandle(sessionHandle);
|
||||
}
|
||||
|
||||
public void removeFinishedSession(TerminalSession finishedSession) {
|
||||
// Return pressed with finished session - remove it.
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
if (service == null) return;
|
||||
|
||||
int index = service.removeTermuxSession(finishedSession);
|
||||
|
||||
int size = service.getTermuxSessionsSize();
|
||||
if (size == 0) {
|
||||
// There are no sessions to show, so finish the activity.
|
||||
mActivity.finishActivityIfNotFinishing();
|
||||
} else {
|
||||
if (index >= size) {
|
||||
index = size - 1;
|
||||
}
|
||||
TermuxSession termuxSession = service.getTermuxSession(index);
|
||||
if (termuxSession != null)
|
||||
setCurrentSession(termuxSession.getTerminalSession());
|
||||
}
|
||||
}
|
||||
|
||||
public void termuxSessionListNotifyUpdated() {
|
||||
mActivity.termuxSessionListNotifyUpdated();
|
||||
}
|
||||
|
||||
public void checkAndScrollToSession(TerminalSession session) {
|
||||
if (!mActivity.isVisible()) return;
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
if (service == null) return;
|
||||
|
||||
final int indexOfSession = service.getIndexOfSession(session);
|
||||
if (indexOfSession < 0) return;
|
||||
final ListView termuxSessionsListView = mActivity.findViewById(R.id.terminal_sessions_list);
|
||||
if (termuxSessionsListView == null) return;
|
||||
|
||||
termuxSessionsListView.setItemChecked(indexOfSession, true);
|
||||
// Delay is necessary otherwise sometimes scroll to newly added session does not happen
|
||||
termuxSessionsListView.postDelayed(() -> termuxSessionsListView.smoothScrollToPosition(indexOfSession), 1000);
|
||||
}
|
||||
|
||||
|
||||
String toToastTitle(TerminalSession session) {
|
||||
TermuxService service = mActivity.getTermuxService();
|
||||
if (service == null) return null;
|
||||
|
||||
final int indexOfSession = service.getIndexOfSession(session);
|
||||
if (indexOfSession < 0) return null;
|
||||
StringBuilder toastTitle = new StringBuilder("[" + (indexOfSession + 1) + "]");
|
||||
if (!TextUtils.isEmpty(session.mSessionName)) {
|
||||
toastTitle.append(" ").append(session.mSessionName);
|
||||
}
|
||||
String title = session.getTitle();
|
||||
if (!TextUtils.isEmpty(title)) {
|
||||
// Space to "[${NR}] or newline after session name:
|
||||
toastTitle.append(session.mSessionName == null ? " " : "\n");
|
||||
toastTitle.append(title);
|
||||
}
|
||||
return toastTitle.toString();
|
||||
}
|
||||
|
||||
|
||||
public void checkForFontAndColors() {
|
||||
try {
|
||||
File colorsFile = TermuxConstants.TERMUX_COLOR_PROPERTIES_FILE;
|
||||
File fontFile = TermuxConstants.TERMUX_FONT_FILE;
|
||||
|
||||
final Properties props = new Properties();
|
||||
if (colorsFile.isFile()) {
|
||||
try (InputStream in = new FileInputStream(colorsFile)) {
|
||||
props.load(in);
|
||||
}
|
||||
}
|
||||
|
||||
TerminalColors.COLOR_SCHEME.updateWith(props);
|
||||
TerminalSession session = mActivity.getCurrentSession();
|
||||
if (session != null && session.getEmulator() != null) {
|
||||
session.getEmulator().mColors.reset();
|
||||
}
|
||||
updateBackgroundColor();
|
||||
|
||||
final Typeface newTypeface = (fontFile.exists() && fontFile.length() > 0) ? Typeface.createFromFile(fontFile) : Typeface.MONOSPACE;
|
||||
mActivity.getTerminalView().setTypeface(newTypeface);
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Error in checkForFontAndColors()", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateBackgroundColor() {
|
||||
if (!mActivity.isVisible()) return;
|
||||
TerminalSession session = mActivity.getCurrentSession();
|
||||
if (session != null && session.getEmulator() != null) {
|
||||
mActivity.getWindow().getDecorView().setBackgroundColor(session.getEmulator().mColors.mCurrentColors[TextStyle.COLOR_INDEX_BACKGROUND]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,605 @@
|
||||
package com.termux.app.terminal;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.media.AudioManager;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Gravity;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.ListView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.shared.shell.ShellUtils;
|
||||
import com.termux.shared.terminal.TermuxTerminalViewClientBase;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.app.activities.ReportActivity;
|
||||
import com.termux.app.models.ReportInfo;
|
||||
import com.termux.app.models.UserAction;
|
||||
import com.termux.app.terminal.io.KeyboardShortcut;
|
||||
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
|
||||
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
import com.termux.shared.view.KeyboardUtils;
|
||||
import com.termux.terminal.KeyHandler;
|
||||
import com.termux.terminal.TerminalEmulator;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
|
||||
public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
||||
|
||||
final TermuxActivity mActivity;
|
||||
|
||||
final TermuxTerminalSessionClient mTermuxTerminalSessionClient;
|
||||
|
||||
/** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */
|
||||
boolean mVirtualControlKeyDown, mVirtualFnKeyDown;
|
||||
|
||||
private Runnable mShowSoftKeyboardRunnable;
|
||||
|
||||
private 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
|
||||
mActivity.getTerminalView().setIsTerminalViewKeyLoggingEnabled(mActivity.getPreferences().isTerminalViewKeyLoggingEnabled());
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when mActivity.onResume() is called
|
||||
*/
|
||||
public void onResume() {
|
||||
// Show the soft keyboard if required
|
||||
setSoftKeyboardState(true, false);
|
||||
|
||||
// Start terminal cursor blinking if enabled
|
||||
setTerminalCursorBlinkerState(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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public float onScale(float scale) {
|
||||
if (scale < 0.9f || scale > 1.1f) {
|
||||
boolean increase = scale > 1.f;
|
||||
changeFontSize(increase);
|
||||
return 1.0f;
|
||||
}
|
||||
return scale;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void onSingleTapUp(MotionEvent e) {
|
||||
if (!KeyboardUtils.areDisableSoftKeyboardFlagsSet(mActivity))
|
||||
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||
else
|
||||
Logger.logVerbose(LOG_TAG, "Not showing soft keyboard onSingleTapUp since its disabled");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldBackButtonBeMappedToEscape() {
|
||||
return mActivity.getProperties().isBackKeyTheEscapeKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldEnforceCharBasedInput() {
|
||||
return mActivity.getProperties().isEnforcingCharBasedInput();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldUseCtrlSpaceWorkaround() {
|
||||
return mActivity.getProperties().isUsingCtrlSpaceWorkaround();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void copyModeChanged(boolean copyMode) {
|
||||
// Disable drawer while copying.
|
||||
mActivity.getDrawer().setDrawerLockMode(copyMode ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED : DrawerLayout.LOCK_MODE_UNLOCKED);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession currentSession) {
|
||||
if (handleVirtualKeys(keyCode, e, true)) return true;
|
||||
|
||||
if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) {
|
||||
mTermuxTerminalSessionClient.removeFinishedSession(currentSession);
|
||||
return true;
|
||||
} else if (e.isCtrlPressed() && e.isAltPressed()) {
|
||||
// Get the unmodified code point:
|
||||
int unicodeChar = e.getUnicodeChar(0);
|
||||
|
||||
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || unicodeChar == 'n'/* next */) {
|
||||
mTermuxTerminalSessionClient.switchToSession(true);
|
||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_UP || unicodeChar == 'p' /* previous */) {
|
||||
mTermuxTerminalSessionClient.switchToSession(false);
|
||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
|
||||
mActivity.getDrawer().openDrawer(Gravity.LEFT);
|
||||
} else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
|
||||
mActivity.getDrawer().closeDrawers();
|
||||
} else if (unicodeChar == 'k'/* keyboard */) {
|
||||
onToggleSoftKeyboardRequest();
|
||||
} else if (unicodeChar == 'm'/* menu */) {
|
||||
mActivity.getTerminalView().showContextMenu();
|
||||
} else if (unicodeChar == 'r'/* rename */) {
|
||||
mTermuxTerminalSessionClient.renameSession(currentSession);
|
||||
} else if (unicodeChar == 'c'/* create */) {
|
||||
mTermuxTerminalSessionClient.addNewSession(false, null);
|
||||
} else if (unicodeChar == 'u' /* urls */) {
|
||||
showUrlSelection();
|
||||
} else if (unicodeChar == 'v') {
|
||||
doPaste();
|
||||
} else if (unicodeChar == '+' || e.getUnicodeChar(KeyEvent.META_SHIFT_ON) == '+') {
|
||||
// We also check for the shifted char here since shift may be required to produce '+',
|
||||
// see https://github.com/termux/termux-api/issues/2
|
||||
changeFontSize(true);
|
||||
} else if (unicodeChar == '-') {
|
||||
changeFontSize(false);
|
||||
} else if (unicodeChar >= '1' && unicodeChar <= '9') {
|
||||
int index = unicodeChar - '1';
|
||||
mTermuxTerminalSessionClient.switchToSession(index);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent e) {
|
||||
return handleVirtualKeys(keyCode, e, false);
|
||||
}
|
||||
|
||||
/** Handle dedicated volume buttons as virtual keys if applicable. */
|
||||
private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) {
|
||||
InputDevice inputDevice = event.getDevice();
|
||||
if (mActivity.getProperties().areVirtualVolumeKeysDisabled()) {
|
||||
return false;
|
||||
} else if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
|
||||
// Do not steal dedicated buttons from a full external keyboard.
|
||||
return false;
|
||||
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
|
||||
mVirtualControlKeyDown = down;
|
||||
return true;
|
||||
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
||||
mVirtualFnKeyDown = down;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public boolean readControlKey() {
|
||||
return (mActivity.getExtraKeysView() != null && mActivity.getExtraKeysView().readSpecialButton(ExtraKeysView.SpecialButton.CTRL)) || mVirtualControlKeyDown;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readAltKey() {
|
||||
return (mActivity.getExtraKeysView() != null && mActivity.getExtraKeysView().readSpecialButton(ExtraKeysView.SpecialButton.ALT));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongPress(MotionEvent event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onCodePoint(final int codePoint, boolean ctrlDown, TerminalSession session) {
|
||||
if (mVirtualFnKeyDown) {
|
||||
int resultingKeyCode = -1;
|
||||
int resultingCodePoint = -1;
|
||||
boolean altDown = false;
|
||||
int lowerCase = Character.toLowerCase(codePoint);
|
||||
switch (lowerCase) {
|
||||
// Arrow keys.
|
||||
case 'w':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_DPAD_UP;
|
||||
break;
|
||||
case 'a':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_DPAD_LEFT;
|
||||
break;
|
||||
case 's':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_DPAD_DOWN;
|
||||
break;
|
||||
case 'd':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
|
||||
break;
|
||||
|
||||
// Page up and down.
|
||||
case 'p':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_PAGE_UP;
|
||||
break;
|
||||
case 'n':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_PAGE_DOWN;
|
||||
break;
|
||||
|
||||
// Some special keys:
|
||||
case 't':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_TAB;
|
||||
break;
|
||||
case 'i':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_INSERT;
|
||||
break;
|
||||
case 'h':
|
||||
resultingCodePoint = '~';
|
||||
break;
|
||||
|
||||
// Special characters to input.
|
||||
case 'u':
|
||||
resultingCodePoint = '_';
|
||||
break;
|
||||
case 'l':
|
||||
resultingCodePoint = '|';
|
||||
break;
|
||||
|
||||
// Function keys.
|
||||
case '1':
|
||||
case '2':
|
||||
case '3':
|
||||
case '4':
|
||||
case '5':
|
||||
case '6':
|
||||
case '7':
|
||||
case '8':
|
||||
case '9':
|
||||
resultingKeyCode = (codePoint - '1') + KeyEvent.KEYCODE_F1;
|
||||
break;
|
||||
case '0':
|
||||
resultingKeyCode = KeyEvent.KEYCODE_F10;
|
||||
break;
|
||||
|
||||
// Other special keys.
|
||||
case 'e':
|
||||
resultingCodePoint = /*Escape*/ 27;
|
||||
break;
|
||||
case '.':
|
||||
resultingCodePoint = /*^.*/ 28;
|
||||
break;
|
||||
|
||||
case 'b': // alt+b, jumping backward in readline.
|
||||
case 'f': // alf+f, jumping forward in readline.
|
||||
case 'x': // alt+x, common in emacs.
|
||||
resultingCodePoint = lowerCase;
|
||||
altDown = true;
|
||||
break;
|
||||
|
||||
// Volume control.
|
||||
case 'v':
|
||||
resultingCodePoint = -1;
|
||||
AudioManager audio = (AudioManager) mActivity.getSystemService(Context.AUDIO_SERVICE);
|
||||
audio.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME, AudioManager.USE_DEFAULT_STREAM_TYPE, AudioManager.FLAG_SHOW_UI);
|
||||
break;
|
||||
|
||||
// Writing mode:
|
||||
case 'q':
|
||||
case 'k':
|
||||
mActivity.toggleTerminalToolbar();
|
||||
mVirtualFnKeyDown=false; // force disable fn key down to restore keyboard input into terminal view, fixes termux/termux-app#1420
|
||||
break;
|
||||
}
|
||||
|
||||
if (resultingKeyCode != -1) {
|
||||
TerminalEmulator term = session.getEmulator();
|
||||
session.write(KeyHandler.getCode(resultingKeyCode, 0, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode()));
|
||||
} else if (resultingCodePoint != -1) {
|
||||
session.writeCodePoint(altDown, resultingCodePoint);
|
||||
}
|
||||
return true;
|
||||
} else if (ctrlDown) {
|
||||
if (codePoint == 106 /* Ctrl+j or \n */ && !session.isRunning()) {
|
||||
mTermuxTerminalSessionClient.removeFinishedSession(session);
|
||||
return true;
|
||||
}
|
||||
|
||||
List<KeyboardShortcut> shortcuts = mActivity.getProperties().getSessionShortcuts();
|
||||
if (shortcuts != null && !shortcuts.isEmpty()) {
|
||||
int codePointLowerCase = Character.toLowerCase(codePoint);
|
||||
for (int i = shortcuts.size() - 1; i >= 0; i--) {
|
||||
KeyboardShortcut shortcut = shortcuts.get(i);
|
||||
if (codePointLowerCase == shortcut.codePoint) {
|
||||
switch (shortcut.shortcutAction) {
|
||||
case TermuxPropertyConstants.ACTION_SHORTCUT_CREATE_SESSION:
|
||||
mTermuxTerminalSessionClient.addNewSession(false, null);
|
||||
return true;
|
||||
case TermuxPropertyConstants.ACTION_SHORTCUT_NEXT_SESSION:
|
||||
mTermuxTerminalSessionClient.switchToSession(true);
|
||||
return true;
|
||||
case TermuxPropertyConstants.ACTION_SHORTCUT_PREVIOUS_SESSION:
|
||||
mTermuxTerminalSessionClient.switchToSession(false);
|
||||
return true;
|
||||
case TermuxPropertyConstants.ACTION_SHORTCUT_RENAME_SESSION:
|
||||
mTermuxTerminalSessionClient.renameSession(mActivity.getCurrentSession());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void changeFontSize(boolean increase) {
|
||||
mActivity.getPreferences().changeFontSize(increase);
|
||||
mActivity.getTerminalView().setTextSize(mActivity.getPreferences().getFontSize());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Called when user requests the soft keyboard to be toggled via "KEYBOARD" toggle button in
|
||||
* drawer or extra keys, or with ctrl+alt+k hardware keyboard shortcut.
|
||||
*/
|
||||
public void onToggleSoftKeyboardRequest() {
|
||||
// If soft keyboard toggle behaviour is enable/disabled
|
||||
if (mActivity.getProperties().shouldEnableDisableSoftKeyboardOnToggle()) {
|
||||
// If soft keyboard is visible
|
||||
if (!KeyboardUtils.areDisableSoftKeyboardFlagsSet(mActivity)) {
|
||||
Logger.logVerbose(LOG_TAG, "Disabling soft keyboard on toggle");
|
||||
mActivity.getPreferences().setSoftKeyboardEnabled(false);
|
||||
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||
} else {
|
||||
Logger.logVerbose(LOG_TAG, "Enabling soft keyboard on toggle");
|
||||
mActivity.getPreferences().setSoftKeyboardEnabled(true);
|
||||
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
|
||||
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) {
|
||||
// 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());
|
||||
} else {
|
||||
// Set flag to automatically push up TerminalView when keyboard is opened instead of showing over it
|
||||
KeyboardUtils.setResizeTerminalViewForSoftKeyboardFlags(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);
|
||||
} else {
|
||||
// Do not force show soft keyboard if termux-reload-settings command was run with hardware keyboard
|
||||
if (isReloadTermuxProperties)
|
||||
return;
|
||||
|
||||
if (mShowSoftKeyboardRunnable == null) {
|
||||
mShowSoftKeyboardRunnable = () -> {
|
||||
Logger.logVerbose(LOG_TAG, "Showing soft keyboard on focus change");
|
||||
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||
};
|
||||
}
|
||||
|
||||
mActivity.getTerminalView().setOnFocusChangeListener(new View.OnFocusChangeListener() {
|
||||
@Override
|
||||
public void onFocusChange(View view, boolean hasFocus) {
|
||||
// Force show soft keyboard if TerminalView has focus and close it if it doesn't
|
||||
KeyboardUtils.setSoftKeyboardVisibility(mShowSoftKeyboardRunnable, mActivity, mActivity.getTerminalView(), hasFocus);
|
||||
}
|
||||
});
|
||||
|
||||
// Request focus for TerminalView
|
||||
mActivity.getTerminalView().requestFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void setTerminalCursorBlinkerState(boolean start) {
|
||||
if (start) {
|
||||
// If set/update the cursor blinking rate is successful, then enable cursor blinker
|
||||
if (mActivity.getTerminalView().setTerminalCursorBlinkerRate(mActivity.getProperties().getTerminalCursorBlinkRate()))
|
||||
mActivity.getTerminalView().setTerminalCursorBlinkerState(true, true);
|
||||
else
|
||||
Logger.logError(LOG_TAG,"Failed to start cursor blinker");
|
||||
} else {
|
||||
// Disable cursor blinker
|
||||
mActivity.getTerminalView().setTerminalCursorBlinkerState(false, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void shareSessionTranscript() {
|
||||
TerminalSession session = mActivity.getCurrentSession();
|
||||
if (session == null) return;
|
||||
|
||||
String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
|
||||
if (transcriptText == null) return;
|
||||
|
||||
try {
|
||||
// See https://github.com/termux/termux-app/issues/1166.
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
transcriptText = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
|
||||
intent.putExtra(Intent.EXTRA_TEXT, transcriptText);
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, mActivity.getString(R.string.title_share_transcript));
|
||||
mActivity.startActivity(Intent.createChooser(intent, mActivity.getString(R.string.title_share_transcript_with)));
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG,"Failed to get share session transcript of length " + transcriptText.length(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public void showUrlSelection() {
|
||||
TerminalSession session = mActivity.getCurrentSession();
|
||||
if (session == null) return;
|
||||
|
||||
String text = ShellUtils.getTerminalSessionTranscriptText(session, true, true);
|
||||
|
||||
LinkedHashSet<CharSequence> urlSet = DataUtils.extractUrls(text);
|
||||
if (urlSet.isEmpty()) {
|
||||
new AlertDialog.Builder(mActivity).setMessage(R.string.title_select_url_none_found).show();
|
||||
return;
|
||||
}
|
||||
|
||||
final CharSequence[] urls = urlSet.toArray(new CharSequence[0]);
|
||||
Collections.reverse(Arrays.asList(urls)); // Latest first.
|
||||
|
||||
// Click to copy url to clipboard:
|
||||
final AlertDialog dialog = new AlertDialog.Builder(mActivity).setItems(urls, (di, which) -> {
|
||||
String url = (String) urls[which];
|
||||
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(url)));
|
||||
Toast.makeText(mActivity, R.string.msg_select_url_copied_to_clipboard, Toast.LENGTH_LONG).show();
|
||||
}).setTitle(R.string.title_select_url_dialog).create();
|
||||
|
||||
// Long press to open URL:
|
||||
dialog.setOnShowListener(di -> {
|
||||
ListView lv = dialog.getListView(); // this is a ListView with your "buds" in it
|
||||
lv.setOnItemLongClickListener((parent, view, position, id) -> {
|
||||
dialog.dismiss();
|
||||
String url = (String) urls[position];
|
||||
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
try {
|
||||
mActivity.startActivity(i, null);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
// If no applications match, Android displays a system message.
|
||||
mActivity.startActivity(Intent.createChooser(i, null));
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
public void reportIssueFromTranscript() {
|
||||
TerminalSession session = mActivity.getCurrentSession();
|
||||
if (session == null) return;
|
||||
|
||||
final String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
|
||||
if (transcriptText == null) return;
|
||||
|
||||
Logger.showToast(mActivity, mActivity.getString(R.string.msg_generating_report), true);
|
||||
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
String transcriptTextTruncated = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
|
||||
|
||||
StringBuilder reportString = new StringBuilder();
|
||||
|
||||
String title = TermuxConstants.TERMUX_APP_NAME + " Report Issue";
|
||||
|
||||
reportString.append("## Transcript\n");
|
||||
reportString.append("\n").append(MarkdownUtils.getMarkdownCodeForString(transcriptTextTruncated, true));
|
||||
|
||||
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, true));
|
||||
reportString.append("\n\n").append(TermuxUtils.getDeviceInfoMarkdownString(mActivity));
|
||||
|
||||
String termuxAptInfo = TermuxUtils.geAPTInfoMarkdownString(mActivity);
|
||||
if (termuxAptInfo != null)
|
||||
reportString.append("\n\n").append(termuxAptInfo);
|
||||
|
||||
ReportActivity.startReportActivity(mActivity, new ReportInfo(UserAction.REPORT_ISSUE_FROM_TRANSCRIPT, TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY_NAME, title, null, reportString.toString(), "\n\n" + TermuxUtils.getReportIssueMarkdownString(mActivity), false));
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
public void doPaste() {
|
||||
TerminalSession session = mActivity.getCurrentSession();
|
||||
if (session == null) return;
|
||||
if (!session.isRunning()) return;
|
||||
|
||||
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clipData = clipboard.getPrimaryClip();
|
||||
if (clipData == null) return;
|
||||
CharSequence paste = clipData.getItemAt(0).coerceToText(mActivity);
|
||||
if (!TextUtils.isEmpty(paste))
|
||||
session.getEmulator().paste(paste.toString());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.termux.app;
|
||||
package com.termux.app.terminal.io;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
@@ -6,15 +6,15 @@ import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
import android.os.Vibrator;
|
||||
|
||||
public class BellUtil {
|
||||
private static BellUtil instance = null;
|
||||
public class BellHandler {
|
||||
private static BellHandler instance = null;
|
||||
private static final Object lock = new Object();
|
||||
|
||||
public static BellUtil getInstance(Context context) {
|
||||
public static BellHandler getInstance(Context context) {
|
||||
if (instance == null) {
|
||||
synchronized (lock) {
|
||||
if (instance == null) {
|
||||
instance = new BellUtil((Vibrator) context.getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE));
|
||||
instance = new BellHandler((Vibrator) context.getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ public class BellUtil {
|
||||
private long lastBell = 0;
|
||||
private final Runnable bellRunnable;
|
||||
|
||||
private BellUtil(final Vibrator vibrator) {
|
||||
private BellHandler(final Vibrator vibrator) {
|
||||
bellRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -47,7 +47,7 @@ public class BellUtil {
|
||||
if (timeSinceLastBell < 0) {
|
||||
// there is a next bell pending; don't schedule another one
|
||||
} else if (timeSinceLastBell < MIN_PAUSE) {
|
||||
// there was a bell recently, scheudle the next one
|
||||
// there was a bell recently, schedule the next one
|
||||
handler.postDelayed(bellRunnable, MIN_PAUSE - timeSinceLastBell);
|
||||
lastBell = lastBell + MIN_PAUSE;
|
||||
} else {
|
||||
@@ -1,9 +1,11 @@
|
||||
package com.termux.app;
|
||||
package com.termux.app.terminal.io;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.termux.app.TermuxActivity;
|
||||
|
||||
/**
|
||||
* Work around for fullscreen mode in Termux to fix ExtraKeysView not being visible.
|
||||
* This class is derived from:
|
||||
@@ -13,11 +15,11 @@ import android.view.ViewGroup;
|
||||
* For more information, see https://issuetracker.google.com/issues/36911528
|
||||
*/
|
||||
public class FullScreenWorkAround {
|
||||
private View mChildOfContent;
|
||||
private final View mChildOfContent;
|
||||
private int mUsableHeightPrevious;
|
||||
private ViewGroup.LayoutParams mViewGroupLayoutParams;
|
||||
private final ViewGroup.LayoutParams mViewGroupLayoutParams;
|
||||
|
||||
private int mNavBarHeight;
|
||||
private final int mNavBarHeight;
|
||||
|
||||
|
||||
public static void apply(TermuxActivity activity) {
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.termux.app.terminal.io;
|
||||
|
||||
public class KeyboardShortcut {
|
||||
|
||||
public final int codePoint;
|
||||
public final int shortcutAction;
|
||||
|
||||
public KeyboardShortcut(int codePoint, int shortcutAction) {
|
||||
this.codePoint = codePoint;
|
||||
this.shortcutAction = shortcutAction;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.termux.app.terminal.io;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.TermuxActivity;
|
||||
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
public class TerminalToolbarViewPager {
|
||||
|
||||
public static class PageAdapter extends PagerAdapter {
|
||||
|
||||
final TermuxActivity mActivity;
|
||||
String mSavedTextInput;
|
||||
|
||||
public PageAdapter(TermuxActivity activity, String savedTextInput) {
|
||||
this.mActivity = activity;
|
||||
this.mSavedTextInput = savedTextInput;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
|
||||
return view == object;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Object instantiateItem(@NonNull ViewGroup collection, int position) {
|
||||
LayoutInflater inflater = LayoutInflater.from(mActivity);
|
||||
View layout;
|
||||
if (position == 0) {
|
||||
layout = inflater.inflate(R.layout.view_terminal_toolbar_extra_keys, collection, false);
|
||||
ExtraKeysView extraKeysView = (ExtraKeysView) layout;
|
||||
extraKeysView.setTermuxTerminalViewClient(mActivity.getTermuxTerminalViewClient());
|
||||
mActivity.setExtraKeysView(extraKeysView);
|
||||
extraKeysView.reload(mActivity.getProperties().getExtraKeysInfo());
|
||||
|
||||
// apply extra keys fix if enabled in prefs
|
||||
if (mActivity.getProperties().isUsingFullScreen() && mActivity.getProperties().isUsingFullScreenWorkAround()) {
|
||||
FullScreenWorkAround.apply(mActivity);
|
||||
}
|
||||
|
||||
} else {
|
||||
layout = inflater.inflate(R.layout.view_terminal_toolbar_text_input, collection, false);
|
||||
final EditText editText = layout.findViewById(R.id.terminal_toolbar_text_input);
|
||||
|
||||
if (mSavedTextInput != null) {
|
||||
editText.setText(mSavedTextInput);
|
||||
mSavedTextInput = null;
|
||||
}
|
||||
|
||||
editText.setOnEditorActionListener((v, actionId, event) -> {
|
||||
TerminalSession session = mActivity.getCurrentSession();
|
||||
if (session != null) {
|
||||
if (session.isRunning()) {
|
||||
String textToSend = editText.getText().toString();
|
||||
if (textToSend.length() == 0) textToSend = "\r";
|
||||
session.write(textToSend);
|
||||
} else {
|
||||
mActivity.getTermuxTerminalSessionClient().removeFinishedSession(session);
|
||||
}
|
||||
editText.setText("");
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
collection.addView(layout);
|
||||
return layout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(@NonNull ViewGroup collection, int position, @NonNull Object view) {
|
||||
collection.removeView((View) view);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static class OnPageChangeListener extends ViewPager.SimpleOnPageChangeListener {
|
||||
|
||||
final TermuxActivity mActivity;
|
||||
final ViewPager mTerminalToolbarViewPager;
|
||||
|
||||
public OnPageChangeListener(TermuxActivity activity, ViewPager viewPager) {
|
||||
this.mActivity = activity;
|
||||
this.mTerminalToolbarViewPager = viewPager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
if (position == 0) {
|
||||
mActivity.getTerminalView().requestFocus();
|
||||
} else {
|
||||
final EditText editText = mTerminalToolbarViewPager.findViewById(R.id.terminal_toolbar_text_input);
|
||||
if (editText != null) editText.requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.termux.app.terminal.io.extrakeys;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ExtraKeyButton {
|
||||
|
||||
/**
|
||||
* The key that will be sent to the terminal, either a control character
|
||||
* defined in ExtraKeysView.keyCodesForString (LEFT, RIGHT, PGUP...) or
|
||||
* some text.
|
||||
*/
|
||||
private final String key;
|
||||
|
||||
/**
|
||||
* If the key is a macro, i.e. a sequence of keys separated by space.
|
||||
*/
|
||||
private final boolean macro;
|
||||
|
||||
/**
|
||||
* The text that will be shown on the button.
|
||||
*/
|
||||
private final String display;
|
||||
|
||||
/**
|
||||
* The information of the popup (triggered by swipe up).
|
||||
*/
|
||||
@Nullable
|
||||
private ExtraKeyButton popup;
|
||||
|
||||
public ExtraKeyButton(ExtraKeysInfo.CharDisplayMap charDisplayMap, JSONObject config) throws JSONException {
|
||||
this(charDisplayMap, config, null);
|
||||
}
|
||||
|
||||
public ExtraKeyButton(ExtraKeysInfo.CharDisplayMap charDisplayMap, JSONObject config, @Nullable ExtraKeyButton popup) throws JSONException {
|
||||
String keyFromConfig = config.optString("key", null);
|
||||
String macroFromConfig = config.optString("macro", null);
|
||||
String[] keys;
|
||||
if (keyFromConfig != null && macroFromConfig != null) {
|
||||
throw new JSONException("Both key and macro can't be set for the same key");
|
||||
} else if (keyFromConfig != null) {
|
||||
keys = new String[]{keyFromConfig};
|
||||
this.macro = false;
|
||||
} else if (macroFromConfig != null) {
|
||||
keys = macroFromConfig.split(" ");
|
||||
this.macro = true;
|
||||
} else {
|
||||
throw new JSONException("All keys have to specify either key or macro");
|
||||
}
|
||||
|
||||
for (int i = 0; i < keys.length; i++) {
|
||||
keys[i] = ExtraKeysInfo.replaceAlias(keys[i]);
|
||||
}
|
||||
|
||||
this.key = TextUtils.join(" ", keys);
|
||||
|
||||
String displayFromConfig = config.optString("display", null);
|
||||
if (displayFromConfig != null) {
|
||||
this.display = displayFromConfig;
|
||||
} else {
|
||||
this.display = Arrays.stream(keys)
|
||||
.map(key -> charDisplayMap.get(key, key))
|
||||
.collect(Collectors.joining(" "));
|
||||
}
|
||||
|
||||
this.popup = popup;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public boolean isMacro() {
|
||||
return macro;
|
||||
}
|
||||
|
||||
public String getDisplay() {
|
||||
return display;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ExtraKeyButton getPopup() {
|
||||
return popup;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,28 @@
|
||||
package com.termux.app;
|
||||
package com.termux.app.terminal.io.extrakeys;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||
import com.termux.shared.settings.properties.TermuxSharedProperties;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ExtraKeysInfos {
|
||||
public class ExtraKeysInfo {
|
||||
|
||||
/**
|
||||
* Matrix of buttons displayed
|
||||
*/
|
||||
private ExtraKeyButton[][] buttons;
|
||||
private final ExtraKeyButton[][] buttons;
|
||||
|
||||
/**
|
||||
* This corresponds to one of the CharMapDisplay below
|
||||
*/
|
||||
private String style = "default";
|
||||
private String style;
|
||||
|
||||
public ExtraKeysInfos(String propertiesInfo, String style) throws JSONException {
|
||||
public ExtraKeysInfo(String propertiesInfo, String style) throws JSONException {
|
||||
this.style = style;
|
||||
|
||||
// Convert String propertiesInfo to Array of Arrays
|
||||
@@ -50,7 +47,7 @@ public class ExtraKeysInfos {
|
||||
|
||||
ExtraKeyButton button;
|
||||
|
||||
if(! jobject.has("popup")) {
|
||||
if (! jobject.has("popup")) {
|
||||
// no popup
|
||||
button = new ExtraKeyButton(getSelectedCharMap(), jobject);
|
||||
} else {
|
||||
@@ -70,10 +67,10 @@ public class ExtraKeysInfos {
|
||||
*/
|
||||
private static JSONObject normalizeKeyConfig(Object key) throws JSONException {
|
||||
JSONObject jobject;
|
||||
if(key instanceof String) {
|
||||
if (key instanceof String) {
|
||||
jobject = new JSONObject();
|
||||
jobject.put("key", key);
|
||||
} else if(key instanceof JSONObject) {
|
||||
} else if (key instanceof JSONObject) {
|
||||
jobject = (JSONObject) key;
|
||||
} else {
|
||||
throw new JSONException("An key in the extra-key matrix must be a string or an object");
|
||||
@@ -91,7 +88,7 @@ public class ExtraKeysInfos {
|
||||
*/
|
||||
static class CleverMap<K,V> extends HashMap<K,V> {
|
||||
V get(K key, V defaultValue) {
|
||||
if(containsKey(key))
|
||||
if (containsKey(key))
|
||||
return get(key);
|
||||
else
|
||||
return defaultValue;
|
||||
@@ -151,7 +148,7 @@ public class ExtraKeysInfos {
|
||||
put("-", "―"); // U+2015 ― HORIZONTAL BAR
|
||||
}};
|
||||
|
||||
/**
|
||||
/*
|
||||
* Multiple maps are available to quickly change
|
||||
* the style of the keys.
|
||||
*/
|
||||
@@ -245,6 +242,8 @@ public class ExtraKeysInfos {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -258,83 +257,3 @@ public class ExtraKeysInfos {
|
||||
}
|
||||
}
|
||||
|
||||
class ExtraKeyButton {
|
||||
|
||||
/**
|
||||
* The key that will be sent to the terminal, either a control character
|
||||
* defined in ExtraKeysView.keyCodesForString (LEFT, RIGHT, PGUP...) or
|
||||
* some text.
|
||||
*/
|
||||
private String key;
|
||||
|
||||
/**
|
||||
* If the key is a macro, i.e. a sequence of keys separated by space.
|
||||
*/
|
||||
private boolean macro;
|
||||
|
||||
/**
|
||||
* The text that will be shown on the button.
|
||||
*/
|
||||
private String display;
|
||||
|
||||
/**
|
||||
* The information of the popup (triggered by swipe up).
|
||||
*/
|
||||
@Nullable
|
||||
private ExtraKeyButton popup = null;
|
||||
|
||||
public ExtraKeyButton(ExtraKeysInfos.CharDisplayMap charDisplayMap, JSONObject config) throws JSONException {
|
||||
this(charDisplayMap, config, null);
|
||||
}
|
||||
|
||||
public ExtraKeyButton(ExtraKeysInfos.CharDisplayMap charDisplayMap, JSONObject config, ExtraKeyButton popup) throws JSONException {
|
||||
String keyFromConfig = config.optString("key", null);
|
||||
String macroFromConfig = config.optString("macro", null);
|
||||
String[] keys;
|
||||
if (keyFromConfig != null && macroFromConfig != null) {
|
||||
throw new JSONException("Both key and macro can't be set for the same key");
|
||||
} else if (keyFromConfig != null) {
|
||||
keys = new String[]{keyFromConfig};
|
||||
this.macro = false;
|
||||
} else if (macroFromConfig != null) {
|
||||
keys = macroFromConfig.split(" ");
|
||||
this.macro = true;
|
||||
} else {
|
||||
throw new JSONException("All keys have to specify either key or macro");
|
||||
}
|
||||
|
||||
for (int i = 0; i < keys.length; i++) {
|
||||
keys[i] = ExtraKeysInfos.replaceAlias(keys[i]);
|
||||
}
|
||||
|
||||
this.key = TextUtils.join(" ", keys);
|
||||
|
||||
String displayFromConfig = config.optString("display", null);
|
||||
if (displayFromConfig != null) {
|
||||
this.display = displayFromConfig;
|
||||
} else {
|
||||
this.display = Arrays.stream(keys)
|
||||
.map(key -> charDisplayMap.get(key, key))
|
||||
.collect(Collectors.joining(" "));
|
||||
}
|
||||
|
||||
this.popup = popup;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public boolean isMacro() {
|
||||
return macro;
|
||||
}
|
||||
|
||||
public String getDisplay() {
|
||||
return display;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ExtraKeyButton getPopup() {
|
||||
return popup;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.termux.app;
|
||||
package com.termux.app.terminal.io.extrakeys;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -78,16 +80,18 @@ public final class ExtraKeysView extends GridLayout {
|
||||
put("F12", KeyEvent.KEYCODE_F12);
|
||||
}};
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
private void sendKey(View view, String keyName, boolean forceCtrlDown, boolean forceLeftAltDown) {
|
||||
TerminalView terminalView = view.findViewById(R.id.terminal_view);
|
||||
if ("KEYBOARD".equals(keyName)) {
|
||||
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);
|
||||
} else if (keyCodesForString.containsKey(keyName)) {
|
||||
int keyCode = keyCodesForString.get(keyName);
|
||||
Integer keyCode = keyCodesForString.get(keyName);
|
||||
if (keyCode == null) return;
|
||||
int metaState = 0;
|
||||
if (forceCtrlDown) {
|
||||
metaState |= KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON;
|
||||
@@ -172,6 +176,7 @@ public final class ExtraKeysView extends GridLayout {
|
||||
|
||||
private Button createSpecialButton(String buttonKey, boolean needUpdate) {
|
||||
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonKey));
|
||||
if (state == null) return null;
|
||||
state.isOn = true;
|
||||
Button button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||
button.setTextColor(state.isActive ? INTERESTING_COLOR : TEXT_COLOR);
|
||||
@@ -185,8 +190,9 @@ public final class ExtraKeysView extends GridLayout {
|
||||
int width = view.getMeasuredWidth();
|
||||
int height = view.getMeasuredHeight();
|
||||
Button button;
|
||||
if(isSpecialButton(extraButton)) {
|
||||
if (isSpecialButton(extraButton)) {
|
||||
button = createSpecialButton(extraButton.getKey(), false);
|
||||
if (button == null) return;
|
||||
} else {
|
||||
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||
button.setTextColor(TEXT_COLOR);
|
||||
@@ -235,8 +241,8 @@ public final class ExtraKeysView extends GridLayout {
|
||||
* "-_-" will input the string "-_-"
|
||||
*/
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
void reload(ExtraKeysInfos infos) {
|
||||
if(infos == null)
|
||||
public void reload(ExtraKeysInfo infos) {
|
||||
if (infos == null)
|
||||
return;
|
||||
|
||||
for(SpecialButtonState state : specialButtons.values())
|
||||
@@ -254,8 +260,9 @@ public final class ExtraKeysView extends GridLayout {
|
||||
final ExtraKeyButton buttonInfo = buttons[row][col];
|
||||
|
||||
Button button;
|
||||
if(isSpecialButton(buttonInfo)) {
|
||||
if (isSpecialButton(buttonInfo)) {
|
||||
button = createSpecialButton(buttonInfo.getKey(), true);
|
||||
if (button == null) return;
|
||||
} else {
|
||||
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||
}
|
||||
@@ -282,6 +289,7 @@ public final class ExtraKeysView extends GridLayout {
|
||||
View root = getRootView();
|
||||
if (isSpecialButton(buttonInfo)) {
|
||||
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonInfo.getKey()));
|
||||
if (state == null) return;
|
||||
state.setIsActive(!state.isActive);
|
||||
} else {
|
||||
sendKey(root, buttonInfo);
|
||||
@@ -343,6 +351,7 @@ public final class ExtraKeysView extends GridLayout {
|
||||
if (buttonInfo.getPopup() != null) {
|
||||
if (isSpecialButton(buttonInfo.getPopup())) {
|
||||
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonInfo.getPopup().getKey()));
|
||||
if (state == null) return true;
|
||||
state.setIsActive(!state.isActive);
|
||||
} else {
|
||||
sendKey(root, buttonInfo.getPopup());
|
||||
@@ -372,4 +381,8 @@ public final class ExtraKeysView extends GridLayout {
|
||||
}
|
||||
}
|
||||
|
||||
public void setTermuxTerminalViewClient(TermuxTerminalViewClient termuxTerminalViewClient) {
|
||||
this.mTermuxTerminalViewClient = termuxTerminalViewClient;
|
||||
}
|
||||
|
||||
}
|
||||
158
app/src/main/java/com/termux/app/utils/CrashUtils.java
Normal file
158
app/src/main/java/com/termux/app/utils/CrashUtils.java
Normal file
@@ -0,0 +1,158 @@
|
||||
package com.termux.app.utils;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.activities.ReportActivity;
|
||||
import com.termux.shared.notification.NotificationUtils;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.app.models.ReportInfo;
|
||||
import com.termux.app.models.UserAction;
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.settings.preferences.TermuxPreferenceConstants;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
public class CrashUtils {
|
||||
|
||||
private static final String LOG_TAG = "CrashUtils";
|
||||
|
||||
/**
|
||||
* Notify the user of 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
|
||||
* created by {@link com.termux.shared.crash.CrashHandler}.
|
||||
*
|
||||
* If the crash log file exists and is not empty and
|
||||
* {@link TermuxPreferenceConstants.TERMUX_APP#KEY_CRASH_REPORT_NOTIFICATIONS_ENABLED} is
|
||||
* enabled, then a notification will be shown for the crash on the
|
||||
* {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME} channel, otherwise nothing will be done.
|
||||
*
|
||||
* After reading from the crash log file, it will be moved to {@link TermuxConstants#TERMUX_CRASH_LOG_BACKUP_FILE_PATH}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param logTagParam The log tag to use for logging.
|
||||
*/
|
||||
public static void notifyCrash(final Context context, final String logTagParam) {
|
||||
if (context == null) return;
|
||||
|
||||
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
||||
if (preferences == null) return;
|
||||
|
||||
// If user has disabled notifications for crashes
|
||||
if (!preferences.areCrashReportNotificationsEnabled())
|
||||
return;
|
||||
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
String logTag = DataUtils.getDefaultIfNull(logTagParam, LOG_TAG);
|
||||
|
||||
if (!FileUtils.regularFileExists(TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, false))
|
||||
return;
|
||||
|
||||
String errmsg;
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
String reportString = reportStringBuilder.toString();
|
||||
|
||||
if (reportString == null || 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, "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());
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get {@link Notification.Builder} for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID}
|
||||
* and {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param title The title for the notification.
|
||||
* @param notificationText The second line text of the notification.
|
||||
* @param notificationBigText The full text of the notification that may optionally be styled.
|
||||
* @param pendingIntent The {@link PendingIntent} which should be sent when notification is clicked.
|
||||
* @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}.
|
||||
* @return Returns the {@link Notification.Builder}.
|
||||
*/
|
||||
@Nullable
|
||||
public static Notification.Builder getCrashReportsNotificationBuilder(final Context context, final CharSequence title, final CharSequence notificationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) {
|
||||
|
||||
Notification.Builder builder = NotificationUtils.geNotificationBuilder(context,
|
||||
TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_HIGH,
|
||||
title, notificationText, notificationBigText, pendingIntent, notificationMode);
|
||||
|
||||
if (builder == null) return null;
|
||||
|
||||
// Enable timestamp
|
||||
builder.setShowWhen(true);
|
||||
|
||||
// Set notification icon
|
||||
builder.setSmallIcon(R.drawable.ic_error_notification);
|
||||
|
||||
// Set background color for small notification icon
|
||||
builder.setColor(0xFF607D8B);
|
||||
|
||||
// Dismiss on click
|
||||
builder.setAutoCancel(true);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the notification channel for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID} and
|
||||
* {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
*/
|
||||
public static void setupCrashReportsNotificationChannel(final Context context) {
|
||||
NotificationUtils.setupNotificationChannel(context, TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID,
|
||||
TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
|
||||
}
|
||||
|
||||
}
|
||||
332
app/src/main/java/com/termux/app/utils/PluginUtils.java
Normal file
332
app/src/main/java/com/termux/app/utils/PluginUtils.java
Normal file
@@ -0,0 +1,332 @@
|
||||
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.notification.NotificationUtils;
|
||||
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.ExecutionCommand;
|
||||
import com.termux.app.models.UserAction;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
|
||||
public class PluginUtils {
|
||||
|
||||
/** 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";
|
||||
|
||||
/**
|
||||
* Process {@link ExecutionCommand} result.
|
||||
*
|
||||
* The ExecutionCommand currentState must be greater or equal to
|
||||
* {@link ExecutionCommand.ExecutionState#EXECUTED}.
|
||||
* If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and
|
||||
* {@link ExecutionCommand#pluginPendingIntent} is not {@code null}, then the result of commands
|
||||
* are sent back to the {@link PendingIntent} creator.
|
||||
*
|
||||
* @param context The {@link Context} that will be used to send result intent to the {@link PendingIntent} creator.
|
||||
* @param logTag The log tag to use for logging.
|
||||
* @param executionCommand The {@link ExecutionCommand} to process.
|
||||
*/
|
||||
public static void processPluginExecutionCommandResult(final Context context, String logTag, final ExecutionCommand executionCommand) {
|
||||
if (executionCommand == null) return;
|
||||
|
||||
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||
|
||||
if (!executionCommand.hasExecuted()) {
|
||||
Logger.logWarn(logTag, "Ignoring call to processPluginExecutionCommandResult() since the execution command state is not higher than the ExecutionState.EXECUTED");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.logDebug(LOG_TAG, executionCommand.toString());
|
||||
|
||||
boolean result = true;
|
||||
|
||||
// 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;
|
||||
|
||||
//Combine errmsg and stacktraces
|
||||
if (executionCommand.isStateFailed()) {
|
||||
errmsg = Logger.getMessageAndStackTracesString(executionCommand.errmsg, executionCommand.throwableList);
|
||||
}
|
||||
|
||||
// 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)
|
||||
executionCommand.setState(ExecutionCommand.ExecutionState.SUCCESS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Otherwise if the {@link TERMUX_APP#KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED} is
|
||||
* enabled, then a flash and a notification will be shown for the error as well
|
||||
* on the {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME} channel instead of just logging
|
||||
* the error.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param logTag The log tag to use for logging.
|
||||
* @param executionCommand The {@link ExecutionCommand} that failed.
|
||||
* @param forceNotification If set to {@code true}, then a flash and notification will be shown
|
||||
* regardless of if pending intent is {@code null} or
|
||||
* {@link TERMUX_APP#KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED}
|
||||
* is {@code false}.
|
||||
*/
|
||||
public static void processPluginExecutionCommandError(final Context context, String logTag, final ExecutionCommand executionCommand, boolean forceNotification) {
|
||||
if (context == null || executionCommand == null) return;
|
||||
|
||||
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||
|
||||
if (!executionCommand.isStateFailed()) {
|
||||
Logger.logWarn(logTag, "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);
|
||||
|
||||
|
||||
// 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;
|
||||
|
||||
//Combine errmsg and stacktraces
|
||||
if (executionCommand.isStateFailed()) {
|
||||
errmsg = Logger.getMessageAndStackTracesString(executionCommand.errmsg, executionCommand.throwableList);
|
||||
}
|
||||
|
||||
sendPluginExecutionCommandResultPendingIntent(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.stdout, executionCommand.stderr, executionCommand.exitCode, executionCommand.errCode, errmsg, executionCommand.pluginPendingIntent);
|
||||
|
||||
// No need to show notifications if a pending intent was sent, let the caller handle the result himself
|
||||
if (!forceNotification) return;
|
||||
}
|
||||
|
||||
|
||||
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
||||
if (preferences == null) return;
|
||||
|
||||
// If user has disabled notifications for plugin, then just return
|
||||
if (!preferences.arePluginErrorNotificationsEnabled() && !forceNotification)
|
||||
return;
|
||||
|
||||
// Flash the errmsg
|
||||
Logger.showToast(context, executionCommand.errmsg, true);
|
||||
|
||||
// Send a notification to show the errmsg which when clicked will open the {@link ReportActivity}
|
||||
// to show the details of the error
|
||||
String title = TermuxConstants.TERMUX_APP_NAME + " Plugin Execution Command Error";
|
||||
|
||||
StringBuilder reportString = new StringBuilder();
|
||||
|
||||
reportString.append(ExecutionCommand.getExecutionCommandMarkdownString(executionCommand));
|
||||
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, true));
|
||||
reportString.append("\n\n").append(TermuxUtils.getDeviceInfoMarkdownString(context));
|
||||
|
||||
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.PLUGIN_EXECUTION_COMMAND, logTag, title, null, reportString.toString(), null,true));
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
// 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;
|
||||
|
||||
// Build the notification
|
||||
Notification.Builder builder = getPluginCommandErrorsNotificationBuilder(context, title, notificationText, notificationText, 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());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param title The title for the notification.
|
||||
* @param notificationText The second line text of the notification.
|
||||
* @param notificationBigText The full text of the notification that may optionally be styled.
|
||||
* @param pendingIntent The {@link PendingIntent} which should be sent when notification is clicked.
|
||||
* @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}.
|
||||
* @return Returns the {@link Notification.Builder}.
|
||||
*/
|
||||
@Nullable
|
||||
public static Notification.Builder getPluginCommandErrorsNotificationBuilder(final Context context, final CharSequence title, final CharSequence notificationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) {
|
||||
|
||||
Notification.Builder builder = NotificationUtils.geNotificationBuilder(context,
|
||||
TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_HIGH,
|
||||
title, notificationText, notificationBigText, pendingIntent, notificationMode);
|
||||
|
||||
if (builder == null) return null;
|
||||
|
||||
// Enable timestamp
|
||||
builder.setShowWhen(true);
|
||||
|
||||
// Set notification icon
|
||||
builder.setSmallIcon(R.drawable.ic_error_notification);
|
||||
|
||||
// Set background color for small notification icon
|
||||
builder.setColor(0xFF607D8B);
|
||||
|
||||
// Dismiss on click
|
||||
builder.setAutoCancel(true);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the notification channel for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID} and
|
||||
* {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
*/
|
||||
public static void setupPluginCommandErrorsNotificationChannel(final Context context) {
|
||||
NotificationUtils.setupNotificationChannel(context, TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID,
|
||||
TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Check if {@link TermuxConstants#PROP_ALLOW_EXTERNAL_APPS} property is not set to "true".
|
||||
*
|
||||
* @param context The {@link Context} to get error string.
|
||||
* @return Returns the {@code errmsg} if policy is violated, otherwise {@code null}.
|
||||
*/
|
||||
public static String checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(final Context context) {
|
||||
String errmsg = null;
|
||||
if (!SharedProperties.isPropertyValueTrue(context, TermuxPropertyConstants.getTermuxPropertiesFile(), TermuxConstants.PROP_ALLOW_EXTERNAL_APPS, true)) {
|
||||
errmsg = context.getString(R.string.error_run_command_service_allow_external_apps_ungranted);
|
||||
}
|
||||
|
||||
return errmsg;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import android.provider.DocumentsProvider;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.TermuxService;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
@@ -22,7 +22,7 @@ import java.util.LinkedList;
|
||||
|
||||
/**
|
||||
* A document provider for the Storage Access Framework which exposes the files in the
|
||||
* $HOME/ folder to other apps.
|
||||
* $HOME/ directory to other apps.
|
||||
* <p/>
|
||||
* Note that this replaces providing an activity matching the ACTION_GET_CONTENT intent:
|
||||
* <p/>
|
||||
@@ -35,7 +35,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
||||
|
||||
private static final String ALL_MIME_TYPES = "*/*";
|
||||
|
||||
private static final File BASE_DIR = new File(TermuxService.HOME_PATH);
|
||||
private static final File BASE_DIR = TermuxConstants.TERMUX_HOME_DIR;
|
||||
|
||||
|
||||
// The default columns to return information about a root if no specific
|
||||
@@ -63,9 +63,9 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
||||
};
|
||||
|
||||
@Override
|
||||
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
|
||||
public Cursor queryRoots(String[] projection) {
|
||||
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION);
|
||||
@SuppressWarnings("ConstantConditions") final String applicationName = getContext().getString(R.string.application_name);
|
||||
final String applicationName = getContext().getString(R.string.application_name);
|
||||
|
||||
final MatrixCursor.RowBuilder row = result.newRow();
|
||||
row.add(Root.COLUMN_ROOT_ID, getDocIdForFile(BASE_DIR));
|
||||
@@ -167,11 +167,11 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
||||
final int MAX_SEARCH_RESULTS = 50;
|
||||
while (!pending.isEmpty() && result.getCount() < MAX_SEARCH_RESULTS) {
|
||||
final File file = pending.removeFirst();
|
||||
// Avoid folders outside the $HOME folders linked in to symlinks (to avoid e.g. search
|
||||
// Avoid directories outside the $HOME directory linked with symlinks (to avoid e.g. search
|
||||
// through the whole SD card).
|
||||
boolean isInsideHome;
|
||||
try {
|
||||
isInsideHome = file.getCanonicalPath().startsWith(TermuxService.HOME_PATH);
|
||||
isInsideHome = file.getCanonicalPath().startsWith(TermuxConstants.TERMUX_HOME_DIR_PATH);
|
||||
} catch (IOException e) {
|
||||
isInsideHome = true;
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@ import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.util.Log;
|
||||
import android.util.Patterns;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.app.DialogUtils;
|
||||
import com.termux.shared.interact.DialogUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||
import com.termux.app.TermuxService;
|
||||
import com.termux.shared.logger.Logger;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
@@ -25,9 +27,9 @@ import java.util.regex.Pattern;
|
||||
|
||||
public class TermuxFileReceiverActivity extends Activity {
|
||||
|
||||
static final String TERMUX_RECEIVEDIR = TermuxService.FILES_PATH + "/home/downloads";
|
||||
static final String EDITOR_PROGRAM = TermuxService.HOME_PATH + "/bin/termux-file-editor";
|
||||
static final String URL_OPENER_PROGRAM = TermuxService.HOME_PATH + "/bin/termux-url-opener";
|
||||
static final String TERMUX_RECEIVEDIR = TermuxConstants.TERMUX_FILES_DIR_PATH + "/home/downloads";
|
||||
static final String EDITOR_PROGRAM = TermuxConstants.TERMUX_HOME_DIR_PATH + "/bin/termux-file-editor";
|
||||
static final String URL_OPENER_PROGRAM = TermuxConstants.TERMUX_HOME_DIR_PATH + "/bin/termux-url-opener";
|
||||
|
||||
/**
|
||||
* If the activity should be finished when the name input dialog is dismissed. This is disabled
|
||||
@@ -37,6 +39,8 @@ public class TermuxFileReceiverActivity extends Activity {
|
||||
*/
|
||||
boolean mFinishOnDismissNameDialog = true;
|
||||
|
||||
private static final String LOG_TAG = "TermuxFileReceiverActivity";
|
||||
|
||||
static boolean isSharedTextAnUrl(String sharedText) {
|
||||
return Patterns.WEB_URL.matcher(sharedText).matches()
|
||||
|| Pattern.matches("magnet:\\?xt=urn:btih:.*?", sharedText);
|
||||
@@ -109,12 +113,12 @@ public class TermuxFileReceiverActivity extends Activity {
|
||||
promptNameAndSave(in, attachmentFileName);
|
||||
} catch (Exception e) {
|
||||
showErrorDialogAndQuit("Unable to handle shared content:\n\n" + e.getMessage());
|
||||
Log.e("termux", "handleContentUri(uri=" + uri + ") failed", e);
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "handleContentUri(uri=" + uri + ") failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
void promptNameAndSave(final InputStream in, final String attachmentFileName) {
|
||||
DialogUtils.textInput(this, R.string.file_received_title, attachmentFileName, R.string.file_received_edit_button, text -> {
|
||||
DialogUtils.textInput(this, R.string.title_file_received, attachmentFileName, R.string.action_file_received_edit, text -> {
|
||||
File outFile = saveStreamWithName(in, text);
|
||||
if (outFile == null) return;
|
||||
|
||||
@@ -131,17 +135,17 @@ public class TermuxFileReceiverActivity extends Activity {
|
||||
|
||||
final Uri scriptUri = new Uri.Builder().scheme("file").path(EDITOR_PROGRAM).build();
|
||||
|
||||
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE, scriptUri);
|
||||
Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, scriptUri);
|
||||
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
||||
executeIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, new String[]{outFile.getAbsolutePath()});
|
||||
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, new String[]{outFile.getAbsolutePath()});
|
||||
startService(executeIntent);
|
||||
finish();
|
||||
},
|
||||
R.string.file_received_open_folder_button, text -> {
|
||||
R.string.action_file_received_open_directory, text -> {
|
||||
if (saveStreamWithName(in, text) == null) return;
|
||||
|
||||
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE);
|
||||
executeIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, TERMUX_RECEIVEDIR);
|
||||
Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE);
|
||||
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, TERMUX_RECEIVEDIR);
|
||||
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
||||
startService(executeIntent);
|
||||
finish();
|
||||
@@ -169,7 +173,7 @@ public class TermuxFileReceiverActivity extends Activity {
|
||||
return outFile;
|
||||
} catch (IOException e) {
|
||||
showErrorDialogAndQuit("Error saving file:\n\n" + e);
|
||||
Log.e("termux", "Error saving file", e);
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Error saving file", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -188,9 +192,9 @@ public class TermuxFileReceiverActivity extends Activity {
|
||||
|
||||
final Uri urlOpenerProgramUri = new Uri.Builder().scheme("file").path(URL_OPENER_PROGRAM).build();
|
||||
|
||||
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE, urlOpenerProgramUri);
|
||||
Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, urlOpenerProgramUri);
|
||||
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
||||
executeIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, new String[]{url});
|
||||
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, new String[]{url});
|
||||
startService(executeIntent);
|
||||
finish();
|
||||
}
|
||||
|
||||
5
app/src/main/res/drawable/ic_copy.xml
Normal file
5
app/src/main/res/drawable/ic_copy.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/>
|
||||
</vector>
|
||||
37
app/src/main/res/drawable/ic_error_notification.xml
Normal file
37
app/src/main/res/drawable/ic_error_notification.xml
Normal file
@@ -0,0 +1,37 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<!--
|
||||
Updated notification icon compliant with system icons guidelines
|
||||
https://material.io/design/iconography/system-icons.html
|
||||
-->
|
||||
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h24v24h-24z"/>
|
||||
|
||||
<path
|
||||
android:pathData="M5,4H2L8,12L2,20H5L11,12L5,4Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
|
||||
<path
|
||||
android:pathData="M19.59,14
|
||||
l-2.09,2.09
|
||||
L15.41,14
|
||||
L14,15.41
|
||||
l2.09,2.09
|
||||
L14,19.59
|
||||
L15.41,21
|
||||
l2.09,-2.08
|
||||
L19.59,21
|
||||
L21,19.59
|
||||
l-2.08,-2.09
|
||||
L21,15.41
|
||||
L19.59,14
|
||||
z"
|
||||
android:fillColor="#ffffff"/>
|
||||
|
||||
</group>
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_settings.xml
Normal file
5
app/src/main/res/drawable/ic_settings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FF000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_share.xml
Normal file
5
app/src/main/res/drawable/ic_share.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
|
||||
</vector>
|
||||
21
app/src/main/res/layout/activity_report.xml
Normal file
21
app/src/main/res/layout/activity_report.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include
|
||||
layout="@layout/partial_toolbar"
|
||||
android:id="@+id/partial_toolbar"/>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:overScrollMode="never"
|
||||
android:paddingTop="@dimen/content_padding"
|
||||
android:paddingBottom="36dip" />
|
||||
|
||||
</LinearLayout>
|
||||
9
app/src/main/res/layout/activity_settings.xml
Normal file
9
app/src/main/res/layout/activity_settings.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</LinearLayout>
|
||||
@@ -8,7 +8,7 @@
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_above="@+id/viewpager"
|
||||
android:layout_above="@+id/terminal_toolbar_view_pager"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.termux.view.TerminalView
|
||||
@@ -36,7 +36,7 @@
|
||||
android:orientation="vertical">
|
||||
|
||||
<ListView
|
||||
android:id="@+id/left_drawer_list"
|
||||
android:id="@+id/terminal_sessions_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_gravity="top"
|
||||
@@ -56,7 +56,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/toggle_soft_keyboard" />
|
||||
android:text="@string/action_toggle_soft_keyboard" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/new_session_button"
|
||||
@@ -64,14 +64,14 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/new_session" />
|
||||
android:text="@string/action_new_session" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
android:id="@+id/viewpager"
|
||||
android:id="@+id/terminal_toolbar_view_pager"
|
||||
android:visibility="gone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="37.5dp"
|
||||
@@ -1,9 +1,9 @@
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/row_line"
|
||||
android:id="@+id/session_title"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="?android:attr/listPreferredItemHeight"
|
||||
android:background="@drawable/selected_session_background"
|
||||
android:background="@drawable/session_background_selected"
|
||||
android:ellipsize="marquee"
|
||||
android:gravity="start|center_vertical"
|
||||
android:padding="6dip"
|
||||
android:textSize="14sp" />
|
||||
android:textSize="14sp" />
|
||||
26
app/src/main/res/layout/markdown_adapter_node_code_block.xml
Normal file
26
app/src/main/res/layout/markdown_adapter_node_code_block.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:fillViewport="true"
|
||||
android:paddingLeft="16dip"
|
||||
android:paddingRight="16dip"
|
||||
android:scrollbarStyle="outsideInset">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/code_text_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/background_markdown_code_block"
|
||||
android:fontFamily="monospace"
|
||||
android:lineSpacingExtra="2dip"
|
||||
android:paddingLeft="16dip"
|
||||
android:paddingTop="8dip"
|
||||
android:paddingRight="16dip"
|
||||
android:paddingBottom="8dip"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textSize="12sp" />
|
||||
|
||||
</HorizontalScrollView>
|
||||
15
app/src/main/res/layout/markdown_adapter_node_default.xml
Normal file
15
app/src/main/res/layout/markdown_adapter_node_default.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/default_text_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="16dip"
|
||||
android:layout_marginRight="16dip"
|
||||
android:breakStrategy="simple"
|
||||
android:hyphenationFrequency="none"
|
||||
android:lineSpacingExtra="2dip"
|
||||
android:paddingTop="8dip"
|
||||
android:paddingBottom="8dip"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textColor="#000"
|
||||
android:textSize="12sp" />
|
||||
22
app/src/main/res/layout/partial_toolbar.xml
Normal file
22
app/src/main/res/layout/partial_toolbar.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/toolbar_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimaryDark"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="?attr/actionBarSize"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
|
||||
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
|
||||
app:titleTextAppearance="@style/Toolbar.Title">
|
||||
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
||||
</LinearLayout>
|
||||
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>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.termux.app.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/extra_keys"
|
||||
<com.termux.app.terminal.io.extrakeys.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/terminal_toolbar_extra_keys"
|
||||
style="?android:attr/buttonBarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<EditText xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/text_input"
|
||||
android:id="@+id/terminal_toolbar_text_input"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:imeOptions="actionSend|flagNoFullscreen"
|
||||
15
app/src/main/res/menu/menu_report.xml
Normal file
15
app/src/main/res/menu/menu_report.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_item_share_report"
|
||||
android:icon="@drawable/ic_share"
|
||||
android:title="@string/action_share"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/menu_item_copy_report"
|
||||
android:icon="@drawable/ic_copy"
|
||||
android:title="@string/action_copy"
|
||||
app:showAsAction="never" />
|
||||
</menu>
|
||||
3
app/src/main/res/values/colors.xml
Normal file
3
app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
</resources>
|
||||
10
app/src/main/res/values/dimens.xml
Normal file
10
app/src/main/res/values/dimens.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Default screen margins, per the Android Design guidelines. -->
|
||||
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||
<dimen name="activity_vertical_margin">16dp</dimen>
|
||||
|
||||
<dimen name="content_padding">8dip</dimen>
|
||||
<dimen name="content_padding_double">16dip</dimen>
|
||||
<dimen name="content_padding_half">4dip</dimen>
|
||||
</resources>
|
||||
@@ -1,54 +1,186 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="application_name">Termux</string>
|
||||
<string name="shared_user_label">Termux user</string>
|
||||
<string name="run_command_permission_label">Run commands in Termux environment</string>
|
||||
<string name="run_command_permission_description">execute arbitrary commands within Termux environment</string>
|
||||
<string name="new_session">New session</string>
|
||||
<string name="new_session_failsafe">Failsafe</string>
|
||||
<string name="toggle_soft_keyboard">Keyboard</string>
|
||||
<string name="reset_terminal">Reset</string>
|
||||
<string name="style_terminal">Style</string>
|
||||
<string name="share_transcript_title">Terminal transcript</string>
|
||||
<string name="help">Help</string>
|
||||
<string name="toggle_keep_screen_on">Keep screen on</string>
|
||||
<string name="autofill_password">Autofill password</string>
|
||||
|
||||
<string name="bootstrap_installer_body">Installing…</string>
|
||||
<string name="bootstrap_error_title">Unable to install</string>
|
||||
<string name="bootstrap_error_body">Termux was unable to install the bootstrap packages.</string>
|
||||
<!DOCTYPE resources [
|
||||
<!ENTITY TERMUX_PACKAGE_NAME "com.termux">
|
||||
<!ENTITY TERMUX_APP_NAME "Termux">
|
||||
<!ENTITY TERMUX_API_APP_NAME "Termux:API">
|
||||
<!ENTITY TERMUX_BOOT_APP_NAME "Termux:Boot">
|
||||
<!ENTITY TERMUX_FLOAT_APP_NAME "Termux:Float">
|
||||
<!ENTITY TERMUX_STYLING_APP_NAME "Termux:Styling">
|
||||
<!ENTITY TERMUX_TASKER_APP_NAME "Termux:Tasker">
|
||||
<!ENTITY TERMUX_WIDGET_APP_NAME "Termux:Widget">
|
||||
<!ENTITY TERMUX_PROPERTIES_PRIMARY_PATH_SHORT "~/.termux/termux.properties">
|
||||
]>
|
||||
|
||||
<resources>
|
||||
<string name="application_name">&TERMUX_APP_NAME;</string>
|
||||
<string name="shared_user_label">&TERMUX_APP_NAME; user</string>
|
||||
|
||||
|
||||
|
||||
<!-- Termux RUN_COMMAND permission -->
|
||||
<string name="permission_run_command_label">Run commands in &TERMUX_APP_NAME; environment</string>
|
||||
<string name="permission_run_command_description">execute arbitrary commands within &TERMUX_APP_NAME;
|
||||
environment</string>
|
||||
|
||||
|
||||
|
||||
<!-- Termux Bootstrap Packages Installation -->
|
||||
<string name="bootstrap_installer_body">Installing bootstrap packages…</string>
|
||||
<string name="bootstrap_error_title">Unable to install bootstrap</string>
|
||||
<string name="bootstrap_error_body">&TERMUX_APP_NAME; was unable to install the bootstrap packages.</string>
|
||||
<string name="bootstrap_error_abort">Abort</string>
|
||||
<string name="bootstrap_error_try_again">Try again</string>
|
||||
<string name="bootstrap_error_not_primary_user_message">Termux can only be installed on the primary user account.</string>
|
||||
<string name="bootstrap_error_not_primary_user_message">&TERMUX_APP_NAME; can only be run as the primary user.\nBootstrap binaries compiled for &TERMUX_APP_NAME; have hardcoded $PREFIX path and cannot be installed under any path other than \"%1$s\".</string>
|
||||
|
||||
<string name="max_terminals_reached_title">Max terminals reached</string>
|
||||
<string name="max_terminals_reached_message">Close down existing ones before creating new.</string>
|
||||
|
||||
<string name="reset_toast_notification">Terminal reset.</string>
|
||||
|
||||
<string name="select_url">Select URL</string>
|
||||
<string name="select_url_dialog_title">Click URL to copy or long press to open</string>
|
||||
<string name="select_all_and_share">Share transcript</string>
|
||||
<string name="select_url_no_found">No URL found in the terminal.</string>
|
||||
<string name="select_url_copied_to_clipboard">URL copied to clipboard</string>
|
||||
<string name="share_transcript_chooser_title">Send text to:</string>
|
||||
<!-- Terminal Sidebar and Shortcuts -->
|
||||
<string name="action_new_session">New session</string>
|
||||
<string name="action_new_session_failsafe">Failsafe</string>
|
||||
<string name="title_max_terminals_reached">Max terminals reached</string>
|
||||
<string name="msg_max_terminals_reached">Close down existing ones before creating new.</string>
|
||||
|
||||
<string name="kill_process">Kill process (%d)</string>
|
||||
<string name="confirm_kill_process">Really kill this session?</string>
|
||||
<string name="title_rename_session">Set session name</string>
|
||||
<string name="action_rename_session_confirm">Set</string>
|
||||
<string name="title_create_named_session">New named session</string>
|
||||
<string name="action_create_named_session_confirm">Create</string>
|
||||
|
||||
<string name="session_rename_title">Set session name</string>
|
||||
<string name="session_rename_positive_button">Set</string>
|
||||
<string name="session_new_named_title">New named session</string>
|
||||
<string name="session_new_named_positive_button">Create</string>
|
||||
<string name="action_toggle_soft_keyboard">Keyboard</string>
|
||||
|
||||
<string name="styling_not_installed">The Termux:Style add-on is not installed.</string>
|
||||
<string name="styling_install">Install</string>
|
||||
<string name="msg_enabling_terminal_toolbar">Enabling Terminal Toolbar</string>
|
||||
<string name="msg_disabling_terminal_toolbar">Disabling Terminal Toolbar</string>
|
||||
|
||||
|
||||
|
||||
<!-- Terminal Popup -->
|
||||
<string name="action_select_url">Select URL</string>
|
||||
<string name="title_select_url_dialog">Click URL to copy or long press to open</string>
|
||||
<string name="title_select_url_none_found">No URL found in the terminal.</string>
|
||||
<string name="msg_select_url_copied_to_clipboard">URL copied to clipboard</string>
|
||||
|
||||
<string name="action_share_transcript">Share transcript</string>
|
||||
<string name="title_share_transcript">Terminal transcript</string>
|
||||
<string name="title_share_transcript_with">Send transcript to:</string>
|
||||
|
||||
<string name="action_autofill_password">Autofill password</string>
|
||||
|
||||
<string name="action_reset_terminal">Reset</string>
|
||||
<string name="msg_terminal_reset">Terminal reset</string>
|
||||
|
||||
<string name="action_kill_process">Kill process (%d)</string>
|
||||
<string name="title_confirm_kill_process">Really kill this session?</string>
|
||||
|
||||
<string name="action_style_terminal">Style</string>
|
||||
<string name="action_toggle_keep_screen_on">Keep screen on</string>
|
||||
<string name="action_open_help">Help</string>
|
||||
<string name="action_open_settings">Settings</string>
|
||||
|
||||
<string name="action_report_issue">Report Issue</string>
|
||||
<string name="msg_generating_report">Generating Report</string>
|
||||
|
||||
<string name="error_styling_not_installed">The &TERMUX_STYLING_APP_NAME; Plugin App is not installed.</string>
|
||||
<string name="action_styling_install">Install</string>
|
||||
|
||||
|
||||
|
||||
<!-- Termux Notifications -->
|
||||
<string name="notification_action_exit">Exit</string>
|
||||
<string name="notification_action_wake_lock">Acquire wakelock</string>
|
||||
<string name="notification_action_wake_unlock">Release wakelock</string>
|
||||
|
||||
<string name="file_received_title">Save file in ~/downloads/</string>
|
||||
<string name="file_received_edit_button">Edit</string>
|
||||
<string name="file_received_open_folder_button">Open folder</string>
|
||||
|
||||
|
||||
<!-- Termux RunCommandService -->
|
||||
<string name="error_run_command_service_invalid_intent_action">Invalid intent action to RunCommandService: `%1$s`</string>
|
||||
<string name="error_run_command_service_mandatory_extra_missing">Mandatory extra missing to RunCommandService: \"%1$s\"</string>
|
||||
<string name="error_run_command_service_allow_external_apps_ungranted">RunCommandService require `allow-external-apps` property to be set to `true` in `&TERMUX_PROPERTIES_PRIMARY_PATH_SHORT;` file.</string>
|
||||
<string name="error_run_command_service_api_help">Visit %1$s for more info on RUN_COMMAND Intent usage.</string>
|
||||
|
||||
|
||||
|
||||
<!-- Termux Execution Commands -->
|
||||
<string name="msg_executable_absolute_path">Executable Absolute Path: \"%1$s\"</string>
|
||||
<string name="msg_working_directory_absolute_path">Working Directory Absolute Path: \"%1$s\"</string>
|
||||
|
||||
|
||||
|
||||
<!-- Termux Report And ShareUtils -->
|
||||
<string name="action_copy">Copy</string>
|
||||
<string name="action_share">Share</string>
|
||||
|
||||
<string name="title_share_with">Share With</string>
|
||||
<string name="title_report_text">Report Text</string>
|
||||
|
||||
|
||||
|
||||
<!-- Termux File Receiver -->
|
||||
<string name="title_file_received">Save file in ~/downloads/</string>
|
||||
<string name="action_file_received_edit">Edit</string>
|
||||
<string name="action_file_received_open_directory">Open directory</string>
|
||||
|
||||
|
||||
|
||||
<!-- Termux Settings -->
|
||||
<string name="title_activity_termux_settings">&TERMUX_APP_NAME; Settings</string>
|
||||
|
||||
<!-- Termux App Preferences -->
|
||||
<string name="termux_preferences_title">&TERMUX_APP_NAME;</string>
|
||||
<string name="termux_preferences_summary">Preferences for &TERMUX_APP_NAME; app</string>
|
||||
|
||||
<!-- Debugging Preferences -->
|
||||
<string name="termux_debugging_preferences_title">Debugging</string>
|
||||
<string name="termux_debugging_preferences_summary">Preferences for debugging</string>
|
||||
|
||||
<!-- Logging Category -->
|
||||
<string name="termux_logging_header">Logging</string>
|
||||
|
||||
<!-- Log Level -->
|
||||
<string name="termux_log_level_title">Log Level</string>
|
||||
|
||||
<!-- Terminal View Key Logging -->
|
||||
<string name="termux_terminal_view_key_logging_enabled_title">Terminal View Key Logging</string>
|
||||
<string name="termux_terminal_view_key_logging_enabled_off">Logs will not have entries for terminal view keys. (Default)</string>
|
||||
<string name="termux_terminal_view_key_logging_enabled_on">Logcat logs will have entries for terminal view keys. These are very verbose and should be disabled under normal circumstances or will cause performance issues.</string>
|
||||
|
||||
<!-- Plugin Error Notifications -->
|
||||
<string name="termux_plugin_error_notifications_enabled_title">Plugin Error Notifications</string>
|
||||
<string name="termux_plugin_error_notifications_enabled_off">Disable flashes and notifications for plugin errors.</string>
|
||||
<string name="termux_plugin_error_notifications_enabled_on">Show flashes and notifications for plugin errors. (Default)</string>
|
||||
|
||||
<!-- Crash Report Notifications -->
|
||||
<string name="termux_crash_report_notifications_enabled_title">Crash Report Notifications</string>
|
||||
<string name="termux_crash_report_notifications_enabled_off">Disable notifications for crash reports.</string>
|
||||
<string name="termux_crash_report_notifications_enabled_on">Show notifications for crash reports. (Default)</string>
|
||||
|
||||
|
||||
<!-- Terminal IO Preferences -->
|
||||
<string name="termux_terminal_io_preferences_title">Terminal I/O</string>
|
||||
<string name="termux_terminal_io_preferences_summary">Preferences for terminal I/O</string>
|
||||
|
||||
<!-- Keyboard Category -->
|
||||
<string name="termux_keyboard_header">Keyboard</string>
|
||||
|
||||
<!-- Soft Keyboard -->
|
||||
<string name="termux_soft_keyboard_enabled_title">Soft Keyboard Enabled</string>
|
||||
<string name="termux_soft_keyboard_enabled_off">Soft keyboard will be disabled.</string>
|
||||
<string name="termux_soft_keyboard_enabled_on">Soft keyboard will be enabled. (Default)</string>
|
||||
|
||||
<!-- Soft Keyboard Only If No Hardware-->
|
||||
<string name="termux_soft_keyboard_enabled_only_if_no_hardware_title">Soft Keyboard Only If No Hardware</string>
|
||||
<string name="termux_soft_keyboard_enabled_only_if_no_hardware_off">Soft keyboard will be enabled even if hardware keyboard is connected. (Default)</string>
|
||||
<string name="termux_soft_keyboard_enabled_only_if_no_hardware_on">Soft keyboard will be enabled only if no hardware keyboard is connected.</string>
|
||||
|
||||
|
||||
<!-- Termux Tasker App Preferences -->
|
||||
<string name="termux_tasker_preferences_title">&TERMUX_TASKER_APP_NAME;</string>
|
||||
<string name="termux_tasker_preferences_summary">Preferences for &TERMUX_TASKER_APP_NAME; app</string>
|
||||
|
||||
|
||||
<!-- About Preference -->
|
||||
<string name="about_preference_title">About</string>
|
||||
|
||||
<!-- Donate Preference -->
|
||||
<string name="donate_preference_title">Donate</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -21,10 +21,6 @@
|
||||
<item name="android:windowAllowEnterTransitionOverlap">true</item>
|
||||
</style>
|
||||
|
||||
<style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert">
|
||||
<!-- Seen in buttons on alert dialog: -->
|
||||
<item name="android:colorAccent">#212121</item>
|
||||
</style>
|
||||
|
||||
<!-- See https://developer.android.com/training/material/theme.html for how to customize the Material theme. -->
|
||||
<!-- NOTE: Cannot use "Light." since it hides the terminal scrollbar on the default black background. -->
|
||||
@@ -46,4 +42,20 @@
|
||||
<item name="android:windowAllowReturnTransitionOverlap">true</item>
|
||||
<item name="android:windowAllowEnterTransitionOverlap">true</item>
|
||||
</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>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
||||
28
app/src/main/res/xml/root_preferences.xml
Normal file
28
app/src/main/res/xml/root_preferences.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<Preference
|
||||
app:key="termux"
|
||||
app:title="@string/termux_preferences_title"
|
||||
app:summary="@string/termux_preferences_summary"
|
||||
app:fragment="com.termux.app.fragments.settings.TermuxPreferencesFragment"/>
|
||||
|
||||
<Preference
|
||||
app:key="termux_tasker"
|
||||
app:title="@string/termux_tasker_preferences_title"
|
||||
app:summary="@string/termux_tasker_preferences_summary"
|
||||
app:isPreferenceVisible="false"
|
||||
app:fragment="com.termux.app.fragments.settings.TermuxTaskerPreferencesFragment"/>
|
||||
|
||||
<Preference
|
||||
app:key="about"
|
||||
app:title="@string/about_preference_title"
|
||||
app:persistent="false"/>
|
||||
<!-- app:layout="@layout/preference_markdown_text" -->
|
||||
|
||||
<Preference
|
||||
app:key="donate"
|
||||
app:title="@string/donate_preference_title"
|
||||
app:persistent="false"
|
||||
app:isPreferenceVisible="false"/>
|
||||
|
||||
</PreferenceScreen>
|
||||
@@ -1,30 +1,54 @@
|
||||
<shortcuts xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!--
|
||||
For shortcut.xml:
|
||||
If applicationId in build.gradle is changed from "com.termux", then targetPackage will
|
||||
need to be manually patched since ${applicationId} variable or resource string does not work.
|
||||
If package name in AndroidManifest is changed from "com.termux", then targetClass will
|
||||
need to be manually patched since dot (.) prefix does not work to automatically prefix the
|
||||
package name.
|
||||
-->
|
||||
|
||||
<shortcut
|
||||
android:shortcutId="new_session"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_new_session"
|
||||
android:shortcutShortLabel="@string/new_session"
|
||||
android:shortcutShortLabel="@string/action_new_session"
|
||||
tools:targetApi="n_mr1">
|
||||
<intent
|
||||
android:action="android.intent.action.RUN"
|
||||
android:targetPackage="com.termux"
|
||||
android:targetClass="com.termux.app.TermuxActivity"/>
|
||||
android:targetClass="com.termux.app.TermuxActivity"
|
||||
android:name="android.shortcut.conversation"/>
|
||||
</shortcut>
|
||||
|
||||
<shortcut
|
||||
android:shortcutId="new_failsafe_session"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_new_session"
|
||||
android:shortcutShortLabel="@string/new_session_failsafe"
|
||||
android:shortcutShortLabel="@string/action_new_session_failsafe"
|
||||
tools:targetApi="n_mr1">
|
||||
<intent
|
||||
android:action="android.intent.action.RUN"
|
||||
android:targetPackage="com.termux"
|
||||
android:targetClass="com.termux.app.TermuxActivity">
|
||||
<extra android:name="com.termux.app.failsafe_session" android:value="true" />
|
||||
android:targetClass="com.termux.app.TermuxActivity"
|
||||
android:name="android.shortcut.conversation">
|
||||
<extra android:name="com.termux.app.failsafe_session" android:value="true"/>
|
||||
</intent>
|
||||
</shortcut>
|
||||
|
||||
<shortcut
|
||||
android:shortcutId="settings"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_settings"
|
||||
android:shortcutShortLabel="@string/action_open_settings"
|
||||
tools:targetApi="n_mr1">
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:targetPackage="com.termux"
|
||||
android:targetClass="com.termux.app.activities.SettingsActivity"
|
||||
android:name="android.shortcut.conversation"/>
|
||||
</shortcut>
|
||||
|
||||
</shortcuts>
|
||||
|
||||
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,5 +1,7 @@
|
||||
package com.termux.app;
|
||||
|
||||
import com.termux.shared.data.DataUtils;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
@@ -11,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, TermuxActivity.extractUrls(text));
|
||||
Assert.assertEquals(expected, DataUtils.extractUrls(text));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.1.1'
|
||||
classpath 'com.android.tools.build:gradle:4.1.3'
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,13 @@
|
||||
org.gradle.jvmargs=-Xmx2048M
|
||||
android.useAndroidX=true
|
||||
|
||||
termuxVersion=0.111
|
||||
termuxVersionCode=111
|
||||
|
||||
minSdkVersion=24
|
||||
targetSdkVersion=28
|
||||
ndkVersion=22.0.7026061
|
||||
compileSdkVersion=28
|
||||
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-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-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
|
||||
|
||||
@@ -1 +1 @@
|
||||
include ':app', ':terminal-emulator', ':terminal-view'
|
||||
include ':app', ':termux-shared', ':terminal-emulator', ':terminal-view'
|
||||
|
||||
@@ -50,16 +50,21 @@ tasks.withType(Test) {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'junit:junit:4.13.1'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
}
|
||||
|
||||
task sourceJar(type: Jar) {
|
||||
from android.sourceSets.main.java.srcDirs
|
||||
classifier "sources"
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
bar(MavenPublication) {
|
||||
groupId 'com.termux'
|
||||
artifactId 'terminal-emulator'
|
||||
version '0.106.1'
|
||||
version "0.113"
|
||||
artifact(sourceJar)
|
||||
artifact("$buildDir/outputs/aar/terminal-emulator-release.aar")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
public final class EmulatorDebug {
|
||||
|
||||
/** The tag to use with {@link Log}. */
|
||||
public static final String LOG_TAG = "termux";
|
||||
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
@@ -109,8 +108,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;
|
||||
@@ -153,6 +152,8 @@ public final class TerminalEmulator {
|
||||
/** The terminal session this emulator is bound to. */
|
||||
private final TerminalOutput mSession;
|
||||
|
||||
TerminalSessionClient mClient;
|
||||
|
||||
/** Keeps track of the current argument of the current escape sequence. Ranges from 0 to MAX_ESCAPE_PARAMETERS-1. */
|
||||
private int mArgIndex;
|
||||
/** Holds the arguments of the current escape sequence. */
|
||||
@@ -204,6 +205,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.
|
||||
@@ -227,6 +240,8 @@ public final class TerminalEmulator {
|
||||
|
||||
public final TerminalColors mColors = new TerminalColors();
|
||||
|
||||
private static final String LOG_TAG = "TerminalEmulator";
|
||||
|
||||
private boolean isDecsetInternalBitSet(int bit) {
|
||||
return (mCurrentDecSetFlags & bit) != 0;
|
||||
}
|
||||
@@ -258,7 +273,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:
|
||||
@@ -279,16 +294,21 @@ public final class TerminalEmulator {
|
||||
}
|
||||
}
|
||||
|
||||
public TerminalEmulator(TerminalOutput session, int columns, int rows, int transcriptRows) {
|
||||
public TerminalEmulator(TerminalOutput session, int columns, int rows, int transcriptRows, TerminalSessionClient client) {
|
||||
mSession = session;
|
||||
mScreen = mMainBuffer = new TerminalBuffer(columns, transcriptRows, rows);
|
||||
mAltBuffer = new TerminalBuffer(columns, rows, rows);
|
||||
mClient = client;
|
||||
mRows = rows;
|
||||
mColumns = columns;
|
||||
mTabStop = new boolean[mColumns];
|
||||
reset();
|
||||
}
|
||||
|
||||
public void updateTerminalSessionClient(TerminalSessionClient client) {
|
||||
mClient = client;
|
||||
}
|
||||
|
||||
public TerminalBuffer getScreen() {
|
||||
return mScreen;
|
||||
}
|
||||
@@ -373,9 +393,27 @@ public final class TerminalEmulator {
|
||||
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);
|
||||
@@ -751,7 +789,7 @@ public final class TerminalEmulator {
|
||||
if (internalBit != -1) {
|
||||
value = isDecsetInternalBitSet(internalBit) ? 1 : 2; // 1=set, 2=reset.
|
||||
} else {
|
||||
Log.e(EmulatorDebug.LOG_TAG, "Got DECRQM for unrecognized private DEC mode=" + mode);
|
||||
mClient.logError(LOG_TAG, "Got DECRQM for unrecognized private DEC mode=" + mode);
|
||||
value = 0; // 0=not recognized, 3=permanently set, 4=permanently reset
|
||||
}
|
||||
}
|
||||
@@ -888,7 +926,7 @@ public final class TerminalEmulator {
|
||||
case "&8": // Undo key - ignore.
|
||||
break;
|
||||
default:
|
||||
Log.w(EmulatorDebug.LOG_TAG, "Unhandled termcap/terminfo name: '" + trans + "'");
|
||||
mClient.logWarn(LOG_TAG, "Unhandled termcap/terminfo name: '" + trans + "'");
|
||||
}
|
||||
// Respond with invalid request:
|
||||
mSession.write("\033P0+r" + part + "\033\\");
|
||||
@@ -900,12 +938,12 @@ public final class TerminalEmulator {
|
||||
mSession.write("\033P1+r" + part + "=" + hexEncoded + "\033\\");
|
||||
}
|
||||
} else {
|
||||
Log.e(EmulatorDebug.LOG_TAG, "Invalid device termcap/terminfo name of odd length: " + part);
|
||||
mClient.logError(LOG_TAG, "Invalid device termcap/terminfo name of odd length: " + part);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (LOG_ESCAPE_SEQUENCES)
|
||||
Log.e(EmulatorDebug.LOG_TAG, "Unrecognized device control string: " + dcs);
|
||||
mClient.logError(LOG_TAG, "Unrecognized device control string: " + dcs);
|
||||
}
|
||||
finishSequence();
|
||||
}
|
||||
@@ -995,7 +1033,7 @@ public final class TerminalEmulator {
|
||||
int externalBit = mArgs[i];
|
||||
int internalBit = mapDecSetBitToInternalBit(externalBit);
|
||||
if (internalBit == -1) {
|
||||
Log.w(EmulatorDebug.LOG_TAG, "Ignoring request to save/recall decset bit=" + externalBit);
|
||||
mClient.logWarn(LOG_TAG, "Ignoring request to save/recall decset bit=" + externalBit);
|
||||
} else {
|
||||
if (b == 's') {
|
||||
mSavedDecSetFlags |= internalBit;
|
||||
@@ -1046,7 +1084,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).
|
||||
@@ -1182,7 +1223,7 @@ public final class TerminalEmulator {
|
||||
// (1) enables this feature for keys except for those with well-known behavior, e.g., Tab, Backarrow and
|
||||
// some special control character cases, e.g., Control-Space to make a NUL.
|
||||
// (2) enables this feature for keys including the exceptions listed.
|
||||
Log.e(EmulatorDebug.LOG_TAG, "(ignored) CSI > MODIFY RESOURCE: " + getArg0(-1) + " to " + getArg1(-1));
|
||||
mClient.logError(LOG_TAG, "(ignored) CSI > MODIFY RESOURCE: " + getArg0(-1) + " to " + getArg1(-1));
|
||||
break;
|
||||
default:
|
||||
parseArg(b);
|
||||
@@ -1729,7 +1770,7 @@ public final class TerminalEmulator {
|
||||
int firstArg = mArgs[i + 1];
|
||||
if (firstArg == 2) {
|
||||
if (i + 4 > mArgIndex) {
|
||||
Log.w(EmulatorDebug.LOG_TAG, "Too few CSI" + code + ";2 RGB arguments");
|
||||
mClient.logWarn(LOG_TAG, "Too few CSI" + code + ";2 RGB arguments");
|
||||
} else {
|
||||
int red = mArgs[i + 2], green = mArgs[i + 3], blue = mArgs[i + 4];
|
||||
if (red < 0 || green < 0 || blue < 0 || red > 255 || green > 255 || blue > 255) {
|
||||
@@ -1754,7 +1795,7 @@ public final class TerminalEmulator {
|
||||
mBackColor = color;
|
||||
}
|
||||
} else {
|
||||
if (LOG_ESCAPE_SEQUENCES) Log.w(EmulatorDebug.LOG_TAG, "Invalid color index: " + color);
|
||||
if (LOG_ESCAPE_SEQUENCES) mClient.logWarn(LOG_TAG, "Invalid color index: " + color);
|
||||
}
|
||||
} else {
|
||||
finishSequenceAndLogError("Invalid ISO-8613-3 SGR first argument: " + firstArg);
|
||||
@@ -1771,7 +1812,7 @@ public final class TerminalEmulator {
|
||||
mBackColor = code - 100 + 8;
|
||||
} else {
|
||||
if (LOG_ESCAPE_SEQUENCES)
|
||||
Log.w(EmulatorDebug.LOG_TAG, String.format("SGR unknown code %d", code));
|
||||
mClient.logWarn(LOG_TAG, String.format("SGR unknown code %d", code));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1905,7 +1946,7 @@ public final class TerminalEmulator {
|
||||
String clipboardText = new String(Base64.decode(textParameter.substring(startIndex), 0), StandardCharsets.UTF_8);
|
||||
mSession.clipboardText(clipboardText);
|
||||
} catch (Exception e) {
|
||||
Log.e(EmulatorDebug.LOG_TAG, "OSC Manipulate selection, invalid string '" + textParameter + "");
|
||||
mClient.logError(LOG_TAG, "OSC Manipulate selection, invalid string '" + textParameter + "");
|
||||
}
|
||||
break;
|
||||
case 104:
|
||||
@@ -2101,7 +2142,7 @@ public final class TerminalEmulator {
|
||||
}
|
||||
|
||||
private void finishSequenceAndLogError(String error) {
|
||||
if (LOG_ESCAPE_SEQUENCES) Log.w(EmulatorDebug.LOG_TAG, error);
|
||||
if (LOG_ESCAPE_SEQUENCES) mClient.logWarn(LOG_TAG, error);
|
||||
finishSequence();
|
||||
}
|
||||
|
||||
@@ -2310,7 +2351,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?
|
||||
|
||||
@@ -7,6 +7,7 @@ public abstract class TerminalOutput {
|
||||
|
||||
/** Write a string using the UTF-8 encoding to the terminal client. */
|
||||
public final void write(String data) {
|
||||
if (data == null) return;
|
||||
byte[] bytes = data.getBytes(StandardCharsets.UTF_8);
|
||||
write(bytes, 0, bytes.length);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.os.Message;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
import android.system.OsConstants;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileDescriptor;
|
||||
@@ -31,41 +30,6 @@ import java.util.UUID;
|
||||
*/
|
||||
public final class TerminalSession extends TerminalOutput {
|
||||
|
||||
/** Callback to be invoked when a {@link TerminalSession} changes. */
|
||||
public interface SessionChangedCallback {
|
||||
void onTextChanged(TerminalSession changedSession);
|
||||
|
||||
void onTitleChanged(TerminalSession changedSession);
|
||||
|
||||
void onSessionFinished(TerminalSession finishedSession);
|
||||
|
||||
void onClipboardText(TerminalSession session, String text);
|
||||
|
||||
void onBell(TerminalSession session);
|
||||
|
||||
void onColorsChanged(TerminalSession session);
|
||||
|
||||
}
|
||||
|
||||
private static FileDescriptor wrapFileDescriptor(int fileDescriptor) {
|
||||
FileDescriptor result = new FileDescriptor();
|
||||
try {
|
||||
Field descriptorField;
|
||||
try {
|
||||
descriptorField = FileDescriptor.class.getDeclaredField("descriptor");
|
||||
} catch (NoSuchFieldException e) {
|
||||
// For desktop java:
|
||||
descriptorField = FileDescriptor.class.getDeclaredField("fd");
|
||||
}
|
||||
descriptorField.setAccessible(true);
|
||||
descriptorField.set(result, fileDescriptor);
|
||||
} catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException e) {
|
||||
Log.wtf(EmulatorDebug.LOG_TAG, "Error accessing FileDescriptor#descriptor private field", e);
|
||||
System.exit(1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static final int MSG_NEW_INPUT = 1;
|
||||
private static final int MSG_PROCESS_EXITED = 4;
|
||||
|
||||
@@ -87,7 +51,7 @@ public final class TerminalSession extends TerminalOutput {
|
||||
private final byte[] mUtf8InputBuffer = new byte[5];
|
||||
|
||||
/** Callback which gets notified when a session finishes or changes title. */
|
||||
final SessionChangedCallback mChangeCallback;
|
||||
TerminalSessionClient mClient;
|
||||
|
||||
/** The pid of the shell process. 0 if not started and -1 if finished running. */
|
||||
int mShellPid;
|
||||
@@ -104,52 +68,32 @@ public final class TerminalSession extends TerminalOutput {
|
||||
/** Set by the application for user identification of session, not by terminal. */
|
||||
public String mSessionName;
|
||||
|
||||
@SuppressLint("HandlerLeak")
|
||||
final Handler mMainThreadHandler = new Handler() {
|
||||
final byte[] mReceiveBuffer = new byte[4 * 1024];
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
int bytesRead = mProcessToTerminalIOQueue.read(mReceiveBuffer, false);
|
||||
if (bytesRead > 0) {
|
||||
mEmulator.append(mReceiveBuffer, bytesRead);
|
||||
notifyScreenUpdate();
|
||||
}
|
||||
|
||||
if (msg.what == MSG_PROCESS_EXITED) {
|
||||
int exitCode = (Integer) msg.obj;
|
||||
cleanupResources(exitCode);
|
||||
mChangeCallback.onSessionFinished(TerminalSession.this);
|
||||
|
||||
String exitDescription = "\r\n[Process completed";
|
||||
if (exitCode > 0) {
|
||||
// Non-zero process exit.
|
||||
exitDescription += " (code " + exitCode + ")";
|
||||
} else if (exitCode < 0) {
|
||||
// Negated signal.
|
||||
exitDescription += " (signal " + (-exitCode) + ")";
|
||||
}
|
||||
exitDescription += " - press Enter]";
|
||||
|
||||
byte[] bytesToWrite = exitDescription.getBytes(StandardCharsets.UTF_8);
|
||||
mEmulator.append(bytesToWrite, bytesToWrite.length);
|
||||
notifyScreenUpdate();
|
||||
}
|
||||
}
|
||||
};
|
||||
final Handler mMainThreadHandler = new MainThreadHandler();
|
||||
|
||||
private final String mShellPath;
|
||||
private final String mCwd;
|
||||
private final String[] mArgs;
|
||||
private final String[] mEnv;
|
||||
|
||||
public TerminalSession(String shellPath, String cwd, String[] args, String[] env, SessionChangedCallback changeCallback) {
|
||||
mChangeCallback = changeCallback;
|
||||
private static final String LOG_TAG = "TerminalSession";
|
||||
|
||||
public TerminalSession(String shellPath, String cwd, String[] args, String[] env, TerminalSessionClient client) {
|
||||
this.mShellPath = shellPath;
|
||||
this.mCwd = cwd;
|
||||
this.mArgs = args;
|
||||
this.mEnv = env;
|
||||
this.mClient = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param client The {@link TerminalSessionClient} interface implementation to allow
|
||||
* for communication between {@link TerminalSession} and its client.
|
||||
*/
|
||||
public void updateTerminalSessionClient(TerminalSessionClient client) {
|
||||
mClient = client;
|
||||
|
||||
if (mEmulator != null)
|
||||
mEmulator.updateTerminalSessionClient(client);
|
||||
}
|
||||
|
||||
/** Inform the attached pty of the new size and reflow or initialize the emulator. */
|
||||
@@ -174,13 +118,13 @@ 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);
|
||||
mEmulator = new TerminalEmulator(this, columns, rows, /* transcript= */2000, mClient);
|
||||
|
||||
int[] processId = new int[1];
|
||||
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns);
|
||||
mShellPid = processId[0];
|
||||
|
||||
final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor);
|
||||
final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor, mClient);
|
||||
|
||||
new Thread("TermSessionInputReader[pid=" + mShellPid + "]") {
|
||||
@Override
|
||||
@@ -246,23 +190,23 @@ public final class TerminalSession extends TerminalOutput {
|
||||
} else if (codePoint <= /* 11 bits */0b11111111111) {
|
||||
/* 110xxxxx leading byte with leading 5 bits */
|
||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11000000 | (codePoint >> 6));
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
|
||||
} else if (codePoint <= /* 16 bits */0b1111111111111111) {
|
||||
/* 1110xxxx leading byte with leading 4 bits */
|
||||
/* 1110xxxx leading byte with leading 4 bits */
|
||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11100000 | (codePoint >> 12));
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 6) & 0b111111));
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
|
||||
} else { /* We have checked codePoint <= 1114111 above, so we have max 21 bits = 0b111111111111111111111 */
|
||||
/* 11110xxx leading byte with leading 3 bits */
|
||||
/* 11110xxx leading byte with leading 3 bits */
|
||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11110000 | (codePoint >> 18));
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 12) & 0b111111));
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | ((codePoint >> 6) & 0b111111));
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
/* 10xxxxxx continuation byte with following 6 bits */
|
||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
|
||||
}
|
||||
write(mUtf8InputBuffer, 0, bufferPosition);
|
||||
@@ -272,9 +216,9 @@ public final class TerminalSession extends TerminalOutput {
|
||||
return mEmulator;
|
||||
}
|
||||
|
||||
/** Notify the {@link #mChangeCallback} that the screen has changed. */
|
||||
/** Notify the {@link #mClient} that the screen has changed. */
|
||||
protected void notifyScreenUpdate() {
|
||||
mChangeCallback.onTextChanged(this);
|
||||
mClient.onTextChanged(this);
|
||||
}
|
||||
|
||||
/** Reset state for terminal emulator state. */
|
||||
@@ -289,7 +233,7 @@ public final class TerminalSession extends TerminalOutput {
|
||||
try {
|
||||
Os.kill(mShellPid, OsConstants.SIGKILL);
|
||||
} catch (ErrnoException e) {
|
||||
Log.w("termux", "Failed sending SIGKILL: " + e.getMessage());
|
||||
mClient.logWarn(LOG_TAG, "Failed sending SIGKILL: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -309,7 +253,7 @@ public final class TerminalSession extends TerminalOutput {
|
||||
|
||||
@Override
|
||||
public void titleChanged(String oldTitle, String newTitle) {
|
||||
mChangeCallback.onTitleChanged(this);
|
||||
mClient.onTitleChanged(this);
|
||||
}
|
||||
|
||||
public synchronized boolean isRunning() {
|
||||
@@ -323,17 +267,17 @@ public final class TerminalSession extends TerminalOutput {
|
||||
|
||||
@Override
|
||||
public void clipboardText(String text) {
|
||||
mChangeCallback.onClipboardText(this, text);
|
||||
mClient.onClipboardText(this, text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBell() {
|
||||
mChangeCallback.onBell(this);
|
||||
mClient.onBell(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onColorsChanged() {
|
||||
mChangeCallback.onColorsChanged(this);
|
||||
mClient.onColorsChanged(this);
|
||||
}
|
||||
|
||||
public int getPid() {
|
||||
@@ -356,10 +300,65 @@ public final class TerminalSession extends TerminalOutput {
|
||||
return outputPath;
|
||||
}
|
||||
} catch (IOException | SecurityException e) {
|
||||
Log.e(EmulatorDebug.LOG_TAG, "Error getting current directory", e);
|
||||
mClient.logStackTraceWithMessage(LOG_TAG, "Error getting current directory", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static FileDescriptor wrapFileDescriptor(int fileDescriptor, TerminalSessionClient client) {
|
||||
FileDescriptor result = new FileDescriptor();
|
||||
try {
|
||||
Field descriptorField;
|
||||
try {
|
||||
descriptorField = FileDescriptor.class.getDeclaredField("descriptor");
|
||||
} catch (NoSuchFieldException e) {
|
||||
// For desktop java:
|
||||
descriptorField = FileDescriptor.class.getDeclaredField("fd");
|
||||
}
|
||||
descriptorField.setAccessible(true);
|
||||
descriptorField.set(result, fileDescriptor);
|
||||
} catch (NoSuchFieldException | IllegalAccessException | IllegalArgumentException e) {
|
||||
client.logStackTraceWithMessage(LOG_TAG, "Error accessing FileDescriptor#descriptor private field", e);
|
||||
System.exit(1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@SuppressLint("HandlerLeak")
|
||||
class MainThreadHandler extends Handler {
|
||||
|
||||
final byte[] mReceiveBuffer = new byte[4 * 1024];
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
int bytesRead = mProcessToTerminalIOQueue.read(mReceiveBuffer, false);
|
||||
if (bytesRead > 0) {
|
||||
mEmulator.append(mReceiveBuffer, bytesRead);
|
||||
notifyScreenUpdate();
|
||||
}
|
||||
|
||||
if (msg.what == MSG_PROCESS_EXITED) {
|
||||
int exitCode = (Integer) msg.obj;
|
||||
cleanupResources(exitCode);
|
||||
|
||||
String exitDescription = "\r\n[Process completed";
|
||||
if (exitCode > 0) {
|
||||
// Non-zero process exit.
|
||||
exitDescription += " (code " + exitCode + ")";
|
||||
} else if (exitCode < 0) {
|
||||
// Negated signal.
|
||||
exitDescription += " (signal " + (-exitCode) + ")";
|
||||
}
|
||||
exitDescription += " - press Enter]";
|
||||
|
||||
byte[] bytesToWrite = exitDescription.getBytes(StandardCharsets.UTF_8);
|
||||
mEmulator.append(bytesToWrite, bytesToWrite.length);
|
||||
notifyScreenUpdate();
|
||||
|
||||
mClient.onSessionFinished(TerminalSession.this);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.termux.terminal;
|
||||
|
||||
/**
|
||||
* The interface for communication between {@link TerminalSession} and its client. It is used to
|
||||
* send callbacks to the client when {@link TerminalSession} changes or for sending other
|
||||
* back data to the client like logs.
|
||||
*/
|
||||
public interface TerminalSessionClient {
|
||||
|
||||
void onTextChanged(TerminalSession changedSession);
|
||||
|
||||
void onTitleChanged(TerminalSession changedSession);
|
||||
|
||||
void onSessionFinished(TerminalSession finishedSession);
|
||||
|
||||
void onClipboardText(TerminalSession session, String text);
|
||||
|
||||
void onBell(TerminalSession session);
|
||||
|
||||
void onColorsChanged(TerminalSession session);
|
||||
|
||||
void onTerminalCursorStateChange(boolean state);
|
||||
|
||||
|
||||
void logError(String tag, String message);
|
||||
|
||||
void logWarn(String tag, String message);
|
||||
|
||||
void logInfo(String tag, String message);
|
||||
|
||||
void logDebug(String tag, String message);
|
||||
|
||||
void logVerbose(String tag, String message);
|
||||
|
||||
void logStackTraceWithMessage(String tag, String message, Exception e);
|
||||
|
||||
void logStackTrace(String tag, Exception e);
|
||||
|
||||
}
|
||||
@@ -88,7 +88,7 @@ static int create_subprocess(JNIEnv* env,
|
||||
struct dirent* entry;
|
||||
while ((entry = readdir(self_dir)) != NULL) {
|
||||
int fd = atoi(entry->d_name);
|
||||
if(fd > 2 && fd != self_dir_fd) close(fd);
|
||||
if (fd > 2 && fd != self_dir_fd) close(fd);
|
||||
}
|
||||
closedir(self_dir);
|
||||
}
|
||||
|
||||
@@ -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,7 +103,8 @@ public abstract class TerminalTestCase extends TestCase {
|
||||
}
|
||||
|
||||
protected TerminalTestCase withTerminalSized(int columns, int rows) {
|
||||
mTerminal = new TerminalEmulator(mOutput, columns, rows, rows * 2);
|
||||
// The tests aren't currently using the client, so a null client will suffice, a dummy client should be implemented if needed
|
||||
mTerminal = new TerminalEmulator(mOutput, columns, rows, rows * 2, null);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,12 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'junit:junit:4.13.1'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
}
|
||||
|
||||
task sourceJar(type: Jar) {
|
||||
from android.sourceSets.main.java.srcDirs
|
||||
classifier "sources"
|
||||
}
|
||||
|
||||
publishing {
|
||||
@@ -37,7 +42,8 @@ publishing {
|
||||
bar(MavenPublication) {
|
||||
groupId 'com.termux'
|
||||
artifactId 'terminal-view'
|
||||
version '0.106.1'
|
||||
version "0.113"
|
||||
artifact(sourceJar)
|
||||
artifact("$buildDir/outputs/aar/terminal-view-release.aar")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
@@ -230,4 +230,12 @@ public final class TerminalRenderer {
|
||||
|
||||
if (savedMatrix) canvas.restore();
|
||||
}
|
||||
|
||||
public float getFontWidth() {
|
||||
return mFontWidth;
|
||||
}
|
||||
|
||||
public int getFontLineSpacing() {
|
||||
return mFontLineSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,13 +3,16 @@ package com.termux.view;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ScaleGestureDetector;
|
||||
import android.view.View;
|
||||
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
/**
|
||||
* Input and scale listener which may be set on a {@link TerminalView} through
|
||||
* {@link TerminalView#setOnKeyListener(TerminalViewClient)}.
|
||||
* <p/>
|
||||
* The interface for communication between {@link TerminalView} and its client. It allows for getting
|
||||
* various configuration options from the client and for sending back data to the client like logs,
|
||||
* key events, both hardware and IME (which makes it different from that available with
|
||||
* {@link View#setOnKeyListener(View.OnKeyListener)}, etc. It must be set for the
|
||||
* {@link TerminalView} through {@link TerminalView#setTerminalViewClient(TerminalViewClient)}.
|
||||
*/
|
||||
public interface TerminalViewClient {
|
||||
|
||||
@@ -18,6 +21,8 @@ public interface TerminalViewClient {
|
||||
*/
|
||||
float onScale(float scale);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* On a single tap on the terminal if terminal mouse reporting not enabled.
|
||||
*/
|
||||
@@ -25,18 +30,45 @@ public interface TerminalViewClient {
|
||||
|
||||
boolean shouldBackButtonBeMappedToEscape();
|
||||
|
||||
boolean shouldEnforceCharBasedInput();
|
||||
|
||||
boolean shouldUseCtrlSpaceWorkaround();
|
||||
|
||||
|
||||
|
||||
void copyModeChanged(boolean copyMode);
|
||||
|
||||
|
||||
|
||||
boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession session);
|
||||
|
||||
boolean onKeyUp(int keyCode, KeyEvent e);
|
||||
|
||||
boolean onLongPress(MotionEvent event);
|
||||
|
||||
|
||||
|
||||
boolean readControlKey();
|
||||
|
||||
boolean readAltKey();
|
||||
|
||||
|
||||
boolean onCodePoint(int codePoint, boolean ctrlDown, TerminalSession session);
|
||||
|
||||
boolean onLongPress(MotionEvent event);
|
||||
|
||||
|
||||
void logError(String tag, String message);
|
||||
|
||||
void logWarn(String tag, String message);
|
||||
|
||||
void logInfo(String tag, String message);
|
||||
|
||||
void logDebug(String tag, String message);
|
||||
|
||||
void logVerbose(String tag, String message);
|
||||
|
||||
void logStackTraceWithMessage(String tag, String message, Exception e);
|
||||
|
||||
void logStackTrace(String tag, Exception e);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.termux.view.textselection;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ViewTreeObserver;
|
||||
|
||||
import com.termux.view.TerminalView;
|
||||
|
||||
/**
|
||||
* A CursorController instance can be used to control cursors in the text.
|
||||
* It is not used outside of {@link TerminalView}.
|
||||
*/
|
||||
public interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
|
||||
/**
|
||||
* Show the cursors on screen. Will be drawn by {@link #render()} by a call during onDraw.
|
||||
* See also {@link #hide()}.
|
||||
*/
|
||||
void show(MotionEvent event);
|
||||
|
||||
/**
|
||||
* Hide the cursors from screen.
|
||||
* See also {@link #show(MotionEvent event)}.
|
||||
*/
|
||||
boolean hide();
|
||||
|
||||
/**
|
||||
* Render the cursors.
|
||||
*/
|
||||
void render();
|
||||
|
||||
/**
|
||||
* Update the cursor positions.
|
||||
*/
|
||||
void updatePosition(TextSelectionHandleView handle, int x, int y);
|
||||
|
||||
/**
|
||||
* This method is called by {@link #onTouchEvent(MotionEvent)} and gives the cursors
|
||||
* a chance to become active and/or visible.
|
||||
*
|
||||
* @param event The touch event
|
||||
*/
|
||||
boolean onTouchEvent(MotionEvent event);
|
||||
|
||||
/**
|
||||
* Called when the view is detached from window. Perform house keeping task, such as
|
||||
* stopping Runnable thread that would otherwise keep a reference on the context, thus
|
||||
* preventing the activity to be recycled.
|
||||
*/
|
||||
void onDetached();
|
||||
|
||||
/**
|
||||
* @return true if the cursors are currently active.
|
||||
*/
|
||||
boolean isActive();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
package com.termux.view.textselection;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.text.TextUtils;
|
||||
import android.view.ActionMode;
|
||||
import android.view.InputDevice;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
import com.termux.terminal.TerminalBuffer;
|
||||
import com.termux.terminal.WcWidth;
|
||||
import com.termux.view.R;
|
||||
import com.termux.view.TerminalView;
|
||||
|
||||
public class TextSelectionCursorController implements CursorController {
|
||||
|
||||
private final TerminalView terminalView;
|
||||
private final TextSelectionHandleView mStartHandle, mEndHandle;
|
||||
private boolean mIsSelectingText = false;
|
||||
private long mShowStartTime = System.currentTimeMillis();
|
||||
|
||||
private final int mHandleHeight;
|
||||
private int mSelX1 = -1, mSelX2 = -1, mSelY1 = -1, mSelY2 = -1;
|
||||
|
||||
private ActionMode mActionMode;
|
||||
private final int ACTION_COPY = 1;
|
||||
private final int ACTION_PASTE = 2;
|
||||
private final int ACTION_MORE = 3;
|
||||
|
||||
public TextSelectionCursorController(TerminalView terminalView) {
|
||||
this.terminalView = terminalView;
|
||||
mStartHandle = new TextSelectionHandleView(terminalView, this, TextSelectionHandleView.LEFT);
|
||||
mEndHandle = new TextSelectionHandleView(terminalView, this, TextSelectionHandleView.RIGHT);
|
||||
|
||||
mHandleHeight = Math.max(mStartHandle.getHandleHeight(), mEndHandle.getHandleHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show(MotionEvent event) {
|
||||
setInitialTextSelectionPosition(event);
|
||||
mStartHandle.positionAtCursor(mSelX1, mSelY1, true);
|
||||
mEndHandle.positionAtCursor(mSelX2 + 1, mSelY2, true);
|
||||
|
||||
setActionModeCallBacks();
|
||||
mShowStartTime = System.currentTimeMillis();
|
||||
mIsSelectingText = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hide() {
|
||||
if (!isActive()) return false;
|
||||
|
||||
// prevent hide calls right after a show call, like long pressing the down key
|
||||
// 300ms seems long enough that it wouldn't cause hide problems if action button
|
||||
// is quickly clicked after the show, otherwise decrease it
|
||||
if (System.currentTimeMillis() - mShowStartTime < 300) {
|
||||
return false;
|
||||
}
|
||||
|
||||
mStartHandle.hide();
|
||||
mEndHandle.hide();
|
||||
|
||||
if (mActionMode != null) {
|
||||
// This will hide the TextSelectionCursorController
|
||||
mActionMode.finish();
|
||||
}
|
||||
|
||||
mSelX1 = mSelY1 = mSelX2 = mSelY2 = -1;
|
||||
mIsSelectingText = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render() {
|
||||
if (!isActive()) return;
|
||||
|
||||
mStartHandle.positionAtCursor(mSelX1, mSelY1, false);
|
||||
mEndHandle.positionAtCursor(mSelX2 + 1, mSelY2, false);
|
||||
|
||||
if (mActionMode != null) {
|
||||
mActionMode.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void setInitialTextSelectionPosition(MotionEvent event) {
|
||||
int cx = (int) (event.getX() / terminalView.mRenderer.getFontWidth());
|
||||
final boolean eventFromMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
|
||||
// Offset for finger:
|
||||
final int SELECT_TEXT_OFFSET_Y = eventFromMouse ? 0 : -40;
|
||||
int cy = (int) ((event.getY() + SELECT_TEXT_OFFSET_Y) / terminalView.mRenderer.getFontLineSpacing()) + terminalView.getTopRow();
|
||||
|
||||
mSelX1 = mSelX2 = cx;
|
||||
mSelY1 = mSelY2 = cy;
|
||||
|
||||
TerminalBuffer screen = terminalView.mEmulator.getScreen();
|
||||
if (!" ".equals(screen.getSelectedText(mSelX1, mSelY1, mSelX1, mSelY1))) {
|
||||
// Selecting something other than whitespace. Expand to word.
|
||||
while (mSelX1 > 0 && !"".equals(screen.getSelectedText(mSelX1 - 1, mSelY1, mSelX1 - 1, mSelY1))) {
|
||||
mSelX1--;
|
||||
}
|
||||
while (mSelX2 < terminalView.mEmulator.mColumns - 1 && !"".equals(screen.getSelectedText(mSelX2 + 1, mSelY1, mSelX2 + 1, mSelY1))) {
|
||||
mSelX2++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setActionModeCallBacks() {
|
||||
final ActionMode.Callback callback = new ActionMode.Callback() {
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
int show = MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT;
|
||||
|
||||
ClipboardManager clipboard = (ClipboardManager) terminalView.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
menu.add(Menu.NONE, ACTION_COPY, Menu.NONE, R.string.copy_text).setShowAsAction(show);
|
||||
menu.add(Menu.NONE, ACTION_PASTE, Menu.NONE, R.string.paste_text).setEnabled(clipboard.hasPrimaryClip()).setShowAsAction(show);
|
||||
menu.add(Menu.NONE, ACTION_MORE, Menu.NONE, R.string.text_selection_more);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
if (!isActive()) {
|
||||
// Fix issue where the dialog is pressed while being dismissed.
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (item.getItemId()) {
|
||||
case ACTION_COPY:
|
||||
String selectedText = terminalView.mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2).trim();
|
||||
terminalView.mTermSession.clipboardText(selectedText);
|
||||
terminalView.stopTextSelectionMode();
|
||||
break;
|
||||
case ACTION_PASTE:
|
||||
terminalView.stopTextSelectionMode();
|
||||
ClipboardManager clipboard = (ClipboardManager) terminalView.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clipData = clipboard.getPrimaryClip();
|
||||
if (clipData != null) {
|
||||
CharSequence paste = clipData.getItemAt(0).coerceToText(terminalView.getContext());
|
||||
if (!TextUtils.isEmpty(paste)) terminalView.mEmulator.paste(paste.toString());
|
||||
}
|
||||
break;
|
||||
case ACTION_MORE:
|
||||
terminalView.stopTextSelectionMode(); //we stop text selection first, otherwise handles will show above popup
|
||||
terminalView.showContextMenu();
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
mActionMode = terminalView.startActionMode(new ActionMode.Callback2() {
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
return callback.onCreateActionMode(mode, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
return callback.onActionItemClicked(mode, item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
// Ignore.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
|
||||
int x1 = Math.round(mSelX1 * terminalView.mRenderer.getFontWidth());
|
||||
int x2 = Math.round(mSelX2 * terminalView.mRenderer.getFontWidth());
|
||||
int y1 = Math.round((mSelY1 - 1 - terminalView.getTopRow()) * terminalView.mRenderer.getFontLineSpacing());
|
||||
int y2 = Math.round((mSelY2 + 1 - terminalView.getTopRow()) * terminalView.mRenderer.getFontLineSpacing());
|
||||
|
||||
|
||||
if (x1 > x2) {
|
||||
int tmp = x1;
|
||||
x1 = x2;
|
||||
x2 = tmp;
|
||||
}
|
||||
|
||||
outRect.set(x1, y1 + mHandleHeight, x2, y2 + mHandleHeight);
|
||||
}
|
||||
}, ActionMode.TYPE_FLOATING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updatePosition(TextSelectionHandleView handle, int x, int y) {
|
||||
TerminalBuffer screen = terminalView.mEmulator.getScreen();
|
||||
final int scrollRows = screen.getActiveRows() - terminalView.mEmulator.mRows;
|
||||
if (handle == mStartHandle) {
|
||||
mSelX1 = terminalView.getCursorX(x);
|
||||
mSelY1 = terminalView.getCursorY(y);
|
||||
if (mSelX1 < 0) {
|
||||
mSelX1 = 0;
|
||||
}
|
||||
|
||||
if (mSelY1 < -scrollRows) {
|
||||
mSelY1 = -scrollRows;
|
||||
|
||||
} else if (mSelY1 > terminalView.mEmulator.mRows - 1) {
|
||||
mSelY1 = terminalView.mEmulator.mRows - 1;
|
||||
|
||||
}
|
||||
|
||||
if (mSelY1 > mSelY2) {
|
||||
mSelY1 = mSelY2;
|
||||
}
|
||||
if (mSelY1 == mSelY2 && mSelX1 > mSelX2) {
|
||||
mSelX1 = mSelX2;
|
||||
}
|
||||
|
||||
if (!terminalView.mEmulator.isAlternateBufferActive()) {
|
||||
int topRow = terminalView.getTopRow();
|
||||
|
||||
if (mSelY1 <= topRow) {
|
||||
topRow--;
|
||||
if (topRow < -scrollRows) {
|
||||
topRow = -scrollRows;
|
||||
}
|
||||
} else if (mSelY1 >= topRow + terminalView.mEmulator.mRows) {
|
||||
topRow++;
|
||||
if (topRow > 0) {
|
||||
topRow = 0;
|
||||
}
|
||||
}
|
||||
|
||||
terminalView.setTopRow(topRow);
|
||||
}
|
||||
|
||||
mSelX1 = getValidCurX(screen, mSelY1, mSelX1);
|
||||
|
||||
} else {
|
||||
mSelX2 = terminalView.getCursorX(x);
|
||||
mSelY2 = terminalView.getCursorY(y);
|
||||
if (mSelX2 < 0) {
|
||||
mSelX2 = 0;
|
||||
}
|
||||
|
||||
if (mSelY2 < -scrollRows) {
|
||||
mSelY2 = -scrollRows;
|
||||
} else if (mSelY2 > terminalView.mEmulator.mRows - 1) {
|
||||
mSelY2 = terminalView.mEmulator.mRows - 1;
|
||||
}
|
||||
|
||||
if (mSelY1 > mSelY2) {
|
||||
mSelY2 = mSelY1;
|
||||
}
|
||||
if (mSelY1 == mSelY2 && mSelX1 > mSelX2) {
|
||||
mSelX2 = mSelX1;
|
||||
}
|
||||
|
||||
if (!terminalView.mEmulator.isAlternateBufferActive()) {
|
||||
int topRow = terminalView.getTopRow();
|
||||
|
||||
if (mSelY2 <= topRow) {
|
||||
topRow--;
|
||||
if (topRow < -scrollRows) {
|
||||
topRow = -scrollRows;
|
||||
}
|
||||
} else if (mSelY2 >= topRow + terminalView.mEmulator.mRows) {
|
||||
topRow++;
|
||||
if (topRow > 0) {
|
||||
topRow = 0;
|
||||
}
|
||||
}
|
||||
|
||||
terminalView.setTopRow(topRow);
|
||||
}
|
||||
|
||||
mSelX2 = getValidCurX(screen, mSelY2, mSelX2);
|
||||
}
|
||||
|
||||
terminalView.invalidate();
|
||||
}
|
||||
|
||||
private int getValidCurX(TerminalBuffer screen, int cy, int cx) {
|
||||
String line = screen.getSelectedText(0, cy, cx, cy);
|
||||
if (!TextUtils.isEmpty(line)) {
|
||||
int col = 0;
|
||||
for (int i = 0, len = line.length(); i < len; i++) {
|
||||
char ch1 = line.charAt(i);
|
||||
if (ch1 == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
int wc;
|
||||
if (Character.isHighSurrogate(ch1) && i + 1 < len) {
|
||||
char ch2 = line.charAt(++i);
|
||||
wc = WcWidth.width(Character.toCodePoint(ch1, ch2));
|
||||
} else {
|
||||
wc = WcWidth.width(ch1);
|
||||
}
|
||||
|
||||
final int cend = col + wc;
|
||||
if (cx > col && cx < cend) {
|
||||
return cend;
|
||||
}
|
||||
if (cend == col) {
|
||||
return col;
|
||||
}
|
||||
col = cend;
|
||||
}
|
||||
}
|
||||
return cx;
|
||||
}
|
||||
|
||||
public void decrementYTextSelectionCursors(int decrement) {
|
||||
mSelY1 -= decrement;
|
||||
mSelY2 -= decrement;
|
||||
}
|
||||
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void onTouchModeChanged(boolean isInTouchMode) {
|
||||
if (!isInTouchMode) {
|
||||
terminalView.stopTextSelectionMode();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetached() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return mIsSelectingText;
|
||||
}
|
||||
|
||||
public void getSelectors(int[] sel) {
|
||||
if (sel == null || sel.length != 4) {
|
||||
return;
|
||||
}
|
||||
|
||||
sel[0] = mSelY1;
|
||||
sel[1] = mSelY2;
|
||||
sel[2] = mSelX1;
|
||||
sel[3] = mSelX2;
|
||||
}
|
||||
|
||||
public ActionMode getActionMode() {
|
||||
return mActionMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if this controller is currently used to move the start selection.
|
||||
*/
|
||||
public boolean isSelectionStartDragged() {
|
||||
return mStartHandle.isDragging();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if this controller is currently used to move the end selection.
|
||||
*/
|
||||
public boolean isSelectionEndDragged() {
|
||||
return mEndHandle.isDragging();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
package com.termux.view.textselection;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.SystemClock;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.PopupWindow;
|
||||
|
||||
import com.termux.view.R;
|
||||
import com.termux.view.TerminalView;
|
||||
|
||||
@SuppressLint("ViewConstructor")
|
||||
public class TextSelectionHandleView extends View {
|
||||
private final TerminalView terminalView;
|
||||
private PopupWindow mHandle;
|
||||
private final CursorController mCursorController;
|
||||
|
||||
private final Drawable mHandleLeftDrawable;
|
||||
private final Drawable mHandleRightDrawable;
|
||||
private Drawable mHandleDrawable;
|
||||
|
||||
private boolean mIsDragging;
|
||||
|
||||
final int[] mTempCoords = new int[2];
|
||||
Rect mTempRect;
|
||||
|
||||
private int mPointX;
|
||||
private int mPointY;
|
||||
private float mTouchToWindowOffsetX;
|
||||
private float mTouchToWindowOffsetY;
|
||||
private float mHotspotX;
|
||||
private float mHotspotY;
|
||||
private float mTouchOffsetY;
|
||||
private int mLastParentX;
|
||||
private int mLastParentY;
|
||||
|
||||
private int mHandleHeight;
|
||||
private int mHandleWidth;
|
||||
|
||||
private final int mInitialOrientation;
|
||||
private int mOrientation;
|
||||
|
||||
public static final int LEFT = 0;
|
||||
public static final int RIGHT = 2;
|
||||
|
||||
private long mLastTime;
|
||||
|
||||
public TextSelectionHandleView(TerminalView terminalView, CursorController cursorController, int initialOrientation) {
|
||||
super(terminalView.getContext());
|
||||
this.terminalView = terminalView;
|
||||
mCursorController = cursorController;
|
||||
mInitialOrientation = initialOrientation;
|
||||
|
||||
mHandleLeftDrawable = getContext().getDrawable(R.drawable.text_select_handle_left_material);
|
||||
mHandleRightDrawable = getContext().getDrawable(R.drawable.text_select_handle_right_material);
|
||||
|
||||
setOrientation(mInitialOrientation);
|
||||
}
|
||||
|
||||
private void initHandle() {
|
||||
mHandle = new PopupWindow(terminalView.getContext(), null,
|
||||
android.R.attr.textSelectHandleWindowStyle);
|
||||
mHandle.setSplitTouchEnabled(true);
|
||||
mHandle.setClippingEnabled(false);
|
||||
mHandle.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
|
||||
mHandle.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
mHandle.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
mHandle.setBackgroundDrawable(null);
|
||||
mHandle.setAnimationStyle(0);
|
||||
mHandle.setEnterTransition(null);
|
||||
mHandle.setExitTransition(null);
|
||||
mHandle.setContentView(this);
|
||||
}
|
||||
|
||||
public void setOrientation(int orientation) {
|
||||
mOrientation = orientation;
|
||||
int handleWidth = 0;
|
||||
switch (orientation) {
|
||||
case LEFT: {
|
||||
mHandleDrawable = mHandleLeftDrawable;
|
||||
handleWidth = mHandleDrawable.getIntrinsicWidth();
|
||||
mHotspotX = (handleWidth * 3) / (float) 4;
|
||||
break;
|
||||
}
|
||||
|
||||
case RIGHT: {
|
||||
mHandleDrawable = mHandleRightDrawable;
|
||||
handleWidth = mHandleDrawable.getIntrinsicWidth();
|
||||
mHotspotX = handleWidth / (float) 4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
mHandleHeight = mHandleDrawable.getIntrinsicHeight();
|
||||
|
||||
mHandleWidth = handleWidth;
|
||||
mTouchOffsetY = -mHandleHeight * 0.3f;
|
||||
mHotspotY = 0;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void show() {
|
||||
if (!isPositionVisible()) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// We remove handle from its parent first otherwise the following exception may be thrown
|
||||
// java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
|
||||
removeFromParent();
|
||||
|
||||
initHandle(); // init the handle
|
||||
invalidate(); // invalidate to make sure onDraw is called
|
||||
|
||||
final int[] coords = mTempCoords;
|
||||
terminalView.getLocationInWindow(coords);
|
||||
coords[0] += mPointX;
|
||||
coords[1] += mPointY;
|
||||
|
||||
if (mHandle != null)
|
||||
mHandle.showAtLocation(terminalView, 0, coords[0], coords[1]);
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
mIsDragging = false;
|
||||
|
||||
if (mHandle != null) {
|
||||
mHandle.dismiss();
|
||||
|
||||
// We remove handle from its parent, otherwise it may still be shown in some cases even after the dismiss call
|
||||
removeFromParent();
|
||||
mHandle = null; // garbage collect the handle
|
||||
}
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void removeFromParent() {
|
||||
if (!isParentNull()) {
|
||||
((ViewGroup)this.getParent()).removeView(this);
|
||||
}
|
||||
}
|
||||
|
||||
public void positionAtCursor(final int cx, final int cy, boolean forceOrientationCheck) {
|
||||
int x = terminalView.getPointX(cx);
|
||||
int y = terminalView.getPointY(cy + 1);
|
||||
moveTo(x, y, forceOrientationCheck);
|
||||
}
|
||||
|
||||
private void moveTo(int x, int y, boolean forceOrientationCheck) {
|
||||
float oldHotspotX = mHotspotX;
|
||||
checkChangedOrientation(x, forceOrientationCheck);
|
||||
mPointX = (int) (x - (isShowing() ? oldHotspotX : mHotspotX));
|
||||
mPointY = y;
|
||||
|
||||
if (isPositionVisible()) {
|
||||
int[] coords = null;
|
||||
|
||||
if (isShowing()) {
|
||||
coords = mTempCoords;
|
||||
terminalView.getLocationInWindow(coords);
|
||||
int x1 = coords[0] + mPointX;
|
||||
int y1 = coords[1] + mPointY;
|
||||
if (mHandle != null)
|
||||
mHandle.update(x1, y1, getWidth(), getHeight());
|
||||
} else {
|
||||
show();
|
||||
}
|
||||
|
||||
if (mIsDragging) {
|
||||
if (coords == null) {
|
||||
coords = mTempCoords;
|
||||
terminalView.getLocationInWindow(coords);
|
||||
}
|
||||
if (coords[0] != mLastParentX || coords[1] != mLastParentY) {
|
||||
mTouchToWindowOffsetX += coords[0] - mLastParentX;
|
||||
mTouchToWindowOffsetY += coords[1] - mLastParentY;
|
||||
mLastParentX = coords[0];
|
||||
mLastParentY = coords[1];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
|
||||
public void changeOrientation(int orientation) {
|
||||
if (mOrientation != orientation) {
|
||||
setOrientation(orientation);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkChangedOrientation(int posX, boolean force) {
|
||||
if (!mIsDragging && !force) {
|
||||
return;
|
||||
}
|
||||
long millis = SystemClock.currentThreadTimeMillis();
|
||||
if (millis - mLastTime < 50 && !force) {
|
||||
return;
|
||||
}
|
||||
mLastTime = millis;
|
||||
|
||||
final TerminalView hostView = terminalView;
|
||||
final int left = hostView.getLeft();
|
||||
final int right = hostView.getWidth();
|
||||
final int top = hostView.getTop();
|
||||
final int bottom = hostView.getHeight();
|
||||
|
||||
if (mTempRect == null) {
|
||||
mTempRect = new Rect();
|
||||
}
|
||||
final Rect clip = mTempRect;
|
||||
clip.left = left + terminalView.getPaddingLeft();
|
||||
clip.top = top + terminalView.getPaddingTop();
|
||||
clip.right = right - terminalView.getPaddingRight();
|
||||
clip.bottom = bottom - terminalView.getPaddingBottom();
|
||||
|
||||
final ViewParent parent = hostView.getParent();
|
||||
if (parent == null || !parent.getChildVisibleRect(hostView, clip, null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (posX - mHandleWidth < clip.left) {
|
||||
changeOrientation(RIGHT);
|
||||
} else if (posX + mHandleWidth > clip.right) {
|
||||
changeOrientation(LEFT);
|
||||
} else {
|
||||
changeOrientation(mInitialOrientation);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isPositionVisible() {
|
||||
// Always show a dragging handle.
|
||||
if (mIsDragging) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final TerminalView hostView = terminalView;
|
||||
final int left = 0;
|
||||
final int right = hostView.getWidth();
|
||||
final int top = 0;
|
||||
final int bottom = hostView.getHeight();
|
||||
|
||||
if (mTempRect == null) {
|
||||
mTempRect = new Rect();
|
||||
}
|
||||
final Rect clip = mTempRect;
|
||||
clip.left = left + terminalView.getPaddingLeft();
|
||||
clip.top = top + terminalView.getPaddingTop();
|
||||
clip.right = right - terminalView.getPaddingRight();
|
||||
clip.bottom = bottom - terminalView.getPaddingBottom();
|
||||
|
||||
final ViewParent parent = hostView.getParent();
|
||||
if (parent == null || !parent.getChildVisibleRect(hostView, clip, null)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final int[] coords = mTempCoords;
|
||||
hostView.getLocationInWindow(coords);
|
||||
final int posX = coords[0] + mPointX + (int) mHotspotX;
|
||||
final int posY = coords[1] + mPointY + (int) mHotspotY;
|
||||
|
||||
return posX >= clip.left && posX <= clip.right &&
|
||||
posY >= clip.top && posY <= clip.bottom;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas c) {
|
||||
final int width = mHandleDrawable.getIntrinsicWidth();
|
||||
int height = mHandleDrawable.getIntrinsicHeight();
|
||||
mHandleDrawable.setBounds(0, 0, width, height);
|
||||
mHandleDrawable.draw(c);
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
terminalView.updateFloatingToolbarVisibility(event);
|
||||
switch (event.getActionMasked()) {
|
||||
case MotionEvent.ACTION_DOWN: {
|
||||
final float rawX = event.getRawX();
|
||||
final float rawY = event.getRawY();
|
||||
mTouchToWindowOffsetX = rawX - mPointX;
|
||||
mTouchToWindowOffsetY = rawY - mPointY;
|
||||
final int[] coords = mTempCoords;
|
||||
terminalView.getLocationInWindow(coords);
|
||||
mLastParentX = coords[0];
|
||||
mLastParentY = coords[1];
|
||||
mIsDragging = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case MotionEvent.ACTION_MOVE: {
|
||||
final float rawX = event.getRawX();
|
||||
final float rawY = event.getRawY();
|
||||
|
||||
final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX;
|
||||
final float newPosY = rawY - mTouchToWindowOffsetY + mHotspotY + mTouchOffsetY;
|
||||
|
||||
mCursorController.updatePosition(this, Math.round(newPosX), Math.round(newPosY));
|
||||
break;
|
||||
}
|
||||
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
mIsDragging = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
setMeasuredDimension(mHandleDrawable.getIntrinsicWidth(),
|
||||
mHandleDrawable.getIntrinsicHeight());
|
||||
}
|
||||
|
||||
public int getHandleHeight() {
|
||||
return mHandleHeight;
|
||||
}
|
||||
|
||||
public int getHandleWidth() {
|
||||
return mHandleWidth;
|
||||
}
|
||||
|
||||
public boolean isShowing() {
|
||||
if (mHandle != null)
|
||||
return mHandle.isShowing();
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isParentNull() {
|
||||
return this.getParent() == null;
|
||||
}
|
||||
|
||||
public boolean isDragging() {
|
||||
return mIsDragging;
|
||||
}
|
||||
|
||||
}
|
||||
1
termux-shared/.gitignore
vendored
Normal file
1
termux-shared/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
77
termux-shared/build.gradle
Normal file
77
termux-shared/build.gradle
Normal file
@@ -0,0 +1,77 @@
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'maven-publish'
|
||||
|
||||
android {
|
||||
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation "androidx.annotation:annotation:1.2.0"
|
||||
implementation "androidx.core:core:1.5.0-rc01"
|
||||
implementation "com.google.guava:guava:24.1-jre"
|
||||
implementation "io.noties.markwon:core:$markwonVersion"
|
||||
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
|
||||
implementation "io.noties.markwon:linkify:$markwonVersion"
|
||||
implementation "io.noties.markwon:recycler:$markwonVersion"
|
||||
|
||||
// Do not increment version higher than 2.5 or there
|
||||
// will be runtime exceptions on android < 8
|
||||
// due to missing classes like java.nio.file.Path.
|
||||
implementation "commons-io:commons-io:2.5"
|
||||
|
||||
implementation project(":terminal-view")
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion project.properties.minSdkVersion.toInteger()
|
||||
targetSdkVersion project.properties.targetSdkVersion.toInteger()
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
}
|
||||
|
||||
task sourceJar(type: Jar) {
|
||||
from android.sourceSets.main.java.srcDirs
|
||||
classifier "sources"
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
bar(MavenPublication) {
|
||||
groupId 'com.termux'
|
||||
artifactId 'termux-shared'
|
||||
version "0.113"
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
termux-shared/proguard-rules.pro
vendored
Normal file
10
termux-shared/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
-dontobfuscate
|
||||
#-renamesourcefileattribute SourceFile
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.termux.shared;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ExampleInstrumentedTest {
|
||||
@Test
|
||||
public void useAppContext() {
|
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||
assertEquals("com.termux.shared.test", appContext.getPackageName());
|
||||
}
|
||||
}
|
||||
3
termux-shared/src/main/AndroidManifest.xml
Normal file
3
termux-shared/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.termux.shared">
|
||||
</manifest>
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.termux.shared.crash;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
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 java.nio.charset.Charset;
|
||||
|
||||
/**
|
||||
* Catches uncaught exceptions and logs them.
|
||||
*/
|
||||
public class CrashHandler implements Thread.UncaughtExceptionHandler {
|
||||
|
||||
private final Context context;
|
||||
private final Thread.UncaughtExceptionHandler defaultUEH;
|
||||
|
||||
private static final String LOG_TAG = "CrashUtils";
|
||||
|
||||
private CrashHandler(final Context context) {
|
||||
this.context = context;
|
||||
this.defaultUEH = Thread.getDefaultUncaughtExceptionHandler();
|
||||
}
|
||||
|
||||
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
|
||||
logCrash(context,thread, throwable);
|
||||
defaultUEH.uncaughtException(thread, throwable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default uncaught crash handler of current thread to {@link CrashHandler}.
|
||||
*/
|
||||
public static void setCrashHandler(final Context context) {
|
||||
if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof CrashHandler)) {
|
||||
Thread.setDefaultUncaughtExceptionHandler(new CrashHandler(context));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a crash in the crash log file at
|
||||
* {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @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) {
|
||||
|
||||
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\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));
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
package com.termux.shared.data;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
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;
|
||||
|
||||
String prefix = "(truncated) ";
|
||||
|
||||
if (addPrefix)
|
||||
maxLength = maxLength - prefix.length();
|
||||
|
||||
if (maxLength < 0 || text.length() < maxLength) return text;
|
||||
|
||||
if (fromEnd) {
|
||||
text = text.substring(0, Math.min(text.length(), maxLength));
|
||||
} else {
|
||||
int cutOffIndex = text.length() - maxLength;
|
||||
|
||||
if (onNewline) {
|
||||
int nextNewlineIndex = text.indexOf('\n', cutOffIndex);
|
||||
if (nextNewlineIndex != -1 && nextNewlineIndex != text.length() - 1) {
|
||||
cutOffIndex = nextNewlineIndex + 1;
|
||||
}
|
||||
}
|
||||
text = text.substring(cutOffIndex);
|
||||
}
|
||||
|
||||
if (addPrefix)
|
||||
text = prefix + text;
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@code float} from a {@link String}.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
public static float getFloatFromString(String value, float def) {
|
||||
if (value == null) return def;
|
||||
|
||||
try {
|
||||
return Float.parseFloat(value);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@code int} from a {@link String}.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
public static int getIntFromString(String value, int def) {
|
||||
if (value == null) return def;
|
||||
|
||||
try {
|
||||
return Integer.parseInt(value);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
*
|
||||
* @param bundle The {@link Bundle} to get the value from.
|
||||
* @param key The key for the 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 stored in
|
||||
* {@link Bundle}, otherwise returns default if failed to read a valid value,
|
||||
* like in case of an exception.
|
||||
*/
|
||||
public static int getIntStoredAsStringFromBundle(Bundle bundle, String key, int def) {
|
||||
if (bundle == null) return def;
|
||||
return getIntFromString(bundle.getString(key, Integer.toString(def)), def);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* If value is not in the range [min, max], set it to either min or max.
|
||||
*/
|
||||
public static int clamp(int value, int min, int max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
/**
|
||||
* If value is not in the range [min, max], set it to default.
|
||||
*/
|
||||
public static float rangedOrDefault(float value, float def, float min, float max) {
|
||||
if (value < min || value > max)
|
||||
return def;
|
||||
else
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the object itself if it is not {@code null}, otherwise default.
|
||||
*
|
||||
* @param object The {@link Object} to check.
|
||||
* @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) {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
1627
termux-shared/src/main/java/com/termux/shared/file/FileUtils.java
Normal file
1627
termux-shared/src/main/java/com/termux/shared/file/FileUtils.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,414 @@
|
||||
/*
|
||||
* Copyright (c) 2008, 2013, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
package com.termux.shared.file.filesystem;
|
||||
|
||||
import android.os.Build;
|
||||
import android.system.StructStat;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.Set;
|
||||
import java.util.HashSet;
|
||||
|
||||
/**
|
||||
* Unix implementation of PosixFileAttributes.
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/sun/nio/fs/UnixFileAttributes.java
|
||||
*/
|
||||
|
||||
public class FileAttributes {
|
||||
private String filePath;
|
||||
private FileDescriptor fileDescriptor;
|
||||
|
||||
private int st_mode;
|
||||
private long st_ino;
|
||||
private long st_dev;
|
||||
private long st_rdev;
|
||||
private long st_nlink;
|
||||
private int st_uid;
|
||||
private int st_gid;
|
||||
private long st_size;
|
||||
private long st_blksize;
|
||||
private long st_blocks;
|
||||
private long st_atime_sec;
|
||||
private long st_atime_nsec;
|
||||
private long st_mtime_sec;
|
||||
private long st_mtime_nsec;
|
||||
private long st_ctime_sec;
|
||||
private long st_ctime_nsec;
|
||||
|
||||
// created lazily
|
||||
private volatile String owner;
|
||||
private volatile String group;
|
||||
private volatile FileKey key;
|
||||
|
||||
private FileAttributes(String filePath) {
|
||||
this.filePath = filePath;
|
||||
}
|
||||
|
||||
private FileAttributes(FileDescriptor fileDescriptor) {
|
||||
this.fileDescriptor = fileDescriptor;
|
||||
}
|
||||
|
||||
// get the FileAttributes for a given file
|
||||
public static FileAttributes get(String filePath, boolean followLinks) throws IOException {
|
||||
FileAttributes fileAttributes;
|
||||
|
||||
if (filePath == null || filePath.isEmpty())
|
||||
fileAttributes = new FileAttributes((String) null);
|
||||
else
|
||||
fileAttributes = new FileAttributes(new File(filePath).getAbsolutePath());
|
||||
|
||||
if (followLinks) {
|
||||
NativeDispatcher.stat(filePath, fileAttributes);
|
||||
} else {
|
||||
NativeDispatcher.lstat(filePath, fileAttributes);
|
||||
}
|
||||
|
||||
// Logger.logDebug(fileAttributes.toString());
|
||||
|
||||
return fileAttributes;
|
||||
}
|
||||
|
||||
// get the FileAttributes for an open file
|
||||
public static FileAttributes get(FileDescriptor fileDescriptor) throws IOException {
|
||||
FileAttributes fileAttributes = new FileAttributes(fileDescriptor);
|
||||
NativeDispatcher.fstat(fileDescriptor, fileAttributes);
|
||||
return fileAttributes;
|
||||
}
|
||||
|
||||
public String file() {
|
||||
if (filePath != null)
|
||||
return filePath;
|
||||
else if (fileDescriptor != null)
|
||||
return fileDescriptor.toString();
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
// package-private
|
||||
public boolean isSameFile(FileAttributes attrs) {
|
||||
return ((st_ino == attrs.st_ino) && (st_dev == attrs.st_dev));
|
||||
}
|
||||
|
||||
// package-private
|
||||
public int mode() {
|
||||
return st_mode;
|
||||
}
|
||||
|
||||
public long blksize() {
|
||||
return st_blksize;
|
||||
}
|
||||
|
||||
public long blocks() {
|
||||
return st_blocks;
|
||||
}
|
||||
|
||||
public long ino() {
|
||||
return st_ino;
|
||||
}
|
||||
|
||||
public long dev() {
|
||||
return st_dev;
|
||||
}
|
||||
|
||||
public long rdev() {
|
||||
return st_rdev;
|
||||
}
|
||||
|
||||
public long nlink() {
|
||||
return st_nlink;
|
||||
}
|
||||
|
||||
public int uid() {
|
||||
return st_uid;
|
||||
}
|
||||
|
||||
public int gid() {
|
||||
return st_gid;
|
||||
}
|
||||
|
||||
private static FileTime toFileTime(long sec, long nsec) {
|
||||
if (nsec == 0) {
|
||||
return FileTime.from(sec, TimeUnit.SECONDS);
|
||||
} else {
|
||||
// truncate to microseconds to avoid overflow with timestamps
|
||||
// way out into the future. We can re-visit this if FileTime
|
||||
// is updated to define a from(secs,nsecs) method.
|
||||
long micro = sec * 1000000L + nsec / 1000L;
|
||||
return FileTime.from(micro, TimeUnit.MICROSECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
public FileTime lastAccessTime() {
|
||||
return toFileTime(st_atime_sec, st_atime_nsec);
|
||||
}
|
||||
|
||||
public FileTime lastModifiedTime() {
|
||||
return toFileTime(st_mtime_sec, st_mtime_nsec);
|
||||
}
|
||||
|
||||
public FileTime lastChangeTime() {
|
||||
return toFileTime(st_ctime_sec, st_ctime_nsec);
|
||||
}
|
||||
|
||||
public FileTime creationTime() {
|
||||
return lastModifiedTime();
|
||||
}
|
||||
|
||||
public boolean isRegularFile() {
|
||||
return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFREG);
|
||||
}
|
||||
|
||||
public boolean isDirectory() {
|
||||
return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFDIR);
|
||||
}
|
||||
|
||||
public boolean isSymbolicLink() {
|
||||
return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFLNK);
|
||||
}
|
||||
|
||||
public boolean isCharacter() {
|
||||
return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFCHR);
|
||||
}
|
||||
|
||||
public boolean isFifo() {
|
||||
return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFIFO);
|
||||
}
|
||||
|
||||
public boolean isBlock() {
|
||||
return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFBLK);
|
||||
}
|
||||
|
||||
public boolean isOther() {
|
||||
int type = st_mode & UnixConstants.S_IFMT;
|
||||
return (type != UnixConstants.S_IFREG &&
|
||||
type != UnixConstants.S_IFDIR &&
|
||||
type != UnixConstants.S_IFLNK);
|
||||
}
|
||||
|
||||
public boolean isDevice() {
|
||||
int type = st_mode & UnixConstants.S_IFMT;
|
||||
return (type == UnixConstants.S_IFCHR ||
|
||||
type == UnixConstants.S_IFBLK ||
|
||||
type == UnixConstants.S_IFIFO);
|
||||
}
|
||||
|
||||
public long size() {
|
||||
return st_size;
|
||||
}
|
||||
|
||||
public FileKey fileKey() {
|
||||
if (key == null) {
|
||||
synchronized (this) {
|
||||
if (key == null) {
|
||||
key = new FileKey(st_dev, st_ino);
|
||||
}
|
||||
}
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
public String owner() {
|
||||
if (owner == null) {
|
||||
synchronized (this) {
|
||||
if (owner == null) {
|
||||
owner = Integer.toString(st_uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
return owner;
|
||||
}
|
||||
|
||||
public String group() {
|
||||
if (group == null) {
|
||||
synchronized (this) {
|
||||
if (group == null) {
|
||||
group = Integer.toString(st_gid);
|
||||
}
|
||||
}
|
||||
}
|
||||
return group;
|
||||
}
|
||||
|
||||
public Set<FilePermission> permissions() {
|
||||
int bits = (st_mode & UnixConstants.S_IAMB);
|
||||
HashSet<FilePermission> perms = new HashSet<>();
|
||||
|
||||
if ((bits & UnixConstants.S_IRUSR) > 0)
|
||||
perms.add(FilePermission.OWNER_READ);
|
||||
if ((bits & UnixConstants.S_IWUSR) > 0)
|
||||
perms.add(FilePermission.OWNER_WRITE);
|
||||
if ((bits & UnixConstants.S_IXUSR) > 0)
|
||||
perms.add(FilePermission.OWNER_EXECUTE);
|
||||
|
||||
if ((bits & UnixConstants.S_IRGRP) > 0)
|
||||
perms.add(FilePermission.GROUP_READ);
|
||||
if ((bits & UnixConstants.S_IWGRP) > 0)
|
||||
perms.add(FilePermission.GROUP_WRITE);
|
||||
if ((bits & UnixConstants.S_IXGRP) > 0)
|
||||
perms.add(FilePermission.GROUP_EXECUTE);
|
||||
|
||||
if ((bits & UnixConstants.S_IROTH) > 0)
|
||||
perms.add(FilePermission.OTHERS_READ);
|
||||
if ((bits & UnixConstants.S_IWOTH) > 0)
|
||||
perms.add(FilePermission.OTHERS_WRITE);
|
||||
if ((bits & UnixConstants.S_IXOTH) > 0)
|
||||
perms.add(FilePermission.OTHERS_EXECUTE);
|
||||
|
||||
return perms;
|
||||
}
|
||||
|
||||
public void loadFromStructStat(StructStat structStat) {
|
||||
this.st_mode = structStat.st_mode;
|
||||
this.st_ino = structStat.st_ino;
|
||||
this.st_dev = structStat.st_dev;
|
||||
this.st_rdev = structStat.st_rdev;
|
||||
this.st_nlink = structStat.st_nlink;
|
||||
this.st_uid = structStat.st_uid;
|
||||
this.st_gid = structStat.st_gid;
|
||||
this.st_size = structStat.st_size;
|
||||
this.st_blksize = structStat.st_blksize;
|
||||
this.st_blocks = structStat.st_blocks;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
this.st_atime_sec = structStat.st_atim.tv_sec;
|
||||
this.st_atime_nsec = structStat.st_atim.tv_nsec;
|
||||
this.st_mtime_sec = structStat.st_mtim.tv_sec;
|
||||
this.st_mtime_nsec = structStat.st_mtim.tv_nsec;
|
||||
this.st_ctime_sec = structStat.st_ctim.tv_sec;
|
||||
this.st_ctime_nsec = structStat.st_ctim.tv_nsec;
|
||||
} else {
|
||||
this.st_atime_sec = structStat.st_atime;
|
||||
this.st_atime_nsec = 0;
|
||||
this.st_mtime_sec = structStat.st_mtime;
|
||||
this.st_mtime_nsec = 0;
|
||||
this.st_ctime_sec = structStat.st_ctime;
|
||||
this.st_ctime_nsec = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public String getFileString() {
|
||||
return "File: `" + file() + "`";
|
||||
}
|
||||
|
||||
public String getTypeString() {
|
||||
return "Type: `" + FileTypes.getFileType(this).getName() + "`";
|
||||
}
|
||||
|
||||
public String getSizeString() {
|
||||
return "Size: `" + size() + "`";
|
||||
}
|
||||
|
||||
public String getBlocksString() {
|
||||
return "Blocks: `" + blocks() + "`";
|
||||
}
|
||||
|
||||
public String getIOBlockString() {
|
||||
return "IO Block: `" + blksize() + "`";
|
||||
}
|
||||
|
||||
public String getDeviceString() {
|
||||
return "Device: `" + Long.toHexString(st_dev) + "`";
|
||||
}
|
||||
|
||||
public String getInodeString() {
|
||||
return "Inode: `" + st_ino + "`";
|
||||
}
|
||||
|
||||
public String getLinksString() {
|
||||
return "Links: `" + nlink() + "`";
|
||||
}
|
||||
|
||||
public String getDeviceTypeString() {
|
||||
return "Device Type: `" + rdev() + "`";
|
||||
}
|
||||
|
||||
public String getOwnerString() {
|
||||
return "Owner: `" + owner() + "`";
|
||||
}
|
||||
|
||||
public String getGroupString() {
|
||||
return "Group: `" + group() + "`";
|
||||
}
|
||||
|
||||
public String getPermissionString() {
|
||||
return "Permissions: `" + FilePermissions.toString(permissions()) + "`";
|
||||
}
|
||||
|
||||
public String getAccessTimeString() {
|
||||
return "Access Time: `" + lastAccessTime() + "`";
|
||||
}
|
||||
|
||||
public String getModifiedTimeString() {
|
||||
return "Modified Time: `" + lastModifiedTime() + "`";
|
||||
}
|
||||
|
||||
public String getChangeTimeString() {
|
||||
return "Change Time: `" + lastChangeTime() + "`";
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return getFileAttributesLogString(this);
|
||||
}
|
||||
|
||||
public static String getFileAttributesLogString(final FileAttributes fileAttributes) {
|
||||
if (fileAttributes == null) return "null";
|
||||
|
||||
StringBuilder logString = new StringBuilder();
|
||||
|
||||
logString.append(fileAttributes.getFileString());
|
||||
|
||||
logString.append("\n").append(fileAttributes.getTypeString());
|
||||
|
||||
logString.append("\n").append(fileAttributes.getSizeString());
|
||||
logString.append("\n").append(fileAttributes.getBlocksString());
|
||||
logString.append("\n").append(fileAttributes.getIOBlockString());
|
||||
|
||||
logString.append("\n").append(fileAttributes.getDeviceString());
|
||||
logString.append("\n").append(fileAttributes.getInodeString());
|
||||
logString.append("\n").append(fileAttributes.getLinksString());
|
||||
|
||||
if (fileAttributes.isBlock() || fileAttributes.isCharacter())
|
||||
logString.append("\n").append(fileAttributes.getDeviceTypeString());
|
||||
|
||||
logString.append("\n").append(fileAttributes.getOwnerString());
|
||||
logString.append("\n").append(fileAttributes.getGroupString());
|
||||
logString.append("\n").append(fileAttributes.getPermissionString());
|
||||
|
||||
logString.append("\n").append(fileAttributes.getAccessTimeString());
|
||||
logString.append("\n").append(fileAttributes.getModifiedTimeString());
|
||||
logString.append("\n").append(fileAttributes.getChangeTimeString());
|
||||
|
||||
return logString.toString();
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user