Compare commits
338 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9272a757af | ||
|
|
d49fd6b00c | ||
|
|
e0ad9ff573 | ||
|
|
7aefd94369 | ||
|
|
dc8bdfe675 | ||
|
|
c6b4114f86 | ||
|
|
cce6dfed22 | ||
|
|
56c3826680 | ||
|
|
2cf21c8409 | ||
|
|
4361c5e0c5 | ||
|
|
a53cc88688 | ||
|
|
48161816e0 | ||
|
|
eabbda8efd | ||
|
|
b90d59479a | ||
|
|
dccd155ba6 | ||
|
|
78be0e793e | ||
|
|
e547c15481 | ||
|
|
c621c35827 | ||
|
|
886e52dcff | ||
|
|
8e4da6cbcd | ||
|
|
bde9d01f76 | ||
|
|
5a511a2ba3 | ||
|
|
5c50964b1f | ||
|
|
dea8c9879e | ||
|
|
2034121798 | ||
|
|
23a900c433 | ||
|
|
93a7525d9b | ||
|
|
5670128236 | ||
|
|
dfd32435af | ||
|
|
49265160f8 | ||
|
|
70e1accafe | ||
|
|
1c7f9166f2 | ||
|
|
553913cde1 | ||
|
|
6bca378cec | ||
|
|
12f910c32d | ||
|
|
94c5f3674a | ||
|
|
28b9f93d13 | ||
|
|
69bebb5916 | ||
|
|
321350256e | ||
|
|
e5a9b99afe | ||
|
|
00f805f7ec | ||
|
|
d3c34ad1f5 | ||
|
|
59877a08d1 | ||
|
|
9c92251595 | ||
|
|
e408fdcc08 | ||
|
|
53c1a49b5b | ||
|
|
2aafcf8435 | ||
|
|
1c1af34374 | ||
|
|
52f18a73fb | ||
|
|
28f81f2cc7 | ||
|
|
4494bc66e4 | ||
|
|
679e0de044 | ||
|
|
80b495e50b | ||
|
|
69e5deedc7 | ||
|
|
7f36d7bbd0 | ||
|
|
b7b12ebe84 | ||
|
|
f77c88633e | ||
|
|
5f2ccca423 | ||
|
|
f0f6927273 | ||
|
|
0fb18c0c8b | ||
|
|
4dfed3320e | ||
|
|
7ac62c9840 | ||
|
|
fd80cdaf23 | ||
|
|
19c690d02b | ||
|
|
e119d34bca | ||
|
|
f545ebf0bd | ||
|
|
0b4bbaf23d | ||
|
|
e7dd0eeebe | ||
|
|
7ef9255437 | ||
|
|
7225e2b379 | ||
|
|
1ad038ece5 | ||
|
|
cb8b0225ca | ||
|
|
7620800cd5 | ||
|
|
6837db0015 | ||
|
|
e08e3b536e | ||
|
|
b711a467c1 | ||
|
|
d736b1eba5 | ||
|
|
58d577066a | ||
|
|
89a1e02713 | ||
|
|
6524a619f6 | ||
|
|
f8ccbb4953 | ||
|
|
31298b8857 | ||
|
|
11f5c0afd1 | ||
|
|
27dc211e2d | ||
|
|
2f828255ee | ||
|
|
339b2a24a2 | ||
|
|
6de3713049 | ||
|
|
79df863b75 | ||
|
|
af115c9966 | ||
|
|
1e30022ce7 | ||
|
|
4629276500 | ||
|
|
d42514d8c9 | ||
|
|
90c9a7b3bc | ||
|
|
e6dac93352 | ||
|
|
e4e638bd31 | ||
|
|
fe8c3ba216 | ||
|
|
4ecea144bb | ||
|
|
116b9b42d8 | ||
|
|
39c69db820 | ||
|
|
4d1851e6be | ||
|
|
596aa56b38 | ||
|
|
4850678d55 | ||
|
|
bc52a4e90c | ||
|
|
3e7b3604a4 | ||
|
|
f3f58c8fc7 | ||
|
|
4711094614 | ||
|
|
42ad3723fd | ||
|
|
b268b6edf7 | ||
|
|
b84854af92 | ||
|
|
cfebb3358d | ||
|
|
93e1b13278 | ||
|
|
0d4bfb7bd5 | ||
|
|
0aa5a123b7 | ||
|
|
2e156d4621 | ||
|
|
fdcf6cb6e1 | ||
|
|
01f2ed0892 | ||
|
|
c9abfe5438 | ||
|
|
8f9771adce | ||
|
|
b34f60b1b0 | ||
|
|
0fe608f91e | ||
|
|
5d911ef93f | ||
|
|
1d06ff9bf0 | ||
|
|
107927f5a1 | ||
|
|
d6eb5e3511 | ||
|
|
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 | ||
|
|
edcebf1336 | ||
|
|
fcfd131ccd | ||
|
|
ce5e9a9042 | ||
|
|
7884cb3bea | ||
|
|
d8fcc1f221 | ||
|
|
397a78f248 | ||
|
|
8baf53b09a | ||
|
|
4139bf9424 | ||
|
|
94deecb7b1 | ||
|
|
a4381b7827 | ||
|
|
866da75fa9 | ||
|
|
2b6e9ade07 | ||
|
|
6d1b0efd3b | ||
|
|
e03858f065 | ||
|
|
01929397cf | ||
|
|
496da3f877 | ||
|
|
407e4e003a | ||
|
|
fec61d315f | ||
|
|
f3a3a89f93 | ||
|
|
5b084f0851 | ||
|
|
a7eb173178 | ||
|
|
fa5117a098 | ||
|
|
2ef45c85b2 | ||
|
|
58b7a26b33 | ||
|
|
6e2a2ed946 | ||
|
|
7be1fe5555 | ||
|
|
0e94d52094 | ||
|
|
096dedffb1 | ||
|
|
b5d491a54c | ||
|
|
f6822d6c24 | ||
|
|
32c3ffd57b | ||
|
|
92570bee06 | ||
|
|
05bb399893 | ||
|
|
fe584940e1 | ||
|
|
78cdaef6d2 | ||
|
|
7b4a69f839 | ||
|
|
831aa69da8 | ||
|
|
a56cba6843 | ||
|
|
9228982632 | ||
|
|
38114784f1 | ||
|
|
b805f1486c | ||
|
|
7d31b7f480 | ||
|
|
a0298285e3 | ||
|
|
538a1d5cdf | ||
|
|
f1e973f0d2 | ||
|
|
b467b68f7b | ||
|
|
b895cbbb1e | ||
|
|
fc30eba247 | ||
|
|
b1d4c0c7fe | ||
|
|
7fe5bd32c8 | ||
|
|
43bbef9a11 | ||
|
|
eaea0f74a5 | ||
|
|
cb13a5a531 | ||
|
|
d267843e36 | ||
|
|
5ca67dd885 | ||
|
|
6b0d531758 | ||
|
|
1b3283bd69 | ||
|
|
2820f6a7b8 | ||
|
|
8925ae67bb | ||
|
|
4066c5df42 | ||
|
|
20aac6aa72 | ||
|
|
e1f799f9a1 | ||
|
|
4479de4b9b | ||
|
|
3a8f53a54c | ||
|
|
2f04a6186b | ||
|
|
3bb2849a88 |
8
.gitattributes
vendored
@@ -1,5 +1,5 @@
|
|||||||
* text=auto
|
* text=auto
|
||||||
*.bat eol=crlf
|
*.bat text eol=crlf
|
||||||
*.gradle eol=lf
|
*.gradle text eol=lf
|
||||||
*.mk eol=lf
|
*.mk text eol=lf
|
||||||
*.sh eol=lf
|
*.sh text eol=lf
|
||||||
|
|||||||
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
patreon: termux
|
||||||
|
custom: https://paypal.me/fornwall
|
||||||
38
.github/workflows/debug_build.yml
vendored
@@ -19,8 +19,38 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
./gradlew assembleDebug
|
./gradlew assembleDebug
|
||||||
- name: Store generated APK file
|
- name: Store generated universal APK file
|
||||||
uses: actions/upload-artifact@v1
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: termux-app
|
name: termux-app-universal
|
||||||
path: ./app/build/outputs/apk/debug/app-debug.apk
|
path: |
|
||||||
|
./app/build/outputs/apk/debug/app-universal-debug.apk
|
||||||
|
./app/build/outputs/apk/debug/output-metadata.json
|
||||||
|
- name: Store generated arm64-v8a APK file
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: termux-app-arm64-v8a
|
||||||
|
path: |
|
||||||
|
./app/build/outputs/apk/debug/app-arm64-v8a-debug.apk
|
||||||
|
./app/build/outputs/apk/debug/output-metadata.json
|
||||||
|
- name: Store generated armeabi-v7a APK file
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: termux-app-armeabi-v7a
|
||||||
|
path: |
|
||||||
|
./app/build/outputs/apk/debug/app-armeabi-v7a-debug.apk
|
||||||
|
./app/build/outputs/apk/debug/output-metadata.json
|
||||||
|
- name: Store generated x86_64 APK file
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: termux-app-x86_64
|
||||||
|
path: |
|
||||||
|
./app/build/outputs/apk/debug/app-x86_64-debug.apk
|
||||||
|
./app/build/outputs/apk/debug/output-metadata.json
|
||||||
|
- name: Store generated x86 APK file
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: termux-app-x86
|
||||||
|
path: |
|
||||||
|
./app/build/outputs/apk/debug/app-x86-debug.apk
|
||||||
|
./app/build/outputs/apk/debug/output-metadata.json
|
||||||
|
|||||||
22
.github/workflows/trigger_library_builds_on_jitpack.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: Trigger Termux Library Builds on Jitpack
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types:
|
||||||
|
- published
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Set tag
|
||||||
|
id: vars
|
||||||
|
run: echo ::set-output name=tag::${GITHUB_REF/refs\/tags\/v/}
|
||||||
|
- name: Trigger termux library builds on jitpack
|
||||||
|
env:
|
||||||
|
RELEASE_VERSION: ${{ steps.vars.outputs.tag }}
|
||||||
|
run: |
|
||||||
|
echo "Triggering termux library builds on jitpack for $RELEASE_VERSION"
|
||||||
|
curl --max-time 600 https://jitpack.io/com/termux/termux-app/terminal-emulator/$RELEASE_VERSION/terminal-emulator-$RELEASE_VERSION.pom
|
||||||
|
curl --max-time 600 https://jitpack.io/com/termux/termux-app/terminal-view/$RELEASE_VERSION/terminal-view-$RELEASE_VERSION.pom
|
||||||
|
curl --max-time 600 https://jitpack.io/com/termux/termux-app/termux-shared/$RELEASE_VERSION/termux-shared-$RELEASE_VERSION.pom
|
||||||
1
.gitignore
vendored
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
# Built application files
|
# Built application files
|
||||||
build/
|
build/
|
||||||
|
release/
|
||||||
*.apk
|
*.apk
|
||||||
*.so
|
*.so
|
||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
Released under [the GPLv3 license](https://www.gnu.org/licenses/gpl.html).
|
The `termux/termux-app` repository is released under [GPLv3 only](https://www.gnu.org/licenses/gpl-3.0.html) license.
|
||||||
|
|
||||||
Contains code from `Terminal Emulator for Android` by which is released under [the Apache License 2.0](https://www.apache.org/licenses/).
|
### Exceptions
|
||||||
|
|
||||||
|
- [Terminal Emulator for Android](https://github.com/jackpal/Android-Terminal-Emulator) code is used which is released under [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). Check [`terminal-view`](terminal-view) and [`terminal-emulator`](terminal-emulator) libraries.
|
||||||
|
- Check [`termux-shared/LICENSE.md`](termux-shared/LICENSE.md) for `termux-shared` library related exceptions.
|
||||||
|
|||||||
155
README.md
@@ -3,63 +3,146 @@
|
|||||||
[](https://github.com/termux/termux-app/actions)
|
[](https://github.com/termux/termux-app/actions)
|
||||||
[](https://github.com/termux/termux-app/actions)
|
[](https://github.com/termux/termux-app/actions)
|
||||||
[](https://gitter.im/termux/termux)
|
[](https://gitter.im/termux/termux)
|
||||||
|
[](https://jitpack.io/#termux/termux-app)
|
||||||
|
|
||||||
|
|
||||||
[Termux](https://termux.com) is an Android terminal application and Linux environment.
|
[Termux](https://termux.com) is an Android terminal application and Linux environment.
|
||||||
|
|
||||||
- [Termux Reddit community](https://reddit.com/r/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).
|
||||||
- [Termux Wiki](https://wiki.termux.com/wiki/)
|
|
||||||
- [Termux Twitter](http://twitter.com/termux/)
|
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 maintainers for implementing new features, fixing bugs and reviewing pull requests since the current one (@fornwall) is inactive.**
|
||||||
|
|
||||||
|
Issue https://github.com/termux/termux-app/issues/1072 needs extra attention.
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
##
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Termux application can be obtained from:
|
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).
|
||||||
|
|
||||||
- [Google Play](https://play.google.com/store/apps/details?id=com.termux)
|
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 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.
|
||||||
- [F-Droid](https://f-droid.org/en/packages/com.termux/)
|
|
||||||
- [Kali Nethunter Store](https://store.nethunter.com/en/packages/com.termux/)
|
|
||||||
|
|
||||||
Additionally we provide per-commit debug builds for those who want to try
|
If you wish to install from a different source, then you must uninstall **any and all existing Termux or its plugin app APKs** from your device first, then install all new APKs from the same new source. Check [Uninstallation](#Uninstallation) section for details. You may also want to consider [Backing up Termux](https://wiki.termux.com/wiki/Backing_up_Termux) before the uninstallation.
|
||||||
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.
|
|
||||||
|
|
||||||
Signature keys of all offered builds are different. Before you switch the
|
### F-Droid
|
||||||
installation source, you will have to uninstall the Termux application and
|
|
||||||
all currently installed plugins.
|
|
||||||
|
|
||||||
## Terminal resources
|
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 the building and publishing of the 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 labelled `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 the uninstallation.
|
||||||
|
|
||||||
|
To uninstall Termux completely, you must uninstall **any and all existing Termux or its plugin app APKs** listed in [Termux App and Plugins](#Termux-App-and-Plugins).
|
||||||
|
|
||||||
|
Go to `Android Settings` -> `Applications` and then look for those apps. You can also use the search feature if it’s available on your device and search `termux` in the applications list.
|
||||||
|
|
||||||
|
Even if you think you have not installed any of the plugins, it’s 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)
|
- [XTerm control sequences](http://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
|
||||||
- [vt100.net](http://vt100.net/)
|
- [vt100.net](http://vt100.net/)
|
||||||
- [Terminal codes (ANSI and terminfo equivalents)](http://wiki.bash-hackers.org/scripting/terminalcodes)
|
- [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.
|
- 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).
|
||||||
[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),
|
- 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)).
|
||||||
[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),
|
- 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).
|
||||||
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),
|
- 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).
|
||||||
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.
|
- xterm: The grandfather of terminal emulators. [Source](http://invisible-island.net/datafiles/release/xterm.tar.gz).
|
||||||
[Source](http://invisible-island.net/datafiles/release/xterm.tar.gz).
|
|
||||||
|
|
||||||
- Connectbot: Android SSH client. [Source](https://github.com/connectbot/connectbot)
|
- Connectbot: Android SSH client. [Source](https://github.com/connectbot/connectbot)
|
||||||
|
|
||||||
- Android Terminal Emulator: Android terminal app which Termux terminal handling
|
- Android Terminal Emulator: Android terminal app which Termux terminal handling is based on. Inactive. [Source](https://github.com/jackpal/Android-Terminal-Emulator).
|
||||||
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 the Termux app and its plugins. It was created to allow for the removal of all hardcoded paths in the 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.
|
||||||
|
|
||||||
|
|||||||
@@ -4,21 +4,39 @@ plugins {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
||||||
ndkVersion project.properties.ndkVersion
|
ndkVersion = System.getenv("JITPACK_NDK_VERSION") ?: project.properties.ndkVersion
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "androidx.annotation:annotation:1.1.0"
|
implementation "androidx.annotation:annotation:1.2.0"
|
||||||
|
implementation "androidx.core:core:1.6.0-rc01"
|
||||||
|
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
|
||||||
|
implementation "androidx.preference:preference:1.1.1"
|
||||||
implementation "androidx.viewpager:viewpager:1.0.0"
|
implementation "androidx.viewpager:viewpager:1.0.0"
|
||||||
implementation "androidx.drawerlayout:drawerlayout:1.1.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(":terminal-view")
|
||||||
|
implementation project(":termux-shared")
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.termux"
|
applicationId "com.termux"
|
||||||
minSdkVersion project.properties.minSdkVersion.toInteger()
|
minSdkVersion project.properties.minSdkVersion.toInteger()
|
||||||
targetSdkVersion project.properties.targetSdkVersion.toInteger()
|
targetSdkVersion project.properties.targetSdkVersion.toInteger()
|
||||||
versionCode 102
|
versionCode 117
|
||||||
versionName "0.102"
|
versionName "0.117"
|
||||||
|
|
||||||
|
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 {
|
externalNativeBuild {
|
||||||
ndkBuild {
|
ndkBuild {
|
||||||
@@ -26,10 +44,14 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ndk {
|
splits {
|
||||||
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
abi {
|
||||||
|
enable gradle.startParameter.taskNames.any { it.contains("Debug") }
|
||||||
|
reset ()
|
||||||
|
include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
||||||
|
universalApk true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
@@ -64,6 +86,10 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
disable 'ProtectedPermissions'
|
||||||
|
}
|
||||||
|
|
||||||
testOptions {
|
testOptions {
|
||||||
unitTests {
|
unitTests {
|
||||||
includeAndroidResources = true
|
includeAndroidResources = true
|
||||||
@@ -72,8 +98,8 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation 'junit:junit:4.13'
|
testImplementation "junit:junit:4.13.2"
|
||||||
testImplementation 'org.robolectric:robolectric:4.3.1'
|
testImplementation "org.robolectric:robolectric:4.4"
|
||||||
}
|
}
|
||||||
|
|
||||||
task versionName {
|
task versionName {
|
||||||
@@ -82,7 +108,7 @@ task versionName {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def downloadBootstrap(String arch, String expectedChecksum, int version) {
|
def downloadBootstrap(String arch, String expectedChecksum, String version) {
|
||||||
def digest = java.security.MessageDigest.getInstance("SHA-256")
|
def digest = java.security.MessageDigest.getInstance("SHA-256")
|
||||||
|
|
||||||
def localUrl = "src/main/cpp/bootstrap-" + arch + ".zip"
|
def localUrl = "src/main/cpp/bootstrap-" + arch + ".zip"
|
||||||
@@ -104,7 +130,7 @@ def downloadBootstrap(String arch, String expectedChecksum, int version) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def remoteUrl = "https://bintray.com/termux/bootstrap/download_file?file_path=bootstrap-" + arch + "-v" + version + ".zip"
|
def remoteUrl = "https://github.com/termux/termux-packages/releases/download/bootstrap-" + version + "/bootstrap-" + arch + ".zip"
|
||||||
logger.quiet("Downloading " + remoteUrl + " ...")
|
logger.quiet("Downloading " + remoteUrl + " ...")
|
||||||
|
|
||||||
file.parentFile.mkdirs()
|
file.parentFile.mkdirs()
|
||||||
@@ -131,13 +157,13 @@ clean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task downloadBootstraps(){
|
task downloadBootstraps() {
|
||||||
doLast {
|
doLast {
|
||||||
def version = 31
|
def version = "2021.06.30-r1"
|
||||||
downloadBootstrap("aarch64", "e9149cb01735f04b180434093dfb8e703015f8a66044acaead7cbff1e536a990", version)
|
downloadBootstrap("aarch64", "ce56ce9a4e8845bd1d35cc2695bbdd636c72625ee10ce21c9b98ab38ebbee5ab", version)
|
||||||
downloadBootstrap("arm", "8e5776074c58b3e94b1336f2ec0e840057fce9c089faee6683ae5c136441da7b", version)
|
downloadBootstrap("arm", "537e81951c7d3d3f3def9ce6778e1032457488e21edb2c037a1e0e680c39e747", version)
|
||||||
downloadBootstrap("i686", "f89be9d0197fb9c6b498922ff0f95562fd17b63c934617858f959b8e452ade27", version)
|
downloadBootstrap("i686", "3c2ca858c0225671c00c44ac182e31819ffa93ec624e95e02824e7d6d30ca1b4", version)
|
||||||
downloadBootstrap("x86_64", "9aa97647afc085fae4e8485458a7d15f23db6e1e3601727f014af8b8eb4519a9", version)
|
downloadBootstrap("x86_64", "93c50d36b45bca42bb014395e8e184e5b540adcad5d4e215f7e64ebf0d655d2b", version)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
10
app/proguard-rules.pro
vendored
@@ -7,5 +7,11 @@
|
|||||||
# For more details, see
|
# For more details, see
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
-renamesourcefileattribute SourceFile
|
-dontobfuscate
|
||||||
-keepattributes SourceFile,LineNumberTable
|
#-renamesourcefileattribute SourceFile
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# Temp fix for androidx.window:window:1.0.0-alpha09 imported by termux-shared
|
||||||
|
# https://issuetracker.google.com/issues/189001730
|
||||||
|
# https://android-review.googlesource.com/c/platform/frameworks/support/+/1757630
|
||||||
|
-keep class androidx.window.** { *; }
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.termux"
|
package="com.termux"
|
||||||
android:installLocation="internalOnly"
|
android:installLocation="internalOnly"
|
||||||
android:sharedUserId="com.termux"
|
android:sharedUserId="${TERMUX_PACKAGE_NAME}"
|
||||||
android:sharedUserLabel="@string/shared_user_label" >
|
android:sharedUserLabel="@string/shared_user_label">
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
<uses-feature
|
||||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
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"
|
<permission
|
||||||
android:label="@string/run_command_permission_label"
|
android:name="${TERMUX_PACKAGE_NAME}.permission.RUN_COMMAND"
|
||||||
android:description="@string/run_command_permission_description"
|
android:description="@string/permission_run_command_description"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/permission_run_command_label"
|
||||||
android:protectionLevel="dangerous" />
|
android:protectionLevel="dangerous" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
@@ -20,59 +26,101 @@
|
|||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
<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.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
|
<application
|
||||||
android:extractNativeLibs="true"
|
android:name=".app.TermuxApplication"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:icon="@drawable/ic_launcher"
|
|
||||||
android:banner="@drawable/banner"
|
android:banner="@drawable/banner"
|
||||||
|
android:extractNativeLibs="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/application_name"
|
android:label="@string/application_name"
|
||||||
android:theme="@style/Theme.Termux"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="false" >
|
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." -->
|
This (or rather, value 2.1 or higher) is needed to make the Samsung Galaxy S8
|
||||||
<meta-data android:name="android.max_aspect" android:value="10.0" />
|
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
|
<activity
|
||||||
android:name="com.termux.app.TermuxActivity"
|
android:name=".app.TermuxActivity"
|
||||||
android:label="@string/application_name"
|
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|uiMode|keyboard|keyboardHidden|navigation"
|
||||||
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|uiMode|keyboard|keyboardHidden|navigation"
|
android:label="@string/application_name"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true">
|
||||||
android:windowSoftInputMode="adjustResize|stateAlwaysVisible" >
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
</intent-filter>
|
</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>
|
||||||
|
|
||||||
<activity
|
<activity-alias
|
||||||
android:name="com.termux.app.TermuxHelpActivity"
|
android:name=".HomeActivity"
|
||||||
android:exported="false"
|
android:targetActivity=".app.TermuxActivity">
|
||||||
android:theme="@android:style/Theme.Material.Light.DarkActionBar"
|
|
||||||
android:parentActivityName=".app.TermuxActivity"
|
<!-- Launch activity automatically on boot on Android Things devices -->
|
||||||
android:resizeableActivity="true"
|
<intent-filter>
|
||||||
android:label="@string/application_name" />
|
<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
|
<activity
|
||||||
android:name="com.termux.filepicker.TermuxFileReceiverActivity"
|
android:name=".app.activities.HelpActivity"
|
||||||
|
android:exported="false"
|
||||||
android:label="@string/application_name"
|
android:label="@string/application_name"
|
||||||
android:taskAffinity="com.termux.filereceiver"
|
android:parentActivityName=".app.TermuxActivity"
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:resizeableActivity="true"
|
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=".shared.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. -->
|
<!-- Accept multiple file types when sending. -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND"/>
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
<data android:mimeType="application/*" />
|
<data android:mimeType="application/*" />
|
||||||
<data android:mimeType="audio/*" />
|
<data android:mimeType="audio/*" />
|
||||||
<data android:mimeType="image/*" />
|
<data android:mimeType="image/*" />
|
||||||
@@ -81,36 +129,25 @@
|
|||||||
<data android:mimeType="text/*" />
|
<data android:mimeType="text/*" />
|
||||||
<data android:mimeType="video/*" />
|
<data android:mimeType="video/*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<!-- Be more restrictive for viewing files, restricting ourselves to text files. -->
|
<!-- Accept multiple file types to let Termux be usable as generic file viewer. -->
|
||||||
<intent-filter tools:ignore="AppLinkUrlError">
|
<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" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
|
<data android:mimeType="application/*" />
|
||||||
|
<data android:mimeType="audio/*" />
|
||||||
|
<data android:mimeType="image/*" />
|
||||||
<data android:mimeType="text/*" />
|
<data android:mimeType="text/*" />
|
||||||
<data android:mimeType="application/*log*" />
|
<data android:mimeType="video/*" />
|
||||||
<data android:mimeType="application/json" />
|
|
||||||
<data android:mimeType="application/*xml*" />
|
|
||||||
<data android:mimeType="application/*latex*" />
|
|
||||||
<data android:mimeType="application/javascript" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</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
|
<provider
|
||||||
android:name=".filepicker.TermuxDocumentsProvider"
|
android:name=".filepicker.TermuxDocumentsProvider"
|
||||||
android:authorities="com.termux.documents"
|
android:authorities="${TERMUX_PACKAGE_NAME}.documents"
|
||||||
android:grantUriPermissions="true"
|
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
android:grantUriPermissions="true"
|
||||||
android:permission="android.permission.MANAGE_DOCUMENTS">
|
android:permission="android.permission.MANAGE_DOCUMENTS">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
||||||
@@ -118,27 +155,32 @@
|
|||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="com.termux.app.TermuxService"
|
android:name=".app.TermuxService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".app.RunCommandService"
|
android:name=".app.RunCommandService"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:permission="com.termux.permission.RUN_COMMAND" >
|
android:permission="${TERMUX_PACKAGE_NAME}.permission.RUN_COMMAND">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.termux.RUN_COMMAND" />
|
<action android:name="${TERMUX_PACKAGE_NAME}.RUN_COMMAND" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<receiver android:name=".app.TermuxOpenReceiver" />
|
<receiver android:name=".app.TermuxOpenReceiver" />
|
||||||
|
|
||||||
<provider android:authorities="com.termux.files"
|
<provider
|
||||||
android:readPermission="android.permission.permRead"
|
android:name=".app.TermuxOpenReceiver$ContentProvider"
|
||||||
android:exported="true"
|
android:authorities="${TERMUX_PACKAGE_NAME}.files"
|
||||||
android:grantUriPermissions="true"
|
android:exported="true"
|
||||||
android:name="com.termux.app.TermuxOpenReceiver$ContentProvider" />
|
android:grantUriPermissions="true"
|
||||||
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
android:readPermission="android.permission.permRead" />
|
||||||
<meta-data android:name="com.samsung.android.multidisplay.keep_process_alive" android:value="true"/>
|
|
||||||
|
<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>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1,240 +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 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("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:" + TermuxService.PREFIX_PATH + "/bin/applets");
|
|
||||||
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,40 @@
|
|||||||
package com.termux.app;
|
package com.termux.app;
|
||||||
|
|
||||||
import android.app.Notification;
|
import android.app.Notification;
|
||||||
import android.app.NotificationChannel;
|
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
|
import com.termux.shared.data.DataUtils;
|
||||||
import java.io.File;
|
import com.termux.shared.data.IntentUtils;
|
||||||
import java.io.FileInputStream;
|
import com.termux.shared.file.TermuxFileUtils;
|
||||||
import java.io.InputStreamReader;
|
import com.termux.shared.file.filesystem.FileType;
|
||||||
import java.nio.charset.StandardCharsets;
|
import com.termux.shared.models.errors.Errno;
|
||||||
import java.util.Properties;
|
import com.termux.shared.models.errors.Error;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE;
|
||||||
|
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||||
|
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.models.ExecutionCommand;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When allow-external-apps property is set to "true" in ~/.termux/termux.properties, Termux
|
* A service that receives {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent from third party apps and
|
||||||
* is able to process execute intents sent by third-party applications.
|
* 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
|
* Check https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent for more info.
|
||||||
* 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'
|
|
||||||
*/
|
*/
|
||||||
public class RunCommandService extends Service {
|
public class RunCommandService extends Service {
|
||||||
|
|
||||||
public static final String RUN_COMMAND_ACTION = "com.termux.RUN_COMMAND";
|
private static final String LOG_TAG = "RunCommandService";
|
||||||
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;
|
|
||||||
|
|
||||||
class LocalBinder extends Binder {
|
class LocalBinder extends Binder {
|
||||||
public final RunCommandService service = RunCommandService.this;
|
public final RunCommandService service = RunCommandService.this;
|
||||||
@@ -84,30 +49,176 @@ public class RunCommandService extends Service {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
|
Logger.logVerbose(LOG_TAG, "onCreate");
|
||||||
runStartForeground();
|
runStartForeground();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
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
|
// Run again in case service is already started and onCreate() is not called
|
||||||
runStartForeground();
|
runStartForeground();
|
||||||
|
|
||||||
if (allowExternalApps() && RUN_COMMAND_ACTION.equals(intent.getAction())) {
|
ExecutionCommand executionCommand = new ExecutionCommand();
|
||||||
Uri programUri = new Uri.Builder().scheme("com.termux.file").path(parsePath(intent.getStringExtra(RUN_COMMAND_PATH))).build();
|
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);
|
Error error;
|
||||||
execIntent.setClass(this, TermuxService.class);
|
String errmsg;
|
||||||
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));
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
// If invalid action passed, then just return
|
||||||
this.startForegroundService(execIntent);
|
if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) {
|
||||||
} else {
|
errmsg = this.getString(R.string.error_run_command_service_invalid_intent_action, intent.getAction());
|
||||||
this.startService(execIntent);
|
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||||
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||||
|
return Service.START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
String executableExtra = executionCommand.executable = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH, null);
|
||||||
|
executionCommand.arguments = IntentUtils.getStringArrayExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS, null);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If intent was sent with `am` command, then normal comma characters may have been replaced
|
||||||
|
* with alternate characters if a normal comma existed in an argument itself to prevent it
|
||||||
|
* splitting into multiple arguments by `am` command.
|
||||||
|
* If `tudo` or `sudo` are used, then simply using their `-r` and `--comma-alternative` command
|
||||||
|
* options can be used without passing the below extras, but native supports is helpful if
|
||||||
|
* they are not being used.
|
||||||
|
* https://github.com/agnostic-apollo/tudo#passing-arguments-using-run_command-intent
|
||||||
|
* https://android.googlesource.com/platform/frameworks/base/+/21bdaf1/cmds/am/src/com/android/commands/am/Am.java#572
|
||||||
|
*/
|
||||||
|
boolean replaceCommaAlternativeCharsInArguments = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS, false);
|
||||||
|
if (replaceCommaAlternativeCharsInArguments) {
|
||||||
|
String commaAlternativeCharsInArguments = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS, null);
|
||||||
|
if (commaAlternativeCharsInArguments == null)
|
||||||
|
commaAlternativeCharsInArguments = TermuxConstants.COMMA_ALTERNATIVE;
|
||||||
|
// Replace any commaAlternativeCharsInArguments characters with normal commas
|
||||||
|
DataUtils.replaceSubStringsInStringArrayItems(executionCommand.arguments, commaAlternativeCharsInArguments, TermuxConstants.COMMA_NORMAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
executionCommand.stdin = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_STDIN, null);
|
||||||
|
executionCommand.workingDirectory = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_WORKDIR, null);
|
||||||
|
executionCommand.inBackground = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false);
|
||||||
|
executionCommand.sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION);
|
||||||
|
executionCommand.commandLabel = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_LABEL, "RUN_COMMAND Execution Intent Command");
|
||||||
|
executionCommand.commandDescription = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION, null);
|
||||||
|
executionCommand.commandHelp = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_HELP, null);
|
||||||
|
executionCommand.isPluginExecutionCommand = true;
|
||||||
|
executionCommand.resultConfig.resultPendingIntent = intent.getParcelableExtra(RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT);
|
||||||
|
executionCommand.resultConfig.resultDirectoryPath = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_DIRECTORY, null);
|
||||||
|
if (executionCommand.resultConfig.resultDirectoryPath != null) {
|
||||||
|
executionCommand.resultConfig.resultSingleFile = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_RESULT_SINGLE_FILE, false);
|
||||||
|
executionCommand.resultConfig.resultFileBasename = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_BASENAME, null);
|
||||||
|
executionCommand.resultConfig.resultFileOutputFormat = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, null);
|
||||||
|
executionCommand.resultConfig.resultFileErrorFormat = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, null);
|
||||||
|
executionCommand.resultConfig.resultFilesSuffix = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILES_SUFFIX, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If "allow-external-apps" property to not set to "true", then just return
|
||||||
|
// We enable force notifications if "allow-external-apps" policy is violated so that the
|
||||||
|
// user knows someone tried to run a command in termux context, since it may be malicious
|
||||||
|
// app or imported (tasker) plugin project and not the user himself. If a pending intent is
|
||||||
|
// also sent, then its creator is also logged and shown.
|
||||||
|
errmsg = PluginUtils.checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(this);
|
||||||
|
if (errmsg != null) {
|
||||||
|
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||||
|
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(Errno.ERRNO_FAILED.getCode(), errmsg);
|
||||||
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||||
|
return Service.START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get canonical path of executable
|
||||||
|
executionCommand.executable = TermuxFileUtils.getCanonicalPath(executionCommand.executable, null, true);
|
||||||
|
|
||||||
|
// If executable is not a regular file, or is not readable or executable, then just return
|
||||||
|
// Setting of missing read and execute permissions is not done
|
||||||
|
error = FileUtils.validateRegularFileExistenceAndPermissions("executable", executionCommand.executable, null,
|
||||||
|
FileUtils.APP_EXECUTABLE_FILE_PERMISSIONS, true, true,
|
||||||
|
false);
|
||||||
|
if (error != null) {
|
||||||
|
error.appendMessage("\n" + this.getString(R.string.msg_executable_absolute_path, executionCommand.executable));
|
||||||
|
executionCommand.setStateFailed(error);
|
||||||
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||||
|
return Service.START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// If workingDirectory is not null or empty
|
||||||
|
if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) {
|
||||||
|
// Get canonical path of workingDirectory
|
||||||
|
executionCommand.workingDirectory = TermuxFileUtils.getCanonicalPath(executionCommand.workingDirectory, null, true);
|
||||||
|
|
||||||
|
// If workingDirectory is not a directory, or is not readable or writable, then just return
|
||||||
|
// Creation of missing directory and setting of read, write and execute permissions are only done if workingDirectory is
|
||||||
|
// under allowed termux working directory paths.
|
||||||
|
// We try to set execute permissions, but ignore if they are missing, since only read and write permissions are required
|
||||||
|
// for working directories.
|
||||||
|
error = TermuxFileUtils.validateDirectoryFileExistenceAndPermissions("working", executionCommand.workingDirectory,
|
||||||
|
true, true, true,
|
||||||
|
false, true);
|
||||||
|
if (error != null) {
|
||||||
|
error.appendMessage("\n" + this.getString(R.string.msg_working_directory_absolute_path, executionCommand.workingDirectory));
|
||||||
|
executionCommand.setStateFailed(error);
|
||||||
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||||
|
return Service.START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the executable passed as the extra was an applet for coreutils/busybox, then we must
|
||||||
|
// use it instead of the canonical path above since otherwise arguments would be passed to
|
||||||
|
// coreutils/busybox instead and command would fail. Broken symlinks would already have been
|
||||||
|
// validated so it should be fine to use it.
|
||||||
|
executableExtra = TermuxFileUtils.getExpandedTermuxPath(executableExtra);
|
||||||
|
if (FileUtils.getFileType(executableExtra, false) == FileType.SYMLINK) {
|
||||||
|
Logger.logVerbose(LOG_TAG, "The executableExtra path \"" + executableExtra + "\" is a symlink so using it instead of the canonical path \"" + executionCommand.executable + "\"");
|
||||||
|
executionCommand.executable = executableExtra;
|
||||||
|
}
|
||||||
|
|
||||||
|
executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(executionCommand.executable).build();
|
||||||
|
|
||||||
|
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.resultConfig.resultPendingIntent);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_DIRECTORY, executionCommand.resultConfig.resultDirectoryPath);
|
||||||
|
if (executionCommand.resultConfig.resultDirectoryPath != null) {
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_SINGLE_FILE, executionCommand.resultConfig.resultSingleFile);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_BASENAME, executionCommand.resultConfig.resultFileBasename);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, executionCommand.resultConfig.resultFileOutputFormat);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, executionCommand.resultConfig.resultFileErrorFormat);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILES_SUFFIX, executionCommand.resultConfig.resultFilesSuffix);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start TERMUX_SERVICE and pass it execution intent
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
this.startForegroundService(execIntent);
|
||||||
|
} else {
|
||||||
|
this.startService(execIntent);
|
||||||
|
}
|
||||||
|
|
||||||
runStopForeground();
|
runStopForeground();
|
||||||
|
|
||||||
return Service.START_NOT_STICKY;
|
return Service.START_NOT_STICKY;
|
||||||
@@ -116,7 +227,7 @@ public class RunCommandService extends Service {
|
|||||||
private void runStartForeground() {
|
private void runStartForeground() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
setupNotificationChannel();
|
setupNotificationChannel();
|
||||||
startForeground(NOTIFICATION_ID, buildNotification());
|
startForeground(TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_ID, buildNotification());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,22 +238,21 @@ public class RunCommandService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Notification buildNotification() {
|
private Notification buildNotification() {
|
||||||
Notification.Builder builder = new Notification.Builder(this);
|
// Build the notification
|
||||||
builder.setContentTitle(getText(R.string.application_name) + " Run Command");
|
Notification.Builder builder = NotificationUtils.geNotificationBuilder(this,
|
||||||
builder.setSmallIcon(R.drawable.ic_service_notification);
|
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_LOW,
|
||||||
|
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_NAME, null, null,
|
||||||
// Use a low priority:
|
null, NotificationUtils.NOTIFICATION_MODE_SILENT);
|
||||||
builder.setPriority(Notification.PRIORITY_LOW);
|
if (builder == null) return null;
|
||||||
|
|
||||||
// No need to show a timestamp:
|
// No need to show a timestamp:
|
||||||
builder.setShowWhen(false);
|
builder.setShowWhen(false);
|
||||||
|
|
||||||
// Background color for small notification icon:
|
// Set notification icon
|
||||||
builder.setColor(0xFF607D8B);
|
builder.setSmallIcon(R.drawable.ic_service_notification);
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
// Set background color for small notification icon
|
||||||
builder.setChannelId(NOTIFICATION_CHANNEL_ID);
|
builder.setColor(0xFF607D8B);
|
||||||
}
|
|
||||||
|
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
@@ -150,40 +260,8 @@ public class RunCommandService extends Service {
|
|||||||
private void setupNotificationChannel() {
|
private void setupNotificationChannel() {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
|
||||||
|
|
||||||
String channelName = "Termux Run Command";
|
NotificationUtils.setupNotificationChannel(this, TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_ID,
|
||||||
int importance = NotificationManager.IMPORTANCE_LOW;
|
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW);
|
||||||
|
|
||||||
NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, importance);
|
|
||||||
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
|
||||||
manager.createNotificationChannel(channel);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
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.TermuxCrashUtils;
|
||||||
|
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
|
||||||
|
TermuxCrashUtils.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,21 @@ import android.content.Context;
|
|||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
import android.os.UserManager;
|
import android.os.UserManager;
|
||||||
import android.system.Os;
|
import android.system.Os;
|
||||||
import android.util.Log;
|
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.terminal.EmulatorDebug;
|
import com.termux.app.utils.CrashUtils;
|
||||||
|
import com.termux.shared.file.FileUtils;
|
||||||
|
import com.termux.shared.interact.MessageDialogUtils;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.models.errors.Error;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -29,11 +32,11 @@ import java.util.zip.ZipInputStream;
|
|||||||
* Install the Termux bootstrap packages if necessary by following the below steps:
|
* Install the Termux bootstrap packages if necessary by following the below steps:
|
||||||
* <p/>
|
* <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
|
* (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/>
|
* <p/>
|
||||||
* (2) A progress dialog is shown with "Installing..." message and a spinner.
|
* (2) A progress dialog is shown with "Installing..." message and a spinner.
|
||||||
* <p/>
|
* <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/>
|
* <p/>
|
||||||
* (4) The zip file is loaded from a shared library.
|
* (4) The zip file is loaded from a shared library.
|
||||||
* <p/>
|
* <p/>
|
||||||
@@ -46,22 +49,38 @@ import java.util.zip.ZipInputStream;
|
|||||||
*/
|
*/
|
||||||
final class TermuxInstaller {
|
final class TermuxInstaller {
|
||||||
|
|
||||||
/** Performs setup if necessary. */
|
private static final String LOG_TAG = "TermuxInstaller";
|
||||||
static void setupIfNeeded(final Activity activity, final Runnable whenDone) {
|
|
||||||
|
/** 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
|
// Termux can only be run as the primary user (device owner) since only that
|
||||||
// account has the expected file system paths. Verify that:
|
// account has the expected file system paths. Verify that:
|
||||||
UserManager um = (UserManager) activity.getSystemService(Context.USER_SERVICE);
|
UserManager um = (UserManager) activity.getSystemService(Context.USER_SERVICE);
|
||||||
boolean isPrimaryUser = um.getSerialNumberForUser(android.os.Process.myUserHandle()) == 0;
|
boolean isPrimaryUser = um.getSerialNumberForUser(android.os.Process.myUserHandle()) == 0;
|
||||||
if (!isPrimaryUser) {
|
if (!isPrimaryUser) {
|
||||||
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_not_primary_user_message)
|
String bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message, TermuxConstants.TERMUX_PREFIX_DIR_PATH);
|
||||||
.setOnDismissListener(dialog -> System.exit(0)).setPositiveButton(android.R.string.ok, null).show();
|
Logger.logError(LOG_TAG, bootstrapErrorMessage);
|
||||||
|
MessageDialogUtils.exitAppWithErrorMessage(activity,
|
||||||
|
activity.getString(R.string.bootstrap_error_title),
|
||||||
|
bootstrapErrorMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final File PREFIX_FILE = new File(TermuxService.PREFIX_PATH);
|
final String PREFIX_FILE_PATH = TermuxConstants.TERMUX_PREFIX_DIR_PATH;
|
||||||
if (PREFIX_FILE.isDirectory()) {
|
final File PREFIX_FILE = TermuxConstants.TERMUX_PREFIX_DIR;
|
||||||
whenDone.run();
|
|
||||||
return;
|
// If prefix directory exists, even if its a symlink to a valid directory and symlink is not broken/dangling
|
||||||
|
if (FileUtils.directoryFileExists(PREFIX_FILE_PATH, true)) {
|
||||||
|
File[] PREFIX_FILE_LIST = PREFIX_FILE.listFiles();
|
||||||
|
// If prefix directory is empty or only contains the tmp directory
|
||||||
|
if(PREFIX_FILE_LIST == null || PREFIX_FILE_LIST.length == 0 || (PREFIX_FILE_LIST.length == 1 && TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH.equals(PREFIX_FILE_LIST[0].getAbsolutePath()))) {
|
||||||
|
Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" exists but is empty or only contains the tmp directory.");
|
||||||
|
} else {
|
||||||
|
whenDone.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (FileUtils.fileExists(PREFIX_FILE_PATH, false)) {
|
||||||
|
Logger.logInfo(LOG_TAG, "The prefix directory \"" + PREFIX_FILE_PATH + "\" does not exist but another file exists at its destination.");
|
||||||
}
|
}
|
||||||
|
|
||||||
final ProgressDialog progress = ProgressDialog.show(activity, null, activity.getString(R.string.bootstrap_installer_body), true, false);
|
final ProgressDialog progress = ProgressDialog.show(activity, null, activity.getString(R.string.bootstrap_installer_body), true, false);
|
||||||
@@ -69,13 +88,29 @@ final class TermuxInstaller {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
final String STAGING_PREFIX_PATH = TermuxService.FILES_PATH + "/usr-staging";
|
Logger.logInfo(LOG_TAG, "Installing " + TermuxConstants.TERMUX_APP_NAME + " bootstrap packages.");
|
||||||
|
|
||||||
|
Error error;
|
||||||
|
|
||||||
|
final String STAGING_PREFIX_PATH = TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH;
|
||||||
final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH);
|
final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH);
|
||||||
|
|
||||||
if (STAGING_PREFIX_FILE.exists()) {
|
// Delete prefix staging directory or any file at its destination
|
||||||
deleteFolder(STAGING_PREFIX_FILE);
|
error = FileUtils.deleteFile("prefix staging directory", STAGING_PREFIX_PATH, true);
|
||||||
|
if (error != null) {
|
||||||
|
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete prefix directory or any file at its destination
|
||||||
|
error = FileUtils.deleteFile("prefix directory", PREFIX_FILE_PATH, true);
|
||||||
|
if (error != null) {
|
||||||
|
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.logInfo(LOG_TAG, "Extracting bootstrap zip to prefix staging directory \"" + STAGING_PREFIX_PATH + "\".");
|
||||||
|
|
||||||
final byte[] buffer = new byte[8096];
|
final byte[] buffer = new byte[8096];
|
||||||
final List<Pair<String, String>> symlinks = new ArrayList<>(50);
|
final List<Pair<String, String>> symlinks = new ArrayList<>(50);
|
||||||
|
|
||||||
@@ -94,14 +129,22 @@ final class TermuxInstaller {
|
|||||||
String newPath = STAGING_PREFIX_PATH + "/" + parts[1];
|
String newPath = STAGING_PREFIX_PATH + "/" + parts[1];
|
||||||
symlinks.add(Pair.create(oldPath, newPath));
|
symlinks.add(Pair.create(oldPath, newPath));
|
||||||
|
|
||||||
ensureDirectoryExists(new File(newPath).getParentFile());
|
error = ensureDirectoryExists(new File(newPath).getParentFile());
|
||||||
|
if (error != null) {
|
||||||
|
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
String zipEntryName = zipEntry.getName();
|
String zipEntryName = zipEntry.getName();
|
||||||
File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName);
|
File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName);
|
||||||
boolean isDirectory = zipEntry.isDirectory();
|
boolean isDirectory = zipEntry.isDirectory();
|
||||||
|
|
||||||
ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile());
|
error = ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile());
|
||||||
|
if (error != null) {
|
||||||
|
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Error.getErrorMarkdownString(error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isDirectory) {
|
if (!isDirectory) {
|
||||||
try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
|
try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
|
||||||
@@ -124,27 +167,18 @@ final class TermuxInstaller {
|
|||||||
Os.symlink(symlink.first, symlink.second);
|
Os.symlink(symlink.first, symlink.second);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.logInfo(LOG_TAG, "Moving prefix staging to prefix directory.");
|
||||||
|
|
||||||
if (!STAGING_PREFIX_FILE.renameTo(PREFIX_FILE)) {
|
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);
|
activity.runOnUiThread(whenDone);
|
||||||
|
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
Log.e(EmulatorDebug.LOG_TAG, "Bootstrap error", e);
|
showBootstrapErrorDialog(activity, PREFIX_FILE_PATH, whenDone, Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)));
|
||||||
activity.runOnUiThread(() -> {
|
|
||||||
try {
|
|
||||||
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
|
|
||||||
.setNegativeButton(R.string.bootstrap_error_abort, (dialog, which) -> {
|
|
||||||
dialog.dismiss();
|
|
||||||
activity.finish();
|
|
||||||
}).setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
|
|
||||||
dialog.dismiss();
|
|
||||||
TermuxInstaller.setupIfNeeded(activity, whenDone);
|
|
||||||
}).show();
|
|
||||||
} catch (WindowManager.BadTokenException e1) {
|
|
||||||
// Activity already dismissed - ignore.
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} finally {
|
} finally {
|
||||||
activity.runOnUiThread(() -> {
|
activity.runOnUiThread(() -> {
|
||||||
try {
|
try {
|
||||||
@@ -158,58 +192,51 @@ final class TermuxInstaller {
|
|||||||
}.start();
|
}.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ensureDirectoryExists(File directory) {
|
public static void showBootstrapErrorDialog(Activity activity, String PREFIX_FILE_PATH, Runnable whenDone, String message) {
|
||||||
if (!directory.isDirectory() && !directory.mkdirs()) {
|
Logger.logErrorExtended(LOG_TAG, "Bootstrap Error:\n" + message);
|
||||||
throw new RuntimeException("Unable to create directory: " + directory.getAbsolutePath());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static byte[] loadZipBytes() {
|
// Send a notification with the exception so that the user knows why bootstrap setup failed
|
||||||
// Only load the shared library when necessary to save memory usage.
|
CrashUtils.sendCrashReportNotification(activity, LOG_TAG, "## Bootstrap Error\n\n" + message, true, true);
|
||||||
System.loadLibrary("termux-bootstrap");
|
|
||||||
return getZip();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static native byte[] getZip();
|
activity.runOnUiThread(() -> {
|
||||||
|
try {
|
||||||
/** Delete a folder and all its content or throw. Don't follow symlinks. */
|
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
|
||||||
static void deleteFolder(File fileOrDirectory) throws IOException {
|
.setNegativeButton(R.string.bootstrap_error_abort, (dialog, which) -> {
|
||||||
if (fileOrDirectory.getCanonicalPath().equals(fileOrDirectory.getAbsolutePath()) && fileOrDirectory.isDirectory()) {
|
dialog.dismiss();
|
||||||
File[] children = fileOrDirectory.listFiles();
|
activity.finish();
|
||||||
|
})
|
||||||
if (children != null) {
|
.setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
|
||||||
for (File child : children) {
|
dialog.dismiss();
|
||||||
deleteFolder(child);
|
FileUtils.deleteFile("prefix directory", PREFIX_FILE_PATH, true);
|
||||||
}
|
TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone);
|
||||||
|
}).show();
|
||||||
|
} catch (WindowManager.BadTokenException e1) {
|
||||||
|
// Activity already dismissed - ignore.
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
if (!fileOrDirectory.delete()) {
|
|
||||||
throw new RuntimeException("Unable to delete " + (fileOrDirectory.isDirectory() ? "directory " : "file ") + fileOrDirectory.getAbsolutePath());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void setupStorageSymlinks(final Context context) {
|
static void setupStorageSymlinks(final Context context) {
|
||||||
final String LOG_TAG = "termux-storage";
|
final String LOG_TAG = "termux-storage";
|
||||||
|
|
||||||
|
Logger.logInfo(LOG_TAG, "Setting up storage symlinks.");
|
||||||
|
|
||||||
new Thread() {
|
new Thread() {
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
File storageDir = new File(TermuxService.HOME_PATH, "storage");
|
Error error;
|
||||||
|
File storageDir = TermuxConstants.TERMUX_STORAGE_HOME_DIR;
|
||||||
|
|
||||||
if (storageDir.exists()) {
|
error = FileUtils.clearDirectory("~/storage", storageDir.getAbsolutePath());
|
||||||
try {
|
if (error != null) {
|
||||||
deleteFolder(storageDir);
|
Logger.logErrorAndShowToast(context, LOG_TAG, error.getMessage());
|
||||||
} catch (IOException e) {
|
Logger.logErrorExtended(LOG_TAG, "Setup Storage Error\n" + error.toString());
|
||||||
Log.e(LOG_TAG, "Could not delete old $HOME/storage, " + e.getMessage());
|
CrashUtils.sendCrashReportNotification(context, LOG_TAG, "## Setup Storage Error\n\n" + Error.getErrorMarkdownString(error), true, true);
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!storageDir.mkdirs()) {
|
|
||||||
Log.e(LOG_TAG, "Unable to mkdirs() for $HOME/storage");
|
|
||||||
return;
|
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();
|
File sharedDir = Environment.getExternalStorageDirectory();
|
||||||
Os.symlink(sharedDir.getAbsolutePath(), new File(storageDir, "shared").getAbsolutePath());
|
Os.symlink(sharedDir.getAbsolutePath(), new File(storageDir, "shared").getAbsolutePath());
|
||||||
|
|
||||||
@@ -234,14 +261,31 @@ final class TermuxInstaller {
|
|||||||
File dir = dirs[i];
|
File dir = dirs[i];
|
||||||
if (dir == null) continue;
|
if (dir == null) continue;
|
||||||
String symlinkName = "external-" + i;
|
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());
|
Os.symlink(dir.getAbsolutePath(), new File(storageDir, symlinkName).getAbsolutePath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.logInfo(LOG_TAG, "Storage symlinks created successfully.");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(LOG_TAG, "Error setting up link", e);
|
Logger.logErrorAndShowToast(context, LOG_TAG, e.getMessage());
|
||||||
|
Logger.logStackTraceWithMessage(LOG_TAG, "Setup Storage Error: Error setting up link", e);
|
||||||
|
CrashUtils.sendCrashReportNotification(context, LOG_TAG, "## Setup Storage Error\n\n" + Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)), true, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.start();
|
}.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Error ensureDirectoryExists(File directory) {
|
||||||
|
return FileUtils.createDirectoryFile(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();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import android.net.Uri;
|
|||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
import android.util.Log;
|
|
||||||
import android.webkit.MimeTypeMap;
|
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.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
@@ -24,11 +24,13 @@ import androidx.annotation.NonNull;
|
|||||||
|
|
||||||
public class TermuxOpenReceiver extends BroadcastReceiver {
|
public class TermuxOpenReceiver extends BroadcastReceiver {
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "TermuxOpenReceiver";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onReceive(Context context, Intent intent) {
|
public void onReceive(Context context, Intent intent) {
|
||||||
final Uri data = intent.getData();
|
final Uri data = intent.getData();
|
||||||
if (data == null) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +44,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||||||
// Ok.
|
// Ok.
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
Log.e(EmulatorDebug.LOG_TAG, "Invalid action '" + intentAction + "', using 'view'");
|
Logger.logError(LOG_TAG, "Invalid action '" + intentAction + "', using 'view'");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,14 +61,14 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||||||
try {
|
try {
|
||||||
context.startActivity(urlIntent);
|
context.startActivity(urlIntent);
|
||||||
} catch (ActivityNotFoundException e) {
|
} 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final File fileToShare = new File(filePath);
|
final File fileToShare = new File(filePath);
|
||||||
if (!(fileToShare.isFile() && fileToShare.canRead())) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +89,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||||||
contentTypeToUse = contentTypeExtra;
|
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)) {
|
if (Intent.ACTION_SEND.equals(intentAction)) {
|
||||||
sendIntent.putExtra(Intent.EXTRA_STREAM, uriToShare);
|
sendIntent.putExtra(Intent.EXTRA_STREAM, uriToShare);
|
||||||
@@ -103,7 +105,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver {
|
|||||||
try {
|
try {
|
||||||
context.startActivity(sendIntent);
|
context.startActivity(sendIntent);
|
||||||
} catch (ActivityNotFoundException e) {
|
} 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 path = file.getCanonicalPath();
|
||||||
String storagePath = Environment.getExternalStorageDirectory().getCanonicalPath();
|
String storagePath = Environment.getExternalStorageDirectory().getCanonicalPath();
|
||||||
// See https://support.google.com/faqs/answer/7496913:
|
// 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);
|
throw new IllegalArgumentException("Invalid path: " + path);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|||||||
@@ -1,251 +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;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
@AsciiBellBehaviour
|
|
||||||
int mBellBehaviour = BELL_VIBRATE;
|
|
||||||
|
|
||||||
boolean mBackIsEscape;
|
|
||||||
boolean mDisableVolumeVirtualKeys;
|
|
||||||
boolean mShowExtraKeys;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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.app.Activity;
|
||||||
import android.content.ActivityNotFoundException;
|
import android.content.ActivityNotFoundException;
|
||||||
@@ -12,8 +12,10 @@ import android.webkit.WebViewClient;
|
|||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.RelativeLayout;
|
import android.widget.RelativeLayout;
|
||||||
|
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
|
||||||
/** Basic embedded browser for viewing help pages. */
|
/** Basic embedded browser for viewing help pages. */
|
||||||
public final class TermuxHelpActivity extends Activity {
|
public final class HelpActivity extends Activity {
|
||||||
|
|
||||||
WebView mWebView;
|
WebView mWebView;
|
||||||
|
|
||||||
@@ -39,7 +41,7 @@ public final class TermuxHelpActivity extends Activity {
|
|||||||
mWebView.setWebViewClient(new WebViewClient() {
|
mWebView.setWebViewClient(new WebViewClient() {
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||||
if (url.startsWith("https://wiki.termux.com")) {
|
if (url.equals(TermuxConstants.TERMUX_WIKI_URL) || url.startsWith(TermuxConstants.TERMUX_WIKI_URL + "/")) {
|
||||||
// Inline help.
|
// Inline help.
|
||||||
setContentView(progressLayout);
|
setContentView(progressLayout);
|
||||||
return false;
|
return false;
|
||||||
@@ -60,7 +62,7 @@ public final class TermuxHelpActivity extends Activity {
|
|||||||
setContentView(mWebView);
|
setContentView(mWebView);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
mWebView.loadUrl("https://wiki.termux.com/wiki/Main_Page");
|
mWebView.loadUrl(TermuxConstants.TERMUX_WIKI_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package com.termux.app.activities;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.preference.Preference;
|
||||||
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.shared.activities.ReportActivity;
|
||||||
|
import com.termux.shared.models.ReportInfo;
|
||||||
|
import com.termux.app.models.UserAction;
|
||||||
|
import com.termux.shared.interact.ShareUtils;
|
||||||
|
import com.termux.shared.packages.PackageUtils;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxTaskerAppSharedPreferences;
|
||||||
|
import com.termux.shared.termux.AndroidUtils;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
import com.termux.shared.termux.TermuxUtils;
|
||||||
|
|
||||||
|
public class SettingsActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
@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(AndroidUtils.getDeviceInfoMarkdownString(context));
|
||||||
|
aboutString.append("\n\n").append(TermuxUtils.getImportantLinksMarkdownString(context));
|
||||||
|
|
||||||
|
ReportActivity.startReportActivity(context, new ReportInfo(UserAction.ABOUT.getName(), TermuxConstants.TERMUX_APP.TERMUX_SETTINGS_ACTIVITY_NAME, title, null, aboutString.toString(), null, false));
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void configureDonatePreference(@NonNull Context context) {
|
||||||
|
Preference donatePreference = findPreference("donate");
|
||||||
|
if (donatePreference != null) {
|
||||||
|
String signingCertificateSHA256Digest = PackageUtils.getSigningCertificateSHA256DigestForPackage(context);
|
||||||
|
if (signingCertificateSHA256Digest != null) {
|
||||||
|
// If APK is a Google Playstore release, then do not show the donation link
|
||||||
|
// since Termux isn't exempted from the playstore policy donation links restriction
|
||||||
|
// Check Fund solicitations: https://pay.google.com/intl/en_in/about/policy/
|
||||||
|
String apkRelease = TermuxUtils.getAPKRelease(signingCertificateSHA256Digest);
|
||||||
|
if (apkRelease == null || apkRelease.equals(TermuxConstants.APK_RELEASE_GOOGLE_PLAYSTORE_SIGNING_CERTIFICATE_SHA256_DIGEST)) {
|
||||||
|
donatePreference.setVisible(false);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
donatePreference.setVisible(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
donatePreference.setOnPreferenceClickListener(preference -> {
|
||||||
|
ShareUtils.openURL(context, TermuxConstants.TERMUX_DONATE_URL);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.termux.app.fragments.settings;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.annotation.Keep;
|
||||||
|
import androidx.preference.PreferenceDataStore;
|
||||||
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
public class TermuxPreferencesFragment extends PreferenceFragmentCompat {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||||
|
Context context = getContext();
|
||||||
|
if (context == null) return;
|
||||||
|
|
||||||
|
PreferenceManager preferenceManager = getPreferenceManager();
|
||||||
|
preferenceManager.setPreferenceDataStore(TermuxPreferencesDataStore.getInstance(context));
|
||||||
|
|
||||||
|
setPreferencesFromResource(R.xml.termux_preferences, rootKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class TermuxPreferencesDataStore extends PreferenceDataStore {
|
||||||
|
|
||||||
|
private final Context mContext;
|
||||||
|
private final TermuxAppSharedPreferences mPreferences;
|
||||||
|
|
||||||
|
private static TermuxPreferencesDataStore mInstance;
|
||||||
|
|
||||||
|
private TermuxPreferencesDataStore(Context context) {
|
||||||
|
mContext = context;
|
||||||
|
mPreferences = TermuxAppSharedPreferences.build(context, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized TermuxPreferencesDataStore getInstance(Context context) {
|
||||||
|
if (mInstance == null) {
|
||||||
|
mInstance = new TermuxPreferencesDataStore(context);
|
||||||
|
}
|
||||||
|
return mInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.termux.app.fragments.settings;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.annotation.Keep;
|
||||||
|
import androidx.preference.PreferenceDataStore;
|
||||||
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxTaskerAppSharedPreferences;
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
public class TermuxTaskerPreferencesFragment extends PreferenceFragmentCompat {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||||
|
Context context = getContext();
|
||||||
|
if (context == null) return;
|
||||||
|
|
||||||
|
PreferenceManager preferenceManager = getPreferenceManager();
|
||||||
|
preferenceManager.setPreferenceDataStore(TermuxTaskerPreferencesDataStore.getInstance(context));
|
||||||
|
|
||||||
|
setPreferencesFromResource(R.xml.termux_tasker_preferences, rootKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class TermuxTaskerPreferencesDataStore extends PreferenceDataStore {
|
||||||
|
|
||||||
|
private final Context mContext;
|
||||||
|
private final TermuxTaskerAppSharedPreferences mPreferences;
|
||||||
|
|
||||||
|
private static TermuxTaskerPreferencesDataStore mInstance;
|
||||||
|
|
||||||
|
private TermuxTaskerPreferencesDataStore(Context context) {
|
||||||
|
mContext = context;
|
||||||
|
mPreferences = TermuxTaskerAppSharedPreferences.build(context, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized TermuxTaskerPreferencesDataStore getInstance(Context context) {
|
||||||
|
if (mInstance == null) {
|
||||||
|
mInstance = new TermuxTaskerPreferencesDataStore(context);
|
||||||
|
}
|
||||||
|
return mInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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,77 @@
|
|||||||
|
package com.termux.app.fragments.settings.termux;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.annotation.Keep;
|
||||||
|
import androidx.preference.PreferenceDataStore;
|
||||||
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
public class TerminalViewPreferencesFragment extends PreferenceFragmentCompat {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||||
|
Context context = getContext();
|
||||||
|
if (context == null) return;
|
||||||
|
|
||||||
|
PreferenceManager preferenceManager = getPreferenceManager();
|
||||||
|
preferenceManager.setPreferenceDataStore(TerminalViewPreferencesDataStore.getInstance(context));
|
||||||
|
|
||||||
|
setPreferencesFromResource(R.xml.termux_terminal_view_preferences, rootKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class TerminalViewPreferencesDataStore extends PreferenceDataStore {
|
||||||
|
|
||||||
|
private final Context mContext;
|
||||||
|
private final TermuxAppSharedPreferences mPreferences;
|
||||||
|
|
||||||
|
private static TerminalViewPreferencesDataStore mInstance;
|
||||||
|
|
||||||
|
private TerminalViewPreferencesDataStore(Context context) {
|
||||||
|
mContext = context;
|
||||||
|
mPreferences = TermuxAppSharedPreferences.build(context, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized TerminalViewPreferencesDataStore getInstance(Context context) {
|
||||||
|
if (mInstance == null) {
|
||||||
|
mInstance = new TerminalViewPreferencesDataStore(context);
|
||||||
|
}
|
||||||
|
return mInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void putBoolean(String key, boolean value) {
|
||||||
|
if (mPreferences == null) return;
|
||||||
|
if (key == null) return;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case "terminal_margin_adjustment":
|
||||||
|
mPreferences.setTerminalMarginAdjustment(value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getBoolean(String key, boolean defValue) {
|
||||||
|
if (mPreferences == null) return false;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case "terminal_margin_adjustment":
|
||||||
|
return mPreferences.isTerminalMarginAdjustmentEnabled();
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
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,107 @@
|
|||||||
|
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.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 {
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the {@link TermuxPropertyConstants#KEY_TERMINAL_TRANSCRIPT_ROWS} value from termux properties file on disk.
|
||||||
|
*/
|
||||||
|
public static int getTerminalTranscriptRows(Context context) {
|
||||||
|
return (int) TermuxSharedProperties.getInternalPropertyValue(context, TermuxPropertyConstants.KEY_TERMINAL_TRANSCRIPT_ROWS);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
package com.termux.app.terminal;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.inputmethodservice.InputMethodService;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.ViewTreeObserver;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.inputmethod.EditorInfo;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
|
|
||||||
|
import com.termux.app.TermuxActivity;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.view.ViewUtils;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link TermuxActivity} relies on {@link android.view.WindowManager.LayoutParams#SOFT_INPUT_ADJUST_RESIZE)}
|
||||||
|
* set by {@link TermuxTerminalViewClient#setSoftKeyboardState(boolean, boolean)} to automatically
|
||||||
|
* resize the view and push the terminal up when soft keyboard is opened. However, this does not
|
||||||
|
* always work properly. When `enforce-char-based-input=true` is set in `termux.properties`
|
||||||
|
* and {@link com.termux.view.TerminalView#onCreateInputConnection(EditorInfo)} sets the inputType
|
||||||
|
* to `InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS`
|
||||||
|
* instead of the default `InputType.TYPE_NULL` for termux, some keyboards may still show suggestions.
|
||||||
|
* Gboard does too, but only when text is copied and clipboard suggestions **and** number keys row
|
||||||
|
* toggles are enabled in its settings. When number keys row toggle is not enabled, Gboard will still
|
||||||
|
* show the row but will switch it with suggestions if needed. If its enabled, then number keys row
|
||||||
|
* is always shown and suggestions are shown in an additional row on top of it. This additional row is likely
|
||||||
|
* part of the candidates view returned by the keyboard app in {@link InputMethodService#onCreateCandidatesView()}.
|
||||||
|
*
|
||||||
|
* With the above configuration, the additional clipboard suggestions row partially covers the
|
||||||
|
* extra keys/terminal. Reopening the keyboard/activity does not fix the issue. This is either a bug
|
||||||
|
* in the Android OS where it does not consider the candidate's view height in its calculation to push
|
||||||
|
* up the view or because Gboard does not include the candidate's view height in the height reported
|
||||||
|
* to android that should be used, hence causing an overlap.
|
||||||
|
*
|
||||||
|
* Gboard logs the following entry to `logcat` when its opened with or without the suggestions bar showing:
|
||||||
|
* I/KeyboardViewUtil: KeyboardViewUtil.calculateMaxKeyboardBodyHeight():62 leave 500 height for app when screen height:2392, header height:176 and isFullscreenMode:false, so the max keyboard body height is:1716
|
||||||
|
* where `keyboard_height = screen_height - height_for_app - header_height` (62 is a hardcoded value in Gboard source code and may be a version number)
|
||||||
|
* So this may in fact be due to Gboard but https://stackoverflow.com/questions/57567272 suggests
|
||||||
|
* otherwise. Another similar report https://stackoverflow.com/questions/66761661.
|
||||||
|
* Also check https://github.com/termux/termux-app/issues/1539.
|
||||||
|
*
|
||||||
|
* This overlap may happen even without `enforce-char-based-input=true` for keyboards with extended layouts
|
||||||
|
* like number row, etc.
|
||||||
|
*
|
||||||
|
* To fix these issues, `activity_termux.xml` has the constant 1sp transparent
|
||||||
|
* `activity_termux_bottom_space_view` View at the bottom. This will appear as a line matching the
|
||||||
|
* activity theme. When {@link TermuxActivity} {@link ViewTreeObserver.OnGlobalLayoutListener} is
|
||||||
|
* called when any of the sub view layouts change, like keyboard opening/closing keyboard,
|
||||||
|
* extra keys/input view switched, etc, we check if the bottom space view is visible or not.
|
||||||
|
* If its not, then we add a margin to the bottom of the root view, so that the keyboard does not
|
||||||
|
* overlap the extra keys/terminal, since the margin will push up the view. By default the margin
|
||||||
|
* added is equal to the height of the hidden part of extra keys/terminal. For Gboard's case, the
|
||||||
|
* hidden part equals the `header_height`. The updates to margins may cause a jitter in some cases
|
||||||
|
* when the view is redrawn if the margin is incorrect, but logic has been implemented to avoid that.
|
||||||
|
*/
|
||||||
|
public class TermuxActivityRootView extends LinearLayout implements ViewTreeObserver.OnGlobalLayoutListener {
|
||||||
|
|
||||||
|
public TermuxActivity mActivity;
|
||||||
|
public Integer marginBottom;
|
||||||
|
public Integer lastMarginBottom;
|
||||||
|
public long lastMarginBottomTime;
|
||||||
|
public long lastMarginBottomExtraTime;
|
||||||
|
|
||||||
|
/** Log root view events. */
|
||||||
|
private boolean ROOT_VIEW_LOGGING_ENABLED = false;
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "TermuxActivityRootView";
|
||||||
|
|
||||||
|
private static int mStatusBarHeight;
|
||||||
|
|
||||||
|
public TermuxActivityRootView(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TermuxActivityRootView(Context context, @Nullable AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TermuxActivityRootView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActivity(TermuxActivity activity) {
|
||||||
|
mActivity = activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether root view logging is enabled or not.
|
||||||
|
*
|
||||||
|
* @param value The boolean value that defines the state.
|
||||||
|
*/
|
||||||
|
public void setIsRootViewLoggingEnabled(boolean value) {
|
||||||
|
ROOT_VIEW_LOGGING_ENABLED = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||||
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||||
|
|
||||||
|
if (marginBottom != null) {
|
||||||
|
if (ROOT_VIEW_LOGGING_ENABLED)
|
||||||
|
Logger.logVerbose(LOG_TAG, "onMeasure: Setting bottom margin to " + marginBottom);
|
||||||
|
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) getLayoutParams();
|
||||||
|
params.setMargins(0, 0, 0, marginBottom);
|
||||||
|
setLayoutParams(params);
|
||||||
|
marginBottom = null;
|
||||||
|
requestLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onGlobalLayout() {
|
||||||
|
if (mActivity == null || !mActivity.isVisible()) return;
|
||||||
|
|
||||||
|
View bottomSpaceView = mActivity.getTermuxActivityBottomSpaceView();
|
||||||
|
if (bottomSpaceView == null) return;
|
||||||
|
|
||||||
|
boolean root_view_logging_enabled = ROOT_VIEW_LOGGING_ENABLED;
|
||||||
|
|
||||||
|
if (root_view_logging_enabled)
|
||||||
|
Logger.logVerbose(LOG_TAG, ":\nonGlobalLayout:");
|
||||||
|
|
||||||
|
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) getLayoutParams();
|
||||||
|
|
||||||
|
// Get the position Rects of the bottom space view and the main window holding it
|
||||||
|
Rect[] windowAndViewRects = ViewUtils.getWindowAndViewRects(bottomSpaceView, mStatusBarHeight);
|
||||||
|
if (windowAndViewRects == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Rect windowAvailableRect = windowAndViewRects[0];
|
||||||
|
Rect bottomSpaceViewRect = windowAndViewRects[1];
|
||||||
|
|
||||||
|
// If the bottomSpaceViewRect is inside the windowAvailableRect, then it must be completely visible
|
||||||
|
//boolean isVisible = windowAvailableRect.contains(bottomSpaceViewRect); // rect.right comparison often fails in landscape
|
||||||
|
boolean isVisible = ViewUtils.isRectAbove(windowAvailableRect, bottomSpaceViewRect);
|
||||||
|
boolean isVisibleBecauseMargin = (windowAvailableRect.bottom == bottomSpaceViewRect.bottom) && params.bottomMargin > 0;
|
||||||
|
boolean isVisibleBecauseExtraMargin = ((bottomSpaceViewRect.bottom - windowAvailableRect.bottom) < 0);
|
||||||
|
|
||||||
|
if (root_view_logging_enabled) {
|
||||||
|
Logger.logVerbose(LOG_TAG, "windowAvailableRect " + ViewUtils.toRectString(windowAvailableRect) + ", bottomSpaceViewRect " + ViewUtils.toRectString(bottomSpaceViewRect));
|
||||||
|
Logger.logVerbose(LOG_TAG, "windowAvailableRect.bottom " + windowAvailableRect.bottom +
|
||||||
|
", bottomSpaceViewRect.bottom " +bottomSpaceViewRect.bottom +
|
||||||
|
", diff " + (bottomSpaceViewRect.bottom - windowAvailableRect.bottom) + ", bottom " + params.bottomMargin +
|
||||||
|
", isVisible " + windowAvailableRect.contains(bottomSpaceViewRect) + ", isRectAbove " + ViewUtils.isRectAbove(windowAvailableRect, bottomSpaceViewRect) +
|
||||||
|
", isVisibleBecauseMargin " + isVisibleBecauseMargin + ", isVisibleBecauseExtraMargin " + isVisibleBecauseExtraMargin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the bottomSpaceViewRect is visible, then remove the margin if needed
|
||||||
|
if (isVisible) {
|
||||||
|
// If visible because of margin, i.e the bottom of bottomSpaceViewRect equals that of windowAvailableRect
|
||||||
|
// and a margin has been added
|
||||||
|
// Necessary so that we don't get stuck in an infinite loop since setting margin
|
||||||
|
// will call OnGlobalLayoutListener again and next time bottom space view
|
||||||
|
// will be visible and margin will be set to 0, which again will call
|
||||||
|
// OnGlobalLayoutListener...
|
||||||
|
// Calling addTermuxActivityRootViewGlobalLayoutListener with a delay fails to
|
||||||
|
// set appropriate margins when views are changed quickly since some changes
|
||||||
|
// may be missed.
|
||||||
|
if (isVisibleBecauseMargin) {
|
||||||
|
if (root_view_logging_enabled)
|
||||||
|
Logger.logVerbose(LOG_TAG, "Visible due to margin");
|
||||||
|
|
||||||
|
// Once the view has been redrawn with new margin, we set margin back to 0 so that
|
||||||
|
// when next time onMeasure() is called, margin 0 is used. This is necessary for
|
||||||
|
// cases when view has been redrawn with new margin because bottom space view was
|
||||||
|
// hidden by keyboard and then view was redrawn again due to layout change (like
|
||||||
|
// keyboard symbol view is switched to), android will add margin below its new position
|
||||||
|
// if its greater than 0, which was already above the keyboard creating x2x margin.
|
||||||
|
// Adding time check since moving split screen divider in landscape causes jitter
|
||||||
|
// and prevents some infinite loops
|
||||||
|
if ((System.currentTimeMillis() - lastMarginBottomTime) > 40) {
|
||||||
|
lastMarginBottomTime = System.currentTimeMillis();
|
||||||
|
marginBottom = 0;
|
||||||
|
} else {
|
||||||
|
if (root_view_logging_enabled)
|
||||||
|
Logger.logVerbose(LOG_TAG, "Ignoring restoring marginBottom to 0 since called to quickly");
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean setMargin = params.bottomMargin != 0;
|
||||||
|
|
||||||
|
// If visible because of extra margin, i.e the bottom of bottomSpaceViewRect is above that of windowAvailableRect
|
||||||
|
// onGlobalLayout: windowAvailableRect 1408, bottomSpaceViewRect 1232, diff -176, bottom 0, isVisible true, isVisibleBecauseMargin false, isVisibleBecauseExtraMargin false
|
||||||
|
// onGlobalLayout: Bottom margin already equals 0
|
||||||
|
if (isVisibleBecauseExtraMargin) {
|
||||||
|
// Adding time check since prevents infinite loops, like in landscape mode in freeform mode in Taskbar
|
||||||
|
if ((System.currentTimeMillis() - lastMarginBottomExtraTime) > 40) {
|
||||||
|
if (root_view_logging_enabled)
|
||||||
|
Logger.logVerbose(LOG_TAG, "Resetting margin since visible due to extra margin");
|
||||||
|
lastMarginBottomExtraTime = System.currentTimeMillis();
|
||||||
|
// lastMarginBottom must be invalid. May also happen when keyboards are changed.
|
||||||
|
lastMarginBottom = null;
|
||||||
|
setMargin = true;
|
||||||
|
} else {
|
||||||
|
if (root_view_logging_enabled)
|
||||||
|
Logger.logVerbose(LOG_TAG, "Ignoring resetting margin since visible due to extra margin since called to quickly");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setMargin) {
|
||||||
|
if (root_view_logging_enabled)
|
||||||
|
Logger.logVerbose(LOG_TAG, "Setting bottom margin to 0");
|
||||||
|
params.setMargins(0, 0, 0, 0);
|
||||||
|
setLayoutParams(params);
|
||||||
|
} else {
|
||||||
|
if (root_view_logging_enabled)
|
||||||
|
Logger.logVerbose(LOG_TAG, "Bottom margin already equals 0");
|
||||||
|
// This is done so that when next time onMeasure() is called, lastMarginBottom is used.
|
||||||
|
// This is done since we **expect** the keyboard to have same dimensions next time layout
|
||||||
|
// changes, so best set margin while view is drawn the first time, otherwise it will
|
||||||
|
// cause a jitter when OnGlobalLayoutListener is called with margin 0 and it sets the
|
||||||
|
// likely same lastMarginBottom again and requesting a redraw. Hopefully, this logic
|
||||||
|
// works fine for all cases.
|
||||||
|
marginBottom = lastMarginBottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ELse find the part of the extra keys/terminal that is hidden and add a margin accordingly
|
||||||
|
else {
|
||||||
|
int pxHidden = bottomSpaceViewRect.bottom - windowAvailableRect.bottom;
|
||||||
|
|
||||||
|
if (root_view_logging_enabled)
|
||||||
|
Logger.logVerbose(LOG_TAG, "pxHidden " + pxHidden + ", bottom " + params.bottomMargin);
|
||||||
|
|
||||||
|
boolean setMargin = params.bottomMargin != pxHidden;
|
||||||
|
|
||||||
|
// If invisible despite margin, i.e a margin was added, but the bottom of bottomSpaceViewRect
|
||||||
|
// is still below that of windowAvailableRect, this will trigger OnGlobalLayoutListener
|
||||||
|
// again, so that margins are set properly. May happen when toolbar/extra keys is disabled
|
||||||
|
// and enabled from left drawer, just like case for isVisibleBecauseExtraMargin.
|
||||||
|
// onMeasure: Setting bottom margin to 176
|
||||||
|
// onGlobalLayout: windowAvailableRect 1232, bottomSpaceViewRect 1408, diff 176, bottom 176, isVisible false, isVisibleBecauseMargin false, isVisibleBecauseExtraMargin false
|
||||||
|
// onGlobalLayout: Bottom margin already equals 176
|
||||||
|
if (pxHidden > 0 && params.bottomMargin > 0) {
|
||||||
|
if (pxHidden != params.bottomMargin) {
|
||||||
|
if (root_view_logging_enabled)
|
||||||
|
Logger.logVerbose(LOG_TAG, "Force setting margin to 0 since not visible due to wrong margin");
|
||||||
|
pxHidden = 0;
|
||||||
|
} else {
|
||||||
|
if (root_view_logging_enabled)
|
||||||
|
Logger.logVerbose(LOG_TAG, "Force setting margin since not visible despite required margin");
|
||||||
|
}
|
||||||
|
setMargin = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pxHidden < 0) {
|
||||||
|
if (root_view_logging_enabled)
|
||||||
|
Logger.logVerbose(LOG_TAG, "Force setting margin to 0 since new margin is negative");
|
||||||
|
pxHidden = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (setMargin) {
|
||||||
|
if (root_view_logging_enabled)
|
||||||
|
Logger.logVerbose(LOG_TAG, "Setting bottom margin to " + pxHidden);
|
||||||
|
params.setMargins(0, 0, 0, pxHidden);
|
||||||
|
setLayoutParams(params);
|
||||||
|
lastMarginBottom = pxHidden;
|
||||||
|
} else {
|
||||||
|
if (root_view_logging_enabled)
|
||||||
|
Logger.logVerbose(LOG_TAG, "Bottom margin already equals " + pxHidden);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class WindowInsetsListener implements View.OnApplyWindowInsetsListener {
|
||||||
|
@Override
|
||||||
|
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
|
||||||
|
mStatusBarHeight = WindowInsetsCompat.toWindowInsetsCompat(insets).getInsets(WindowInsetsCompat.Type.statusBars()).top;
|
||||||
|
// Let view window handle insets however it wants
|
||||||
|
return v.onApplyWindowInsets(insets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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,474 @@
|
|||||||
|
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.TextInputDialogUtils;
|
||||||
|
import com.termux.app.TermuxActivity;
|
||||||
|
import com.termux.shared.terminal.TermuxTerminalSessionClientBase;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
import com.termux.app.TermuxService;
|
||||||
|
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||||
|
import com.termux.app.terminal.io.BellHandler;
|
||||||
|
import com.termux.shared.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be called when mActivity.onResetTerminalSession() is called
|
||||||
|
*/
|
||||||
|
public void onResetTerminalSession() {
|
||||||
|
// Ensure blinker starts again after reset if cursor blinking was disabled before reset like
|
||||||
|
// with "tput civis" which would have called onTerminalCursorStateChange()
|
||||||
|
mActivity.getTerminalView().setTerminalCursorBlinkerState(true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer getTerminalCursorStyle() {
|
||||||
|
return mActivity.getProperties().getTerminalCursorStyle();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** Initialize and get mBellSoundPool */
|
||||||
|
private synchronized SoundPool getBellSoundPool() {
|
||||||
|
if (mBellSoundPool == null) {
|
||||||
|
mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
|
||||||
|
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||||
|
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
|
||||||
|
|
||||||
|
mBellSoundId = mBellSoundPool.load(mActivity, R.raw.bell, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mBellSoundPool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Release mBellSoundPool resources */
|
||||||
|
private synchronized void releaseBellSoundPool() {
|
||||||
|
if (mBellSoundPool != null) {
|
||||||
|
mBellSoundPool.release();
|
||||||
|
mBellSoundPool = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** Try switching to session. */
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (!mActivity.getProperties().areTerminalSessionChangeToastsDisabled()) {
|
||||||
|
TerminalSession session = mActivity.getCurrentSession();
|
||||||
|
mActivity.showToast(toToastTitle(session), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void switchToSession(boolean forward) {
|
||||||
|
TermuxService service = mActivity.getTermuxService();
|
||||||
|
if (service == null) return;
|
||||||
|
|
||||||
|
TerminalSession currentTerminalSession = mActivity.getCurrentSession();
|
||||||
|
int index = service.getIndexOfSession(currentTerminalSession);
|
||||||
|
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;
|
||||||
|
|
||||||
|
TextInputDialogUtils.textInput(mActivity, R.string.title_rename_session, sessionToRename.mSessionName, R.string.action_rename_session_confirm, text -> {
|
||||||
|
sessionToRename.mSessionName = text;
|
||||||
|
termuxSessionListNotifyUpdated();
|
||||||
|
}, -1, null, -1, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addNewSession(boolean isFailSafe, String sessionName) {
|
||||||
|
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,701 @@
|
|||||||
|
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.EditText;
|
||||||
|
import android.widget.ListView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.app.TermuxActivity;
|
||||||
|
import com.termux.shared.data.UrlUtils;
|
||||||
|
import com.termux.shared.shell.ShellUtils;
|
||||||
|
import com.termux.shared.terminal.TermuxTerminalViewClientBase;
|
||||||
|
import com.termux.shared.termux.AndroidUtils;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
import com.termux.shared.activities.ReportActivity;
|
||||||
|
import com.termux.shared.models.ReportInfo;
|
||||||
|
import com.termux.app.models.UserAction;
|
||||||
|
import com.termux.app.terminal.io.KeyboardShortcut;
|
||||||
|
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
|
||||||
|
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||||
|
import com.termux.shared.data.DataUtils;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.markdown.MarkdownUtils;
|
||||||
|
import com.termux.shared.termux.TermuxUtils;
|
||||||
|
import com.termux.shared.view.KeyboardUtils;
|
||||||
|
import com.termux.shared.view.ViewUtils;
|
||||||
|
import com.termux.terminal.KeyHandler;
|
||||||
|
import com.termux.terminal.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 boolean mShowSoftKeyboardIgnoreOnce;
|
||||||
|
private boolean mShowSoftKeyboardWithDelayOnce;
|
||||||
|
|
||||||
|
private boolean mTerminalCursorBlinkerStateAlreadySet;
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "TermuxTerminalViewClient";
|
||||||
|
|
||||||
|
public TermuxTerminalViewClient(TermuxActivity activity, TermuxTerminalSessionClient termuxTerminalSessionClient) {
|
||||||
|
this.mActivity = activity;
|
||||||
|
this.mTermuxTerminalSessionClient = termuxTerminalSessionClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be called when mActivity.onCreate() is called
|
||||||
|
*/
|
||||||
|
public void onCreate() {
|
||||||
|
mActivity.getTerminalView().setTextSize(mActivity.getPreferences().getFontSize());
|
||||||
|
mActivity.getTerminalView().setKeepScreenOn(mActivity.getPreferences().shouldKeepScreenOn());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be called when mActivity.onStart() is called
|
||||||
|
*/
|
||||||
|
public void onStart() {
|
||||||
|
// Set {@link TerminalView#TERMINAL_VIEW_KEY_LOGGING_ENABLED} value
|
||||||
|
// Also required if user changed the preference from {@link TermuxSettings} activity and returns
|
||||||
|
boolean isTerminalViewKeyLoggingEnabled = mActivity.getPreferences().isTerminalViewKeyLoggingEnabled();
|
||||||
|
mActivity.getTerminalView().setIsTerminalViewKeyLoggingEnabled(isTerminalViewKeyLoggingEnabled);
|
||||||
|
|
||||||
|
// Piggyback on the terminal view key logging toggle for now, should add a separate toggle in future
|
||||||
|
mActivity.getTermuxActivityRootView().setIsRootViewLoggingEnabled(isTerminalViewKeyLoggingEnabled);
|
||||||
|
ViewUtils.setIsViewUtilsLoggingEnabled(isTerminalViewKeyLoggingEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be called when mActivity.onResume() is called
|
||||||
|
*/
|
||||||
|
public void onResume() {
|
||||||
|
// Show the soft keyboard if required
|
||||||
|
setSoftKeyboardState(true, false);
|
||||||
|
|
||||||
|
mTerminalCursorBlinkerStateAlreadySet = false;
|
||||||
|
|
||||||
|
if (mActivity.getTerminalView().mEmulator != null) {
|
||||||
|
// Start terminal cursor blinking if enabled
|
||||||
|
// If emulator is already set, then start blinker now, otherwise wait for onEmulatorSet()
|
||||||
|
// event to start it. This is needed since onEmulatorSet() may not be called after
|
||||||
|
// TermuxActivity is started after device display timeout with double tap and not power button.
|
||||||
|
setTerminalCursorBlinkerState(true);
|
||||||
|
mTerminalCursorBlinkerStateAlreadySet = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be called when mActivity.onStop() is called
|
||||||
|
*/
|
||||||
|
public void onStop() {
|
||||||
|
// Stop terminal cursor blinking if enabled
|
||||||
|
setTerminalCursorBlinkerState(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be called when mActivity.reloadActivityStyling() is called
|
||||||
|
*/
|
||||||
|
public void onReload() {
|
||||||
|
// Show the soft keyboard if required
|
||||||
|
setSoftKeyboardState(false, true);
|
||||||
|
|
||||||
|
// Start terminal cursor blinking if enabled
|
||||||
|
setTerminalCursorBlinkerState(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be called when {@link com.termux.view.TerminalView#mEmulator}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onEmulatorSet() {
|
||||||
|
if (!mTerminalCursorBlinkerStateAlreadySet) {
|
||||||
|
// Start terminal cursor blinking if enabled
|
||||||
|
// We need to wait for the first session to be attached that's set in
|
||||||
|
// TermuxActivity.onServiceConnected() and then the multiple calls to TerminalView.updateSize()
|
||||||
|
// where the final one eventually sets the mEmulator when width/height is not 0. Otherwise
|
||||||
|
// blinker will not start again if TermuxActivity is started again after exiting it with
|
||||||
|
// double back press. Check TerminalView.setTerminalCursorBlinkerState().
|
||||||
|
setTerminalCursorBlinkerState(true);
|
||||||
|
mTerminalCursorBlinkerStateAlreadySet = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float onScale(float scale) {
|
||||||
|
if (scale < 0.9f || scale > 1.1f) {
|
||||||
|
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) {
|
||||||
|
// If emulator is not set, like if bootstrap installation failed and user dismissed the error
|
||||||
|
// dialog, then just exit the activity, otherwise they will be stuck in a broken state.
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_BACK && mActivity.getTerminalView().mEmulator == null) {
|
||||||
|
mActivity.finishActivityIfNotFinishing();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return handleVirtualKeys(keyCode, e, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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 {
|
||||||
|
// Show with a delay, otherwise pressing keyboard toggle won't show the keyboard after
|
||||||
|
// switching back from another app if keyboard was previously disabled by user.
|
||||||
|
// Also request focus, since it wouldn't have been requested at startup by
|
||||||
|
// setSoftKeyboardState if keyboard was disabled. #2112
|
||||||
|
Logger.logVerbose(LOG_TAG, "Enabling soft keyboard on toggle");
|
||||||
|
mActivity.getPreferences().setSoftKeyboardEnabled(true);
|
||||||
|
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
|
||||||
|
if(mShowSoftKeyboardWithDelayOnce) {
|
||||||
|
mShowSoftKeyboardWithDelayOnce = false;
|
||||||
|
mActivity.getTerminalView().postDelayed(getShowSoftKeyboardRunnable(), 500);
|
||||||
|
mActivity.getTerminalView().requestFocus();
|
||||||
|
} else
|
||||||
|
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If soft keyboard toggle behaviour is show/hide
|
||||||
|
else {
|
||||||
|
// If soft keyboard is disabled by user for Termux
|
||||||
|
if (!mActivity.getPreferences().isSoftKeyboardEnabled()) {
|
||||||
|
Logger.logVerbose(LOG_TAG, "Maintaining disabled soft keyboard on toggle");
|
||||||
|
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||||
|
} else {
|
||||||
|
Logger.logVerbose(LOG_TAG, "Showing/Hiding soft keyboard on toggle");
|
||||||
|
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
|
||||||
|
KeyboardUtils.toggleSoftKeyboard(mActivity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSoftKeyboardState(boolean isStartup, boolean isReloadTermuxProperties) {
|
||||||
|
boolean noShowKeyboard = false;
|
||||||
|
|
||||||
|
// Requesting terminal view focus is necessary regardless of if soft keyboard is to be
|
||||||
|
// disabled or hidden at startup, otherwise if hardware keyboard is attached and user
|
||||||
|
// starts typing on hardware keyboard without tapping on the terminal first, then a colour
|
||||||
|
// tint will be added to the terminal as highlight for the focussed view. Test with a light
|
||||||
|
// theme.
|
||||||
|
|
||||||
|
// If soft keyboard is disabled by user for Termux (check function docs for Termux behaviour info)
|
||||||
|
if (KeyboardUtils.shouldSoftKeyboardBeDisabled(mActivity,
|
||||||
|
mActivity.getPreferences().isSoftKeyboardEnabled(),
|
||||||
|
mActivity.getPreferences().isSoftKeyboardEnabledOnlyIfNoHardware())) {
|
||||||
|
Logger.logVerbose(LOG_TAG, "Maintaining disabled soft keyboard");
|
||||||
|
KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||||
|
mActivity.getTerminalView().requestFocus();
|
||||||
|
noShowKeyboard = true;
|
||||||
|
// Delay is only required if onCreate() is called like when Termux app is exited with
|
||||||
|
// double back press, not when Termux app is switched back from another app and keyboard
|
||||||
|
// toggle is pressed to enable keyboard
|
||||||
|
if (isStartup && mActivity.isOnResumeAfterOnCreate())
|
||||||
|
mShowSoftKeyboardWithDelayOnce = true;
|
||||||
|
} else {
|
||||||
|
// Set flag to automatically push up TerminalView when keyboard is opened instead of showing over it
|
||||||
|
KeyboardUtils.setSoftInputModeAdjustResize(mActivity);
|
||||||
|
|
||||||
|
// Clear any previous flags to disable soft keyboard in case setting updated
|
||||||
|
KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity);
|
||||||
|
|
||||||
|
// If soft keyboard is to be hidden on startup
|
||||||
|
if (isStartup && mActivity.getProperties().shouldSoftKeyboardBeHiddenOnStartup()) {
|
||||||
|
Logger.logVerbose(LOG_TAG, "Hiding soft keyboard on startup");
|
||||||
|
// Required to keep keyboard hidden when Termux app is switched back from another app
|
||||||
|
KeyboardUtils.setSoftKeyboardAlwaysHiddenFlags(mActivity);
|
||||||
|
|
||||||
|
KeyboardUtils.hideSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||||
|
mActivity.getTerminalView().requestFocus();
|
||||||
|
noShowKeyboard = true;
|
||||||
|
// Required to keep keyboard hidden on app startup
|
||||||
|
mShowSoftKeyboardIgnoreOnce = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mActivity.getTerminalView().setOnFocusChangeListener(new View.OnFocusChangeListener() {
|
||||||
|
@Override
|
||||||
|
public void onFocusChange(View view, boolean hasFocus) {
|
||||||
|
// Force show soft keyboard if TerminalView or toolbar text input view has
|
||||||
|
// focus and close it if they don't
|
||||||
|
boolean textInputViewHasFocus = false;
|
||||||
|
final EditText textInputView = mActivity.findViewById(R.id.terminal_toolbar_text_input);
|
||||||
|
if (textInputView != null) textInputViewHasFocus = textInputView.hasFocus();
|
||||||
|
|
||||||
|
if (hasFocus || textInputViewHasFocus) {
|
||||||
|
if (mShowSoftKeyboardIgnoreOnce) {
|
||||||
|
mShowSoftKeyboardIgnoreOnce = false; return;
|
||||||
|
}
|
||||||
|
Logger.logVerbose(LOG_TAG, "Showing soft keyboard on focus change");
|
||||||
|
} else {
|
||||||
|
Logger.logVerbose(LOG_TAG, "Hiding soft keyboard on focus change");
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyboardUtils.setSoftKeyboardVisibility(getShowSoftKeyboardRunnable(), mActivity, mActivity.getTerminalView(), hasFocus || textInputViewHasFocus);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Do not force show soft keyboard if termux-reload-settings command was run with hardware keyboard
|
||||||
|
// or soft keyboard is to be hidden or is disabled
|
||||||
|
if (!isReloadTermuxProperties && !noShowKeyboard) {
|
||||||
|
// Request focus for TerminalView
|
||||||
|
// Also show the keyboard, since onFocusChange will not be called if TerminalView already
|
||||||
|
// had focus on startup to show the keyboard, like when opening url with context menu
|
||||||
|
// "Select URL" long press and returning to Termux app with back button. This
|
||||||
|
// will also show keyboard even if it was closed before opening url. #2111
|
||||||
|
Logger.logVerbose(LOG_TAG, "Requesting TerminalView focus and showing soft keyboard");
|
||||||
|
mActivity.getTerminalView().requestFocus();
|
||||||
|
mActivity.getTerminalView().postDelayed(getShowSoftKeyboardRunnable(), 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Runnable getShowSoftKeyboardRunnable() {
|
||||||
|
if (mShowSoftKeyboardRunnable == null) {
|
||||||
|
mShowSoftKeyboardRunnable = () -> {
|
||||||
|
KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return mShowSoftKeyboardRunnable;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void setTerminalCursorBlinkerState(boolean start) {
|
||||||
|
if (start) {
|
||||||
|
// If set/update the cursor blinking rate is successful, then enable cursor blinker
|
||||||
|
if (mActivity.getTerminalView().setTerminalCursorBlinkerRate(mActivity.getProperties().getTerminalCursorBlinkRate()))
|
||||||
|
mActivity.getTerminalView().setTerminalCursorBlinkerState(true, true);
|
||||||
|
else
|
||||||
|
Logger.logError(LOG_TAG,"Failed to start cursor blinker");
|
||||||
|
} else {
|
||||||
|
// Disable cursor blinker
|
||||||
|
mActivity.getTerminalView().setTerminalCursorBlinkerState(false, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void shareSessionTranscript() {
|
||||||
|
TerminalSession session = mActivity.getCurrentSession();
|
||||||
|
if (session == null) return;
|
||||||
|
|
||||||
|
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 = UrlUtils.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(AndroidUtils.getDeviceInfoMarkdownString(mActivity));
|
||||||
|
|
||||||
|
String termuxAptInfo = TermuxUtils.geAPTInfoMarkdownString(mActivity);
|
||||||
|
if (termuxAptInfo != null)
|
||||||
|
reportString.append("\n\n").append(termuxAptInfo);
|
||||||
|
|
||||||
|
ReportActivity.startReportActivity(mActivity, new ReportInfo(UserAction.REPORT_ISSUE_FROM_TRANSCRIPT.getName(), TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY_NAME, title, null, reportString.toString(), "\n\n" + TermuxUtils.getReportIssueMarkdownString(mActivity), false));
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void doPaste() {
|
||||||
|
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.content.Context;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
@@ -6,15 +6,15 @@ import android.os.Looper;
|
|||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.os.Vibrator;
|
import android.os.Vibrator;
|
||||||
|
|
||||||
public class BellUtil {
|
public class BellHandler {
|
||||||
private static BellUtil instance = null;
|
private static BellHandler instance = null;
|
||||||
private static final Object lock = new Object();
|
private static final Object lock = new Object();
|
||||||
|
|
||||||
public static BellUtil getInstance(Context context) {
|
public static BellHandler getInstance(Context context) {
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
if (instance == null) {
|
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 long lastBell = 0;
|
||||||
private final Runnable bellRunnable;
|
private final Runnable bellRunnable;
|
||||||
|
|
||||||
private BellUtil(final Vibrator vibrator) {
|
private BellHandler(final Vibrator vibrator) {
|
||||||
bellRunnable = new Runnable() {
|
bellRunnable = new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
@@ -47,7 +47,7 @@ public class BellUtil {
|
|||||||
if (timeSinceLastBell < 0) {
|
if (timeSinceLastBell < 0) {
|
||||||
// there is a next bell pending; don't schedule another one
|
// there is a next bell pending; don't schedule another one
|
||||||
} else if (timeSinceLastBell < MIN_PAUSE) {
|
} 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);
|
handler.postDelayed(bellRunnable, MIN_PAUSE - timeSinceLastBell);
|
||||||
lastBell = lastBell + MIN_PAUSE;
|
lastBell = lastBell + MIN_PAUSE;
|
||||||
} else {
|
} else {
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
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:
|
||||||
|
* https://stackoverflow.com/questions/7417123/android-how-to-adjust-layout-in-full-screen-mode-when-softkeyboard-is-visible
|
||||||
|
* and has some additional tweaks
|
||||||
|
* ---
|
||||||
|
* For more information, see https://issuetracker.google.com/issues/36911528
|
||||||
|
*/
|
||||||
|
public class FullScreenWorkAround {
|
||||||
|
private final View mChildOfContent;
|
||||||
|
private int mUsableHeightPrevious;
|
||||||
|
private final ViewGroup.LayoutParams mViewGroupLayoutParams;
|
||||||
|
|
||||||
|
private final int mNavBarHeight;
|
||||||
|
|
||||||
|
|
||||||
|
public static void apply(TermuxActivity activity) {
|
||||||
|
new FullScreenWorkAround(activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FullScreenWorkAround(TermuxActivity activity) {
|
||||||
|
ViewGroup content = activity.findViewById(android.R.id.content);
|
||||||
|
mChildOfContent = content.getChildAt(0);
|
||||||
|
mViewGroupLayoutParams = mChildOfContent.getLayoutParams();
|
||||||
|
mNavBarHeight = activity.getNavBarHeight();
|
||||||
|
mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(this::possiblyResizeChildOfContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void possiblyResizeChildOfContent() {
|
||||||
|
int usableHeightNow = computeUsableHeight();
|
||||||
|
if (usableHeightNow != mUsableHeightPrevious) {
|
||||||
|
int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();
|
||||||
|
int heightDifference = usableHeightSansKeyboard - usableHeightNow;
|
||||||
|
if (heightDifference > (usableHeightSansKeyboard / 4)) {
|
||||||
|
// keyboard probably just became visible
|
||||||
|
|
||||||
|
// ensures that usable layout space does not extend behind the
|
||||||
|
// soft keyboard, causing the extra keys to not be visible
|
||||||
|
mViewGroupLayoutParams.height = (usableHeightSansKeyboard - heightDifference) + getNavBarHeight();
|
||||||
|
} else {
|
||||||
|
// keyboard probably just became hidden
|
||||||
|
mViewGroupLayoutParams.height = usableHeightSansKeyboard;
|
||||||
|
}
|
||||||
|
mChildOfContent.requestLayout();
|
||||||
|
mUsableHeightPrevious = usableHeightNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getNavBarHeight() {
|
||||||
|
return mNavBarHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int computeUsableHeight() {
|
||||||
|
Rect r = new Rect();
|
||||||
|
mChildOfContent.getWindowVisibleDisplayFrame(r);
|
||||||
|
return (r.bottom - r.top);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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 com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||||
import androidx.annotation.Nullable;
|
import com.termux.shared.settings.properties.TermuxSharedProperties;
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public class ExtraKeysInfos {
|
public class ExtraKeysInfo {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Matrix of buttons displayed
|
* Matrix of buttons displayed
|
||||||
*/
|
*/
|
||||||
private ExtraKeyButton[][] buttons;
|
private final ExtraKeyButton[][] buttons;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This corresponds to one of the CharMapDisplay below
|
* 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;
|
this.style = style;
|
||||||
|
|
||||||
// Convert String propertiesInfo to Array of Arrays
|
// Convert String propertiesInfo to Array of Arrays
|
||||||
@@ -50,7 +47,7 @@ public class ExtraKeysInfos {
|
|||||||
|
|
||||||
ExtraKeyButton button;
|
ExtraKeyButton button;
|
||||||
|
|
||||||
if(! jobject.has("popup")) {
|
if (! jobject.has("popup")) {
|
||||||
// no popup
|
// no popup
|
||||||
button = new ExtraKeyButton(getSelectedCharMap(), jobject);
|
button = new ExtraKeyButton(getSelectedCharMap(), jobject);
|
||||||
} else {
|
} else {
|
||||||
@@ -70,10 +67,10 @@ public class ExtraKeysInfos {
|
|||||||
*/
|
*/
|
||||||
private static JSONObject normalizeKeyConfig(Object key) throws JSONException {
|
private static JSONObject normalizeKeyConfig(Object key) throws JSONException {
|
||||||
JSONObject jobject;
|
JSONObject jobject;
|
||||||
if(key instanceof String) {
|
if (key instanceof String) {
|
||||||
jobject = new JSONObject();
|
jobject = new JSONObject();
|
||||||
jobject.put("key", key);
|
jobject.put("key", key);
|
||||||
} else if(key instanceof JSONObject) {
|
} else if (key instanceof JSONObject) {
|
||||||
jobject = (JSONObject) key;
|
jobject = (JSONObject) key;
|
||||||
} else {
|
} else {
|
||||||
throw new JSONException("An key in the extra-key matrix must be a string or an object");
|
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> {
|
static class CleverMap<K,V> extends HashMap<K,V> {
|
||||||
V get(K key, V defaultValue) {
|
V get(K key, V defaultValue) {
|
||||||
if(containsKey(key))
|
if (containsKey(key))
|
||||||
return get(key);
|
return get(key);
|
||||||
else
|
else
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
@@ -151,7 +148,7 @@ public class ExtraKeysInfos {
|
|||||||
put("-", "―"); // U+2015 ― HORIZONTAL BAR
|
put("-", "―"); // U+2015 ― HORIZONTAL BAR
|
||||||
}};
|
}};
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* Multiple maps are available to quickly change
|
* Multiple maps are available to quickly change
|
||||||
* the style of the keys.
|
* the style of the keys.
|
||||||
*/
|
*/
|
||||||
@@ -245,6 +242,8 @@ public class ExtraKeysInfos {
|
|||||||
case "none":
|
case "none":
|
||||||
return new CharDisplayMap();
|
return new CharDisplayMap();
|
||||||
default:
|
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;
|
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.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@@ -6,6 +6,9 @@ import android.os.Build;
|
|||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
@@ -13,19 +16,19 @@ import java.util.concurrent.ScheduledExecutorService;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
import android.view.HapticFeedbackConstants;
|
import android.view.HapticFeedbackConstants;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.inputmethod.InputMethodManager;
|
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.GridLayout;
|
import android.widget.GridLayout;
|
||||||
import android.widget.PopupWindow;
|
import android.widget.PopupWindow;
|
||||||
import android.widget.ToggleButton;
|
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
|
import com.termux.app.terminal.TermuxTerminalViewClient;
|
||||||
import com.termux.view.TerminalView;
|
import com.termux.view.TerminalView;
|
||||||
|
|
||||||
import androidx.drawerlayout.widget.DrawerLayout;
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||||||
@@ -41,6 +44,8 @@ public final class ExtraKeysView extends GridLayout {
|
|||||||
private static final int INTERESTING_COLOR = 0xFF80DEEA;
|
private static final int INTERESTING_COLOR = 0xFF80DEEA;
|
||||||
private static final int BUTTON_PRESSED_COLOR = 0xFF7F7F7F;
|
private static final int BUTTON_PRESSED_COLOR = 0xFF7F7F7F;
|
||||||
|
|
||||||
|
TermuxTerminalViewClient mTermuxTerminalViewClient;
|
||||||
|
|
||||||
public ExtraKeysView(Context context, AttributeSet attrs) {
|
public ExtraKeysView(Context context, AttributeSet attrs) {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
}
|
}
|
||||||
@@ -75,16 +80,18 @@ public final class ExtraKeysView extends GridLayout {
|
|||||||
put("F12", KeyEvent.KEYCODE_F12);
|
put("F12", KeyEvent.KEYCODE_F12);
|
||||||
}};
|
}};
|
||||||
|
|
||||||
|
@SuppressLint("RtlHardcoded")
|
||||||
private void sendKey(View view, String keyName, boolean forceCtrlDown, boolean forceLeftAltDown) {
|
private void sendKey(View view, String keyName, boolean forceCtrlDown, boolean forceLeftAltDown) {
|
||||||
TerminalView terminalView = view.findViewById(R.id.terminal_view);
|
TerminalView terminalView = view.findViewById(R.id.terminal_view);
|
||||||
if ("KEYBOARD".equals(keyName)) {
|
if ("KEYBOARD".equals(keyName)) {
|
||||||
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
if(mTermuxTerminalViewClient != null)
|
||||||
imm.toggleSoftInput(0, 0);
|
mTermuxTerminalViewClient.onToggleSoftKeyboardRequest();
|
||||||
} else if ("DRAWER".equals(keyName)) {
|
} else if ("DRAWER".equals(keyName)) {
|
||||||
DrawerLayout drawer = view.findViewById(R.id.drawer_layout);
|
DrawerLayout drawer = view.findViewById(R.id.drawer_layout);
|
||||||
drawer.openDrawer(Gravity.LEFT);
|
drawer.openDrawer(Gravity.LEFT);
|
||||||
} else if (keyCodesForString.containsKey(keyName)) {
|
} else if (keyCodesForString.containsKey(keyName)) {
|
||||||
int keyCode = keyCodesForString.get(keyName);
|
Integer keyCode = keyCodesForString.get(keyName);
|
||||||
|
if (keyCode == null) return;
|
||||||
int metaState = 0;
|
int metaState = 0;
|
||||||
if (forceCtrlDown) {
|
if (forceCtrlDown) {
|
||||||
metaState |= KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON;
|
metaState |= KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON;
|
||||||
@@ -129,15 +136,27 @@ public final class ExtraKeysView extends GridLayout {
|
|||||||
|
|
||||||
private static class SpecialButtonState {
|
private static class SpecialButtonState {
|
||||||
boolean isOn = false;
|
boolean isOn = false;
|
||||||
ToggleButton button = null;
|
boolean isActive = false;
|
||||||
|
List<Button> buttons = new ArrayList<>();
|
||||||
|
|
||||||
|
void setIsActive(boolean value) {
|
||||||
|
isActive = value;
|
||||||
|
buttons.forEach(button -> button.setTextColor(value ? INTERESTING_COLOR : TEXT_COLOR));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<SpecialButton, SpecialButtonState> specialButtons = new HashMap<SpecialButton, SpecialButtonState>() {{
|
private final Map<SpecialButton, SpecialButtonState> specialButtons = new HashMap<SpecialButton, SpecialButtonState>() {{
|
||||||
put(SpecialButton.CTRL, new SpecialButtonState());
|
put(SpecialButton.CTRL, new SpecialButtonState());
|
||||||
put(SpecialButton.ALT, new SpecialButtonState());
|
put(SpecialButton.ALT, new SpecialButtonState());
|
||||||
put(SpecialButton.FN, new SpecialButtonState());
|
put(SpecialButton.FN, new SpecialButtonState());
|
||||||
}};
|
}};
|
||||||
|
|
||||||
|
private final Set<String> specialButtonsKeys = specialButtons.keySet().stream().map(Enum::name).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
private boolean isSpecialButton(ExtraKeyButton button) {
|
||||||
|
return specialButtonsKeys.contains(button.getKey());
|
||||||
|
}
|
||||||
|
|
||||||
private ScheduledExecutorService scheduledExecutor;
|
private ScheduledExecutorService scheduledExecutor;
|
||||||
private PopupWindow popupWindow;
|
private PopupWindow popupWindow;
|
||||||
private int longPressCount;
|
private int longPressCount;
|
||||||
@@ -147,30 +166,38 @@ public final class ExtraKeysView extends GridLayout {
|
|||||||
if (state == null)
|
if (state == null)
|
||||||
throw new RuntimeException("Must be a valid special button (see source)");
|
throw new RuntimeException("Must be a valid special button (see source)");
|
||||||
|
|
||||||
if (! state.isOn)
|
if (!state.isOn || !state.isActive)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (state.button == null) {
|
state.setIsActive(false);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.button.isPressed())
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (! state.button.isChecked())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
state.button.setChecked(false);
|
|
||||||
state.button.setTextColor(TEXT_COLOR);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void popup(View view, String text) {
|
private Button createSpecialButton(String buttonKey, boolean needUpdate) {
|
||||||
|
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonKey));
|
||||||
|
if (state == null) return null;
|
||||||
|
state.isOn = true;
|
||||||
|
Button button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||||
|
button.setTextColor(state.isActive ? INTERESTING_COLOR : TEXT_COLOR);
|
||||||
|
if (needUpdate) {
|
||||||
|
state.buttons.add(button);
|
||||||
|
}
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
void popup(View view, ExtraKeyButton extraButton) {
|
||||||
int width = view.getMeasuredWidth();
|
int width = view.getMeasuredWidth();
|
||||||
int height = view.getMeasuredHeight();
|
int height = view.getMeasuredHeight();
|
||||||
Button button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
Button button;
|
||||||
button.setText(text);
|
if (isSpecialButton(extraButton)) {
|
||||||
button.setTextColor(TEXT_COLOR);
|
button = createSpecialButton(extraButton.getKey(), false);
|
||||||
|
if (button == null) return;
|
||||||
|
} else {
|
||||||
|
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||||
|
button.setTextColor(TEXT_COLOR);
|
||||||
|
}
|
||||||
|
button.setText(extraButton.getDisplay());
|
||||||
button.setPadding(0, 0, 0, 0);
|
button.setPadding(0, 0, 0, 0);
|
||||||
button.setMinHeight(0);
|
button.setMinHeight(0);
|
||||||
button.setMinWidth(0);
|
button.setMinWidth(0);
|
||||||
@@ -214,12 +241,12 @@ public final class ExtraKeysView extends GridLayout {
|
|||||||
* "-_-" will input the string "-_-"
|
* "-_-" will input the string "-_-"
|
||||||
*/
|
*/
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
void reload(ExtraKeysInfos infos) {
|
public void reload(ExtraKeysInfo infos) {
|
||||||
if(infos == null)
|
if (infos == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
for(SpecialButtonState state : specialButtons.values())
|
for(SpecialButtonState state : specialButtons.values())
|
||||||
state.button = null;
|
state.buttons = new ArrayList<>();
|
||||||
|
|
||||||
removeAllViews();
|
removeAllViews();
|
||||||
|
|
||||||
@@ -233,11 +260,9 @@ public final class ExtraKeysView extends GridLayout {
|
|||||||
final ExtraKeyButton buttonInfo = buttons[row][col];
|
final ExtraKeyButton buttonInfo = buttons[row][col];
|
||||||
|
|
||||||
Button button;
|
Button button;
|
||||||
if(Arrays.asList("CTRL", "ALT", "FN").contains(buttonInfo.getKey())) {
|
if (isSpecialButton(buttonInfo)) {
|
||||||
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonInfo.getKey())); // for valueOf: https://stackoverflow.com/a/604426/1980630
|
button = createSpecialButton(buttonInfo.getKey(), true);
|
||||||
state.isOn = true;
|
if (button == null) return;
|
||||||
button = state.button = new ToggleButton(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
|
||||||
button.setClickable(true);
|
|
||||||
} else {
|
} else {
|
||||||
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||||
}
|
}
|
||||||
@@ -262,10 +287,10 @@ public final class ExtraKeysView extends GridLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
View root = getRootView();
|
View root = getRootView();
|
||||||
if (Arrays.asList("CTRL", "ALT", "FN").contains(buttonInfo.getKey())) {
|
if (isSpecialButton(buttonInfo)) {
|
||||||
ToggleButton self = (ToggleButton) finalButton;
|
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonInfo.getKey()));
|
||||||
self.setChecked(self.isChecked());
|
if (state == null) return;
|
||||||
self.setTextColor(self.isChecked() ? INTERESTING_COLOR : TEXT_COLOR);
|
state.setIsActive(!state.isActive);
|
||||||
} else {
|
} else {
|
||||||
sendKey(root, buttonInfo);
|
sendKey(root, buttonInfo);
|
||||||
}
|
}
|
||||||
@@ -295,8 +320,7 @@ public final class ExtraKeysView extends GridLayout {
|
|||||||
scheduledExecutor = null;
|
scheduledExecutor = null;
|
||||||
}
|
}
|
||||||
v.setBackgroundColor(BUTTON_COLOR);
|
v.setBackgroundColor(BUTTON_COLOR);
|
||||||
String extraButtonDisplayedText = buttonInfo.getPopup().getDisplay();
|
popup(v, buttonInfo.getPopup());
|
||||||
popup(v, extraButtonDisplayedText);
|
|
||||||
}
|
}
|
||||||
if (popupWindow != null && event.getY() > 0) {
|
if (popupWindow != null && event.getY() > 0) {
|
||||||
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
|
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
|
||||||
@@ -325,7 +349,13 @@ public final class ExtraKeysView extends GridLayout {
|
|||||||
popupWindow.dismiss();
|
popupWindow.dismiss();
|
||||||
popupWindow = null;
|
popupWindow = null;
|
||||||
if (buttonInfo.getPopup() != null) {
|
if (buttonInfo.getPopup() != null) {
|
||||||
sendKey(root, buttonInfo.getPopup());
|
if (isSpecialButton(buttonInfo.getPopup())) {
|
||||||
|
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonInfo.getPopup().getKey()));
|
||||||
|
if (state == null) return true;
|
||||||
|
state.setIsActive(!state.isActive);
|
||||||
|
} else {
|
||||||
|
sendKey(root, buttonInfo.getPopup());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
v.performClick();
|
v.performClick();
|
||||||
@@ -351,4 +381,8 @@ public final class ExtraKeysView extends GridLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setTermuxTerminalViewClient(TermuxTerminalViewClient termuxTerminalViewClient) {
|
||||||
|
this.mTermuxTerminalViewClient = termuxTerminalViewClient;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
198
app/src/main/java/com/termux/app/utils/CrashUtils.java
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
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.shared.activities.ReportActivity;
|
||||||
|
import com.termux.shared.models.errors.Error;
|
||||||
|
import com.termux.shared.notification.NotificationUtils;
|
||||||
|
import com.termux.shared.file.FileUtils;
|
||||||
|
import com.termux.shared.models.ReportInfo;
|
||||||
|
import com.termux.app.models.UserAction;
|
||||||
|
import com.termux.shared.notification.TermuxNotificationUtils;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxPreferenceConstants;
|
||||||
|
import com.termux.shared.data.DataUtils;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.termux.AndroidUtils;
|
||||||
|
import com.termux.shared.termux.TermuxUtils;
|
||||||
|
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
|
||||||
|
public class CrashUtils {
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "CrashUtils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify the user of an app crash at last run by reading the crash info from the crash log file
|
||||||
|
* at {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}. The crash log file would have been
|
||||||
|
* created by {@link com.termux.shared.crash.CrashHandler}.
|
||||||
|
*
|
||||||
|
* If the crash log file exists and is not empty and
|
||||||
|
* {@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 notifyAppCrashOnLastRun(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;
|
||||||
|
|
||||||
|
Error error;
|
||||||
|
StringBuilder reportStringBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
// Read report string from crash log file
|
||||||
|
error = FileUtils.readStringFromFile("crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, Charset.defaultCharset(), reportStringBuilder, false);
|
||||||
|
if (error != null) {
|
||||||
|
Logger.logErrorExtended(logTag, error.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move crash log file to backup location if it exists
|
||||||
|
error = FileUtils.moveRegularFile("crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, TermuxConstants.TERMUX_CRASH_LOG_BACKUP_FILE_PATH, true);
|
||||||
|
if (error != null) {
|
||||||
|
Logger.logErrorExtended(logTag, error.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
String reportString = reportStringBuilder.toString();
|
||||||
|
|
||||||
|
if (reportString.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Logger.logDebug(logTag, "A crash log file found at \"" + TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH + "\".");
|
||||||
|
|
||||||
|
sendCrashReportNotification(context, logTag, reportString, false, false);
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a crash report notification for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID}
|
||||||
|
* and {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} for operations.
|
||||||
|
* @param logTag The log tag to use for logging.
|
||||||
|
* @param message The message for the crash report.
|
||||||
|
* @param forceNotification If set to {@code true}, then a notification will be shown
|
||||||
|
* regardless of if pending intent is {@code null} or
|
||||||
|
* {@link TermuxPreferenceConstants.TERMUX_APP#KEY_CRASH_REPORT_NOTIFICATIONS_ENABLED}
|
||||||
|
* is {@code false}.
|
||||||
|
* @param addAppAndDeviceInfo If set to {@code true}, then app and device info will be appended
|
||||||
|
* to the message.
|
||||||
|
*/
|
||||||
|
public static void sendCrashReportNotification(final Context context, String logTag, String message, boolean forceNotification, boolean addAppAndDeviceInfo) {
|
||||||
|
if (context == null) return;
|
||||||
|
|
||||||
|
TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
|
||||||
|
if (preferences == null) return;
|
||||||
|
|
||||||
|
// If user has disabled notifications for crashes
|
||||||
|
if (!preferences.areCrashReportNotificationsEnabled() && !forceNotification)
|
||||||
|
return;
|
||||||
|
|
||||||
|
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||||
|
|
||||||
|
// Send a notification to show the crash log which when clicked will open the {@link ReportActivity}
|
||||||
|
// to show the details of the crash
|
||||||
|
String title = TermuxConstants.TERMUX_APP_NAME + " Crash Report";
|
||||||
|
|
||||||
|
Logger.logDebug(logTag, "Sending \"" + title + "\" notification.");
|
||||||
|
|
||||||
|
StringBuilder reportString = new StringBuilder(message);
|
||||||
|
|
||||||
|
if (addAppAndDeviceInfo) {
|
||||||
|
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, true));
|
||||||
|
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.CRASH_REPORT.getName(), logTag, title, null, reportString.toString(), "\n\n" + TermuxUtils.getReportIssueMarkdownString(context), true));
|
||||||
|
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
|
||||||
|
// Setup the notification channel if not already set up
|
||||||
|
setupCrashReportsNotificationChannel(context);
|
||||||
|
|
||||||
|
// Build the notification
|
||||||
|
Notification.Builder builder = getCrashReportsNotificationBuilder(context, title, null, null, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
|
||||||
|
if (builder == null) return;
|
||||||
|
|
||||||
|
// Send the notification
|
||||||
|
int nextNotificationId = TermuxNotificationUtils.getNextNotificationId(context);
|
||||||
|
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
|
||||||
|
if (notificationManager != null)
|
||||||
|
notificationManager.notify(nextNotificationId, builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get {@link Notification.Builder} for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID}
|
||||||
|
* and {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}.
|
||||||
|
*
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
307
app/src/main/java/com/termux/app/utils/PluginUtils.java
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
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.shared.activities.ReportActivity;
|
||||||
|
import com.termux.shared.file.TermuxFileUtils;
|
||||||
|
import com.termux.shared.models.ResultConfig;
|
||||||
|
import com.termux.shared.models.ResultData;
|
||||||
|
import com.termux.shared.models.errors.Errno;
|
||||||
|
import com.termux.shared.models.errors.Error;
|
||||||
|
import com.termux.shared.notification.NotificationUtils;
|
||||||
|
import com.termux.shared.notification.TermuxNotificationUtils;
|
||||||
|
import com.termux.shared.shell.ResultSender;
|
||||||
|
import com.termux.shared.shell.ShellUtils;
|
||||||
|
import com.termux.shared.termux.AndroidUtils;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||||
|
import com.termux.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.shared.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 {
|
||||||
|
|
||||||
|
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 ResultConfig#resultPendingIntent} or {@link ResultConfig#resultDirectoryPath}
|
||||||
|
* is not {@code null}, then the result of commands are sent back to the command caller.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} that will be used to send result intent to the {@link PendingIntent} creator.
|
||||||
|
* @param logTag The log tag to use for logging.
|
||||||
|
* @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);
|
||||||
|
Error error = null;
|
||||||
|
ResultData resultData = executionCommand.resultData;
|
||||||
|
|
||||||
|
if (!executionCommand.hasExecuted()) {
|
||||||
|
Logger.logWarn(logTag, executionCommand.getCommandIdAndLabelLogString() + ": Ignoring call to processPluginExecutionCommandResult() since the execution command state is not higher than the ExecutionState.EXECUTED");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isPluginExecutionCommandWithPendingResult = executionCommand.isPluginExecutionCommandWithPendingResult();
|
||||||
|
|
||||||
|
// Log the output. ResultData should not be logged if pending result since ResultSender will do it
|
||||||
|
Logger.logDebugExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, !isPluginExecutionCommandWithPendingResult));
|
||||||
|
|
||||||
|
// If execution command was started by a plugin which expects the result back
|
||||||
|
if (isPluginExecutionCommandWithPendingResult) {
|
||||||
|
// Set variables which will be used by sendCommandResultData to send back the result
|
||||||
|
if (executionCommand.resultConfig.resultPendingIntent != null)
|
||||||
|
setPluginResultPendingIntentVariables(executionCommand);
|
||||||
|
if (executionCommand.resultConfig.resultDirectoryPath != null)
|
||||||
|
setPluginResultDirectoryVariables(executionCommand);
|
||||||
|
|
||||||
|
// Send result to caller
|
||||||
|
error = ResultSender.sendCommandResultData(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.resultConfig, executionCommand.resultData);
|
||||||
|
if (error != null) {
|
||||||
|
// error will be added to existing Errors
|
||||||
|
resultData.setStateFailed(error);
|
||||||
|
Logger.logDebugExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, true));
|
||||||
|
|
||||||
|
// Flash and send notification for the error
|
||||||
|
Logger.showToast(context, ResultData.getErrorsListMinimalString(resultData), true);
|
||||||
|
sendPluginCommandErrorNotification(context, logTag, executionCommand, ResultData.getErrorsListMinimalString(resultData));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!executionCommand.isStateFailed() && error == null)
|
||||||
|
executionCommand.setState(ExecutionCommand.ExecutionState.SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process {@link ExecutionCommand} error.
|
||||||
|
*
|
||||||
|
* The ExecutionCommand currentState must be equal to {@link ExecutionCommand.ExecutionState#FAILED}.
|
||||||
|
* The {@link ResultData#getErrCode()} must have been set to a value greater than
|
||||||
|
* {@link Errno#ERRNO_SUCCESS}.
|
||||||
|
* The {@link ResultData#errorsList} must also be set with appropriate error info.
|
||||||
|
*
|
||||||
|
* If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and
|
||||||
|
* {@link ResultConfig#resultPendingIntent} or {@link ResultConfig#resultDirectoryPath}
|
||||||
|
* is not {@code null}, then the errors of commands are sent back to the command caller.
|
||||||
|
*
|
||||||
|
* Otherwise if the {@link TERMUX_APP#KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED} is
|
||||||
|
* enabled, then a flash and a notification will be shown for the error as well
|
||||||
|
* 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);
|
||||||
|
Error error;
|
||||||
|
ResultData resultData = executionCommand.resultData;
|
||||||
|
|
||||||
|
if (!executionCommand.isStateFailed()) {
|
||||||
|
Logger.logWarn(logTag, executionCommand.getCommandIdAndLabelLogString() + ": Ignoring call to processPluginExecutionCommandError() since the execution command is not in ExecutionState.FAILED");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isPluginExecutionCommandWithPendingResult = executionCommand.isPluginExecutionCommandWithPendingResult();
|
||||||
|
|
||||||
|
// Log the error and any exception. ResultData should not be logged if pending result since ResultSender will do it
|
||||||
|
Logger.logErrorExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, !isPluginExecutionCommandWithPendingResult));
|
||||||
|
|
||||||
|
// If execution command was started by a plugin which expects the result back
|
||||||
|
if (isPluginExecutionCommandWithPendingResult) {
|
||||||
|
// Set variables which will be used by sendCommandResultData to send back the result
|
||||||
|
if (executionCommand.resultConfig.resultPendingIntent != null)
|
||||||
|
setPluginResultPendingIntentVariables(executionCommand);
|
||||||
|
if (executionCommand.resultConfig.resultDirectoryPath != null)
|
||||||
|
setPluginResultDirectoryVariables(executionCommand);
|
||||||
|
|
||||||
|
// Send result to caller
|
||||||
|
error = ResultSender.sendCommandResultData(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.resultConfig, executionCommand.resultData);
|
||||||
|
if (error != null) {
|
||||||
|
// error will be added to existing Errors
|
||||||
|
resultData.setStateFailed(error);
|
||||||
|
Logger.logErrorExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, true));
|
||||||
|
forceNotification = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 commands, then just return
|
||||||
|
if (!preferences.arePluginErrorNotificationsEnabled() && !forceNotification)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Flash and send notification for the error
|
||||||
|
Logger.showToast(context, ResultData.getErrorsListMinimalString(resultData), true);
|
||||||
|
sendPluginCommandErrorNotification(context, logTag, executionCommand, ResultData.getErrorsListMinimalString(resultData));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set variables which will be used by {@link ResultSender#sendCommandResultData(Context, String, String, ResultConfig, ResultData)}
|
||||||
|
* to send back the result via {@link ResultConfig#resultPendingIntent}. */
|
||||||
|
public static void setPluginResultPendingIntentVariables(ExecutionCommand executionCommand) {
|
||||||
|
ResultConfig resultConfig = executionCommand.resultConfig;
|
||||||
|
|
||||||
|
resultConfig.resultBundleKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE;
|
||||||
|
resultConfig.resultStdoutKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT;
|
||||||
|
resultConfig.resultStdoutOriginalLengthKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH;
|
||||||
|
resultConfig.resultStderrKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR;
|
||||||
|
resultConfig.resultStderrOriginalLengthKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH;
|
||||||
|
resultConfig.resultExitCodeKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE;
|
||||||
|
resultConfig.resultErrCodeKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERR;
|
||||||
|
resultConfig.resultErrmsgKey = TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set variables which will be used by {@link ResultSender#sendCommandResultData(Context, String, String, ResultConfig, ResultData)}
|
||||||
|
* to send back the result by writing it to files in {@link ResultConfig#resultDirectoryPath}. */
|
||||||
|
public static void setPluginResultDirectoryVariables(ExecutionCommand executionCommand) {
|
||||||
|
ResultConfig resultConfig = executionCommand.resultConfig;
|
||||||
|
|
||||||
|
resultConfig.resultDirectoryPath = TermuxFileUtils.getCanonicalPath(resultConfig.resultDirectoryPath, null, true);
|
||||||
|
resultConfig.resultDirectoryAllowedParentPath = TermuxFileUtils.getMatchedAllowedTermuxWorkingDirectoryParentPathForPath(resultConfig.resultDirectoryPath);
|
||||||
|
|
||||||
|
// Set default resultFileBasename if resultSingleFile is true to `<executable_basename>-<timestamp>.log`
|
||||||
|
if (resultConfig.resultSingleFile && resultConfig.resultFileBasename == null)
|
||||||
|
resultConfig.resultFileBasename = ShellUtils.getExecutableBasename(executionCommand.executable) + "-" + AndroidUtils.getCurrentMilliSecondLocalTimeStamp() + ".log";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an error notification for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
|
||||||
|
* and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} for operations.
|
||||||
|
* @param executionCommand The {@link ExecutionCommand} that failed.
|
||||||
|
* @param notificationTextString The text of the notification.
|
||||||
|
*/
|
||||||
|
public static void sendPluginCommandErrorNotification(Context context, String logTag, ExecutionCommand executionCommand, String notificationTextString) {
|
||||||
|
// Send a notification to show the error which when clicked will open the ReportActivity
|
||||||
|
// to show the details of the error
|
||||||
|
String title = TermuxConstants.TERMUX_APP_NAME + " Plugin Execution Command Error";
|
||||||
|
|
||||||
|
StringBuilder reportString = new StringBuilder();
|
||||||
|
|
||||||
|
reportString.append(ExecutionCommand.getExecutionCommandMarkdownString(executionCommand));
|
||||||
|
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, true));
|
||||||
|
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context));
|
||||||
|
|
||||||
|
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.PLUGIN_EXECUTION_COMMAND.getName(), logTag, title, null, reportString.toString(), null,true));
|
||||||
|
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
|
||||||
|
// Setup the notification channel if not already set up
|
||||||
|
setupPluginCommandErrorsNotificationChannel(context);
|
||||||
|
|
||||||
|
// Use markdown in notification
|
||||||
|
CharSequence notificationTextCharSequence = MarkdownUtils.getSpannedMarkdownText(context, notificationTextString);
|
||||||
|
//CharSequence notificationTextCharSequence = notificationTextString;
|
||||||
|
|
||||||
|
// Build the notification
|
||||||
|
Notification.Builder builder = getPluginCommandErrorsNotificationBuilder(context, title, notificationTextCharSequence, notificationTextCharSequence, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
|
||||||
|
if (builder == null) return;
|
||||||
|
|
||||||
|
// Send the notification
|
||||||
|
int nextNotificationId = TermuxNotificationUtils.getNextNotificationId(context);
|
||||||
|
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
|
||||||
|
if (notificationManager != null)
|
||||||
|
notificationManager.notify(nextNotificationId, builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get {@link Notification.Builder} for {@link TermuxConstants#TERMUX_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 error} if policy is violated, otherwise {@code null}.
|
||||||
|
*/
|
||||||
|
public static String checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(final Context context) {
|
||||||
|
String errmsg = null;
|
||||||
|
if (!SharedProperties.isPropertyValueTrue(context, TermuxPropertyConstants.getTermuxPropertiesFile(), TermuxConstants.PROP_ALLOW_EXTERNAL_APPS, 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 android.webkit.MimeTypeMap;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.app.TermuxService;
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
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
|
* 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/>
|
* <p/>
|
||||||
* Note that this replaces providing an activity matching the ACTION_GET_CONTENT intent:
|
* Note that this replaces providing an activity matching the ACTION_GET_CONTENT intent:
|
||||||
* <p/>
|
* <p/>
|
||||||
@@ -35,7 +35,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
|||||||
|
|
||||||
private static final String ALL_MIME_TYPES = "*/*";
|
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
|
// The default columns to return information about a root if no specific
|
||||||
@@ -63,9 +63,9 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
|
public Cursor queryRoots(String[] projection) {
|
||||||
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_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();
|
final MatrixCursor.RowBuilder row = result.newRow();
|
||||||
row.add(Root.COLUMN_ROOT_ID, getDocIdForFile(BASE_DIR));
|
row.add(Root.COLUMN_ROOT_ID, getDocIdForFile(BASE_DIR));
|
||||||
@@ -75,7 +75,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
|||||||
row.add(Root.COLUMN_TITLE, applicationName);
|
row.add(Root.COLUMN_TITLE, applicationName);
|
||||||
row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES);
|
row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES);
|
||||||
row.add(Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.getFreeSpace());
|
row.add(Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.getFreeSpace());
|
||||||
row.add(Root.COLUMN_ICON, R.drawable.ic_launcher);
|
row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,9 +91,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
|||||||
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
|
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
|
||||||
final File parent = getFileForDocId(parentDocumentId);
|
final File parent = getFileForDocId(parentDocumentId);
|
||||||
for (File file : parent.listFiles()) {
|
for (File file : parent.listFiles()) {
|
||||||
if (!file.getName().startsWith(".")) {
|
includeFile(result, null, file);
|
||||||
includeFile(result, null, file);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -169,16 +167,15 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
|||||||
final int MAX_SEARCH_RESULTS = 50;
|
final int MAX_SEARCH_RESULTS = 50;
|
||||||
while (!pending.isEmpty() && result.getCount() < MAX_SEARCH_RESULTS) {
|
while (!pending.isEmpty() && result.getCount() < MAX_SEARCH_RESULTS) {
|
||||||
final File file = pending.removeFirst();
|
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).
|
// through the whole SD card).
|
||||||
boolean isInsideHome;
|
boolean isInsideHome;
|
||||||
try {
|
try {
|
||||||
isInsideHome = file.getCanonicalPath().startsWith(TermuxService.HOME_PATH);
|
isInsideHome = file.getCanonicalPath().startsWith(TermuxConstants.TERMUX_HOME_DIR_PATH);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
isInsideHome = true;
|
isInsideHome = true;
|
||||||
}
|
}
|
||||||
final boolean isHidden = file.getName().startsWith(".");
|
if (isInsideHome) {
|
||||||
if (isInsideHome && !isHidden) {
|
|
||||||
if (file.isDirectory()) {
|
if (file.isDirectory()) {
|
||||||
Collections.addAll(pending, file.listFiles());
|
Collections.addAll(pending, file.listFiles());
|
||||||
} else {
|
} else {
|
||||||
@@ -265,7 +262,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
|||||||
row.add(Document.COLUMN_MIME_TYPE, mimeType);
|
row.add(Document.COLUMN_MIME_TYPE, mimeType);
|
||||||
row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
|
row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
|
||||||
row.add(Document.COLUMN_FLAGS, flags);
|
row.add(Document.COLUMN_FLAGS, flags);
|
||||||
row.add(Document.COLUMN_ICON, R.drawable.ic_launcher);
|
row.add(Document.COLUMN_ICON, R.mipmap.ic_launcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import android.content.Intent;
|
|||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.provider.OpenableColumns;
|
import android.provider.OpenableColumns;
|
||||||
import android.util.Log;
|
|
||||||
import android.util.Patterns;
|
import android.util.Patterns;
|
||||||
|
|
||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.app.DialogUtils;
|
import com.termux.shared.interact.TextInputDialogUtils;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||||
import com.termux.app.TermuxService;
|
import com.termux.app.TermuxService;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -25,9 +27,9 @@ import java.util.regex.Pattern;
|
|||||||
|
|
||||||
public class TermuxFileReceiverActivity extends Activity {
|
public class TermuxFileReceiverActivity extends Activity {
|
||||||
|
|
||||||
static final String TERMUX_RECEIVEDIR = TermuxService.FILES_PATH + "/home/downloads";
|
static final String TERMUX_RECEIVEDIR = TermuxConstants.TERMUX_FILES_DIR_PATH + "/home/downloads";
|
||||||
static final String EDITOR_PROGRAM = TermuxService.HOME_PATH + "/bin/termux-file-editor";
|
static final String EDITOR_PROGRAM = TermuxConstants.TERMUX_HOME_DIR_PATH + "/bin/termux-file-editor";
|
||||||
static final String URL_OPENER_PROGRAM = TermuxService.HOME_PATH + "/bin/termux-url-opener";
|
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
|
* 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;
|
boolean mFinishOnDismissNameDialog = true;
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "TermuxFileReceiverActivity";
|
||||||
|
|
||||||
static boolean isSharedTextAnUrl(String sharedText) {
|
static boolean isSharedTextAnUrl(String sharedText) {
|
||||||
return Patterns.WEB_URL.matcher(sharedText).matches()
|
return Patterns.WEB_URL.matcher(sharedText).matches()
|
||||||
|| Pattern.matches("magnet:\\?xt=urn:btih:.*?", sharedText);
|
|| Pattern.matches("magnet:\\?xt=urn:btih:.*?", sharedText);
|
||||||
@@ -109,12 +113,12 @@ public class TermuxFileReceiverActivity extends Activity {
|
|||||||
promptNameAndSave(in, attachmentFileName);
|
promptNameAndSave(in, attachmentFileName);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
showErrorDialogAndQuit("Unable to handle shared content:\n\n" + e.getMessage());
|
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) {
|
void promptNameAndSave(final InputStream in, final String attachmentFileName) {
|
||||||
DialogUtils.textInput(this, R.string.file_received_title, attachmentFileName, R.string.file_received_edit_button, text -> {
|
TextInputDialogUtils.textInput(this, R.string.title_file_received, attachmentFileName, R.string.action_file_received_edit, text -> {
|
||||||
File outFile = saveStreamWithName(in, text);
|
File outFile = saveStreamWithName(in, text);
|
||||||
if (outFile == null) return;
|
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();
|
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.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);
|
startService(executeIntent);
|
||||||
finish();
|
finish();
|
||||||
},
|
},
|
||||||
R.string.file_received_open_folder_button, text -> {
|
R.string.action_file_received_open_directory, text -> {
|
||||||
if (saveStreamWithName(in, text) == null) return;
|
if (saveStreamWithName(in, text) == null) return;
|
||||||
|
|
||||||
Intent executeIntent = new Intent(TermuxService.ACTION_EXECUTE);
|
Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE);
|
||||||
executeIntent.putExtra(TermuxService.EXTRA_CURRENT_WORKING_DIRECTORY, TERMUX_RECEIVEDIR);
|
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, TERMUX_RECEIVEDIR);
|
||||||
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
||||||
startService(executeIntent);
|
startService(executeIntent);
|
||||||
finish();
|
finish();
|
||||||
@@ -169,7 +173,7 @@ public class TermuxFileReceiverActivity extends Activity {
|
|||||||
return outFile;
|
return outFile;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
showErrorDialogAndQuit("Error saving file:\n\n" + 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;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,9 +192,9 @@ public class TermuxFileReceiverActivity extends Activity {
|
|||||||
|
|
||||||
final Uri urlOpenerProgramUri = new Uri.Builder().scheme("file").path(URL_OPENER_PROGRAM).build();
|
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.setClass(TermuxFileReceiverActivity.this, TermuxService.class);
|
||||||
executeIntent.putExtra(TermuxService.EXTRA_ARGUMENTS, new String[]{url});
|
executeIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, new String[]{url});
|
||||||
startService(executeIntent);
|
startService(executeIntent);
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:height="108dp"
|
|
||||||
android:width="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
|
|
||||||
<path
|
|
||||||
android:fillColor="#000000"
|
|
||||||
android:pathData="M18,54
|
|
||||||
A36,36 0 1,1 90,54
|
|
||||||
A36,36 0 1,1 18,54 Z" />
|
|
||||||
|
|
||||||
<!-- Keep in sync with adaptive ic_foreground.xml: -->
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M34,38
|
|
||||||
h6
|
|
||||||
l12,16
|
|
||||||
l-12,16
|
|
||||||
h-6
|
|
||||||
l12,-16
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M56,66
|
|
||||||
h18
|
|
||||||
v4
|
|
||||||
h-18
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</vector>
|
|
||||||
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>
|
||||||
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>
|
||||||
109
app/src/main/res/layout/activity_termux.xml
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<com.termux.app.terminal.TermuxActivityRootView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/activity_termux_root_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<androidx.drawerlayout.widget.DrawerLayout
|
||||||
|
android:id="@+id/drawer_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:layout_above="@+id/terminal_toolbar_view_pager"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<com.termux.view.TerminalView
|
||||||
|
android:id="@+id/terminal_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginRight="3dp"
|
||||||
|
android:layout_marginLeft="3dp"
|
||||||
|
android:focusableInTouchMode="true"
|
||||||
|
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
android:importantForAutofill="no"
|
||||||
|
android:autofillHints="password" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/left_drawer"
|
||||||
|
android:layout_width="240dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="start"
|
||||||
|
android:background="@android:color/white"
|
||||||
|
android:choiceMode="singleChoice"
|
||||||
|
android:divider="@android:color/transparent"
|
||||||
|
android:dividerHeight="0dp"
|
||||||
|
android:descendantFocusability="blocksDescendants"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/settings_button"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:src="@drawable/ic_settings"
|
||||||
|
android:background="@null"
|
||||||
|
android:contentDescription="@string/action_open_settings" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ListView
|
||||||
|
android:id="@+id/terminal_sessions_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_gravity="top"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:choiceMode="singleChoice"
|
||||||
|
android:longClickable="true" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
style="?android:attr/buttonBarStyle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/toggle_keyboard_button"
|
||||||
|
style="?android:attr/buttonBarButtonStyle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/action_toggle_soft_keyboard" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/new_session_button"
|
||||||
|
style="?android:attr/buttonBarButtonStyle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/action_new_session" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.drawerlayout.widget.DrawerLayout>
|
||||||
|
|
||||||
|
<androidx.viewpager.widget.ViewPager
|
||||||
|
android:id="@+id/terminal_toolbar_view_pager"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="37.5dp"
|
||||||
|
android:background="@android:drawable/screen_background_dark_transparent"
|
||||||
|
android:layout_alignParentBottom="true" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/activity_termux_bottom_space_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="@android:color/transparent" />
|
||||||
|
|
||||||
|
</com.termux.app.terminal.TermuxActivityRootView>
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:fitsSystemWindows="true">
|
|
||||||
|
|
||||||
<androidx.drawerlayout.widget.DrawerLayout
|
|
||||||
android:id="@+id/drawer_layout"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_alignParentTop="true"
|
|
||||||
android:layout_above="@+id/viewpager"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<com.termux.view.TerminalView
|
|
||||||
android:id="@+id/terminal_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_marginRight="3dp"
|
|
||||||
android:layout_marginLeft="3dp"
|
|
||||||
android:focusableInTouchMode="true"
|
|
||||||
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
|
|
||||||
android:scrollbars="vertical"
|
|
||||||
android:importantForAutofill="no"
|
|
||||||
android:autofillHints="password" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/left_drawer"
|
|
||||||
android:layout_width="240dp"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_gravity="start"
|
|
||||||
android:background="@android:color/white"
|
|
||||||
android:choiceMode="singleChoice"
|
|
||||||
android:divider="@android:color/transparent"
|
|
||||||
android:dividerHeight="0dp"
|
|
||||||
android:descendantFocusability="blocksDescendants"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<ListView
|
|
||||||
android:id="@+id/left_drawer_list"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_gravity="top"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:choiceMode="singleChoice"
|
|
||||||
android:longClickable="true" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
style="?android:attr/buttonBarStyle"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/toggle_keyboard_button"
|
|
||||||
style="?android:attr/buttonBarButtonStyle"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:text="@string/toggle_soft_keyboard" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/new_session_button"
|
|
||||||
style="?android:attr/buttonBarButtonStyle"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:text="@string/new_session" />
|
|
||||||
</LinearLayout>
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</androidx.drawerlayout.widget.DrawerLayout>
|
|
||||||
|
|
||||||
<androidx.viewpager.widget.ViewPager
|
|
||||||
android:id="@+id/viewpager"
|
|
||||||
android:visibility="gone"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="37.5dp"
|
|
||||||
android:background="@android:drawable/screen_background_dark_transparent"
|
|
||||||
android:layout_alignParentBottom="true" />
|
|
||||||
</RelativeLayout>
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
<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_width="fill_parent"
|
||||||
android:layout_height="?android:attr/listPreferredItemHeight"
|
android:layout_height="?android:attr/listPreferredItemHeight"
|
||||||
android:background="@drawable/selected_session_background"
|
android:background="@drawable/session_background_selected"
|
||||||
android:ellipsize="marquee"
|
android:ellipsize="marquee"
|
||||||
android:gravity="start|center_vertical"
|
android:gravity="start|center_vertical"
|
||||||
android:padding="6dip"
|
android:padding="6dip"
|
||||||
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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<com.termux.app.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
|
<com.termux.app.terminal.io.extrakeys.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:id="@+id/extra_keys"
|
android:id="@+id/terminal_toolbar_extra_keys"
|
||||||
style="?android:attr/buttonBarStyle"
|
style="?android:attr/buttonBarStyle"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<EditText xmlns:android="http://schemas.android.com/apk/res/android"
|
<EditText xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
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_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:imeOptions="actionSend|flagNoFullscreen"
|
android:imeOptions="actionSend|flagNoFullscreen"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:inputType="text"
|
android:inputType="text"
|
||||||
|
android:importantForAutofill="no"
|
||||||
android:textColor="@android:color/white"
|
android:textColor="@android:color/white"
|
||||||
android:textColorHighlight="@android:color/darker_gray"
|
android:textColorHighlight="@android:color/darker_gray"
|
||||||
android:paddingTop="0dp"
|
android:paddingTop="0dp"
|
||||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@android:color/black"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
3
app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
</resources>
|
||||||
@@ -1,54 +1,196 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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>
|
<!DOCTYPE resources [
|
||||||
<string name="bootstrap_error_title">Unable to install</string>
|
<!ENTITY TERMUX_PACKAGE_NAME "com.termux">
|
||||||
<string name="bootstrap_error_body">Termux was unable to install the bootstrap packages.</string>
|
<!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_abort">Abort</string>
|
||||||
<string name="bootstrap_error_try_again">Try again</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>
|
<!-- Terminal Sidebar and Shortcuts -->
|
||||||
<string name="select_url_dialog_title">Click URL to copy or long press to open</string>
|
<string name="action_new_session">New session</string>
|
||||||
<string name="select_all_and_share">Share transcript</string>
|
<string name="action_new_session_failsafe">Failsafe</string>
|
||||||
<string name="select_url_no_found">No URL found in the terminal.</string>
|
<string name="title_max_terminals_reached">Max terminals reached</string>
|
||||||
<string name="select_url_copied_to_clipboard">URL copied to clipboard</string>
|
<string name="msg_max_terminals_reached">Close down existing ones before creating new.</string>
|
||||||
<string name="share_transcript_chooser_title">Send text to:</string>
|
|
||||||
|
|
||||||
<string name="kill_process">Kill process (%d)</string>
|
<string name="title_rename_session">Set session name</string>
|
||||||
<string name="confirm_kill_process">Really kill this session?</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="action_toggle_soft_keyboard">Keyboard</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="styling_not_installed">The Termux:Style add-on is not installed.</string>
|
<string name="msg_enabling_terminal_toolbar">Enabling Terminal Toolbar</string>
|
||||||
<string name="styling_install">Install</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_exit">Exit</string>
|
||||||
<string name="notification_action_wake_lock">Acquire wakelock</string>
|
<string name="notification_action_wake_lock">Acquire wakelock</string>
|
||||||
<string name="notification_action_wake_unlock">Release 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>
|
<!-- TermuxService -->
|
||||||
|
<string name="error_display_over_other_apps_permission_not_granted">&TERMUX_APP_NAME; requires \"Display over other apps\" permission to start terminal sessions from background on Android >= 10. Grants it from Settings -> Apps -> &TERMUX_APP_NAME; -> Advanced</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Termux RunCommandService -->
|
||||||
|
<string name="error_run_command_service_invalid_intent_action">Invalid intent action to RunCommandService: `%1$s`</string>
|
||||||
|
<string name="error_run_command_service_mandatory_extra_missing">Mandatory extra missing to RunCommandService: \"%1$s\"</string>
|
||||||
|
<string name="error_run_command_service_allow_external_apps_ungranted">RunCommandService require `allow-external-apps` property to be set to `true` in `&TERMUX_PROPERTIES_PRIMARY_PATH_SHORT;` file.</string>
|
||||||
|
<string name="error_run_command_service_api_help">Visit %1$s for more info on RUN_COMMAND Intent usage.</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Termux Execution Commands -->
|
||||||
|
<string name="msg_executable_absolute_path">Executable Absolute Path: \"%1$s\"</string>
|
||||||
|
<string name="msg_working_directory_absolute_path">Working Directory Absolute Path: \"%1$s\"</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Termux 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>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Terminal View Preferences -->
|
||||||
|
<string name="termux_terminal_view_preferences_title">Terminal View</string>
|
||||||
|
<string name="termux_terminal_view_preferences_summary">Preferences for terminal view</string>
|
||||||
|
|
||||||
|
<!-- View Category -->
|
||||||
|
<string name="termux_terminal_view_view_header">View</string>
|
||||||
|
|
||||||
|
<!-- Terminal View Margin Adjustment -->
|
||||||
|
<string name="termux_terminal_view_terminal_margin_adjustment_title">Terminal Margin Adjustment</string>
|
||||||
|
<string name="termux_terminal_view_terminal_margin_adjustment_off">Terminal margin adjustment will be disabled.</string>
|
||||||
|
<string name="termux_terminal_view_terminal_margin_adjustment_on">Terminal margin adjustment will be enabled. It should be enabled to try to fix the issue where soft keyboard covers part of extra keys/terminal view. If it causes screen flickering on your devices, then disable it. (Default)</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Termux 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>
|
</resources>
|
||||||
|
|||||||
@@ -21,10 +21,6 @@
|
|||||||
<item name="android:windowAllowEnterTransitionOverlap">true</item>
|
<item name="android:windowAllowEnterTransitionOverlap">true</item>
|
||||||
</style>
|
</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. -->
|
<!-- 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. -->
|
<!-- NOTE: Cannot use "Light." since it hides the terminal scrollbar on the default black background. -->
|
||||||
@@ -46,4 +42,11 @@
|
|||||||
<item name="android:windowAllowReturnTransitionOverlap">true</item>
|
<item name="android:windowAllowReturnTransitionOverlap">true</item>
|
||||||
<item name="android:windowAllowEnterTransitionOverlap">true</item>
|
<item name="android:windowAllowEnterTransitionOverlap">true</item>
|
||||||
</style>
|
</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>
|
</resources>
|
||||||
|
|||||||
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"
|
<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
|
<shortcut
|
||||||
android:shortcutId="new_session"
|
android:shortcutId="new_session"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:icon="@drawable/ic_new_session"
|
android:icon="@drawable/ic_new_session"
|
||||||
android:shortcutShortLabel="@string/new_session"
|
android:shortcutShortLabel="@string/action_new_session"
|
||||||
tools:targetApi="n_mr1">
|
tools:targetApi="n_mr1">
|
||||||
<intent
|
<intent
|
||||||
android:action="android.intent.action.RUN"
|
android:action="android.intent.action.RUN"
|
||||||
android:targetPackage="com.termux"
|
android:targetPackage="com.termux"
|
||||||
android:targetClass="com.termux.app.TermuxActivity"/>
|
android:targetClass="com.termux.app.TermuxActivity"
|
||||||
|
android:name="android.shortcut.conversation"/>
|
||||||
</shortcut>
|
</shortcut>
|
||||||
|
|
||||||
<shortcut
|
<shortcut
|
||||||
android:shortcutId="new_failsafe_session"
|
android:shortcutId="new_failsafe_session"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:icon="@drawable/ic_new_session"
|
android:icon="@drawable/ic_new_session"
|
||||||
android:shortcutShortLabel="@string/new_session_failsafe"
|
android:shortcutShortLabel="@string/action_new_session_failsafe"
|
||||||
tools:targetApi="n_mr1">
|
tools:targetApi="n_mr1">
|
||||||
<intent
|
<intent
|
||||||
android:action="android.intent.action.RUN"
|
android:action="android.intent.action.RUN"
|
||||||
android:targetPackage="com.termux"
|
android:targetPackage="com.termux"
|
||||||
android:targetClass="com.termux.app.TermuxActivity">
|
android:targetClass="com.termux.app.TermuxActivity"
|
||||||
<extra android:name="com.termux.app.failsafe_session" android:value="true" />
|
android:name="android.shortcut.conversation">
|
||||||
|
<extra android:name="com.termux.app.failsafe_session" android:value="true"/>
|
||||||
</intent>
|
</intent>
|
||||||
</shortcut>
|
</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>
|
</shortcuts>
|
||||||
|
|||||||
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>
|
||||||
18
app/src/main/res/xml/termux_preferences.xml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<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"/>
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
app:title="@string/termux_terminal_view_preferences_title"
|
||||||
|
app:summary="@string/termux_terminal_view_preferences_summary"
|
||||||
|
app:fragment="com.termux.app.fragments.settings.termux.TerminalViewPreferencesFragment"/>
|
||||||
|
|
||||||
|
</PreferenceScreen>
|
||||||
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
@@ -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
@@ -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>
|
||||||
15
app/src/main/res/xml/termux_terminal_view_preferences.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<PreferenceCategory
|
||||||
|
app:key="view"
|
||||||
|
app:title="@string/termux_terminal_view_view_header">
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:key="terminal_margin_adjustment"
|
||||||
|
app:summaryOff="@string/termux_terminal_view_terminal_margin_adjustment_off"
|
||||||
|
app:summaryOn="@string/termux_terminal_view_terminal_margin_adjustment_on"
|
||||||
|
app:title="@string/termux_terminal_view_terminal_margin_adjustment_title" />
|
||||||
|
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
</PreferenceScreen>
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.termux.app;
|
package com.termux.app;
|
||||||
|
|
||||||
|
import com.termux.shared.data.UrlUtils;
|
||||||
|
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
@@ -11,7 +13,7 @@ public class TermuxActivityTest {
|
|||||||
private void assertUrlsAre(String text, String... urls) {
|
private void assertUrlsAre(String text, String... urls) {
|
||||||
LinkedHashSet<String> expected = new LinkedHashSet<>();
|
LinkedHashSet<String> expected = new LinkedHashSet<>();
|
||||||
Collections.addAll(expected, urls);
|
Collections.addAll(expected, urls);
|
||||||
Assert.assertEquals(expected, TermuxActivity.extractUrls(text));
|
Assert.assertEquals(expected, UrlUtils.extractUrls(text));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
BIN
art/ic_launcher2.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
art/ic_launcher2_round.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
@@ -1,17 +1,18 @@
|
|||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
jcenter()
|
mavenCentral()
|
||||||
google()
|
google()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:4.0.1'
|
classpath 'com.android.tools.build:gradle:4.2.1'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
mavenCentral()
|
||||||
|
maven { url "https://jitpack.io" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
13
fastlane/metadata/android/en-US/full_description.txt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
Termux is a terminal emulator application enhanced with a large set of command line utilities ported to Android OS. The main goal is to bring a Linux command line experience to users of mobile devices with no rooting or other special setup required.
|
||||||
|
|
||||||
|
* Enjoy the Bash and Zsh shells.
|
||||||
|
* Edit files with nano and vim.
|
||||||
|
* Access servers over SSH.
|
||||||
|
* Compile C/C++ code with clang.
|
||||||
|
* Use the Python console as a pocket calculator.
|
||||||
|
* Check out projects with Git and Subversion.
|
||||||
|
* Run text-based games with frotz.
|
||||||
|
|
||||||
|
At first start a small base system is being configured. The GNU Bash, Coreutils, Findutils and other core utilities are available out-of-box. Additionally, we provide more than 1000 other packages installable by using the 'pkg' utility which currently is a frontend for the 'apt' package manager. All provided software has been patched and compiled with Android NDK to provide max compatibility with Android OS.
|
||||||
|
|
||||||
|
To learn more about application usage tips and tricks, long-press anywhere on the terminal and select the Help menu option to access Termux Wiki. This resource is also accessible directly in a web browser: https://wiki.termux.com/wiki/Main_Page.
|
||||||
BIN
fastlane/metadata/android/en-US/images/icon.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/4.jpg
Normal file
|
After Width: | Height: | Size: 158 KiB |
1
fastlane/metadata/android/en-US/short_description.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Terminal emulator app with a large set of command line utilities
|
||||||
@@ -17,5 +17,8 @@ android.useAndroidX=true
|
|||||||
|
|
||||||
minSdkVersion=24
|
minSdkVersion=24
|
||||||
targetSdkVersion=28
|
targetSdkVersion=28
|
||||||
ndkVersion=21.3.6528147
|
ndkVersion=22.1.7171670
|
||||||
compileSdkVersion=28
|
compileSdkVersion=30
|
||||||
|
|
||||||
|
markwonVersion=4.6.2
|
||||||
|
|
||||||
|
|||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-all.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
2
jitpack.yml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
env:
|
||||||
|
JITPACK_NDK_VERSION: "21.1.6352462"
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
// Start https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle
|
|
||||||
group = publishedGroupId // Maven Group ID for the artifact
|
|
||||||
install {
|
|
||||||
repositories.mavenInstaller {
|
|
||||||
pom {
|
|
||||||
project {
|
|
||||||
packaging 'aar'
|
|
||||||
groupId publishedGroupId
|
|
||||||
artifactId artifact
|
|
||||||
|
|
||||||
name libraryName
|
|
||||||
description libraryDescription
|
|
||||||
url siteUrl
|
|
||||||
|
|
||||||
licenses {
|
|
||||||
license {
|
|
||||||
name 'GNU General Public License version 3'
|
|
||||||
url 'https://opensource.org/licenses/gpl-3.0.html'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
developers {
|
|
||||||
developer {
|
|
||||||
id 'fornwall'
|
|
||||||
name 'Fredrik Fornwall'
|
|
||||||
email 'fredrik@fornwall.net'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scm {
|
|
||||||
connection gitUrl
|
|
||||||
developerConnection gitUrl
|
|
||||||
url siteUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// End https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle
|
|
||||||
|
|
||||||
// Start https://raw.githubusercontent.com/nuuneoi/JCenter/master/bintrayv1.gradle
|
|
||||||
apply plugin: 'com.jfrog.bintray'
|
|
||||||
|
|
||||||
version = libraryVersion
|
|
||||||
|
|
||||||
task sourcesJar(type: Jar) {
|
|
||||||
classifier = 'sources'
|
|
||||||
from android.sourceSets.main.java.srcDirs
|
|
||||||
}
|
|
||||||
|
|
||||||
artifacts {
|
|
||||||
archives sourcesJar
|
|
||||||
}
|
|
||||||
|
|
||||||
bintray {
|
|
||||||
user = System.getenv('BINTRAY_USER')
|
|
||||||
key = System.getenv('BINTRAY_API_KEY')
|
|
||||||
|
|
||||||
configurations = ['archives']
|
|
||||||
pkg {
|
|
||||||
repo = 'maven'
|
|
||||||
name = bintrayName
|
|
||||||
userOrg = 'termux'
|
|
||||||
desc = libraryDescription
|
|
||||||
websiteUrl = siteUrl
|
|
||||||
vcsUrl = gitUrl
|
|
||||||
licenses = ['GPL-3.0']
|
|
||||||
publish = true
|
|
||||||
publicDownloadNumbers = true
|
|
||||||
version {
|
|
||||||
desc = libraryDescription
|
|
||||||
gpg {
|
|
||||||
sign = false //Determines whether to GPG sign the files. The default is false
|
|
||||||
// passphrase = properties.getProperty("bintray.gpg.password")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +1 @@
|
|||||||
include ':app', ':terminal-emulator', ':terminal-view'
|
include ':app', ':termux-shared', ':terminal-emulator', ':terminal-view'
|
||||||
|
|||||||
@@ -1,24 +1,9 @@
|
|||||||
plugins {
|
|
||||||
id "com.jfrog.bintray" version "1.7.3"
|
|
||||||
id "com.github.dcendents.android-maven" version "2.0"
|
|
||||||
}
|
|
||||||
|
|
||||||
apply plugin: 'com.android.library'
|
apply plugin: 'com.android.library'
|
||||||
|
apply plugin: 'maven-publish'
|
||||||
ext {
|
|
||||||
bintrayName = 'terminal-emulator'
|
|
||||||
publishedGroupId = 'com.termux'
|
|
||||||
libraryName = 'TerminalEmulator'
|
|
||||||
artifact = 'terminal-emulator'
|
|
||||||
libraryDescription = 'The terminal emulator used in Termux'
|
|
||||||
siteUrl = 'https://github.com/termux/termux'
|
|
||||||
gitUrl = 'https://github.com/termux/termux.git'
|
|
||||||
libraryVersion = '0.52'
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
||||||
ndkVersion project.properties.ndkVersion
|
ndkVersion = System.getenv("JITPACK_NDK_VERSION") ?: project.properties.ndkVersion
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion project.properties.minSdkVersion.toInteger()
|
minSdkVersion project.properties.minSdkVersion.toInteger()
|
||||||
@@ -52,6 +37,10 @@ android {
|
|||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests.returnDefaultValues = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(Test) {
|
tasks.withType(Test) {
|
||||||
@@ -61,7 +50,25 @@ tasks.withType(Test) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation 'junit:junit:4.13'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: '../scripts/bintray-publish.gradle'
|
task sourceJar(type: Jar) {
|
||||||
|
from android.sourceSets.main.java.srcDirs
|
||||||
|
classifier "sources"
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEvaluate {
|
||||||
|
publishing {
|
||||||
|
publications {
|
||||||
|
// Creates a Maven publication called "release".
|
||||||
|
release(MavenPublication) {
|
||||||
|
from components.release
|
||||||
|
groupId = 'com.termux'
|
||||||
|
artifactId = 'terminal-emulator'
|
||||||
|
version = '0.117'
|
||||||
|
artifact(sourceJar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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";
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -59,6 +59,7 @@ public final class KeyHandler {
|
|||||||
public static final int KEYMOD_ALT = 0x80000000;
|
public static final int KEYMOD_ALT = 0x80000000;
|
||||||
public static final int KEYMOD_CTRL = 0x40000000;
|
public static final int KEYMOD_CTRL = 0x40000000;
|
||||||
public static final int KEYMOD_SHIFT = 0x20000000;
|
public static final int KEYMOD_SHIFT = 0x20000000;
|
||||||
|
public static final int KEYMOD_NUM_LOCK = 0x10000000;
|
||||||
|
|
||||||
private static final Map<String, Integer> TERMCAP_TO_KEYCODE = new HashMap<>();
|
private static final Map<String, Integer> TERMCAP_TO_KEYCODE = new HashMap<>();
|
||||||
|
|
||||||
@@ -145,10 +146,16 @@ public final class KeyHandler {
|
|||||||
keyMod |= KEYMOD_ALT;
|
keyMod |= KEYMOD_ALT;
|
||||||
keyCode &= ~KEYMOD_ALT;
|
keyCode &= ~KEYMOD_ALT;
|
||||||
}
|
}
|
||||||
|
if ((keyCode & KEYMOD_NUM_LOCK) != 0) {
|
||||||
|
keyMod |= KEYMOD_NUM_LOCK;
|
||||||
|
keyCode &= ~KEYMOD_NUM_LOCK;
|
||||||
|
}
|
||||||
return getCode(keyCode, keyMod, cursorKeysApplication, keypadApplication);
|
return getCode(keyCode, keyMod, cursorKeysApplication, keypadApplication);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getCode(int keyCode, int keyMode, boolean cursorApp, boolean keypadApplication) {
|
public static String getCode(int keyCode, int keyMode, boolean cursorApp, boolean keypadApplication) {
|
||||||
|
boolean numLockOn = (keyMode & KEYMOD_NUM_LOCK) != 0;
|
||||||
|
keyMode &= ~KEYMOD_NUM_LOCK;
|
||||||
switch (keyCode) {
|
switch (keyCode) {
|
||||||
case KEYCODE_DPAD_CENTER:
|
case KEYCODE_DPAD_CENTER:
|
||||||
return "\015";
|
return "\015";
|
||||||
@@ -228,8 +235,11 @@ public final class KeyHandler {
|
|||||||
// Just do what xterm and gnome-terminal does:
|
// Just do what xterm and gnome-terminal does:
|
||||||
return prefix + (((keyMode & KEYMOD_CTRL) == 0) ? "\u007F" : "\u0008");
|
return prefix + (((keyMode & KEYMOD_CTRL) == 0) ? "\u007F" : "\u0008");
|
||||||
case KEYCODE_NUM_LOCK:
|
case KEYCODE_NUM_LOCK:
|
||||||
return "\033OP";
|
if (keypadApplication) {
|
||||||
|
return "\033OP";
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case KEYCODE_SPACE:
|
case KEYCODE_SPACE:
|
||||||
// If ctrl is not down, return null so that it goes through normal input processing (which may e.g. cause a
|
// If ctrl is not down, return null so that it goes through normal input processing (which may e.g. cause a
|
||||||
// combining accent to be written):
|
// combining accent to be written):
|
||||||
@@ -249,31 +259,81 @@ public final class KeyHandler {
|
|||||||
case KEYCODE_NUMPAD_COMMA:
|
case KEYCODE_NUMPAD_COMMA:
|
||||||
return ",";
|
return ",";
|
||||||
case KEYCODE_NUMPAD_DOT:
|
case KEYCODE_NUMPAD_DOT:
|
||||||
return keypadApplication ? "\033On" : ".";
|
if (numLockOn) {
|
||||||
|
return keypadApplication ? "\033On" : ".";
|
||||||
|
} else {
|
||||||
|
// DELETE
|
||||||
|
return transformForModifiers("\033[3", keyMode, '~');
|
||||||
|
}
|
||||||
case KEYCODE_NUMPAD_SUBTRACT:
|
case KEYCODE_NUMPAD_SUBTRACT:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'm') : "-";
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'm') : "-";
|
||||||
case KEYCODE_NUMPAD_DIVIDE:
|
case KEYCODE_NUMPAD_DIVIDE:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'o') : "/";
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'o') : "/";
|
||||||
case KEYCODE_NUMPAD_0:
|
case KEYCODE_NUMPAD_0:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'p') : "0";
|
if (numLockOn) {
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'p') : "0";
|
||||||
|
} else {
|
||||||
|
// INSERT
|
||||||
|
return transformForModifiers("\033[2", keyMode, '~');
|
||||||
|
}
|
||||||
case KEYCODE_NUMPAD_1:
|
case KEYCODE_NUMPAD_1:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'q') : "1";
|
if (numLockOn) {
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'q') : "1";
|
||||||
|
} else {
|
||||||
|
// END
|
||||||
|
return (keyMode == 0) ? (cursorApp ? "\033OF" : "\033[F") : transformForModifiers("\033[1", keyMode, 'F');
|
||||||
|
}
|
||||||
case KEYCODE_NUMPAD_2:
|
case KEYCODE_NUMPAD_2:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'r') : "2";
|
if (numLockOn) {
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'r') : "2";
|
||||||
|
} else {
|
||||||
|
// DOWN
|
||||||
|
return (keyMode == 0) ? (cursorApp ? "\033OB" : "\033[B") : transformForModifiers("\033[1", keyMode, 'B');
|
||||||
|
}
|
||||||
case KEYCODE_NUMPAD_3:
|
case KEYCODE_NUMPAD_3:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 's') : "3";
|
if (numLockOn) {
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 's') : "3";
|
||||||
|
} else {
|
||||||
|
// PGDN
|
||||||
|
return "\033[6~";
|
||||||
|
}
|
||||||
case KEYCODE_NUMPAD_4:
|
case KEYCODE_NUMPAD_4:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 't') : "4";
|
if (numLockOn) {
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 't') : "4";
|
||||||
|
} else {
|
||||||
|
// LEFT
|
||||||
|
return (keyMode == 0) ? (cursorApp ? "\033OD" : "\033[D") : transformForModifiers("\033[1", keyMode, 'D');
|
||||||
|
}
|
||||||
case KEYCODE_NUMPAD_5:
|
case KEYCODE_NUMPAD_5:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'u') : "5";
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'u') : "5";
|
||||||
case KEYCODE_NUMPAD_6:
|
case KEYCODE_NUMPAD_6:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'v') : "6";
|
if (numLockOn) {
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'v') : "6";
|
||||||
|
} else {
|
||||||
|
// RIGHT
|
||||||
|
return (keyMode == 0) ? (cursorApp ? "\033OC" : "\033[C") : transformForModifiers("\033[1", keyMode, 'C');
|
||||||
|
}
|
||||||
case KEYCODE_NUMPAD_7:
|
case KEYCODE_NUMPAD_7:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'w') : "7";
|
if (numLockOn) {
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'w') : "7";
|
||||||
|
} else {
|
||||||
|
// HOME
|
||||||
|
return (keyMode == 0) ? (cursorApp ? "\033OH" : "\033[H") : transformForModifiers("\033[1", keyMode, 'H');
|
||||||
|
}
|
||||||
case KEYCODE_NUMPAD_8:
|
case KEYCODE_NUMPAD_8:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'x') : "8";
|
if (numLockOn) {
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'x') : "8";
|
||||||
|
} else {
|
||||||
|
// UP
|
||||||
|
return (keyMode == 0) ? (cursorApp ? "\033OA" : "\033[A") : transformForModifiers("\033[1", keyMode, 'A');
|
||||||
|
}
|
||||||
case KEYCODE_NUMPAD_9:
|
case KEYCODE_NUMPAD_9:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'y') : "9";
|
if (numLockOn) {
|
||||||
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'y') : "9";
|
||||||
|
} else {
|
||||||
|
// PGUP
|
||||||
|
return "\033[5~";
|
||||||
|
}
|
||||||
case KEYCODE_NUMPAD_EQUALS:
|
case KEYCODE_NUMPAD_EQUALS:
|
||||||
return keypadApplication ? transformForModifiers("\033O", keyMode, 'X') : "=";
|
return keypadApplication ? transformForModifiers("\033O", keyMode, 'X') : "=";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.termux.terminal;
|
package com.termux.terminal;
|
||||||
|
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@@ -39,10 +38,6 @@ public final class TerminalEmulator {
|
|||||||
public static final int MOUSE_WHEELUP_BUTTON = 64;
|
public static final int MOUSE_WHEELUP_BUTTON = 64;
|
||||||
public static final int MOUSE_WHEELDOWN_BUTTON = 65;
|
public static final int MOUSE_WHEELDOWN_BUTTON = 65;
|
||||||
|
|
||||||
public static final int CURSOR_STYLE_BLOCK = 0;
|
|
||||||
public static final int CURSOR_STYLE_UNDERLINE = 1;
|
|
||||||
public static final int CURSOR_STYLE_BAR = 2;
|
|
||||||
|
|
||||||
/** Used for invalid data - http://en.wikipedia.org/wiki/Replacement_character#Replacement_character */
|
/** Used for invalid data - http://en.wikipedia.org/wiki/Replacement_character#Replacement_character */
|
||||||
public static final int UNICODE_REPLACEMENT_CHAR = 0xFFFD;
|
public static final int UNICODE_REPLACEMENT_CHAR = 0xFFFD;
|
||||||
|
|
||||||
@@ -109,8 +104,8 @@ public final class TerminalEmulator {
|
|||||||
* characters received when the cursor is at the right border of the page replace characters already on the page."
|
* 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;
|
private static final int DECSET_BIT_AUTOWRAP = 1 << 3;
|
||||||
/** DECSET 25 - if the cursor should be visible, {@link #isShowingCursor()}. */
|
/** DECSET 25 - if the cursor should be enabled, {@link #isCursorEnabled()}. */
|
||||||
private static final int DECSET_BIT_SHOWING_CURSOR = 1 << 4;
|
private static final int DECSET_BIT_CURSOR_ENABLED = 1 << 4;
|
||||||
private static final int DECSET_BIT_APPLICATION_KEYPAD = 1 << 5;
|
private static final int DECSET_BIT_APPLICATION_KEYPAD = 1 << 5;
|
||||||
/** DECSET 1000 - if to report mouse press&release events. */
|
/** DECSET 1000 - if to report mouse press&release events. */
|
||||||
private static final int DECSET_BIT_MOUSE_TRACKING_PRESS_RELEASE = 1 << 6;
|
private static final int DECSET_BIT_MOUSE_TRACKING_PRESS_RELEASE = 1 << 6;
|
||||||
@@ -127,17 +122,35 @@ public final class TerminalEmulator {
|
|||||||
/** Not really DECSET bit... - http://www.vt100.net/docs/vt510-rm/DECSACE */
|
/** Not really DECSET bit... - http://www.vt100.net/docs/vt510-rm/DECSACE */
|
||||||
private static final int DECSET_BIT_RECTANGULAR_CHANGEATTRIBUTE = 1 << 12;
|
private static final int DECSET_BIT_RECTANGULAR_CHANGEATTRIBUTE = 1 << 12;
|
||||||
|
|
||||||
|
|
||||||
private String mTitle;
|
private String mTitle;
|
||||||
private final Stack<String> mTitleStack = new Stack<>();
|
private final Stack<String> mTitleStack = new Stack<>();
|
||||||
|
|
||||||
|
|
||||||
/** The cursor position. Between (0,0) and (mRows-1, mColumns-1). */
|
/** The cursor position. Between (0,0) and (mRows-1, mColumns-1). */
|
||||||
private int mCursorRow, mCursorCol;
|
private int mCursorRow, mCursorCol;
|
||||||
|
|
||||||
private int mCursorStyle = CURSOR_STYLE_BLOCK;
|
|
||||||
|
|
||||||
/** The number of character rows and columns in the terminal screen. */
|
/** The number of character rows and columns in the terminal screen. */
|
||||||
public int mRows, mColumns;
|
public int mRows, mColumns;
|
||||||
|
|
||||||
|
/** The number of terminal transcript rows that can be scrolled back to. */
|
||||||
|
public static final int TERMINAL_TRANSCRIPT_ROWS_MIN = 100;
|
||||||
|
public static final int TERMINAL_TRANSCRIPT_ROWS_MAX = 50000;
|
||||||
|
public static final int DEFAULT_TERMINAL_TRANSCRIPT_ROWS = 2000;
|
||||||
|
|
||||||
|
|
||||||
|
/* The supported terminal cursor styles. */
|
||||||
|
|
||||||
|
public static final int TERMINAL_CURSOR_STYLE_BLOCK = 0;
|
||||||
|
public static final int TERMINAL_CURSOR_STYLE_UNDERLINE = 1;
|
||||||
|
public static final int TERMINAL_CURSOR_STYLE_BAR = 2;
|
||||||
|
public static final int DEFAULT_TERMINAL_CURSOR_STYLE = TERMINAL_CURSOR_STYLE_BLOCK;
|
||||||
|
public static final Integer[] TERMINAL_CURSOR_STYLES_LIST = new Integer[]{TERMINAL_CURSOR_STYLE_BLOCK, TERMINAL_CURSOR_STYLE_UNDERLINE, TERMINAL_CURSOR_STYLE_BAR};
|
||||||
|
|
||||||
|
/** The terminal cursor styles. */
|
||||||
|
private int mCursorStyle = DEFAULT_TERMINAL_CURSOR_STYLE;
|
||||||
|
|
||||||
|
|
||||||
/** The normal screen buffer. Stores the characters that appear on the screen of the emulated terminal. */
|
/** The normal screen buffer. Stores the characters that appear on the screen of the emulated terminal. */
|
||||||
private final TerminalBuffer mMainBuffer;
|
private final TerminalBuffer mMainBuffer;
|
||||||
/**
|
/**
|
||||||
@@ -153,6 +166,8 @@ public final class TerminalEmulator {
|
|||||||
/** The terminal session this emulator is bound to. */
|
/** The terminal session this emulator is bound to. */
|
||||||
private final TerminalOutput mSession;
|
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. */
|
/** Keeps track of the current argument of the current escape sequence. Ranges from 0 to MAX_ESCAPE_PARAMETERS-1. */
|
||||||
private int mArgIndex;
|
private int mArgIndex;
|
||||||
/** Holds the arguments of the current escape sequence. */
|
/** Holds the arguments of the current escape sequence. */
|
||||||
@@ -204,6 +219,18 @@ public final class TerminalEmulator {
|
|||||||
*/
|
*/
|
||||||
private boolean mAboutToAutoWrap;
|
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.
|
* 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.
|
* For a 24-bit value the top byte (0xff000000) is set.
|
||||||
@@ -227,6 +254,8 @@ public final class TerminalEmulator {
|
|||||||
|
|
||||||
public final TerminalColors mColors = new TerminalColors();
|
public final TerminalColors mColors = new TerminalColors();
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "TerminalEmulator";
|
||||||
|
|
||||||
private boolean isDecsetInternalBitSet(int bit) {
|
private boolean isDecsetInternalBitSet(int bit) {
|
||||||
return (mCurrentDecSetFlags & bit) != 0;
|
return (mCurrentDecSetFlags & bit) != 0;
|
||||||
}
|
}
|
||||||
@@ -258,7 +287,7 @@ public final class TerminalEmulator {
|
|||||||
case 7:
|
case 7:
|
||||||
return DECSET_BIT_AUTOWRAP;
|
return DECSET_BIT_AUTOWRAP;
|
||||||
case 25:
|
case 25:
|
||||||
return DECSET_BIT_SHOWING_CURSOR;
|
return DECSET_BIT_CURSOR_ENABLED;
|
||||||
case 66:
|
case 66:
|
||||||
return DECSET_BIT_APPLICATION_KEYPAD;
|
return DECSET_BIT_APPLICATION_KEYPAD;
|
||||||
case 69:
|
case 69:
|
||||||
@@ -279,16 +308,23 @@ public final class TerminalEmulator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public TerminalEmulator(TerminalOutput session, int columns, int rows, int transcriptRows) {
|
public TerminalEmulator(TerminalOutput session, int columns, int rows, Integer transcriptRows, TerminalSessionClient client) {
|
||||||
mSession = session;
|
mSession = session;
|
||||||
mScreen = mMainBuffer = new TerminalBuffer(columns, transcriptRows, rows);
|
mScreen = mMainBuffer = new TerminalBuffer(columns, getTerminalTranscriptRows(transcriptRows), rows);
|
||||||
mAltBuffer = new TerminalBuffer(columns, rows, rows);
|
mAltBuffer = new TerminalBuffer(columns, rows, rows);
|
||||||
|
mClient = client;
|
||||||
mRows = rows;
|
mRows = rows;
|
||||||
mColumns = columns;
|
mColumns = columns;
|
||||||
mTabStop = new boolean[mColumns];
|
mTabStop = new boolean[mColumns];
|
||||||
reset();
|
reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void updateTerminalSessionClient(TerminalSessionClient client) {
|
||||||
|
mClient = client;
|
||||||
|
setCursorStyle();
|
||||||
|
setCursorBlinkState(true);
|
||||||
|
}
|
||||||
|
|
||||||
public TerminalBuffer getScreen() {
|
public TerminalBuffer getScreen() {
|
||||||
return mScreen;
|
return mScreen;
|
||||||
}
|
}
|
||||||
@@ -297,6 +333,13 @@ public final class TerminalEmulator {
|
|||||||
return mScreen == mAltBuffer;
|
return mScreen == mAltBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int getTerminalTranscriptRows(Integer transcriptRows) {
|
||||||
|
if (transcriptRows == null || transcriptRows < TERMINAL_TRANSCRIPT_ROWS_MIN || transcriptRows > TERMINAL_TRANSCRIPT_ROWS_MAX)
|
||||||
|
return DEFAULT_TERMINAL_TRANSCRIPT_ROWS;
|
||||||
|
else
|
||||||
|
return transcriptRows;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mouseButton one of the MOUSE_* constants of this class.
|
* @param mouseButton one of the MOUSE_* constants of this class.
|
||||||
*/
|
*/
|
||||||
@@ -364,18 +407,49 @@ public final class TerminalEmulator {
|
|||||||
return mCursorCol;
|
return mCursorCol;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link #CURSOR_STYLE_BAR}, {@link #CURSOR_STYLE_BLOCK} or {@link #CURSOR_STYLE_UNDERLINE} */
|
/** Get the terminal cursor style. It will be one of {@link #TERMINAL_CURSOR_STYLES_LIST} */
|
||||||
public int getCursorStyle() {
|
public int getCursorStyle() {
|
||||||
return mCursorStyle;
|
return mCursorStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Set the terminal cursor style. */
|
||||||
|
public void setCursorStyle() {
|
||||||
|
Integer cursorStyle = null;
|
||||||
|
|
||||||
|
if (mClient != null)
|
||||||
|
cursorStyle = mClient.getTerminalCursorStyle();
|
||||||
|
|
||||||
|
if (cursorStyle == null || !Arrays.asList(TERMINAL_CURSOR_STYLES_LIST).contains(cursorStyle))
|
||||||
|
mCursorStyle = DEFAULT_TERMINAL_CURSOR_STYLE;
|
||||||
|
else
|
||||||
|
mCursorStyle = cursorStyle;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isReverseVideo() {
|
public boolean isReverseVideo() {
|
||||||
return isDecsetInternalBitSet(DECSET_BIT_REVERSE_VIDEO);
|
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() {
|
public boolean isKeypadApplicationMode() {
|
||||||
return isDecsetInternalBitSet(DECSET_BIT_APPLICATION_KEYPAD);
|
return isDecsetInternalBitSet(DECSET_BIT_APPLICATION_KEYPAD);
|
||||||
@@ -751,7 +825,7 @@ public final class TerminalEmulator {
|
|||||||
if (internalBit != -1) {
|
if (internalBit != -1) {
|
||||||
value = isDecsetInternalBitSet(internalBit) ? 1 : 2; // 1=set, 2=reset.
|
value = isDecsetInternalBitSet(internalBit) ? 1 : 2; // 1=set, 2=reset.
|
||||||
} else {
|
} 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
|
value = 0; // 0=not recognized, 3=permanently set, 4=permanently reset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -768,15 +842,15 @@ public final class TerminalEmulator {
|
|||||||
case 0: // Blinking block.
|
case 0: // Blinking block.
|
||||||
case 1: // Blinking block.
|
case 1: // Blinking block.
|
||||||
case 2: // Steady block.
|
case 2: // Steady block.
|
||||||
mCursorStyle = CURSOR_STYLE_BLOCK;
|
mCursorStyle = TERMINAL_CURSOR_STYLE_BLOCK;
|
||||||
break;
|
break;
|
||||||
case 3: // Blinking underline.
|
case 3: // Blinking underline.
|
||||||
case 4: // Steady underline.
|
case 4: // Steady underline.
|
||||||
mCursorStyle = CURSOR_STYLE_UNDERLINE;
|
mCursorStyle = TERMINAL_CURSOR_STYLE_UNDERLINE;
|
||||||
break;
|
break;
|
||||||
case 5: // Blinking bar (xterm addition).
|
case 5: // Blinking bar (xterm addition).
|
||||||
case 6: // Steady bar (xterm addition).
|
case 6: // Steady bar (xterm addition).
|
||||||
mCursorStyle = CURSOR_STYLE_BAR;
|
mCursorStyle = TERMINAL_CURSOR_STYLE_BAR;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -888,7 +962,7 @@ public final class TerminalEmulator {
|
|||||||
case "&8": // Undo key - ignore.
|
case "&8": // Undo key - ignore.
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
Log.w(EmulatorDebug.LOG_TAG, "Unhandled termcap/terminfo name: '" + trans + "'");
|
mClient.logWarn(LOG_TAG, "Unhandled termcap/terminfo name: '" + trans + "'");
|
||||||
}
|
}
|
||||||
// Respond with invalid request:
|
// Respond with invalid request:
|
||||||
mSession.write("\033P0+r" + part + "\033\\");
|
mSession.write("\033P0+r" + part + "\033\\");
|
||||||
@@ -900,12 +974,12 @@ public final class TerminalEmulator {
|
|||||||
mSession.write("\033P1+r" + part + "=" + hexEncoded + "\033\\");
|
mSession.write("\033P1+r" + part + "=" + hexEncoded + "\033\\");
|
||||||
}
|
}
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
if (LOG_ESCAPE_SEQUENCES)
|
if (LOG_ESCAPE_SEQUENCES)
|
||||||
Log.e(EmulatorDebug.LOG_TAG, "Unrecognized device control string: " + dcs);
|
mClient.logError(LOG_TAG, "Unrecognized device control string: " + dcs);
|
||||||
}
|
}
|
||||||
finishSequence();
|
finishSequence();
|
||||||
}
|
}
|
||||||
@@ -995,7 +1069,7 @@ public final class TerminalEmulator {
|
|||||||
int externalBit = mArgs[i];
|
int externalBit = mArgs[i];
|
||||||
int internalBit = mapDecSetBitToInternalBit(externalBit);
|
int internalBit = mapDecSetBitToInternalBit(externalBit);
|
||||||
if (internalBit == -1) {
|
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 {
|
} else {
|
||||||
if (b == 's') {
|
if (b == 's') {
|
||||||
mSavedDecSetFlags |= internalBit;
|
mSavedDecSetFlags |= internalBit;
|
||||||
@@ -1046,7 +1120,10 @@ public final class TerminalEmulator {
|
|||||||
case 8: // Auto-repeat Keys (DECARM). Do not implement.
|
case 8: // Auto-repeat Keys (DECARM). Do not implement.
|
||||||
case 9: // X10 mouse reporting - outdated. Do not implement.
|
case 9: // X10 mouse reporting - outdated. Do not implement.
|
||||||
case 12: // Control cursor blinking - ignore.
|
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 40: // Allow 80 => 132 Mode, ignore.
|
||||||
case 45: // TODO: Reverse wrap-around. Implement???
|
case 45: // TODO: Reverse wrap-around. Implement???
|
||||||
case 66: // Application keypad (DECNKM).
|
case 66: // Application keypad (DECNKM).
|
||||||
@@ -1182,7 +1259,7 @@ public final class TerminalEmulator {
|
|||||||
// (1) enables this feature for keys except for those with well-known behavior, e.g., Tab, Backarrow and
|
// (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.
|
// some special control character cases, e.g., Control-Space to make a NUL.
|
||||||
// (2) enables this feature for keys including the exceptions listed.
|
// (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;
|
break;
|
||||||
default:
|
default:
|
||||||
parseArg(b);
|
parseArg(b);
|
||||||
@@ -1729,7 +1806,7 @@ public final class TerminalEmulator {
|
|||||||
int firstArg = mArgs[i + 1];
|
int firstArg = mArgs[i + 1];
|
||||||
if (firstArg == 2) {
|
if (firstArg == 2) {
|
||||||
if (i + 4 > mArgIndex) {
|
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 {
|
} else {
|
||||||
int red = mArgs[i + 2], green = mArgs[i + 3], blue = mArgs[i + 4];
|
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) {
|
if (red < 0 || green < 0 || blue < 0 || red > 255 || green > 255 || blue > 255) {
|
||||||
@@ -1754,7 +1831,7 @@ public final class TerminalEmulator {
|
|||||||
mBackColor = color;
|
mBackColor = color;
|
||||||
}
|
}
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
finishSequenceAndLogError("Invalid ISO-8613-3 SGR first argument: " + firstArg);
|
finishSequenceAndLogError("Invalid ISO-8613-3 SGR first argument: " + firstArg);
|
||||||
@@ -1771,7 +1848,7 @@ public final class TerminalEmulator {
|
|||||||
mBackColor = code - 100 + 8;
|
mBackColor = code - 100 + 8;
|
||||||
} else {
|
} else {
|
||||||
if (LOG_ESCAPE_SEQUENCES)
|
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 +1982,7 @@ public final class TerminalEmulator {
|
|||||||
String clipboardText = new String(Base64.decode(textParameter.substring(startIndex), 0), StandardCharsets.UTF_8);
|
String clipboardText = new String(Base64.decode(textParameter.substring(startIndex), 0), StandardCharsets.UTF_8);
|
||||||
mSession.clipboardText(clipboardText);
|
mSession.clipboardText(clipboardText);
|
||||||
} catch (Exception e) {
|
} 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;
|
break;
|
||||||
case 104:
|
case 104:
|
||||||
@@ -2101,7 +2178,7 @@ public final class TerminalEmulator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void finishSequenceAndLogError(String error) {
|
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();
|
finishSequence();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2289,7 +2366,7 @@ public final class TerminalEmulator {
|
|||||||
|
|
||||||
/** Reset terminal state so user can interact with it regardless of present state. */
|
/** Reset terminal state so user can interact with it regardless of present state. */
|
||||||
public void reset() {
|
public void reset() {
|
||||||
mCursorStyle = CURSOR_STYLE_BLOCK;
|
setCursorStyle();
|
||||||
mArgIndex = 0;
|
mArgIndex = 0;
|
||||||
mContinueSequence = false;
|
mContinueSequence = false;
|
||||||
mEscapeState = ESC_NONE;
|
mEscapeState = ESC_NONE;
|
||||||
@@ -2310,7 +2387,7 @@ public final class TerminalEmulator {
|
|||||||
mCurrentDecSetFlags = 0;
|
mCurrentDecSetFlags = 0;
|
||||||
// Initial wrap-around is not accurate but makes terminal more useful, especially on a small screen:
|
// Initial wrap-around is not accurate but makes terminal more useful, especially on a small screen:
|
||||||
setDecsetinternalBit(DECSET_BIT_AUTOWRAP, true);
|
setDecsetinternalBit(DECSET_BIT_AUTOWRAP, true);
|
||||||
setDecsetinternalBit(DECSET_BIT_SHOWING_CURSOR, true);
|
setDecsetinternalBit(DECSET_BIT_CURSOR_ENABLED, true);
|
||||||
mSavedDecSetFlags = mSavedStateMain.mSavedDecFlags = mSavedStateAlt.mSavedDecFlags = mCurrentDecSetFlags;
|
mSavedDecSetFlags = mSavedStateMain.mSavedDecFlags = mSavedStateAlt.mSavedDecFlags = mCurrentDecSetFlags;
|
||||||
|
|
||||||
// XXX: Should we set terminal driver back to IUTF8 with termios?
|
// 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. */
|
/** Write a string using the UTF-8 encoding to the terminal client. */
|
||||||
public final void write(String data) {
|
public final void write(String data) {
|
||||||
|
if (data == null) return;
|
||||||
byte[] bytes = data.getBytes(StandardCharsets.UTF_8);
|
byte[] bytes = data.getBytes(StandardCharsets.UTF_8);
|
||||||
write(bytes, 0, bytes.length);
|
write(bytes, 0, bytes.length);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import android.os.Message;
|
|||||||
import android.system.ErrnoException;
|
import android.system.ErrnoException;
|
||||||
import android.system.Os;
|
import android.system.Os;
|
||||||
import android.system.OsConstants;
|
import android.system.OsConstants;
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileDescriptor;
|
import java.io.FileDescriptor;
|
||||||
@@ -31,41 +30,6 @@ import java.util.UUID;
|
|||||||
*/
|
*/
|
||||||
public final class TerminalSession extends TerminalOutput {
|
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_NEW_INPUT = 1;
|
||||||
private static final int MSG_PROCESS_EXITED = 4;
|
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];
|
private final byte[] mUtf8InputBuffer = new byte[5];
|
||||||
|
|
||||||
/** Callback which gets notified when a session finishes or changes title. */
|
/** 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. */
|
/** The pid of the shell process. 0 if not started and -1 if finished running. */
|
||||||
int mShellPid;
|
int mShellPid;
|
||||||
@@ -104,52 +68,35 @@ public final class TerminalSession extends TerminalOutput {
|
|||||||
/** Set by the application for user identification of session, not by terminal. */
|
/** Set by the application for user identification of session, not by terminal. */
|
||||||
public String mSessionName;
|
public String mSessionName;
|
||||||
|
|
||||||
@SuppressLint("HandlerLeak")
|
final Handler mMainThreadHandler = new MainThreadHandler();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private final String mShellPath;
|
private final String mShellPath;
|
||||||
private final String mCwd;
|
private final String mCwd;
|
||||||
private final String[] mArgs;
|
private final String[] mArgs;
|
||||||
private final String[] mEnv;
|
private final String[] mEnv;
|
||||||
|
private final Integer mTranscriptRows;
|
||||||
|
|
||||||
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, Integer transcriptRows, TerminalSessionClient client) {
|
||||||
this.mShellPath = shellPath;
|
this.mShellPath = shellPath;
|
||||||
this.mCwd = cwd;
|
this.mCwd = cwd;
|
||||||
this.mArgs = args;
|
this.mArgs = args;
|
||||||
this.mEnv = env;
|
this.mEnv = env;
|
||||||
|
this.mTranscriptRows = transcriptRows;
|
||||||
|
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. */
|
/** Inform the attached pty of the new size and reflow or initialize the emulator. */
|
||||||
@@ -174,13 +121,13 @@ public final class TerminalSession extends TerminalOutput {
|
|||||||
* @param rows The number of rows in the terminal window.
|
* @param rows The number of rows in the terminal window.
|
||||||
*/
|
*/
|
||||||
public void initializeEmulator(int columns, int rows) {
|
public void initializeEmulator(int columns, int rows) {
|
||||||
mEmulator = new TerminalEmulator(this, columns, rows, /* transcript= */2000);
|
mEmulator = new TerminalEmulator(this, columns, rows, mTranscriptRows, mClient);
|
||||||
|
|
||||||
int[] processId = new int[1];
|
int[] processId = new int[1];
|
||||||
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns);
|
mTerminalFileDescriptor = JNI.createSubprocess(mShellPath, mCwd, mArgs, mEnv, processId, rows, columns);
|
||||||
mShellPid = processId[0];
|
mShellPid = processId[0];
|
||||||
|
|
||||||
final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor);
|
final FileDescriptor terminalFileDescriptorWrapped = wrapFileDescriptor(mTerminalFileDescriptor, mClient);
|
||||||
|
|
||||||
new Thread("TermSessionInputReader[pid=" + mShellPid + "]") {
|
new Thread("TermSessionInputReader[pid=" + mShellPid + "]") {
|
||||||
@Override
|
@Override
|
||||||
@@ -246,23 +193,23 @@ public final class TerminalSession extends TerminalOutput {
|
|||||||
} else if (codePoint <= /* 11 bits */0b11111111111) {
|
} else if (codePoint <= /* 11 bits */0b11111111111) {
|
||||||
/* 110xxxxx leading byte with leading 5 bits */
|
/* 110xxxxx leading byte with leading 5 bits */
|
||||||
mUtf8InputBuffer[bufferPosition++] = (byte) (0b11000000 | (codePoint >> 6));
|
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));
|
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
|
||||||
} else if (codePoint <= /* 16 bits */0b1111111111111111) {
|
} 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));
|
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));
|
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));
|
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
|
||||||
} else { /* We have checked codePoint <= 1114111 above, so we have max 21 bits = 0b111111111111111111111 */
|
} 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));
|
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));
|
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));
|
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));
|
mUtf8InputBuffer[bufferPosition++] = (byte) (0b10000000 | (codePoint & 0b111111));
|
||||||
}
|
}
|
||||||
write(mUtf8InputBuffer, 0, bufferPosition);
|
write(mUtf8InputBuffer, 0, bufferPosition);
|
||||||
@@ -272,9 +219,9 @@ public final class TerminalSession extends TerminalOutput {
|
|||||||
return mEmulator;
|
return mEmulator;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Notify the {@link #mChangeCallback} that the screen has changed. */
|
/** Notify the {@link #mClient} that the screen has changed. */
|
||||||
protected void notifyScreenUpdate() {
|
protected void notifyScreenUpdate() {
|
||||||
mChangeCallback.onTextChanged(this);
|
mClient.onTextChanged(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Reset state for terminal emulator state. */
|
/** Reset state for terminal emulator state. */
|
||||||
@@ -289,7 +236,7 @@ public final class TerminalSession extends TerminalOutput {
|
|||||||
try {
|
try {
|
||||||
Os.kill(mShellPid, OsConstants.SIGKILL);
|
Os.kill(mShellPid, OsConstants.SIGKILL);
|
||||||
} catch (ErrnoException e) {
|
} catch (ErrnoException e) {
|
||||||
Log.w("termux", "Failed sending SIGKILL: " + e.getMessage());
|
mClient.logWarn(LOG_TAG, "Failed sending SIGKILL: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -309,7 +256,7 @@ public final class TerminalSession extends TerminalOutput {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void titleChanged(String oldTitle, String newTitle) {
|
public void titleChanged(String oldTitle, String newTitle) {
|
||||||
mChangeCallback.onTitleChanged(this);
|
mClient.onTitleChanged(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized boolean isRunning() {
|
public synchronized boolean isRunning() {
|
||||||
@@ -323,17 +270,17 @@ public final class TerminalSession extends TerminalOutput {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void clipboardText(String text) {
|
public void clipboardText(String text) {
|
||||||
mChangeCallback.onClipboardText(this, text);
|
mClient.onClipboardText(this, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBell() {
|
public void onBell() {
|
||||||
mChangeCallback.onBell(this);
|
mClient.onBell(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onColorsChanged() {
|
public void onColorsChanged() {
|
||||||
mChangeCallback.onColorsChanged(this);
|
mClient.onColorsChanged(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getPid() {
|
public int getPid() {
|
||||||
@@ -356,10 +303,65 @@ public final class TerminalSession extends TerminalOutput {
|
|||||||
return outputPath;
|
return outputPath;
|
||||||
}
|
}
|
||||||
} catch (IOException | SecurityException e) {
|
} 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;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||