Compare commits
324 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
ad64dd7c3d | ||
|
|
dfc4595ec5 | ||
|
|
8c80efb904 | ||
|
|
564079c7e9 | ||
|
|
5b6fd9b88c | ||
|
|
63bfe95848 | ||
|
|
af95a99854 | ||
|
|
25523ae224 | ||
|
|
665adb6895 | ||
|
|
eb7fda28df | ||
|
|
7063a3a9da | ||
|
|
6e02b99ff6 | ||
|
|
9aae665bfc | ||
|
|
52ce6cc94b | ||
|
|
f91168eff4 | ||
|
|
8faa5b2151 | ||
|
|
216cc10f3c | ||
|
|
cba80b6c0b | ||
|
|
850faa25dd | ||
|
|
a108b2bd6b | ||
|
|
b95823d7a8 | ||
|
|
382da7e8f7 | ||
|
|
ba9c118b50 | ||
|
|
531c32f3c9 | ||
|
|
db2f50c76e | ||
|
|
784affe39c | ||
|
|
b486d29d23 | ||
|
|
332f1104a3 | ||
|
|
5a70be1523 | ||
|
|
619552ec5c | ||
|
|
70580abd50 | ||
|
|
f191c35851 | ||
|
|
bd7ed28981 | ||
|
|
b68bd107c1 | ||
|
|
5075273362 | ||
|
|
04268f4c20 | ||
|
|
6f24628fd2 | ||
|
|
debbe44809 | ||
|
|
b2ff0e4051 | ||
|
|
9e7029b76a | ||
|
|
51370799c7 | ||
|
|
0910844896 | ||
|
|
f33ebf810f | ||
|
|
930029b5d2 | ||
|
|
33def928cf | ||
|
|
fc04a93990 | ||
|
|
8d302aa9fe | ||
|
|
2224d917a3 | ||
|
|
664ec43f94 | ||
|
|
6cf742460c | ||
|
|
72981fb981 | ||
|
|
2c5534e2c1 | ||
|
|
5b32540635 | ||
|
|
db3ff7b24a | ||
|
|
5f71e3e73a | ||
|
|
3e04ea4cb0 | ||
|
|
0af823607a | ||
|
|
4d9c0c315e | ||
|
|
9c32935ca2 | ||
|
|
669c336e2f | ||
|
|
35842cf4a6 | ||
|
|
b086270a5a | ||
|
|
f39f06a540 | ||
|
|
58440bc88d | ||
|
|
9f438e2912 | ||
|
|
f794bfcadc | ||
|
|
b6d7831646 | ||
|
|
d212198e30 | ||
|
|
c2843897ac | ||
|
|
c4c4912a7e | ||
|
|
38a3319ca2 | ||
|
|
7e13b8aa2e | ||
|
|
6dca19ae00 | ||
|
|
2659c06c5d | ||
|
|
9b7c7102b2 | ||
|
|
1819087ca0 | ||
|
|
366a61f052 | ||
|
|
6e224cabcf | ||
|
|
8c8fa96133 | ||
|
|
537f2ed97e | ||
|
|
0e23315c41 | ||
|
|
2cde986419 | ||
|
|
d28939810c | ||
|
|
93724b7aa6 | ||
|
|
5d06f040e8 | ||
|
|
ed9afa082a | ||
|
|
9703bd31ad | ||
|
|
9749f25eba | ||
|
|
453b838b24 | ||
|
|
9fdf2a49fd | ||
|
|
3270506bff | ||
|
|
a240f4cf45 | ||
|
|
4647beb0d2 | ||
|
|
d92e806461 | ||
|
|
b75cf0bb84 | ||
|
|
f928efed4e | ||
|
|
36db64d585 | ||
|
|
b8f0430699 | ||
|
|
3bb2849a88 |
20
.cirrus.yml
@@ -1,20 +0,0 @@
|
|||||||
container:
|
|
||||||
image: cirrusci/android-sdk:28
|
|
||||||
cpu: 2
|
|
||||||
memory: 8G
|
|
||||||
|
|
||||||
task:
|
|
||||||
name: tests
|
|
||||||
script: ./gradlew test
|
|
||||||
|
|
||||||
task:
|
|
||||||
name: debug-build
|
|
||||||
|
|
||||||
depends_on:
|
|
||||||
- tests
|
|
||||||
|
|
||||||
script: |
|
|
||||||
./gradlew assembleDebug
|
|
||||||
|
|
||||||
output_artifacts:
|
|
||||||
path: "./app/build/outputs/apk/debug/*.apk"
|
|
||||||
8
.gitattributes
vendored
@@ -1,3 +1,5 @@
|
|||||||
* text=auto
|
* text=auto
|
||||||
*.bat eol=crlf
|
*.bat eol=crlf
|
||||||
*.sh eol=lf
|
*.gradle eol=lf
|
||||||
|
*.mk eol=lf
|
||||||
|
*.sh eol=lf
|
||||||
|
|||||||
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
patreon: termux
|
||||||
|
custom: https://paypal.me/fornwall
|
||||||
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,20 +1,35 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Create a report to help us improve termux-app
|
about: Create a report to help us improve Termux application
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- Important note: Refusing to provide needed information may result in issue closing. If you are having problems with a package in termux then a bug report should be filed in the termux-packages repo: https://github.com/termux/termux-packages -->
|
<!--
|
||||||
|
IMPORTANT:
|
||||||
|
|
||||||
|
1. Support of Android 5.x - 6.x is finished.
|
||||||
|
2. Fill the template AFTER comments.
|
||||||
|
-->
|
||||||
|
|
||||||
**Problem description**
|
**Problem description**
|
||||||
A clear and concise description of what the problem with the termux app is. You may post screenshots in addition to description.
|
<!--
|
||||||
|
A clear and concise description of what the problem is.
|
||||||
|
You may post screenshots in addition to description.
|
||||||
|
-->
|
||||||
|
|
||||||
**Steps to reproduce**
|
**Steps to reproduce**
|
||||||
Please post all steps that are needed to reproduce the issue.
|
<!--
|
||||||
|
Steps to reproduce the behavior. Please post all necessary
|
||||||
|
commands that are needed to reproduce the issue.
|
||||||
|
-->
|
||||||
|
|
||||||
**Expected behavior**
|
**Expected behavior**
|
||||||
|
<!--
|
||||||
A clear and concise description of what you expected to happen.
|
A clear and concise description of what you expected to happen.
|
||||||
|
-->
|
||||||
|
|
||||||
**Additional information**
|
**Additional information**
|
||||||
Post output of command `termux-info`.
|
|
||||||
If you are rooted or have access to adb then capture a logcat with `logcat -d "*:W"`, from a adb or root shell.
|
* Termux application version:
|
||||||
|
* Android OS version:
|
||||||
|
* Device model:
|
||||||
|
|||||||
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,12 +1,22 @@
|
|||||||
---
|
---
|
||||||
name: Feature request
|
name: Feature request
|
||||||
about: Suggest a new feature in termux-app
|
about: Suggest a new feature for Termux application
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
IMPORTANT:
|
||||||
|
|
||||||
|
1. Support of Android 5.x - 6.x is finished.
|
||||||
|
2. Fill the template AFTER comments.
|
||||||
|
-->
|
||||||
|
|
||||||
**Feature description**
|
**Feature description**
|
||||||
|
<!--
|
||||||
Describe the feature and why you want it.
|
Describe the feature and why you want it.
|
||||||
|
-->
|
||||||
|
|
||||||
**Reference implementation**
|
**Reference implementation**
|
||||||
|
|
||||||
Does another app/terminal emulator have this feature?
|
Does another app/terminal emulator have this feature?
|
||||||
Provide links to more background information
|
Provide links to more background information.
|
||||||
|
|||||||
26
.github/workflows/debug_build.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- android-10
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- android-10
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Clone repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
./gradlew assembleDebug
|
||||||
|
- name: Store generated APK file
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: termux-app
|
||||||
|
path: ./app/build/outputs/apk/debug
|
||||||
19
.github/workflows/gradle-wrapper-validation.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
name: "Validate Gradle Wrapper"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- android-10
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- android-10
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validation:
|
||||||
|
name: "Validation"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: gradle/wrapper-validation-action@v1
|
||||||
26
.github/workflows/publish_libraries.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
name: Publish library packages
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
paths:
|
||||||
|
- 'terminal-emulator/build.gradle'
|
||||||
|
- 'terminal-view/build.gradle'
|
||||||
|
- 'termux-shared/build.gradle'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Clone repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Perform release build
|
||||||
|
run: |
|
||||||
|
./gradlew assembleRelease
|
||||||
|
- name: Publish libraries on Github Packages
|
||||||
|
env:
|
||||||
|
GH_USERNAME: xeffyr
|
||||||
|
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
|
run: |
|
||||||
|
./gradlew publish
|
||||||
21
.github/workflows/run_tests.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: Unit tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- android-10
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- android-10
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
testing:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Clone repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Execute tests
|
||||||
|
run: |
|
||||||
|
./gradlew test
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
Released under [the GPLv3 license](https://www.gnu.org/licenses/gpl.html).
|
This repository is released under [GPLv3 only](https://www.gnu.org/licenses/gpl-3.0.html) license.
|
||||||
|
|
||||||
Contains code from `Terminal Emulator for Android` by which is released under [the Apache License 2.0](https://www.apache.org/licenses/).
|
### Exceptions
|
||||||
|
|
||||||
|
- [Terminal Emulator for Android](https://github.com/jackpal/Android-Terminal-Emulator) code is used which is released under [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). Check [terminal-view](terminal-view) and [terminal-emulator](terminal-emulator) modules.
|
||||||
|
- [libcore/ojluni](https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/) code is used which is released under [GPLv2 only with "Classpath" exception](https://openjdk.java.net/legal/gplv2+ce.html). Check `com.termux.shared.file` package under [termux-shared](termux-shared) module.
|
||||||
|
- [libsuperuser ](https://github.com/Chainfire/libsuperuser) code is used which is released under [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). Check `com.termux.shared.shell.StreamGobbler` class under [termux-shared](termux-shared) module.
|
||||||
|
|||||||
54
README.md
@@ -1,6 +1,7 @@
|
|||||||
# Termux application
|
# Termux application
|
||||||
|
|
||||||
[](https://cirrus-ci.com/termux/termux-app)
|
[](https://github.com/termux/termux-app/actions)
|
||||||
|
[](https://github.com/termux/termux-app/actions)
|
||||||
[](https://gitter.im/termux/termux)
|
[](https://gitter.im/termux/termux)
|
||||||
|
|
||||||
[Termux](https://termux.com) is an Android terminal application and Linux environment.
|
[Termux](https://termux.com) is an Android terminal application and Linux environment.
|
||||||
@@ -13,21 +14,43 @@ Note that this repository is for the app itself (the user interface and the
|
|||||||
terminal emulation). For the packages installable inside the app, see
|
terminal emulation). For the packages installable inside the app, see
|
||||||
[termux/termux-packages](https://github.com/termux/termux-packages)
|
[termux/termux-packages](https://github.com/termux/termux-packages)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
**@termux is looking for Termux Application maintainer for implementing new features,
|
||||||
|
fixing bugs and reviewing pull requests since current one (@fornwall) is inactive.**
|
||||||
|
|
||||||
|
Issue https://github.com/termux/termux-app/issues/1072 needs extra attention.
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Termux:Widget application can be obtained from:
|
Termux can be obtained through various sources listed below.
|
||||||
|
|
||||||
- [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 a different signatures and you will get an error on installation but this restriction can be bypassed with root or with custom roms. If you wish to install from a different source, then you must uninstall any and all existing Termux app or its plugin APKs from your device first, then install all new APKs from the same new source.
|
||||||
- [F-Droid](https://f-droid.org/en/packages/com.termux/)
|
|
||||||
- [Kali Nethunter Store](https://store.nethunter.com/en/packages/com.termux/)
|
Following is a list of Termux app and its plugins.
|
||||||
|
|
||||||
|
- [Termux](https://github.com/termux/termux-app)
|
||||||
|
- [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)
|
||||||
|
|
||||||
|
If you wish to install Termux from a difference source, you must uninstall all the apps listed above before installing from new source. Go to `Android Settings` -> `Applications` and then look for the following apps. You can also use the search feature if its available on your device and search `termux` in the applications list. Even if you think you have not installed any of the plugins, its strongly suggesting to go through the application list in Android settings and double check if installation is failing.
|
||||||
|
|
||||||
|
### F-Droid
|
||||||
|
|
||||||
|
Termux application can be obtained from F-Droid [here](https://f-droid.org/en/packages/com.termux/). It usually takes a few days (or even a week or more) for updates to be available on F-Droid once an update has been released on Github. F-Droid releases are built and published by F-Droid once they detect a new Github release. The Termux maintainers **do not** have any control over building and publishing of Termux app on F-Droid. Moreover, the Termux maintainers also do not have access to the APK signing keys of F-Droid releases, so we cannot release an APK ourselves on Github that would be compatible with F-Droid releases.
|
||||||
|
|
||||||
|
### Debug Builds
|
||||||
|
|
||||||
|
For users who don't want to wait for F-Droid releases and want to try out the latest features immediately or want to test their pull requests can get the APKs from [Github Actions](https://github.com/termux/termux-app/actions) page from the workflow runs labeled `Build`. The APK will be listed under `Artifacts` section. These are published for each commit done to the repository. These APKs are [debuggable](https://developer.android.com/studio/debug) and are also not compatible with other sources.
|
||||||
|
##
|
||||||
|
|
||||||
Additionally we offer development builds for those who want to try out latest
|
|
||||||
features ready to be included in future versions. Such build can be obtained
|
|
||||||
directly from [Cirrus CI artifacts](https://api.cirrus-ci.com/v1/artifact/github/termux/termux-app/debug-build/output/app/build/outputs/apk/debug/app-debug.apk).
|
|
||||||
|
|
||||||
Signature keys of all offered builds are different. Before you switch the
|
|
||||||
installation source, you will have to uninstall the Termux application and
|
|
||||||
all currently installed plugins.
|
|
||||||
|
|
||||||
## Terminal resources
|
## Terminal resources
|
||||||
|
|
||||||
@@ -61,3 +84,12 @@ all currently installed plugins.
|
|||||||
|
|
||||||
- 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 Termux app and its plugins. It was created to allow for removal of all hardcoded paths in Termux app. The termux plugins will hopefully use this in future as well. If you are contributing code that is using a constant or a util that may be shared, then define it in `termux-shared` library if it currently doesn't exist and reference it from there. Update the relevant changelogs as well. Pull requests using hardcoded values **will 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.
|
||||||
|
|||||||
@@ -3,21 +3,40 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 28
|
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
||||||
|
ndkVersion project.properties.ndkVersion
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "androidx.annotation:annotation:1.1.0"
|
implementation "androidx.annotation:annotation:1.2.0"
|
||||||
|
implementation "androidx.core:core:1.5.0-rc01"
|
||||||
|
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
|
||||||
|
implementation "androidx.preference:preference:1.1.1"
|
||||||
implementation "androidx.viewpager:viewpager:1.0.0"
|
implementation "androidx.viewpager:viewpager:1.0.0"
|
||||||
implementation "androidx.drawerlayout:drawerlayout:1.0.0"
|
implementation "com.google.guava:guava:24.1-jre"
|
||||||
|
implementation "io.noties.markwon:core:$markwonVersion"
|
||||||
|
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
|
||||||
|
implementation "io.noties.markwon:linkify:$markwonVersion"
|
||||||
|
implementation "io.noties.markwon:recycler:$markwonVersion"
|
||||||
|
|
||||||
implementation project(":terminal-view")
|
implementation project(":terminal-view")
|
||||||
|
implementation project(":termux-shared")
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.termux"
|
applicationId "com.termux"
|
||||||
minSdkVersion 24
|
minSdkVersion project.properties.minSdkVersion.toInteger()
|
||||||
targetSdkVersion 28
|
targetSdkVersion project.properties.targetSdkVersion.toInteger()
|
||||||
versionCode 88
|
versionCode 112
|
||||||
versionName "0.88"
|
versionName "0.112"
|
||||||
|
|
||||||
|
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 {
|
||||||
@@ -63,6 +82,10 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
disable 'ProtectedPermissions'
|
||||||
|
}
|
||||||
|
|
||||||
testOptions {
|
testOptions {
|
||||||
unitTests {
|
unitTests {
|
||||||
includeAndroidResources = true
|
includeAndroidResources = true
|
||||||
@@ -71,8 +94,8 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation 'junit:junit:4.13'
|
testImplementation "junit:junit:4.13.2"
|
||||||
testImplementation 'org.robolectric:robolectric:4.3'
|
testImplementation "org.robolectric:robolectric:4.4"
|
||||||
}
|
}
|
||||||
|
|
||||||
task versionName {
|
task versionName {
|
||||||
@@ -81,7 +104,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"
|
||||||
@@ -103,7 +126,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()
|
||||||
@@ -130,13 +153,13 @@ clean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task downloadBootstraps(){
|
task downloadBootstraps() {
|
||||||
doLast {
|
doLast {
|
||||||
def version = 18
|
def version = "2021.04.13-r1"
|
||||||
downloadBootstrap("aarch64", "1a4c08a696d452b58f69102428239ec0c07521c0ca9f48b23ef70ae0e5e3d4f8", version)
|
downloadBootstrap("aarch64", "ff82e5755d947cd1f3e0b30916d125c6ddd8ba3254801ca7499d73653417e158", version)
|
||||||
downloadBootstrap("arm", "bff11f2c7e9c1055a22fc5f20bb7507b75f6034e0f5d591ec6725b3407981b85", version)
|
downloadBootstrap("arm", "53a7df2d6d0a36a8c9ab5259c8b5457c93b8bae8aec2321a470236b6da54e59a", version)
|
||||||
downloadBootstrap("i686", "6fb93020db2807337d82a1537e24612400cacbd10cf4bccaeb0714d51e653da1", version)
|
downloadBootstrap("i686", "f0e1399a13ebed6c5229fde161f9848d9f5eeae7b8cd82f31250a813b52e371", version)
|
||||||
downloadBootstrap("x86_64", "a6067e5decc486dcad190c1ed9e15366c798e5e7d9b9b9ee6b4b8231290524c3", version)
|
downloadBootstrap("x86_64", "e36c4d8c933dc12b3f48937b7747c7a4dcfaa70f0dd89ad5e8b4465930075ae9", version)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,4 +168,3 @@ afterEvaluate {
|
|||||||
variant.javaCompileProvider.get().dependsOn(downloadBootstraps)
|
variant.javaCompileProvider.get().dependsOn(downloadBootstraps)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
5
app/proguard-rules.pro
vendored
@@ -7,5 +7,6 @@
|
|||||||
# 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
|
||||||
|
|||||||
@@ -1,12 +1,24 @@
|
|||||||
|
<?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="${TERMUX_PACKAGE_NAME}.permission.RUN_COMMAND"
|
||||||
|
android:description="@string/permission_run_command_description"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/permission_run_command_label"
|
||||||
|
android:protectionLevel="dangerous" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
@@ -14,58 +26,102 @@
|
|||||||
<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.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:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|uiMode|keyboard|keyboardHidden|navigation"
|
||||||
android:label="@string/application_name"
|
android:label="@string/application_name"
|
||||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:windowSoftInputMode="adjustResize|stateAlwaysVisible" >
|
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=".app.activities.ReportActivity"
|
||||||
|
android:theme="@style/Theme.AppCompat.TermuxReportActivity"
|
||||||
|
android:documentLaunchMode="intoExisting"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".filepicker.TermuxFileReceiverActivity"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:label="@string/application_name"
|
||||||
|
android:noHistory="true"
|
||||||
|
android:resizeableActivity="true"
|
||||||
|
android:taskAffinity="${TERMUX_PACKAGE_NAME}.filereceiver">
|
||||||
|
|
||||||
<!-- Accept multiple file types when sending. -->
|
<!-- 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/*" />
|
||||||
@@ -74,35 +130,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/json" />
|
<data android:mimeType="video/*" />
|
||||||
<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" />
|
||||||
@@ -110,18 +156,32 @@
|
|||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="com.termux.app.TermuxService"
|
android:name=".app.TermuxService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
<service
|
||||||
|
android:name=".app.RunCommandService"
|
||||||
|
android:exported="true"
|
||||||
|
android:permission="${TERMUX_PACKAGE_NAME}.permission.RUN_COMMAND">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="${TERMUX_PACKAGE_NAME}.RUN_COMMAND" />
|
||||||
|
</intent-filter>
|
||||||
|
</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"
|
||||||
|
android:readPermission="android.permission.permRead" />
|
||||||
|
|
||||||
<meta-data android:name="com.sec.android.support.multiwindow" 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,251 +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("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"));
|
|
||||||
// ANDROID_RUNTIME_ROOT and ANDROID_TZDATA_ROOT are required for `am` to run on Android Q
|
|
||||||
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 {
|
|
||||||
if (shouldAddLdLibraryPath()) {
|
|
||||||
environment.add("LD_LIBRARY_PATH=" + TermuxService.PREFIX_PATH + "/lib");
|
|
||||||
}
|
|
||||||
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]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean shouldAddLdLibraryPath() {
|
|
||||||
try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(TermuxService.PREFIX_PATH + "/etc/apt/sources.list")))) {
|
|
||||||
String line;
|
|
||||||
while ((line = in.readLine()) != null) {
|
|
||||||
if (!line.startsWith("#") && line.contains("//termux.net stable")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(LOG_TAG, "Error trying to read sources.list", e);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
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,451 +0,0 @@
|
|||||||
package com.termux.app;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.provider.Settings;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
import android.view.HapticFeedbackConstants;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.GridLayout;
|
|
||||||
import android.widget.PopupWindow;
|
|
||||||
import android.widget.ToggleButton;
|
|
||||||
|
|
||||||
import com.termux.R;
|
|
||||||
import com.termux.terminal.TerminalSession;
|
|
||||||
import com.termux.view.TerminalView;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A view showing extra keys (such as Escape, Ctrl, Alt) not normally available on an Android soft
|
|
||||||
* keyboard.
|
|
||||||
*/
|
|
||||||
public final class ExtraKeysView extends GridLayout {
|
|
||||||
|
|
||||||
private static final int TEXT_COLOR = 0xFFFFFFFF;
|
|
||||||
private static final int BUTTON_COLOR = 0x00000000;
|
|
||||||
private static final int INTERESTING_COLOR = 0xFF80DEEA;
|
|
||||||
private static final int BUTTON_PRESSED_COLOR = 0x7FFFFFFF;
|
|
||||||
|
|
||||||
public ExtraKeysView(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HashMap that implements Python dict.get(key, default) function.
|
|
||||||
* Default java.util .get(key) is then the same as .get(key, null);
|
|
||||||
*/
|
|
||||||
static class CleverMap<K,V> extends HashMap<K,V> {
|
|
||||||
V get(K key, V defaultValue) {
|
|
||||||
if(containsKey(key))
|
|
||||||
return get(key);
|
|
||||||
else
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class CharDisplayMap extends CleverMap<String, String> {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Keys are displayed in a natural looking way, like "→" for "RIGHT"
|
|
||||||
*/
|
|
||||||
static final Map<String, Integer> keyCodesForString = new HashMap<String, Integer>() {{
|
|
||||||
put("ESC", KeyEvent.KEYCODE_ESCAPE);
|
|
||||||
put("TAB", KeyEvent.KEYCODE_TAB);
|
|
||||||
put("HOME", KeyEvent.KEYCODE_MOVE_HOME);
|
|
||||||
put("END", KeyEvent.KEYCODE_MOVE_END);
|
|
||||||
put("PGUP", KeyEvent.KEYCODE_PAGE_UP);
|
|
||||||
put("PGDN", KeyEvent.KEYCODE_PAGE_DOWN);
|
|
||||||
put("INS", KeyEvent.KEYCODE_INSERT);
|
|
||||||
put("DEL", KeyEvent.KEYCODE_FORWARD_DEL);
|
|
||||||
put("BKSP", KeyEvent.KEYCODE_DEL);
|
|
||||||
put("UP", KeyEvent.KEYCODE_DPAD_UP);
|
|
||||||
put("LEFT", KeyEvent.KEYCODE_DPAD_LEFT);
|
|
||||||
put("RIGHT", KeyEvent.KEYCODE_DPAD_RIGHT);
|
|
||||||
put("DOWN", KeyEvent.KEYCODE_DPAD_DOWN);
|
|
||||||
put("ENTER", KeyEvent.KEYCODE_ENTER);
|
|
||||||
put("F1", KeyEvent.KEYCODE_F1);
|
|
||||||
put("F2", KeyEvent.KEYCODE_F2);
|
|
||||||
put("F3", KeyEvent.KEYCODE_F3);
|
|
||||||
put("F4", KeyEvent.KEYCODE_F4);
|
|
||||||
put("F5", KeyEvent.KEYCODE_F5);
|
|
||||||
put("F6", KeyEvent.KEYCODE_F6);
|
|
||||||
put("F7", KeyEvent.KEYCODE_F7);
|
|
||||||
put("F8", KeyEvent.KEYCODE_F8);
|
|
||||||
put("F9", KeyEvent.KEYCODE_F9);
|
|
||||||
put("F10", KeyEvent.KEYCODE_F10);
|
|
||||||
put("F11", KeyEvent.KEYCODE_F11);
|
|
||||||
put("F12", KeyEvent.KEYCODE_F12);
|
|
||||||
}};
|
|
||||||
|
|
||||||
static void sendKey(View view, String keyName) {
|
|
||||||
TerminalView terminalView = view.findViewById(R.id.terminal_view);
|
|
||||||
if (keyCodesForString.containsKey(keyName)) {
|
|
||||||
int keyCode = keyCodesForString.get(keyName);
|
|
||||||
terminalView.onKeyDown(keyCode, new KeyEvent(KeyEvent.ACTION_UP, keyCode));
|
|
||||||
// view.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
|
|
||||||
} else {
|
|
||||||
// not a control char
|
|
||||||
TerminalSession session = terminalView.getCurrentSession();
|
|
||||||
if (session != null && keyName.length() > 0)
|
|
||||||
session.write(keyName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum SpecialButton {
|
|
||||||
CTRL, ALT, FN
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class SpecialButtonState {
|
|
||||||
boolean isOn = false;
|
|
||||||
ToggleButton button = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<SpecialButton, SpecialButtonState> specialButtons = new HashMap<SpecialButton, SpecialButtonState>() {{
|
|
||||||
put(SpecialButton.CTRL, new SpecialButtonState());
|
|
||||||
put(SpecialButton.ALT, new SpecialButtonState());
|
|
||||||
put(SpecialButton.FN, new SpecialButtonState());
|
|
||||||
}};
|
|
||||||
|
|
||||||
private ScheduledExecutorService scheduledExecutor;
|
|
||||||
private PopupWindow popupWindow;
|
|
||||||
private int longPressCount;
|
|
||||||
|
|
||||||
public boolean readSpecialButton(SpecialButton name) {
|
|
||||||
SpecialButtonState state = specialButtons.get(name);
|
|
||||||
if (state == null)
|
|
||||||
throw new RuntimeException("Must be a valid special button (see source)");
|
|
||||||
|
|
||||||
if (! state.isOn)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (state.button == null) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
void popup(View view, String text) {
|
|
||||||
int width = view.getMeasuredWidth();
|
|
||||||
int height = view.getMeasuredHeight();
|
|
||||||
Button button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
|
||||||
button.setText(text);
|
|
||||||
button.setTextColor(TEXT_COLOR);
|
|
||||||
button.setPadding(0, 0, 0, 0);
|
|
||||||
button.setMinHeight(0);
|
|
||||||
button.setMinWidth(0);
|
|
||||||
button.setMinimumWidth(0);
|
|
||||||
button.setMinimumHeight(0);
|
|
||||||
button.setWidth(width);
|
|
||||||
button.setHeight(height);
|
|
||||||
button.setBackgroundColor(BUTTON_PRESSED_COLOR);
|
|
||||||
popupWindow = new PopupWindow(this);
|
|
||||||
popupWindow.setWidth(LayoutParams.WRAP_CONTENT);
|
|
||||||
popupWindow.setHeight(LayoutParams.WRAP_CONTENT);
|
|
||||||
popupWindow.setContentView(button);
|
|
||||||
popupWindow.setOutsideTouchable(true);
|
|
||||||
popupWindow.setFocusable(false);
|
|
||||||
popupWindow.showAsDropDown(view, 0, -2 * height);
|
|
||||||
}
|
|
||||||
|
|
||||||
static final CharDisplayMap classicArrowsDisplay = new CharDisplayMap() {{
|
|
||||||
// classic arrow keys (for ◀ ▶ ▲ ▼ @see arrowVariationDisplay)
|
|
||||||
put("LEFT", "←"); // U+2190 ← LEFTWARDS ARROW
|
|
||||||
put("RIGHT", "→"); // U+2192 → RIGHTWARDS ARROW
|
|
||||||
put("UP", "↑"); // U+2191 ↑ UPWARDS ARROW
|
|
||||||
put("DOWN", "↓"); // U+2193 ↓ DOWNWARDS ARROW
|
|
||||||
}};
|
|
||||||
|
|
||||||
static final CharDisplayMap wellKnownCharactersDisplay = new CharDisplayMap() {{
|
|
||||||
// well known characters // https://en.wikipedia.org/wiki/{Enter_key, Tab_key, Delete_key}
|
|
||||||
put("ENTER", "↲"); // U+21B2 ↲ DOWNWARDS ARROW WITH TIP LEFTWARDS
|
|
||||||
put("TAB", "↹"); // U+21B9 ↹ LEFTWARDS ARROW TO BAR OVER RIGHTWARDS ARROW TO BAR
|
|
||||||
put("BKSP", "⌫"); // U+232B ⌫ ERASE TO THE LEFT sometimes seen and easy to understand
|
|
||||||
put("DEL", "⌦"); // U+2326 ⌦ ERASE TO THE RIGHT not well known but easy to understand
|
|
||||||
}};
|
|
||||||
|
|
||||||
static final CharDisplayMap lessKnownCharactersDisplay = new CharDisplayMap() {{
|
|
||||||
// https://en.wikipedia.org/wiki/{Home_key, End_key, Page_Up_and_Page_Down_keys}
|
|
||||||
// home key can mean "goto the beginning of line" or "goto first page" depending on context, hence the diagonal
|
|
||||||
put("HOME", "⇱"); // from IEC 9995 // U+21F1 ⇱ NORTH WEST ARROW TO CORNER
|
|
||||||
put("END", "⇲"); // from IEC 9995 // ⇲ // U+21F2 ⇲ SOUTH EAST ARROW TO CORNER
|
|
||||||
put("PGUP", "⇑"); // no ISO character exists, U+21D1 ⇑ UPWARDS DOUBLE ARROW will do the trick
|
|
||||||
put("PGDN", "⇓"); // no ISO character exists, U+21D3 ⇓ DOWNWARDS DOUBLE ARROW will do the trick
|
|
||||||
}};
|
|
||||||
|
|
||||||
static final CharDisplayMap arrowTriangleVariationDisplay = new CharDisplayMap() {{
|
|
||||||
// alternative to classic arrow keys
|
|
||||||
put("LEFT", "◀"); // U+25C0 ◀ BLACK LEFT-POINTING TRIANGLE
|
|
||||||
put("RIGHT", "▶"); // U+25B6 ▶ BLACK RIGHT-POINTING TRIANGLE
|
|
||||||
put("UP", "▲"); // U+25B2 ▲ BLACK UP-POINTING TRIANGLE
|
|
||||||
put("DOWN", "▼"); // U+25BC ▼ BLACK DOWN-POINTING TRIANGLE
|
|
||||||
}};
|
|
||||||
|
|
||||||
static final CharDisplayMap notKnownIsoCharacters = new CharDisplayMap() {{
|
|
||||||
// Control chars that are more clear as text // https://en.wikipedia.org/wiki/{Function_key, Alt_key, Control_key, Esc_key}
|
|
||||||
// put("FN", "FN"); // no ISO character exists
|
|
||||||
put("CTRL", "⎈"); // ISO character "U+2388 ⎈ HELM SYMBOL" is unknown to people and never printed on computers, however "U+25C7 ◇ WHITE DIAMOND" is a nice presentation, and "^" for terminal app and mac is often used
|
|
||||||
put("ALT", "⎇"); // ISO character "U+2387 ⎇ ALTERNATIVE KEY SYMBOL'" is unknown to people and only printed as the Option key "⌥" on Mac computer
|
|
||||||
put("ESC", "⎋"); // ISO character "U+238B ⎋ BROKEN CIRCLE WITH NORTHWEST ARROW" is unknown to people and not often printed on computers
|
|
||||||
}};
|
|
||||||
|
|
||||||
static final CharDisplayMap nicerLookingDisplay = new CharDisplayMap() {{
|
|
||||||
// nicer looking for most cases
|
|
||||||
put("-", "―"); // U+2015 ― HORIZONTAL BAR
|
|
||||||
}};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Keys are displayed in a natural looking way, like "→" for "RIGHT" or "↲" for ENTER
|
|
||||||
*/
|
|
||||||
public static final CharDisplayMap defaultCharDisplay = new CharDisplayMap() {{
|
|
||||||
putAll(classicArrowsDisplay);
|
|
||||||
putAll(wellKnownCharactersDisplay);
|
|
||||||
putAll(nicerLookingDisplay);
|
|
||||||
// all other characters are displayed as themselves
|
|
||||||
}};
|
|
||||||
|
|
||||||
public static final CharDisplayMap lotsOfArrowsCharDisplay = new CharDisplayMap() {{
|
|
||||||
putAll(classicArrowsDisplay);
|
|
||||||
putAll(wellKnownCharactersDisplay);
|
|
||||||
putAll(lessKnownCharactersDisplay); // NEW
|
|
||||||
putAll(nicerLookingDisplay);
|
|
||||||
}};
|
|
||||||
|
|
||||||
public static final CharDisplayMap arrowsOnlyCharDisplay = new CharDisplayMap() {{
|
|
||||||
putAll(classicArrowsDisplay);
|
|
||||||
// putAll(wellKnownCharactersDisplay); // REMOVED
|
|
||||||
// putAll(lessKnownCharactersDisplay); // REMOVED
|
|
||||||
putAll(nicerLookingDisplay);
|
|
||||||
}};
|
|
||||||
|
|
||||||
public static final CharDisplayMap fullIsoCharDisplay = new CharDisplayMap() {{
|
|
||||||
putAll(classicArrowsDisplay);
|
|
||||||
putAll(wellKnownCharactersDisplay);
|
|
||||||
putAll(lessKnownCharactersDisplay); // NEW
|
|
||||||
putAll(nicerLookingDisplay);
|
|
||||||
putAll(notKnownIsoCharacters); // NEW
|
|
||||||
}};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Some people might call our keys differently
|
|
||||||
*/
|
|
||||||
static final CharDisplayMap controlCharsAliases = new CharDisplayMap() {{
|
|
||||||
put("ESCAPE", "ESC");
|
|
||||||
put("CONTROL", "CTRL");
|
|
||||||
put("RETURN", "ENTER"); // Technically different keys, but most applications won't see the difference
|
|
||||||
put("FUNCTION", "FN");
|
|
||||||
// no alias for ALT
|
|
||||||
|
|
||||||
// Directions are sometimes written as first and last letter for brevety
|
|
||||||
put("LT", "LEFT");
|
|
||||||
put("RT", "RIGHT");
|
|
||||||
put("DN", "DOWN");
|
|
||||||
// put("UP", "UP"); well, "UP" is already two letters
|
|
||||||
|
|
||||||
put("PAGEUP", "PGUP");
|
|
||||||
put("PAGE_UP", "PGUP");
|
|
||||||
put("PAGE UP", "PGUP");
|
|
||||||
put("PAGE-UP", "PGUP");
|
|
||||||
|
|
||||||
// no alias for HOME
|
|
||||||
// no alias for END
|
|
||||||
|
|
||||||
put("PAGEDOWN", "PGDN");
|
|
||||||
put("PAGE_DOWN", "PGDN");
|
|
||||||
put("PAGE-DOWN", "PGDN");
|
|
||||||
|
|
||||||
put("DELETE", "DEL");
|
|
||||||
put("BACKSPACE", "BKSP");
|
|
||||||
|
|
||||||
// easier for writing in termux.properties
|
|
||||||
put("BACKSLASH", "\\");
|
|
||||||
put("QUOTE", "\"");
|
|
||||||
put("APOSTROPHE", "'");
|
|
||||||
}};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies the 'controlCharsAliases' mapping to all the strings in *buttons*
|
|
||||||
* Modifies the array, doesn't return a new one.
|
|
||||||
*/
|
|
||||||
void replaceAliases(String[][] buttons) {
|
|
||||||
for(int i = 0; i < buttons.length; i++)
|
|
||||||
for(int j = 0; j < buttons[i].length; j++)
|
|
||||||
buttons[i][j] = controlCharsAliases.get(buttons[i][j], buttons[i][j]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* General util function to compute the longest column length in a matrix.
|
|
||||||
*/
|
|
||||||
static int maximumLength(String[][] matrix) {
|
|
||||||
int m = 0;
|
|
||||||
for (String[] aMatrix : matrix) m = Math.max(m, aMatrix.length);
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reload the view given parameters in termux.properties
|
|
||||||
*
|
|
||||||
* @param buttons matrix of String as defined in termux.properties extrakeys
|
|
||||||
* Can Contain The Strings CTRL ALT TAB FN ENTER LEFT RIGHT UP DOWN or normal strings
|
|
||||||
* Some aliases are possible like RETURN for ENTER, LT for LEFT and more (@see controlCharsAliases for the whole list).
|
|
||||||
* Any string of length > 1 in total Uppercase will print a warning
|
|
||||||
*
|
|
||||||
* Examples:
|
|
||||||
* "ENTER" will trigger the ENTER keycode
|
|
||||||
* "LEFT" will trigger the LEFT keycode and be displayed as "←"
|
|
||||||
* "→" will input a "→" character
|
|
||||||
* "−" will input a "−" character
|
|
||||||
* "-_-" will input the string "-_-"
|
|
||||||
*/
|
|
||||||
void reload(String[][] buttons, CharDisplayMap charDisplayMap) {
|
|
||||||
for(SpecialButtonState state : specialButtons.values())
|
|
||||||
state.button = null;
|
|
||||||
|
|
||||||
removeAllViews();
|
|
||||||
|
|
||||||
replaceAliases(buttons); // modifies the array
|
|
||||||
|
|
||||||
final int rows = buttons.length;
|
|
||||||
final int cols = maximumLength(buttons);
|
|
||||||
|
|
||||||
setRowCount(rows);
|
|
||||||
setColumnCount(cols);
|
|
||||||
|
|
||||||
for (int row = 0; row < rows; row++) {
|
|
||||||
for (int col = 0; col < buttons[row].length; col++) {
|
|
||||||
final String buttonText = buttons[row][col];
|
|
||||||
|
|
||||||
Button button;
|
|
||||||
if(Arrays.asList("CTRL", "ALT", "FN").contains(buttonText)) {
|
|
||||||
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonText)); // for valueOf: https://stackoverflow.com/a/604426/1980630
|
|
||||||
state.isOn = true;
|
|
||||||
button = state.button = new ToggleButton(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
|
||||||
button.setClickable(true);
|
|
||||||
} else {
|
|
||||||
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
|
||||||
}
|
|
||||||
|
|
||||||
final String displayedText = charDisplayMap.get(buttonText, buttonText);
|
|
||||||
button.setText(displayedText);
|
|
||||||
button.setTextColor(TEXT_COLOR);
|
|
||||||
button.setPadding(0, 0, 0, 0);
|
|
||||||
|
|
||||||
final Button finalButton = button;
|
|
||||||
button.setOnClickListener(v -> {
|
|
||||||
if (Settings.System.getInt(getContext().getContentResolver(),
|
|
||||||
Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) != 0) {
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= 28) {
|
|
||||||
finalButton.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
|
|
||||||
} else {
|
|
||||||
// Perform haptic feedback only if no total silence mode enabled.
|
|
||||||
if (Settings.Global.getInt(getContext().getContentResolver(), "zen_mode", 0) != 2) {
|
|
||||||
finalButton.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
View root = getRootView();
|
|
||||||
if(Arrays.asList("CTRL", "ALT", "FN").contains(buttonText)) {
|
|
||||||
ToggleButton self = (ToggleButton) finalButton;
|
|
||||||
self.setChecked(self.isChecked());
|
|
||||||
self.setTextColor(self.isChecked() ? INTERESTING_COLOR : TEXT_COLOR);
|
|
||||||
} else {
|
|
||||||
sendKey(root, buttonText);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
button.setOnTouchListener((v, event) -> {
|
|
||||||
final View root = getRootView();
|
|
||||||
switch (event.getAction()) {
|
|
||||||
case MotionEvent.ACTION_DOWN:
|
|
||||||
longPressCount = 0;
|
|
||||||
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
|
|
||||||
if (Arrays.asList("UP", "DOWN", "LEFT", "RIGHT").contains(buttonText)) {
|
|
||||||
scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
|
|
||||||
scheduledExecutor.scheduleWithFixedDelay(() -> {
|
|
||||||
longPressCount++;
|
|
||||||
sendKey(root, buttonText);
|
|
||||||
}, 400, 80, TimeUnit.MILLISECONDS);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case MotionEvent.ACTION_MOVE:
|
|
||||||
// These two keys have a Move-Up button appearing
|
|
||||||
if (Arrays.asList("/", "-").contains(buttonText)) {
|
|
||||||
if (popupWindow == null && event.getY() < 0) {
|
|
||||||
v.setBackgroundColor(BUTTON_COLOR);
|
|
||||||
String text = "-".equals(buttonText) ? "|" : "\\";
|
|
||||||
popup(v, text);
|
|
||||||
}
|
|
||||||
if (popupWindow != null && event.getY() > 0) {
|
|
||||||
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
|
|
||||||
popupWindow.dismiss();
|
|
||||||
popupWindow = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case MotionEvent.ACTION_CANCEL:
|
|
||||||
v.setBackgroundColor(BUTTON_COLOR);
|
|
||||||
if (scheduledExecutor != null) {
|
|
||||||
scheduledExecutor.shutdownNow();
|
|
||||||
scheduledExecutor = null;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
case MotionEvent.ACTION_UP:
|
|
||||||
v.setBackgroundColor(BUTTON_COLOR);
|
|
||||||
if (scheduledExecutor != null) {
|
|
||||||
scheduledExecutor.shutdownNow();
|
|
||||||
scheduledExecutor = null;
|
|
||||||
}
|
|
||||||
if (longPressCount == 0) {
|
|
||||||
if (popupWindow != null && Arrays.asList("/", "-").contains(buttonText)) {
|
|
||||||
popupWindow.setContentView(null);
|
|
||||||
popupWindow.dismiss();
|
|
||||||
popupWindow = null;
|
|
||||||
sendKey(root, "-".equals(buttonText) ? "|" : "\\");
|
|
||||||
} else {
|
|
||||||
v.performClick();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
LayoutParams param = new GridLayout.LayoutParams();
|
|
||||||
param.width = 0;
|
|
||||||
param.height = 0;
|
|
||||||
param.setMargins(0, 0, 0, 0);
|
|
||||||
param.columnSpec = GridLayout.spec(col, GridLayout.FILL, 1.f);
|
|
||||||
param.rowSpec = GridLayout.spec(row, GridLayout.FILL, 1.f);
|
|
||||||
button.setLayoutParams(param);
|
|
||||||
|
|
||||||
addView(button);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
219
app/src/main/java/com/termux/app/RunCommandService.java
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
package com.termux.app;
|
||||||
|
|
||||||
|
import android.app.Notification;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Binder;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.IBinder;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE;
|
||||||
|
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||||
|
import com.termux.shared.file.FileUtils;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.notification.NotificationUtils;
|
||||||
|
import com.termux.app.utils.PluginUtils;
|
||||||
|
import com.termux.shared.data.DataUtils;
|
||||||
|
import com.termux.shared.models.ExecutionCommand;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service that receives {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent from third party apps and
|
||||||
|
* plugins that contains info on command execution and forwards the extras to {@link TermuxService}
|
||||||
|
* for the actual execution.
|
||||||
|
*
|
||||||
|
* Check https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent for more info.
|
||||||
|
*/
|
||||||
|
public class RunCommandService extends Service {
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "RunCommandService";
|
||||||
|
|
||||||
|
class LocalBinder extends Binder {
|
||||||
|
public final RunCommandService service = RunCommandService.this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final IBinder mBinder = new RunCommandService.LocalBinder();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
return mBinder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
Logger.logVerbose(LOG_TAG, "onCreate");
|
||||||
|
runStartForeground();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
|
Logger.logDebug(LOG_TAG, "onStartCommand");
|
||||||
|
|
||||||
|
if (intent == null) return Service.START_NOT_STICKY;
|
||||||
|
|
||||||
|
// Run again in case service is already started and onCreate() is not called
|
||||||
|
runStartForeground();
|
||||||
|
|
||||||
|
ExecutionCommand executionCommand = new ExecutionCommand();
|
||||||
|
executionCommand.pluginAPIHelp = this.getString(R.string.error_run_command_service_api_help, RUN_COMMAND_SERVICE.RUN_COMMAND_API_HELP_URL);
|
||||||
|
|
||||||
|
String errmsg;
|
||||||
|
|
||||||
|
// If invalid action passed, then just return
|
||||||
|
if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) {
|
||||||
|
errmsg = this.getString(R.string.error_run_command_service_invalid_intent_action, intent.getAction());
|
||||||
|
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
||||||
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||||
|
return Service.START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
executionCommand.executable = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH);
|
||||||
|
executionCommand.arguments = intent.getStringArrayExtra(RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS);
|
||||||
|
executionCommand.stdin = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_STDIN);
|
||||||
|
executionCommand.workingDirectory = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_WORKDIR);
|
||||||
|
executionCommand.inBackground = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false);
|
||||||
|
executionCommand.sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION);
|
||||||
|
executionCommand.commandLabel = DataUtils.getDefaultIfNull(intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_LABEL), "RUN_COMMAND Execution Intent Command");
|
||||||
|
executionCommand.commandDescription = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION);
|
||||||
|
executionCommand.commandHelp = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_HELP);
|
||||||
|
executionCommand.isPluginExecutionCommand = true;
|
||||||
|
executionCommand.pluginPendingIntent = intent.getParcelableExtra(RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// If "allow-external-apps" property to not set to "true", then just return
|
||||||
|
// We enable force notifications if "allow-external-apps" policy is violated so that the
|
||||||
|
// user knows someone tried to run a command in termux context, since it may be malicious
|
||||||
|
// app or imported (tasker) plugin project and not the user himself. If a pending intent is
|
||||||
|
// also sent, then its creator is also logged and shown.
|
||||||
|
errmsg = PluginUtils.checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(this);
|
||||||
|
if (errmsg != null) {
|
||||||
|
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
||||||
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true);
|
||||||
|
return Service.START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// If executable is null or empty, then exit here instead of getting canonical path which would expand to "/"
|
||||||
|
if (executionCommand.executable == null || executionCommand.executable.isEmpty()) {
|
||||||
|
errmsg = this.getString(R.string.error_run_command_service_mandatory_extra_missing, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH);
|
||||||
|
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
||||||
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||||
|
return Service.START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get canonical path of executable
|
||||||
|
executionCommand.executable = FileUtils.getCanonicalPath(executionCommand.executable, null, true);
|
||||||
|
|
||||||
|
// If executable is not a regular file, or is not readable or executable, then just return
|
||||||
|
// Setting of missing read and execute permissions is not done
|
||||||
|
errmsg = FileUtils.validateRegularFileExistenceAndPermissions(this, "executable", executionCommand.executable, null,
|
||||||
|
PluginUtils.PLUGIN_EXECUTABLE_FILE_PERMISSIONS, true, true,
|
||||||
|
false);
|
||||||
|
if (errmsg != null) {
|
||||||
|
errmsg += "\n" + this.getString(R.string.msg_executable_absolute_path, executionCommand.executable);
|
||||||
|
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
||||||
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||||
|
return Service.START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// If workingDirectory is not null or empty
|
||||||
|
if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) {
|
||||||
|
// Get canonical path of workingDirectory
|
||||||
|
executionCommand.workingDirectory = FileUtils.getCanonicalPath(executionCommand.workingDirectory, null, true);
|
||||||
|
|
||||||
|
// If workingDirectory is not a directory, or is not readable or writable, then just return
|
||||||
|
// Creation of missing directory and setting of read, write and execute permissions are only done if workingDirectory is
|
||||||
|
// under {@link TermuxConstants#TERMUX_FILES_DIR_PATH}
|
||||||
|
// We try to set execute permissions, but ignore if they are missing, since only read and write permissions are required
|
||||||
|
// for working directories.
|
||||||
|
errmsg = FileUtils.validateDirectoryFileExistenceAndPermissions(this, "working", executionCommand.workingDirectory, TermuxConstants.TERMUX_FILES_DIR_PATH, true,
|
||||||
|
PluginUtils.PLUGIN_WORKING_DIRECTORY_PERMISSIONS, true, true,
|
||||||
|
true, true);
|
||||||
|
if (errmsg != null) {
|
||||||
|
errmsg += "\n" + this.getString(R.string.msg_working_directory_absolute_path, executionCommand.workingDirectory);
|
||||||
|
executionCommand.setStateFailed(ExecutionCommand.RESULT_CODE_FAILED, errmsg, null);
|
||||||
|
PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
|
||||||
|
return Service.START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(FileUtils.getExpandedTermuxPath(executionCommand.executable)).build();
|
||||||
|
|
||||||
|
Logger.logVerbose(LOG_TAG, executionCommand.toString());
|
||||||
|
|
||||||
|
// Create execution intent with the action TERMUX_SERVICE#ACTION_SERVICE_EXECUTE to be sent to the TERMUX_SERVICE
|
||||||
|
Intent execIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, executionCommand.executableUri);
|
||||||
|
execIntent.setClass(this, TermuxService.class);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, executionCommand.arguments);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_STDIN, executionCommand.stdin);
|
||||||
|
if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) execIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, executionCommand.workingDirectory);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, executionCommand.inBackground);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION, executionCommand.sessionAction);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_LABEL, executionCommand.commandLabel);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, executionCommand.commandDescription);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_HELP, executionCommand.commandHelp);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP, executionCommand.pluginAPIHelp);
|
||||||
|
execIntent.putExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT, executionCommand.pluginPendingIntent);
|
||||||
|
|
||||||
|
// Start TERMUX_SERVICE and pass it execution intent
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
this.startForegroundService(execIntent);
|
||||||
|
} else {
|
||||||
|
this.startService(execIntent);
|
||||||
|
}
|
||||||
|
|
||||||
|
runStopForeground();
|
||||||
|
|
||||||
|
return Service.START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runStartForeground() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
setupNotificationChannel();
|
||||||
|
startForeground(TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_ID, buildNotification());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runStopForeground() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
stopForeground(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Notification buildNotification() {
|
||||||
|
// Build the notification
|
||||||
|
Notification.Builder builder = NotificationUtils.geNotificationBuilder(this,
|
||||||
|
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_LOW,
|
||||||
|
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_NAME, null, null,
|
||||||
|
null, NotificationUtils.NOTIFICATION_MODE_SILENT);
|
||||||
|
if (builder == null) return null;
|
||||||
|
|
||||||
|
// No need to show a timestamp:
|
||||||
|
builder.setShowWhen(false);
|
||||||
|
|
||||||
|
// Set notification icon
|
||||||
|
builder.setSmallIcon(R.drawable.ic_service_notification);
|
||||||
|
|
||||||
|
// Set background color for small notification icon
|
||||||
|
builder.setColor(0xFF607D8B);
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupNotificationChannel() {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
|
||||||
|
|
||||||
|
NotificationUtils.setupNotificationChannel(this, TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_ID,
|
||||||
|
TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
28
app/src/main/java/com/termux/app/TermuxApplication.java
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package com.termux.app;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
|
||||||
|
import com.termux.shared.crash.CrashHandler;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
|
||||||
|
|
||||||
|
public class TermuxApplication extends Application {
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
|
||||||
|
// Set crash handler for the app
|
||||||
|
CrashHandler.setCrashHandler(this);
|
||||||
|
|
||||||
|
// Set log level for the app
|
||||||
|
setLogLevel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setLogLevel() {
|
||||||
|
// Load the log level from shared preferences and set it to the {@link Logger.CURRENT_LOG_LEVEL}
|
||||||
|
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(getApplicationContext());
|
||||||
|
preferences.setLogLevel(null, preferences.getLogLevel());
|
||||||
|
Logger.logDebug("Starting Application");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -7,18 +7,18 @@ 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.shared.file.FileUtils;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
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 +29,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 +46,37 @@ 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);
|
||||||
|
Logger.logError(LOG_TAG, bootstrapErrorMessage);
|
||||||
|
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(bootstrapErrorMessage)
|
||||||
.setOnDismissListener(dialog -> System.exit(0)).setPositiveButton(android.R.string.ok, null).show();
|
.setOnDismissListener(dialog -> System.exit(0)).setPositiveButton(android.R.string.ok, null).show();
|
||||||
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 +84,27 @@ 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.");
|
||||||
|
|
||||||
|
String errmsg;
|
||||||
|
|
||||||
|
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);
|
errmsg = FileUtils.deleteFile(activity, "prefix staging directory", STAGING_PREFIX_PATH, true);
|
||||||
|
if (errmsg != null) {
|
||||||
|
throw new RuntimeException(errmsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete prefix directory or any file at its destination
|
||||||
|
errmsg = FileUtils.deleteFile(activity, "prefix directory", PREFIX_FILE_PATH, true);
|
||||||
|
if (errmsg != null) {
|
||||||
|
throw new RuntimeException(errmsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.logInfo(LOG_TAG, "Extracting bootstrap zip to prefix staging directory \"" + STAGING_PREFIX_PATH + "\".");
|
||||||
|
|
||||||
final byte[] buffer = new byte[8096];
|
final 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 +123,14 @@ 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());
|
ensureDirectoryExists(activity, new File(newPath).getParentFile());
|
||||||
}
|
}
|
||||||
} 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());
|
ensureDirectoryExists(activity, isDirectory ? targetFile : targetFile.getParentFile());
|
||||||
|
|
||||||
if (!isDirectory) {
|
if (!isDirectory) {
|
||||||
try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
|
try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
|
||||||
@@ -124,13 +153,16 @@ 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);
|
Logger.logStackTraceWithMessage(LOG_TAG, "Bootstrap error", e);
|
||||||
activity.runOnUiThread(() -> {
|
activity.runOnUiThread(() -> {
|
||||||
try {
|
try {
|
||||||
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
|
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
|
||||||
@@ -138,9 +170,9 @@ final class TermuxInstaller {
|
|||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
activity.finish();
|
activity.finish();
|
||||||
}).setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
|
}).setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
TermuxInstaller.setupIfNeeded(activity, whenDone);
|
TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone);
|
||||||
}).show();
|
}).show();
|
||||||
} catch (WindowManager.BadTokenException e1) {
|
} catch (WindowManager.BadTokenException e1) {
|
||||||
// Activity already dismissed - ignore.
|
// Activity already dismissed - ignore.
|
||||||
}
|
}
|
||||||
@@ -158,58 +190,25 @@ final class TermuxInstaller {
|
|||||||
}.start();
|
}.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ensureDirectoryExists(File directory) {
|
|
||||||
if (!directory.isDirectory() && !directory.mkdirs()) {
|
|
||||||
throw new RuntimeException("Unable to create directory: " + directory.getAbsolutePath());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static byte[] loadZipBytes() {
|
|
||||||
// Only load the shared library when necessary to save memory usage.
|
|
||||||
System.loadLibrary("termux-bootstrap");
|
|
||||||
return getZip();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static native byte[] getZip();
|
|
||||||
|
|
||||||
/** Delete a folder and all its content or throw. Don't follow symlinks. */
|
|
||||||
static void deleteFolder(File fileOrDirectory) throws IOException {
|
|
||||||
if (fileOrDirectory.getCanonicalPath().equals(fileOrDirectory.getAbsolutePath()) && fileOrDirectory.isDirectory()) {
|
|
||||||
File[] children = fileOrDirectory.listFiles();
|
|
||||||
|
|
||||||
if (children != null) {
|
|
||||||
for (File child : children) {
|
|
||||||
deleteFolder(child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fileOrDirectory.delete()) {
|
|
||||||
throw new RuntimeException("Unable to delete " + (fileOrDirectory.isDirectory() ? "directory " : "file ") + fileOrDirectory.getAbsolutePath());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void setupStorageSymlinks(final Context context) {
|
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");
|
String errmsg;
|
||||||
|
File storageDir = TermuxConstants.TERMUX_STORAGE_HOME_DIR;
|
||||||
|
|
||||||
if (storageDir.exists()) {
|
errmsg = FileUtils.clearDirectory(context, "~/storage", storageDir.getAbsolutePath());
|
||||||
try {
|
if (errmsg != null) {
|
||||||
deleteFolder(storageDir);
|
Logger.logErrorAndShowToast(context, LOG_TAG, errmsg);
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(LOG_TAG, "Could not delete old $HOME/storage, " + e.getMessage());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!storageDir.mkdirs()) {
|
|
||||||
Log.e(LOG_TAG, "Unable to mkdirs() for $HOME/storage");
|
|
||||||
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 +233,34 @@ 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.logStackTraceWithMessage(LOG_TAG, "Error setting up link", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.start();
|
}.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ensureDirectoryExists(Context context, File directory) {
|
||||||
|
String errmsg;
|
||||||
|
|
||||||
|
errmsg = FileUtils.createDirectoryFile(context, directory.getAbsolutePath());
|
||||||
|
if (errmsg != null) {
|
||||||
|
throw new RuntimeException(errmsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] loadZipBytes() {
|
||||||
|
// Only load the shared library when necessary to save memory usage.
|
||||||
|
System.loadLibrary("termux-bootstrap");
|
||||||
|
return getZip();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static native byte[] getZip();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import android.net.Uri;
|
|||||||
import android.os.Environment;
|
import android.os.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,234 +0,0 @@
|
|||||||
package com.termux.app;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
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 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.List;
|
|
||||||
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 String mUseDarkUI;
|
|
||||||
private boolean mScreenAlwaysOn;
|
|
||||||
private int mFontSize;
|
|
||||||
|
|
||||||
@AsciiBellBehaviour
|
|
||||||
int mBellBehaviour = BELL_VIBRATE;
|
|
||||||
|
|
||||||
boolean mBackIsEscape;
|
|
||||||
boolean mDisableVolumeVirtualKeys;
|
|
||||||
boolean mShowExtraKeys;
|
|
||||||
|
|
||||||
String[][] 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.toLowerCase().equals("true");
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (IOException e) {
|
|
||||||
Toast.makeText(context, "Could not open properties file termux.properties.", 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
mUseDarkUI = props.getProperty("use-black-ui", "false");
|
|
||||||
|
|
||||||
try {
|
|
||||||
JSONArray arr = new JSONArray(props.getProperty("extra-keys", "[['ESC', 'TAB', 'CTRL', 'ALT', '-', 'DOWN', 'UP']]"));
|
|
||||||
|
|
||||||
mExtraKeys = new String[arr.length()][];
|
|
||||||
for (int i = 0; i < arr.length(); i++) {
|
|
||||||
JSONArray line = arr.getJSONArray(i);
|
|
||||||
mExtraKeys[i] = new String[line.length()];
|
|
||||||
for (int j = 0; j < line.length(); j++) {
|
|
||||||
mExtraKeys[i][j] = line.getString(j);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} 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);
|
|
||||||
mExtraKeys = new String[0][];
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
@@ -13,7 +13,7 @@ import android.widget.ProgressBar;
|
|||||||
import android.widget.RelativeLayout;
|
import android.widget.RelativeLayout;
|
||||||
|
|
||||||
/** 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;
|
||||||
|
|
||||||
180
app/src/main/java/com/termux/app/activities/ReportActivity.java
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
package com.termux.app.activities;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuInflater;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
import com.termux.shared.markdown.MarkdownUtils;
|
||||||
|
import com.termux.shared.interact.ShareUtils;
|
||||||
|
import com.termux.app.models.ReportInfo;
|
||||||
|
|
||||||
|
import org.commonmark.node.FencedCodeBlock;
|
||||||
|
|
||||||
|
import io.noties.markwon.Markwon;
|
||||||
|
import io.noties.markwon.recycler.MarkwonAdapter;
|
||||||
|
import io.noties.markwon.recycler.SimpleEntry;
|
||||||
|
|
||||||
|
public class ReportActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private static final String EXTRA_REPORT_INFO = "report_info";
|
||||||
|
|
||||||
|
ReportInfo mReportInfo;
|
||||||
|
String mReportMarkdownString;
|
||||||
|
String mReportActivityMarkdownString;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_report);
|
||||||
|
|
||||||
|
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||||
|
if (toolbar != null) {
|
||||||
|
setSupportActionBar(toolbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bundle bundle = null;
|
||||||
|
Intent intent = getIntent();
|
||||||
|
if (intent != null)
|
||||||
|
bundle = intent.getExtras();
|
||||||
|
else if (savedInstanceState != null)
|
||||||
|
bundle = savedInstanceState;
|
||||||
|
|
||||||
|
updateUI(bundle);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onNewIntent(Intent intent) {
|
||||||
|
super.onNewIntent(intent);
|
||||||
|
setIntent(intent);
|
||||||
|
|
||||||
|
if (intent != null)
|
||||||
|
updateUI(intent.getExtras());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateUI(Bundle bundle) {
|
||||||
|
|
||||||
|
if (bundle == null) {
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mReportInfo = (ReportInfo) bundle.getSerializable(EXTRA_REPORT_INFO);
|
||||||
|
|
||||||
|
if (mReportInfo == null) {
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
final ActionBar actionBar = getSupportActionBar();
|
||||||
|
if (actionBar != null) {
|
||||||
|
if (mReportInfo.reportTitle != null)
|
||||||
|
actionBar.setTitle(mReportInfo.reportTitle);
|
||||||
|
else
|
||||||
|
actionBar.setTitle(TermuxConstants.TERMUX_APP_NAME + " App Report");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
RecyclerView recyclerView = findViewById(R.id.recycler_view);
|
||||||
|
|
||||||
|
final Markwon markwon = MarkdownUtils.getRecyclerMarkwonBuilder(this);
|
||||||
|
|
||||||
|
final MarkwonAdapter adapter = MarkwonAdapter.builderTextViewIsRoot(R.layout.activity_report_adapter_node_default)
|
||||||
|
.include(FencedCodeBlock.class, SimpleEntry.create(R.layout.activity_report_adapter_node_code_block, R.id.code_text_view))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||||
|
recyclerView.setAdapter(adapter);
|
||||||
|
|
||||||
|
|
||||||
|
generateReportActivityMarkdownString();
|
||||||
|
adapter.setMarkdown(markwon, mReportActivityMarkdownString);
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||||
|
super.onSaveInstanceState(outState);
|
||||||
|
|
||||||
|
outState.putSerializable(EXTRA_REPORT_INFO, mReportInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||||
|
final MenuInflater inflater = getMenuInflater();
|
||||||
|
inflater.inflate(R.menu.menu_report, menu);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
// Remove activity from recents menu on back button press
|
||||||
|
finishAndRemoveTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
|
int id = item.getItemId();
|
||||||
|
if (id == R.id.menu_item_share_report) {
|
||||||
|
if (mReportMarkdownString != null)
|
||||||
|
ShareUtils.shareText(this, getString(R.string.title_report_text), mReportMarkdownString);
|
||||||
|
} else if (id == R.id.menu_item_copy_report) {
|
||||||
|
if (mReportMarkdownString != null)
|
||||||
|
ShareUtils.copyTextToClipboard(this, mReportMarkdownString, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the markdown {@link String} to be shown in {@link ReportActivity}.
|
||||||
|
*/
|
||||||
|
private void generateReportActivityMarkdownString() {
|
||||||
|
mReportMarkdownString = ReportInfo.getReportInfoMarkdownString(mReportInfo);
|
||||||
|
|
||||||
|
mReportActivityMarkdownString = "";
|
||||||
|
if (mReportInfo.reportStringPrefix != null)
|
||||||
|
mReportActivityMarkdownString += mReportInfo.reportStringPrefix;
|
||||||
|
|
||||||
|
mReportActivityMarkdownString += mReportMarkdownString;
|
||||||
|
|
||||||
|
if (mReportInfo.reportStringSuffix != null)
|
||||||
|
mReportActivityMarkdownString += mReportInfo.reportStringSuffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static void startReportActivity(@NonNull final Context context, @NonNull final ReportInfo reportInfo) {
|
||||||
|
context.startActivity(newInstance(context, reportInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Intent newInstance(@NonNull final Context context, @NonNull final ReportInfo reportInfo) {
|
||||||
|
Intent intent = new Intent(context, ReportActivity.class);
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putSerializable(EXTRA_REPORT_INFO, reportInfo);
|
||||||
|
intent.putExtras(bundle);
|
||||||
|
|
||||||
|
// Note that ReportActivity task has documentLaunchMode="intoExisting" set in AndroidManifest.xml
|
||||||
|
// which has equivalent behaviour to the following. The following dynamic way doesn't seem to
|
||||||
|
// work for notification pending intent, i.e separate task isn't created and activity is
|
||||||
|
// launched in the same task as TermuxActivity.
|
||||||
|
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.termux.app.activities;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.preference.PreferenceFragmentCompat;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
setPreferencesFromResource(R.xml.root_preferences, rootKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package com.termux.app.fragments.settings;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.annotation.Keep;
|
||||||
|
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) {
|
||||||
|
PreferenceManager preferenceManager = getPreferenceManager();
|
||||||
|
preferenceManager.setPreferenceDataStore(DebuggingPreferencesDataStore.getInstance(getContext()));
|
||||||
|
|
||||||
|
setPreferencesFromResource(R.xml.debugging_preferences, rootKey);
|
||||||
|
|
||||||
|
PreferenceCategory loggingCategory = findPreference("logging");
|
||||||
|
|
||||||
|
if (loggingCategory != null) {
|
||||||
|
final ListPreference logLevelListPreference = setLogLevelListPreferenceData(findPreference("log_level"), getActivity());
|
||||||
|
loggingCategory.addPreference(logLevelListPreference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ListPreference setLogLevelListPreferenceData(ListPreference logLevelListPreference, Context context) {
|
||||||
|
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(Logger.getLogLevel()));
|
||||||
|
logLevelListPreference.setDefaultValue(Logger.getLogLevel());
|
||||||
|
|
||||||
|
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 = new TermuxAppSharedPreferences(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized DebuggingPreferencesDataStore getInstance(Context context) {
|
||||||
|
if (mInstance == null) {
|
||||||
|
mInstance = new DebuggingPreferencesDataStore(context.getApplicationContext());
|
||||||
|
}
|
||||||
|
return mInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public String getString(String key, @Nullable String defValue) {
|
||||||
|
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 (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 (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) {
|
||||||
|
switch (key) {
|
||||||
|
case "terminal_view_key_logging_enabled":
|
||||||
|
return mPreferences.getTerminalViewKeyLoggingEnabled();
|
||||||
|
case "plugin_error_notifications_enabled":
|
||||||
|
return mPreferences.getPluginErrorNotificationsEnabled();
|
||||||
|
case "crash_report_notifications_enabled":
|
||||||
|
return mPreferences.getCrashReportNotificationsEnabled();
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
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 TerminalIOPreferencesFragment extends PreferenceFragmentCompat {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||||
|
PreferenceManager preferenceManager = getPreferenceManager();
|
||||||
|
preferenceManager.setPreferenceDataStore(TerminalIOPreferencesDataStore.getInstance(getContext()));
|
||||||
|
|
||||||
|
setPreferencesFromResource(R.xml.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 = new TermuxAppSharedPreferences(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized TerminalIOPreferencesDataStore getInstance(Context context) {
|
||||||
|
if (mInstance == null) {
|
||||||
|
mInstance = new TerminalIOPreferencesDataStore(context.getApplicationContext());
|
||||||
|
}
|
||||||
|
return mInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void putBoolean(String key, boolean value) {
|
||||||
|
if (key == null) return;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case "soft_keyboard_enabled":
|
||||||
|
mPreferences.setSoftKeyboardEnabled(value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getBoolean(String key, boolean defValue) {
|
||||||
|
switch (key) {
|
||||||
|
case "soft_keyboard_enabled":
|
||||||
|
return mPreferences.getSoftKeyboardEnabled();
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
64
app/src/main/java/com/termux/app/models/ReportInfo.java
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package com.termux.app.models;
|
||||||
|
|
||||||
|
import com.termux.shared.markdown.MarkdownUtils;
|
||||||
|
import com.termux.shared.termux.TermuxUtils;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
public class ReportInfo implements Serializable {
|
||||||
|
|
||||||
|
/** The user action that was being processed for which the report was generated. */
|
||||||
|
public final UserAction userAction;
|
||||||
|
/** The internal app component that sent the report. */
|
||||||
|
public final String sender;
|
||||||
|
/** The report title. */
|
||||||
|
public final String reportTitle;
|
||||||
|
/** The markdown report text prefix. Will not be part of copy and share operations, etc. */
|
||||||
|
public final String reportStringPrefix;
|
||||||
|
/** The markdown report text. */
|
||||||
|
public final String reportString;
|
||||||
|
/** The markdown report text suffix. Will not be part of copy and share operations, etc. */
|
||||||
|
public final String reportStringSuffix;
|
||||||
|
/** If set to {@code true}, then report, app and device info will be added to the report when
|
||||||
|
* markdown is generated.
|
||||||
|
*/
|
||||||
|
public final boolean addReportInfoToMarkdown;
|
||||||
|
/** The timestamp for the report. */
|
||||||
|
public final String reportTimestamp;
|
||||||
|
|
||||||
|
public ReportInfo(UserAction userAction, String sender, String reportTitle, String reportStringPrefix, String reportString, String reportStringSuffix, boolean addReportInfoToMarkdown) {
|
||||||
|
this.userAction = userAction;
|
||||||
|
this.sender = sender;
|
||||||
|
this.reportTitle = reportTitle;
|
||||||
|
this.reportStringPrefix = reportStringPrefix;
|
||||||
|
this.reportString = reportString;
|
||||||
|
this.reportStringSuffix = reportStringSuffix;
|
||||||
|
this.addReportInfoToMarkdown = addReportInfoToMarkdown;
|
||||||
|
this.reportTimestamp = TermuxUtils.getCurrentTimeStamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a markdown {@link String} for {@link ReportInfo}.
|
||||||
|
*
|
||||||
|
* @param reportInfo The {@link ReportInfo} to convert.
|
||||||
|
* @return Returns the markdown {@link String}.
|
||||||
|
*/
|
||||||
|
public static String getReportInfoMarkdownString(final ReportInfo reportInfo) {
|
||||||
|
if (reportInfo == null) return "null";
|
||||||
|
|
||||||
|
StringBuilder markdownString = new StringBuilder();
|
||||||
|
|
||||||
|
if (reportInfo.addReportInfoToMarkdown) {
|
||||||
|
markdownString.append("## Report Info\n\n");
|
||||||
|
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("User Action", reportInfo.userAction, "-"));
|
||||||
|
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Sender", reportInfo.sender, "-"));
|
||||||
|
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Report Timestamp", reportInfo.reportTimestamp, "-"));
|
||||||
|
markdownString.append("\n##\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
markdownString.append(reportInfo.reportString);
|
||||||
|
|
||||||
|
return markdownString.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
19
app/src/main/java/com/termux/app/models/UserAction.java
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package com.termux.app.models;
|
||||||
|
|
||||||
|
public enum UserAction {
|
||||||
|
|
||||||
|
PLUGIN_EXECUTION_COMMAND("plugin execution command"),
|
||||||
|
CRASH_REPORT("crash report"),
|
||||||
|
REPORT_ISSUE_FROM_TRANSCRIPT("report issue from transcript");
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
UserAction(final String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package com.termux.app.settings.properties;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import com.termux.app.terminal.io.KeyboardShortcut;
|
||||||
|
import com.termux.app.terminal.io.extrakeys.ExtraKeysInfo;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.settings.properties.SharedPropertiesParser;
|
||||||
|
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||||
|
import com.termux.shared.settings.properties.TermuxSharedProperties;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public class TermuxAppSharedProperties extends TermuxSharedProperties implements SharedPropertiesParser {
|
||||||
|
|
||||||
|
private ExtraKeysInfo mExtraKeysInfo;
|
||||||
|
private List<KeyboardShortcut> mSessionShortcuts = new ArrayList<>();
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "TermuxAppSharedProperties";
|
||||||
|
|
||||||
|
public TermuxAppSharedProperties(@Nonnull Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload the termux properties from disk into an in-memory cache.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void loadTermuxPropertiesFromDisk() {
|
||||||
|
super.loadTermuxPropertiesFromDisk();
|
||||||
|
|
||||||
|
setExtraKeys();
|
||||||
|
setSessionShortcuts();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the terminal extra keys and style.
|
||||||
|
*/
|
||||||
|
private void setExtraKeys() {
|
||||||
|
mExtraKeysInfo = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// The mMap stores the extra key and style string values while loading properties
|
||||||
|
// Check {@link #getExtraKeysInternalPropertyValueFromValue(String)} and
|
||||||
|
// {@link #getExtraKeysStyleInternalPropertyValueFromValue(String)}
|
||||||
|
String extrakeys = (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS, true);
|
||||||
|
String extraKeysStyle = (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE, true);
|
||||||
|
mExtraKeysInfo = new ExtraKeysInfo(extrakeys, extraKeysStyle);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
Logger.showToast(mContext, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: " + e.toString(), true);
|
||||||
|
Logger.logStackTraceWithMessage(LOG_TAG, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: ", e);
|
||||||
|
|
||||||
|
try {
|
||||||
|
mExtraKeysInfo = new ExtraKeysInfo(TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE);
|
||||||
|
} catch (JSONException e2) {
|
||||||
|
Logger.showToast(mContext, "Can't create default extra keys",true);
|
||||||
|
Logger.logStackTraceWithMessage(LOG_TAG, "Could create default extra keys: ", e);
|
||||||
|
mExtraKeysInfo = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the terminal sessions shortcuts.
|
||||||
|
*/
|
||||||
|
private void setSessionShortcuts() {
|
||||||
|
if (mSessionShortcuts == null)
|
||||||
|
mSessionShortcuts = new ArrayList<>();
|
||||||
|
else
|
||||||
|
mSessionShortcuts.clear();
|
||||||
|
|
||||||
|
// The {@link TermuxPropertyConstants#MAP_SESSION_SHORTCUTS} stores the session shortcut key and action pair
|
||||||
|
for (Map.Entry<String, Integer> entry : TermuxPropertyConstants.MAP_SESSION_SHORTCUTS.entrySet()) {
|
||||||
|
// The mMap stores the code points for the session shortcuts while loading properties
|
||||||
|
Integer codePoint = (Integer) getInternalPropertyValue(entry.getKey(), true);
|
||||||
|
// If codePoint is null, then session shortcut did not exist in properties or was invalid
|
||||||
|
// as parsed by {@link #getCodePointForSessionShortcuts(String,String)}
|
||||||
|
// If codePoint is not null, then get the action for the MAP_SESSION_SHORTCUTS key and
|
||||||
|
// add the code point to sessionShortcuts
|
||||||
|
if (codePoint != null)
|
||||||
|
mSessionShortcuts.add(new KeyboardShortcut(codePoint, entry.getValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<KeyboardShortcut> getSessionShortcuts() {
|
||||||
|
return mSessionShortcuts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtraKeysInfo getExtraKeysInfo() {
|
||||||
|
return mExtraKeysInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package com.termux.app.terminal;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.text.SpannableString;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.style.StyleSpan;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.AdapterView;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.app.TermuxActivity;
|
||||||
|
import com.termux.shared.shell.TermuxSession;
|
||||||
|
import com.termux.terminal.TerminalSession;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class TermuxSessionsListViewController extends ArrayAdapter<TermuxSession> implements AdapterView.OnItemClickListener, AdapterView.OnItemLongClickListener {
|
||||||
|
|
||||||
|
final TermuxActivity mActivity;
|
||||||
|
|
||||||
|
final StyleSpan boldSpan = new StyleSpan(Typeface.BOLD);
|
||||||
|
final StyleSpan italicSpan = new StyleSpan(Typeface.ITALIC);
|
||||||
|
|
||||||
|
public TermuxSessionsListViewController(TermuxActivity activity, List<TermuxSession> sessionList) {
|
||||||
|
super(activity.getApplicationContext(), R.layout.item_terminal_sessions_list, sessionList);
|
||||||
|
this.mActivity = activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public View getView(int position, View convertView, @NonNull ViewGroup parent) {
|
||||||
|
View sessionRowView = convertView;
|
||||||
|
if (sessionRowView == null) {
|
||||||
|
LayoutInflater inflater = mActivity.getLayoutInflater();
|
||||||
|
sessionRowView = inflater.inflate(R.layout.item_terminal_sessions_list, parent, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextView sessionTitleView = sessionRowView.findViewById(R.id.session_title);
|
||||||
|
|
||||||
|
TerminalSession sessionAtRow = getItem(position).getTerminalSession();
|
||||||
|
if (sessionAtRow == null) {
|
||||||
|
sessionTitleView.setText("null session");
|
||||||
|
return sessionRowView;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isUsingBlackUI = mActivity.getProperties().isUsingBlackUI();
|
||||||
|
|
||||||
|
if (isUsingBlackUI) {
|
||||||
|
sessionTitleView.setBackground(
|
||||||
|
ContextCompat.getDrawable(mActivity, R.drawable.session_background_black_selected)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String name = sessionAtRow.mSessionName;
|
||||||
|
String sessionTitle = sessionAtRow.getTitle();
|
||||||
|
|
||||||
|
String numberPart = "[" + (position + 1) + "] ";
|
||||||
|
String sessionNamePart = (TextUtils.isEmpty(name) ? "" : name);
|
||||||
|
String sessionTitlePart = (TextUtils.isEmpty(sessionTitle) ? "" : ((sessionNamePart.isEmpty() ? "" : "\n") + sessionTitle));
|
||||||
|
|
||||||
|
String fullSessionTitle = numberPart + sessionNamePart + sessionTitlePart;
|
||||||
|
SpannableString fullSessionTitleStyled = new SpannableString(fullSessionTitle);
|
||||||
|
fullSessionTitleStyled.setSpan(boldSpan, 0, numberPart.length() + sessionNamePart.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
fullSessionTitleStyled.setSpan(italicSpan, numberPart.length() + sessionNamePart.length(), fullSessionTitle.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
|
||||||
|
sessionTitleView.setText(fullSessionTitleStyled);
|
||||||
|
|
||||||
|
boolean sessionRunning = sessionAtRow.isRunning();
|
||||||
|
|
||||||
|
if (sessionRunning) {
|
||||||
|
sessionTitleView.setPaintFlags(sessionTitleView.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
|
||||||
|
} else {
|
||||||
|
sessionTitleView.setPaintFlags(sessionTitleView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
|
||||||
|
}
|
||||||
|
int defaultColor = isUsingBlackUI ? Color.WHITE : Color.BLACK;
|
||||||
|
int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? defaultColor : Color.RED;
|
||||||
|
sessionTitleView.setTextColor(color);
|
||||||
|
return sessionRowView;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||||
|
TermuxSession clickedSession = getItem(position);
|
||||||
|
mActivity.getTermuxTerminalSessionClient().setCurrentSession(clickedSession.getTerminalSession());
|
||||||
|
mActivity.getDrawer().closeDrawers();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
|
||||||
|
final TermuxSession selectedSession = getItem(position);
|
||||||
|
mActivity.getTermuxTerminalSessionClient().renameSession(selectedSession.getTerminalSession());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,343 @@
|
|||||||
|
package com.termux.app.terminal;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.ClipboardManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.media.AudioAttributes;
|
||||||
|
import android.media.SoundPool;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.widget.ListView;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.shared.shell.TermuxSession;
|
||||||
|
import com.termux.shared.interact.DialogUtils;
|
||||||
|
import com.termux.app.TermuxActivity;
|
||||||
|
import com.termux.shared.terminal.TermuxTerminalSessionClientBase;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
import com.termux.app.TermuxService;
|
||||||
|
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||||
|
import com.termux.app.terminal.io.BellHandler;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.terminal.TerminalColors;
|
||||||
|
import com.termux.terminal.TerminalSession;
|
||||||
|
import com.termux.terminal.TextStyle;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
public class TermuxTerminalSessionClient extends TermuxTerminalSessionClientBase {
|
||||||
|
|
||||||
|
private final TermuxActivity mActivity;
|
||||||
|
|
||||||
|
private static final int MAX_SESSIONS = 8;
|
||||||
|
|
||||||
|
private final SoundPool mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes(
|
||||||
|
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||||
|
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build();
|
||||||
|
|
||||||
|
private final int mBellSoundId;
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "TermuxTerminalSessionClient";
|
||||||
|
|
||||||
|
public TermuxTerminalSessionClient(TermuxActivity activity) {
|
||||||
|
this.mActivity = activity;
|
||||||
|
|
||||||
|
mBellSoundId = mBellSoundPool.load(activity, R.raw.bell, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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) {
|
||||||
|
if (mActivity.getTermuxService().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 = mActivity.getTermuxService().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 (mActivity.getTermuxService().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:
|
||||||
|
mBellSoundPool.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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** Try switching to session. */
|
||||||
|
public void setCurrentSession(TerminalSession session) {
|
||||||
|
if (session == null) return;
|
||||||
|
|
||||||
|
if (mActivity.getTerminalView().attachSession(session)) {
|
||||||
|
// notify about switched session if not already displaying the session
|
||||||
|
notifyOfSessionChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We call the following even when the session is already being displayed since config may
|
||||||
|
// be stale, like current session not selected or scrolled to.
|
||||||
|
checkAndScrollToSession(session);
|
||||||
|
updateBackgroundColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
void notifyOfSessionChange() {
|
||||||
|
if (!mActivity.isVisible()) return;
|
||||||
|
|
||||||
|
TerminalSession session = mActivity.getCurrentSession();
|
||||||
|
mActivity.showToast(toToastTitle(session), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void switchToSession(boolean forward) {
|
||||||
|
TermuxService service = mActivity.getTermuxService();
|
||||||
|
|
||||||
|
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) {
|
||||||
|
TermuxSession termuxSession = mActivity.getTermuxService().getTermuxSession(index);
|
||||||
|
if (termuxSession != null)
|
||||||
|
setCurrentSession(termuxSession.getTerminalSession());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InflateParams")
|
||||||
|
public void renameSession(final TerminalSession sessionToRename) {
|
||||||
|
if (sessionToRename == null) return;
|
||||||
|
|
||||||
|
DialogUtils.textInput(mActivity, R.string.title_rename_session, sessionToRename.mSessionName, R.string.action_rename_session_confirm, text -> {
|
||||||
|
sessionToRename.mSessionName = text;
|
||||||
|
termuxSessionListNotifyUpdated();
|
||||||
|
}, -1, null, -1, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addNewSession(boolean isFailSafe, String sessionName) {
|
||||||
|
if (mActivity.getTermuxService().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 = mActivity.getTermuxService().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(mActivity);
|
||||||
|
|
||||||
|
if (stored != null) {
|
||||||
|
// If a stored session is in the list of currently running sessions, then return it
|
||||||
|
return stored;
|
||||||
|
} else {
|
||||||
|
// Else return the last session currently running
|
||||||
|
TermuxSession termuxSession = mActivity.getTermuxService().getLastTermuxSession();
|
||||||
|
if (termuxSession != null)
|
||||||
|
return termuxSession.getTerminalSession();
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private TerminalSession getCurrentStoredSession(TermuxActivity context) {
|
||||||
|
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
|
||||||
|
return context.getTermuxService().getTerminalSessionForHandle(sessionHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeFinishedSession(TerminalSession finishedSession) {
|
||||||
|
// Return pressed with finished session - remove it.
|
||||||
|
TermuxService service = mActivity.getTermuxService();
|
||||||
|
|
||||||
|
int index = service.removeTermuxSession(finishedSession);
|
||||||
|
|
||||||
|
int size = mActivity.getTermuxService().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;
|
||||||
|
final int indexOfSession = mActivity.getTermuxService().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) {
|
||||||
|
final int indexOfSession = mActivity.getTermuxService().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,443 @@
|
|||||||
|
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.inputmethod.InputMethodManager;
|
||||||
|
import android.widget.ListView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.app.TermuxActivity;
|
||||||
|
import com.termux.shared.shell.ShellUtils;
|
||||||
|
import com.termux.shared.terminal.TermuxTerminalViewClientBase;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
import com.termux.app.activities.ReportActivity;
|
||||||
|
import com.termux.app.models.ReportInfo;
|
||||||
|
import com.termux.app.models.UserAction;
|
||||||
|
import com.termux.app.terminal.io.KeyboardShortcut;
|
||||||
|
import com.termux.app.terminal.io.extrakeys.ExtraKeysView;
|
||||||
|
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||||
|
import com.termux.shared.data.DataUtils;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.markdown.MarkdownUtils;
|
||||||
|
import com.termux.shared.termux.TermuxUtils;
|
||||||
|
import com.termux.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;
|
||||||
|
|
||||||
|
public TermuxTerminalViewClient(TermuxActivity activity, TermuxTerminalSessionClient termuxTerminalSessionClient) {
|
||||||
|
this.mActivity = activity;
|
||||||
|
this.mTermuxTerminalSessionClient = termuxTerminalSessionClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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) {
|
||||||
|
InputMethodManager mgr = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
|
mgr.showSoftInput(mActivity.getTerminalView(), InputMethodManager.SHOW_IMPLICIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 */) {
|
||||||
|
InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
|
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
|
||||||
|
} else if (unicodeChar == 'm'/* menu */) {
|
||||||
|
mActivity.getTerminalView().showContextMenu();
|
||||||
|
} else if (unicodeChar == 'r'/* rename */) {
|
||||||
|
mTermuxTerminalSessionClient.renameSession(currentSession);
|
||||||
|
} else if (unicodeChar == 'c'/* create */) {
|
||||||
|
mTermuxTerminalSessionClient.addNewSession(false, null);
|
||||||
|
} else if (unicodeChar == 'u' /* urls */) {
|
||||||
|
showUrlSelection();
|
||||||
|
} else if (unicodeChar == 'v') {
|
||||||
|
doPaste();
|
||||||
|
} else if (unicodeChar == '+' || e.getUnicodeChar(KeyEvent.META_SHIFT_ON) == '+') {
|
||||||
|
// We also check for the shifted char here since shift may be required to produce '+',
|
||||||
|
// see https://github.com/termux/termux-api/issues/2
|
||||||
|
changeFontSize(true);
|
||||||
|
} else if (unicodeChar == '-') {
|
||||||
|
changeFontSize(false);
|
||||||
|
} else if (unicodeChar >= '1' && unicodeChar <= '9') {
|
||||||
|
int index = unicodeChar - '1';
|
||||||
|
mTermuxTerminalSessionClient.switchToSession(index);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onKeyUp(int keyCode, KeyEvent e) {
|
||||||
|
return handleVirtualKeys(keyCode, e, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Handle dedicated volume buttons as virtual keys if applicable. */
|
||||||
|
private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) {
|
||||||
|
InputDevice inputDevice = event.getDevice();
|
||||||
|
if (mActivity.getProperties().areVirtualVolumeKeysDisabled()) {
|
||||||
|
return false;
|
||||||
|
} else if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
|
||||||
|
// Do not steal dedicated buttons from a full external keyboard.
|
||||||
|
return false;
|
||||||
|
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
|
||||||
|
mVirtualControlKeyDown = down;
|
||||||
|
return true;
|
||||||
|
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
||||||
|
mVirtualFnKeyDown = down;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean readControlKey() {
|
||||||
|
return (mActivity.getExtraKeysView() != null && mActivity.getExtraKeysView().readSpecialButton(ExtraKeysView.SpecialButton.CTRL)) || mVirtualControlKeyDown;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean readAltKey() {
|
||||||
|
return (mActivity.getExtraKeysView() != null && mActivity.getExtraKeysView().readSpecialButton(ExtraKeysView.SpecialButton.ALT));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onLongPress(MotionEvent event) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCodePoint(final int codePoint, boolean ctrlDown, TerminalSession session) {
|
||||||
|
if (mVirtualFnKeyDown) {
|
||||||
|
int resultingKeyCode = -1;
|
||||||
|
int resultingCodePoint = -1;
|
||||||
|
boolean altDown = false;
|
||||||
|
int lowerCase = Character.toLowerCase(codePoint);
|
||||||
|
switch (lowerCase) {
|
||||||
|
// Arrow keys.
|
||||||
|
case 'w':
|
||||||
|
resultingKeyCode = KeyEvent.KEYCODE_DPAD_UP;
|
||||||
|
break;
|
||||||
|
case 'a':
|
||||||
|
resultingKeyCode = KeyEvent.KEYCODE_DPAD_LEFT;
|
||||||
|
break;
|
||||||
|
case 's':
|
||||||
|
resultingKeyCode = KeyEvent.KEYCODE_DPAD_DOWN;
|
||||||
|
break;
|
||||||
|
case 'd':
|
||||||
|
resultingKeyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Page up and down.
|
||||||
|
case 'p':
|
||||||
|
resultingKeyCode = KeyEvent.KEYCODE_PAGE_UP;
|
||||||
|
break;
|
||||||
|
case 'n':
|
||||||
|
resultingKeyCode = KeyEvent.KEYCODE_PAGE_DOWN;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Some special keys:
|
||||||
|
case 't':
|
||||||
|
resultingKeyCode = KeyEvent.KEYCODE_TAB;
|
||||||
|
break;
|
||||||
|
case 'i':
|
||||||
|
resultingKeyCode = KeyEvent.KEYCODE_INSERT;
|
||||||
|
break;
|
||||||
|
case 'h':
|
||||||
|
resultingCodePoint = '~';
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Special characters to input.
|
||||||
|
case 'u':
|
||||||
|
resultingCodePoint = '_';
|
||||||
|
break;
|
||||||
|
case 'l':
|
||||||
|
resultingCodePoint = '|';
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Function keys.
|
||||||
|
case '1':
|
||||||
|
case '2':
|
||||||
|
case '3':
|
||||||
|
case '4':
|
||||||
|
case '5':
|
||||||
|
case '6':
|
||||||
|
case '7':
|
||||||
|
case '8':
|
||||||
|
case '9':
|
||||||
|
resultingKeyCode = (codePoint - '1') + KeyEvent.KEYCODE_F1;
|
||||||
|
break;
|
||||||
|
case '0':
|
||||||
|
resultingKeyCode = KeyEvent.KEYCODE_F10;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Other special keys.
|
||||||
|
case 'e':
|
||||||
|
resultingCodePoint = /*Escape*/ 27;
|
||||||
|
break;
|
||||||
|
case '.':
|
||||||
|
resultingCodePoint = /*^.*/ 28;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'b': // alt+b, jumping backward in readline.
|
||||||
|
case 'f': // alf+f, jumping forward in readline.
|
||||||
|
case 'x': // alt+x, common in emacs.
|
||||||
|
resultingCodePoint = lowerCase;
|
||||||
|
altDown = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Volume control.
|
||||||
|
case 'v':
|
||||||
|
resultingCodePoint = -1;
|
||||||
|
AudioManager audio = (AudioManager) mActivity.getSystemService(Context.AUDIO_SERVICE);
|
||||||
|
audio.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME, AudioManager.USE_DEFAULT_STREAM_TYPE, AudioManager.FLAG_SHOW_UI);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Writing mode:
|
||||||
|
case 'q':
|
||||||
|
case 'k':
|
||||||
|
mActivity.toggleTerminalToolbar();
|
||||||
|
mVirtualFnKeyDown=false; // force disable fn key down to restore keyboard input into terminal view, fixes termux/termux-app#1420
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resultingKeyCode != -1) {
|
||||||
|
TerminalEmulator term = session.getEmulator();
|
||||||
|
session.write(KeyHandler.getCode(resultingKeyCode, 0, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode()));
|
||||||
|
} else if (resultingCodePoint != -1) {
|
||||||
|
session.writeCodePoint(altDown, resultingCodePoint);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else if (ctrlDown) {
|
||||||
|
if (codePoint == 106 /* Ctrl+j or \n */ && !session.isRunning()) {
|
||||||
|
mTermuxTerminalSessionClient.removeFinishedSession(session);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<KeyboardShortcut> shortcuts = mActivity.getProperties().getSessionShortcuts();
|
||||||
|
if (shortcuts != null && !shortcuts.isEmpty()) {
|
||||||
|
int codePointLowerCase = Character.toLowerCase(codePoint);
|
||||||
|
for (int i = shortcuts.size() - 1; i >= 0; i--) {
|
||||||
|
KeyboardShortcut shortcut = shortcuts.get(i);
|
||||||
|
if (codePointLowerCase == shortcut.codePoint) {
|
||||||
|
switch (shortcut.shortcutAction) {
|
||||||
|
case TermuxPropertyConstants.ACTION_SHORTCUT_CREATE_SESSION:
|
||||||
|
mTermuxTerminalSessionClient.addNewSession(false, null);
|
||||||
|
return true;
|
||||||
|
case TermuxPropertyConstants.ACTION_SHORTCUT_NEXT_SESSION:
|
||||||
|
mTermuxTerminalSessionClient.switchToSession(true);
|
||||||
|
return true;
|
||||||
|
case TermuxPropertyConstants.ACTION_SHORTCUT_PREVIOUS_SESSION:
|
||||||
|
mTermuxTerminalSessionClient.switchToSession(false);
|
||||||
|
return true;
|
||||||
|
case TermuxPropertyConstants.ACTION_SHORTCUT_RENAME_SESSION:
|
||||||
|
mTermuxTerminalSessionClient.renameSession(mActivity.getCurrentSession());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void changeFontSize(boolean increase) {
|
||||||
|
mActivity.getPreferences().changeFontSize(increase);
|
||||||
|
mActivity.getTerminalView().setTextSize(mActivity.getPreferences().getFontSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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("Failed to get share session transcript of length " + transcriptText.length(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showUrlSelection() {
|
||||||
|
TerminalSession session = mActivity.getCurrentSession();
|
||||||
|
if (session == null) return;
|
||||||
|
|
||||||
|
String text = ShellUtils.getTerminalSessionTranscriptText(session, true, true);
|
||||||
|
|
||||||
|
LinkedHashSet<CharSequence> urlSet = DataUtils.extractUrls(text);
|
||||||
|
if (urlSet.isEmpty()) {
|
||||||
|
new AlertDialog.Builder(mActivity).setMessage(R.string.title_select_url_none_found).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final CharSequence[] urls = urlSet.toArray(new CharSequence[0]);
|
||||||
|
Collections.reverse(Arrays.asList(urls)); // Latest first.
|
||||||
|
|
||||||
|
// Click to copy url to clipboard:
|
||||||
|
final AlertDialog dialog = new AlertDialog.Builder(mActivity).setItems(urls, (di, which) -> {
|
||||||
|
String url = (String) urls[which];
|
||||||
|
ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
|
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(url)));
|
||||||
|
Toast.makeText(mActivity, R.string.msg_select_url_copied_to_clipboard, Toast.LENGTH_LONG).show();
|
||||||
|
}).setTitle(R.string.title_select_url_dialog).create();
|
||||||
|
|
||||||
|
// Long press to open URL:
|
||||||
|
dialog.setOnShowListener(di -> {
|
||||||
|
ListView lv = dialog.getListView(); // this is a ListView with your "buds" in it
|
||||||
|
lv.setOnItemLongClickListener((parent, view, position, id) -> {
|
||||||
|
dialog.dismiss();
|
||||||
|
String url = (String) urls[position];
|
||||||
|
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||||
|
try {
|
||||||
|
mActivity.startActivity(i, null);
|
||||||
|
} catch (ActivityNotFoundException e) {
|
||||||
|
// If no applications match, Android displays a system message.
|
||||||
|
mActivity.startActivity(Intent.createChooser(i, null));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reportIssueFromTranscript() {
|
||||||
|
TerminalSession session = mActivity.getCurrentSession();
|
||||||
|
if (session == null) return;
|
||||||
|
|
||||||
|
String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
|
||||||
|
if (transcriptText == null) return;
|
||||||
|
|
||||||
|
transcriptText = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim();
|
||||||
|
|
||||||
|
StringBuilder reportString = new StringBuilder();
|
||||||
|
|
||||||
|
String title = TermuxConstants.TERMUX_APP_NAME + " Report Issue";
|
||||||
|
|
||||||
|
reportString.append("## Transcript\n");
|
||||||
|
reportString.append("\n").append(MarkdownUtils.getMarkdownCodeForString(transcriptText, true));
|
||||||
|
|
||||||
|
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, true));
|
||||||
|
reportString.append("\n\n").append(TermuxUtils.getDeviceInfoMarkdownString(mActivity));
|
||||||
|
|
||||||
|
String termuxAptInfo = TermuxUtils.geAPTInfoMarkdownString(mActivity);
|
||||||
|
if (termuxAptInfo != null)
|
||||||
|
reportString.append("\n\n").append(termuxAptInfo);
|
||||||
|
|
||||||
|
ReportActivity.startReportActivity(mActivity, new ReportInfo(UserAction.REPORT_ISSUE_FROM_TRANSCRIPT, TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY_NAME, title, null, reportString.toString(), "\n\n" + TermuxUtils.getReportIssueMarkdownString(mActivity), false));
|
||||||
|
}
|
||||||
|
|
||||||
|
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,114 @@
|
|||||||
|
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;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
package com.termux.app.terminal.io.extrakeys;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
public class ExtraKeysInfo {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matrix of buttons displayed
|
||||||
|
*/
|
||||||
|
private final ExtraKeyButton[][] buttons;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This corresponds to one of the CharMapDisplay below
|
||||||
|
*/
|
||||||
|
private String style;
|
||||||
|
|
||||||
|
public ExtraKeysInfo(String propertiesInfo, String style) throws JSONException {
|
||||||
|
this.style = style;
|
||||||
|
|
||||||
|
// Convert String propertiesInfo to Array of Arrays
|
||||||
|
JSONArray arr = new JSONArray(propertiesInfo);
|
||||||
|
Object[][] matrix = new Object[arr.length()][];
|
||||||
|
for (int i = 0; i < arr.length(); i++) {
|
||||||
|
JSONArray line = arr.getJSONArray(i);
|
||||||
|
matrix[i] = new Object[line.length()];
|
||||||
|
for (int j = 0; j < line.length(); j++) {
|
||||||
|
matrix[i][j] = line.get(j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert matrix to buttons
|
||||||
|
this.buttons = new ExtraKeyButton[matrix.length][];
|
||||||
|
for (int i = 0; i < matrix.length; i++) {
|
||||||
|
this.buttons[i] = new ExtraKeyButton[matrix[i].length];
|
||||||
|
for (int j = 0; j < matrix[i].length; j++) {
|
||||||
|
Object key = matrix[i][j];
|
||||||
|
|
||||||
|
JSONObject jobject = normalizeKeyConfig(key);
|
||||||
|
|
||||||
|
ExtraKeyButton button;
|
||||||
|
|
||||||
|
if (! jobject.has("popup")) {
|
||||||
|
// no popup
|
||||||
|
button = new ExtraKeyButton(getSelectedCharMap(), jobject);
|
||||||
|
} else {
|
||||||
|
// a popup
|
||||||
|
JSONObject popupJobject = normalizeKeyConfig(jobject.get("popup"));
|
||||||
|
ExtraKeyButton popup = new ExtraKeyButton(getSelectedCharMap(), popupJobject);
|
||||||
|
button = new ExtraKeyButton(getSelectedCharMap(), jobject, popup);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.buttons[i][j] = button;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "hello" -> {"key": "hello"}
|
||||||
|
*/
|
||||||
|
private static JSONObject normalizeKeyConfig(Object key) throws JSONException {
|
||||||
|
JSONObject jobject;
|
||||||
|
if (key instanceof String) {
|
||||||
|
jobject = new JSONObject();
|
||||||
|
jobject.put("key", key);
|
||||||
|
} else if (key instanceof JSONObject) {
|
||||||
|
jobject = (JSONObject) key;
|
||||||
|
} else {
|
||||||
|
throw new JSONException("An key in the extra-key matrix must be a string or an object");
|
||||||
|
}
|
||||||
|
return jobject;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtraKeyButton[][] getMatrix() {
|
||||||
|
return buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HashMap that implements Python dict.get(key, default) function.
|
||||||
|
* Default java.util .get(key) is then the same as .get(key, null);
|
||||||
|
*/
|
||||||
|
static class CleverMap<K,V> extends HashMap<K,V> {
|
||||||
|
V get(K key, V defaultValue) {
|
||||||
|
if (containsKey(key))
|
||||||
|
return get(key);
|
||||||
|
else
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class CharDisplayMap extends CleverMap<String, String> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keys are displayed in a natural looking way, like "→" for "RIGHT"
|
||||||
|
*/
|
||||||
|
static final CharDisplayMap classicArrowsDisplay = new CharDisplayMap() {{
|
||||||
|
// classic arrow keys (for ◀ ▶ ▲ ▼ @see arrowVariationDisplay)
|
||||||
|
put("LEFT", "←"); // U+2190 ← LEFTWARDS ARROW
|
||||||
|
put("RIGHT", "→"); // U+2192 → RIGHTWARDS ARROW
|
||||||
|
put("UP", "↑"); // U+2191 ↑ UPWARDS ARROW
|
||||||
|
put("DOWN", "↓"); // U+2193 ↓ DOWNWARDS ARROW
|
||||||
|
}};
|
||||||
|
|
||||||
|
static final CharDisplayMap wellKnownCharactersDisplay = new CharDisplayMap() {{
|
||||||
|
// well known characters // https://en.wikipedia.org/wiki/{Enter_key, Tab_key, Delete_key}
|
||||||
|
put("ENTER", "↲"); // U+21B2 ↲ DOWNWARDS ARROW WITH TIP LEFTWARDS
|
||||||
|
put("TAB", "↹"); // U+21B9 ↹ LEFTWARDS ARROW TO BAR OVER RIGHTWARDS ARROW TO BAR
|
||||||
|
put("BKSP", "⌫"); // U+232B ⌫ ERASE TO THE LEFT sometimes seen and easy to understand
|
||||||
|
put("DEL", "⌦"); // U+2326 ⌦ ERASE TO THE RIGHT not well known but easy to understand
|
||||||
|
put("DRAWER", "☰"); // U+2630 ☰ TRIGRAM FOR HEAVEN not well known but easy to understand
|
||||||
|
put("KEYBOARD", "⌨"); // U+2328 ⌨ KEYBOARD not well known but easy to understand
|
||||||
|
}};
|
||||||
|
|
||||||
|
static final CharDisplayMap lessKnownCharactersDisplay = new CharDisplayMap() {{
|
||||||
|
// https://en.wikipedia.org/wiki/{Home_key, End_key, Page_Up_and_Page_Down_keys}
|
||||||
|
// home key can mean "goto the beginning of line" or "goto first page" depending on context, hence the diagonal
|
||||||
|
put("HOME", "⇱"); // from IEC 9995 // U+21F1 ⇱ NORTH WEST ARROW TO CORNER
|
||||||
|
put("END", "⇲"); // from IEC 9995 // ⇲ // U+21F2 ⇲ SOUTH EAST ARROW TO CORNER
|
||||||
|
put("PGUP", "⇑"); // no ISO character exists, U+21D1 ⇑ UPWARDS DOUBLE ARROW will do the trick
|
||||||
|
put("PGDN", "⇓"); // no ISO character exists, U+21D3 ⇓ DOWNWARDS DOUBLE ARROW will do the trick
|
||||||
|
}};
|
||||||
|
|
||||||
|
static final CharDisplayMap arrowTriangleVariationDisplay = new CharDisplayMap() {{
|
||||||
|
// alternative to classic arrow keys
|
||||||
|
put("LEFT", "◀"); // U+25C0 ◀ BLACK LEFT-POINTING TRIANGLE
|
||||||
|
put("RIGHT", "▶"); // U+25B6 ▶ BLACK RIGHT-POINTING TRIANGLE
|
||||||
|
put("UP", "▲"); // U+25B2 ▲ BLACK UP-POINTING TRIANGLE
|
||||||
|
put("DOWN", "▼"); // U+25BC ▼ BLACK DOWN-POINTING TRIANGLE
|
||||||
|
}};
|
||||||
|
|
||||||
|
static final CharDisplayMap notKnownIsoCharacters = new CharDisplayMap() {{
|
||||||
|
// Control chars that are more clear as text // https://en.wikipedia.org/wiki/{Function_key, Alt_key, Control_key, Esc_key}
|
||||||
|
// put("FN", "FN"); // no ISO character exists
|
||||||
|
put("CTRL", "⎈"); // ISO character "U+2388 ⎈ HELM SYMBOL" is unknown to people and never printed on computers, however "U+25C7 ◇ WHITE DIAMOND" is a nice presentation, and "^" for terminal app and mac is often used
|
||||||
|
put("ALT", "⎇"); // ISO character "U+2387 ⎇ ALTERNATIVE KEY SYMBOL'" is unknown to people and only printed as the Option key "⌥" on Mac computer
|
||||||
|
put("ESC", "⎋"); // ISO character "U+238B ⎋ BROKEN CIRCLE WITH NORTHWEST ARROW" is unknown to people and not often printed on computers
|
||||||
|
}};
|
||||||
|
|
||||||
|
static final CharDisplayMap nicerLookingDisplay = new CharDisplayMap() {{
|
||||||
|
// nicer looking for most cases
|
||||||
|
put("-", "―"); // U+2015 ― HORIZONTAL BAR
|
||||||
|
}};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Multiple maps are available to quickly change
|
||||||
|
* the style of the keys.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some classic symbols everybody knows
|
||||||
|
*/
|
||||||
|
private static final CharDisplayMap defaultCharDisplay = new CharDisplayMap() {{
|
||||||
|
putAll(classicArrowsDisplay);
|
||||||
|
putAll(wellKnownCharactersDisplay);
|
||||||
|
putAll(nicerLookingDisplay);
|
||||||
|
// all other characters are displayed as themselves
|
||||||
|
}};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classic symbols and less known symbols
|
||||||
|
*/
|
||||||
|
private static final CharDisplayMap lotsOfArrowsCharDisplay = new CharDisplayMap() {{
|
||||||
|
putAll(classicArrowsDisplay);
|
||||||
|
putAll(wellKnownCharactersDisplay);
|
||||||
|
putAll(lessKnownCharactersDisplay); // NEW
|
||||||
|
putAll(nicerLookingDisplay);
|
||||||
|
}};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only arrows
|
||||||
|
*/
|
||||||
|
private static final CharDisplayMap arrowsOnlyCharDisplay = new CharDisplayMap() {{
|
||||||
|
putAll(classicArrowsDisplay);
|
||||||
|
// putAll(wellKnownCharactersDisplay); // REMOVED
|
||||||
|
// putAll(lessKnownCharactersDisplay); // REMOVED
|
||||||
|
putAll(nicerLookingDisplay);
|
||||||
|
}};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full Iso
|
||||||
|
*/
|
||||||
|
private static final CharDisplayMap fullIsoCharDisplay = new CharDisplayMap() {{
|
||||||
|
putAll(classicArrowsDisplay);
|
||||||
|
putAll(wellKnownCharactersDisplay);
|
||||||
|
putAll(lessKnownCharactersDisplay); // NEW
|
||||||
|
putAll(nicerLookingDisplay);
|
||||||
|
putAll(notKnownIsoCharacters); // NEW
|
||||||
|
}};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some people might call our keys differently
|
||||||
|
*/
|
||||||
|
static private final CharDisplayMap controlCharsAliases = new CharDisplayMap() {{
|
||||||
|
put("ESCAPE", "ESC");
|
||||||
|
put("CONTROL", "CTRL");
|
||||||
|
put("RETURN", "ENTER"); // Technically different keys, but most applications won't see the difference
|
||||||
|
put("FUNCTION", "FN");
|
||||||
|
// no alias for ALT
|
||||||
|
|
||||||
|
// Directions are sometimes written as first and last letter for brevety
|
||||||
|
put("LT", "LEFT");
|
||||||
|
put("RT", "RIGHT");
|
||||||
|
put("DN", "DOWN");
|
||||||
|
// put("UP", "UP"); well, "UP" is already two letters
|
||||||
|
|
||||||
|
put("PAGEUP", "PGUP");
|
||||||
|
put("PAGE_UP", "PGUP");
|
||||||
|
put("PAGE UP", "PGUP");
|
||||||
|
put("PAGE-UP", "PGUP");
|
||||||
|
|
||||||
|
// no alias for HOME
|
||||||
|
// no alias for END
|
||||||
|
|
||||||
|
put("PAGEDOWN", "PGDN");
|
||||||
|
put("PAGE_DOWN", "PGDN");
|
||||||
|
put("PAGE-DOWN", "PGDN");
|
||||||
|
|
||||||
|
put("DELETE", "DEL");
|
||||||
|
put("BACKSPACE", "BKSP");
|
||||||
|
|
||||||
|
// easier for writing in termux.properties
|
||||||
|
put("BACKSLASH", "\\");
|
||||||
|
put("QUOTE", "\"");
|
||||||
|
put("APOSTROPHE", "'");
|
||||||
|
}};
|
||||||
|
|
||||||
|
CharDisplayMap getSelectedCharMap() {
|
||||||
|
switch (style) {
|
||||||
|
case "arrows-only":
|
||||||
|
return arrowsOnlyCharDisplay;
|
||||||
|
case "arrows-all":
|
||||||
|
return lotsOfArrowsCharDisplay;
|
||||||
|
case "all":
|
||||||
|
return fullIsoCharDisplay;
|
||||||
|
case "none":
|
||||||
|
return new CharDisplayMap();
|
||||||
|
default:
|
||||||
|
return defaultCharDisplay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies the 'controlCharsAliases' mapping to all the strings in *buttons*
|
||||||
|
* Modifies the array, doesn't return a new one.
|
||||||
|
*/
|
||||||
|
public static String replaceAlias(String key) {
|
||||||
|
return controlCharsAliases.get(key, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,382 @@
|
|||||||
|
package com.termux.app.terminal.io.extrakeys;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.provider.Settings;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.HapticFeedbackConstants;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.GridLayout;
|
||||||
|
import android.widget.PopupWindow;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.view.TerminalView;
|
||||||
|
|
||||||
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A view showing extra keys (such as Escape, Ctrl, Alt) not normally available on an Android soft
|
||||||
|
* keyboard.
|
||||||
|
*/
|
||||||
|
public final class ExtraKeysView extends GridLayout {
|
||||||
|
|
||||||
|
private static final int TEXT_COLOR = 0xFFFFFFFF;
|
||||||
|
private static final int BUTTON_COLOR = 0x00000000;
|
||||||
|
private static final int INTERESTING_COLOR = 0xFF80DEEA;
|
||||||
|
private static final int BUTTON_PRESSED_COLOR = 0xFF7F7F7F;
|
||||||
|
|
||||||
|
public ExtraKeysView(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
static final Map<String, Integer> keyCodesForString = new HashMap<String, Integer>() {{
|
||||||
|
put("SPACE", KeyEvent.KEYCODE_SPACE);
|
||||||
|
put("ESC", KeyEvent.KEYCODE_ESCAPE);
|
||||||
|
put("TAB", KeyEvent.KEYCODE_TAB);
|
||||||
|
put("HOME", KeyEvent.KEYCODE_MOVE_HOME);
|
||||||
|
put("END", KeyEvent.KEYCODE_MOVE_END);
|
||||||
|
put("PGUP", KeyEvent.KEYCODE_PAGE_UP);
|
||||||
|
put("PGDN", KeyEvent.KEYCODE_PAGE_DOWN);
|
||||||
|
put("INS", KeyEvent.KEYCODE_INSERT);
|
||||||
|
put("DEL", KeyEvent.KEYCODE_FORWARD_DEL);
|
||||||
|
put("BKSP", KeyEvent.KEYCODE_DEL);
|
||||||
|
put("UP", KeyEvent.KEYCODE_DPAD_UP);
|
||||||
|
put("LEFT", KeyEvent.KEYCODE_DPAD_LEFT);
|
||||||
|
put("RIGHT", KeyEvent.KEYCODE_DPAD_RIGHT);
|
||||||
|
put("DOWN", KeyEvent.KEYCODE_DPAD_DOWN);
|
||||||
|
put("ENTER", KeyEvent.KEYCODE_ENTER);
|
||||||
|
put("F1", KeyEvent.KEYCODE_F1);
|
||||||
|
put("F2", KeyEvent.KEYCODE_F2);
|
||||||
|
put("F3", KeyEvent.KEYCODE_F3);
|
||||||
|
put("F4", KeyEvent.KEYCODE_F4);
|
||||||
|
put("F5", KeyEvent.KEYCODE_F5);
|
||||||
|
put("F6", KeyEvent.KEYCODE_F6);
|
||||||
|
put("F7", KeyEvent.KEYCODE_F7);
|
||||||
|
put("F8", KeyEvent.KEYCODE_F8);
|
||||||
|
put("F9", KeyEvent.KEYCODE_F9);
|
||||||
|
put("F10", KeyEvent.KEYCODE_F10);
|
||||||
|
put("F11", KeyEvent.KEYCODE_F11);
|
||||||
|
put("F12", KeyEvent.KEYCODE_F12);
|
||||||
|
}};
|
||||||
|
|
||||||
|
@SuppressLint("RtlHardcoded")
|
||||||
|
private void sendKey(View view, String keyName, boolean forceCtrlDown, boolean forceLeftAltDown) {
|
||||||
|
TerminalView terminalView = view.findViewById(R.id.terminal_view);
|
||||||
|
if ("KEYBOARD".equals(keyName)) {
|
||||||
|
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
|
imm.toggleSoftInput(0, 0);
|
||||||
|
} else if ("DRAWER".equals(keyName)) {
|
||||||
|
DrawerLayout drawer = view.findViewById(R.id.drawer_layout);
|
||||||
|
drawer.openDrawer(Gravity.LEFT);
|
||||||
|
} else if (keyCodesForString.containsKey(keyName)) {
|
||||||
|
Integer keyCode = keyCodesForString.get(keyName);
|
||||||
|
if (keyCode == null) return;
|
||||||
|
int metaState = 0;
|
||||||
|
if (forceCtrlDown) {
|
||||||
|
metaState |= KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON;
|
||||||
|
}
|
||||||
|
if (forceLeftAltDown) {
|
||||||
|
metaState |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON;
|
||||||
|
}
|
||||||
|
KeyEvent keyEvent = new KeyEvent(0, 0, KeyEvent.ACTION_UP, keyCode, 0, metaState);
|
||||||
|
terminalView.onKeyDown(keyCode, keyEvent);
|
||||||
|
} else {
|
||||||
|
// not a control char
|
||||||
|
keyName.codePoints().forEach(codePoint -> {
|
||||||
|
terminalView.inputCodePoint(codePoint, forceCtrlDown, forceLeftAltDown);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendKey(View view, ExtraKeyButton buttonInfo) {
|
||||||
|
if (buttonInfo.isMacro()) {
|
||||||
|
String[] keys = buttonInfo.getKey().split(" ");
|
||||||
|
boolean ctrlDown = false;
|
||||||
|
boolean altDown = false;
|
||||||
|
for (String key : keys) {
|
||||||
|
if ("CTRL".equals(key)) {
|
||||||
|
ctrlDown = true;
|
||||||
|
} else if ("ALT".equals(key)) {
|
||||||
|
altDown = true;
|
||||||
|
} else {
|
||||||
|
sendKey(view, key, ctrlDown, altDown);
|
||||||
|
ctrlDown = false;
|
||||||
|
altDown = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sendKey(view, buttonInfo.getKey(), false, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum SpecialButton {
|
||||||
|
CTRL, ALT, FN
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SpecialButtonState {
|
||||||
|
boolean isOn = false;
|
||||||
|
boolean isActive = false;
|
||||||
|
List<Button> buttons = new ArrayList<>();
|
||||||
|
|
||||||
|
void setIsActive(boolean value) {
|
||||||
|
isActive = value;
|
||||||
|
buttons.forEach(button -> button.setTextColor(value ? INTERESTING_COLOR : TEXT_COLOR));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Map<SpecialButton, SpecialButtonState> specialButtons = new HashMap<SpecialButton, SpecialButtonState>() {{
|
||||||
|
put(SpecialButton.CTRL, new SpecialButtonState());
|
||||||
|
put(SpecialButton.ALT, new SpecialButtonState());
|
||||||
|
put(SpecialButton.FN, new SpecialButtonState());
|
||||||
|
}};
|
||||||
|
|
||||||
|
private final Set<String> specialButtonsKeys = specialButtons.keySet().stream().map(Enum::name).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
private boolean isSpecialButton(ExtraKeyButton button) {
|
||||||
|
return specialButtonsKeys.contains(button.getKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ScheduledExecutorService scheduledExecutor;
|
||||||
|
private PopupWindow popupWindow;
|
||||||
|
private int longPressCount;
|
||||||
|
|
||||||
|
public boolean readSpecialButton(SpecialButton name) {
|
||||||
|
SpecialButtonState state = specialButtons.get(name);
|
||||||
|
if (state == null)
|
||||||
|
throw new RuntimeException("Must be a valid special button (see source)");
|
||||||
|
|
||||||
|
if (!state.isOn || !state.isActive)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
state.setIsActive(false);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Button createSpecialButton(String buttonKey, boolean needUpdate) {
|
||||||
|
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonKey));
|
||||||
|
if (state == null) return null;
|
||||||
|
state.isOn = true;
|
||||||
|
Button button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||||
|
button.setTextColor(state.isActive ? INTERESTING_COLOR : TEXT_COLOR);
|
||||||
|
if (needUpdate) {
|
||||||
|
state.buttons.add(button);
|
||||||
|
}
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
void popup(View view, ExtraKeyButton extraButton) {
|
||||||
|
int width = view.getMeasuredWidth();
|
||||||
|
int height = view.getMeasuredHeight();
|
||||||
|
Button button;
|
||||||
|
if (isSpecialButton(extraButton)) {
|
||||||
|
button = createSpecialButton(extraButton.getKey(), false);
|
||||||
|
if (button == null) return;
|
||||||
|
} else {
|
||||||
|
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||||
|
button.setTextColor(TEXT_COLOR);
|
||||||
|
}
|
||||||
|
button.setText(extraButton.getDisplay());
|
||||||
|
button.setPadding(0, 0, 0, 0);
|
||||||
|
button.setMinHeight(0);
|
||||||
|
button.setMinWidth(0);
|
||||||
|
button.setMinimumWidth(0);
|
||||||
|
button.setMinimumHeight(0);
|
||||||
|
button.setWidth(width);
|
||||||
|
button.setHeight(height);
|
||||||
|
button.setBackgroundColor(BUTTON_PRESSED_COLOR);
|
||||||
|
popupWindow = new PopupWindow(this);
|
||||||
|
popupWindow.setWidth(LayoutParams.WRAP_CONTENT);
|
||||||
|
popupWindow.setHeight(LayoutParams.WRAP_CONTENT);
|
||||||
|
popupWindow.setContentView(button);
|
||||||
|
popupWindow.setOutsideTouchable(true);
|
||||||
|
popupWindow.setFocusable(false);
|
||||||
|
popupWindow.showAsDropDown(view, 0, -2 * height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* General util function to compute the longest column length in a matrix.
|
||||||
|
*/
|
||||||
|
static int maximumLength(Object[][] matrix) {
|
||||||
|
int m = 0;
|
||||||
|
for (Object[] row : matrix)
|
||||||
|
m = Math.max(m, row.length);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload the view given parameters in termux.properties
|
||||||
|
*
|
||||||
|
* @param infos matrix as defined in termux.properties extrakeys
|
||||||
|
* Can Contain The Strings CTRL ALT TAB FN ENTER LEFT RIGHT UP DOWN or normal strings
|
||||||
|
* Some aliases are possible like RETURN for ENTER, LT for LEFT and more (@see controlCharsAliases for the whole list).
|
||||||
|
* Any string of length > 1 in total Uppercase will print a warning
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* "ENTER" will trigger the ENTER keycode
|
||||||
|
* "LEFT" will trigger the LEFT keycode and be displayed as "←"
|
||||||
|
* "→" will input a "→" character
|
||||||
|
* "−" will input a "−" character
|
||||||
|
* "-_-" will input the string "-_-"
|
||||||
|
*/
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
public void reload(ExtraKeysInfo infos) {
|
||||||
|
if (infos == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for(SpecialButtonState state : specialButtons.values())
|
||||||
|
state.buttons = new ArrayList<>();
|
||||||
|
|
||||||
|
removeAllViews();
|
||||||
|
|
||||||
|
ExtraKeyButton[][] buttons = infos.getMatrix();
|
||||||
|
|
||||||
|
setRowCount(buttons.length);
|
||||||
|
setColumnCount(maximumLength(buttons));
|
||||||
|
|
||||||
|
for (int row = 0; row < buttons.length; row++) {
|
||||||
|
for (int col = 0; col < buttons[row].length; col++) {
|
||||||
|
final ExtraKeyButton buttonInfo = buttons[row][col];
|
||||||
|
|
||||||
|
Button button;
|
||||||
|
if (isSpecialButton(buttonInfo)) {
|
||||||
|
button = createSpecialButton(buttonInfo.getKey(), true);
|
||||||
|
if (button == null) return;
|
||||||
|
} else {
|
||||||
|
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.setText(buttonInfo.getDisplay());
|
||||||
|
button.setTextColor(TEXT_COLOR);
|
||||||
|
button.setPadding(0, 0, 0, 0);
|
||||||
|
|
||||||
|
final Button finalButton = button;
|
||||||
|
button.setOnClickListener(v -> {
|
||||||
|
if (Settings.System.getInt(getContext().getContentResolver(),
|
||||||
|
Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) != 0) {
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= 28) {
|
||||||
|
finalButton.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||||
|
} else {
|
||||||
|
// Perform haptic feedback only if no total silence mode enabled.
|
||||||
|
if (Settings.Global.getInt(getContext().getContentResolver(), "zen_mode", 0) != 2) {
|
||||||
|
finalButton.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
View root = getRootView();
|
||||||
|
if (isSpecialButton(buttonInfo)) {
|
||||||
|
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonInfo.getKey()));
|
||||||
|
if (state == null) return;
|
||||||
|
state.setIsActive(!state.isActive);
|
||||||
|
} else {
|
||||||
|
sendKey(root, buttonInfo);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
button.setOnTouchListener((v, event) -> {
|
||||||
|
final View root = getRootView();
|
||||||
|
switch (event.getAction()) {
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
longPressCount = 0;
|
||||||
|
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
|
||||||
|
if (Arrays.asList("UP", "DOWN", "LEFT", "RIGHT", "BKSP", "DEL").contains(buttonInfo.getKey())) {
|
||||||
|
// autorepeat
|
||||||
|
scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
|
||||||
|
scheduledExecutor.scheduleWithFixedDelay(() -> {
|
||||||
|
longPressCount++;
|
||||||
|
sendKey(root, buttonInfo);
|
||||||
|
}, 400, 80, TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_MOVE:
|
||||||
|
if (buttonInfo.getPopup() != null) {
|
||||||
|
if (popupWindow == null && event.getY() < 0) {
|
||||||
|
if (scheduledExecutor != null) {
|
||||||
|
scheduledExecutor.shutdownNow();
|
||||||
|
scheduledExecutor = null;
|
||||||
|
}
|
||||||
|
v.setBackgroundColor(BUTTON_COLOR);
|
||||||
|
popup(v, buttonInfo.getPopup());
|
||||||
|
}
|
||||||
|
if (popupWindow != null && event.getY() > 0) {
|
||||||
|
v.setBackgroundColor(BUTTON_PRESSED_COLOR);
|
||||||
|
popupWindow.dismiss();
|
||||||
|
popupWindow = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_CANCEL:
|
||||||
|
v.setBackgroundColor(BUTTON_COLOR);
|
||||||
|
if (scheduledExecutor != null) {
|
||||||
|
scheduledExecutor.shutdownNow();
|
||||||
|
scheduledExecutor = null;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
v.setBackgroundColor(BUTTON_COLOR);
|
||||||
|
if (scheduledExecutor != null) {
|
||||||
|
scheduledExecutor.shutdownNow();
|
||||||
|
scheduledExecutor = null;
|
||||||
|
}
|
||||||
|
if (longPressCount == 0 || popupWindow != null) {
|
||||||
|
if (popupWindow != null) {
|
||||||
|
popupWindow.setContentView(null);
|
||||||
|
popupWindow.dismiss();
|
||||||
|
popupWindow = null;
|
||||||
|
if (buttonInfo.getPopup() != null) {
|
||||||
|
if (isSpecialButton(buttonInfo.getPopup())) {
|
||||||
|
SpecialButtonState state = specialButtons.get(SpecialButton.valueOf(buttonInfo.getPopup().getKey()));
|
||||||
|
if (state == null) return true;
|
||||||
|
state.setIsActive(!state.isActive);
|
||||||
|
} else {
|
||||||
|
sendKey(root, buttonInfo.getPopup());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
v.performClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
LayoutParams param = new GridLayout.LayoutParams();
|
||||||
|
param.width = 0;
|
||||||
|
param.height = 0;
|
||||||
|
param.setMargins(0, 0, 0, 0);
|
||||||
|
param.columnSpec = GridLayout.spec(col, GridLayout.FILL, 1.f);
|
||||||
|
param.rowSpec = GridLayout.spec(row, GridLayout.FILL, 1.f);
|
||||||
|
button.setLayoutParams(param);
|
||||||
|
|
||||||
|
addView(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
156
app/src/main/java/com/termux/app/utils/CrashUtils.java
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
package com.termux.app.utils;
|
||||||
|
|
||||||
|
import android.app.Notification;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.app.activities.ReportActivity;
|
||||||
|
import com.termux.shared.notification.NotificationUtils;
|
||||||
|
import com.termux.shared.file.FileUtils;
|
||||||
|
import com.termux.app.models.ReportInfo;
|
||||||
|
import com.termux.app.models.UserAction;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxPreferenceConstants;
|
||||||
|
import com.termux.shared.data.DataUtils;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.termux.TermuxUtils;
|
||||||
|
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
|
||||||
|
public class CrashUtils {
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "CrashUtils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify the user of a previous app crash by reading the crash info from the crash log file at
|
||||||
|
* {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}. The crash log file would have been
|
||||||
|
* created by {@link com.termux.shared.crash.CrashHandler}.
|
||||||
|
*
|
||||||
|
* If the crash log file exists and is not empty and
|
||||||
|
* {@link TermuxPreferenceConstants.TERMUX_APP#KEY_CRASH_REPORT_NOTIFICATIONS_ENABLED} is
|
||||||
|
* enabled, then a notification will be shown for the crash on the
|
||||||
|
* {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME} channel, otherwise nothing will be done.
|
||||||
|
*
|
||||||
|
* After reading from the crash log file, it will be moved to {@link TermuxConstants#TERMUX_CRASH_LOG_BACKUP_FILE_PATH}.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} for operations.
|
||||||
|
* @param logTagParam The log tag to use for logging.
|
||||||
|
*/
|
||||||
|
public static void notifyCrash(final Context context, final String logTagParam) {
|
||||||
|
if (context == null) return;
|
||||||
|
|
||||||
|
|
||||||
|
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(context);
|
||||||
|
// If user has disabled notifications for crashes
|
||||||
|
if (!preferences.getCrashReportNotificationsEnabled())
|
||||||
|
return;
|
||||||
|
|
||||||
|
new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
String logTag = DataUtils.getDefaultIfNull(logTagParam, LOG_TAG);
|
||||||
|
|
||||||
|
if (!FileUtils.regularFileExists(TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, false))
|
||||||
|
return;
|
||||||
|
|
||||||
|
String errmsg;
|
||||||
|
StringBuilder reportStringBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
// Read report string from crash log file
|
||||||
|
errmsg = FileUtils.readStringFromFile(context, "crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, Charset.defaultCharset(), reportStringBuilder, false);
|
||||||
|
if (errmsg != null) {
|
||||||
|
Logger.logError(logTag, errmsg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move crash log file to backup location if it exists
|
||||||
|
FileUtils.moveRegularFile(context, "crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, TermuxConstants.TERMUX_CRASH_LOG_BACKUP_FILE_PATH, true);
|
||||||
|
if (errmsg != null) {
|
||||||
|
Logger.logError(logTag, errmsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
String reportString = reportStringBuilder.toString();
|
||||||
|
|
||||||
|
if (reportString == null || reportString.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Send a notification to show the crash log which when clicked will open the {@link ReportActivity}
|
||||||
|
// to show the details of the crash
|
||||||
|
String title = TermuxConstants.TERMUX_APP_NAME + " Crash Report";
|
||||||
|
|
||||||
|
Logger.logDebug(logTag, "The crash log file at \"" + TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH + "\" found. Sending \"" + title + "\" notification.");
|
||||||
|
|
||||||
|
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.CRASH_REPORT, logTag, title, null, reportString, "\n\n" + TermuxUtils.getReportIssueMarkdownString(context), true));
|
||||||
|
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
|
||||||
|
// Setup the notification channel if not already set up
|
||||||
|
setupCrashReportsNotificationChannel(context);
|
||||||
|
|
||||||
|
// Build the notification
|
||||||
|
Notification.Builder builder = getCrashReportsNotificationBuilder(context, title, null, null, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
|
||||||
|
if (builder == null) return;
|
||||||
|
|
||||||
|
// Send the notification
|
||||||
|
int nextNotificationId = NotificationUtils.getNextNotificationId(context);
|
||||||
|
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
|
||||||
|
if (notificationManager != null)
|
||||||
|
notificationManager.notify(nextNotificationId, builder.build());
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get {@link Notification.Builder} for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID}
|
||||||
|
* and {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} for operations.
|
||||||
|
* @param title The title for the notification.
|
||||||
|
* @param notificationText The second line text of the notification.
|
||||||
|
* @param notificationBigText The full text of the notification that may optionally be styled.
|
||||||
|
* @param pendingIntent The {@link PendingIntent} which should be sent when notification is clicked.
|
||||||
|
* @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}.
|
||||||
|
* @return Returns the {@link Notification.Builder}.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static Notification.Builder getCrashReportsNotificationBuilder(final Context context, final CharSequence title, final CharSequence notificationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) {
|
||||||
|
|
||||||
|
Notification.Builder builder = NotificationUtils.geNotificationBuilder(context,
|
||||||
|
TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_HIGH,
|
||||||
|
title, notificationText, notificationBigText, pendingIntent, notificationMode);
|
||||||
|
|
||||||
|
if (builder == null) return null;
|
||||||
|
|
||||||
|
// Enable timestamp
|
||||||
|
builder.setShowWhen(true);
|
||||||
|
|
||||||
|
// Set notification icon
|
||||||
|
builder.setSmallIcon(R.drawable.ic_error_notification);
|
||||||
|
|
||||||
|
// Set background color for small notification icon
|
||||||
|
builder.setColor(0xFF607D8B);
|
||||||
|
|
||||||
|
// Dismiss on click
|
||||||
|
builder.setAutoCancel(true);
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup the notification channel for {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID} and
|
||||||
|
* {@link TermuxConstants#TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME}.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} for operations.
|
||||||
|
*/
|
||||||
|
public static void setupCrashReportsNotificationChannel(final Context context) {
|
||||||
|
NotificationUtils.setupNotificationChannel(context, TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID,
|
||||||
|
TermuxConstants.TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
330
app/src/main/java/com/termux/app/utils/PluginUtils.java
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
package com.termux.app.utils;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.Notification;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.termux.R;
|
||||||
|
import com.termux.shared.notification.NotificationUtils;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||||
|
import com.termux.app.activities.ReportActivity;
|
||||||
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||||
|
import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_APP;
|
||||||
|
import com.termux.shared.settings.properties.SharedProperties;
|
||||||
|
import com.termux.shared.settings.properties.TermuxPropertyConstants;
|
||||||
|
import com.termux.app.models.ReportInfo;
|
||||||
|
import com.termux.shared.models.ExecutionCommand;
|
||||||
|
import com.termux.app.models.UserAction;
|
||||||
|
import com.termux.shared.data.DataUtils;
|
||||||
|
import com.termux.shared.markdown.MarkdownUtils;
|
||||||
|
import com.termux.shared.termux.TermuxUtils;
|
||||||
|
|
||||||
|
public class PluginUtils {
|
||||||
|
|
||||||
|
/** Required file permissions for the executable file of execute intent. Executable file must have read and execute permissions */
|
||||||
|
public static final String PLUGIN_EXECUTABLE_FILE_PERMISSIONS = "r-x"; // Default: "r-x"
|
||||||
|
/** Required file permissions for the working directory of execute intent. Working directory must have read and write permissions.
|
||||||
|
* Execute permissions should be attempted to be set, but ignored if they are missing */
|
||||||
|
public static final String PLUGIN_WORKING_DIRECTORY_PERMISSIONS = "rwx"; // Default: "rwx"
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "PluginUtils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process {@link ExecutionCommand} result.
|
||||||
|
*
|
||||||
|
* The ExecutionCommand currentState must be greater or equal to
|
||||||
|
* {@link ExecutionCommand.ExecutionState#EXECUTED}.
|
||||||
|
* If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and
|
||||||
|
* {@link ExecutionCommand#pluginPendingIntent} is not {@code null}, then the result of commands
|
||||||
|
* are sent back to the {@link PendingIntent} creator.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} that will be used to send result intent to the {@link PendingIntent} creator.
|
||||||
|
* @param logTag The log tag to use for logging.
|
||||||
|
* @param executionCommand The {@link ExecutionCommand} to process.
|
||||||
|
*/
|
||||||
|
public static void processPluginExecutionCommandResult(final Context context, String logTag, final ExecutionCommand executionCommand) {
|
||||||
|
if (executionCommand == null) return;
|
||||||
|
|
||||||
|
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||||
|
|
||||||
|
if (!executionCommand.hasExecuted()) {
|
||||||
|
Logger.logWarn(logTag, "Ignoring call to processPluginExecutionCommandResult() since the execution command state is not higher than the ExecutionState.EXECUTED");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.logDebug(LOG_TAG, executionCommand.toString());
|
||||||
|
|
||||||
|
boolean result = true;
|
||||||
|
|
||||||
|
// If isPluginExecutionCommand is true and pluginPendingIntent is not null, then
|
||||||
|
// send pluginPendingIntent to its creator with the result
|
||||||
|
if (executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null) {
|
||||||
|
String errmsg = executionCommand.errmsg;
|
||||||
|
|
||||||
|
//Combine errmsg and stacktraces
|
||||||
|
if (executionCommand.isStateFailed()) {
|
||||||
|
errmsg = Logger.getMessageAndStackTracesString(executionCommand.errmsg, executionCommand.throwableList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send pluginPendingIntent to its creator
|
||||||
|
result = sendPluginExecutionCommandResultPendingIntent(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.stdout, executionCommand.stderr, executionCommand.exitCode, executionCommand.errCode, errmsg, executionCommand.pluginPendingIntent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!executionCommand.isStateFailed() && result)
|
||||||
|
executionCommand.setState(ExecutionCommand.ExecutionState.SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process {@link ExecutionCommand} error.
|
||||||
|
*
|
||||||
|
* The ExecutionCommand currentState must be equal to {@link ExecutionCommand.ExecutionState#FAILED}.
|
||||||
|
* The {@link ExecutionCommand#errCode} must have been set to a value greater than
|
||||||
|
* {@link ExecutionCommand#RESULT_CODE_OK}.
|
||||||
|
* The {@link ExecutionCommand#errmsg} and any {@link ExecutionCommand#throwableList} must also
|
||||||
|
* be set with appropriate error info.
|
||||||
|
*
|
||||||
|
* If the {@link ExecutionCommand#isPluginExecutionCommand} is {@code true} and
|
||||||
|
* {@link ExecutionCommand#pluginPendingIntent} is not {@code null}, then the errors of commands
|
||||||
|
* are sent back to the {@link PendingIntent} creator.
|
||||||
|
*
|
||||||
|
* Otherwise if the {@link TERMUX_APP#KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED} is
|
||||||
|
* enabled, then a flash and a notification will be shown for the error as well
|
||||||
|
* on the {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME} channel instead of just logging
|
||||||
|
* the error.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} for operations.
|
||||||
|
* @param logTag The log tag to use for logging.
|
||||||
|
* @param executionCommand The {@link ExecutionCommand} that failed.
|
||||||
|
* @param forceNotification If set to {@code true}, then a flash and notification will be shown
|
||||||
|
* regardless of if pending intent is {@code null} or
|
||||||
|
* {@link TERMUX_APP#KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED}
|
||||||
|
* is {@code false}.
|
||||||
|
*/
|
||||||
|
public static void processPluginExecutionCommandError(final Context context, String logTag, final ExecutionCommand executionCommand, boolean forceNotification) {
|
||||||
|
if (context == null || executionCommand == null) return;
|
||||||
|
|
||||||
|
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||||
|
|
||||||
|
if (!executionCommand.isStateFailed()) {
|
||||||
|
Logger.logWarn(logTag, "Ignoring call to processPluginExecutionCommandError() since the execution command is not in ExecutionState.FAILED");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the error and any exception
|
||||||
|
Logger.logStackTracesWithMessage(logTag, "(" + executionCommand.errCode + ") " + executionCommand.errmsg, executionCommand.throwableList);
|
||||||
|
|
||||||
|
|
||||||
|
// If isPluginExecutionCommand is true and pluginPendingIntent is not null, then
|
||||||
|
// send pluginPendingIntent to its creator with the errors
|
||||||
|
if (executionCommand.isPluginExecutionCommand && executionCommand.pluginPendingIntent != null) {
|
||||||
|
String errmsg = executionCommand.errmsg;
|
||||||
|
|
||||||
|
//Combine errmsg and stacktraces
|
||||||
|
if (executionCommand.isStateFailed()) {
|
||||||
|
errmsg = Logger.getMessageAndStackTracesString(executionCommand.errmsg, executionCommand.throwableList);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendPluginExecutionCommandResultPendingIntent(context, logTag, executionCommand.getCommandIdAndLabelLogString(), executionCommand.stdout, executionCommand.stderr, executionCommand.exitCode, executionCommand.errCode, errmsg, executionCommand.pluginPendingIntent);
|
||||||
|
|
||||||
|
// No need to show notifications if a pending intent was sent, let the caller handle the result himself
|
||||||
|
if (!forceNotification) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(context);
|
||||||
|
// If user has disabled notifications for plugin, then just return
|
||||||
|
if (!preferences.getPluginErrorNotificationsEnabled() && !forceNotification)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Flash the errmsg
|
||||||
|
Logger.showToast(context, executionCommand.errmsg, true);
|
||||||
|
|
||||||
|
// Send a notification to show the errmsg which when clicked will open the {@link ReportActivity}
|
||||||
|
// to show the details of the error
|
||||||
|
String title = TermuxConstants.TERMUX_APP_NAME + " Plugin Execution Command Error";
|
||||||
|
|
||||||
|
StringBuilder reportString = new StringBuilder();
|
||||||
|
|
||||||
|
reportString.append(ExecutionCommand.getExecutionCommandMarkdownString(executionCommand));
|
||||||
|
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, true));
|
||||||
|
reportString.append("\n\n").append(TermuxUtils.getDeviceInfoMarkdownString(context));
|
||||||
|
|
||||||
|
Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.PLUGIN_EXECUTION_COMMAND, logTag, title, null, reportString.toString(), null,true));
|
||||||
|
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
|
||||||
|
// Setup the notification channel if not already set up
|
||||||
|
setupPluginCommandErrorsNotificationChannel(context);
|
||||||
|
|
||||||
|
// Use markdown in notification
|
||||||
|
CharSequence notificationText = MarkdownUtils.getSpannedMarkdownText(context, executionCommand.errmsg);
|
||||||
|
//CharSequence notificationText = executionCommand.errmsg;
|
||||||
|
|
||||||
|
// Build the notification
|
||||||
|
Notification.Builder builder = getPluginCommandErrorsNotificationBuilder(context, title, notificationText, notificationText, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE);
|
||||||
|
if (builder == null) return;
|
||||||
|
|
||||||
|
// Send the notification
|
||||||
|
int nextNotificationId = NotificationUtils.getNextNotificationId(context);
|
||||||
|
NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
|
||||||
|
if (notificationManager != null)
|
||||||
|
notificationManager.notify(nextNotificationId, builder.build());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send {@link ExecutionCommand} result {@link PendingIntent} in the
|
||||||
|
* {@link TERMUX_SERVICE#EXTRA_PLUGIN_RESULT_BUNDLE} bundle.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} that will be used to send result intent to the {@link PendingIntent} creator.
|
||||||
|
* @param logTag The log tag to use for logging.
|
||||||
|
* @param label The label of {@link ExecutionCommand}.
|
||||||
|
* @param stdout The stdout of {@link ExecutionCommand}.
|
||||||
|
* @param stderr The stderr of {@link ExecutionCommand}.
|
||||||
|
* @param exitCode The exitCode of {@link ExecutionCommand}.
|
||||||
|
* @param errCode The errCode of {@link ExecutionCommand}.
|
||||||
|
* @param errmsg The errmsg of {@link ExecutionCommand}.
|
||||||
|
* @param pluginPendingIntent The pluginPendingIntent of {@link ExecutionCommand}.
|
||||||
|
* @return Returns {@code true} if pluginPendingIntent was successfully send, otherwise [@code false}.
|
||||||
|
*/
|
||||||
|
public static boolean sendPluginExecutionCommandResultPendingIntent(Context context, String logTag, String label, String stdout, String stderr, Integer exitCode, Integer errCode, String errmsg, PendingIntent pluginPendingIntent) {
|
||||||
|
if (context == null || pluginPendingIntent == null) return false;
|
||||||
|
|
||||||
|
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||||
|
|
||||||
|
Logger.logDebug(logTag, "Sending execution result for Execution Command \"" + label + "\" to " + pluginPendingIntent.getCreatorPackage());
|
||||||
|
|
||||||
|
String truncatedStdout = null;
|
||||||
|
String truncatedStderr = null;
|
||||||
|
|
||||||
|
String stdoutOriginalLength = (stdout == null) ? null: String.valueOf(stdout.length());
|
||||||
|
String stderrOriginalLength = (stderr == null) ? null: String.valueOf(stderr.length());
|
||||||
|
|
||||||
|
// Truncate stdout and stdout to max TRANSACTION_SIZE_LIMIT_IN_BYTES
|
||||||
|
if (stderr == null || stderr.isEmpty()) {
|
||||||
|
truncatedStdout = DataUtils.getTruncatedCommandOutput(stdout, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false);
|
||||||
|
} else if (stdout == null || stdout.isEmpty()) {
|
||||||
|
truncatedStderr = DataUtils.getTruncatedCommandOutput(stderr, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false);
|
||||||
|
} else {
|
||||||
|
truncatedStdout = DataUtils.getTruncatedCommandOutput(stdout, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 2, false, false, false);
|
||||||
|
truncatedStderr = DataUtils.getTruncatedCommandOutput(stderr, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 2, false, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (truncatedStdout != null && truncatedStdout.length() < stdout.length()) {
|
||||||
|
Logger.logWarn(logTag, "Execution Result for Execution Command \"" + label + "\" stdout length truncated from " + stdoutOriginalLength + " to " + truncatedStdout.length());
|
||||||
|
stdout = truncatedStdout;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (truncatedStderr != null && truncatedStderr.length() < stderr.length()) {
|
||||||
|
Logger.logWarn(logTag, "Execution Result for Execution Command \"" + label + "\" stderr length truncated from " + stderrOriginalLength + " to " + truncatedStderr.length());
|
||||||
|
stderr = truncatedStderr;
|
||||||
|
}
|
||||||
|
|
||||||
|
String errmsgOriginalLength = (errmsg == null) ? null: String.valueOf(errmsg.length());
|
||||||
|
|
||||||
|
// Truncate errmsg to max TRANSACTION_SIZE_LIMIT_IN_BYTES / 4
|
||||||
|
// trim from end to preserve start of stacktraces
|
||||||
|
String truncatedErrmsg = DataUtils.getTruncatedCommandOutput(errmsg, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 4, true, false, false);
|
||||||
|
if (truncatedErrmsg != null && truncatedErrmsg.length() < errmsg.length()) {
|
||||||
|
Logger.logWarn(logTag, "Execution Result for Execution Command \"" + label + "\" errmsg length truncated from " + errmsgOriginalLength + " to " + truncatedErrmsg.length());
|
||||||
|
errmsg = truncatedErrmsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
final Bundle resultBundle = new Bundle();
|
||||||
|
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT, stdout);
|
||||||
|
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH, stdoutOriginalLength);
|
||||||
|
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR, stderr);
|
||||||
|
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH, stderrOriginalLength);
|
||||||
|
if (exitCode != null) resultBundle.putInt(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE, exitCode);
|
||||||
|
if (errCode != null) resultBundle.putInt(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERR, errCode);
|
||||||
|
resultBundle.putString(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG, errmsg);
|
||||||
|
|
||||||
|
Intent resultIntent = new Intent();
|
||||||
|
resultIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_RESULT_BUNDLE, resultBundle);
|
||||||
|
|
||||||
|
try {
|
||||||
|
pluginPendingIntent.send(context, Activity.RESULT_OK, resultIntent);
|
||||||
|
} catch (PendingIntent.CanceledException e) {
|
||||||
|
// The caller doesn't want the result? That's fine, just ignore
|
||||||
|
Logger.logDebug(logTag, "The Execution Command \"" + label + "\" creator " + pluginPendingIntent.getCreatorPackage() + " does not want the results anymore");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get {@link Notification.Builder} for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
|
||||||
|
* and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} for operations.
|
||||||
|
* @param title The title for the notification.
|
||||||
|
* @param notificationText The second line text of the notification.
|
||||||
|
* @param notificationBigText The full text of the notification that may optionally be styled.
|
||||||
|
* @param pendingIntent The {@link PendingIntent} which should be sent when notification is clicked.
|
||||||
|
* @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}.
|
||||||
|
* @return Returns the {@link Notification.Builder}.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static Notification.Builder getPluginCommandErrorsNotificationBuilder(final Context context, final CharSequence title, final CharSequence notificationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) {
|
||||||
|
|
||||||
|
Notification.Builder builder = NotificationUtils.geNotificationBuilder(context,
|
||||||
|
TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_HIGH,
|
||||||
|
title, notificationText, notificationBigText, pendingIntent, notificationMode);
|
||||||
|
|
||||||
|
if (builder == null) return null;
|
||||||
|
|
||||||
|
// Enable timestamp
|
||||||
|
builder.setShowWhen(true);
|
||||||
|
|
||||||
|
// Set notification icon
|
||||||
|
builder.setSmallIcon(R.drawable.ic_error_notification);
|
||||||
|
|
||||||
|
// Set background color for small notification icon
|
||||||
|
builder.setColor(0xFF607D8B);
|
||||||
|
|
||||||
|
// Dismiss on click
|
||||||
|
builder.setAutoCancel(true);
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup the notification channel for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID} and
|
||||||
|
* {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} for operations.
|
||||||
|
*/
|
||||||
|
public static void setupPluginCommandErrorsNotificationChannel(final Context context) {
|
||||||
|
NotificationUtils.setupNotificationChannel(context, TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID,
|
||||||
|
TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if {@link TermuxConstants#PROP_ALLOW_EXTERNAL_APPS} property is not set to "true".
|
||||||
|
*
|
||||||
|
* @param context The {@link Context} to get error string.
|
||||||
|
* @return Returns the {@code errmsg} if policy is violated, otherwise {@code null}.
|
||||||
|
*/
|
||||||
|
public static String checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(final Context context) {
|
||||||
|
String errmsg = null;
|
||||||
|
if (!SharedProperties.isPropertyValueTrue(context, TermuxPropertyConstants.getTermuxPropertiesFile(), TermuxConstants.PROP_ALLOW_EXTERNAL_APPS)) {
|
||||||
|
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,19 +63,19 @@ 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));
|
||||||
row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(BASE_DIR));
|
row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(BASE_DIR));
|
||||||
row.add(Root.COLUMN_SUMMARY, null);
|
row.add(Root.COLUMN_SUMMARY, null);
|
||||||
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH);
|
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD);
|
||||||
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;
|
||||||
}
|
}
|
||||||
@@ -117,6 +115,29 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException {
|
||||||
|
File newFile = new File(parentDocumentId, displayName);
|
||||||
|
int noConflictId = 2;
|
||||||
|
while (newFile.exists()) {
|
||||||
|
newFile = new File(parentDocumentId, displayName + " (" + noConflictId++ + ")");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
boolean succeeded;
|
||||||
|
if (Document.MIME_TYPE_DIR.equals(mimeType)) {
|
||||||
|
succeeded = newFile.mkdir();
|
||||||
|
} else {
|
||||||
|
succeeded = newFile.createNewFile();
|
||||||
|
}
|
||||||
|
if (!succeeded) {
|
||||||
|
throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
|
||||||
|
}
|
||||||
|
return newFile.getPath();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deleteDocument(String documentId) throws FileNotFoundException {
|
public void deleteDocument(String documentId) throws FileNotFoundException {
|
||||||
File file = getFileForDocId(documentId);
|
File file = getFileForDocId(documentId);
|
||||||
@@ -146,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 {
|
||||||
@@ -169,6 +189,11 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isChildDocument(String parentDocumentId, String documentId) {
|
||||||
|
return documentId.startsWith(parentDocumentId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the document id given a file. This document id must be consistent across time as other
|
* Get the document id given a file. This document id must be consistent across time as other
|
||||||
* applications may save the ID and use it to reference documents later.
|
* applications may save the ID and use it to reference documents later.
|
||||||
@@ -220,10 +245,11 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
|
|||||||
|
|
||||||
int flags = 0;
|
int flags = 0;
|
||||||
if (file.isDirectory()) {
|
if (file.isDirectory()) {
|
||||||
if (file.isDirectory() && file.canWrite()) flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
|
if (file.canWrite()) flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
|
||||||
} else if (file.canWrite()) {
|
} else if (file.canWrite()) {
|
||||||
flags |= Document.FLAG_SUPPORTS_WRITE | Document.FLAG_SUPPORTS_DELETE;
|
flags |= Document.FLAG_SUPPORTS_WRITE;
|
||||||
}
|
}
|
||||||
|
if (file.getParentFile().canWrite()) flags |= Document.FLAG_SUPPORTS_DELETE;
|
||||||
|
|
||||||
final String displayName = file.getName();
|
final String displayName = file.getName();
|
||||||
final String mimeType = getMimeType(file);
|
final String mimeType = getMimeType(file);
|
||||||
@@ -236,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.DialogUtils;
|
||||||
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE;
|
||||||
import com.termux.app.TermuxService;
|
import com.termux.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 -> {
|
DialogUtils.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();
|
||||||
}
|
}
|
||||||
|
|||||||
5
app/src/main/res/drawable/ic_copy.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/>
|
||||||
|
</vector>
|
||||||
37
app/src/main/res/drawable/ic_error_notification.xml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<!--
|
||||||
|
Updated notification icon compliant with system icons guidelines
|
||||||
|
https://material.io/design/iconography/system-icons.html
|
||||||
|
-->
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M0,0h24v24h-24z"/>
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:pathData="M5,4H2L8,12L2,20H5L11,12L5,4Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:pathData="M19.59,14
|
||||||
|
l-2.09,2.09
|
||||||
|
L15.41,14
|
||||||
|
L14,15.41
|
||||||
|
l2.09,2.09
|
||||||
|
L14,19.59
|
||||||
|
L15.41,21
|
||||||
|
l2.09,-2.08
|
||||||
|
L19.59,21
|
||||||
|
L21,19.59
|
||||||
|
l-2.08,-2.09
|
||||||
|
L21,15.41
|
||||||
|
L19.59,14
|
||||||
|
z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -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>
|
|
||||||
@@ -1,33 +1,24 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:height="48dp"
|
android:width="24dp"
|
||||||
android:width="48dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="48"
|
android:viewportWidth="24"
|
||||||
android:viewportHeight="48">
|
android:viewportHeight="24">
|
||||||
<!--
|
<!--
|
||||||
https://material.google.com/style/icons.html
|
Updated notification icon compliant with system icons guidelines
|
||||||
|
https://material.io/design/iconography/system-icons.html
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<!-- Screen border. -->
|
<group>
|
||||||
<path android:fillColor="#00000000"
|
<clip-path
|
||||||
android:strokeColor="#FFF"
|
android:pathData="M0,0h24v24h-24z"/>
|
||||||
android:strokeWidth="3"
|
|
||||||
android:pathData="M7,4
|
|
||||||
l34,0
|
|
||||||
q3 0,3 3
|
|
||||||
l0,34
|
|
||||||
q0 3, -3 3
|
|
||||||
l-34,0
|
|
||||||
q-3 0, -3-3
|
|
||||||
l0 -34
|
|
||||||
q0 -3, 3 -3"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Block cursor. -->
|
<path
|
||||||
<path android:fillColor="#FFF"
|
android:pathData="M5,4H2L8,12L2,20H5L11,12L5,4Z"
|
||||||
android:pathData="M14,14
|
android:fillColor="#ffffff"/>
|
||||||
l5,0
|
|
||||||
l0,10
|
|
||||||
l-5,0"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:pathData="M13,18H22V20H13V18Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
|
||||||
|
</group>
|
||||||
</vector>
|
</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>
|
||||||
5
app/src/main/res/drawable/ic_share.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
|
||||||
|
</vector>
|
||||||
21
app/src/main/res/layout/activity_report.xml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<include
|
||||||
|
layout="@layout/partial_toolbar"
|
||||||
|
android:id="@+id/partial_toolbar"/>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:overScrollMode="never"
|
||||||
|
android:paddingTop="@dimen/content_padding"
|
||||||
|
android:paddingBottom="36dip" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:fillViewport="true"
|
||||||
|
android:paddingLeft="16dip"
|
||||||
|
android:paddingRight="16dip"
|
||||||
|
android:scrollbarStyle="outsideInset">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/code_text_view"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@color/background_markdown_code_block"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:lineSpacingExtra="2dip"
|
||||||
|
android:paddingLeft="16dip"
|
||||||
|
android:paddingTop="8dip"
|
||||||
|
android:paddingRight="16dip"
|
||||||
|
android:paddingBottom="8dip"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
</HorizontalScrollView>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/default_text_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="16dip"
|
||||||
|
android:layout_marginRight="16dip"
|
||||||
|
android:breakStrategy="simple"
|
||||||
|
android:hyphenationFrequency="none"
|
||||||
|
android:lineSpacingExtra="2dip"
|
||||||
|
android:paddingTop="8dip"
|
||||||
|
android:paddingBottom="8dip"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
|
android:textColor="#000"
|
||||||
|
android:textSize="12sp" />
|
||||||
9
app/src/main/res/layout/activity_settings.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/settings"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
</LinearLayout>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
android:id="@+id/drawer_layout"
|
android:id="@+id/drawer_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_alignParentTop="true"
|
android:layout_alignParentTop="true"
|
||||||
android:layout_above="@+id/viewpager"
|
android:layout_above="@+id/terminal_toolbar_view_pager"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<com.termux.view.TerminalView
|
<com.termux.view.TerminalView
|
||||||
@@ -19,7 +19,9 @@
|
|||||||
android:layout_marginLeft="3dp"
|
android:layout_marginLeft="3dp"
|
||||||
android:focusableInTouchMode="true"
|
android:focusableInTouchMode="true"
|
||||||
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
|
android:scrollbarThumbVertical="@drawable/terminal_scroll_shape"
|
||||||
android:scrollbars="vertical" />
|
android:scrollbars="vertical"
|
||||||
|
android:importantForAutofill="no"
|
||||||
|
android:autofillHints="password" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/left_drawer"
|
android:id="@+id/left_drawer"
|
||||||
@@ -34,7 +36,7 @@
|
|||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<ListView
|
<ListView
|
||||||
android:id="@+id/left_drawer_list"
|
android:id="@+id/terminal_sessions_list"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_gravity="top"
|
android:layout_gravity="top"
|
||||||
@@ -54,7 +56,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="@string/toggle_soft_keyboard" />
|
android:text="@string/action_toggle_soft_keyboard" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/new_session_button"
|
android:id="@+id/new_session_button"
|
||||||
@@ -62,14 +64,14 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="@string/new_session" />
|
android:text="@string/action_new_session" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</androidx.drawerlayout.widget.DrawerLayout>
|
</androidx.drawerlayout.widget.DrawerLayout>
|
||||||
|
|
||||||
<androidx.viewpager.widget.ViewPager
|
<androidx.viewpager.widget.ViewPager
|
||||||
android:id="@+id/viewpager"
|
android:id="@+id/terminal_toolbar_view_pager"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="37.5dp"
|
android:layout_height="37.5dp"
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
<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"
|
||||||
android:textSize="14sp" />
|
android:textSize="14sp" />
|
||||||
22
app/src/main/res/layout/partial_toolbar.xml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/toolbar_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:background="?attr/colorPrimaryDark"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:minHeight="?attr/actionBarSize"
|
||||||
|
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
|
||||||
|
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
|
||||||
|
app:titleTextAppearance="@style/Toolbar.Title">
|
||||||
|
|
||||||
|
</androidx.appcompat.widget.Toolbar>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -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"
|
||||||
15
app/src/main/res/menu/menu_report.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_item_share_report"
|
||||||
|
android:icon="@drawable/ic_share"
|
||||||
|
android:title="@string/action_share"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_item_copy_report"
|
||||||
|
android:icon="@drawable/ic_copy"
|
||||||
|
android:title="@string/action_copy"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
</menu>
|
||||||
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>
|
||||||
10
app/src/main/res/values/dimens.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Default screen margins, per the Android Design guidelines. -->
|
||||||
|
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||||
|
<dimen name="activity_vertical_margin">16dp</dimen>
|
||||||
|
|
||||||
|
<dimen name="content_padding">8dip</dimen>
|
||||||
|
<dimen name="content_padding_double">16dip</dimen>
|
||||||
|
<dimen name="content_padding_half">4dip</dimen>
|
||||||
|
</resources>
|
||||||
@@ -1,51 +1,158 @@
|
|||||||
<?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="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="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.\n\nCheck your network connection and try again.</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="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>
|
<!-- Termux RunCommandService -->
|
||||||
|
<string name="error_run_command_service_invalid_intent_action">Invalid intent action to RunCommandService: `%1$s`</string>
|
||||||
|
<string name="error_run_command_service_mandatory_extra_missing">Mandatory extra missing to RunCommandService: \"%1$s\"</string>
|
||||||
|
<string name="error_run_command_service_allow_external_apps_ungranted">RunCommandService require `allow-external-apps` property to be set to `true` in `&TERMUX_PROPERTIES_PRIMARY_PATH_SHORT;` file.</string>
|
||||||
|
<string name="error_run_command_service_api_help">Visit %1$s for more info on RUN_COMMAND Intent usage.</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Termux Execution Commands -->
|
||||||
|
<string name="msg_executable_absolute_path">Executable Absolute Path: \"%1$s\"</string>
|
||||||
|
<string name="msg_working_directory_absolute_path">Working Directory Absolute Path: \"%1$s\"</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Termux Report And ShareUtils -->
|
||||||
|
<string name="action_copy">Copy</string>
|
||||||
|
<string name="action_share">Share</string>
|
||||||
|
|
||||||
|
<string name="title_share_with">Share With</string>
|
||||||
|
<string name="title_report_text">Report Text</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Termux File Receiver -->
|
||||||
|
<string name="title_file_received">Save file in ~/downloads/</string>
|
||||||
|
<string name="action_file_received_edit">Edit</string>
|
||||||
|
<string name="action_file_received_open_directory">Open directory</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Termux Settings -->
|
||||||
|
<string name="title_activity_termux_settings">&TERMUX_APP_NAME; Settings</string>
|
||||||
|
|
||||||
|
<!-- Debugging Preferences -->
|
||||||
|
<string name="debugging_preferences">Debugging</string>
|
||||||
|
|
||||||
|
<!-- Logging Category -->
|
||||||
|
<string name="logging_header">Logging</string>
|
||||||
|
|
||||||
|
<!-- Terminal View Key Logging -->
|
||||||
|
<string name="terminal_view_key_logging_title">Terminal View Key Logging</string>
|
||||||
|
<string name="terminal_view_key_logging_off">Logs will not have entries for terminal view keys. (Default)</string>
|
||||||
|
<string name="terminal_view_key_logging_on">Logcat logs will have entries for terminal view keys. These are very verbose and should be disabled under normal circumstances or will cause performance issues.</string>
|
||||||
|
|
||||||
|
<!-- Plugin Error Notifications -->
|
||||||
|
<string name="plugin_error_notifications_title">Plugin Error Notifications</string>
|
||||||
|
<string name="plugin_error_notifications_off">Disable flashes and notifications for plugin errors.</string>
|
||||||
|
<string name="plugin_error_notifications_on">Show flashes and notifications for plugin errors. (Default)</string>
|
||||||
|
|
||||||
|
<!-- Crash Report Notifications -->
|
||||||
|
<string name="crash_report_notifications_title">Crash Report Notifications</string>
|
||||||
|
<string name="crash_report_notifications_off">Disable notifications for crash reports.</string>
|
||||||
|
<string name="crash_report_notifications_on">Show notifications for crash reports. (Default)</string>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Terminal IO Preferences -->
|
||||||
|
<string name="terminal_io_preferences">Terminal I/O</string>
|
||||||
|
|
||||||
|
<!-- Keyboard Category -->
|
||||||
|
<string name="keyboard_header">Keyboard</string>
|
||||||
|
|
||||||
|
<!-- Soft Keyboard -->
|
||||||
|
<string name="soft_keyboard_title">Soft Keyboard</string>
|
||||||
|
<string name="soft_keyboard_off">Soft keyboard will be disabled.</string>
|
||||||
|
<string name="soft_keyboard_on">Soft keyboard will be enabled. (Default)</string>
|
||||||
|
|
||||||
</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,20 @@
|
|||||||
<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="Theme.AppCompat.TermuxReportActivity" parent="Theme.AppCompat.Light.NoActionBar">
|
||||||
|
<item name="colorPrimaryDark">#FF0000</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Toolbar.Title" parent="TextAppearance.Widget.AppCompat.Toolbar.Title">
|
||||||
|
<item name="android:textSize">14sp</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<style name="TermuxAlertDialogStyle" parent="@android:style/Theme.Material.Light.Dialog.Alert">
|
||||||
|
<!-- Seen in buttons on alert dialog: -->
|
||||||
|
<item name="android:colorAccent">#212121</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
33
app/src/main/res/xml/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/logging_header">
|
||||||
|
|
||||||
|
<ListPreference
|
||||||
|
app:defaultValue="1"
|
||||||
|
app:key="log_level"
|
||||||
|
app:title="@string/log_level_title"
|
||||||
|
app:useSimpleSummaryProvider="true" />
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:key="terminal_view_key_logging_enabled"
|
||||||
|
app:summaryOff="@string/terminal_view_key_logging_off"
|
||||||
|
app:summaryOn="@string/terminal_view_key_logging_on"
|
||||||
|
app:title="@string/terminal_view_key_logging_title" />
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:key="plugin_error_notifications_enabled"
|
||||||
|
app:summaryOff="@string/plugin_error_notifications_off"
|
||||||
|
app:summaryOn="@string/plugin_error_notifications_on"
|
||||||
|
app:title="@string/plugin_error_notifications_title" />
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:key="crash_report_notifications_enabled"
|
||||||
|
app:summaryOff="@string/crash_report_notifications_off"
|
||||||
|
app:summaryOn="@string/crash_report_notifications_on"
|
||||||
|
app:title="@string/crash_report_notifications_title" />
|
||||||
|
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
</PreferenceScreen>
|
||||||
13
app/src/main/res/xml/root_preferences.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
app:title="@string/debugging_preferences"
|
||||||
|
app:summary="Preferences for debugging"
|
||||||
|
app:fragment="com.termux.app.fragments.settings.DebuggingPreferencesFragment"/>
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
app:title="@string/terminal_io_preferences"
|
||||||
|
app:summary="Preferences for terminal I/O"
|
||||||
|
app:fragment="com.termux.app.fragments.settings.TerminalIOPreferencesFragment"/>
|
||||||
|
|
||||||
|
</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>
|
||||||
|
|||||||
15
app/src/main/res/xml/terminal_io_preferences.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<PreferenceCategory
|
||||||
|
app:key="keyboard"
|
||||||
|
app:title="@string/keyboard_header">
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:key="soft_keyboard_enabled"
|
||||||
|
app:summaryOff="@string/soft_keyboard_off"
|
||||||
|
app:summaryOn="@string/soft_keyboard_on"
|
||||||
|
app:title="@string/soft_keyboard_title" />
|
||||||
|
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
</PreferenceScreen>
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.termux.app;
|
package com.termux.app;
|
||||||
|
|
||||||
|
import com.termux.shared.data.DataUtils;
|
||||||
|
|
||||||
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, DataUtils.extractUrls(text));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -22,6 +24,9 @@ public class TermuxActivityTest {
|
|||||||
|
|
||||||
assertUrlsAre("hello http://example.com world and http://more.example.com with secure https://more.example.com",
|
assertUrlsAre("hello http://example.com world and http://more.example.com with secure https://more.example.com",
|
||||||
"http://example.com", "http://more.example.com", "https://more.example.com");
|
"http://example.com", "http://more.example.com", "https://more.example.com");
|
||||||
|
|
||||||
|
assertUrlsAre("hello https://example.com/#bar https://example.com/foo#bar",
|
||||||
|
"https://example.com/#bar", "https://example.com/foo#bar");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
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 |
@@ -4,7 +4,7 @@ buildscript {
|
|||||||
google()
|
google()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.5.3'
|
classpath 'com.android.tools.build:gradle:4.1.3'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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
|
||||||
@@ -13,3 +13,15 @@
|
|||||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
# org.gradle.parallel=true
|
# org.gradle.parallel=true
|
||||||
org.gradle.jvmargs=-Xmx2048M
|
org.gradle.jvmargs=-Xmx2048M
|
||||||
|
android.useAndroidX=true
|
||||||
|
|
||||||
|
termuxVersion=0.111
|
||||||
|
termuxVersionCode=111
|
||||||
|
|
||||||
|
minSdkVersion=24
|
||||||
|
targetSdkVersion=28
|
||||||
|
ndkVersion=22.0.7026061
|
||||||
|
compileSdkVersion=29
|
||||||
|
|
||||||
|
markwonVersion=4.6.2
|
||||||
|
|
||||||
|
|||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
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.0.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
2
gradlew
vendored
@@ -82,6 +82,7 @@ esac
|
|||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
# Determine the Java command to use to start the JVM.
|
||||||
if [ -n "$JAVA_HOME" ] ; then
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
@@ -129,6 +130,7 @@ fi
|
|||||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
|
||||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
# We build the pattern for arguments to be converted via cygpath
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
|||||||
25
gradlew.bat
vendored
@@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=.
|
|||||||
set APP_BASE_NAME=%~n0
|
set APP_BASE_NAME=%~n0
|
||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
@@ -37,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
|||||||
|
|
||||||
set JAVA_EXE=java.exe
|
set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if "%ERRORLEVEL%" == "0" goto init
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
@@ -51,7 +54,7 @@ goto fail
|
|||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto init
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
@@ -61,28 +64,14 @@ echo location of your Java installation.
|
|||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
:init
|
|
||||||
@rem Get command-line arguments, handling Windows variants
|
|
||||||
|
|
||||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
|
||||||
|
|
||||||
:win9xME_args
|
|
||||||
@rem Slurp the command line arguments.
|
|
||||||
set CMD_LINE_ARGS=
|
|
||||||
set _SKIP=2
|
|
||||||
|
|
||||||
:win9xME_args_slurp
|
|
||||||
if "x%~1" == "x" goto execute
|
|
||||||
|
|
||||||
set CMD_LINE_ARGS=%*
|
|
||||||
|
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
|||||||